linfan's blog

Cocoa菜单栏右侧图标的实现

与Windows中的Tray Icon不同,Mac OS X中类似的UI元素位于屏幕右上角菜单栏的右侧,官方命名为Status Item,我们可以使用NSStatusBar和NSStatusItem类在菜单栏上为自己的应用添加图标。

1
2
3
4
5
6
NSStatusBar *statusBar = [NSStatusBar systemStatusBar];
NSStatusItem *statusItem = [statusBar statusItemWithLength:NSVariableStatusItemLength];
[statusItem setImage:image];
[statusItem setMenu:menu];
[statusItem setHighlightMode:YES];
[statusItem retain];

使用这种方法创建的图标,位于菜单栏图标区域的最左侧,如果菜单栏上图标太多,很容易就被程序菜单遮挡了。那么,有没有办法在菜单栏的右侧添加图标呢?

根据Apple的开发文档,这是不可能实现的。不过,Apple留了后门,通过调用NSStatusBar类的一些非公开API,是可以将图标移动到菜单栏右侧的。我们来看看怎么做。

首先要声明将要调用的非公开API。

1
2
3
4
@interface NSStatusBar (NSStatusBar_Private)
- (id)_statusItemWithLength:(float)l withPriority:(int)p;
- (id)_insertStatusItem:(NSStatusItem *)i withPriority:(int)p;
@end

与NSStatusBar的常规接口相比,最关键的是新增的withPriority参数,这个参数指明了图标的优先级,实际上就是图标在菜单栏上的位置。

那么,怎么使用这两个API呢?根据我从茫茫网络收集的资料,有不完全相同的两种方法,各有优劣。

方法一

1
2
3
4
5
6
7
8
9
10
11
12
13
if ([statusBar respondsToSelector:@selector(_statusItemWithLength:withPriority:)] &&
    [statusBar respondsToSelector:@selector(_insertStatusItem:withPriority:)])
{
    int priority = INT_MAX - 1;
    if (!statusItem)
    {
        statusItem = [statusBar _statusItemWithLength:0 withPriority:priority];
        [statusItem retain];
    }
    [statusBar removeStatusItem:statusItem];
    [statusBar _insertStatusItem:statusItem withPriority:priority];
    [statusItem setLength:NSVariableStatusItemLength];
}

方法二

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
- (void)restartSystemUIServer
{
    NSTask *killSystemUITask = [[[NSTask alloc] init] autorelease];
    NSMutableArray *args = [NSMutableArray array];
    [args addObject:@"SystemUIServer"];
    [args addObject:@"-HUP"];
    [killSystemUITask setLaunchPath:@"/usr/bin/killall"];
    [killSystemUITask setArguments:args];
    [killSystemUITask launch];
}

- (void)createStatusBar
{
    if ([statusBar respondsToSelector:@selector(_statusItemWithLength:withPriority:)])
    {
        if (!statusItem)
        {
            statusItem = [statusBar _statusItemWithLength:0 withPriority:INT_MAX];
            [statusItem retain];
        }
        [statusItem setLength:NSVariableStatusItemLength];
        [self restartSystemUIServer];
    }
}

方法一不需要重新启动SystemUIServer服务,但是在Lion的全屏模式下,右侧一部分会被Spotlight的图标所遮挡;方法二没有此问题,但重启SystemUIServer服务的过程会删除菜单栏上所有图标并重建,视觉效果不好。

另外,方法一在Snow Leopard下运行时,如果用户输入killall SystemUIServer重启SystemUIServer服务,会导致图标错位到Spotlight图标的右侧。解决方法是捕获系统的“com.apple.menuextra.added”通知(这个通知也是非公开的),调用方法一重新添加图标。

1
2
3
4
[[NSDistributedNotificationCenter defaultCenter] addObserver:self
                                                    selector:@selector(menuExtrasWereAddedHandler:)
                                                        name:@"com.apple.menuextra.added"
                                                      object:nil];

两种方法都在Snow Leopard和Lion操作系统中通过测试。

另,Mac OS X系统内建的各种图标并非由上述方法创建,而使用了另一个非公开的类NSMenuExtra,可以按住⌘键拖动调整位置。

方法一来自Tunnelblick项目,方法二来自Day-O作者Shaun Inman的指导,特此感谢!

Comments