This tutorial is about implementing a custom control class. The goal is not to explain how to subclass NSControl or NSCell, but how to think more generally about controls, what they are and how to implement one with an NSView subclass. As with the last tutorial, it will be most useful if you download the example project and follow along in the source code. The comments will provide the implementation details. It’s also fun to give Asimo a pompadour.
The tutorial is based on a similar presentation that I gave at a New York City CocoaHeads meeting a few months ago.
What is a Control?
A control is an interface element that allows users to manipulate data in an application. It’s main responsibilities are to draw itself, handle events, and communicate its values to other parts of the application.
Cocoa’s AppKit framework comes with several controls out of the box:
All of the controls pictured above are decedents of Cocoa’s control class, NSControl. As the documentation states, NSControl works closely with NSCell to provide the basic features of a user interface object. In most cases, you can use an AppKit control with no extra code.
Why Make a Custom Control?
With all of AppKit’s ready-made controls, why would you need to implement your own? Here are two situations where the Cocoa control classes are not a good choice for your interface:
1. None of the AppKit controls support the interaction model you want to implement
Take Photoshop’s curves interface as an example. This interface requires that the user is able to add control points to a bezier curve. They must then be able to select the control points and drag them around to adjust the values of the curve. If they click on the pencil button, the interface must switch to a mode that lets them draw the curve directly into the view.

