@import url(http://www.cppblog.com/CuteSoft_Client/CuteEditor/Load.ashx?type=style&file=SyntaxHighlighter.css);@import url(/css/cuteeditor.css);
首先大家看Apple关于NSButton的描述,NSButton跟NSWindow一样,它的外观形式也是委托给NSButtonCell来处理的,自身只包含逻辑代码。
所以重绘NSButton就是重绘NSButtonCell啦,然后把NSButton的cell设置位你自己的cell就好了。
1)重绘目标
首先观察一下系统NSButton的行为和外观表现,可以发现默认Button(快捷健设置为return)是有一个一闪一闪的效果,鼠标点击其他非默认button的时候同window上默认button的蓝色消失,同时被点中button变成蓝色。放开鼠标,默认button恢复蓝色背景并闪烁,被点击button变白色。
重绘一个控件最好是不要改变其默认行为,也最好不要违反Apple的关于界面设计的建议文档。所以我们的目标是重绘出来的button是灰色渐变背景,默认button有一个黄色的圈圈围在周围,不闪烁。被点中的button显示黄色圈圈,默认button黄色圈圈消失。
效果如下图:
鼠标未按下效果
鼠标按下效果
2)渐变背景
NSButtonCell的重绘方法很简单,重写下面的方法即可。
逻辑就是
1)检测当前button的类型(普通button,checkbox,radiobutton等)
2)画button的基本形状和颜色
3)如果当前button被click了,那么显然的画一个黄色的圈圈上去
4)如果没有被click,那么检测是否为默认button,如果是,并且当前window没有被click的其他button,那么为自己画一个黄色的圈圈,否则不画。
// buttoncell有一个私有方法来标示当前button的类型
// 这里只列出关心的三种类型
typedef enum KAButtonType{
KACheckBox = 3,
KARadioButton = 4,
KARoundButton = 7
};
- (void)drawWithFrame: (NSRect)cellFrame inView: (NSView *)controlView
{
switch ([self _buttonType]) { // buttonCell的私有函数,可以确定button类型,10.4/10.5/10.6都可用
case KACheckBox:
[self drawCheckInFrame:cellFrame isRadio:NO]; // 画checkbox的形状,这里忽略不画
break;
case KARadioButton:
[self drawCheckInFrame:cellFrame isRadio:YES]; // 画radiobutton的形状,这里忽略不画
break;
default:
switch ([buttonCell bezelStyle]) { // 这就是button啦,默认的形状,这个参数可以在IB里设置,
// 所以button的类型必须为NSRoundedBezelStyle,当然你可以改为其他的
case NSRoundedBezelStyle:
[self drawRoundedButtonInFrame: cellFrame inView: controlView];
break;
case NSRegularSquareBezelStyle:
[self drawHyperLinkButtonInFrame: cellFrame];
break;
default:
break;
}
break;
}
// 画Button的图片哦
// Comment by yoyokko
// if [buttonCell _normalImage] is nil, that to say there is a missing
// field in nib file for this check box -->
// NSButtonCell uses function <(int)_buttonType> to determine button type.
// After hacking, I found that 3==Checkbox, 4==Radio, 7==RoundedButton
if([buttonCell _buttonType] == KARoundButton)
{
if([buttonCell imagePosition] != NSNoImage) {
[self drawImage: [buttonCell image] withFrame: cellFrame inView: [buttonCell controlView]];
}
}
}
// 查询当前window上有没有被click的button
- (void)travelSubViews: (NSView*)view
{
NSArray *items = [view subviews];
NSEnumerator *enumerator = [items objectEnumerator];
id anObject = nil;
while (anObject = [enumerator nextObject])
{
if ([anObject isKindOfClass: [NSButton class]])
{
NSButtonCell *buttonCell = [anObject cell];
NSBezelStyle buttonStyle = [buttonCell bezelStyle];
if ([buttonCell isHighlighted] &&
(buttonStyle == NSRoundedBezelStyle || buttonStyle == NSTexturedRoundedBezelStyle))
{
[self setMIsFound: YES];
break;
}
}
else
{
[self travelSubViews: anObject];
}
}
}
// 画渐变的button和黄色圈圈
-(void)drawRoundedButtonInFrame:(NSRect)frame inView: (NSView *)controlView
{
NSRect textFrame;
//Adjust Rect so strokes are true and
//shadows are visible
frame.origin.x += .5f;
frame.origin.y += .5f;
frame.size.height -= 1;
frame.size.width -= 1;
//Adjust Rect based on ControlSize so that
//my controls match as closely to apples
//as possible.
switch ([buttonCell controlSize]) {
default: // Silence uninitialized variable warnings for textFrame fields.
case NSRegularControlSize:
frame.origin.x += 4;
frame.origin.y += 4;
frame.size.width -= 8;
frame.size.height -= 12;
textFrame = frame;
break;
case NSSmallControlSize:
frame.origin.x += 4;
frame.origin.y += 4;
frame.size.width -= 8;
frame.size.height -= 11;
textFrame = frame;
textFrame.origin.y += 1;
break;
case NSMiniControlSize:
frame.origin.y -= 1;
textFrame = frame;
textFrame.origin.y += 1;
break;
}
//Create Path
NSBezierPath *path = [[NSBezierPath alloc] init];
[path appendBezierPathWithRoundedRect: frame cornerRadius:6.0f];
if([buttonCell isEnabled])
{
// draw inner part of button first
// 画button的灰色渐变部分
[self drawShadingWithStartingColor: [self colorVlaueWithRed: 239 green: 239 blue: 239]//[NSColor blackColor]
withEndingColor: [self colorVlaueWithRed: 93 green: 93 blue: 93]//[NSColor whiteColor]
inBezierPath: path];
// draw focus ring second
// 当当前button被click时,画那个黄色的圈圈
// if the button is highlighted, then draw a ring around the button
if([buttonCell isHighlighted]) // 当button被click时,isHighlighted返回YES
{
[[self colorVlaueWithRed: 246 green: 186 blue: 55] set];
[path setLineWidth: 3.0f];
[path stroke];
}
else
{
// button没有被click,那就检查是否为默认的button
// otherwise, check if it is a default button
id btnControl = [buttonCell controlView];
if ([btnControl respondsToSelector: @selector(keyEquivalent)] && [[btnControl keyEquivalent] isEqualToString: @"\r"])
{
// 如果是默认button
NSView *superView = controlView;
NSView *tempView = nil;
for (tempView = superView; tempView != nil; tempView = [tempView superview])
superView = tempView;
// 找到当前window的contentview
if (superView)
{
[buttonCell setMIsFound:NO];
[buttonCell travelSubViews: superView];
}
// 看当前window中有没有被click的button,没有就把自己这个默认button画一个黄圈
if (![buttonCell mIsFound])
{
[[self colorVlaueWithRed: 246 green: 186 blue: 55] set];
[path setLineWidth: 3.0f];
[path stroke];
}
[buttonCell setMIsFound:NO];
}
}
}
else
{
// button 没有enable
[self drawShadingWithStartingColor: [self colorVlaueWithRed: 220 green: 220 blue: 220]//[NSColor blackColor]
withEndingColor: [self colorVlaueWithRed: 112 green: 112 blue: 112]//[NSColor whiteColor]
inBezierPath: path];
}
[path release];
// 画button的text,这里忽略不画
if([buttonCell imagePosition] != NSImageOnly) {
[self drawTitle: [buttonCell attributedTitle] withFrame: textFrame inView: [buttonCell controlView]];
}
}
至此,所有绘制的代码工作都已经完成了,包括黄色圈圈和点击其他button的行为都写好了~
但这样做会有一个问题……
3)更改系统默认画黄色圈圈的行为
释下面一段代码的行为,这个很重要,否则会出现非常巧妙的bug……很奇妙,困扰了我两个星期的bug,恨哪~
- (void)heartBeat:(CDAnonymousStruct7 *)fp8
{
id btnControl = [self controlView];
if ([btnControl respondsToSelector: @selector(keyEquivalent)] && [[btnControl keyEquivalent] isEqualToString: @"\r"])// && !oneButtonClicked)
{
[btnControl setNeedsDisplay:YES];
}
}
首先探索一下系统默认button的一闪一闪的行为是怎么做的,blabla一大堆,经过hack发现,每个程序在起来之后都会启动一个叫做HeartBeat的线程。每个control都有一个heartBeat:的函数。
这个线程负责默认button的一闪一闪的刷新,spin的旋转等,所以在你的主界面block住的时候你会发现button还在闪,spin还在转,而你自己用timer写的progressspin是不会转的。对于一个window来说,它上面的button不会一直刷新,只是显示的时候刷几次,而默认button会被heartbeat线程调用一直刷新。
问题就出在这里,这是一个线程啊,我们重写了buttoncell的绘制函数,但我们并没有做处理并保证这个函数是原子的调用啊,所以这里会发生非常极品的问题(当用多线程绘制界面时一定要注意是原子操作)
首先有一个程序弹出了一个sheet,然后这个sheet上有一个button,点击button会再次弹出一个sheet,不知道是不是apple的这里的消息循环有问题,在点击这个button弹出sheet的同时,button所在的window或者新弹出的window上有的button会被刷成别的形状,比如某个radiobutton的字变成了OK,或者就变成了一个拉长版的普通button,并且只会变成默认button的字或者形状。
这就是因为多线程的原因造成的。在刷当前button的时候,heartbeat来捣乱了,不知道怎么搞得就把默认button的字或者形状刷到了当前button的信息上面(button的text就是被改变了)。不太清楚默认的heartBeat:里面做了些什么。
所以这里只能重写heartBeat:函数(亦或把重绘函数变成原子的,没试过),在这个函数里面啥都不做,只是检测当前button是否为默认button,是的画就通知主线程来刷新。
因为这里只是加一个黄色圈圈而已,所以即使主线程block住也没什么问题。
JB,非常JB~
PS:在10.4上程序起来时heartbeat线程不能正常起来,所以需要在程序结束launching之后谈一个sheet,再把之关闭就可以了(很奇怪,估计Tiger上的消息循环还是有很大的问题的)。
@import url(http://www.cppblog.com/CuteSoft_Client/CuteEditor/Load.ashx?type=style&file=SyntaxHighlighter.css);@import url(/css/cuteeditor.css);
From: http://www.cocoachina.com/bbs/read.php?tid=14590