The Human Interface Toolbox provides user interface elements (controls) for application developers to use in their programs. Mac OS X v10.2 Jaguar introduced Moving Mac OS 9 (or earlier) applications forward to the Mac OS X architecture can be a daunting task, in spite of the porting tools available. Old-style Event Manager calls should be updated to use the Carbon Event Manager, resources replaced with nibs, and so on. Custom controls should be rewritten to load from nib files and have their UPPs replaced with Carbon Event handlers.
Evolution and History of HIToolboxThe collection of user interface elements and routines known as the Toolbox has evolved significantly over the years. System 1.0 introduced the original Toolbox in 1984. The How does HIView AdvantagesHIView relies on the following Carbon/Mac OS X technologies, sharing the first with the Control Manager:
In addition, having an underlying object-oriented model means your application's user interface becomes easier to manage because controls inherit from a common ancestor, thus sharing functions and functionality and reducing the cerebral load on the programmer. The development cycle becomes faster because you will write less code to handle "normal" situations and can instead focus on the special cases. Less code also leads to faster compiles, so you can turn around updates sooner. What's New in HIView?
Mac OS X v10.3 Panther adds these types:
If you use pre-Carbon custom controls, you will find that the process for registering and creating your control is similar in spirit though not in syntax. Table 1 highlights some of the differences you will encounter. Table 1: Old vs. New Code Styles
Creating a Scrolling Text WindowUse the EmbeddedTextView (4MB) example to work with the finished project. Or, you can create a new Carbon application and open its nib file in Interface Builder, then follow these steps. Let's create a scrolling text editing window without writing any code.
Figure 1: Drag an HITextView onto the Application Main Window Figure 2: Embed the Text View in a Scroll View Figure 3: Set the Scroll View Bounds to Match the Window Figure 4: Bind the Scroll View to the Window Edges Figure 5: Scrolling "Just Works" The HITextViewShowcase sample code project contains both an Interface Builder example as shown above and a "code-only" version. Fetching and Displaying an ImageThe HIViewDemo (40MB) example
illustrates how standard controls easily coexist and interact
with Figure 6: Main Window Layout (Left) and ComboBox Attributes
The ComboBox (type
Listing 1 contains definitions for various constants and global variables. Note the Listing 1: Constant and Global Definitions enum { kComboBox = 'CBox', kFetchButton = 'Ftch', kImage = 'Imge', kCheckbox = 'Chck' }; const ControlID kComboBoxID = { kComboBox, 0 }; const ControlID kFetchButtonID = { kFetchButton, 0 }; const ControlID kImageID = { kImage, 0 }; const ControlID kImageOverlayID = { kImage, 1 }; const ControlID kCheckboxID = { kCheckbox, 0 }; WindowRef gMainWindowRef = 0L; Boolean gOverlayImageIsInited = false; The Fetch button, when clicked, reads the URL from the text field, then fetches the JPEG image at that URL. Listing 2 shows the registration of the Fetch event handler. The button is currently needed because of a bug in the combo box: pressing Enter after entering text or selecting a menu item does not trigger a unique event. Instead, we make the user press the Fetch button, and then retrieve the URL from the edit text area of the combo box. Listing 2: Registration of the Button Event Handler int main( int argc, char* argv[] ) { OSStatus err; int i = 0; EventTypeSpec eventTypes[ 10 ]; // max number of events to register ControlRef control; // snip // Get the button control err = GetControlByID( window, &kFetchButtonID, &control ); require_noerr( err, CantGetControlByID ); i = 0; eventTypes[ i ].eventClass = kEventClassControl; eventTypes[ i ].eventKind = kEventControlHit; err = InstallControlEventHandler( control, NewEventHandlerUPP( MyButtonHandler ), i + 1, eventTypes, NULL, NULL ); // snip CantGetControlByID: return err; }
The retrieved image is used to create a
The checkbox shows or hides a small overlay image (also an Figure 7: Setting the Alpha Value for The Overlay Image
Clicking the Fetch button starts the download and display
process. The event handler for the button first checks the event
type, as shown in Listing 3. A press and
release of the Fetch button constitutes a hit. The
handler first gets the URL text that was entered into the edit
text portion of the combo box by means of the menu or typing.
The URL text is used to create a Listing 3: The Button Event Handler OSStatus MyButtonHandler( EventHandlerCallRef inHandlerCallRef, EventRef inEvent, void *inUserData ) { OSStatus status = noErr; HIViewRef theView; ControlRef theControl; UInt32 kind; Size size = 512; status = GetEventParameter( inEvent, kEventParamDirectObject, typeControlRef, NULL, sizeof( ControlRef ), NULL, &theControl ); kind = GetEventKind( inEvent ); if ( kind == kEventControlHit ) { // Get the combo box control status = HIViewFindByID( HIViewGetRoot( gMainWindowRef ), kComboBoxID, &theControl ); CFStringRef urlString; status = GetControlData( theControl, kControlEditTextPart, kControlEditTextCFStringTag, sizeof( CFStringRef ), &urlString, &size ); CFURLRef urlRef = CFURLCreateWithString( kCFAllocatorDefault, urlString, NULL ); if ( urlRef != NULL ) { CGDataProviderRef source = CGDataProviderCreateWithURL( urlRef ); if ( source != NULL ) { CGImageRef image = CGImageCreateWithJPEGDataProvider( source, NULL, false, kCGRenderingIntentDefault ); if ( image != NULL ) { HIViewRef theImageView; status = HIViewFindByID( HIViewGetRoot( gMainWindowRef ), kImageID, &theImageView ); status = HIImageViewSetImage( theImageView, image ); CGImageRelease( image ); } CGDataProviderRelease( source ); } CFRelease( urlRef ); } CFRelease( urlString ); Because the overlay image is static, we check a global flag to see whether this is the first fetch, and if so, acquire the image using the same process just described. Note that the URL text is hardcoded at the top of this function. Refer to Listing 4. Listing 4: Loading the Overlay in the Button Event Handler if ( gOverlayImageIsInited == false ) { urlRef = CFURLCreateWithString( kCFAllocatorDefault, CFSTR( "file:///caption.jpg" ), NULL ); if ( urlRef != NULL ) { CGDataProviderRef source = CGDataProviderCreateWithURL( urlRef ); if ( source != NULL ) { CGImageRef image = CGImageCreateWithJPEGDataProvider( source, NULL, false, kCGRenderingIntentDefault ); status = HIViewFindByID( HIViewGetRoot( gMainWindowRef ), kImageOverlayID, &theView ); status = HIImageViewSetImage( theView, image ); gOverlayImageIsInited = true; CGImageRelease( image ); CGDataProviderRelease( source ); } CFRelease( urlRef ); } }
Next up is the checkbox. In Listing 5, we use standard Control Manager calls to get the reference to the checkbox, though Listing 5: Handling the Checkbox State status = GetControlByID( gMainWindowRef, &kCheckboxID, &theControl ); SInt32 isChecked = GetControl32BitValue( theControl ); if ( kControlCheckBoxCheckedValue == isChecked && gOverlayImageIsInited == true ) { status = HIViewFindByID( HIViewGetRoot( gMainWindowRef ), kImageOverlayID, &theView ); status = HIViewSetVisible( theView, true ); } else { status = HIViewFindByID( HIViewGetRoot( gMainWindowRef ), kImageOverlayID, &theView ); status = HIViewSetVisible( theView, false ); } } return status; } Figure 8 shows the application when it first launches. No images have been fetched. The horizontal separator across the center of the display area is for aesthetics only, and was added in Interface Builder. Figure 8: The Running Application Figure 9 illustrates a retrieved image with the semi-transparent caption overlaid on top. Figure 9: Overlaying The Caption on the Primary Image Performance ImprovementsApplication performance improves most noticeably in redrawing. With compositing enabled in the containing window, the rendering system calculates and redraws the smallest areas possible. Events dispatched through the Carbon application event loop go directly to their target event handlers, and only registered event types will be sent. Your application no longer needs to provide a centralized event dispatching loop covering all event types. This difference is illustrated in Listing 6. Listing 6: Classic vs. Carbon Event Handling From Classic: void handleEvent( void ) { while ( !gDone ) { Boolean isEvent = WaitNextEvent(...); if ( isEvent ) { switch ( event.what ) { case nullEvent: break; case mouseDown: handleMouseDown(); break; ... } } } } To Carbon: // Not shown: register desired events for this object. pascal OSStatus ViewHandler( EventHandlerCallRef inCaller, EventRef inEvent, void* inRefcon ) { OSStatus result = eventNotHandledErr; MyData* myData = ( MyData * )inRefcon; switch ( GetEventClass( inEvent ) ) { case kEventClassHIObject: switch ( GetEventKind( inEvent ) ) { case kEventHIObjectConstruct: ... break; case kEventHIObjectDestruct: ... break; default: break; } break; } return result; }
Layering controls becomes easier and more consistent using Listing 7: Embedding A View During The Initialize Event case kEventHIObjectInitialize: // snip if ( result == noErr ) { Rect itemRect = { 20, 20, 40, 220 }; ControlRef outControl; result = CreatePushButtonControl( NULL, &itemRect, CFSTR( "First Button" ), &outControl ); HIViewAddSubview( myData->view, outControl ); } break; When placed in a scroll view and registered to receive scroll events, the embedder scrolls in reaction to user clicks (Listing 8). Listing 8: Handling A Scroll Event case kEventScrollableScrollTo: // We're being asked to scroll, we just do a sanity check and ask for a redraw. HIPoint where; GetEventParameter( inEvent, kEventParamOrigin, typeHIPoint, NULL, sizeof( where ), NULL, &where ); HIViewSetNeedsDisplay( myData->view, true ); myData->originPoint.y = ( where.y < 0.0 ) ? 0.0 : where.y; HIViewSetBoundsOrigin( mydata->view, 0, myData->originPoint.y );
Drawing occurs in response to the Listing 9: Handling a Draw Event // Draw the view. case kEventControlDraw: CGContextRef context; result = GetEventParameter( inEvent, kEventParamCGContextRef, typeCGContextRef, NULL, sizeof( context ), NULL, &context ); if ( result != noErr ) { DebugStr( "\pGetEventParameter failed for kEventControlDraw" ); break; } HIRect bounds; HIViewGetBounds( myData->view, &bounds ); CGContextSaveGState( context ); CGAffineTransform transform = CGAffineTransformIdentity; // Adjust the transform so the text doesn't draw upside down. transform = CGAffineTransformScale( transform, 1, -1 ); CGContextSetTextMatrix( context, transform ); // snip (setup) // Now that the proper parameters and configurations have been dealt with, // let's draw. EmbedderViewDrawing( context, &bounds, myData ); CGContextRestoreGState( context ); result = noErr; break;
Currently only Listing 10: Handling Transparency In Conjunction With Shading typedef struct { float red; float green; float blue; float alpha; } CGRGB; OSStatus EmbedderViewDrawing( CGContextRef context, const HIRect * bounds, const EmbedderData * myData ) { HIRect FullBounds = { {0.0, 0.0}, {1000.0, kEmbedderDefaultHeight} }; // Alpha shading working only in Panther CGRGB inStartColor = {1.0, 0.0, 0.0, 0.8}, inEndColor = {0.0, 0.0, 1.0, 0.8}; ShadeRectColor( &inStartColor, &inEndColor, &FullBounds, context ); return noErr; } OSStatus ShadeRectColor( const CGRGB* inStartColor, const CGRGB* inEndColor, const HIRect* inRect, CGContextRef inContext ) { OSStatus err = noErr; CGColorSpaceRef colorSpace; CGFunctionCallbacks callbacks = { 0, ColorGradientEvaluate, NULL }; CGFunctionRef function; CGShadingRef shading; ColorShadeData data; // Warning: this stuff is sitting on the stack. Be careful if you move // the shading code around. data.start = *inStartColor; data.range.red = inEndColor->red - inStartColor->red; data.range.green = inEndColor->green - inStartColor->green; data.range.blue = inEndColor->blue - inStartColor->blue; data.range.alpha = inEndColor->alpha - inStartColor->alpha; CGContextSaveGState( inContext ); CGContextClipToRect( inContext, *inRect ); colorSpace = CGColorSpaceCreateDeviceRGB(); require_action( colorSpace != NULL, CantCreateColorSpace, err = memFullErr ); function = CGFunctionCreate( &data, // info 1, // domainDimension NULL, // input domain NULL == no range clipping 4, // rangeDimension, NULL, // output domain NULL == no range clipping &callbacks ); // CGFunctionCallbacks require_action( function != NULL, CantCreateFunction, err = memFullErr ); shading = CGShadingCreateAxial( colorSpace, inRect->origin, // start CGPointMake( CGRectGetMinX( *inRect ), CGRectGetMaxY( *inRect ) ), // end function, false, // extendStart false ); // extendEnd require_action( colorSpace != NULL, CantCreateShading, err = memFullErr ); CGContextDrawShading( inContext, shading); CantCreateFunction: CGShadingRelease( shading ); CantCreateShading: CGColorSpaceRelease( colorSpace ); CantCreateColorSpace: CGContextRestoreGState( inContext ); return err; } Custom ControlsCustom controls may easily be derived from The mechanics are straightforward: you provide an event handling function that responds to specific events (several of which are required) and then register your class at runtime. When the application creates an instance of your class, create and initialize events get passed to the instance allowing it to allocate necessary data structures and initialize values. After this, the instance is ready to respond to the control events for which it registered. Destruction of the object sends one more event, allowing your control to clean up. The HICustomPushButton example contains a simple custom push button control.
A control may create custom data that gets stored with the control instance. Incoming events contain a pointer to this data (as a typedef struct { HIViewRef view; // The HIViewRef for our button. } CustomPushButtonData; The control class ID identifies the control during registration and creation. Its value may be programmer-defined, but make sure it is unique. #define kCustomPushButtonClassID CFSTR( "com.apple.sample.dts.HICustomPushButton" ) The control must include an event handler. Note the last argument, which is the pointer to the custom data discussed above. pascal OSStatus ViewHandler( EventHandlerCallRef inCaller, EventRef inEvent, void* inRefcon ); CFStringRef GetCustomPushButtonClass();
You may find it convenient to register the control using a separate function. See Listing 11. This example contains "boiler plate" code that includes the required events during registration. You always need to handle the
Class registration using Listing 11: Class Registration CFStringRef GetCustomPushButtonClass() { // The following code is pretty much boiler plate. static HIObjectClassRef theClass; if ( theClass == NULL ) { static EventTypeSpec kFactoryEvents[] = { // The next 2 messages are required. { kEventClassHIObject, kEventHIObjectConstruct }, { kEventClassHIObject, kEventHIObjectDestruct }, // The next 3 messages are the actual minimum messages you need to // implement a simple custom push button: { kEventClassControl, kEventControlHitTest }, { kEventClassControl, kEventControlHiliteChanged }, { kEventClassControl, kEventControlDraw } }; HIObjectRegisterSubclass( kCustomPushButtonClassID, kHIViewClassID, 0, ViewHandler, GetEventTypeCount( kFactoryEvents ), kFactoryEvents, 0, &theClass ); } return kCustomPushButtonClassID; }
The event handler (refer to Listing 12 and following) should be able to process each type of event registered for the class. Note the cast of the The construct event must be handled. Here the custom data gets allocated, and then the superclass instance gets stored in the structure's view field. The new pointer must be pushed back into the event before returning. Nothing is lost, since inRefcon had no underlying data coming in to this call, which is the first event dispatched upon object creation.
Note: Do not call
Object destruction must also be handled. In this control simply deallocate the pointer. Note: your choice of allocation and deallocation methods depends on how you want to handle memory allocation, but they must match ( Listing 12: Handling Events in a Custom Control pascal OSStatus ViewHandler( EventHandlerCallRef inCaller, EventRef inEvent, void* inRefcon ) { OSStatus result = eventNotHandledErr; CustomPushButtonData* myData = ( CustomPushButtonData* )inRefcon; switch ( GetEventClass( inEvent ) ) { case kEventClassHIObject: switch ( GetEventKind( inEvent ) ) { case kEventHIObjectConstruct: // Allocate some instance data. myData = ( CustomPushButtonData* ) calloc( 1, sizeof( CustomPushButtonData ) ); // Get our superclass instance. HIViewRef epView; GetEventParameter( inEvent, kEventParamHIObjectInstance, typeHIObjectRef, NULL, sizeof( epView ), NULL, &epView ); // Remember our superclass in our instance data. myData->view = epView; // Store our instance data into the event. result = SetEventParameter( inEvent, kEventParamHIObjectInstance, typeVoidPtr, sizeof( myData ), &myData ); break; case kEventHIObjectDestruct: free( myData ); result = noErr; break; default: break; } // End switch ( GetEventKind() ) break; // End kEventClassHIObject Also handle the draw, hit test, and hilite changed events (Listing 13): Listing 13: Handling Events in a Custom Control case kEventClassControl: switch ( GetEventKind( inEvent ) ) { // Draw the view. case kEventControlDraw: CGContextRef context; HIRect bounds; result = GetEventParameter( inEvent, kEventParamCGContextRef, typeCGContextRef, NULL, sizeof( context ), NULL, &context ); if ( result != noErr ) { DebugStr( "\pGetEventParameter failed for kEventControlDraw" ); break; } HIViewGetBounds( myData->view, &bounds ); if ( ( !IsControlHilited( myData->view ) ) || ( !IsControlActive( myData->view ) ) ) CGContextSetGrayFillColor( context, 0.5, 0.3 ); else CGContextSetRGBFillColor( context, 0.1, 0.1, 1.0, 0.3 ); CGContextFillRect( context, bounds ); result = noErr; break; // Determine if a point is in the view. case kEventControlHitTest: HIPoint pt; HIRect bounds; // The point parameter is in view-local coords. GetEventParameter( inEvent, kEventParamMouseLocation, typeHIPoint, NULL, sizeof( pt ), NULL, &pt ); HIViewGetBounds( myData->view, &bounds ); if ( CGRectContainsPoint( bounds, pt ) ) { ControlPartCode part = kControlButtonPart; SetEventParameter( inEvent, kEventParamControlPart, typeControlPartCode, sizeof( part ), &part ); result = noErr; } break; // React to hilite changes by invalidating the view so that // it will be redrawn. case kEventControlHiliteChanged: HIViewSetNeedsDisplay( myData->view, true ); break; default: break; } // End switch ( GetEventKind() ) break; // End kEventClassControl: default: break; } // End switch ( GetEventClass() ) return result; } Finally, let's look at how to create an instance (Listing 14). In this code fragment, once the owning window gets created (not shown) an instance of the custom class gets instantiated, positioned in the window's content view, and made visible.
The second parameter to Listing 14: Creating an instance WindowRef theWind; // Create Window here (not shown). HIObjectRef hiObject; theStatus = HIObjectCreate( GetCustomPushButtonClass(), 0, &hiObject ); if ( theStatus != noErr ) { DebugStr( "\pHIObjectCreate failed" ); goto show_the_window; } // Place the view into the Window content view. root = HIViewGetRoot( theWind ); HIViewFindByID( root, kHIViewWindowContentID, &contentView ); HIViewAddSubview( contentView, ( HIViewRef )hiObject ); // Position the view. HIViewSetFrame( ( HIViewRef )hiObject, &frame ); // Views are initially invisible, so make it visible. ShowControl( ( HIViewRef )hiObject ); For More Information
|