You’re not going to find an out of the box control that supports this interface in AppKit and NSControl may not be an appropriate place to start your custom implementation. If you look at its API, it is clear that NSControl was designed to create the AppKit controls. They all have fairly straightforward drawing, event handling and target/action needs. Point-and-click. If you were to use NSControl as a starting point for this interface, you would need to create a few custom cell classes that you would manage in your control’s custom cell. At this point, you’re asking for a headache. This interface is not what NSControl was designed for. You’re going to spend more time fighting with the framework than implementing your ideas.
2. You are drawing with OpenGL instead of Quartz
All of the controls in the iTunes Cover Flow interface are drawn with OpenGL textures. That includes the buttons, scrollbar and slider. These are absolutely not NSControls, which can only draw in a Quartz graphics context. If you want users to interact with your NSOpenGL view, you’re going to have to start from scratch with your controls. Don’t let this scare you away from using OpenGL views for your interface. They’re fast fast fast and if your app is displaying lots and lots of images, especially in a full screen situation with animation, they’re the way to go.
Leopard’s LayerKit (Core Animation Layers) technology advertises mixed Quartz and OpenGL drawing, but I just have a hard time getting behind that idea. More on this thought another time.
Using NSView
The subject of creating controls for an OpenGL view is a little out of the scope of this tutorial. Again, I will come back to the issue of OpenGL views in the not-too-distant future.
Working with NSView will let us touch on the very basics of what we need to make a control. For really specialized, one-off interfaces that you can add to your window’s view hierarchy with no fuss, it makes a lot of sense to start here.
As I mentioned before, there are three parts to a control. Let’s make a to-do list for our class design. Our view must be able to handle:
- Drawing
- Event Handling
- Communication with other parts of your app
We’ll handle number one with NSView and number two with its superclass, NSResponder. For number three, we’ll use Cocoa bindings.
What Are we Controlling?
Before we subclass anything, we need to figure out what our control will control. The example project contains a control for adjusting the values of a twirl distortion CoreImage filter. To start the design, I looked at the documentation for CITwirlDistortion. It lists the three attributes of the filter: center postition, radius and angle. I decided to create a circular design for controling all three attributes at once. This seemed to be a nicer interface than providing three sliders. Of course this design has some usability issues that will be obvious once you play around with it a bit, but it’s fun to experiment.
KDTwirlDistortionControl has this design:
It’s basically a circle. Users can drag on the main gray part of the circle to adjust its radius - this will change the radius attribute of the filter. Dragging on the blue center point will change the position of the circle and the center position attribute of the filter. Finally, dragging the small black circle around its perimeter will adjust the angle attribute of the filter.
We have an interface design. Time for code.
Defining the NSView subclass
Our NSView subclass, KDTwirlDistortionControl, needs to keep track of three rectangles (pictured in the diagram) that it will use for drawing and hit detection. These rectangles will also be used to calculate the adjustment values of the filter. It declares the following ivars for the rectangles in the header file:
// drawing and hit detection
NSRect mPositionControlRect;
NSRect mAngleControlRect;
NSRect mRadiusControlRect;
The control also contains 4 float values that can be bound to. It does this by declaring the following ivars and their getters and setters:
// the values we bind to
float mPositionX; // a value between 0 and 1
float mPositionY; // a value between 0 and 1
float mRadius; // a value between 0 and 1
float mAngle; // a value between 0 and 6 radians
The getters and setters will always return normalized values for these ivars, which are calculated based on the positions and sizes of the rectangles in the view’s coordinate system. Look in the code for more details about this part of the implementation.
With the getters and setters in place, our control can communicate changes to these values to other classes with no more code. We can check number three off of our to-do list. Thanks Cocoa.
The last thing our control needs to manage is its state. There are four possible states in our design:
- the control is inactive
- the position rectangle is active
- the radius rectangle is active
- the angle rectangle is active
We’ll use this information when we draw to give the user feedback about what they are adjusting. We’ll also use it in our mouse drag handler so that we know which rectangle the user is adjusting. To define the states of the control, create an enum type like this:
typedef enum
{
kKDTwirlDistortionControlState_Inactive = 0,
kKDTwirlDistortionControlState_AngleControlActive,
kKDTwirlDistortionControlState_RadiusControlActive,
kKDTwirlDistortionControlState_PositionControlActive
}KDTwirlDistortionControlState;
The view subclass declares an ivar that it will use to keep track of its current state:
// state
KDTwirlDistortionControlState mControlState;
This is all the data we need for our control. Now we will use NSView’s drawing and NSResponder’s event handler methods to implement the control.
Drawing
All we need to do to draw in an NSView subclass is to override the following NSView method:
- (void)drawRect:(NSRect)theRect;
To keep things organized, KDTwirlDistortionControl uses separate drawing methods for each part of the control. It also creates a separate method to lay out the rectangles in the view. It declares them as private methods in a category in the .m file:
@interface KDTwirlDistortionControl (Private)
- (void)drawPositionControlInContext:(CGContextRef)theContext;
- (void)drawAngleControlInContext:(CGContextRef)theContext;
- (void)drawRadiusControlInContext:(CGContextRef)theContext;
- (void)layoutControls;
@end
KDTwirlDistortionControl’s drawRect method looks like this:
- (void)drawRect:(NSRect)theRect
{
CGContextRef aCGContextRef = [[NSGraphicsContext currentContext] graphicsPort];
// draw the background
CGContextSetRGBFillColor(aCGContextRef, 0.8, 0.8, 0.8, 1.0);
CGRect aCGBackgroundRect = *((CGRect*)&theRect);
CGContextFillRect(aCGContextRef, aCGBackgroundRect);
// layout the controls
[self layoutControls];
// draw the radius control rect
[self drawRadiusControlInContext:aCGContextRef];
// draw the position control rect
[self drawPositionControlInContext:aCGContextRef];
// draw the angle control rect
[self drawAngleControlInContext:aCGContextRef];
}
If you look at the draw methods, you’ll notice that before they draw, they check the control state ivar to determine what color they will use to draw. This gives the user feedback about the state of the control. You might also notice that the control uses Quartz2D drawing commands. Your control could just as well use Cocoa’s NSBezierPath class to draw itself. It’s good to be familiar with both drawing APIs. There might be cases where you will notice the performance difference, so it’s nice to be able to fall back on Quartz for drawing straight into the view instead of using another object.
We can check number 1 off of our to-do list. The drawing is finished. One more.
Event Handling
NSView is a subclass of NSResponder. The mechanism of the view hierarchy ensures that any view you add to it will be placed in the application’s “Responder Chain”. The window will use the view hierarchy to deliver mouse events to the view that the user clicked on. Keyboard events will be delivered through the responder chain. It’s up to the view to implement the NSResponder event handlers that it is interested in.
Most mouse events will be sent automatically to the appropriate view, but there is one step that a view must take to receive keyboard events. The keyboard events go to the “firstResponder” first and then travel through the responder chain. If your view is to receive keyboard events it must accept first responder status. To do this, the view needs to override the NSResponder method:
- (BOOL)acceptsFirstResponder
to return YES. By default it returns NO.
Here’s a list of the most common mouse event handlers that a custom view can impelment:
- (void)mouseDown:(NSEvent*)theEvent;
- (void)mouseDragged:(NSEvent*)theEvent;
- (void)mouseUp:(NSEvent*)theEvent;
- (void)mouseMoved:(NSEvent*)theEvent;
- (void)mouseEntered:(NSEvent*)theEvent;
- (void)mouseExited:(NSEvent*)theEvent;
And common keyboard event handlers:
- (void)keyDown:(NSEvent*)theEvent;
- (void)keyUp:(NSEvent*)theEvent;
NSEvent
All of the event methods are going to pass us an NSEvent object that describes the event. If it’s a mouse event, we can ask the event for the location of the mouse point using the NSEvent method:
- (NSPoint)locationInWindow;
This is going to give us the location of the mouse event in the window (duh). We need to convert this point to a coordinate in our view’s internal coordinate system. We’ll use NSView’s method
- (NSPoint)convertPoint:(NSPoint)thePoint fromView:(NSView*)theView;
If the event is a keyboard event, we can ask the NSEvent object for a string of characters associated with the event with the following methods:
- (NSString *)charactersIgnoringModifiers;
- (NSString *)characters;
Another useful bit of information we can get from NSEvent is whether or not a modifier key is associated with the event. Use the method
- (unsigned int)modifierFlags
to get the current modifier flags, which you can find documented in the list of NSEvent’s constants. Check this against the modifier flag you are looking for. This bit of code is checking for the command key:
if(([theEvent modifierFlags] & NSCommandKeyMask) != 0)
{
// do something special
}
Check the NSEvent documentation for more information on getting neat things like tablet data.
Ok, Let’s get back to our control.
Mouse Down
When our view receives a mouse down, we want to check to see if the mouse point is inside any of the rectangles. There’s a handy function we can use to check a point struct against a rectangle struct:
BOOL NSPointInRect(NSPoint thePoint, NSRect theRect);
KDTwirlDistortionControl’s mouse down method checks all of the rects against the mouse point. If it determines that a click has occured inside any of its rects, it sets the control state to indicate which rectangle has been hit, tells itself to redraw and returns. Here’s an excerpt:
// down on the position control rect
if(NSPointInRect(aMousePoint,mPositionControlRect))
{
mControlState = kKDTwirlDistortionControlState_PositionControlActive;
[self setNeedsDisplay:YES];
return;
}
If none of the rectangles have been hit, the view centers the position rectangle around the mouse point. This lets user click the background to set the position - hint: this feature comes in handy if you happen to drag it to an unusable position, like I said there are problems in this design ; )
Mouse Dragged
This might be the most important method our class implements. The mouse drag is going to change the values of our active rectangle. It’s also going to call the setter methods of our control’s float values, which will automatically trigger notification of a value change to any class that is bound to the control.
The first thing the mouse dragged handler does is check the control state to determine which rectangle the user is adjusting. It will then perform the calculations it needs to make adjustments to both the rectangle and the associated filter value. It sets the value, tells itself to redraw and returns. Here’s an excerpt:
// dragging the position control rect
if(mControlState == kKDTwirlDistortionControlState_PositionControlActive)
{
// center the rect around the mouse point
float aNewXPosition = aMousePoint.x-mPositionControlRect.size.width*.5;
float aNewYPosition = aMousePoint.y-mPositionControlRect.size.height*.5;
mPositionControlRect.origin = NSMakePoint(aNewXPosition, aNewYPosition);
[self setPositionX:aNewXPosition];
[self setPositionY:aNewYPosition];
[self setNeedsDisplay:YES];
return;
}
Mouse Up
The mouse up simply sets the control state to be inactive and tells itself to redraw.
Key Down and Key Up
The key down event handler looks for arrow key presses. When an arrow key is pressed, it adjusts the origin of the position rectangle a few points in the appropriate direction. It also sets the current control state so that the position rectangle is active and redraws. Here’s an excerpt from the method. It uses a switch statement to find the arrow keys:
case NSUpArrowFunctionKey:
mControlState = kKDTwirlDistortionControlState_PositionControlActive;
mPositionControlRect.origin.y = mPositionControlRect.origin.y+5;
[self setPositionY:mPositionControlRect.origin.y];
[self setNeedsDisplay:YES];
break;
The key up method also checks for arrow keys. If the event is from one of the arrow keys, it resets the control state to inactive and redraws.
Event handling was the last thing on our to-do list. Scratch it off, we’re done. We have a control.
Hi Asimo! Nice Hair-do!
The KDTwirlDistortionControl example is very specific to the kind of data it is controlling and it could be used with any Core Image filter that has the same attributes, despite its name.
In a future post, I will explore ways to abstract the design a little more to make the control more useful in more situations. This will be necessary for designing OpenGL controls and could be useful if you’re using CALayers as the basis for a control. I haven’t had a chance to work with CALayers, but I notice that they’re not descendants of NSResponder. Odd for such a view-like class.
Resources
The Example Project
Cocoa Event handling Guide
The Responder Chain
Cocoa drawing guide
Quartz2D Programming Guide
NSView - Working With the View Hierarchy
Cocoa Bindings
More advanced bindings examples: mmalc’s bindings examples, look at graphics bindings