< Previous PageNext Page > Hide TOC

Window and Control Tasks

This chapter describes how to implement windows and controls in your application using one of the following methods:

Several sections also describe commonly used Control Manager and Window Manager functions, as well as functions that reproduce features found in Interface Builder. In addition, this chapter includes implementation details about special features such as

In addition, if you are interested in learning more about the new object-oriented model that underlies all Carbon user interface elements, see “Introducing HIObject and HIView (Mac OS X 10. 2 and Later).”)

Note: Unless specifically indicated otherwise, all the functions described in this section belong to either the Window Manager or Control Manager.

In this section:

Using Interface Builder
Handling Events
Calling Functions to Create Windows and Controls
Basic Window Manipulation
Basic Control Manipulation
Control Implementation Examples
Custom Windows and Controls
Introducing HIObject and HIView (Mac OS X 10. 2 and Later)


Using Interface Builder

Interface Builder is Apple’s graphical user interface layout tool. In true WYSIWYG fashion, you simply drag user interface elements onto windows, menus, and controls to create your interfaces. This information is stored in a nib file, which your application can access using a few simple function calls.

Interface Builder has many advantages over other layout methods:

Important: Interface Builder is often associated with Project Builder, Apple’s development environment. However, you do not need Project Builder to take advantage of Interface Builder’s nib files.

You can use Interface Builder’s nib files even if you are working with legacy code. Applications can support both nib-based and older resource-based windows and controls at the same time, so you can make the transition as gradual as you like. Nib file support is available back to Mac OS 8.6 using CarbonLib.

Interface Builder is included on the Developer Tools CD available with Mac OS X.

Note: Although Interface Builder lets you create menus as well as windows and controls, this document focuses only on the latter elements.

Interface Builder also makes it easy to associate controls and windows with Carbon event handlers, again minimizing the amount of work required to implement your user interface.

The Nib File

Interface Builder stores all the information about your application’s windows, menus, and controls in a nib file (typically named filename.nib ). When creating a new file, Interface Builder gives you the option of selecting what type of nib file you want to create. When creating interfaces for Carbon applications, you should always select one of the Carbon options, as shown in Figure 3-1.


Figure 2-1  Opening dialog for new nib files

Opening dialog for new nib files

Interface Builder always displays the windows shown in Figure 3-2 for an open nib file.


Figure 2-2  Nib file windows

Nib file windows

If you chose to create a particular interface object (menu bar, dialog, or window) in the opening dialog, Interface Builder also creates empty versions of the objects.

For example, to create a simple dialog you can do the following:

Laying out your window is essentially as simple as dragging and placing the interface objects you want.

The Layout Palettes

For Carbon applications, Interface Builder provides five different layout palettes, which are displayed in the Carbon palettes window. If the window is not already open, you can do so by choosing Palettes from Interface Builder’s Tools menu.

Note: The Carbon layout palettes differ from those used for Cocoa applications.. Make sure that you select a Carbon-based nib when creating a new nib file.

The Menus Palette

The Menus palette allows you to build menus. Although it is not covered in this document, Interface Builder lets you build menus using the same simple drag-and-drop method you use for creating windows.

The Controls Palette

The Controls palette, as shown in Figure 3-3, contains the most commonly used controls.


Figure 2-3  The Controls palette

The Controls palette

The Enhanced Controls Palette

The Enhanced Controls palette, shown in Figure 3-4, contains more specialized controls, many of which are used in combination with other controls. For example, the separator lines are used to isolate controls from each other.


Figure 2-4  The Enhanced Controls palette

The Enhanced Controls palette

The PICT box is used as a container for an image you want to add to the interface. You use the custom box to place a custom (that is, application-defined) control. See “Custom Windows and Controls” for information about creating custom controls.

The Data Views Palette

The Data Views palette, shown in Figure 3-5, contains special controls that are specifically designed to organize information for the user in list or column format.


Figure 2-5  The Data Views palette

The Data Views palette

The table viewer and browser are subsets of the data browser control, while the tabs are simply a tab control paired with panes.

The Windows Palette

The Windows palette, shown in Figure 3-6, holds windows that you can use to interact with the user.


Figure 2-6  The windows palette

The windows palette

The standard window (containing the close, minimize, and zoom buttons) should be used for documents, dialogs, and any other windows that the user can close or minimize.

The buttonless window should be used for windows that must remain open while the application is running (such as a status window), or those that are dismissed in other ways, such as an alert.

The Info Window

Aside from the palettes, the Interface Builder window you will use the most is the Info window, which displays information about the currently selected object (such as a window or control) and lets you set attributes and other information that determine how the object behaves and appears to the user.

To display the Info window, choose the Show Info menu item in the Tools menu of Interface Builder.

Figure 3-7 shows an Info window for a window object.


Figure 2-7  The Info window for a window object

The Info window for a window object

The pop-up menu at the top of the Info window lets you choose between four different panes:

The available selections vary depending on which interface object you have selected.

The Layout Menu and the Aqua Guides

Although Interface Builder lets you manually drag and place interface components into your windows, it also provides tools for more precise placement.

The Alignment submenu in the Layout menu (as shown in Figure 3-8) lets you align groups of objects by their edges or centers.


Figure 2-8  The Alignment submenu

The Alignment submenu

For more sophisticated layout options, you can choose the Alignment panel (chosen from the Alignment submenu) as shown in Figure 3-9.


Figure 2-9  The Alignment panel

The Alignment panel

In addition, Interface Builder also provides special Aqua guides, which you can use to make sure your window conforms to the Aqua placement guidelines. For example, when dragging a control near the edge of a window, dotted blue lines appear indicating the proper placement for Aqua compliance. Releasing the control makes it snap to the specified placement. Figure 3-10 shows the Aqua guides suggesting the proper placement for a checkbox.


Figure 2-10  Aqua guides

Aqua guides

The Guides submenu in the Layout menu contains options for the Aqua guides.

Important: Although the Aqua guides can simplify layout of your windows and dialogs, you should not rely on them as a substitute for reading the Aqua human interface guidelines. The Aqua look-and-feel contains subtleties that the Aqua guides may not be able to predict.

Finally, there are additional commands available in the Layout menu that you might find useful:

Designing a Simple Preferences Window

This section gives a step-by-step example of how you would lay out a window in Interface Builder. While not particularly complex, the ideas and methods used here apply to any type of window.

This example creates a preferences window containing a few checkboxes and radio buttons, along with push buttons that allow the user to save or cancel the preferences.

Creating the Window

To create the initial window, you can either select a Carbon window or dialog when creating a new nib file (as shown in Figure 3-1) or drag a window from the Windows palette of an existing nib file. By bringing up the Info window and choosing the Attributes pane (using the pop-up menu), you can do the following:

In the Size pane, set the size of the window to be 175 pixels high and 480 pixels wide.

Finally, in the Instances pane of the nib file, double-click the text below the window’s icon and assign it a unique name (such as “GuitarPrefs”). This is the name that an application uses to load the window from the nib file.

Your simple dialog should now look like Figure 3-11.


Figure 2-11  An empty dialog

An empty dialog

Adding Controls

Most Preferences dialogs (and many dialogs in general) contain push buttons in the lower-right corner: OK to close the dialog, accepting any changes that were made, and Cancel, which closes without accepting changes. To add these buttons, drag two push button objects from the Controls palette to your window. If you have the Aqua guides turned on, they indicate the proper placement in the lower-right corner.


Figure 2-12  Using the Aqua guides to place push buttons

Using the Aqua guides to place push buttons

After placing the buttons, you can specify the following using the Attributes pane of the Info window:

In the Control pane, you can add the following:


Figure 2-13  Assigning a command ID from the Info window

Assigning a command ID from the Info window

Next you can add some preference controls. This example includes four different types of controls:

To view the dialog as it would actually appear in an application, choose the Test Interface command in the File menu. The completed preferences dialog appears in Figure 3-14.


Figure 2-14  A preferences dialog

A preferences dialog

Creating a Window From a Nib file

After you have created a nib file containing your windows, you can access them from your application.

Note that while a nib file can contain multiple windows, menus, and so on, to make the best use of resources, you may want to break up your user interface elements among several nib files. For example, you can put only the most commonly used windows in one nib file and the rarely used ones in another.

To make sure your application can find the nib file, you should place it in the Resources folder of your application’s bundle hierarchy, as shown in Figure 3-15. For information about creating application bundles, see Inside Mac OS X: System Overview.

Note: If your nib files contain localizable text, you should create separate nib files for each language you want to support. Each nib should be placed in the appropriate .lproj folder within the Resources folder.


Figure 2-15  The nib file in an application bundle

The nib file in an application bundle

Listing 3-1 shows how you use these functions to create a window.

Listing 2-1  Creating a window from a nib file

OSStatus err;
IBNibRef theNib;
WindowRef theWindow;
 
err = CreateNibReference (CFSTR("MyGuitar"), &theNib); // 1
if (!err)
    CreateWindowFromNib (theNib, CFSTR("GuitarPrefs"), &theWindow); // 2
 
ShowWindow(theWindow); // 3

Here is what the code does:

  1. The Interface Builder Services function CreateNibReference simply creates a nib reference that points to the specified file. In this case, the file is MyGuitar.nib (you don’t need to specify the .nib extension when calling this function). The CFSTR function converts the string into a Core Foundation string, which is the format that CreateNibReference expects.

  2. The Interface Builder Services function CreateWindowFromNib uses the nib reference to access a window within the nib file. The name of the window (GuitarPrefs in this example) is the name you assigned to it in the Instances pane of the nib file window. As with the CreateNibReference function, CreateWindowFromNib expects a Core Foundation string for the window name, so it must first be converted using CFSTR. The created window is stored as a window reference in theWindow.

  3. Windows are normally hidden when first created, so you should call ShowWindow to make them visible.

The complete window can now appear in your application. However, while the facade is there (and many of the controls are functional), this window does not do anything useful. To make the windows and controls do useful work, you must attach Carbon event handlers, which are described in detail in “Handling Events.”

Handling Events

After you have created your windows and controls, you need to make them functional, which means that they must be able to respond to events. To do so, you must install one or more Carbon event handlers. To get the most out of this section, you should be familiar with the workings of the Carbon Event Manager, as described in the document Inside Mac OS X: Handling Carbon Events.

This document assumes that you are installing your handler on the specific control or window it is intended to act upon, but this is not a requirement. The Carbon Event Manager lets you install your handlers anywhere up the containment hierarchy from your specified object.

Note: Some Apple technologies install additional event handlers to take care of basic window events. For example, beginning with Mac OS X version 10.1, the Multilingual Text Engine (MLTE) installs default handlers that take care of drawing the content region and content clicks, so you don’t have to call specific MLTE functions to accomplish these tasks within your own event handlers. These default handlers are added to the handler stack for the particular event when the MLTE object is created. See Inside Mac OS X: Setting Up MLTE to Use Carbon Events for more details.

You install your event handlers using the InstallEventHandler function:

OSStatus InstallEventHandler (EventTargetRef target,// 1
                                EventHandlerUPP handlerProc,// 2
                                UInt32 numTypes,// 3
                                const EventTypeSpec* typeList,// 4
                                void* userData,// 5
                                EventHandlerRef handlerRef);// 6
  1. The target parameter is an event reference indicating which object you want to install your handler on. You obtain an event reference by calling GetWindowEventTarget or GetControlEventTarget, passing in the appropriate window or control reference. Similar functions also exist for menu and application event targets (GetMenuEventTarget and GetApplicationEventTarget) respectively.

  2. The handlerProc parameter is a universal procedure pointer (UPP) to your event handling function. To convert a normal procedure pointer to a UPP, call the NewEventHandlerUPP function.

  3. The numTypes parameter indicates the number of events you want to register. If you don’t want to hard code this value, you can call the Carbon Event Manager macro GetEventTypeCount, passing in the array of events desired.

  4. The typeList parameter is an array describing the events you want to register. Each event is defined by its class (such as kEventWindowClass) and its kind (such as kEventWindowDrawContent).

  5. If you have any arbitrary data that you want passed to your handler, store it in the userData parameter. This data is passed to your handler each time it is called.

  6. If you want a reference to your installed event handler, pass a pointer here. On return, handlerRef contains a reference to your event handler.

The Carbon Event Manager includes macros that make it simpler to install event handlers by eliminating the need to create the event target reference. For example, the InstallControlEventHandler macro requires only that you pass the control reference where you would pass the event target reference in InstallEventHandler; the macro converts the control reference by calling GetControlEventTarget for you.

Listing 3-2shows how you would install a window event handler named MyWindowEventHandler for two events using the InstallWindowEventHandler macro.

Listing 2-2  Installing a window event handler

EventHandlerUPP myHandlerUPP;
 
EventTypeSpec eventList[] = {
                    {kEventClassWindow, kEventWindowDrawContent},
                    {kEventClassWindow, kEventWindowBoundsChanged}};
 
myHandlerUPP = NewEventHandlerUPP (MyWindowEventHandler);
 
InstallWindowEventHandler(theWindow,
                        myHandlerUPP,
                        GetEventTypeCount(eventList),
                        eventList, theWindow, NULL);

This example passes the window reference as user data.

Listing 3-3 shows a sample function to handle the events registered in Listing 3-2.

Listing 2-3  A sample window event handling function

