Cocoa:[系统控件重绘教程(二)]:重绘NSButton

转载 2015年11月18日 15:02:40
@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

相关文章推荐

李明杰控件刷新框架

更多App信息可以关注:M了个J-博客园 MJRefresh类结构图 图中红色文字的类:可以直接拿来用 下拉刷新控件的种类 默认(Normal):MJRefreshNormalHeader...

初学cocoa开发:带你走入不一样的世界

http://blog.csdn.net/y_zhangpengwei/article/details/50803016  最近由于项目需求,特意研究了一下mac端app的相关开发,...

Mac中各种控件重绘

/********************************************************************  Filename:  ControlWithScheme...

[系统控件重绘教程(二)]重绘NSButton

首先大家看Apple关于NSButton的描述,NSButton跟NSWindow一样,它的外观形式也是委托给NSButtonCell来处理的, 自身只包含逻辑代码。 所以重绘NSButton就是重...

android控件重绘

android中实现view的更新有两组方法,一组是invalidate,另一组是postInvalidate,其中前者是在UI线程自身中使用,而后者在非UI线程中使用。 View的重绘——系统不会...

重绘控件的用法代码DOC

  • 2010-10-08 09:40
  • 83KB
  • 下载

MFC 控件重绘(2) NM_CUSTOMDRAW, WM_DRAWITEM, 虚函数DrawItem

控件重绘有三种方法: 1 设定界面属性 2 利用Windows的消息机制,通过Windows消息映射(Message Mapping)和反映射(Message Reflecting),在合适的时机...
内容举报
返回顶部
收藏助手
不良信息举报
您举报文章:深度学习:神经网络中的前向传播和反向传播算法推导
举报原因:
原因补充:

(最多只允许输入30个字)