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.