static pascal OSStatus MyWindowEventHandler (
                            EventHandlerCallRef myHandler, // 1
                            EventRef theEvent, // 2
                            void* userData)// 3
{
    #pragma unused (myHandler)
 
    OSStatus result = eventNotHandledErr;// 4
    WindowRef theWindow = (WindowRef) userData;
    UInt32 whatHappened;
 
    whatHappened = GetEventKind (theEvent);// 5
 
    switch (whatHappened)
    {
        case kEventWindowDrawContent:// 6
 
            DoMyWindowDrawing (window); // dummy drawing function
            result = noErr;// 7
            break;
 
        case kEventWindowBoundsChanged:// 8
 
            DoMyWindowBoundsChange (theWindow); // dummy bounds function
            result = noErr;
            break;
    }
 
    return (result);
 
}

Here is what the code does:

  1. The Carbon Event Manager passes three parameters to your event-handling callback function. The myHandler parameter is a reference to the next event handler in the calling chain. That is, the event handler that will be called next if your handler chooses not to take this event.

  2. The theEvent parameter is an event reference; it points to an opaque data structure that describes the event that occurred.

  3. The userData parameter is the user data you specified when you registered your handler (in this case it contains the window reference).

  4. If your event handler chooses not to handle the event for any reason, you should return eventNotHandledErr to give other handlers in the calling chain a chance to take it.

  5. The GetEventKind function returns a constant that corresponds to the type of event that occurred. You can also call the related function GetEventClass to obtain the event class, but in this case, you know that it’s a window event.

  6. If the event kind indicates a draw content event, call your function to draw the window. See “The Drawing Event” for more information about what to do for this event.

  7. Set result to noErr to tell the Carbon Event Manager that you handled the event.

  8. If the event kind indicates that the window bounds changed, call your function to change the bounds. See “Window Bounds Changed Events” for more information about how to handle this event.

In this example, you receive the window reference as user data. However, you can also obtain parameters such as the window reference from the event reference. See “Event Parameters” for information on how to do this.

Event Parameters

Every event has event parameters associated with it. For example, when your application receives a kEventWindowActivated event, the event reference structure also contains a window reference indicating which window received the activate event.

Note: You can also pass information to your event handler using the userData parameter when you install your event handler. For example, when installing a window handler, you could store the window reference as user data. For events that don’t store the window reference in the event structure, this may be the only way to determine in which window an event occurred.

To obtain the associated parameters, you call the Carbon Event Manager function GetEventParameter, indicating which parameter you wish to obtain (see Listing 3-4 for an example). Often you want to specify kEventParamDirectObject, which indicates the object on which the event was directed. For example, for a window event, the direct object would be a reference to the window in which the event occurred.

For a list of the permissible parameters (and associated constants to pass to GetEventParameter), see the Carbon Event Manager documentation or the CarbonEvents.h header file.

Window Events

This section describes how to implement handlers to handle the most common window events:

Window Activation and Deactivation Events

When your window is activated or deactivated, it receives the events kEventWindowActivated and kEventWindowDeactivated respectively. The standard handler automatically activates and deactivates the title bar.

In response to the activation event, your handler must handle any requisite changes to the content region. For example, if your content region holds text, you may need to set the keyboard focus to the text field and begin blinking the insertion cursor. Any controls in the window receive their own activate events, so you do not need to handle them in your window activation handler.

For deactivate events, your handler should do the reverse of your activation handler. That is, stop blinking the cursor, relinquish keyboard focus, and so on.

Window Bounds Changed Events

Your window receives a kEventWindowBoundsChanged event when the user moves or resizes a window.

If the window is merely moved, the standard handler can handle the repositioning of both the title bar and the content region. If it is resized, usually because the user dragged the resize control or clicked the zoom button, the standard handler automatically resizes the title bar and the content region.

If the kEventWindowBoundsChanged event indicates the window size is changing, your handler should adjust the content region to reflect the new size. For example, you may need to expose or hide more of an image, or word wrap text to conform to the new size. Note that your handler should not redraw the new content. If the update region is nonempty, your application will receive a drawing event, and you can draw the content from that handler.

In Mac OS X you should constrain the resize to make sure that it does not overwrite the Dock. You should call the function GetAvailableWindowPositioningBounds to determine the largest allowable bounding rectangle for a given screen device (that is, the largest rectangle that does not overwrite the menu bar or the Dock):

OSStatus GetAvailableWindowPositioningBounds (
                                    GDHandle inDevice.
                                    Rect *availableRect);

To constrain the size of a window, you can get the kEventParamCurrentBounds parameter from the kEventWindowBoundsChanged event, modify the bounds and then replace the bounds parameter using the Carbon Event Manager function SetEventParameter.

If you want your window to support live resizing, you must specify the live resize attribute either in the nib file or by setting the kWindowLiveResizeAttribute bit in your application. If you do this, your application receives the kEventWindowBoundsChanged event whenever a kEventMouseMoved event is sent. By updating your content each time, the window then resizes on the fly.

To determine which action is occurring when the bounds changed event is sent (that is, whether the window is being resized or merely moved), you call GetEventParameter, specifying the kEventParamAttributes bit field as shown in Listing 3-4.

Listing 2-4  Obtaining parameter attributes for a kEventWindowBoundsChanged event

EventRef theEvent;
UInt32 Attributes;
OSStatus err;
 
err = GetEventParameter (theEvent, kEventParamAttributes, typeUInt32,
                            NULL, sizeof(UInt32), NULL, &Attributes);
