Creating Custom Controls on MacOS X Using HIViews

about | archive


[ 2003-August-10 18:48 ]

With the introduction of Jaguar, Apple introduced a new model for controls, the HIToolbox. This document describes how to use this new model to create custom controls. It assumes that you are familar with Apple's Carbon API. Here is what Apple has to say about it:

HIObject is the HIToolbox's base class for various objects. Over time, all of our common objects (controls, windows, menus, etc.) will be derived from HIObject. 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.

In short, the HIToolbox is the future API for MacOS X controls. The HIView class is very similar to Cocoa's NSView class, with a similar drawing and event model. In fact, they are so similar that I'm hoping (as are others) that NSViews and HIViews will be interchangeable in the future, allowing a tight integration between Objective-C (Cocoa) code and C/C++ (Carbon) code. Apple has now posted many examples of using the HIToolbox, mostly for creating custom controls, however this document is still useful as an overview, at least until Apple finalizes their documentation. Apple has also posted HIFramework, a library of C++ classes to make it easier to build HIView widgets in C++ programs.

Subclassing HIView

The first step in creating an HIView custom control is to create a subclass of HIView. This is done with the HIObject API, and is pretty well described in Apple's HIObject documentation. Here is a sample event handler:

static pascal OSStatus CustomControlHandler( EventHandlerCallRef handlerRef, EventRef event, void *userData )
{
#pragma unused (myHandler, userData)
    OSStatus result = eventNotHandledErr;

    ScintillaControl* scintilla = reinterpret_cast<ScintillaControl*>( userData );

    UInt32 whatHappened = GetEventKind (event);

    switch (whatHappened)
        {
        // Allocate the corresponding C++ object
        case kEventHIObjectConstruct:
            fprintf( stderr, "Constructing...\n" );

            /* If you want, you can get a reference to the instance:
            HIViewRef view;
            result = GetEventParameter (event, kEventParamHIObjectInstance,
                                        typeHIObjectRef, NULL, sizeof(view),
                                        NULL, &view);
            assert( result == noErr ); */

            // Allocate some instance specific information this will get passed into your
            // event handler as "userData" when events occur
            void* instance = NULL;
            result = SetEventParameter( event, kEventParamHIObjectInstance, typeVoidPtr, sizeof( instance ), &scintilla );
            assert( result == noErr );
            break;

        case kEventHIObjectDestruct:
            fprintf( stderr, "Destructing...\n" );

            // Free the instance data that you allocated in the constructor, for example:
            // delete userData;
            result = noErr;
            break;
        }
    return (result);
}

To register the new control and the events that it can handle, call HIObjectRegisterSubclass:

    const EventTypeSpec CEventList[] = {
        { kEventClassHIObject, kEventHIObjectConstruct },
        { kEventClassHIObject, kEventHIObjectDestruct },
    };

    OSStatus err = HIObjectRegisterSubclass( CFSTR( "someid" ), kHIViewClassID,
                                    0, NewEventHandlerUPP( CustomControlHandler ),
                                    sizeof( CEventList ) / sizeof( *CEventList ), CEventList,
                                    NULL, NULL );
    assert( err == noErr );

Adding a Custom HIView to a Window

Now that your custom class is registered, it is time to add it to the window. First, you need to enable compositing for the window. HIViews use the new Jaguar "composited" window rendering which is similar to Cocoa's rendering model. One important difference is that the background behind your view is always erased for you. Te enable composite rendering, check the "Composite" box in Interface Builder under the window properties. Next, you can use Interface builder to place your custom view widget in the window, as described in the Control Manager documentation. Alternatively, to manually add the view, you need to create an instance, then add it as a subview to the window's content view. The window's root view is the square frame that defines the window itself, including the title bar, resize handle and other decorations. The content view is the part of the window that shows your application. If you want your view to be drawn, do not forget to call HIViewSetVisible.

    // Create an instance of our custom class
    HIViewRef view;
    OSStatus err = HIObjectCreate( CFSTR( "someid" ), NULL, reinterpret_cast<HIObjectRef*>( &view ) );
    assert( err == noErr && view != NULL );

    // Get a reference to the content view
    HIViewRef contentView = NULL;
    err = HIViewFindByID( HIViewGetRoot( window ), kHIViewWindowContentID, &contentView );
    assert( err == noErr && contentView != NULL );

    // Add our view as a subview of the content view
    err = HIViewAddSubview( contentView, view );
    assert( err == noErr );

    // Make the new control visible
    err = HIViewSetVisible( control, true );
    assert( err == noErr );

Drawing in HIViews

As mentioned before, HIViews use Jaguar's composited window rendering, which is a fancy way to say that Quartz is the default rendering technology instead of QuickDraw. To draw the control, you obtain a CGContextRef from the event object. This Quartz context is already clipped to the control for you, with the origin in the top left corner and the y-axis increasing as you descend the screen. Note that this is different than standard Quartz rendering, where the origin is in the lower left corner of the screen.

    // Get a reference to the Quartz context
    CGContextRef gc = NULL;
    OSStatus err = GetEventParameter( event, kEventParamCGContextRef,
                                      typeCGContextRef, NULL, sizeof( gc ), NULL, &gc );
    assert( err == noErr );


    /* Do some drawing using Quartz. For example, to fill the control with red:
    CGRect controlFrame;
    HIViewGetFrame( view, &controlFrame );

    controlFrame.origin.x = 0.0;
    controlFrame.origin.y = 0.0;

    CGContextSetRGBFillColor( gc, 1.0, 0.0, 0.0, 1.0 );
    CGContextFillRect( gc, controlFrame );*/

References

HIToolbox
Apple's API documentation for HIView.
HIFramework
A library of C++ classes to facilitate creating HIViews.
HIToolbox examples
Apple's examples of using HIView.