老谭笔记

MAC系统状态栏通过插件添加图标MenuExtra Plugin

在开发MAC软件时,为是让用户更方便的使用功能并且不占用过多桌面空间,我们一般都会选择在状态栏添加图标,就如Windows上的任务栏一样.我们通常使用如下的方式的生成状态栏图标:

1
[NSStatusBar systemStatusBar] statusItemWithLength:NSSquareStatusItemLength];

当然这也是推荐的方式,因为采用的是标准的API,对于需要上架MAS(Mac App Store)的软件来说,这也是唯一的方法.但这样生成的图标优先级低,当左边应用程序的菜单项过多时很容易就被遮挡而不显示,对于不上架MAS的软件来说,我们可以使用以下的Private API:

1
[NSStatusBar systemStatusBar] _statusItemWithLength:NSSquareStatusItemLength withPriority:USHRT_MAX - 1];

使用上面的方法便可让图标的优先级仅次于系统的图标,放在其它应用程序图标的最右边,但即便如此仍然不能像系统图标一样,按住command就能随便拖动位置,并且在点击之后也不能像系统图标一样左右自由切换,所以还有一种方式就是采用插件的方式.

系统的状态栏图标由SystemUIServer这个系统进程负责管理,它是通过插件的方式展示每一个系统图标,我们可以在/System/Library/CoreServices/Menu Extras这个目录下面找到所有的系统状态栏图标.而这里面每一个menu扩展名的bundle就是状态栏图标插件.称之为MenuExtra plugin.但是在10.2以后的,MAX OSX就不再允许第三方的MenuExtra plugin,而此时我们就需要一个开源的项目,叫做MenuCracker,它本身也是一个MenuExtra plugin,但是当加载了这个插件之后,便开启了第三方MenuExtra plugin的支持,它的项目托管在sourceforge,地址是:http://sourceforge.net/projects/menucracker/,至于这个项目的稳定性,可以在sourceforge看到它已经有十年的历史了,所以…

当然MAC OSX系统并未开放MenuExtra plugin的开发,Xcode也没有相应的模板,标准的API也没有任何支持.所以如果我们要开发MenuExtra plugin就只能采用Private API,所以需要上架MAS的软件或拒绝使用Private API就可以打住了,下面就是实际动手来开发一个简单的MenuExtra plugin.

1.首先通过class-dump导出Private API的头文件
class-dump对于做MAC OSX开发的童鞋来说应该都不陌生了,这是下载地址:http://stevenygard.com/projects/class-dump/
而我们所需要的头文件都在SystemUIPlugin.framework这个框架下,通过以下的命令便可将需要的头文件都导出来:

1
./class-dump -H -o ~/Desktop/NSMenuExtra /System/Library/PrivateFrameworks/SystemUIPlugin.framework/Versions/A/SystemUIPlugin

2.在Xcode中创建插件的工程
在Xcode中选择Bundle这个模板,然后在Build Settind中将Wrapper Extension项修改为”menu”.
将步骤1中导出的头文件添加到工程中(删除类似这样的#import “NSObject.h”无用的头文件引用,因为可能编译会出错).
在工程的Link Binary With Libraries中引用/System/Library/PrivateFrameworks/SystemUIPlugin.framework这个框架(直接拖进来即可).
创建用于显示图标的View的类,让它继承于NSMenuExtraView,然后让它画上你想要的图形,比如:

1
2
3
4
5
6
7
8
9
- (void)drawRect:(NSRect)rect
{
NSBezierPath *path = [NSBezierPath bezierPathWithOvalInRect:NSInsetRect(self.bounds, 2, 2)];
if ([_menuExtra isMenuDown])
[[NSColor blackColor] set];
else
[[NSColor redColor] set];
[path fill];
}

创建图标这个对象,也是这个插件的Principal class,让它继承于NSMenuExtra(NSStatusItem的子类),比如我们类名叫THMenuExtra,那么也将工程的info.plist中将Principal class的值设置为THMenuExtra,这是插件创建的入口.
THMenuExtra类需要去重写父类的以下两个方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
- (NSMenu *)menu
{
return myMenu;
}
- (id)initWithBundle:(NSBundle *)bundle
{
self = [super initWithBundle:bundle];
if( self == nil )
return nil;
//设置图标的View
THMenuExtraView *theView = [[THMenuExtraView alloc] initWithFrame:[[self view] frame] menuExtra:self];
[self setView:theView];
//创建myMenu和其它一些初使化的工作
myMenu =[[NSMenu alloc] initWithTitle:@""];
NSMenuItem *quitItem = [[NSMenuItem alloc] initWithTitle:@"Just Test!" action:nil keyEquivalent:@""];
[myMenu addItem:quitItem];
return self;
}

3.插件安装及卸载
如上文所描述的,如果需要让我们的插件跑起来,肯定需要先行安装MenuCracker.menu(双击即可),如果我们有其它外部程序,可以将MenuCracker打包我们的程序之内,并拷贝到用户合适的目标再open它.
完成了上一步骤,我们插件的安装就非常简单,与MenuCracker的安装一样,直接Open即可.
但卸载略麻烦,你可以在插件中将自己unload完成卸载,或者将插件删除,然后结束SystemUIServer进程即可(SystemUIServer会自动重新启动).
插件的更新,如果你修改了插件的内容,即便插件自身已经unload,你双击新的插件,你会发现仍然是旧的版本,那是因为SystemUIServer会自动缓存你的Bundle,所以如果需要更新的话,你依然需要结束SystemUIServer进程才可完成.

以上插件Demo源码请访问:https://github.com/tanhaogg/THExtraMenu