if (!err)
    {
     if (attributes & kWindowBoundsChangeSizeChanged)
        {
        // Window is being resized
        }
     else if (attributes & kWindowBoundsChangeOriginChanged)
        {
        // Window is being moved
        }

If you want to intercept the resize or move before it actually begins, you should install your handler on the kEventWindowBoundsChanging event.

The Drawing Event

Whenever elements in your content region are hidden or shown, you must redraw those portions that are now visible. Typically you do so by creating a handler to handle the kEventWindowDrawContent event.

When you receive the kEventWindowDrawContent event, the standard handler has already called the QuickDraw functions BeginUpdate and SetPort, and it will call EndUpdate after you finish handling the event. These functions simplify the updating process by setting the visible region of the window to be the intersection of the visible region and the update region during the redraw. When your drawing handler executes, it automatically draws only those visible portions that have changed.

Note: In the rare case where you want to draw directly to into a window by modifying a port’s pixmap data (as opposed to using standard QuickDraw calls), you must call the QuickDraw function QDAddRectToDirtyRegion in your draw handle to tell the system that a portion of the window changed. Otherwise, your drawing is not guaranteed to appear.

If your update region contains any system-defined controls, the standard handler also calls DrawControls to redraw them before your handler is called.

If for some reason you want to call the BeginUpdate, EndUpdate, and SetPort functions yourself, you should register your handler to be called for the kEventWindowUpdate function instead.

Note: The standard handler for kEventWindowUpdate calls BeginUpdate ,SetPort, and (if necessary) DrawControls, then sends a kEventWindowDrawContent event to your window. After your handler handles the draw event, the standard handler then calls EndUpdate.

The Content Region Click Event

When the user presses in the content region of a window, your application receives a kEventWindowContentClick event. At this point, the mouse is still down, so typically you want to determine where the mouse press occurred and begin tracking. Doing so may require your application to present visual feedback, such as highlighting a selection or dragging an object. You do so using the Carbon Event Manager function TrackMouseLocation.

Note that if the initial mouse press occurred in a control, the events are sent to the control, not the window. In most cases, you can let the standard handler handle the tracking of the mouse in the control and take action only after the mouse is released.

The Window Close Event

The user can close a window by

In all of these cases, the window to be closed is sent a kEventWindowClose event. In response to this event your application should do the following:

The DisposeWindow function sends an additional event, kEventWindowClosed, before actually disposing of the window.

Note: The standard handler for the kEventWindowClose event automatically calls DisposeWindow; do not call DisposeWindow unless you are overriding the standard handler.

Control Events

This section describes some common control events. Note that in most cases, the standard handler (if it is installed on the owning window) automatically handles most of the work required to respond to these events. That is, the visual cues that accompany button presses, toggling, scroller and slider dragging, and so on, are taken care of for you.

Control events are of the class kEventClassControl.

Control Activation and Deactivation Events

The kEventControlActivate and kEventControlDeactivate events are analogous to the activation and deactivation events for windows. These events are sent to your control when ActivateControl or DeactivateControl is called on the control or any higher control in its embedding hierarchy.

For system-defined controls, the standard handler automatically redraws the control to reflect its new state (for example, graying out the control when deactivated.)

The Control Hit Event

When the user clicks a control, that control receives a kEventControlHit event. The standard handler can take care of the visual details (such as making a checkbox appear to toggle), but your application must address the consequences of the action. For example, if the user selects a checkbox, you must update your application to reflect the new state.

If you don’t need to take any action until the mouse is released, it is simpler to assign a command ID to the control and then install a handler for the kEventProcessCommand event. See “Command Events” for more details.

The Control Track Event (Mac OS X Only)

When the user presses and holds down the mouse to adjust a control (such as a pop-up menu or the scroller of a scroll bar), the Carbon Event Manager sends the kEventControlTrack event continuously while the mouse is down. The standard handler automatically adjusts system-defined controls (moving the scroller, highlighting a button, and so on), so in most cases you don’t need to register for this event.

The Control Draw Event

When a control needs to be redrawn, it receives a kEventControlDraw event. The drawing of system-defined controls is taken care of by the standard handler, so you need to register for this event only if you are using a custom control.

The Control Bounds Changed Event

When a control is resized or moved, the control receives a kEventControlBoundsChanged event. Typically this happens only when you move or resize an embedding control; that is, if you move a group box, the controls embedded within in receive a bounds changed event. For system-defined controls, the standard handler automatically takes care of scaling and redrawing the control, so you usually don’t need to take any additional action.

Note: You often want to resize or move your controls when the owning window changes size. For example, an editable text field often changes size to maintain a size proportional to that of its owning window. For such cases, your window handler should take the window bounds changing event and call SetControlBounds to move or resize the control accordingly.

Command Events

If you assigned a command ID to your control, your application is sent command events whenever the control is activated. Command events are of the class kEventClassCommand.

The Carbon Event Manager defines command ID’s for many common commands, such as OK, Cancel, Cut, Paste, and so on. You can also define your own for application-specific commands. You assign the command ID to a control in the Control pane of Interface Builder’s Info window, as shown previously in Figure 3-13. You can also call the Control Manager function SetControlCommandID.

Note: You can also assign command IDs to menu items by using Interface Builder or by calling the Menu Manager function SetMenuItemCommandID.

The kEventCommandProcess event (which is identical to the kEventProcessCommand event) indicates that your control was triggered. The actual command ID is stored within an HICommand structure in the event reference, so you must call the Carbon Event Manager function GetEventParameter to retrieve it, as shown in Listing 3-5.

Listing 2-5  Obtaining the command ID from the event reference

HICommand commandStruct;
UInt32 the CommandID;
 
GetEventParameter (event, kEventParamDirectObject, // 1
                    typeHICommand, NULL, sizeof(HICommand),
                    NULL, &commandStruct);
 
theCommandID = commandStruct.commandID;// 2

Here is what the code does:

  1. When calling GetEventParameter, you must specify which parameter you want to obtain. For command events, the direct object (kEventParamDirectObject) is the HICommand structure, which describes the command that occurred.

  2. The command ID of the control (or menu) that generated the event is stored in the commandID field of the HICommand structure.

Note that because command events may be triggered from either a control or a menu item, you may want to install your command event handler at the application level to make sure that the handler can take events coming from either location.

After handling a command, your application may need to change the state of a control or menu item. For example, after saving a document, the Save menu item should be disabled until the document changes. Whenever the status of a command item might be in question, the system makes a note of it. When the user takes an action that may require updating the status (such as pulling down a menu), your application receives a kEventCommandUpdate event. To make sure that the states of your controls and menus are properly synchronized, you should install a handler for the kEventCommandUpdate event. This handler should check the attributes bit of the command event to determine which items may need updating. Some examples of possible updates include

If the kHICommandFromMenu bit in the attributes field of the HICommand structure (shown in Listing 3-6) is set, then you should check the menu item in question to see if you need to update it.

Listing 2-6  The HICommand structure

struct HIComamnd
{
    UInt32  attributes;
    UInt32  commandID;
    struct
    {
        MenuRef         menuRef;
        MenuItemIndex   menuItemIndex;
    } menu;
};

Calling Functions to Create Windows and Controls

Interface Builder lets you easily create and lay out windows and controls. However, in theory your application can create and lay out windows and controls solely by calling Window Manager and Control Manager functions. In most cases, this method is more involved, requires much more work on your part, and makes your application much more difficult to localize. However, if you are working with large amounts of legacy code, familiarity with the programmatic methods of window and control creation may be useful.

Note: As the Mac OS has evolved, many functions were added to create and manipulate window and controls. Although many of these functions are still usable, this document describes only those that represent the preferred way of creating windows and controls today.

Creating Windows and Controls From Resources

Historically, windows and controls were stored as resources in the resource fork of an executable file. This storage method made it relatively easy to create and access these interface elements as well as to localize them. Today, data fork–based nib files provide the same easy accessibility while also providing the layout benefits of Interface Builder. However, if you have older legacy code that uses resources, you can still use them and call CreateWindowFromResource to add them into your application.

Important: For Carbon applications, resources should no longer be stored in the resource fork of your application. Instead, you should store them as data fork–based .rsrc files in the Resources folder of the application bundle. Localized nib files should go into the .lproj language folders within the Resources folder.

Window Creation Functions

To programmatically create a window, the preferred method is to call the function CreateNewWindow.

OSStatus CreateNewWindow (
                WindowClass windowClass,
                WindowAttributes attributes,
                const Rect * contentBounds,
                WindowRef * outWindow);

You specify the type of window you want in the windowClassand attributes parameters. The contentBounds parameter is a structure describing the global coordinates of the content region (that is, both the dimensions of the content region and its location onscreen).

While you probably would use nib files to create dialogs and other complex windows, CreateNewWindow is useful for creating windows that have no application-unique features. A good example would be a plain document window. Listing 3-7 shows how you can create one.

Listing 2-7  Creating a document window

 WindowRef         theWindow;
 WindowAttributes  windowAttrs;
 Rect              contentRect;
 CFStringRef       titleKey;
 CFStringRef       windowTitle;
 OSStatus          result;
 
 windowAttrs = kWindowStandardDocumentAttributes // 1
                       | kWindowStandardHandlerAttribute
                       | kWindowInWindowMenuAttribute;
 SetRect (&contentRect, kWindowLeft,  kWindowTop, // 2
                        kWindowRight, kWindowBottom);
 
 CreateNewWindow (kDocumentWindowClass, windowAttrs,// 3
                        &contentRect, &theWindow);
 
 titleKey    = CFSTR(kMyWindowTitleKey); // 4
 windowTitle = CFCopyLocalizedString(titleKey, NULL); // 5
 result = SetWindowTitleWithCFString (theWindow, windowTitle); // 6
 myErrorCheck (result);                              // Check for error
 CFRelease (titleKey); // 7
 CFRelease (windowTitle);
 
/* Add application-specific window initialization here *// 8
 
 RepositionWindow (theWindow, NULL, // 9
                    kWindowCascadeOnMainScreen);
 ShowWindow (theWindow); // 10

Here is what the code does:

  1. The windowAttrs parameter is a bit field that you can set with all the attributes you want for your window. This window has the standard document window controls, uses the standard window handler, and appears in the Window menu of the application.

  2. You specify the dimensions of the window and its location by setting a structure of type Rect, which contains the coordinates of the top left and bottom right corners of the window’s content region. The constants included here are simply examples (although you could define actual values for them in the file).

  3. When calling CreateNewWindow, you pass the window class of the desired window, its attributes, and its dimensions. On return theWindow contains a reference to the new window.

  4. The next several lines let you assign a localized title to the new document window. The myWindowTitleKey string is the name of the key that defines the title in your localized property list (plist) file.

  5. CFCopyLocalizedString gets the actual localized title string using the title’s key.

  6. After getting the title string, SetWindowTitleWithCFString sets the window title.

  7. You should dispose of your Core Foundation objects when you no longer need them.

  8. This is where you would add application-specific initializations for your window (such as registering event handlers, initializing the Multilingual Text Engine (MLTE), and so on.)

  9. Call RepositionWindow to specify where you want the window to appear onscreen. Passing kWindowCascadeOnMainScreen indicates that you want the window to appear on the main screen, offset to overlap the currently frontmost application window (this is the usual setting for document windows).

  10. Display the window.

Creating an Alert

To create a simple alert, you can call the Dialog Manager functions CreateStandardAlert and RunStandardAlert, as shown in Listing 3-8. This method is convenient for on-the-fly alert messages that require only minimal user interaction, such as to click on only the OK or Cancel buttons. Alerts created with these functions are automatically Aqua-compliant in look and placement.

Note: CreateStandardAlert and RunStandardAlert are available only in Mac OS X. If are writing a Mac OS 9–compatible Carbon application, you should use the function StandardAlert instead.

Listing 2-8  Creating a simple alert

DialogRef theItem;
DialogItemIndex itemIndex;
 
CreateStandardAlert(kAlertStopAlert, // 1
                CFSTR("Oh dear, the penguin’s disappeared."), // 2
                CFSTR("I hope you weren’t planning to open source him."),
                NULL, &theItem);// 3
 
RunStandardAlert (theItem, NULL, &itemIndex); // 4

Here is what the code does:

  1. When calling CreateStandardAlert, passing kAlertStopAlert specifies that you want the Stop alert icon to be used. Other possible constants you can pass are kAlertNoteAlert, kAlertCautionAlert, and kAlertPlainAlert. Your application icon is automatically added to the alert icon in accordance with the Aqua guidelines.

  2. The Core Foundation strings (created using CFSTR) specify the alert message you want displayed. The second string contains the smaller, informative text.

  3. If you have a custom parameter block describing how to create the alert, you would pass it here. Otherwise pass NULL. On return, theItem contains a reference to the new alert.

  4. RunStandardAlert displays the alert and puts the window in an application-modal state. When the user exits the alert (by clicking OK or Cancel), itemIndex contains the index of the control the user clicked.

Figure 3-16 shows the alert created by the code in Listing 3-8.


Figure 2-16  A simple alert

A simple alert

Creating and Displaying Sheets

A sheet is simply a window with the window class kSheetWindowClass or kAlertSheetWindowClass, and as such you can create one from a nib, from a resource, or by using a window creation function. You should attach event handlers to the sheet, just as any other window.

Here are some cases where you should use sheets:

Here are some cases when you should not use sheets:

Only one sheet should be open for a document at any time. If the user’s response to a sheet requires another sheet to open, you must close the first sheet before opening the second.

To display a sheet, you call the function ShowSheetWindow (analogous to the ShowWindow function used for other window types), passing the window references of the sheet to be displayed and the window to associate with the sheet. If you have installed the standard handler on the sheet, the window contents are automatically drawn before display; otherwise your sheet receives a kEventWindowDrawContent event requesting the same. The now-visible sheet is grouped with its parent window (so that they move and activate/deactivate together).

To remove a sheet, you call the HideSheetWindow function, which you typically do from one of the event handlers attached to the window. For example, you can call HideSheetWindow when the user clicks the OK button or otherwise signals that he or she is done with the sheet.

Note: Mac OS 9 does not support the visual look or window-modality of sheets. If you use ShowSheetWindow for a sheet class window in Mac OS 9 (using CarbonLib 1.3 or later), the sheet is displayed as a movable application-modal dialog or alert, depending on which sheet class (kSheetWindowClass or kAlertSheetWindowClass) you specified.

Creating a Sheet Alert

If you want to create a simple alert that appears as a sheet, you can call the function CreateStandardSheet. This function is analogous in format to the CreateStandardAlert function. However, it includes an additional parameter to specify an event target. When the user dismisses the sheet alert (by clicking OK or Cancel), the system sends a command event (kEventClassCommand, type kEventCommandProcess) to the specified event target. You can use this event to determine which control the user clicked.

To make the sheet alert visible, you call ShowSheetWindow, just as you would for any other sheet.

Creating Controls

The Control Manager has a creation function for each system-defined control, as shown in Table 3-1.

Table 2-1  Control creation functions

Control type

Creation function

Notes

Root control

CreateRootControl

Push button

CreatePushButtonControl or CreatePushButtonWithIconControl

Checkbox

CreateCheckboxControl

Radio button

CreateRadioButtonControl

Bevel button

CreateBevelButtonControl

You set the button behavior when calling the function. Additional bevel button functions exist to set the alignment of text or images within the button, set or obtain menu information, and so on.

Round button

CreateRoundButtonControl

Pop-up menu

CreatePopUpButtonControl

Scroll bar

CreateScrollBarControl

Slider

CreateSliderControl

List box

CreateListBoxControl

Scroll text field

CreateScrollingTextBoxControl

Progress indicator

CreateProgressBarControl

Chasing arrows

CreateChasingArrowsControl

Relevance control

CreateRelevanceBarControl

Static text

CreateStaticTextControl

Editable text field

CreateEditTextControl

Editable Unicode text field

CreateEditUnicodeTextControl

Mac OS X only

Icon control

CreateIconControl

Picture control

CreatePictureControl

Image well

CreateImageWellControl

Group box

CreateGroupBoxControl, CreateCheckGroupBoxControl, or CreatePopupGroupBoxControl

Radio group

CreateRadioGroupControl

Pane

CreateUserPaneControl

Tabs

CreateTabsControl

Disclosure triangle

CreateDisclosureTriangleControl

Disclosure button

CreateDisclosureButtonControl

Little arrows

CreateLittleArrowsControl

Separator lines

CreateVisualSeparatorControl

Data browser

CreateDataBrowserControl

This complex control also requires a number of additional configuration and manipulation functions.

Placard

CreatePlacardControl

Window header

CreateWindowHeaderControl

Clock

CreateClockControl

The clock automatically contains a little arrows control to adjust the date or time.

These functions let you specify all the attributes or options necessary for creating the appropriate control. In addition, most control creation functions require you to specify the bounds of the control. This is the bounding rectangle (specified by the Rect data type) that defines the position (in the window’s local coordinates) and size of the control. Note if the bounds you specify are smaller than the minimum control size, the control will exceed the requested bounds.

Functions That Correspond to Interface Builder Options

Most of the options for Carbon windows and controls are handled by calling Window and Control Manager functions. If you want to reproduce or modify functionality that you see in Interface Builder, you can call the underlying functions yourself. This section describes the correspondence between options found in the Interface Builder Info windows and their Control and Window Manager counterparts.

Window Options

Table 3-2 describes the correspondence between Interface Builder window options and Window Manager functions

Table 2-2  Window Manager functions for setting window options

Interface Builder Window Option

Window Manager equivalent

Title

SetWindowTitleWithCFString

Class

Set when you call CreateNewWindow. You can also call SetWindowGroup or GetWindowClass after creation. Do not use SetWindowClass.

Theme brush

Set using the Appearance Manager function SetThemeWindowBackground.

Position

Set when you specify the contentBounds parameter in CreateNewWindow. You can also pass window position constants (type WindowPositionMethod) to RepositionWindow.

Buttons

Attributes you can set when calling CreateNewWindow or by calling ChangeWindowAttributes.

Attributes

Attributes you can set when calling CreateNewWindow or by calling ChangeWindowAttributes.

Receives

Attributes you can set when calling CreateNewWindow or by calling ChangeWindowAttributes.

Size

Set in the contentBounds parameter when calling CreateNewWindow.

Help tags

Implemented by calling the Carbon Help Manager. See the Carbon Help Manager documentation for more details.

Control Options

Options unique to a control type (for example, specifying a determinate or indeterminate progress indicator) are usually specified in the control’s creation function. See Table 3-1 for the list of control creation functions.

Table 3-3 describes the correspondence between general Interface Builder control options and Control Manager functions.

Table 2-3  Control options

Interface Builder Control Option

Control Manager equivalent

Control ID

SetControlID

Signature

SetControlID

Command

SetControlCommandID

Enabled checkbox

EnableControl and DisableControl

Hidden checkbox

HideControl and ShowControl

Small checkbox

Pass kControlSizeSmall in SetControlData to specify the small control variant.

Size

Set in the inBoundsRect parameter of the particular control creation function. You can also set the size by calling SetControlBounds.

Help tags

Implemented by calling the Carbon Help Manager. See the Carbon Help Manager documentation for more details.

Basic Window Manipulation

This section describes some common window manipulation functions that you may want to use in your application. You can use these functions with windows created in any manner.

Window Activation

Activating a window typically brings it forward, gives it keyboard focus, and deactivates the previously active window. However, because of floating windows, the active window may not always be the frontmost window on the screen.

Finding Windows

The Carbon Window Manager provides a number of functions to find particular windows.

Window Visibility

At times you may want to change the visibility of your windows. For example, if the user closes a floating palette that is used often, it may be better to hide it rather than dispose of it, as doing so avoids the overhead of disposing of the window and recreating it later. On the other hand, keeping many windows available does use up memory, so use your judgment in determining which windows to hide and which to dispose of.

Changing the Modification State

When a document window is in an unsaved state, the close button should display a small dot in its center, and the proxy icon (if there is one) should be disabled. (Disabled proxy icons cannot be dragged because unsaved documents cannot be moved or copied in a manner predictable to the user.) You accomplish both of these tasks by calling the SetWindowModified function to change the modification state:

OSStatus SetWindowModified (WindowRef window, Boolean modified);

Ordering Windows

At times you may want to change the layering order of the windows in your application.

Cycling Through Windows (Mac OS X 10.2 and Later)

Beginning in Mac OS X version 10.2, the user should be able to cycle through the open document windows of an application by entering Command-tilde (~) to rotate forward, or Command-Shift-tilde to rotate backwards. The standard handler provides default support for these keyboard shortcuts, so you do not need to add any additional code.

Note: The standard handler for window cycling intercepts the kEventRawKeyDown Carbon event at the application level. If your application has its own raw key event handler, you should avoid processing the event before the standard handler can act on it.

If you use the standard window menu (see “The Standard Window Menu”), you can specify the option kWindowMenuIncludeRotate when calling CreateStandardWindowMenu to place a “Cycle Windows” menu item in the Window menu. Doing so makes window cycling an explicit, rather than hidden, feature in the application.

If you want to add menu items for window cycling, and you want to use the standard handler’s cycling code, you can assign the following command IDs to your menu items:

kHICommandRotateWindowsForward = 'rotw',
kHICommandRotateWindowsBackward = 'rotb'

The Standard Window Menu

Most applications that handle document windows should have a Window menu, which allows the user to choose among any of the currently open windows. The easiest way to implement this menu is to call the CreateStandardWindowMenu function:

OSStatus CreateStandardWindowMenu (OptionBits inOptions,
                                    MenuRef *outMenu);

Each open window that has the kWindowInWindowMenuAttribute attribute set automatically appears in the Window menu.

At times, the window title that shows up in the Window menu may not be expressive enough to identify the window. In those cases, you can set an alternate title for the window that will appear only in the Window menu by calling the SetWindowAlternateTitle function:

OSStatus SetWindowAlternateTitle (WindowRef inWindow
                                    CFStringRef inTitle);

To get an existing alternate window title, use the CopyWindowAlternateTitle function:

OSStatus CopyWindowAlternateTitle (WindowRef InWindow,
                                    CFStringRef *outTitle);

Window Reference Constants

If desired, you can assign arbitrary data to your windows by calling the SetWindowProperty function:

OSStatus SetWindowProperty (
                        WindowRef window,
                        PropertyCreator PropertyCreator,
                        PropertyTag PropertyTag,
                        UInt32 PropertySize,
                        void* PropertyBuffer);

You assign a creator code (typically the application’s signature) and a unique four-character tag to identify the data; this way you can associate multiple pieces of data with a window. To retrieve particular data, call the GetWindowProperty function:

OSStatus GetWindowProperty (
                        WindowRef window,
                        PropertyCreator PropertyCreator,
                        PropertyTag PropertyTag,
                        UInt32 bufferSize,
                        UInt32 *actualSize, // can be NULL
                        void *propertyBuffer);

If you need to set only one value, (such as a pointer to a data structure), you can also use the SetWRefCon function:

void SetWRefCon (WindowRef window, UInt32 data);

To retrieve the stored data, call the GetWRefCon function:

long GetWRefCon (WindowRef window);

Adding Window Proxy Icons

If you create a document window, you should add a proxy icon to the title bar. This icon, which appears next to the window’s title, serves as a proxy for the document’s icon in the Finder. This proxy icon should appear and behave the way the document's icon does in the Finder.

Your application can call the function SetWindowProxyCreatorAndType when you want to establish a proxy icon for a window, but the window's data has not yet been saved to a file. By passing SetWindowProxyCreatorAndType the creator and type of the file that the window is to contain, you can provide visual consistency with other windows that have saved files with the Finder. If the window's data has been saved to a file, your application can call the functions SetWindowProxyFSSpec or SetWindowProxyAlias to associate the file with the window and thereby establish the proxy icon.

Once a window has a proxy icon, the user should be able to manipulate it as if he or she were performing actions with a Finder icon for the window's file. For example, if a user drags a proxy icon to a folder, Finder window, the desktop, or another volume, the file represented by the proxy icon should be moved or copied accordingly, as if the user had dragged the file's icon in the Finder.

If your window has the standard event handler installed, then the Window Manager automatically handles any proxy icon dragging feedback. Note that standard handler sends you kEventWindowProxyBeginDrag and kEventWindowProxyEndDrag events so that you can modify or obtain information from the drag reference structure if you desire. For more detailed information about drag-and-drop actions, see the Drag Manager documentation.

Because a user can so readily use a proxy icon to manipulate a document file while the document is itself open, your application should be sure to synchronize the file data for all of its document windows on an ongoing basis. Because a proxy icon is much more prominent to a user than a Finder icon when the user is working in an open document, it is more likely that the user may move the file represented by the proxy icon while the document is open.

For example, if a user opens "My Document" in an application, then drags the proxy icon for "My Document" to a different folder, the application may still expect "My Document" to be in its original location. Additionally, the user may change the name of "My Document" to "Your Document" or place "My Document" in the Trash folder while "My Document" is open.

Note: See the document Programming with the Mac OS 8.5 Window Manager for an example of how you might search the file system to determine if document locations have changed.

If a document window contains unsaved changes, you should disable the proxy icon by calling the SetWindowModified function described in “Changing the Modification State.” Doing so prevents the user from dragging the proxy icon to another location. Your application should not disable the proxy icon at any other time.

Finally, when the user drags content that your application can accept into the content area of one of its windows, the window's structure region, including the proxy icon, should become highlighted. This gives visual feedback that the window is a valid destination for the content. Applications typically call the Drag Manager functions ShowDragHilite and HideDragHilite to indicate that a window is a valid drag-and-drop destination. If your application does not do this—that is, if your application implements any type of custom drag highlighting, such as highlighting more than one area of a window at a time—it must call the function HiliteWindowFrameForDrag.

Window Groups (Mac OS X Only)

The Window Manager supports the concept of window groups, which is essentially a way to associate windows together. Depending on which attributes you set for the group, windows in a group can share one or more of the following behaviors as if they were a single window:

Using window groups, you can link several different classes of window together. For example, you could lock a toolbar window to a document window so that when one moved the other would also move.

Each window group is associated with a layer in the window layering hierarchy. In fact, in Mac OS X, the Window Manager defines each of the standard window classes (document, floating, and so on) as a window group. Window groups can contain other window groups, so you can create hierarchies of groups.

A document window with a sheet is an example of a window group. The sheet (when visible) is locked to a particular document window, and it moves, collapses, and changes layers along with the document. Here are some other examples of when you might want to group windows together:

To create a window group, call the CreateWindowGroup function:

OSStatus CreateWindowGroup (
                    WindowGroupAttributes inAttributes
                    WindowGroupRef *outGroup);

The attribute bits you pass to CreateWindowGroup determine which grouping properties windows in the group will have. Table 3-4 shows the possible constants you can pass.

Table 2-4  Window group attribute bits

Constant

Description

kWindowGroupAttrSelectAsLayer

All windows in this group act as if they occupy the same layer. However, the user can change the ordering of the windows within the group.

kWindowGroupAttrMoveTogether

The positions of each window in the group with respect to each other is locked; moving one window also moves the others.

kWindowGroupAttrLayerTogether

The ordering of the windows within the group is locked; if the user attempts to bring one window forward, all the windows in the group are brought forward.

kWindowGroupAttrSharedActivation

All windows within the group activate and deactivate together.

kWindowGroupAttrHideOnCollapse

All windows within the group collapse into the Dock together. If any subgroup of this group has the kWindowGroupAttrHideOnCollapse attribute set, that group’s windows are collapsed as well.

For example, to obtain the proper behavior for a window displaying a sheet, you would group the window and sheet together specifying the following attributes:

Listing 3-9 shows an example of grouping two windows together, a toolbar window and a document window.

Listing 2-9  Grouping two windows together

WindowRef myDocWindow, myToolbar;
WindowGroupRef myGroup;
 
/* Create your two windows here and show them */
 
CreateWindowGroup (0, &myGroup);// 1
 
SetWindowGroupParent (myGroup, // 2
                GetWindowGroupOfClass (kDocumentWindowClass));
 
SetWindowGroup (myDocWindow, myGroup);// 3
SetWindowGroup (myToolbar, myGroup);
 
ChangeWindowGroupAttributes (myGroup,// 4
            kWindowGroupAttrMoveTogether | kWindowGroupAttrLayerTogether,
            0);

Here is what the code does:

  1. Call the CreateWindowGroup function to create a new group. The first parameter specifies any attributes you want to set (none in this case), and on return myGroup references the newly-created group.

  2. Assign a parent group for the new group. As described earlier, groups can contain other groups, and in this case you assign your group to the group containing document class windows. To obtain a group reference from a window class, you call GetWindowGroupOfClass.

    Note that currently you cannot assign a parent to a group if the group already contains windows.

  3. To add windows to your group, call the SetWindowGroup function. Note that you can only assign a window to one group. If the window is already in a group, SetWindowGroup removes the window from the old group and assigns it to the new one.

  4. Now set attributes for the window group by calling ChangeWindowGroupAttributes. The second parameter indicates attributes to set, the third indicates those to remove. In this case, you specify that the windows in the group should move together and layer together.

The resulting pair of windows acts as though they were a single document window.

If you wanted to change the window ordering within the group, you can call functions such as SelectWindow and SendBehind on individual windows.

Some other useful window grouping functions include the following:

Manipulating Drawers (Mac OS X 10.2 and later)

Drawers are a special class of window that appear to slide out from behind another window. You use drawers to hold commonly used items or controls that do not have to be visible all the time. For example, a drawer could hold bookmarks for a web browser, or mailboxes for an email program. Figure shows a drawer attached to a document window.

You create a drawer just as you would any other type of window, except that you specify that its window class be a drawer. That is, you specify a drawer when creating your window in Interface Builder (need to confirm this) or you pass kDrawerWindowClass for the window class when calling CreateNewWindow. When creating your drawer, you must also specify the window compositing attribute, kWindowCompositingAttribute.

In most cases you should specify the standard event handler when creating a drawer. You can then add controls to the drawer just as you would for any other type of window.

Note: Because the height and position of the drawer is determined by the parent window, the only aspect of the bounds you pass into CreateNewWindow that is used is the drawer width.

If you want the drawer to have the default background for drawers, call the Appearance Manager function SetThemeWindowBackground, specifying kThemeBrushDrawerBackground.

Listing 3-10 shows the code to set up and attach a simple drawer.

Listing 2-10  Displaying a simple drawer

WindowRef myDrawer, parentWindow;
Rect drawerBounds = {0,0,250,150};
 
/* Assume that the parent window exists already and is visible */
 
CreateNewWindow (kDrawerWindowClass, kWindowStandardHandlerAttribute |
                kWindowCompositingAttribute, &drawerBounds, &myDrawer);
 
SetThemeWindowBackground (myDrawer, kThemeBrushDrawerBackground, true);
 
SetDrawerParent(myDrawer, parentWindow);
SetDrawerOffsets (myDrawer, 0.0, 25.0);
 
ToggleDrawer (myDrawer);

By default, the edge of the parent window where the drawer appears is determined by the current script system. If the script reads left to right, the drawer appears on the left side. For right to left scripts (such as Arabic), the drawer appears on the right side. If there is not enough screen space for the drawer, the drawer appears on the opposite edge. If space is constrained on both sides, the drawer appears on the preferred side, but possibly obscured (for example, partly offscreen or hidden by the Dock).

Basic Control Manipulation

This section describes various control manipulation functions that you may want to use in your application.

Activating Controls

You can use the following functions to activate or deactivate your controls:

In general, a control’s active state should match that of its owning window (that is, when a window is active, all of its controls should likewise be active). However, the Control Manager does not enforce this matching; your application is responsible for maintaining this correspondence. The simplest way to do so is to activate or deactivate the window’s root control as the window’s state changes.

Note: In earlier versions of Mac OS system software, you would deactivate controls to make them nonfunctional in an active window. However, currently the preferred method is to disable them instead.

Enabling Controls

You can use the following functions to enable or disable your controls. Disabled controls are “grayed out" and do not receive user events.

You disable controls if you do not want them to function for a particular reason. For example, in the standard Save dialog, the Save button is disabled until the user enters a name for the file to be saved.

Showing and Hiding Controls

You use the following functions to show or hide a control:

Drawing Controls

In certain cases (such as when draw content events or update events occur) you may want to redraw your controls.

If you want to redraw all the controls associated with a window, call the DrawControls function:

void DrawControls (WindowRef theWindow);

In most cases, however, you want to redraw only those controls that were added to the update region. In response to the draw content event or update event, you should call UpdateControls:

void UpdateControls (WindowRef the Window, RgnHandle updateRegion);

If you want to redraw only a particular control (say a scroll bar after the user resized a window), you call the Draw1Control function:

void Draw1Control (ControlRef theControl);

Embedding Controls

All controls are embedded in one or more other controls, so you may need to use these functions to manipulate them properly:

Note: On Mac OS X (but not Mac OS 9 and earlier), you can embed controls from one window into another window. For example, you might want to create and store controls outside your current window and move them in as needed.

Control Implementation Examples

This section contains some examples of how to implement functions to handle the following tasks:

Live Scrolling

Scroll bars have been a part of the user interface since the first Macintosh computer. All Mac OS X applications should support live scrolling, meaning that the window’s contents should update on the fly as the user manipulates the scroll bars.

Scrolling and the Control Action Callback Function

You implement live scrolling by assigning a control action callback to your scroll controls. The callback function is called periodically as the user interacts with the scroll bar, giving your application the opportunity to update the contents of the window.

You register a callback for a control using the SetControlAction function:

void SetControlAction (ControlRef theControl,                         ControlActionUPP actionProc);

Note that you can attach control action callbacks to any type of control. Typically you use them whenever you need to do some sort of update on the fly. For example, a size-control slider could use an action callback to update the size of an image as it is moved.

The scrolling action itself is relatively straightforward if you understand conceptually what is happening to the window’s contents. The simplest way to look at scrolling is to imagine a window as the rectangular cutout in a sheet of opaque paper resting on a two-dimensional image, such as a photograph. Moving the horizontal scroller to the right moves the window to the right, exposing more of what is on that side. Moving the vertical scroller downwards effectively moves the window down.

The tricky part is translating the apparent movement of the window in a manner that you can implement in your application. For example, when displaying a picture onscreen, the window actually remains stationary while scrolling. Therefore, to accomplish the same relative movement across the picture, you must move the picture in the opposite direction. That is, as the user moves a scroller to the right, you must move the picture to the left.

Scrolling a Simple Picture

This section describes how to create a window containing an image that you can scroll vertically or horizontally. For this example, assume the following:

Listing 3-11 shows an initialization function that creates a window from a nib file, loads the picture, and then displays the picture in the window.

Listing 2-11  Creating a window and displaying a picture

/* First define various global variables */
PicHandle myPict;
Rect pictRect;
UInt32 picWidth, picHeight;
Point picOffset;
ControlActionUPP myLiveScrollerUPP;
 
ControlID vScrollID, hScrollID;
ControlRef horizontalScroller, verticalScroller;
 
UInt32 contentWidth, contentHeight;
 
void MyInitializeScrollWindow (void)
 {
 
    IBNibRef theNib;
    OSStatus err;
    WindowRef scrollWindow;
    Rect theBounds;
 
    err = CreateNibReference (CFSTR("simpleScroller"),&theNib);// 1
    CreateWindowFromNib(theNib, CFSTR("Scroller"), &scrollWindow);
 
/* Install your window event handlers here *// 2
 
    ShowWindow(scrollWindow);// 3
 
    hScrollID.signature = 'Moof';
    vScrollID.signature = 'Moof';
    hScrollID.id = 129;
    vScrollID.id = 128;
 
    GetControlByID (scrollWindow, &vScrollID, &verticalScroller);// 4
    GetControlByID (scrollWindow, &hScrollID, &horizontalScroller);
 
 
    myPict = GetPicture (10001); // get picture from resource// 5
 
    if (myPict == NULL) SysBeep(1);
 
 
    /* Get dimensions of the picture */
    picWidth = (**myPict).picFrame.right - (**myPict).picFrame.left;
    picHeight = (**myPict).picFrame.bottom - (**myPict).picFrame.top;
 
    SetPortWindowPort(scrollWindow);                    // 6
 
    GetWindowBounds (scrollWindow, kWindowContentRgn, &theBounds); // 7
 
    contentWidth = theBounds.right -theBounds.left - 15;// 8
    contentHeight = theBounds.bottom - theBounds.top - 15;
 
    SetControl32BitMaximum (verticalScroller,
                             picHeight - contentHeight);// 9
    SetControl32BitMaximum (horizontalScroller, picWidth - contentWidth);
 
    SetControl32BitMinimum (verticalScroller, 0);// 10
    SetControl32BitMinimum (horizonalScroller, 0);
 
    SetControlViewSize (verticalScroller, contentHeight);// 11
    SetControlViewSize (horizontalScroller, contentWidth);
 
    SetControl32BitValue (verticalScroller, 0);// 12
    SetControl32BitValue (horizontalScroller, 0);
 
    /* Assign initial offset of picture */
    picOffset.v = 0// 13
    picOffset.h = 0
 
    myLiveScrollerUPP = NewControlActionUPP (MyLiveScrollProc);
 
    SetControlAction (verticalScroller,myLiveScrollerUPP);// 14
    SetControlAction (horizontalScroller, myLiveScrollerUPP);
 
    MyDrawThePic (scrollWindow); // 15
 }

Here is what the code does:

  1. First obtain a nib reference and load the window from the nib file.

  2. In a real window initialization function, you would probably want to install any custom event handlers here. Live scrolling requires only the standard event handler.

  3. Display the window.

  4. Call the GetControlByID function twice to obtain the control references for the two scroll controls. These control references are stored globally. In a more flexible case, you may want to pass these references as parameters into your scrolling function.

  5. Obtain the image from a resource. The image is stored as a PICT resource in the data fork with (in this example) resource ID 10001.

  6. Call SetPortWindowPort to set the graphics port to be the content region of the window. Once set, all drawing is directed to this window, with the pixel coordinates (0,0) corresponding to the upper-left corner of the content region.

  7. Pass the kWindowContentRgn constant into GetWindowBounds to obtain a rectangle that defines the boundaries of the content region.

  8. Note that the content region obtained by GetWindowBounds also includes the area currently occupied by the scroll controls. Because you don’t want to draw over the controls, subtract the width of the scroll controls (15 pixels) to obtain the true dimensions of allowable drawing region.

  9. Call SetControl32BitMaximum twice to set the maximum allowable values for the scrollers (which are the indicators for the scroll bars). In this example, the values that the scrollers can take correspond to pixels. The maximum horizontal scroll value is the difference between the picture width (500 pixels) and the drawing region width (185 pixels). When the scroller value is 0, the window displays the leftmost portion of the picture. When the value is at its maximum (315), the window displays the rightmost portion. Similarly, the maximum vertical scroller value is the difference between the picture height and the drawing region height, or 215 pixels.

  10. Similarly, you call SetControl32BitMinimum twice to set the minimum scroller values to 0.

  11. Call SetControlViewSize to set the correct proportional scroller size. The value you pass indicates how much of the actual image is visible in the window, in terms of the units you specified in SetControl32BitMaximum. For example, because the units are pixels, the value to set for the horizontal scroller corresponds to the number of horizontal pixels that are visible in the window, which is the value of contentWidth.

    For resizable windows, you must update the sizes of the scrollers whenever the window dimensions change.

  12. Call SetControl32BitValue to assign initial values to the scrollers. Setting these to their minimum values means that the scrollers are positioned at the left and top of their respective scroll bars.

  13. Because the initial scroller values are 0, the initial picture offsets should also be 0. Note however, that as the scroller’s value is increased, the picture must be displaced in the opposite direction to create the illusion of movement.

  14. Using the control references obtained earlier, call SetControlAction for each scroll control to assign an action callback, MyLiveScrollProc. This callback is called whenever the user activates a scroll control.

  15. Call the function MyDrawThePic to draw the picture inside the window.

Listing 3-12 shows the implementation for the MyDrawThePic function, which draws the picture in the window. Much of the graphical manipulation relies on QuickDraw function calls. Their usage and functionality are described briefly, but if you want more details, you should consult the QuickDraw documentation available in Carbon Graphics and Imaging documentation.

Note: QuickDraw is deprecated in Mac OS X v10.4 and later. If you are building for Mac OS X v10.4 or later, you should implement drawing using Quartz instead.

Listing 2-12  The picture-drawing function

void MyDrawThePic (WindowRef theWindow)
{
    RgnHandle saveClip;
    Rect theBounds;
    CGrafPtr thePort;
 
    thePort = GetWindowPort(theWindow);// 1
    GetPortBounds(thePort, &theBounds);     // 2
 
    saveClip = NewRgn(); // allocate new region// 3
    GetClip (saveClip); // save old clip region// 4
 
    theBounds.bottom = theBounds.bottom -15;// 5
    theBounds.right = theBounds.right -15;
 
    ClipRect (&theBounds); // 6
 
    pictRect.left = picOffset.h ;// 7
    pictRect.top = picOffset.v;
    pictRect.right = pictRect.left + picWidth;
    pictRect.bottom = pictRect.top + picHeight;
 
    DrawPicture (myPict, &pictRect ); // 8
 
    SetClip (saveClip); // 9
    DisposeRgn (saveClip);
}

Here is what the code does:

  1. Call GetWindowPort to obtain the graphics port associated with the window. This CGrafPtr pointer references a structure containing information about the current drawing area.

  2. Calling the QuickDraw function GetPortBounds obtains the boundaries of the current drawing area (both in size and location). For system-defined windows, this area is equivalent to the window’s content region.

  3. Allocate space for a new clip region by calling the QuickDraw function NewRgn. The clip region is an arbitrary region within which drawing is allowed. For example, while the actual drawing region (as defined by the graphics port) may be arbitrarily large, the only drawing that actually appears onscreen is that which intersects the clip region.

  4. Only one clip region is defined at any time, so you must store the previously defined clip region in saveClip by calling the QuickDraw function GetClip. Doing so ensures that you don’t corrupt the clip region used by a different application.

  5. Reduce the obtained bounds for the content region by 15 pixels in each dimension to exclude the window’s scroll bars from the drawing area.

  6. Set the clip region for the window’s graphics environment to be the reduced bounds.

  7. Set the coordinates in which to draw the picture. These coordinates define a rectangle the size of the actual picture, offset from the local origin by an amount determined by the scroller values. For example, if there is no offset (because both scrollers are at their minimum values) then the top-left corner of the picture is at the window’s local origin.

  8. Draw the picture using the QuickDraw function DrawPicture , which is a simple way to draw images of type PICT. Because the clip region is confined to the area inside the window, only the portion of the picture within that region actually appears onscreen.

  9. After drawing, restore the old clip region using the SetClip function, and dispose of the memory used to store it by calling DisposeRgn.

Your drawing function may be more complex, but the basic principles still apply. Because the clip region defines the area within which the picture is visible, you change what portion is visible by moving the picture around “behind the window,” so to speak. Scrolling merely gives you a measured way to change the position of the picture.

Figure 3-17 shows the relative positions of the window and the image when the scroller values are 0.


Figure 2-17  Picture in window for the minimum scroll control values

Picture in window for the minimum scroll control values

Listing 3-13 shows the live scrolling callback function. This function takes two parameters indicating the control and the part of the control that activated the action callback.

Note that the standard event handlers take care of moving the scrollers, tracking the mouse, and any other feedback related to control itself; our scrolling callback function only has to adjust the position of the image based on the actions taken by the user.

Listing 2-13  The live scrolling callback function

static pascal void MyLiveScrollProc (ControlHandle control, SInt16 part)
{
    SInt16 currentValue, min, max, delta;
 
    currentValue = GetControl32BitValue (control);// 1
    min = GetControl32BitMinimum (control);
    max = GetControl32BitMaximum (control);
 
    delta = 0;// 2
 
    switch (part)
    {
        case kControlUpButtonPart:// 3
            if (currentValue >min)
                    delta = (currentValue-min < 5 ?
                                    -(currentValue-min): -5);
            break;
 
        case kControlDownButtonPart:
            if (currentValue < max)
                    delta =  (max - currentValue > 5 ?
                                    5 : max - currentValue);
            break;
 
        case kControlPageUpPart:// 4
            if (currentValue >min)
                    {
                    if (control == horizontalScroller)
                            delta = ( currentValue-min >contentWidth-1 ?
                                -(contentWidth-1) : -(currentValue-min));
                    else
                            delta = ( currentValue-min >contentHeight-1 ?
                                -(contentHeight-1) : -(currentValue-min));
                    }
            break;
 
        case kControlPageDownPart:
            if (currentValue < max)
                    {
                    if (control == horizontalScroller)
                            delta = (contentWidth-1 < max-currentValue ?
                                contentWidth-1: max-currentValue);
                    else
                            delta = (contentHeight-1 < max-currentValue ?
                                contentHeight-1 : max-currentValue));
                    }
            break;
    }
 
    if (delta != 0)
    {
        SetControl32BitValue (control, currentValue + delta);// 5
 
        if (control == horizontalScroller)// 6
                picOffset.h -= delta;
 
        if (control == verticalScroller)
                picOffset.v -= delta;
 
 
        MyDrawThePic(GetControlOwner(control));// 7
    }
 
    else if (part == kControlIndicatorPart)// 8
    {
        if (control == horizontalScroller)
                picOffset.h = -(currentValue - min);
 
        if (control == verticalScroller)
                picOffset.v = -(currentValue - min);
 
        MyDrawThePic (GetControlOwner (control));
 
    }
}

