UI Elements HOWTO

This HOWTO shows how you can create Cocoa user-interface elements by making lisp calls to instantiate and initialize Objective-C objects.

Cocoa programmers usually create UI elements using Apple's InterfaceBuilder application, and then load those elements from a nibfile, but Cocoa supports creating all the same UI elements by making Objective-C method calls. In fact, that's how it loads nibfiles: by making method calls to instantiate the objects described in them.

For Lisp programmers, accustomed to working incrementally and interactively, it may sometimes make more sense to create user-interface elements by making method calls interactively, rather than by constructing a complete user interface in InterfaceBuilder. This HOWTO shows how you can use Objective-C method calls to create and display windows and other UI elements.

For more information about how to load nibfiles from Lisp, see the "nib-loading" example. For a complete discussion of how to construct a Cocoa application using nibfiles created with InterfaceBuilder, see the "currency-converter" example.

Creating a Window

Every user-interface element under Mac OS X appears either in a window or in a menu. We'll begin by exploring how to create and display windows.

First, switch to the CCL package, for convenience. Most of Clozure CL's Objective-C utilities are in the CCL package:

? (in-package :ccl)
#<Package "CCL">
    

Creating a Cocoa window follows the common Objective-C pattern of allocating an object and then initializing it with some starting data. To allocate a window, just call the alloc method of the NSWindow class:

? (setf my-window (#/alloc (@class ns-window)))
#<NS-WINDOW <NSWindow: 0x13b68580> (#x13B68580)>
    

The above expression creates a new window, but doesn't display it. Before it shows up on the screen we must initialize it with some appropriate values. For that, we'll use the method initWithContentRect:styleMask:backing:defer:.

As always in Objective-C, the name of the method reveals something about the arguments it expects. The NSRect that we pass for the initWithContentRect: segment of the method name describes the shape of the window. The mask for styleMask: is a sequence of bits that specify which window features are turned on. The backing: argument is a constant of type NSBackingStoreType that specifies how Cocoa will draw the contents of the window. Finally, the defer: argument is a Boolean that determines whether to display the window as soon as it's created.

Next, we'll create data values to pass in these parameters, so that we can display our new window on the screen. We'll build the proper initialization form up piece-by-piece.

The first argument, of course, is the window object to be initialized. We pass the window that we created before:

(#/initWithContentRect:styleMask:backing:defer: my-window ...)
    

The next argument, the NSRect, is a structure that we need only temporarily. Because NSRect values appear so often in Cocoa code, Clozure CL provides a handy way to allocate them temporarily, disposing of them automatically. The with-ns-rect macro (in the NS package) creates an NSRect value, and then disposes of it when control leaves the scope of the macro; for example:

(ns:with-ns-rect (r 100 100 400 300)
   ...)
    

We can use this rectangle to initialize the shape of our new window:

(ns:with-ns-rect (r 100 100 400 300)
   (#/initWithContentRect:styleMask:backing:defer: 
    my-window 
    r 
    ...))
    

To specify the window features we want, we must combine several flags to form the proper style mask. Cocoa provides named constants for each of the various window features. To create the syle mask that describes a new window, use inclusive-or to combine the named flags into a style mask:

(logior  #$NSTitledWindowMask 
         #$NSClosableWindowMask  
         #$NSMiniaturizableWindowMask 
         #$NSResizableWindowMask)
    

You can find definitions for all the window masks in the Apple Developer documentation for NSWindow Constants.

Passing the window mask as the next argument gives us this expression:

(ns:with-ns-rect (r 100 100 400 300)
  (#/initWithContentRect:styleMask:backing:defer: 
   my-window 
   r 
   (logior  #$NSTitledWindowMask 
            #$NSClosableWindowMask  
            #$NSMiniaturizableWindowMask 
            #$NSResizableWindowMask)
   ...))
    

Like the style masks, the NSBackingStoreType value is a named constant that describes which drawing strategy Cocoa should use for the contents of the window. The value can be NSBackingStoreRetained, NSBackingStoreNonretained, or NSBackingStoreBuffered. For this example, we'll use NSBackingStoreBuffered:

(ns:with-ns-rect (r 100 100 400 300)
  (#/initWithContentRect:styleMask:backing:defer: 
   my-window 
   r 
   (logior  #$NSTitledWindowMask 
            #$NSClosableWindowMask  
            #$NSMiniaturizableWindowMask 
            #$NSResizableWindowMask)
   #$NSBackingStoreBuffered
   ...))
    

Finally, the defer argument is just a Boolean. If we pass a true value, Cocoa will defer displaying the window until we explicitly tell it to. If we pass a False value, it will instead display the window right away. We can pass the Lisp values T or NIL, and the Objective-C bridge automatically converts them for us, but in the spirit of using Objective-C values for Objective-C operations, let's use the Objective-C constants #$YES and #$NO:

(ns:with-ns-rect (r 100 100 400 300)
  (#/initWithContentRect:styleMask:backing:defer: 
   my-window 
   r 
   (logior  #$NSTitledWindowMask 
            #$NSClosableWindowMask  
            #$NSMiniaturizableWindowMask 
            #$NSResizableWindowMask)
   #$NSBackingStoreBuffered
   #$NO))
    

There; the expression to initialize our window object is finally complete. We can evaluate it in the Listener to initialize the window:

(ns:with-ns-rect (r 100 100 400 300)
  (#/initWithContentRect:styleMask:backing:defer: 
   my-window 
   r 
   (logior  #$NSTitledWindowMask 
            #$NSClosableWindowMask  
            #$NSMiniaturizableWindowMask 
            #$NSResizableWindowMask)
   #$NSBackingStoreBuffered
   #$NO))
    

Then we can call makeKeyAndOrderFront: to display the window:

(#/makeKeyAndOrderFront: my-window nil)
    

The window, empty, but with the shape and features we specified, appears on the left lower corner of the screen.

Adding a Button

Once we have a window on the screen, we might like to put something in it. Let's start by adding a button.

Creating a button object is as simple as creating a window object; we simply allocate one:

(setf my-button (#/alloc ns:ns-button))
#<NS-BUTTON <NSButton: 0x13b7bec0> (#x13B7BEC0)>
    

As with the window, most of the interesting work is in configuring the allocated button after it's allocated.

Instances of NSButton include pushbuttons with either text or image labels (or both), checkboxes, and radio buttons. In order to make a text pushbutton, we need to tell our button to use a button-type of NSMomentaryPushInButton, an image position of NSNoImage, and a border style of NSRoundedBezelStyle. These style options are represented by Cocoa constants.

We also need to give the button a frame rectangle that defines its size and position. We can once again use ns:with-ns-rect to specify a temporary rectangle for the purpose of initializing our button:

(ns:with-ns-rect (frame 10 10 72 32)
  (#/initWithFrame: my-button frame)
  (#/setButtonType: my-button #$NSMomentaryPushInButton)
  (#/setImagePosition: my-button #$NSNoImage)
  (#/setBezelStyle: my-button #$NSRoundedBezelStyle))
;Compiler warnings :
;   Undeclared free variable MY-BUTTON (4 references), in an anonymous lambda form
NIL
    

Now we just need to add the button to the window. This we do by asking the window for its content view, and asking that view to add the button as a subview:

(#/addSubview: (#/contentView my-window) my-button)
    

The button appears in the window with the rather uninspired title "Button". Clicking it highlights the button but, since we didn't give it any action to perform, does nothing else.

We can give the button a more interesting title and, perhaps more importantly, an action to perform, by passing a string and an action to it. First, let's set the button title:

(let ((label (%make-nsstring "Hello!")))
  (#/setTitle: my-button label)
  (#/release label))
;Compiler warnings :
;   Undeclared free variable MY-BUTTON, in an anonymous lambda form
NIL
    

The button changes to display the text "Hello!". Notice that we are careful to save a reference to the button text and release it after changing the button title. The normal memory-management policy in Cocoa is that if we allocate an object (like the NSString "Hello!") we are responsible for releasing it. Unlike Lisp, Cocoa does not automatically garbage-collect all allocated objects by default.

Giving the button an action is slightly more complicated. Clicking a button causes the button object to send a message to a target object. We haven't given our button a message to send, nor a target object to send it to, so it doesn't do anything. In order to get it do perform some kind of action, we need to give it a target object and a message to send. Then, when we click the button, it will send the message we specify to the target we provide. Naturally, the target object had better be able to respond to the message, or else we'll just see a runtime error.

Let's define a class that knows how to respond to a greeting message, and then make an object of that class to serve as our button's target.

We can define a subclass of NSObject to handle our button's message:

(defclass greeter (ns:ns-object)
  ()
  (:metaclass ns:+ns-object))
#<OBJC:OBJC-CLASS GREETER (#x13BAF810)>
    

We'll need to define a method to execute in response to the button's message. Action methods accept one argument (in addition to the receiver): a sender. Normally Cocoa passes the button object itself as the sender argument; the method can do anything it likes (or nothing at all) with the sender.

Here's a method that displays an alert dialog:

(objc:defmethod #/greet: ((self greeter) (sender :id))
  (declare (ignore sender))
  (let ((title (%make-nsstring "Hello!"))
        (msg (%make-nsstring "Hello, World!"))
        (default-button (%make-nsstring "Hi!"))
        (alt-button (%make-nsstring "Hello!"))
        (other-button (%make-nsstring "Go Away")))
    (#_NSRunAlertPanel title msg default-button alt-button other-button)
    (#/release title)
    (#/release msg)
    (#/release default-button)
    (#/release other-button)))
    

Now we can create an instance of the Greeter class and use it as the button's target:

(setf my-greeter (#/init (#/alloc greeter)))
#<GREETER <Greeter: 0x136c58e0> (#x136C58E0)>

(#/setTarget: my-button my-greeter)
NIL

(#/setAction: my-button (@SELECTOR "greet:"))
NIL
    

Now, if you click the button, an Alert panel appears.