Custom Window Widgets:
This is tricky and didn't really think it would work because all the info found was more than 3 years old. But it did!
I could get a window with custom widgets (Close/Miminize/Maximize and theorically fullscreen buttons), :) . I hope someday I know photoshop as much as objc and make my own cool buttons, for now I have to play with iCal's buttons.
![]() Normal Cocoa NSWindowButtons | ![]() Customized NSWindowButtons |
The left picture shows the normal cocoa NSWindowButtons, right picture on the right shows darker and yellowish buttons, not very nice on the gray titlebar but ... Is because I don't know photoshop! (涙) But with great potential huh?! (笑)
Introduction:
In cocoa you get the buttons of a window :
NSButton *button = [window standardWindowButton:NSWindowZoomButton];
But
button is not really of kind NSButton is in fact an instance of _NSThemeWidget. Furthermore, it does not use a normal NSButtonCell but it has its own _NSThemeWidgetCell. Furthermore, the close button is a subclass of them: _NSThemeCloseWidget and _NSThemeCloseWidget because it has a dirty state of the documentIn this post I show how to customize the images of these private classes because
setImage: does nothing :(How this works
I tried to make a new class and pose it as an
_NSThemeWidgetCell but posing is being deprecated and is not possible anymore, at least not that I know.Crayson told me that I should better try the method swizzling approach. This was a huge hint!.
So using a couple of objective-c runtime functions I added a method dynamically to _NSThemeWidgetCell class.
Then it was very easy to exchange methods : the one newly added
alt_drawWithFrame:inView: with the other that does the drawing as usual drawWithFrame:inView:.This part the code and the whole source + the images I borrowed from Lion's iCal can be downloaded from github :)
void drawWithFrameInView(id self, SEL _cmd, NSRect frame, id view)
{
NSLog(@"hacking drawWithFrameInView ...");
NSString *imageName = @"titlebarcontrols_regularwin";
//Get button ID
int buttonID = (int)[self buttonID];
NSLog(@"%d", buttonID);
switch (buttonID)
{
case 127: // Close button
imageName = [imageName stringByAppendingFormat:@"_close"];
break;
case 128: // Minimize button
imageName = [imageName stringByAppendingFormat:@"_minimize"];
break;
case 129: // Zoom button
imageName = [imageName stringByAppendingFormat:@"_zoom" ];
break;
case 130: // Toolbar button
imageName = [imageName stringByAppendingFormat:@"_toolbar_button" ];
break;
}
//Get System preferences: Window style: (Aqua or graphite)
NSString * const kAppleAquaColorVariant = @"AppleAquaColorVariant";
NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults];
[userDefaults addSuiteNamed:NSGlobalDomain];
NSNumber *color = [userDefaults objectForKey:kAppleAquaColorVariant];
if ([color intValue] == 6) {//graphite is 6
imageName = [imageName stringByAppendingFormat:@"_graphite"];
}else{//defaults to aqua, (aqua is 1)
imageName = [imageName stringByAppendingFormat:@"_colorsryg"];
}
//Get button state
if ([self respondsToSelector:@selector(getState:)]) {
int state = (int)[self getState:view];
//NSLog(@"state %d", state);
switch (state) {
//Known states
//active = 0
//activenokey = not used?
//disabled = not used?
//inactive = 3
//pressed = 2
//rollover = 1
case 0:
imageName = [imageName stringByAppendingFormat:@"_active"];
break;
case 1:
imageName = [imageName stringByAppendingFormat:@"_rollover"];
break;
case 2:
imageName = [imageName stringByAppendingFormat:@"_pressed"];
break;
case 3:
imageName = [imageName stringByAppendingFormat:@"_inactive"];
break;
case 4:
break;//disabled? activenokey?
case 5:
break;//disabled? activenokey?
default:
break;
}
NSImage *img = [NSImage imageNamed:imageName];
if (img){
[img dissolveToPoint:NSMakePoint(frame.origin.x, frame.origin.y + frame.size.height) fraction:1.0];
}else{
[(NSButtonCell*)self alt_drawWithFrame:frame inView:view];//original implementation
}
}
}
And the part of code that does the objective-c runtime magic:
@implementation TestAppDelegate
@synthesize window;
- (void)applicationWillFinishLaunching:(NSNotification *)notification
{
Class class = NSClassFromString(@"_NSThemeWidgetCell");
SEL new_selector = @selector(alt_drawWithFrame:inView:);
SEL orig_selector = @selector(drawWithFrame:inView:);
//Add a new method dinamically because _NSThemeWidgetCell is a private class
BOOL success = class_addMethod(class, new_selector,
(IMP)drawWithFrameInView,
"v@:{CGRect={CGPoint=dd}{CGSize=dd}}@");
if (success) {
//Get the methods to exchange
Method originalMethod = class_getInstanceMethod(class, orig_selector);
Method newMethod = class_getInstanceMethod(class, new_selector);
// If both are found, swizzle them
if ((originalMethod != nil) && (newMethod != nil)){
method_exchangeImplementations(originalMethod, newMethod);
}
}
//TEST:a new method should appear "alt_drawWithFrame:inView:" in the console
//uint methodCount = 0;
//class = NSClassFromString(@"UIWebDocumentView");
//Method *mlist = class_copyMethodList(class, &methodCount);
//for (int i = 0; i < methodCount; ++i){
// NSLog(@"%@", NSStringFromSelector(method_getName(mlist[i])));
//}
//TEST: "hacking drawWithFrameInView ..." should appear in the console
//NSButton *but = [window standardWindowButton:NSWindowZoomButton];
//[[but cell] drawWithFrame:NSZeroRect inView:nil];
}
References and useful Links


3 comments :
This uses a private API right?
Yes it uses _NSThemeWidget and _NSThemeWidgetCell class which are respectively subclasses of NSButton and NSButtonCell
Post a Comment