Here is what the code does:

  1. First obtain the values assigned to the control. The maximum and minimum values are those that were set in the MyInitializeScrollWindow function. The current value is determined by the position of the scroller.

  2. The delta variable indicates the distance (in pixels) to move the picture in response to the scroll bar manipulation. This value is also used to set the new value of the scroller.

  3. The switch statement assigns the delta value depending on which part of the scroll control was manipulated. The kControlUpButtonPart and kControlDownButtonPart constants refer to the scroll arrows. Clicking one of these arrows once scrolls the content by a small fixed amount. The incremental amount chosen for this example is 5 pixels. Note that if the scroller value is less than 5 away from its minimum or maximum, it delta is adjusted to be only the amount necessary to reach the endpoint.

  4. The kControlPageUpPart and kControlPageDownPart constants refer to the white track area of the scroll control within which the Aqua scrollers move. If the user has the scroll bar preference in the General pane of System Preferences set to to “Jump to next page, “ a click in the track area causes the content to shift by the corresponding window content dimension. For example, clicking above the vertical scroller moves the image up by the height of the window’s content region. A click below the scroller moves the image down by the same amount. As with the scroll arrows, in the end cases where a full page shift would take you beyond the end of the picture, delta is set to be only enough to reach the edge.

  5. If delta is not zero, add it to the appropriate scroller value (horizontal or vertical).

  6. Because the control value is in the same units as the offset (pixels), you also adjust the offset to reflect the new picture position. Note that you subtract delta from the offset because the control values and picture offset move in opposite directions. For example, as you increase the horizontal scroller value (that is, as the scroller moves to the right), the picture must move left to show the proper portion.

  7. Call the MyDrawThePic function to update the picture. Because you don’t currently have a window reference, use the function GetControlOwner to obtain one.

    Note that updating the entire picture each time is not necessarily the most efficient way to update the window contents. A more sophisticated application might determine how much of the picture has actually changed, move the picture using a function such as ScrollWindowRect or ScrollWindowRegion, ) and update only the portion that is new.

  8. The final scroll bar part that the user can manipulate is the scroller. Typically the user drags the scroller to a new position. In such cases, rather than changing the picture’s offset by an incremental amount, you simply set the offset based on the new value of the scroller and call MyDrawThePic to redraw the picture.

    If the user has the “Scroll to here” system preference set, a click in the track area moves the scroller to that point and sets the scroller value, just as if the user had actually dragged the scroller.

