Good afternoon,% username%!
Not so long ago came the need to customize the title of the window of his program in Mac OS X. If iCal.app and Adress Book.app do it, then why shouldn’t I do the same?

The very first links from Google gave me some leads, and even one test program (after long dances with a tambourine) compiled and displayed its non-standard title. But it required connecting private headers, modifying them (to match the new version of Mac OS X), etc ... And I wanted the best, I wanted to make it easier, and also set the text color of the window title (to harmonize with the new header color). Dropping all the bad examples, I started digging leads ...
And I found out that in an ordinary program, the undocumented
NSThemeFrame class is responsible for drawing the window, and we will work with it.
')
Caution!
Runtime magic is present
under the cut .
First we need a private header NSThemeFrame.h (not original, but reversed, of course), it is easy to google it. If laziness, then
here is a direct link . It is not necessary to add it to the project, we need it only for studying.
Running through his eyes, pay attention to the methods drawRect: and _drawTitleStringIn: withColor :. The names speak, that’s what we’ll overload in order to fully control the window drawing. Armed with <objc / runtime.h>, we start.
First, we need to somehow get the class NSThemeFrame. You can get it from a private header, but this is a bad option. Suppose, in AppDelegate, we have an outlet of our NSWindow, then, to get the desired class, do this:
id _class = [[[self.window contentView] superview] class];
Why? Because NSThemeFrame is the base View in the window, and our contentView is already located on it.
Second, go to
magic .
We need to declare our class, in it the drawInRect: and _drawTitleStringIn: withColor: methods, then add these methods to the NSThemeFrame class (but under different names), and, finally, swap the methods with the original ones in order to be able to call the original ones from the new ones.
Sounds hard? Well, to help!
Let's declare the auxiliary class DrawHelper (it will not be used directly, so we do not pay attention to the warning when compiling).
#import <objc/runtime.h> // global frame color static NSColor * gFrameColor = nil; // global title color static NSColor * gTitleColor = nil; @interface DrawHelper : NSObject { } // to prevent errors - (float)roundedCornerRadius; - (void)drawRectOriginal:(NSRect)rect; - (void) _drawTitleStringOriginalIn: (NSRect) rect withColor: (NSColor *) color; - (NSWindow*)window; - (id)_displayName; - (NSRect)bounds; - (void)_setTextShadow:(BOOL)on; - (void)drawRect:(NSRect)rect; - (void) _drawTitleStringIn: (NSRect) rect withColor: (NSColor *) color; @end @implementation DrawHelper - (void)drawRect:(NSRect)rect { // Call original drawing method [self drawRectOriginal:rect]; [self _setTextShadow:NO]; NSRect titleRect; NSRect brect = [self bounds]; // creating round-rected bounding path float radius = [self roundedCornerRadius]; NSBezierPath *path = [NSBezierPath alloc]; NSPoint topMid = NSMakePoint(NSMidX(brect), NSMaxY(brect)); NSPoint topLeft = NSMakePoint(NSMinX(brect), NSMaxY(brect)); NSPoint topRight = NSMakePoint(NSMaxX(brect), NSMaxY(brect)); NSPoint bottomRight = NSMakePoint(NSMaxX(brect), NSMinY(brect)); [path moveToPoint: topMid]; [path appendBezierPathWithArcFromPoint: topRight toPoint: bottomRight radius: radius]; [path appendBezierPathWithArcFromPoint: bottomRight toPoint: brect.origin radius: radius]; [path appendBezierPathWithArcFromPoint: brect.origin toPoint: topLeft radius: radius]; [path appendBezierPathWithArcFromPoint: topLeft toPoint: topRight radius: radius]; [path closePath]; [path addClip]; // rect for title titleRect = NSMakeRect(0, 0, brect.size.width, brect.size.height); // get current context CGContextRef context = (CGContextRef)[[NSGraphicsContext currentContext] graphicsPort]; // multiply mode - for colorizing original border CGContextSetBlendMode(context, kCGBlendModeMultiply); // draw background if (!gFrameColor) // default bg color gFrameColor = [NSColor colorWithCalibratedRed: (126 / 255.0) green: (161 / 255.0) blue: (177 / 255.0) alpha: 1.0]; [gFrameColor set]; [[NSBezierPath bezierPathWithRect:rect] fill]; // copy mode - for title CGContextSetBlendMode(context, kCGBlendModeCopy); // draw title text [self _drawTitleStringIn: titleRect withColor: nil]; } - (void)_drawTitleStringIn: (NSRect) rect withColor: (NSColor *) color { if (!gTitleColor) // default text color gTitleColor = [NSColor colorWithCalibratedRed: 1.0 green: 1.0 blue: 1.0 alpha: 1.0]; [self _drawTitleStringOriginalIn: rect withColor: gTitleColor]; } @end
Everything is quite simple here. We declare two colors - the color of the header and the text color, we declare our class, it has a bunch of methods that we need (we don’t need to implement them, we have them in NSThemeFrame) and, in fact, our two methods for drawing text and background.
For the sake of simplicity, I made drawing a standard header and “colorizing” it with one color (this allows you to keep the usual “volume” of the header in a simple way). You can also make custom rendering completely using NSImage or gradients, and you don’t even have to call drawRectOriginal: then we don’t need a standard header. But we will leave it for independent exercises.
After calling the standard header rendering method, proceed to the creation of our drawing area. This is usually a rectangle with rounded corners. Implementation for other types of windows (for example, with non-rounded bottom corners) is also left for independent work.
Well, and then comes the rendering of our color over the already drawn standard header in multiply mode (for more information about the modes, read the documentation from Apple).
And at the very end we draw our heading text. Our function is again called, which ignores the color transmitted to it and forcibly draws the text with a predetermined color (through the original drawing function).
And so we got to the most interesting! Actually, the
magic :
- (void)applicationWillFinishLaunching:(NSNotification *)aNotification { id _class = [[[self.window contentView] superview] class];
(In my case, I put this code in AppDelegate.m, in order to be sure that the window will already be created)
In order:
1. get the class NSThemeFrame
2. we take the drawRect method: from the class DrawHelper
3. Add this method to the NSThemeFrame class under the name drawRectOriginal:
4. we take from the class NSThemeFrame the methods drawInRect: and drawRectOriginal:
5. we change their implementation in some places!
Then we do the same for the _drawTitleStringIn: withColor: method.
And now we can rejoice! Our window pleases (or not) our eye with its non-standard header color.
If you really want to do a kind of “skinning” (changing the color of the header on the fly), then the DrawHelper class and the contents of the applicationWillFinishLaunching function: you need to put it in a separate .m file, as well as declare and implement the access functions to gFrameColor and gTitleColor. And do not forget to redraw all your windows after changing these parameters. But this, again, leave the reader as an independent work.
But, as one would expect, this approach has disadvantages:
1. to get the class NSThemeFrame, we need an already created window;
2. this method does not imply separate customization of windows, for example, it is impossible to make two windows with different headers (of course, it is possible, but it will require a lot of effort and a lot of code);
3. windows can be rendered around the NSThemeFrame, for example, using NSGrayFrame, then this method most likely will not help, and you will have to play with the second class too;
4. Runtime games are good in moderation.
PS: initially all this was done in a bunch of Qt + Cocoa, but it was transferred to pure Cocoa. If anyone is interested in the tricks of Qt interaction with Cocoa, then I can share my experience.
PPS: I see no reason to put the code on the githab; it is very easily transferred to any project with a simple copy-paste in AppDelegate.m.