Copyright © 2003 Yen-Ju Chen, Dennis Leeuw
Permission is granted to copy, distribute and/or modify this document under the terms of the GNU Free Documentation License, Version 1.2 or any later version published by the Free Software Foundation; with no Invariant Sections, no Front-Cover Texts, and no Back-Cover Texts.
And now you can forget everything you know about Objective-C programming, because there is GNUstep. While not entirely true, this section will explain why I wrote this sentence and what GNUstep is and how it relates to Objective-C.
GNUstep tries to be a platform independend programming environment. That means it tries to give you an abstraction layer on which you can program your applications, so that they are portable to every platform that adheres to the OpenStep standard. Currently that means that with the least possible effort your applications can be compiled for GNU/Linux (and most other POSIX compliant systems), MacOS X (using Cocoa), and to a certain extend to Windows with the use of GNUstep and Cygwin.
This means: write once, compile anywhere.
With this abstraction comes also the runtime abstraction. That means that your root of all objects is not the Objective-C runtime Object, but NSObject. All objects eventually have NSObject as their root object.
The fact that you can't completely forget Objective-C is that fact that GNUstep is programmed in Objective-C. And all you need to program your program is GNUstep and Objective-C. It's the next logical step in evolution. First there was C, then there was Objective-C and now there is GNUstep.
GNUstep offers ProjectCenter and Gorm to help you with the development of GNUstep applications. Both are similiar to Project Builder and Interface Builder on MacOS X. ProjectCenter is for project management, and Gorm for interface building. We will come back to them later.
ProjectCenter and Gorm can be found on ftp://ftp.gnustep.org/pub/gnustep/dev-apps/.
There are many GNUstep related resources:
GNUstep.org - The Official site.
GNUstep.it - Another GNUstep site with documents and examples.
GNUstep wiki - GNUstep Wiki.
GNUstep Help Center - A GNUstep document collection.
Cocoa - The Cocoa environment of MacOS X.
Stepwise.com - OpenStep/Cocoa programming site.
Cocoa Dev Central - Cocoa programming site.
Objective-C is called a superset of the C language. It is C plus object-oriented design. It is suggested to use GCC 3.x series in order to get better Objective-C support.
The syntax of Objective-C can be found at Apples site. There is also a PDF file to download. It is very easy to learn Objective-C if you have some idea about C.
There are some more documents about Objective-C:
The basis of GNUstep is formed by the GNUstep makefile system and GNUstep base. With only these two you are able to program platform independend non-graphical programs.
The makefile system is an extension to the normal Makefile. You need GNU make, which might on your system be present as gmake. The makefiles are called GNUmakefile. The GNUstep makefile system simplyfies your makefiles, all makefile logic will be done for you so that makefile functionality will be consistent across all GNUstep programs.
GNUstep base contains what is defined in the OpenStep standard as the Foundation. It has the logic for creating strings, arrays, file handling and the like. All objects are prefixed with NS or GS. The NS prefixed objects are the ones that comply to the OpenStep standard as closely as possible, while the GS ones are GNUstep extensions.
We start with the traditional "Hello World" program, which is mainly a demonstration of gnustep-make.
main.c:
#include <stdio.h>
int main(void)
{
printf("Hello World\n");
} |
This is a very simple C program. In order to compile it, we create a GNUmakefile file, which is a simplified, but powerful version of a Makefile.
GNUmakefile:
include $(GNUSTEP_MAKEFILES)/common.make CTOOL_NAME = HelloWorld HelloWorld_HEADERS = HelloWorld_C_FILES = main.c HelloWorld_RESOURCE_FILES = include $(GNUSTEP_MAKEFILES)/ctool.make |
Type make and do ./shared_obj/HelloWorld. It will print "Hello World", as you might have expected.
This example shows you a couple of powerful things about the GNUstep makefile system. As you can see there are no targets. You can type make and it works. You might also have noticed that you told, through the CTOOL_NAME variable, the system that your application is called HelloWorld.
Let's do the same for an Objective-C version of Hello World.
main.m:
#include <objc/Object.h>
@interface Greeter:Object
{
/* This is left empty on purpose:
** Normally instance variables would be declared here,
** but these are not used in our example.
*/
}
- (void)greet;
@end
#include <stdio.h>
@implementation Greeter
- (void)greet
{
printf("Hello, World!\n");
}
@end
#include <stdlib.h>
int main(void)
{
id myGreeter;
myGreeter=[Greeter new];
[myGreeter greet];
[myGreeter free];
return EXIT_SUCCESS;
} |
GNUmakefile:
include $(GNUSTEP_MAKEFILES)/common.make OBJC_PROGRAM_NAME = HelloWorld HelloWorld_HEADERS = HelloWorld_OBJC_FILES = main.m HelloWorld_RESOURCE_FILES = include $(GNUSTEP_MAKEFILES)/objc.make |
The basic logic for Objective-C files is included through objc.make and off you go for plain Objective-C programs.
GNUstep knows two kinds of programs: Tools and Applications. Tools are mostly non-graphical programs, but more exactly Tools are programs with no-resources, while Applications are programs with resources.
main.m:
#include <Foundation/Foundation.h>
@interface Greeter:NSObject
{
/* This is left empty on purpose:
** Normally instance variables would be declared here,
** but these are not used in our example.
*/
}
- (void)greet;
@end
#include <stdio.h>
@implementation Greeter
- (void)greet
{
printf("Hello, World!\n");
}
@end
#include <stdlib.h>
int main(void)
{
id myGreeter;
myGreeter=[[Greeter alloc] init];
[myGreeter greet];
[myGreeter release];
return EXIT_SUCCESS;
} |
GNUmakefile:
include $(GNUSTEP_MAKEFILES)/common.make TOOL_NAME = HelloWorld HelloWorld_HEADERS = HelloWorld_OBJC_FILES = main.m HelloWorld_RESOURCE_FILES = include $(GNUSTEP_MAKEFILES)/tool.make |
To show you the difference more clearly type make clean and change the line
include $(GNUSTEP_MAKEFILES)/tool.make |
include $(GNUSTEP_MAKEFILES)/application.make |
This HelloWorld.app directory is a self contained GNUstep application. Type openapp HelloWorld.app and you see that it works. Now out of curiosity list the contents of the HelloWorld.app directory and you have your first glimps at the difference between a Tool and an Application.
Apart from being able to select the kind of program you want to create you get a couple of things for free. The first thing you hopefully already noticed is make clean, but there is more. make install will install your program in it's default place, which for Tools is GNUSTEP_LOCAL_ROOT/Tools and as you might have quessed GNUSTEP_LOCAL_ROOT/Applications for applications.
Ofcourse that path can be overridden by using e.g. make PREFIX=/my/path install, but remember that Tools and Applications are only searched within the GNUstep tree. It is a self contained system with three so called domains: GNUSTEP_SYSTEM_ROOT, GNUSTEP_LOCAL_ROOT and GNUSTEP_NETWORK_ROOT. Tools like openapp will only search through these domains and the current directory to find Applications and Tools.
A couple of the most used make options are:
Basic options
all. The same as make without options
check. Run tests if applicable
clean. Remove all that is build during make
distclean. Remove all that is build during make and ... ???
install. Install all that is build, and if nothing is build, do the build first.
uninstall. Uninstall a previously install program.
cvs-dist, cvs-snapshot, strings. ??? What do these???
Boolean options
debug=[yes|no]. yes builds a program with debugging symbols. With debugging symbols the program can be started with debugapp.
filelist=[yes|no]. Creates a filelist of the installed files (easy for packaging binaries).
messages=[yes|no]. More verbose messaging when building sources.
shared=[yes|no]. ???
standalone=[yes|no]. ???
strip=[yes|no]. Strip the created programs and libraries.
There are some GNUmakefile tutorials. Therefore, I won't touch this topic too much. Here is another example, which is a real Objective-C program. If you are not familiar with Objective-C, read the Objective-C book from apple first.
Since GNUstep is build with Objective-C, which in turn is based on C, headers are important. Within the GNUstep domains they can be found in the Library/Headers directory where all have their own subdirectory; all foundation headers can be found in Library/Headers/Foundation.
To include header files in your code you can use the C syntax:
#include <Foundation/NSArray.h> |
#ifndef HAVE_NSARRAY_H #define HAVE_NSARRAY_H #include <Foundation/NSArray.h> #endif |
Easier would be to use the Objective-C macro #import. import takes care of all the things that concern headers:
#import <Foundation/NSArray.h> |
And since GNUstep is here to make it easier on you, you might want to use:
#import <Foundation/Foundation.h> |
![]() | Foundation tries to implement as close as possible the OpenStep standard. Which means that the Foundation/ directory holds the headers that correspond to that standard. The extensions of GNUstep can be found in GNUstepBase/. The same goes for AppKit/ and GNUstepGUI/. |
There are four files:
say.h:
#ifndef _Say_H_
#define _Say_H_
#include <Foundation/NSObject.h>
@interface Say: NSObject
{
}
- (void) sayHello;
- (void) sayHelloTo: (NSString *)name;
@end
#endif /* _Say_H_ */ |
say.m:
#include "say.h"
#include <Foundation/Foundation.h>
@implementation Say
- (void) sayHello
{
NSLog(@"Hello World");
}
- (void) sayHelloTo: (NSString *)name
{
NSLog(@"Hello World, %@", name);
}
@end |
main.m:
#include "say.h"
#include <Foundation/Foundation.h>
int main (void)
{
id speaker;
NSString *name = @"GNUstep !";
NSAutoreleasePool *pool;
pool = [NSAutoreleasePool new];
speaker = [[Say alloc] init];
[speaker sayHello];
[speaker sayHelloTo:name];
RELEASE(speaker);
RELEASE(pool);
} |
GNUmakefile:
include $(GNUSTEP_MAKEFILES)/common.make APP_NAME = HelloWorld HelloWorld_HEADERS = say.h HelloWorld_OBJC_FILES = main.m say.m HelloWorld_RESOURCE_FILES = include $(GNUSTEP_MAKEFILES)/application.make |
Again, use 'make' and 'openapp' to compile and run this application. This example is self-explained. There are several things worthy to metion.
The class "Say" inherits from "NSObject", which is the root class of GNUstep. Always inherit from NSObject if you don't know which class to use. NSObject contains many fundamental methods which you won't want to implement by yourself.
NSLog() is used instead of printf() because it is as easy as printf(). And most importantly, NSLog() accept the %@ symbol, which represents an object. That means you can print objects in NSLog(). Very handy for debug.
NSString is one of the most used classes in GNUstep. You can use @"..." to create a NSString. There are many useful methods in NSString to manipulate strings and pathes. Check these articles: String in Cocoa: Part I, Part II
NSAutoreleasePool is the place for autoreleased instances. In this example, it is not much useful, but I think it is a good habit to use it. When the application should end, remember to release it. Methods -alloc and -init is the standard method to create an instance. Method -new is a shortcut if there is no messages for -init.
Arrays are defined by NSArray. There is static version and a dynamic version. The dynamic version is called NSMutableArray. To define them in your code goes the same for both:
NSArray *anArray; NSMutableArray *anMutableArray; |
Now let us create a simple array:
NSArray *weekArray;
NSAutoreleasePool *pool;
pool = [NSAutoreleasePool new];
weekArray = [NSArray arrayWithObjects: @"Sun", @"Mon" @"Tue", @"Wed", \
@"Thr", @"Fri", @"Sat", nil];
printf("Day 0: %@\n", [weekArray objectAtIndex: 0]);
RELEASE(pool); |
To output the elements from the array we ask from weekArray to output the object at a certain index. Note that arrays start counting at 0!
Debugging your application often involves watching what happens while the program is running. You could use a debugger for that, like gdb, but there are some other options that make life easy.
Since you can mix C and Objective-C you could use normal printf statements, or it's GNUstep counterpart GSPrintf. GNUstep however also has a function NSLog and the Makefile system offers you to use make debug=yes and use ifdefs in your code.
To make this all a bit more clear we use and example.
#include <Foundation/Foundation.h>
int main(void)
{
NSArray *outArray;
NSAutoreleasePool *pool;
pool = [NSAutoreleasePool new];
outArray = [NSArray arrayWithObjects: @"Msg1", @"Msg2" @"Msg3" "Msg4";
printf("%@\n", [outArray objectAtIndex: 0]);
GSPrintf(stdout, "%@",[outArray objectAtIndex: 1]);
NSLog(@"%@\n", [outArray objectAtIndex: 2]);
#ifdef DEBUG
NSLog(@"%@\n", [outArray objectAtIndex: 3]);
#endif
RELEASE(pool);
} |
printf prints it's output naar stdout, while NSLog outputs to stderr and with GSPrintf you have full control. You can print to stdout or stderr.Now run make clean; make debug=yes and rerun the command from the obj directory. Your output should look like this:
Msg1 Msg2 2003-12-30 17:40:53.846 array[10769] Msg3 2003-12-30 17:40:53.848 array[10769] Msg4 |
With these you can simply debug the most common problems, apart from code that won't compile :)
Memory management is very important, but easy, in GNUstep. Here are some good articles:
GNUstep offers some good macros to ease the coding related to memory management. Take a look at NSObject.h for ASSIGN(), RELEASE(), RETAIN(), etc.
The most common way to handle release/retain is this:
@interface MyObject: NSObject
{
id myData;
}
-(void) setMyData: (id) newData;
-(id) myData;
@end
@implementation MyObject
- (void) setMyData: (id) newData
{
ASSIGN(myData, newData);
}
- (id) myData
{
return myData;
}
- (void) dealloc
{
RELEASE(myData);
}
@end |
Basically it works for me. ASSIGNCOPY() can also be used to copy object. Always use [self setMyData: newData] to set myData, or at least use ASSIGN(myData, newData). Don't use myData = newData. By this way, you don't need to worry about the memory management.
In some case, I will use mutable classes because they are thread-safe. For example:
@interface MyObject: NSObject
{
NSMutableArray *myArray;
}
-(void) setMyArray: (NSArray *) newArray;
-(NSArray *) myArray;
@end
@implementation MyObject
- (id) init
{
myArray = [NSMutableArray new];
}
- (void) setMyArray: (NSArray *) newArray
{
[myArray setArray: newArray];
}
- (NSArray *) myArray
{
return myArray;
}
- (void) dealloc
{
RELEASE(myArray);
}
@end |
Mutable classes cost more resources. But it is a lazy way to avoid problems.
Also be aware of the return values from GNUstep classes. Some are autoreleased. For example, [NSArray arrayWith...], or [@"A string" stringBy...]. If you release them again, the application usually crashes. And remember to retain them if you want to use them later, and release them in -dealloc. It's safe to use ASSIGN() in this case. For example:
ASSIGN(myString, [@"A string", stringBy...]) |
ASSIGN() will handle all the details. Again, once you use ASSIGN(), release it in -dealloc.
Here is a demonstration of file I/O. GNUstep offer NSFileHandle and NSFileManager to handle files. NSFileHandle is mainly to read and write, and NSFileManager is for file management.
main.m:
#include <Foundation/Foundation.h>
int main (void)
{
NSString *path;
NSAutoreleasePool *pool;
NSFileHandle *readFile, *writeFile;
NSData *fileData;
pool = [NSAutoreleasePool new];
path = @"main.m";
readFile = [NSFileHandle fileHandleForReadingAtPath:path];
fileData = [readFile readDataToEndOfFile];
writeFile = [NSFileHandle fileHandleWithStandardOutput];
[writeFile writeData:fileData];
RELEASE(pool);
return 0;
} |
GNUmakefile:
include $(GNUSTEP_MAKEFILES)/common.make APP_NAME = FileHandle FileHandle_HEADERS = FileHandle_OBJC_FILES = main.m FileHandle_RESOURCE_FILES = include $(GNUSTEP_MAKEFILES)/application.make |
It is not too much useful to only trasfer data in NSData. Here, I can take the data out of NSData and manipulate it in C. By this way, I can combine GNUstep with C libraries.
main.m:
#import <Foundation/Foundation.h>
int main (void)
{
NSString *path;
NSAutoreleasePool *pool;
NSFileHandle *readFile;
NSData* fileData;
char *buffer;
unsigned int length;
pool = [NSAutoreleasePool new];
// path = @"main.m";
path = @"GNUmakefile";
readFile = [NSFileHandle fileHandleForReadingAtPath:path];
fileData = [readFile readDataToEndOfFile];
length = [fileData length];
buffer = malloc(sizeof(char)*length);
[fileData getBytes: buffer];
printf("%s\n", buffer);
printf("%d\n", length);
free(buffer);
RELEASE(pool);
return 0;
} |
Before using Gorm, it would be better to know how to write a program from scratch, because Gorm is still under development, and sometimes it is useful to manipulate the user interface from source code. It is also good to know the logic of GNUstep application so that you won't get lost when mixing Gorm files with source code. There are other tutorials which give more details.
The idea of GNUstep application is that you start up the default NSApplication, and it will keep running and receiving events. When NSApplication, or NSApp as the shorter form, receives events, it will call it's delegate, which is user-defined. Therefore, programmers are responsible to write the delegate. If the delegate is interested in some specific events, it can intercept them. Otherwise, NSApp will handle them by itself. Therefore, delegate only need to intercept the events which it is interested. This makes the design of delegate very easy.
The example here looks like this:
The "Info" window is a built-in function of GNUstep. I only need to make the menu and the "Hello World" window.
Start with the main.m file.
main.m:
#include "AppController.h"
#include <AppKit/AppKit.h>
int main(int argc, const char *argv[])
{
NSAutoreleasePool *pool;
AppController *delegate;
pool = [[NSAutoreleasePool alloc] init];
delegate = [[AppController alloc] init];
[NSApplication sharedApplication];
[NSApp setDelegate: delegate];
RELEASE(pool);
return NSApplicationMain (argc, argv);
} |
I define a class called "AppController" as the delegate of this application. After the NSApp starts up ( [NSApplication sharedApplication] ), I set an instance of AppController as the delegate of this application ( [NSApp setDelegate: delegate] ). Then let the NSApp run ( NSApplicationMain (argc, argv) ).
Then what even should I intercept ? I want to display the menu, a window and a text inside. Therefore, I need to know when the application is *going* to launch, and when it is *finished* launching. Here is the AppController.h.
AppController.h:
#ifndef _AppController_H_
#define _AppController_H_
#include <Foundation/NSObject.h>
@class NSWindow;
@class NSTextField;
@class NSNotification;
@interface AppController : NSObject
{
NSWindow *window;
NSTextField *label;
}
- (void)applicationWillFinishLaunching:(NSNotification *) not;
- (void)applicationDidFinishLaunching:(NSNotification *) not;
@end
#endif /* _AppController_H_ */ |
The NSWindow and NSTextField are the window and text field in GNUstep. I just declare them here for later use. These two methods: -applicationWillFinishLaunching: and -applicationDidFinishLaunching: are self-explained. When NSApp is going to launch, it will call the method -applicationWillFinishLaunching, in delegate. So does NSApp finish launching. No matter what class the delegate is, it can be the delegate of NSApp, as long as it implements the proper methods so that NSApp can notify the delegate when proper events come. If the delegate doesn't implement these two methods, NSApp will handle the events by itself. Therefore, implement proper methods when you want to intercept some specific events. The usage of delegate is everywhere in GNUstep, which makes the programming very easy.
Usually, not always, I set up the user interface in method -applicationWillFinishLaunching:, and display the user interface in method -applicationDidFinishLaunching:. Here is the implementation.
AppController.m:
#include "AppController.h"
#include <AppKit/AppKit.h>
@implementation AppController
- (void) applicationWillFinishLaunching: (NSNotification *) not
{
/* Create Menu */
NSMenu *menu;
NSMenu *info;
menu = [NSMenu new];
[menu addItemWithTitle: @"Info"
action: NULL
keyEquivalent: @""];
[menu addItemWithTitle: @"Hide"
action: @selector(hide:)
keyEquivalent: @"h"];
[menu addItemWithTitle: @"Quit"
action: @selector(terminate:)
keyEquivalent: @"q"];
info = [NSMenu new];
[info addItemWithTitle: @"Info Panel..."
action: @selector(orderFrontStandardInfoPanel:)
keyEquivalent: @""];
[info addItemWithTitle: @"Preferences"
action: NULL
keyEquivalent: @""];
[info addItemWithTitle: @"Help"
action: @selector (orderFrontHelpPanel:)
keyEquivalent: @"?"];
[menu setSubmenu: info
forItem: [menu itemWithTitle:@"Info"]];
RELEASE(info);
[NSApp setMainMenu:menu];
RELEASE(menu);
/* Create Window */
window = [[NSWindow alloc] initWithContentRect: NSMakeRect(300, 300, 200, 100)
styleMask: (NSTitledWindowMask |
NSMiniaturizableWindowMask |
NSResizableWindowMask)
backing: NSBackingStoreBuffered
defer: YES];
[window setTitle: @"Hello World"];
/* Create Label */
label = [[NSTextField alloc] initWithFrame: NSMakeRect(30, 30, 80, 30)];
[label setSelectable: NO];
[label setBezeled: NO];
[label setDrawsBackground: NO];
[label setStringValue: @"Hello World"];
[[window contentView] addSubview: label];
RELEASE(label);
}
- (void) applicationDidFinishLaunching: (NSNotification *) not
{
[window makeKeyAndOrderFront: self];
}
- (void) dealloc
{
RELEASE(window);
[super dealloc];
}
@end |
Firstly, menu is created. NSMenu is a collection of NSMenuItem. Add NSMenuItem into NSMenu, connect the NSMenuItem to an action, or to another NSMenu. For example, the NSMenuItem "Hide" is connected to the action "hide:" ( @selector(hide:) ). When this NSMenuItem "Hide" is clicked, the method -hide: will be called. But where is the method -hide: ? It is a built-in method in NSApp. Therefore, I don't need to implement it. So does the method -terminate:. NSMenuItem will automatically find these methods. The NSMenuItem "Info" is connected to the NSMenu "Info", which is how the submenu is constructed. That's the reason why the action of NSMenuItem "Info" is NULL. Once NSMenu "info" is set into NSMenu "menu" ( [menu setSubmenu: forItem:] ), NSMenu "info" will be retained by NSMenu "menu". Therefore I can release the NSMenu "info". So does the NSMenu "menu" after it is set into the main menu of NSApp ( [NSApp setMainMenu: ] ). The methods, -orderFrontStandardInfoPanel and -orderFrontHelpPanel, are also the built-in methods in NSApp.
Secondly, NSWindow is created and title is set. NSMakeRect is a shortcut function to create a NSRect structure. The origin of GNUstep is at left-bottom. The first two numbers are the left-bottom position (x, y) of the window related to the monitor, and the rest two numbers are the width and height of the window. GUI classes are usually initialized with -initWithFrame: method or something similar in order to assign the position and size. GNUstep use absolute position/size rather than relative position/size in most of the cases.
Finally, a NSTextField is set up with many properties. Most important one is the method -setStringValue:, which sets the text in NSTextField.
The way GNUstep build up the user interface is that you add some GUI classes on other GUI classes. In this case, I want to add the NSTextField on the NSWindow. Therefore, NSTextField will be the *subview* of NSWindow, and NSWindow will be the *superview* of NSTextField. The only tricky part is that NSWindow is not a single view, but contains many components, like the title, border, etc. Therefore, you can not add NSTextField on top of NSWindow, but you can add NSTextField on top of the content view of NSWindow. That's how I build up the user interface. Read Drawing and Views for more details about NSViews.
Once everything is set up, they are ready to display. Menu is already set to NSApp. So NSApp will handle the display by itself. I only need to display the window. In method -applicationDidFinishLaunching:, I use [window makeKeyAndOrderFront:] to display the window and its subviews. This method will put the window in front of the display, and make it is the key window, the window which can accept the key-in.
So the idea is very simple: create the menu and user interface you want to display; connect the menu to the methods, which could be built-in or user-defined; put all the GUI into the window, then display it.
Again, I need the GNUmakefile.
GNUmakefile:
include $(GNUSTEP_MAKEFILES)/common.make APP_NAME = HelloWorld HelloWorld_HEADERS = AppController.h HelloWorld_OBJC_FILES = main.m AppController.m HelloWorld_RESOURCE_FILES = HelloWorldInfo.plist include $(GNUSTEP_MAKEFILES)/application.make |
You will notice that there is a file called "HelloWorldInfo.plist". This is the file which will be used by the info panel from the method -orderFrontStandardInfoPanel:. The file name has to be the "application name" + "Info.plist". In this case, it is "HelloWorldInfo.plist". If this file exists, the info panel of the application will display its content automatically.
Here is the content of Info.plist file. You can modify the content.
HelloWorldInfo.plist:
{
ApplicationDescription = "Hello World Tutorial";
ApplicationIcon = "";
ApplicationName = HelloWorld;
ApplicationRelease = 0.1;
Authors = "";
Copyright = "Copyright (C) 200x by ...";
CopyrightDescription = "Released under...";
FullVersionID = 0.1;
URL = "";
} |
Gorm is the tool to build the user interface for GNUstep application. The idea is that there is an instance called "controller", and an user interface called "view". The "view" is created by Gorm, and the "controller" controls the behavior of "view". Therefore, any event from this "view" will go to this "controller", and this "controller" can control the GUI component of this "view". Since the "view" is created by Gorm, and the "controller" is written as source code, there must be some way to connect these two instances. "Controller" must define the variables which correspond to the components on the "view" in order to control them. These variables called "outlets". Once these outlets (variables) are connected to the components on the "view", manipulation on these outlets equals to the manipulation on these components. Any event on the "view" should call a specific method in the "controller". For example, when user click the menu "Quit", it should call the method -terminate: in NSApp so that this application can quit. The methods called by the components on the "view" are called "actions". So the "actions" are the "methods" in the "controller" which are called when the components on the "view" are triggered. And the "outlets" are the variables in "controller" which corresponds to the components on the "view".
There is a tutorial which use the older Gorm. The operation might be a little bit different, but the comcept of "action" and "outlet" are the same. They are worthy to read. Gorm can also be used with ProjectCenter.
Assume the Gorm is installed. Use 'openapp Gorm" to start. You should see these window/panels of Gorm:
Choose "Document->New Application" to create a new user interface for this application. A window and a menu will appear. They are the main window and the main menu for this new application.
Click the window, and the inspector will become "NSWindow Inspector". Change the "Title" in "Attributes". Remember to press enter key to make it effective. You can change the size of this windows by drag the border, or set the number in "Size" in inspector.
For the text inside the window, drag the "title" from palettes into the main window like this:
Double-click it and type.
That's all for the main window. Let's work on the menu. Besides the default "Hide" and "Quit", I need an "Info" menu. Click on the left button of the palettes to show up the menus. Drag the "Info" submenu into the main menu. Clicking on the new "Info" menu item will show the submenu "Info".
That's all the user interface I need. Since I don't manipulate any GUI component in this example, I don't need any outlet. But some GUI component need to set the "action" so that they can trigger the specific events. Therefore, I need to connect these GUI components to the "actions".
"Actions" are the methods in the controller for this view. And where is the controller for the main window ? The answer is the NSApp in this case. Since NSApp already exist, and contains many built-in methods, I don't even need to write any method by my own. I only need to connect each GUI component to the existing method in NSApp. The more general examples about "outlets" and "actions" will be in the following tutorials.
There are two steps to connect GUI component to the action in an specific instance. The first step is connect the GUI component to the instance which has the "action". The second is to specify the "action" I want to use in this instance.
I start with the "Info Panel...", which is a "NSMenuItem". There is a method -orderFrontStandardInfoPanel in NSApp. Therefore, I need to connect the NSMenuItem "Info Panel" to NSApp. Then where is NSApp ? Since this is the main menu of this application, its owner is this application. Therefore, the instance NSOwner is NSApp. Look at the "Attributes" of NSOwner. It will tell you which class the NSOwner is.
Hold on the "Ctrl" key, and drag the "Info Panel..." into the NSOwner. There should be a small green circle with "s" (source) and a red circle with "t" (target). That means the source is connected to the target.
Look at the inspector now. It should be the "NSMenuItem Inspector" because the "Info Panel" is a NSMenuItem instance. Choose the "Connections" in the inspector. In the "Outlets" column, there is a "target", which means the target of this NSMenuItem. Since I connect "Info Panel" to NSOwer, which is NSApp, the target refers to NSApp. Click on this target, it will show all the actions available in the target, NSApp. In the "Actions" column, you can find the method -orderFrontStandardInfoPanel:. Choose it, and click the "Connect" button in the inspector.
In the "Connections" column, you can see this NSMenuItem is connected to the method -orderFrontStandardInfoPanel: of NSOwer, which is NSApp in this case. Keep doing that for the menu "Hide" and "Quit": Ctrl-drag, and connect the proper action.
Then the part for Gorm is done ! Save it as "HelloWorld.gorm", which is actually an directory and contains all the stuff it needs.
I still need the basic GNUmakefile and main.m to make it work. Here are the files:
GNUmakefile:
include $(GNUSTEP_MAKEFILES)/common.make APP_NAME = HelloWorld HelloWorld_HEADERS = HelloWorld_OBJC_FILES = main.m HelloWorld_RESOURCE_FILES = HelloWorldInfo.plist HelloWorld.gorm HelloWorld_MAIN_MODEL_FILE = HelloWorld.gorm include $(GNUSTEP_MAKEFILES)/application.make |
main.m:
#include <AppKit/NSApplication.h>
int main(int argc, const char *argv[])
{
return NSApplicationMain (argc, argv);
} |
HelloWorldInfo.plist:
{
ApplicationDescription = "Hello World Tutorial";
ApplicationIcon = "";
ApplicationName = HelloWorld;
ApplicationRelease = 0.1;
Authors = "";
Copyright = "Copyright (C) 200x by ...";
CopyrightDescription = "Released under...";
FullVersionID = 0.1;
URL = "";
} |
You can find the extra entry, HelloWorld_MAIN_MODEL_FILE, which means the HelloWorld.gorm is the main user interface file for this application. It is a shortcut to use the main user interface. Therefore, in main.m, I can use very simple codes than previous lesson. In the following tutorials, I will show the more general usage of Gorm file.
Single window application is one of the most commonly used application type. Here, a TimeMachine application is made to demonstrate several techniques of GNUstep programming.
I'll write an application which show the current time with a button to update. It looks like this:
This application is very easy. Just to show how to use "outlet" and "action" in Gorm, and the relation between "controller" and "view".
Open Gorm, choose "Document->New Application", and build a window with a title and a button.
You can set the title of the button in the inspector. So does the title of window.
That's all I need to do on the interface.
Now, I need an "controller" to receive the action from the "view", and display the time on the "view". I need to create a object for this "controller".
First, I need to design the class of this "controller". Click the "classes" in the main window of Gorm.
It will show all the classes you can use. I want the class of this "controller" to inherit from NSObject. Therefore, choose "NSObject", then select the menu "Classes->Create Subclass...". It will create a new class under the NSObject.
Double-click on it to change the name. I call it "Controller" here. That's the name of this class.
Then I need a "outlet" in this class to connect to the label in the window, and an "action" which is triggered when the button is press.
Choose the "Controller" class and click "Outlets" tab in the inspector. Press "Add" button, and change the name to "label" by double-clicking the "newOutlet". You will notice that "Controller" class now has one outlet. Click on the small round button will list the outlets it has.
Now, click "Actions" tab and add a new action called "showCurrentTime:". You should notice that the action is ended with colon (:), which will be explained later.
Once I finish designing this class, I can create an instance from it.
Choose the class "Controller", then select the menu "Classes->Instantiate". Look at the objects of Gorm main window. You should find a new instance, called "Controller". That's the instance I want to connect to the interface.
Now, I can connect the "controller" to the "view" for the outlet. Hold on "Ctrl", select the "Controller" instance, drag to the label in the window. Look at the inspector, which will show all the outlet the "Controller" instance has. Select the only one "label", click the "connect" button. That's it.
Again, I can connect the "view" to the "controller" for the action. Hold on "Ctrl", select the button in the window, drag to the "Controller" instance. Look at the inspector, which will show all the outlet the button has. Select the "target" in the outlet column, then select the only one action the target has, "showCurrentTime". Click the "connect" button. That's it.
Finally, I need to create the files for this "Controller" class in order to add the the source code. Otherwise, this class has not function at all. Select the class "Controller" ( not the instance !) in the Gorm main window. Choose menu "Classes->Create Class Files". A "Save" panel will show up and ask the filename. The default is "Controller.m", which is good enough. Click "O.K.". Save this Gorm file as "TimeMachine.gorm", then leave Gorm.
Now, I have a directory, "TimeMachine.gorm", and two file, "Controller.h" and "Controller.m".
Controller.h:
/* All Rights reserved */
#import <AppKit/AppKit.h>
@interface Controller : NSObject
{
id label;
}
- (void) showCurrentTime: (id)sender;
@end |
Controller.m:
/* All Rights reserved */
#import <AppKit/AppKit.h>
#import "Controller.h"
@implementation Controller
- (void) showCurrentTime: (id)sender
{
/* insert your code here */
} |
That's the source code of the class "Controller" I just created in Gorm. The method -showCurrentTime: has the message "sender". That's the reason the action name in Gorm must has the colon (:) in the end. In the "Controller.h", you can find the outlet "label" and the action "showCurrentTime:". Therefore, whatever you do on the outlet "label" will reflect to the "label" in the interface, and when the button "Get Current Time" is pressed, the action -showCurrentTime: will be called. That's how the outlet and action work.
Now, I can put some source code in this class.
Controller.m:
/* All Rights reserved */
#import <AppKit/AppKit.h>
#import "Controller.h"
@implementation Controller
- (void) showCurrentTime: (id)sender
{
/* insert your code here */
NSCalendarDate *date = [NSCalendarDate date];
[date setCalendarFormat: @"%H : %M : %S"];
[label setStringValue: [date description]];
} |
First, [NSCalendarDate date] will create an instance with the current date and time. After setting the format, put the description of current time into the outlet "label". When users click the "Get Current Time" button, it will show the current time.
That's it. Add the main.m and GNUmakefile, then it is ready to go.
main.m:
#import <AppKit/AppKit.h>
int main(int argc, const char *argv[])
{
return NSApplicationMain (argc, argv);
} |
GNUmakefile:
include $(GNUSTEP_MAKEFILES)/common.make APP_NAME = TimeMachine TimeMachine_HEADERS = Controller.h TimeMachine_OBJC_FILES = main.m Controller.m TimeMachine_RESOURCE_FILES = TimeMachineInfo.plist TimeMachine.gorm TimeMachine_MAIN_MODEL_FILE = TimeMachine.gorm include $(GNUSTEP_MAKEFILES)/application.make |
In this example, the owner of the interface is NSApp, which is the NSOwner in Gorm, but the controller is the "Controller" instance. Sometimes, the owner and the controller could be the same. In simple application, the owner/controller of the window can also be the delegate of NSApp. That's all depending on the design of your application.
Gorm can work on the custom view, which is designed by the programmer, not the built-in GUI component. Here, I continue my "time machine" application to demonstrate how to do that. The new interface looks like this:
Since I want to use the custom view in Gorm, I have to design the class first. The custom view can inherit from NSView, or NSControl, depending on what kind of functions you want. Actually, NSControl is a subclass of NSView. So I will inherit from NSControl. Click on the small circle of NSResponder to open its subclasses, then do the same thing on NSView and NSControl. Now, you can see that many GUI component inherit from NSControl, ex. NSTextField.
I want my custom view, called "TimeView", inherit from NSControl. Choose "NSControl", then select menu "Classes->Create Subclass...". Double-click to change the name.
You can notice that class "TimeView" also inherits 3 outlets and 7 actions from NSControl. Once the class "TimeView" is created, I can use it as custom view.
Build the interface as below:
Look at the "Attributes" in inspector of CustomView. Choose the class "TimeView".
The "CustomView" becomes "TimeView". That's it !
As I did before, create another class for the "controller". Add one outlet for this "TimeView", and one action for the button. I name the outlet "timeView".
Create an instance of class "Controller". Connect the button to the action "showCurrentTime", and the outlet "timeView" to the custom view "TimeView".
Finally, create the class file for the classes "TimeView" and "Controller". Save this application as "TimeMachine.gorm".
Now, I need to design the interface for the class "TimeView". The class "TimeView" is actually four NSTextField in a NSBox. The reason that I made them in one class is because I can reuse it later on. Classes inherited from NSView will be initialized by calling method -initWithFrame:. Therefore, I only need to rewrite the method -initWithFrame: in the class "TimeView". Here are the files:
TimeView.h:
#import <AppKit/AppKit.h>
@interface TimeView : NSControl
{
NSTextField *labelDate, *labelTime;
NSTextField *localDate, *localTime;
NSCalendarDate *date;
}
- (NSCalendarDate *) date;
- (void) setDate: (NSCalendarDate *) date;
@end |
TimeView.m:
#import <AppKit/AppKit.h>
#import "TimeView.h"
@implementation TimeView
- (id) initWithFrame: (NSRect) frame
{
NSBox *box;
self = [super initWithFrame: frame];
box = [[NSBox alloc] initWithFrame: NSMakeRect(0, 0,
frame.size.width,
frame.size.height)];
[box setBorderType: NSGrooveBorder];
[box setTitlePosition: NSAtTop];
[box setTitle: @"Local Time"];
labelDate = [[NSTextField alloc] initWithFrame: NSMakeRect(10, 45, 35, 20)];
[labelDate setStringValue: @"Date: "];
[labelDate setBezeled: NO];
[labelDate setBackgroundColor: [NSColor windowBackgroundColor]];
[labelDate setEditable: NO];
labelTime = [[NSTextField alloc] initWithFrame: NSMakeRect(10, 15, 35, 20)];
[labelTime setStringValue: @"Time: "];
[labelTime setBezeled: NO];
[labelTime setBackgroundColor: [NSColor windowBackgroundColor]];
[labelTime setEditable: NO];
localDate = [[NSTextField alloc] initWithFrame: NSMakeRect(55, 45, 130, 20)];
localTime = [[NSTextField alloc] initWithFrame: NSMakeRect(55, 15, 130, 20)];
[box addSubview: labelDate];
[box addSubview: labelTime];
[box addSubview: localDate];
[box addSubview: localTime];
RELEASE(labelDate);
RELEASE(labelTime);
RELEASE(localDate);
RELEASE(localTime);
[self addSubview: box];
RELEASE(box);
return self;
}
- (NSCalendarDate *) date
{
return date;
}
- (void) setDate: (NSCalendarDate *) aDate
{
ASSIGN(date, aDate);
[date setCalendarFormat: @"%a, %b %e, %Y"];
[localDate setStringValue: [date description]];
[date setCalendarFormat: @"%H : %M : %S"];
[localTime setStringValue: [date description]];
}
- (void) dealloc
{
RELEASE(date);
[super dealloc];
}
@end |
When Gorm generates the class files, it contains some default codes in it. Since I don't need any of them, they are safe to be removed. In TimeView.h, I declare four NSTextField for display, and on NSCalendarDate to store the date. I also declare two accessory methods to set and get the date. In the -initWithFrame, one NSBox is set up, and four NSTextField is put into it. And class "TimeView" is a subclass of NSView, I add the NSBox as the subview of the class "TimeView".
Other parts of this application should be very easy. Here are the files:
Controller.h:
#import <AppKit/AppKit.h>
#import "TimeView.h"
@interface Controller : NSObject
{
id timeView;
}
- (void) showCurrentTime: (id)sender;
@end |
Controller.m:
#import <AppKit/AppKit.h>
#import "Controller.h"
@implementation Controller
- (void) showCurrentTime: (id)sender
{
/* insert your code here */
NSCalendarDate *date = [NSCalendarDate date];
[timeView setDate: date];
}
@end |
main.m:
#import <AppKit/AppKit.h>
int main(int argc, const char *argv[])
{
return NSApplicationMain (argc, argv);
} |
GNUmakefile:
include $(GNUSTEP_MAKEFILES)/common.make APP_NAME = TimeMachine TimeMachine_HEADERS = Controller.h TimeView.h TimeMachine_OBJC_FILES = main.m Controller.m TimeView.m TimeMachine_RESOURCE_FILES = TimeMachineInfo.plist TimeMachine.gorm TimeMachine_MAIN_MODEL_FILE = TimeMachine.gorm include $(GNUSTEP_MAKEFILES)/application.make |
You should notice that I didn't instantiate the class "TimeView" in class "Controller" because when I add an custom view to the window, it is instantiated automatically. I only need to specify the class the custom view should be. On the contrary, I have to instantiate the class "Controller" in Gorm because it is not a GUI component. Without instantiation, I can't connect the "controller" to the "view".
I want a graphic clock, which looks like this:
It is very simple. I only need to add a new GUI component in the class TimeView. I call this new class "ClockView". Since "ClockView" will be the subview of "TimeView", when "TimeView" is update, I also need to update the "ClockView".
ClockView.h:
#import <AppKit/AppKit.h>
#include <math.h>
@interface ClockView : NSView
{
NSPoint posHour, posMinute;
}
- (void) setDate: (NSCalendarDate *) aDate;
@end |
ClockView.m:
#import "ClockView.h"
@implementation ClockView
- (id) init
{
self = [super init];
posHour = NSMakePoint(0,0);
posMinute = NSMakePoint(0,0);
return self;
}
- (void) drawRect: (NSRect) frame
{
NSPoint origin = NSMakePoint(frame.size.width/2, frame.size.height/2);
NSBezierPath *bp = [NSBezierPath bezierPathWithRect: [self bounds]];
[[NSColor yellowColor] set];
[BP fill];
BP = [NSBezierPath bezierPathWithRect: NSMakeRect(1, 1,
frame.size.width-2,
frame.size.height-2)];
[[NSColor blackColor] set];
[BP stroke];
BP = [NSBezierPath bezierPath];
[BP setLineWidth: 3];
[BP moveToPoint: origin];
[BP relativeLineToPoint: posHour];
[BP stroke];
[BP setLineWidth: 1];
[BP moveToPoint: origin];
[BP relativeLineToPoint: posMinute];
[BP stroke];
}
- (void) setDate: (NSCalendarDate *) date;
{
int hour = [date hourOfDay];
int minute = [date minuteOfHour];
float hour_x = 40*sin((M_PI*hour/6)+(M_PI*minute/360));
float hour_y = 40*cos((M_PI*hour/6)+(M_PI*minute/360));
float minute_x = 60*sin(M_PI*minute/30);
float minute_y = 60*COs(M_PI*minute/30);
posHour = NSMakePoint(hour_x, hour_y);
posMinute = NSMakePoint(minute_x, minute_y);
[self setNeedsDisplay: YES];
}
@end |
ClockView inherits from NSView. The most important method it should override is -drawRect:. When this view need to update, -drawRect: will be called. Therefore, I put all the drawing in this method. NSBezierPath is how GNUstep draws. I assign the path, set the color, then draw. There are good article about drawing: Introduction to Cocoa Graphics, Part I, Part II.
A few codes are needed to include ClockView in TimeView. One is to add ClockView as a subview of NSBox in TimeView. Another is to update ClockView when TimeView is update. In method -setDate: of ClockView, it uses [self setNeedsDisplay: YES] to make this view update. This modification is easy to do. You can play around it.
Panel is a special kind of window. Read the Cocoa document for more details: Windows and Panels. Since I have a clock already, I want to know the time in different time zones. My idea is that when I click the title of the NSBox, a panel will show up and ask the time zone. Once the time zone is inputted, it will display the time in that area. Since NSPanel is a subclass of NSWindow, the usage of NSPanel is similar to the NSWindow. Again, I need a "controller" to control the "view", which is NSPanel in this case. "View" is generated by Gorm, and I need to write the "controller" by myself. In this example, I'll show how to load the gorm file. There are many built-in panel in GNUstep. I also use one in this example.
Firstly, I need to build the interface for the panel. Open Gorm, Choose menu "Document -> New Module -> New Empty". Look at the palettes. There is one for panel.
Drag the panel out of palettes. Build the interface as below.
You can change the size of panel in the inspector. Here is the attributes of this panel I use.
Now, I got the "view". Then where is the "controller" ? Generally, I can write a new class as the controller of this view, but this is a small program. It is not necessary to write a new class only as the controller. So I decide to use the class "TimeView" as the controller for this panel. So class "TimeView" acts as the custom view for that main window interface, and the controller for the panel. Since "TimeView" is the controller of this panel, I need to connect the outlets and actions. Therefore, I need to create the class "TimeView" again in this gorm file, even though there is already one in TimeMachine.gorm file.
You already know how to create the class "TimeView". I add two outlets, "zonePanel" and "zoneField", and two actions, "okAction:" and "cancelAction:".
But rather than creating an instance to connect the panel, I'll set the owner of this panel as the class "TimeView". The advantage is that I can reduce the number of instances to use.
Select the NSOwner in Gorm main window, then select class "TimeView" in "attributes" of inspector.
By this way, I can connect the panel to the NSOwner, which is an instance of class "TimeView". Connect the two buttons to the actions in NSOwner, the outlet "zoneField" to the NSTextField in panel, and the outlet "zonePanel" to the panel. Pay attention to how the NSOwner connects to the panel.
Save this interface as "TimeZonePanel.gorm" file, and quit Gorm. Don't generate the files for class "TimeView" because I already have the files. GNUstep can figure out where the classes are and where the outlets/actions are.
Now, I need to add the new outlets and actions into the files of "TimeView". Here is the header.
TimeView.h:
#import <AppKit/AppKit.h>
#import "ClockView.h"
@interface TimeView : NSControl
{
id zonePanel;
id zoneField;
NSBox *box;
NSTextField *labelDate, *labelTime;
NSTextField *localDate, *localTime;
NSCalendarDate *date;
ClockView *clockView;
}
- (NSCalendarDate *) date;
- (void) setDate: (NSCalendarDate *) date;
- (void) okAction: (id) sender;
- (void) cancelAction: (id) sender;
@end |
I add the outlets and actions by myself.
TimeView.m:
- (void) mouseDown: (NSEvent *) event
{
NSRect titleFrame = [box titleRect];
NSPoint windowLocation = [event locationInWindow];
NSPoint viewLocation = [self convertPoint: windowLocation fromView: [self superview]];
BOOL status = NSMouseInRect(viewLocation, titleFrame, NO);
if (status == YES)
{
[NSBundle loadNibNamed: @"TimeZonePanel.gorm" owner: self];
[NSApp runModalForWindow: zonePanel];
}
} |
The method -mouseDown: is called when mouse is clicked within this view. Here, I calculate whether the mouse is clicked in the area of the title of NSBox. If so, use [NSBundle loadNibName: owner:] to load the window, and [NSApp runModalForWindow] display it. Read Cocoa's document about "How Modal Windows Work".
Now, I just need to finish the actions part in TimeView.m.
TimeView.m:
- (void) cancelAction: (id) sender
{
[NSApp abortModal];
[zonePanel close];
}
- (void) okAction: (id) sender
{
NSTimeZone *tempZone;
tempZone = [NSTimeZone timeZoneWithName: [zoneField stringValue]];
[NSApp stopModal];
[zonePanel close];
if (tempZone == nil)
{
NSRunAlertPanel(@"Warning!",
@"Wrong Time Zone !!",
@"OK", nil, nil);
}
else
{
[date setTimeZone: tempZone];
[box setTitle: [tempZone description]];
[self setDate: date];
}
} |
In method -okAction:, I use a built-in panel, NSRunAlertPanel. There are several built-in panels in GNUstep ready to use. Now, you can display the current time in different time zone.
It is inconvenient to use this pop-up panel because you have to click the NSTextField before typing. Sometimes, it is more convenient to control the user interface via keyboard rather than mouse. Here, I touch a little bit this topic to make the panel more easy to use.
When a window pop-up, it is the first object to receive events and key-in. It is called the "First Responder". But usually we want some other objects in this window to receive the key-in. Therefore, we need to change the "first responder" of this window, which I can do that by using [NSWindow makeFirstResonpder:].
When I want to use "Tab" key to switch between different views in the window, I need to assign the "nextKeyView" for the next view when "Tab" key is pressed so that the application know where the responder should be.
Finally, when I finish typing in the NSTextField, I want to hit the "Return" key equivalent to press the "OK" button by mouse so that I don't need to move my hand out of the keyboard. In this case, since NSTextField is also a subclass of NSControl, I can set the target and action of NSTextField the same as the NSButton "OK". Therefore, when I hit the "Return", it is equivalent to click on the "OK" button.
These are small tune-ups for the application, but it makes users more easy to use the application.
Firstly, let's set the "first responder" of the window to the NSTextField:
TimeView.m:
- (void) mouseDown: (NSEvent *) event
{
NSRect titleFrame = [box titleRect];
NSPoint windowLocation = [event locationInWindow];
NSPoint viewLocation = [self convertPoint: windowLocation fromView: [self superview]];
BOOL status = NSMouseInRect(viewLocation, titleFrame, NO);
if (status == YES)
{
[NSBundle loadNibNamed: @"TimeZonePanel.gorm" owner: self];
[zonePanel makeFirstResponder: zoneField];
[NSApp runModalForWindow: zonePanel];
}
} |
Only one line is enough. Now, when this panel shows up, the cursor will automatically in the NSTextField, and this NSTextField are ready to type.
Secondly, I want to set the target and action of NSTextField the same as the NSButton "O.K.". Open the TimeZonePanel.gorm, connect the NSTextField to the method -okAction: of the NSOwner. That's it. Whenever you hit the "Return" key in the NSTextField, the method -okAction: is called.
Thirdly, I need to connect the nextKeyView outlet between the views in the window. I'll connect the nextKeyView of NSTextField to NSButton "O.K.", the nextKeyView outlet of NSButton "O.K." to NSButton "Cancel", and the nextKeyView outlet of NSButton "Cancel" to the NSTextField. By doing that, I can switch between these views by "Tab" key. Here, I just show how the nextKeyView of NSTextField connects to NSButton "O.K.". You can do the rest of the part.
Source code: Panel-src.tar.gz.
Since I can change the time zone, I would like to display several clock at the same window, and adjust the number of clocks dynamically. First, I need to add new submenu: "Edit", and two menu item in it: "Add Clock" and "Delete Clock". Then add two action in the class "Controller": "addClock:" and "deleteClock". Connect the menu item to the action in the instance of class "Controller" in the gorm file. Then I have done the part of interface. When user select the menu item "Add Clock", the method "addClock:" will be called, so does the menu item "Delete Clock".
Now, how do I manage these clocks dynamically ? I can trace each of them manually, but it will be complicated. Another way is that I don't trace any of them, but the problem is how do it control any of them ? I can use the notification in GNUstep, which is a pretty handy way to communicate between objects.
Here is a related article: NSNotificationCenter
Firstly, I need to adjust the interface according to the adding and deleting clock. I need trace how many clocks exist so that I won't accidentally delete the last clock.
Controller.h:
#import <AppKit/AppKit.h>
#import "TimeView.h"
@interface Controller : NSObject
{
id timeView;
unsigned int totalNumber;
}
- (void) showCurrentTime: (id) sender;
- (void) addClock: (id) sender;
- (void) deleteClock: (id) sender;
@end |
I add a variable, totalNumber to trace the number of clocks, and add two actions manually since I didn't generate the class files from Gorm.
Controller.m:
- (id) init
{
self = [super init];
totalNumber = 1;
return self;
}
- (void) addClock: (id) sender
{
TimeView *aView;
NSWindow *mainWindow = [NSApp mainWindow];
NSRect windowFrame, timeViewFrame;
windowFrame = [mainWindow frame];
timeViewFrame = [timeView frame];
[mainWindow setFrame: NSMakeRect(windowFrame.origin.x,
windowFrame.origin.y,
windowFrame.size.width+timeViewFrame.size.width,
windowFrame.size.height)
display: YES];
aView = [[TimeView alloc] initWithFrame: NSMakeRect(timeViewFrame.origin.x + totalNumber*timeViewFrame.size.width,
timeViewFrame.origin.y,
timeViewFrame.size.width,
timeViewFrame.size.height)];
[[mainWindow contentView] addSubview: aView];
RELEASE(aView);
totalNumber ++;
}
- (void) deleteClock: (id) sender
{
NSArray *subviews;
NSWindow *mainWindow = [NSApp mainWindow];
NSRect windowFrame, timeViewFrame;
int i;
windowFrame = [mainWindow frame];
timeViewFrame = [timeView frame];
subviews = [[mainWindow contentView] subviews];
for (i = [subviews count]-1; i > 1; i--)
{
if ([[subviews objectAtIndex: i] isMemberOfClass: [TimeView class]])
[[subviews objectAtIndex: i] removeFromSuperview];
totalNumber--;
[mainWindow setFrame: NSMakeRect(windowFrame.origin.x,
windowFrame.origin.y,
windowFrame.size.width-timeViewFrame.size.width,
windowFrame.size.height)
display: YES];
break;
}
} |
In the method -init, I initiate the variable, totalNumber, as 1 since there is already one in the Gorm file. In method -addClock:, I have to calculate the change of window size and where to put the new clock. They are done by very simple calculation. Once I add the new clock into the window, the window will retain this clock. Therefore, I can release it and no longer trace it. In the method -deleteClock:, I also need to change the size of window by simple calculation. The only problem is that since I don't trace the clocks, how do I delete them ? I can get all the subviews from the window, and delete from the last "TimeView" object. That's the way I can access the objects in the window. Very easy to maintain.
Now, if you hit the button "Get Current Time", you will notice that only the origin clock is updated. That's because it is the only one which is connected by the outlet. I can get all the subviews from the window, and call their method one by one. That will work, but is not a elegent way. I can use the "Notification" and "Notification Center" to archive this goal. Read the Cocoa document for more details.
The idea is that an object can be a speaker, and many objects can be the audience. So one talks, many listen. That's the way it works. So when user press the button "Get Current Time", the "Controller" must speak to all the clocks. Below is how it speak:
Controller.h:
- (void) showCurrentTime: (id)sender
{
[[NSNotificationCenter defaultCenter] postNotificationName: @"TimeViewShouldUpdateCurrentTime"
object: [NSCalendarDate date]];
} |
Actually, it talks to the "Notification Center", and the notification center will broadcast what it say. I need to specify the name of notification because there are so many notifications on the air. The name of notification is the way to distinguish them. And a notification can contain an object within it. That's how the speaker and audience transfer the information. It can be nil. Here, I use [NSCalendarDate date].
Now, the speaker speaks. How do the audience listen ? In this example, all the instance of class TimeView should listen in order to update the current time. I need to register the audience to the notification center so that they can receive the notification.
TimeView.m:
- (id) initWithFrame: (NSRect) frame
{
self = [super initWithFrame: frame];
box = [[NSBox alloc] initWithFrame: NSMakeRect(0, 0,
frame.size.width,
frame.size.height)];
[box setBorderType: NSGrooveBorder];
[box setTitlePosition: NSAtTop];
[box setTitle: @"Local Time"];
clockView = [[ClockView alloc] initWithFrame: NSMakeRect(0, 70,
frame.size.width,
frame.size.height)];
labelDate = [[NSTextField alloc] initWithFrame: NSMakeRect(10, 45, 35, 20)];
[labelDate setStringValue: @"Date: "];
[labelDate setBezeled: NO];
[labelDate setBackgroundColor: [NSColor windowBackgroundColor]];
[labelDate setEditable: NO];
labelTime = [[NSTextField alloc] initWithFrame: NSMakeRect(10, 15, 35, 20)];
[labelTime setStringValue: @"Time: "];
[labelTime setBezeled: NO];
[labelTime setBackgroundColor: [NSColor windowBackgroundColor]];
[labelTime setEditable: NO];
localDate = [[NSTextField alloc] initWithFrame: NSMakeRect(55, 45, 130, 20)];
localTime = [[NSTextField alloc] initWithFrame: NSMakeRect(55, 15, 130, 20)];
[box addSubview: clockView];
[box addSubview: labelDate];
[box addSubview: labelTime];
[box addSubview: localDate];
[box addSubview: localTime];
RELEASE(clockView);
RELEASE(labelDate);
RELEASE(labelTime);
RELEASE(localDate);
RELEASE(localTime);
[self addSubview: box];
RELEASE(box);
[[NSNotificationCenter defaultCenter] addObserver: self
selector: @selector(setDate:)
name: @"TimeViewShouldUpdateCurrentTime"
object: nil];
[self showCurrentTime: self];
return self;
} |
Only one line is needed for register. It specify what object to receive the notification (addObserver:), which method to handle the notification (selector:), what's the name of notification (name:), and what's the object of the notification. It is important that the name of notification should be the same as what the speaker use. So once the speaker say "TimeViewShouldUpdateCurrentTime", the objects which register them using "TimeViewShouldUpdateCurrentTime" will receive the notification, and the selector will be called. The "object: nil" means that this object accept all the notification with the name "TimeViewShouldUpdateCurrentTime" no matter what kind of object it carries on.
Now, I register the TimeView for the notification "TimeViewShouldUpdateCurrentTime". Once the speaker speak, the method -setDate: will be called. So I need to set up this method.
TimeView.m:
- (void) setDate: (NSNotification *) not { ASSIGN(date, [not object]); [date setTimeZone: [NSTimeZone timeZoneWithName: [box title]]]; [date setCalendarFormat: @"%a, %b %e, %Y"]; [localDate setStringValue: [date description]]; [date setCalendarFormat: @"%H : %M : %S"]; [localTime setStringValue: [date description]]; [clockView setDate: date]; } |
I reuse the -setDate in previous lesson, but change the interface because right now, it is called by notification center. And I can get the object the notification carries on by using [NSNotification object] method.
Finally, I need to remove the observer from notification center when it is released. Otherwise, it causes problems. So here is the -dealloc.
TimeView.m:
- (void) dealloc
{
[[NSNotificationCenter defaultCenter] removeObserver: self];
RELEASE(date);
[super dealloc];
} |
To sum up, the speaker speak to notification center with a specific name of notification, and may or may not carry an object with the notification. The audience register themselves to notification center with what kinds of notification they want to receive by the name of notification. When notification center get the notification, it will call the registered method in the audience.
There are some source codes needed to be modified due to the change of setDate: method. They are not shown here, and it's not hard to figure them out.
Since I can update all the clocks manually, I can do it automatically. NSTimer is a timer which can trigger an action after a given time repeatly or not. Here, I'll use a NSTimer to make the clock "run".
I need to add new submenu: "Timer", and two menu item: "Start" and "Stop". Add two action in class "Controller": "startTimer:" and "stopTimer:". Then connect the menu item to the action. This should be very easy now.
Add these two actions and a NSTimer in Controller.
Controller.h:
#import <AppKit/AppKit.h>
#import "TimeView.h"
@interface Controller : NSObject
{
id timeView;
unsigned int totalNumber;
NSTimer *timer;
}
- (void) showCurrentTime: (id) sender;
- (void) addClock: (id) sender;
- (void) deleteClock: (id) sender;
- (void) startTimer: (id) sender;
- (void) stopTimer: (id) sender;
@end |
Controller.m:
- (void) startTimer: (id) sender
{
timer = [NSTimer scheduledTimerWithTimeInterval: 1
target: self
selector: @selector(showCurrentTime:)
userInfo: nil
repeats: YES];
}
- (void) stopTimer: (id) sender
{
[timer invalidate];
} |
That's all. In NSTimer, set the interval, target, selector (action), and repeats. Then it will trigger the action -showCurrentTime: every second. Use -invalidate to stop the timer. Generally, you need to write a thread in order not to block the user interface. But with the help of timer, you can totally avoid this problem. Finger (in gnustep/usr-apps/) is another good example how to avoid thread using non-blocking I/O.
![]() | Since timer is autoreleased, it might disappear anytime in this example, which causes serious memory problem, and usually make application unstable. It would be better to retain the timer in -startTimer: and release it in -stopTimer:, and to ensure that only one timer exist when user click the "start" menu more than once. |
The Document-base application is a kind of special, but commonly used applications. You can open multiple documents, edit and save each of them. Most text editors belong to the document-base application. The Ink.app in GNUstep is a very good example to look at. Here is the Cocoa document about document-base application. You have to read it first. Otherwise, you probably will not know what I am doing. It looks complicated at the first time. You have to deal with NSDocumentController, NSWindowController, NSDocument and NSWindow. Actually, since I use Gorm to build the NSWindow, and NSDocumentController and NSWindowController are not required to subclass, NSDocument is the only class I have to deal with. That makes everything very easy. In this tutorial, I only make the skeleton of the document-base application. The real functions will be add in the later tutorials.
Now, I have to create the main user interface first. Since it is a document-base application, there is not main window at all. I only need a menu so that users can open, save and close each document. So open Gorm, choose "Document->New Application". Click the window in the Gorm main window, use "Edit->Delete" to delete the window. Drag the menu "Info" and "Document" into the main menu. The main user interface will be like this:
Only the menu, no window.
Next, I need a NSDocumentController. Choose the class "NSDocumentController", use menu "Classes->Instantiate" to make an instance.
The main user interface is done. Save to as "Money.gorm".
NSDocumentController will look at the property list of this application in order to know what kind of document it should handle. Here is an example of propery list.
MoneyInfo.plist
{
ApplicationDescription = "Money";
ApplicationIcon = "";
ApplicationName = Money;
ApplicationRelease = 0.1;
Authors = "";
Copyright = "Copyright (C) 200x by ...";
CopyrightDescription = "Released under...";
FullVersionID = 0.1;
URL = "";
NSTypes = (
{
NSName = "mon";
NSHumanReadableName = "Money Document";
NSUnixExtensions = ("mon");
NSRole = Editor;
NSDocumentClass = Document;
}
);
} |
The important part is that NSTypes define what kind of document it should handle, and what class to call. In this case, it is class "Document", which I haven't implement yet. This is a simplified property list, but it works. Look at the InkInfo.plist as a better one.
Now, I need to make the class "Document" as the property indicates, and the window for each document. Open Gorm, choose "Document->New Empty". Drag a window out of the palettes. Since this tutorial is only the skeleton, I do nothing in the window.
Look at the classes in the Gorm main window, and use "Classes->Create Subclass" to create a subclass of NSDocument, called "Document".
Don't instantiate it. Instead, it will be the owner of the document window. Click the NSOwner in the Gorm main window, Look at the "Attributes" in the inspector. Choose the "Document" class.
Now, the NSOwner is the class of Document. I need to connect the _window outlet of NSOwner (class "Document") to the window,
and the set the delegate of window to the NSOwner (class "Document").
Finally, use "Classes->Create Class Files" to create the files of class "Document". Save them to Document.m and Document.h. Save the Gorm file into "Document.gorm".
Now, I have Money.gorm, MoneyInfo.plist, Document.h, Document.m and Document.gorm. Five files (Gorm files are actually directories).
NSDocumentController know what class to use because it is written in the property list file. How do NSDocument (or its subclass, "Document") know where the interface of document is ? In this case, the interface of document is Document.gorm. The simple way is to implement the -windowNibName: in NSDocument. The Document files created from Gorm won't work perfectly. Here is the working one:
Document.h:
#import <AppKit/AppKit.h>
#import <AppKit/NSDocument.h>
@interface Document : NSDocument
{
}
@end |
Document.m:
#import "Document.h"
@implementation Document
- (NSString *) windowNibName
{
return @"Document.gorm";
}
@end |
I deleted the extra codes inherited from NSDocument. Include the NSDocument explicitly in the header file because AppKit.h doesn't include it. The most important part is that I return the name of the document interface, "Document.gorm", so that NSDocument can find where the interface are.
Here are the rest of files:
main.m:
#import <AppKit/AppKit.h>
int main(int argc, const char *argv[])
{
return NSApplicationMain (argc, argv);
} |
GNUmakefile:
include $(GNUSTEP_MAKEFILES)/common.make
APP_NAME = Money
Money_HEADERS = Document.h
Money_OBJC_FILES = main.m \
Document.m
Money_RESOURCE_FILES = MoneyInfo.plist \
Money.gorm \
Document.gorm
Money_MAIN_MODEL_FILE = Money.gorm
include $(GNUSTEP_MAKEFILES)/application.make |
Here is the source code: Money-src.tar.gz
Once the application starts up, you will see only the menu. Use "Document->New" to open a new document. A empty window will show up. You can keep as many documents as you want. The menu items will be automatically enabled depending on the existance of the document. Most menu doesn't work yet.
GNUstep offer a greate framework for document-base applications. Right now, I only need to focus on the Document itself, and don't need to worry about how to manage the multiple documents/windows. GNUstep will handle it perfectly.
I have the skeleton of document-base application already. I want it be a spreadsheet-like application to track the expense. If you are interested in text editors, Ink.app in GNUstep CVS is a pretty good one. It involves the text system of GNUstep, which is a big topic. Spreadsheet-like applications need a table. NSTableView is a good start. NSTableView is a more complicated user interface than NSButton, NSTextField, etc. So do NSBrowser, NSOutlineView, NSMatrix, etc. GNUstep does a great job to make it very easy to use. I'll try to explain it step by step.
Here is a related article: Getting Started With NSTableView
Use Gorm to open Document.gorm. Add a table view into the window. Try to resize it until it fit the whole window.
Check the "Horizontal" scroller. Actually it doesn't matter for now.
Look at the "Size" in the inspector of NSTableView. Click the line in the box to make them the spring.
The box represent the NSTableView. The straight line or spring represent the distance relationship. Line outside the box is the distance between NSTableView and its superview. It is the window in this case. The line inside the box is the size of the NSTableView. Straight line means the distance is fixed, the spring means it is resizable. In this case, when window is resized, since the distance between NSTableView and window is fixed, NSTableView will be resized according to the window. That's the behavior I want.
You can change the title of the column by double-click on it. But it is not necessary for now. You will find that it is still hard to control the interface of NSTableView from Gorm. I'll do that programmingly. Therefore, I need a outlet connected to this NSTableView from NSOwner.
Add an outlet, tableView, in the class "Document".
Set NSOwner as the data source and delegate of the NSTableView. I'll explain the data source later.
Connect the outlet "tableView" to NSTableView.
Save the Gorm file and quit Gorm.
Add the new outlet in Document.h.
Document.h:
#import <AppKit/AppKit.h>
#import <AppKit/NSDocument.h>
@interface Document : NSDocument
{
id tableView;
}
@end |
The way NSTableView work is that when it need to display, it will ask its data source to provide the data it needs. So I need to implement some methods to provide NSTableView the data it need. There are necessary two methods:
Document.m:
- (int) numberOfRowsInTableView: (NSTableView *) view
{
return 5;
}
- (id) tableView: (NSTableView *) view
objectValueForTableColumn: (NSTableColumn *) column
row: (int) row
{
return [NSString stringWithFormat: @"column %@ row %d", [column identifier], row];
} |
Method -numberOfRowsInTableView: will ask how many rows NSTableView should display. I set it to 5. Method -tableView:objectValueForTableColumn:Row: will ask the object for a given column and row. I return a string with the identifier of column and the number of row.
Now, this application is ready to run, even though it does nothing but display the data of 5 rows. This is merely a demo how the NSTableView works. I provide the number of rows, and the object in a given column and row. As long as these two kinds of data are provided, the NSTableView can display anything, even a image in the cell. I'll talk about more details about data source later on.
Here is the source code: Table-1-src.tar.gz
Let's work on the interface first. NSTableView is a collection of NSTableColumn. I want three columns for date, item and amount. There are two default columns in the Gorm. Therefore, I need to add a NSTableColumn into it.
Document.m:
- (void) windowControllerDidLoadNib: (NSWindowController *) controller
{
NSTableColumn *column;
NSArray *columns = [tableView tableColumns];
column = [columns objectAtIndex: 0];
[column setWidth: 100];
[column setEditable: NO];
[column setResizable: YES];
[column setIdentifier: @"date"];
[[column headerCell] setStringValue: @"Date"];
column = [columns objectAtIndex: 1];
[column setWidth: 100];
[column setEditable: NO];
[column setResizable: YES];
[column setIdentifier: @"item"];
[[column headerCell] setStringValue: @"Item"];
column = [[NSTableColumn alloc] initWithIdentifier: @"amount"];
[column setWidth: 100];
[column setEditable: NO];
[column setResizable: YES];
[[column headerCell] setStringValue: @"Amount"];
[tableView addTableColumn: column];
RELEASE(column);
[tableView sizeLastColumnToFit];
[tableView setAutoresizesAllColumnsToFit: YES];
} |
I adjust the interface of NSTableView in method -windowControllerDidLoadNib:, which quarantees that the Gorm file is loaded. This is similar to -awakeFromNib. Firstly, I get the existing columns, change their property. Secondly I create a new NSTableColumn and add it into NSTableView. Finally, I adjust the layout of NSTableView. By this way, I can arrange the user interface programmingly even though Gorm is not fully functional yet. Run this application again, and you will see the new column.
An important thing of NSTableColumn is the "identifier". Every NSTableColumn has an unique "identifier" to distinguish them. "Identifier" can be any object, but used to being NSString. The identifier is not necessary to be the same as the header of column, but used to being the same for easilier management. So we access the NSTableColumn via its identifier. You can find the "identifier" in many GNUstep objects.
The interface is done. Go back for the data source. Data source is an object which provide the data for NSTableView. Therefore, data source is the model of NSTableView in MVC paradigm. Depending on the behavior of NSTableView, I need to implement the proper methods in the data source of NSTableView. I already show the methods for display. But they are useless. Now, I will make them real functional.
The data for NSTableView can be considered as an NSArray of NSDictionary. The object in each index of NSArray corresponds to each row of NSTableView. And the object of each NSDictionary with a given key corresponds to each NSTableColumn with a given identifier. That's the simplest way to build the model for NSTableView. Therefore, I add an NSMutableArray in Document class.
Document.h:
#import <AppKit/AppKit.h>
#import <AppKit/NSDocument.h>
@interface Document : NSDocument
{
id tableView;
NSMutableArray *records;
}
@end |
The "records" will store the data of NSTableView. About the usage of NSMutableArray, read Basic GNUstep Base Library Classes.
I want there are always an empty row so that user can add data into the table. Before I add the function of adding data, look at the new methods for display.
Document.m:
- (id) init
{
self = [super init];
records = [NSMutableArray new];
return self;
}
- (void) dealloc
{
RELEASE(records);
[super dealloc];
}
- (int) numberOfRowsInTableView: (NSTableView *) view
{
return [records count] + 1;
}
- (id) tableView: (NSTableView *) view
objectValueForTableColumn: (NSTableColumn *) column
row: (int) row
{
if (row >= [records count])
{
return @"";
}
else
{
return [[records objectAtIndex: row] objectForKey: [column identifier]];
}
} |
I create the instance of NSMutableArray in method -init, and release it in -dealloc. In the method -numberOfRowsInTableView:, I return one more number because I want it to display an extra empty row. Hence, in the method -tableView:objectValueForTableColumn:row:, I have to check whether the row the NSTableView request is large than the actuall number of data. If so, it is request the empty row. I just return an empty string "". The trick to use a NSArray of NSDictionary is to make the key of NSDictionary the same as the identifier of NSTableColumn. So I can get the object directly by knowing the identifier of NSTableColumn. If you are not using the NSDictionary for each row, you can consider the Key Value Coding (KVC), which offer similar way to get the right object. Otherwise, you have to use if-else to get the right object. The advantage of NSDictionary (or KVC) will be more clear for data input.
Now, I'll add the function of data input. Firstly, I have to set the NSTableColumn editable.
- (void) windowControllerDidLoadNib: (NSWindowController *) controller
{
NSTableColumn *column;
NSArray *columns = [tableView tableColumns];
column = [columns objectAtIndex: 0];
[column setWidth: 100];
[column setEditable: |