Figure 3-18 shows the relative positions of the window and the image when the horizontal and vertical scroller values are each set to 100.


Figure 2-18  Picture in window when scroller values are set to (100,100)

Picture in window when scroller values are set to (100,100)

Using Tab Controls

You typically use tab controls to switch between several panes. Interface Builder lets you place panes and tabs together as one unit. However, while each tab looks as though it is attached to a particular pane, this appearance is actually an illusion that you must implement using code in your control event handlers. This section gives an example of how to do this. For this example, assume the following:

First you should install an event handler on the tab control that determines which tab was clicked and then switch panes accordingly.

Listing 3-14 shows how you would install your tab event handler.

Listing 2-14  Installing the tab control event handler

ControlID   tabControlID;
ControlRef  tabControl;
EventHandlerUPP myTabHandlerUPP;
 
tabControlID.signature = 'Moof';// 1
tabControlID.id = 128;
 
static const EventTypeSpec tabControlEvents[] =// 2
{
    {kEventClassControl, kEventControlHit}
};
 
GetControlByID (theWindow, &tabControlID, &tabControl);// 3
 
myTabHandlerUPP = NewEventHandlerUPP (MyTabEventHandlerProc);// 4
 
err = InstallControlEventHandler (tabControl, // 5
                            myTabHandlerUPP,
                            GetEventTypeCount (tabControlEvents),
                            tabControlEvents,
                            theWindow, NULL);// 6

