@import url(http://www.shnenglu.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)重繪目標
首先觀察一下系統(tǒng)NSButton的行為和外觀表現(xiàn),可以發(fā)現(xiàn)默認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的私有函數(shù),可以確定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啦,默認的形狀,這個參數(shù)可以在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]];
}
}
至此,所有繪制的代碼工作都已經(jīng)完成了,包括黃色圈圈和點擊其他button的行為都寫好了~
但這樣做會有一個問題……
3)更改系統(tǒng)默認畫黃色圈圈的行為
釋下面一段代碼的行為,這個很重要,否則會出現(xiàn)非常巧妙的bug……很奇妙,困擾了我兩個星期的bug,恨哪~
- (void)heartBeat:(CDAnonymousStruct7 *)fp8
{
id btnControl = [self controlView];
if ([btnControl respondsToSelector: @selector(keyEquivalent)] && [[btnControl keyEquivalent] isEqualToString: @"\r"])// && !oneButtonClicked)
{
[btnControl setNeedsDisplay:YES];
}
}
首先探索一下系統(tǒng)默認button的一閃一閃的行為是怎么做的,blabla一大堆,經(jīng)過hack發(fā)現(xiàn),每個程序在起來之后都會啟動一個叫做HeartBeat的線程。每個control都有一個heartBeat:的函數(shù)。
這個線程負責默認button的一閃一閃的刷新,spin的旋轉(zhuǎn)等,所以在你的主界面block住的時候你會發(fā)現(xiàn)button還在閃,spin還在轉(zhuǎn),而你自己用timer寫的progressspin是不會轉(zhuǎn)的。對于一個window來說,它上面的button不會一直刷新,只是顯示的時候刷幾次,而默認button會被heartbeat線程調(diào)用一直刷新。
問題就出在這里,這是一個線程啊,我們重寫了buttoncell的繪制函數(shù),但我們并沒有做處理并保證這個函數(shù)是原子的調(diào)用啊,所以這里會發(fā)生非常極品的問題(當用多線程繪制界面時一定要注意是原子操作)
首先有一個程序彈出了一個sheet,然后這個sheet上有一個button,點擊button會再次彈出一個sheet,不知道是不是apple的這里的消息循環(huán)有問題,在點擊這個button彈出sheet的同時,button所在的window或者新彈出的window上有的button會被刷成別的形狀,比如某個radiobutton的字變成了OK,或者就變成了一個拉長版的普通button,并且只會變成默認button的字或者形狀。
這就是因為多線程的原因造成的。在刷當前button的時候,heartbeat來搗亂了,不知道怎么搞得就把默認button的字或者形狀刷到了當前button的信息上面(button的text就是被改變了)。不太清楚默認的heartBeat:里面做了些什么。
所以這里只能重寫heartBeat:函數(shù)(亦或把重繪函數(shù)變成原子的,沒試過),在這個函數(shù)里面啥都不做,只是檢測當前button是否為默認button,是的畫就通知主線程來刷新。
因為這里只是加一個黃色圈圈而已,所以即使主線程block住也沒什么問題。
JB,非常JB~
PS:在10.4上程序起來時heartbeat線程不能正常起來,所以需要在程序結束launching之后談一個sheet,再把之關閉就可以了(很奇怪,估計Tiger上的消息循環(huán)還是有很大的問題的)。
@import url(http://www.shnenglu.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