Colored Controls
If you ever wanted to give any of your UI controls some strange color tint, there are two major ways to do that:

- The first one that comes to mind would be redrawing all graphics in Photoshop and then doing all custom drawing in drawRect: method of your view subclass. But that way you will have to recreate all you graphic files if you decide to change a color of your control.
- The second way, that we will discuss today, is not that straightforward than the first one but requires no images and probably will work with any type of controls. The only limitation it has—it will work only Mac OS X 10.4 Tiger and later system versions as it uses CoreImage filters to do all the coloring.
In our example, we will create colored checkboxes for a table view, so we’re going to subclass NSButtonCell for that purpose. I’ll not go into details about subclassing cells and controls and just skip ahead to a drawing method. If we were subclassing some other control like a NSButton we probably would override drawRect: method, but NSCells subclasses are drawn in a different way.
First, we will call super in case we don’t have our custom color set:
- (void) drawInteriorWithFrame:(NSRect)cellFrame inView:(NSView *)controlView { if ( color == nil ) { [super drawInteriorWithFrame:cellFrame inView:controlView]; return; }
The next section is a bit tricky… As the color converting and CoreImage filtering are not that simple and lightweight tasks to do, it will be better to cache all our drawing. But unfortunately, NSControl and their related NSCells have a number of concrete states with their own graphics representation.
So, we’re going to create an unique string key for each combination of color, control size, enabled state and value. Then we will use that keys to store our images in a cache dictionary:
unsigned size = [self controlSize]; float h = [color hueComponent]; float s = [color saturationComponent]; float b = [color brightnessComponent]; BOOL enabled = [self isEnabled]; int state = [self state] NSString *cacheKey = [NSString stringWithFormat:@"%u_(%.1f|%.1f|%.1f)_%d_%d", size, h, s, b, enabled, state]; CIImage *cachedImage = [imagesCache objectForKey:cacheKey];
If there are no cached image for our current combination of states and color—we’re going to draw it. First, we will ask our superclass to draw itself into a custom image template, and then convert this image to a CIImage to prepare it to use with CoreImage filters.
if ( !cachedImage ) { NSRect originalCellRect = NSZeroRect; originalCellRect.size = cellFrame.size; NSImage *templateImg = [[NSImage alloc] initWithSize:cellFrame.size]; [templateImg lockFocus]; [super drawInteriorWithFrame:originalCellRect inView:controlView]; [templateImg unlockFocus]; cachedImage = [CIImage imageWithData:[templateImg TIFFRepresentation]]; [templateImg release]; ...
Then we will create a CIHueAdjust filter and set it’s inputAngle property to a difference between our current control tint and a target color.
We chose 0.583f as an hue value for default blue color of Mac OS X controls, but feel free to expand this code and make it more versatile by using
Also notice that while CIColor and NSColor uses hue scale from 0.0f to 1.0f, but our CIFilter takes radian values from zero to 2π.
CIFilter *filterHue = [CIFilter filterWithName:@"CIHueAdjust"]; [filterHue setValue:cachedImage forKey:@"inputImage"]; static float m_2pi = 2.0f*M_PI; float newHue = (h-0.583f/*blue tint*/)*m_2pi; while ( newHue < 0.0 ) newHue += m_2pi; while ( newHue > m_2pi ) newHue -= m_2pi; [filterHue setValue:[NSNumber numberWithFloat:newHue] forKey:@"inputAngle"]; cachedImage = [filterHue valueForKey:@"outputImage"];
Some colors like a gray or black have no meaning in hue value, so let’s also make brightness/saturation adjustment to our image so we could use grayscale colors in our new control. Just pass an outputImage from the previous CIHueAdjust filter as an inputImage to the new CIColorControls filter:
CIFilter *filterColor = [CIFilter filterWithName:@"CIColorControls"]; [filterColor setValue:cachedImage forKey:@"inputImage"]; float brightness = 0.3*(s - 1.0)*(b*2.0 - 1.0); [filterColor setValue:[NSNumber numberWithFloat:s] forKey:@"inputSaturation"]; [filterColor setValue:[NSNumber numberWithFloat:b] forKey:@"inputBrightness"]; [filterColor setValue:[NSNumber numberWithFloat:1.0f] forKey:@"inputContrast"]; cachedImage = [filterColor valueForKey:@"outputImage"];
So, while have our colored checkbox in hands now, it’s about time to store it in cache dictionary for later use and draw it in our view. The drawing part is pretty clear if you have ever made any custom NSView in the past, just don’t forget to flip your image before drawing into NSTableView or any other view that has flipped coordinate system.
... [imagesCache setObject:cachedImage forKey:cacheKey]; } NSRect drawRect = cellFrame; [NSGraphicsContext saveGraphicsState]; if ( [controlView isFlipped] ) { float viewHeight = [controlView frame].size.height; NSAffineTransform *transform = [NSAffineTransform transform]; [transform scaleXBy:1.0 yBy:-1.0]; [transform translateXBy:0.0 yBy:-viewHeight]; [transform concat]; drawRect.origin.y = viewHeight - drawRect.origin.y - drawRect.size.height; } NSRect fRect = NSZeroRect; fromRect.size = [cachedImage extent].size; [cachedImage drawInRect:drawRect fromRect:fRect operation:NSCompositeSourceOver fraction:1.0]; [NSGraphicsContext restoreGraphicsState]; }
You probably will want to clear your caches directory at some point, especially when using a wide range of colors, but for the most cases dealloc is the best place to release our cache.
The full source code of MLColoredButtonCell class is available for download.
But please… don’t take this as a permission to color everything you have in your app, use this neatly and don’t forget to follow Apple’s Human Interface Guidelines as often as possible. Thanks!