Here is what the code does:

  1. First, define the tab control’s signature and control ID. These should be the same values that you set for the tab control in your nib file.

  2. Define the events for which you want the tab event handler to be called. In this example, there is only one, the control hit event. The system sends this event whenever the user clicks the control.

  3. Call GetControlByID to get the control reference for the tab control. The theWindow parameter is a reference to the window containing the control. You would have obtained this reference when creating the window from the nib file (that is, when calling CreateWindowFromNib).

  4. When designating the tab event handler, you must call NewEventHandlerUPP on the function pointer to convert it to a universal procedure pointer.

  5. The function InstallControlEventHandler is the macro that combines the standard InstallEventHandler call with GetControlEventTarget so you don’t have to call the latter function yourself.

  6. You pass the window reference theWindow as arbitrary data to make it easy to obtain the control references for the panes.

The actual tab event handler, MyTabEventHandlerProc, determines whether the user has switched tabs. If so, it calls another function to switch panes. Listing 3-15 shows the event handler.

Listing 2-15  The tab control event handler

static pascal OSStatus MyTabEventHandlerProc(
                                EventHandlerCallRef inCallRef,
                                EventRef inEvent, void* inUserData )
{
    ControlRef          tabControl;
    static short        lastTabIndex;
    short               controlValue;
    WindowRef           window  = (WindowRef)inUserData;
    ControlID           tabControlID;
 
    tabControlID.signature = 'Moof';// 1
    tabControlID.id = 128;
 
    GetControlByID( window, &tabControlID, &tabControl );// 2
    controlValue = GetControlValue( tabControl );// 3
    if ( controlValue != lastTabIndex )// 4
    {
        MySwitchItemOfTabControl( window, tabControl, controlValue );
        lastTabIndex = controlValue;
        return (noErr);
    }
    else
    {
        return( eventNotHandledErr );
     }
}

Here is what the code does:

  1. Redefine the tab control’s signature and control ID. Alternatively, you could define the control ID in a global variable.

  2. Obtain the control reference for the tab control by calling GetControlByID. Note that you used the window reference passed into the event handler as custom data.

  3. Now that you have the tab control reference, you can obtain the value of the control. This value indicates which of the tabs the user selected.

  4. Compare the obtained tab value with the “current” tab value. If they are the same, that means that the user clicked the tab that was currently active, so you don’t need to do anything; the handler simply returns eventNotHandledErr. If they differ, then call the function to switch panes (mySwitchItemOfTabControl), update the current tab value, and then return.

Note that this event handler doesn’t have to call GetEventParameter to obtain any needed information; the value of the tab control is set automatically by the Control Manager when the user clicks a specific tab.

The actual function to switch panes is shown in Listing 3-16.

Listing 2-16  The tab switching function

static  void    MySwitchItemOfTabControl( WindowRef window,
                            ControlRef tabControl, short currentTabIndex )
{
    ControlID       controlID;
    ControlRef      userPaneControl;
    short           i;
    ControlRef      selectedPaneControl = NULL;
     int            tabList[]           = {2, 129, 130};// 1
 
    controlID.signature = 'Moof';
 
    for ( i=1 ; i<tabList[0]+1 ; i++ )
    {
        controlID.id    = tabList[i];
        GetControlByID( window, &controlID, &userPaneControl );// 2
 
        if ( i == currentTabIndex )// 3
            selectedPaneControl = userPaneControl;
        else
            SetControlVisibility( userPaneControl, false, true );
    }
    if ( selectedPaneControl != NULL )// 4
    {
        (void) ClearKeyboardFocus( window );
        SetControlVisibility( selectedPaneControl, true, true );
    }
 
    Draw1Control( tabControl );         //  Redraw the tab control itself
}

Here’s what the code does:

  1. The tabList array contains the number of tabs (two in this case) followed by the control IDs assigned to each of the panes (called user pane IDs in the nib file).

  2. Obtain the control reference for the pane indexed by the counter i.

  3. If the index is the same as that for the current pane, then save the corresponding control reference. Otherwise, call SetControlVisibility to set the pane visibility to false (hidden) and update the display (by passing true).

  4. If saved control reference is valid, then make that pane visible and update the display. If the previous pane had keyboard focus, you should clear it by calling the ClearKeyboardFocus function.

When you create the window with the tab controls, you should call your tab switching function to initialize the tab settings. Your code might look something like Listing 3-17.

Listing 2-17  Initalializing the tab control setting when creating the window

WindowRef   tabWindow;
ControlRef  tabControl;
ControlID   tabControlID;
 
tabControlID.signature = 'Moof';// 1
tabControlID.id =128;
 
CreateWindowFromNib (theNib, CFSTR("Tabs"), &tabWindow);
 
GetControlByID (tabWindow, &tabControlID, &tabControl);// 2
SetControlValue (tabControl, 1);
 
MySwitchItemOfTabControl (tabWindow, tabControl, // 3
                            GetControlValue (tabControl));
ShowWindow(tabWindow);
  1. First, set the tab control signature and ID so you can obtain the tab control.

  2. After calling GetControlbyID to obtain the tab control reference, call SetControlValue to set its index. This value corresponds to the first, or leftmost, tab.

  3. Call MySwitchItemOfTabControl to make the corresponding pane visible.

Custom Windows and Controls

For specialized applications you may want to create your own windows or controls. An example might be an immersive science-fiction game with a customized look-and-feel (that is, one that does not rely on Aqua).

Important: The system-defined controls and windows have a great deal of flexibility in implementing the Aqua user interface. You should not introduce new elements into the Aqua look-and-feel unless you are sure you need functionality that it does not provide.

The Carbon Event Manager lets you implement custom objects such as windows and controls by creating toolbox object classes for them. A toolbox object class is similar to a class in object-oriented programming, except that instead of methods you have event handlers to create and define your objects.

For example, when first creating a window, the Carbon Event Manager sends an event requesting that you draw the window outline. If a mouse click occurs in the window, an event is sent to determine what part of the window was hit, and so on. Your custom window is essentially a collection of event handlers that define its appearance and behavior.

Note: Mac OS systems before Mac OS X relied on message-based custom definitions. The principle was similar: You registered a callback function to handle your custom implementation, and the system could then call your function with specific message parameters that specified what was required. For a list of tables that map the older window and control messages to Carbon events, see “Carbon Events Versus Classic DefProc Messages”

The standard system-defined controls and windows are also described by event handlers. The only difference for custom objects is that you must supply the implementation code to handle the events.

You register your collection of event handlers as a custom object using the RegisterToolboxObjectClass function:

OSStatus RegisterToolboxObjectClass (
                            CFStringRef inClassID// 1
                            ToolboxObjectClassRef inBaseClass// 2
                            UInt32 inNumEvents// 3
                            const EventTypeSpec* inEventList// 4
                            EventHandlerUPP inEventHandler// 5
                            void* inEventHandlerData// 6
                            ToolboxObjectClassRef *outClassRef);// 7
  1. The inClassID parameter lets you assign a Core Foundation string to label this class of object. You must assign a unique class name; Apple suggests that you follow the standard naming convention com.CompanyName.Application.ClassName.

  2. If you are subclassing this object from another class, you can specify the base class in the inBaseClass parameter. In true object-oriented fashion, the new object retains the event handlers registered for the base class, although you can override them if you like.

  3. The inNumEvents parameter specifies the number of events you want to register for this object class.

  4. The inEventList array contains the events to register for this object. You define these just as you would for any other Carbon Event handler. In addition to your custom definition events, you should also install the usual event handlers required for the object (such as kEventWindowDrawContent for a window).

  5. The inEventHandler parameter is a universal procedure pointer to the event handler for this object class.

  6. The inEventHandlerData parameter contains any special data you want to pass to your event handler.

  7. On return, outClassRef contains a reference to the new object class.

Drawing Using the Appearance Manager

In cases where you want to add a specialized control or window to the standard interface, you can use Appearance Manager functions to ensure that whatever you draw matches the current theme.

For example, if you use the Appearance Manager function DrawThemeWindowFrame to draw your window’s frame region, then the frame automatically has the proper look for the type of window you specified. The function SetThemeWindowBackground ensures that your window’s background has the proper look for its type given the current theme. Similar functions exist for drawing controls or parts of controls. For example, even if you use nonstandard controls, you may want to use the Appearance Manager to draw focus rings, obtain the preferred font type and size, and so on. “Creating a Custom Control” uses a call to the Appearance Manager function DrawThemeButton to draw a bevel button.

The Appearance Manager also includes functions to obtain the fonts appropriate for a given theme. For example, you could call GetThemeFont passing kThemePushButtonFont and the appropriate script encoding to obtain the proper font for drawing push button text.

On Mac OS X, applications are automatically registered with the Appearance Manager, so you do not need to call RegisterAppearanceClient before calling Appearance Manager functions (but there is no penalty if you do).

For more detailed information, see Programming with the Appearance Manager in Carbon Human Interface Toolbox documentation.

Creating a Custom Window

To create a custom window, you must write handlers to respond to a number of different events. Table 3-5 lists the most common events.

Table 2-5  Custom window events

Event

Required Action

kEventWindowDrawFrame

Draw the frame region, including any controls within it.

kEventWindowDrawPart

Draw a specific part in the window, such as the close button. The part to be drawn is specified by the kEventParamWindowDefPart parameter.

kEventWindowPaint

Draw the frame region and the content region. In cases where you need to draw both at the same time, acting on this event is more efficient than handling both kEventWindowDrawFrame and kEventWindowDrawPart (specifying the content region).

kEventWindowGetRegion

Return the region specified by kEventParamWindowRegionCode in the kEventParamRgnHandle parameter. The window region codes are defined in MacWindows.h.

kEventWindowHitTest

Based on the mouse location passed into kEventParamMouseLocation, return the part hit (close button, size button, drag region, and so on) in the kEventParamWindowDefPart parameter.

kEventWindowInit

Perform any specific window initialization. Return the features of the window in the kEventParamWindowFeatures parameter. The window feature bits are defined in MacWindows.h.

kEventWindowDispose

Perform any cleanup required before disposing of the window. Note that you do not dispose of the window in this handler.

kEventWindowStateChanged

Any changes required to reflect the new state-change flag settings (passed in the kEventParamWindowStateChangedFlagsparameter. An example would be if the title changed for a document window.

kEventWindowDragHilite

Depending on the state of the Boolean kEventParamWindowDragHiliteFlag parameter, draw or erase any highlighting associated with a dragged object (such as when the user attempts to drag a selection into the window). Needed only if your window supports drag-and-drop.

kEventWindowModified

Redraw the window to reflect whether this document window is in its modified or unmodified state, depending on the value of the kEventParamWindowModififiedFlag parameter. For example, for Aqua document windows, if the content has been modified but not saved, the close button contains a small black dot.

Implementing the event handlers is the most involved part of creating a custom window; all the drawing, hit-testing, and similar manipulation that was previously performed by the Window Manager is now your responsibility.

Important: The events in Table 3-5 indicate handlers you should implement in addition to the usual window event handlers. Also, some of the usual handlers may need to accomplish additional tasks. For example, the kEventWindowDeactivated handler must now gray out or otherwise change the look of the window to indicate its inactive state.

Listing 3-18 shows the handler for a very simple custom window. It creates a 200 pixel x 200 pixel box with a 10 pixel wide black border which the user can drag around the screen.

Listing 2-18  A simple custom window event handler

static pascal OSStatus MyCustomWindowEventHandler (
                                EventHandlerCallRef myHandler,
                                EventRef theEvent, void* userData)
{
 
    #pragma unused (myHandler,userData)
 
    OSStatus result = eventNotHandledErr;
 
    UInt32 whatHappened;
    WindowDefPartCode where;
 
    GrafPtr thePort;
    Rect windBounds;
 
    whatHappened = GetEventKind (theEvent);
 
    switch (whatHappened)
    {
        case kEventWindowInit:
 
            GetEventParameter (theEvent, kEventParamDirectObject,                                typeWindowRef, NULL, sizeof(WindowRef),                                NULL, &newWindow);
            SetThemeWindowBackground (newWindow,                            kThemeBrushMovableModalBackground, true);// 1
            result = noErr;
            break;
 
        case kEventWindowDrawFrame:// 2
 
            GetPort (&thePort);// 3
            GetPortBounds (thePort, &windBounds);
 
            PenNormal();// 4
            PenSize (10,10);
            FrameRect (windBounds);// 5
 
            result = noErr;
            break;
 
        case kEventWindowHitTest:// 6
 
            /* determine what part of the window the user hit */
            where = wInDrag;
 
            SetEventParameter (theEvent, kEventParamWindowDefPart,// 7
                                typeWindowDefPartCode,
                                sizeof(WindowDefPartCode), &where);
 
            result = noErr;
            break;
    }
 
    return (result);
 
}

Here is what the code does:

  1. When your event handler receives the kEventWindowInit event, it should do any one time initialization. This example uses the Carbon Event Manager function GetEventParameter to obtain the reference to the new window, and then calls the Appearance Manager function SetThemeWindowBackground to set the window’s background pattern. The kThemeBrushMovableModalBackground specifies the gray striped background associated with Mac OS X dialogs and alerts.

  2. When your handler receives the kEventWindowDrawFrame event, it should draw (or redraw) the frame region of the window (That is, the area of the window that is not part of the content region). You can do so using QuickDraw or Quartz (Core Graphics) function calls. This example uses QuickDraw. While this document briefly describes the functions required, for more detailed information, you should consult the QuickDraw documentation.

    Note that prior to Mac OS X, if an object required a fair amount of drawing to render, you would first draw it offscreen and then copy the completed object to the screen when you are finished. (An offscreen graphics world is simply a drawing environment that is not part of the visible screen.) However, because Mac OS X automatically provides window buffering, you no longer need to draw offscreen. All drawing is directed to a window back buffer which is then copied to the screen as necessary. In fact, buffering the window yourself can actually affect performance.

  3. Call the QuickDraw functions GetPort and GetPortBounds to find the bounds of the current graphics world. These are the bounds you specify when calling CreateCustomWindow to create the window.

  4. The QuickDraw function PenNormal sets the drawing “pen” to its default state. In default mode, the pen size is 1x1 pixel and it draws in solid black. The QuickDraw function PenSize sets the pen size to be 10 pixels wide by 10 pixels high.

  5. The QuickDraw function FrameRect draws an outline of the specified rectangle using the current pen values.

  6. When your handler receives the kEventWindowHitTest event, it should check to see what part of the window the mouse is in. Your application receives this event whether the mouse is up or down. See Table 3-6 for a list of possible part constants. This example simply returns wInDrag indicating that the mouse is in the drag region, which means that the user can drag the window if the mouse is down. If your window uses the standard window handler, then the actual dragging is done for you.

    To determine the location of the mouse, you should call the Carbon Event Manager function GetEventParameter, specifying the kEventParamMouseLocation parameter.

  7. To return the part in which the mouse is located, call the Carbon Event Manager function SetEventParameter, setting the appropriate part constant in the kEventParamWindowDefPart parameter.

Table 2-6  Window part definition constants

Constant

Mouse location

wNoHit

No part hit.

wInContent

In content region

wInDrag

In the drag region

wInGrow

In the resize control

wInGoAway

In the Close button

wInZoomIn

In the Size button (for zooming in)

wZoomOut

In the Size button (for zooming out)

wInCollapseBox

In the Minimize button

wInProxyIcon

In the proxy icon

wInA

Used if you want to define a nonstandard region or control

wInB

Used if you want to define a nonstandard region or control

After you have created your window definition handlers, you register them as a toolbox object by calling RegisterToolboxObjectClass. Then, to instantiate one of your custom windows, you call the CreateCustomWindow function. Listing 3-19 gives an example of registering and creating a custom window.

Listing 2-19  Registering a toolbox object class and instantiating a custom window

ToolboxObjectClassRef customWindow;
WindowRef myWindow;
WindowDefSpec myCustomWindowSpec;
EventHandlerUPP myCustomWindowUPP;
Rect theBounds = {200,200,400,400};
 
EventTypeSpec eventList[] = {{kEventClassWindow, kEventClassWindowInit},// 1
                            {kEventClassWindow, kEventWindowDrawFrame},
                            {kEventClassWindow, kEventWindowHitTest}};
 
myCustomWindowUPP = NewEventHandlerUPP(MyCustomWindowEventHandler;
 
RegisterToolboxObjectClass ( CFSTR("com.myCompany.myApp.customWindow"),// 2
                        NULL, EventTypeCount(eventList), eventList,
                        myCustomWindowUPP), NULL, &customWindow);
 
myCustomWindowSpec.defType = kWindowDefObjectClass;// 3
myCustomWindowSpec.u.classRef = customWindow;// 4
 
CreateCustomWindow (&myCustomWindowSpec,kMovableModalWindowClass,// 5
                    kWindowStandardHandlerAttribute,
                    &theBounds,
                    &myWindow);
ShowWindow(myWindow);

Here is what the code does:

  1. You set up your EventTypeSpec array just as if you were specifying events for a system-defined window. The only difference is that you also want to register for the events required for custom windows. In this simple example, you register for only three events: kEventWindowInit for initialization, kEventWindowDrawFrame to draw the window’s frame region, and kEventWindowHitTest, to determine which part of the window the mouse is in

  2. Call RegisterToolboxObjectClass, passing your event list and specifying the UPP of the event handler to be called (MyCustomWindowEventHandler, in this case). The object class reference is stored in customWindow.

  3. The CreateCustomWindow function requires you to pass a window definition specification structure (type WindowDefSpec) which describes the window definition you are passing. The first field, defType, indicates the kind of window definition you are using. kWindowDefObjectClass specifies a toolbox object class. Other options are kWindowDefProcPtr, for a pointer to a window definition function, and kWindowDefProcID, indicating a resource-based window definition. Note that the latter two are supported mostly for legacy purposes.

  4. The WindowDefSpec structure also includes a union which, depending on the constant you passed in the defType field, can be either a toolbox object reference (as in this example), a pointer to a window definition function, or the ID of a window definition resource.

  5. The CreateCustomWindow function is similar to the CreateNewWindow function, except that you pass your WindowDefSpec structure in addition to the usual document class, window attributes, and window bounds. The window class you specify is partly determined by the type of window you defined, but it also defines other behaviors (such as in which window layer your custom window will appear). Specifying the standard window handler gives us some additional functionality, such as window dragging, for free.

    On return CreateCustomWindow gives you a window reference, which you can then manipulate as desired.

Drawing Using Quartz

Most of the examples in this document assume you are using QuickDraw to draw your windows. However, if you are targeting Mac OS X only, you can also use Quartz (sometimes called Core Graphics) to draw, or draw into, your windows.

Note: If you plan to use Quartz as your primary graphics engine, you should think about adopting the new HIView-based control model. See “Introducing HIObject and HIView (Mac OS X 10. 2 and Later)” for details.

Drawing using Quartz is similar to drawing with QuickDraw except that you draw into a Core Graphics context instead of a graphics port. The coordinate system is also different. Listing 3-20 shows how you would draw the rectangular custom window in Listing 3-18 if you were using Quartz.

Listing 2-20  Drawing a simple custom window with Quartz

GrafPtr thePort;
Rect windBounds;
 
CGContextRef theCGContext;
CGRect myCGRect;
case kEventWindowDraw:
 
        GetPort (&thePort);
        GetPortBounds(thePort, &windBounds);
 
        QDBeginCGContext(thePort, &theCGContext);// 1
 
        CGContextTranslateCTM(theCGContext, 0, // 2
                        (float)(windBounds.bottom - windBounds.top));
        CGContextScaleCTM(theCGContext, 1, -1);// 3
 
        myCGRect.origin.x = (float) windBounds.left + 5.0 ;// 4
        myCGRect.origin.y = (float) windBounds.top + 5.0 ;
        myCGRect.size.width =
                    (float)(windBounds.right - windBounds.left) - 10.0;
        myCGRect.size.height =
                    (float)(windBounds.bottom -windBounds.top) - 10.0;
 
        CGContextStrokeRectWithWidth (theCGContext, myCGRect,10.0);// 5
        QDEndCGContext(thePort, &theCGContext);// 6
 
        result = noErr;
        break;

Here is how the code works:

  1. After obtaining the window’s graphics port as usual, you then pass it into the QuickDraw function QDBeginContext to create the Core Graphics context.

  2. The Quartz coordinate system is different from that for QuickDraw, so for convenience you can transform the context to accept QuickDraw-style coordinates. Quartz assumes the origin to be at the bottom left of the context, while QuickDraw assumes the upper left. The Core Graphics function CGContextTranslateCTM function shifts all y coordinates up by the height of the context (equivalent to the height of the port).

  3. As increasing y coordinate values in Quartz move in the opposite direction to QuickDraw coordinates, you call the Core Graphics function CGContextScaleCTM to invert the coordinate system. Note that doing so means that any text you draw will be inverted!

  4. The myCGRect rectangle (essentially the Quartz equivalent of a QuickDraw Rect structure) defines the rectangular window to be drawn. Because the rectangle defines the center lines of the 10-pixel wide border, you need to “inset” the rectangle by 5 pixels on each side. The resulting rectangle then fills out to the edges of the context.

  5. Call the CGContextStrokeRectWithWidth function to draw the rectangle. Note that all Quartz functions take floating-point coordinates and values.

  6. After drawing, call the QuickDraw function QDEndCGContext to remove the graphics context. Any drawing you do after this call is in the QuickDraw graphics port.

For more information about Quartz, see Quartz 2D Programming Guide in Carbon Graphics and Imaging documentation.

Creating a Custom Control

You create custom controls in a manner similar to creating custom windows: you write event handlers to take care of drawing and manipulating your control, and then you register your control as a custom toolbox object. However, the variety of control events available is much larger, because controls have a much wider range of functionality than windows. One thing you can assume, however, is that you must now implement more control functionality that was previously handled by the standard handler. Table 3-7 indicates additional steps your custom control must take for the common control events described in “Control Events.”

Table 2-7  Actions required for common control events

Event

Required Action

kEventControlActivate

Draw your control to reflect its active state

kEventControlDeactivate

Gray out or otherwise change the appearance of your control to reflect its inactive state.

kEventControlHitTest

Based on the mouse location passed into kEventParamMouseLocation, return the part hit (indicator, inactive, or disabled part, application-defined part, and so on) in the kEventParamControlPart parameter.

kEventControlHit

Make changes to the control to reflect the state after it was hit (for example, putting a check in a checkbox).

kEventControlTrack

Provide any visual feedback required while the mouse is down. For example, you must move scroll bars, highlight or unhighlight buttons, and so on. Even if you are using a control action callback to perform live updates, you should handle any control-related changes during this tracking event. Note that you may not need to handle this event for simple controls (see Listing 3-21 for an example).

kEventControlDraw

Draw your control or (if the kEventParamControlPart parameter is present) a specific part of your control.

kEventControlBoundsChanged

Resize your control to reflect its new size. The Control Manager will send you a kEventControlDraw event to redraw the control (assuming that it is visible).

In addition, Table 3-8 lists some additional control events that your custom control may need to handle.

Table 2-8  Additional custom control events

Event

Required Action

kEventControlInitialize

Perform any initializations required before creating the control.

kEventControlDispose

Perform any required cleanup before the control is disposed. Note that you do not dispose of the control in this handler.

kEventControlSimulateHit

Provide visual feedback as if the control was actually hit. You normally receive this event in response to some other action, such as a Return key being hit as a default button.

kEventControlSetFocusPart

Change the keyboard focus to reflect what was sent in the kEventParamControlPart parameter.

kEventControlGetFocusPart

Set the kEventParamControlPart parameter to the part that currently has the keyboard focus.

kEventControlGetPartRegion

Set the region handle passed in the kEventParamControlRegion parameter to the region of the part specified in the kEventParamControlPart parameter.

Listing 3-21 shows a handler for creating a very simple custom control. This example simply handles the hit test, draw, and hit control events, and relies on the standard handler to take care of mouse tracking. It uses Appearance Manager functions to draw the actual control.

Listing 2-21  A simple custom control handler

static pascal OSStatus MyCustomControlHandler (
                                    EventHandlerCallRef myHandler,
                                    EventRef theEvent, void* userData)
{
    #pragma unused (myHandler, userData)
 
    OSStatus result = eventNotHandledErr;
    UInt32 whatHappened;
 
    ControlRef theControl;
    Rect controlBounds;
    ControlPartCode whatPart;
    UInt16 hiliteState;
    RgnHandle controlRegion;
 
    Point mouseLocation;
 
    ThemeButtonDrawInfo myButtonInfo;
 
 
    myButtonInfo.state = kThemeStateActive;// 1
    myButtonInfo.value = kThemeButtonOff;
    myButtonInfo.adornment = kThemeAdornmentDefault;
 
    hiliteState = 0;
 
    whatHappened = GetEventKind (theEvent);
 
    switch (whatHappened)
    {
        case kEventControlHitTest:// 2
 
            GetEventParameter (theEvent, kEventParamDirectObject,
                                typeControlRef, NULL, sizeof(ControlRef),
                                NULL, &theControl);
            GetEventParameter (theEvent, kEventParamMouseLocation,// 3
                                typeQDPoint, NULL, sizeof (Point), NULL,
                                &mouseLocation);
 
            GetControlBounds(theControl, &controlBounds);
            GetThemeButtonRegion (&controlBounds,// 4
                            kThemeRoundedBevelButton,
                            &myButtonInfo, controlRegion);
 
            if (PtInRgn(mouseLocation, controlRegion) == true// 5
                    whatPart = 5; // == kSomeAppDefinedPart
                else
                    whatPart = kControlNoPart;
 
            SetEventParameter (theEvent, kEventParamControlPart,// 6
                                typeControlPartCode,
                                sizeof(ControlPartCode),
                                &whatPart);
 
            result = noErr;
            break;
 
        case kEventControlDraw:// 7
 
            GetEventParameter (theEvent, kEventParamDirectObject,// 8
                                typeControlRef, NULL, sizeof(ControlRef),
                                NULL, &theControl);
 
            GetControlBounds (theControl, &controlBounds);// 9
 
            hiliteState = GetControlHilite (theControl);// 10
            if (hiliteState !=0)// 11
                    myButtonInfo.value = kThemeButtonOn;
                else
                    myButtonInfo.value = kThemeButtonOff;
 
            DrawThemeButton (&controlBounds, kThemeRoundedBevelButton,// 12
                             &myButtonInfo,NULL, NULL, NULL,0);
 
            result = noErr;
            break;
 
        case kEventControlHit:// 13
            SysBeep (1);
            result = noErr;
            break;
    }
 
    return (result);
}

Here is what the code does:

  1. When calling the Appearance Manager function DrawThemeButton, you must pass a ThemeButtonDrawInfo structure describing the state of the button. The kThemeStateActive constant indicates the control is active. kThemeButtonOff indicates that the button is in its off (that is, unpressed) state. kThemeAdornmentDefault specifies that you want the default look for the type of button.

  2. Whenever the Control Manager needs to know which part of the control the mouse is in, it sends the kEventHitTest event. Your application should determine the part the mouse is in and then return a part code in the event reference. Note that you receive the hit test event whenever the user presses the mouse within the control’s bounding rectangle; if the control does not fully occupy the rectangle (for example if it is round), it is possible that the control itself was not hit.

  3. First, use GetEventParameter to obtain the control reference and the mouse location.

  4. After obtaining the control bounds, pass this value into the Appearance Manager function GetThemeButtonRegion. This function returns the area of any standard Appearance-compliant button as a region. You specify the button type and a button info structure (which you filled out in step 1.)

  5. Now that you have both the mouse location and the region of the button, call the QuickDraw function PtInRgn to determine if the mouse point is within the control. If so, set the part code to be some nonnegative value in the range 1-127. Note that the actual part code can be arbitrary, depending on your control. For example, if your control has multiple states, you can assign specific values for each. If the control has several subparts, you can assign values specifying each subpart as well as highlight states for each subpart. Table 3-9 shows some standard constants defined for control parts. (Apple reserves all negative part codes.) If the mouse is outside the control, set the part code to kControlNoPart.

    Note that if you knew that your control entirely filled the control bounds (that is, the control was rectangular), you could use the QuickDraw function PtInRect to determine if the mouse was within the bounds without having to obtain a region.

  6. Use the SetEventParameter function to return the part code in the event reference. The value you pass is set as the control’s highlight value, which you can access later from your drawing routine.

  7. When the kEventControlDraw event occurs, you must draw your control. This example draws an Appearance-compliant bevel button within the control bounds.

  8. Call the Carbon Event Manager function GetEventParameter to get the control reference.

  9. With the control reference, you can call GetControlBounds to get the bounds of your control.

  10. Call the GetControlHilite function to get the highlight state of the button. This value is simply the part code you passed back in your hit test routine. While some controls may have several states, a simple push button has only two: the unpressed state, and the highlighted (pressed) state.

  11. If the button is to be in its highlighted state, change the value in the button info structure to specify that it should be drawn in its “on” state.

  12. Call the Appearance Manager function DrawThemeButton to draw your button. You pass the bounds within which to draw the control, a constant specifying the type of button, and the button info structure, just as you did for the GetThemeButtonRegion function.

    Note that while the bounds you pass are the “base” bounds used to draw the control, the control may not always be wholly within those bounds. For example, a control’s drop shadow is drawn outside of the bounds. If the bounds are too small, much of the control may appear outside of them. Some Appearance Manager calls actually draw entirely outside the specified bounds. For example, the DrawThemeWindowFrame function assumes the bounds you pass define the content region, so it draws the frame outside those bounds.

    DrawThemeButton also takes additional parameters (set to NULL in this example) that you may find useful:

    • A second ThemeButtonDrawInfo structure that describes the previously-drawn button. This state information is useful if you are drawing a control that uses transition animation, such as a disclosure triangle, to switch between states.

    • A universal procedure pointer to a custom erase function.

    • A universal procedure pointer to a custom draw function.

    The final parameter (set to 0 in this example) is for any custom data you may want to assign to the button.

  13. If the user releases the mouse while in the control, the Carbon Event Manager sends a kEventControlHit event to the control, and your handler can take appropriate action. This example simply beeps to indicate a hit.

Table 2-9  Control part constants

Constant

Mouse location

kControlNoPart = 0

Not in the control

kControlIndicatorPart = 129

In the indicator

kControlDisabledPart = 254

In a part that is disabled (the control is disabled)

kControlInactivePart = 255

In an inactive part (the control is deactivated)

Any other positive value

In an application-defined part

Custom Control Tracking

In Listing 3-21, all of the actual mouse tracking is handled by the standard event handler, and in most cases that is all you need. However, if you want more control over the tracking behavior, you must incorporate a kEventControlTrack event handler into your application. The basic event sequence that occurs during mouse tracking is as follows:

  1. When the user presses the mouse in a control’s bounding area (that is, the rectangle containing the control, that control receives a kEventControlHitTest event.

  2. The control then needs to determine what part of itself was hit, and return a part code (using the Carbon Event Manager function SetEventParameter). If no part of the control was actually hit (that is, the mouse-down occurred within the control’s bounding rectangle, but not actually within the control), the part hit should be kControlNoPart. This step is essentially implemented in Listing 3-21.

  3. If a valid part of the control was hit (that is, the part code was not kControlNoPart), the control then receives a kEventControlTrack event.

  4. Upon receiving the tracking event, the control must then track the mouse by calling the Carbon Event Manager function TrackMouseLocation or TrackMouseRegion. If you need to redraw your control in response to the tracking, you must do so within this handler.

    If your control has an action callback associated with it, the callback is called continuously during tracking.

    When mouse tracking ends, your tracking handler should call SetEventParameter to indicate which part of the control the mouse was in when it was released.

  5. If the mouse was released in a valid part of the control, the control is sent a kEventControlHit event indicating which part was hit. The handler for the control hit event can then take any required action.

Note:  The above sequence assumes that you are using the standard window handler. If you did not specify the standard handler, you must intercept the original kEventMouseDown event and perform the initial hit testing yourself. If the hit test was valid, you must call HandleControlClick on the control , which sends a kEventControlTrack event to the control. You can then handle steps 4 and 5 as before.

Listing 3-22 shows a template for what your control tracking event handler might look like:

Listing 2-22  A template for a custom tracking handler

static pascal OSStatus MyCustomControlTrackingHandler (
                                    EventHandlerCallRef myHandler,
                                    EventRef theEvent, void* userData)
{
    #pragma unused (myHandler, userData)
 
    OSStatus result = eventNotHandledErr;
    ControlPartCode whatPart;
 
    ControlRef theControl;
    Rect controlBounds;
    RgnHandle controlRegion;
    Boolean IsInRegion;
    MouseTrackingResult trackingResult;
 
    GetEventParameter (theEvent, kEventParamDirectObject,
                        typeControlRef, NULL, sizeof(ControlRef), NULL,
                        &theControl);
    GetControlBounds (theControl, &controlBounds);
    /* obtain a region based on the control bounds here *// 1
 
    trackingResult = kMouseTrackingMouseEntered;// 2
 
    while (trackingResult != kMouseTrackingMouseUp)// 3
        {
        switch (trackingResult)
            {
             case kMouseTrackingMouseEntered:// 4
                /* Mouse has entered the region */
                 break;
             case kMouseTrackingMouseExited:// 5
                /* Mouse has exited the region */
                 break;
 
             case kMouseTrackingMouseDragged:// 6
                /* Mouse moved while still down*/
                 break;
            }
         TrackMouseRegion (NULL, controlRegion, &isInRegion,
                         &trackingResult);
        }
    if (isInRegion == true)// 7
             whatPart = kMyPartCode; // application-defined
        else
             whatPart = kControlNoPart;
 
 
    SetEventParameter (theEvent, kEventParamControlPart,
                                typeControlPartCode,
                                sizeof(ControlPartCode),
                                &whatPart);
    result = noErr;
}

Here is what the code does:

  1. This tracking handler uses the Carbon Event Manager function TrackMouseRegion, which requires you to pass a region handle that indicates the valid control area. Typically you need to calculate a region based on the bounds of the control.

  2. You can assume that the initial state is that the mouse is down and within the control region (otherwise you would never have gotten the tracking event in the first place).

  3. The tracking loop continually calls TrackMouseLocation while the user keeps the mouse down and takes action based on what that function returns.

  4. When the tracking result is kMouseTrackingMouseEntered, the mouse has reentered the designated region. Typically you would highlight the control (or control part) when this occurs.

  5. When the tracking result is kMouseTrackingMouseExited, the mouse has left the region. You usually want to unhighlight your control (or control part) when this happens.

  6. When the tracking result is kMouseTrackingMouseDragged, the mouse has moved within the designated region while still being pressed. For simple controls such as buttons, you probably don’t need to handle this result, but if the region is a scrollbar or slider indicator, you should drag the indicator. Your control action callback should handle any changes to settings or content that result from this drag.

  7. After the user releases the mouse, you should return the part code indicating where the mouse-up occurred in the event reference by calling SetEventParameter.

Registering Your Custom Control

After you have written the handlers for your custom control, you register them as a toolbox object class, just as you would for a custom window. Listing 3-23 shows an example of registering MyCustomControlHandler.

Listing 2-23  Registering your custom control handler

ToolboxObjectClassRef customControl;
EventHandlerUPP myCustomControlUPP;
EventTypeSpec CEventList[] = {{kEventClassControl, kEventControlDraw},
                            {kEventClassControl, kEventControlHitTest}};
 
myCustomControlUPP = NewEventHandlerUPP (MyCustomControlHandler);
 
RegisterToolboxObjectClass (CFSTR ("com.Moof.MyApp.cntrl"), NULL,
                            GetEventTypeCount(CEventList), CEventList,
                            myCustomControlUPP, NULL, &customControl);

To create your control, you have two options:

The simplest way is to add a custom control to your nib file. You do so by dragging a Custom object from the Enhanced Controls palette (previously shown in Figure 3-4 to your window. The dimensions of the custom object should be the desired bounds of your control.

In the Attributes pane of the Info panel for the custom object, you can set the class ID of the toolbox object class corresponding to your control, as shown in Figure 3-19. You should specify an event-based control definition type.


Figure 2-19  Assigning the class ID and name for a custom control

Assigning the class ID and name for a custom control

If your control requires a title, you can set that in this panel. (Doing so is the same as calling the function SetControlTitle; your application can obtain the title by calling GetControlTitle). If your control has minimum, maximum, and initial control values, you can set these as well. Later, when you call CreateWindowFromNib from your application, your custom control is automatically created and placed within the window.

If you want to create your custom control within your application, you use the CreateCustomControl function as shown in Listing 3-24.

Listing 2-24  Creating controls using CreateCustomControl

ToolboxObjectClassRef customControl;
ControlDefSpec myCustomControlSpec;
 
WindowRef theWindow;
ControlRef myControl;
 
Rect controlBounds = {100, 100, 200, 300};
 
myCustomControlSpec.defType = kControlDefObjectClass;// 1
myCustomControlSpec.u.classRef = customControl;
 
CreateCustomControl (theWindow, &controlBounds, &myCustomControlSpec,// 2
                        NULL, &myControl);

Here is what the code does:

  1. The CreateCustomControl function requires you to pass a control definition specification structure to describe the type of control definition you want to use. This structure is analogous to the WindowDefSpec structure used in CreateCustomWindow. In this case, you set the defType field to kControlDefObjectClass to indicate an event-based toolbox object class. Then you set the classRef field of the union to specify the class reference you obtained from calling RegisterToolboxObjectClass.

  2. When calling CreateCustomControl, you pass

    • the reference of the window to contain the control (theWindow).

    • the bounds of the control, in the window’s local coordinates (controlBounds).

    • the control definition specification structure (myCustomControlSpec).

    • any specialized data to associate with the control. In this example you pass NULL to indicate no data.

    On return, myControl contains a reference to the newly-created control.

After you create the control, you still need to embed it within a root control or other control by calling EmbedControl or AutoEmbedControl.

Introducing HIObject and HIView (Mac OS X 10. 2 and Later)

HIObject is a common base class for all user interface objects. That is, for Mac OS X version 10.2 and later, all menus, windows, controls, toolbars, and so on, are subclasses of HIObject.

Essentially the HIObject model brings an object-oriented approach to the Mac OS Human Interface Toolbox, where the HIObject is the data store (instance) and the Carbon event handlers are the methods.

Note: HIObjects are actually Core Foundation–based types (type CFType) and as such are subject to CFRetain/Release and other functions.

Use of HIObject is entirely optional. Windows, controls (views), menus and so on are built on top of the HIObject base class, but you can continue to use the various toolbox managers to manipulate them. However, HIObjects make it easy to create custom objects as you can simply subclass them from the standard classes.

HIView is an object-oriented view system subclassed from HIObject. All controls are implemented as HIView objects ("views"). You can easily subclass HIView classes, making it easy to implement custom controls. Over time the HIView API will replace the current Control Manager.

Using the HIView model, every item within a window is a view: the root control, controls, and even the standard window "widgets" (close, zoom, and minimize buttons, resize control, and so on).

Current Control Manager function calls are layered on top of this HIView model and will be supported for the foreseeable future.

Additional benefits of the new HIView model include the following:

For more details, see the separate HIObject and HIView documentation grouped under HIToolbox on the Carbon Developer documentation site.



< Previous PageNext Page > Hide TOC


© 2002, 2005 Apple Computer, Inc. All Rights Reserved. (Last updated: 2005-07-07)


Did this document help you?
Yes: Tell us what works for you.
It’s good, but: Report typos, inaccuracies, and so forth.
It wasn’t helpful: Tell us what would have helped.