老谭笔记

在OSX Application中实现Services

对于刚接触OSX开发的程序来讲,经常会很惊奇别人的程序是如何在系统的右键菜单中添加了一个功能,比如对于一个图片文件,右键菜单可以用它生成一个icns文件,正如我的上一篇文章中写到的那样(跳转),当然那个小工具的实现是通过automator来制作的一个Services,而我们今天要讨论的是如何在一个Application中来实现一个Services。

1.Services是怎样运行的

services是不同的程序之间通过剪贴板(pasteboard)的共享来运作的,服务请求者与服务提供者之间是完全独立的,所以它们之间不会有共享内存等直接的联系,通常它们之间交互的剪切板也是独立于标准的剪贴板,所以也不会影响我们正常的使用复制、粘贴等操作。

如图,当用户选择了一个services的菜单项,当前的选择就会被复制到剪贴板,然后传递给提供服务的应用程序(如果该程序没有启动,则会先启动该程序),然后该程序从剪贴板中读取数据,并用于程序的一些处理逻辑,当然服务提供程序也可以通过将处理后的结果写入这个剪贴板而返回给原程序中。

2.Services的实现方式

提供服务的程序可以在系统的任意地方,该程序提供的服务属性被定义在info.plist文件之中,OSX会自动收集所有的服务,并在匹配的场景之下提供相应的服务菜单项。服务菜单默认会被显示在服务请求程序的默认的nib文件中的menu之中,如果你希望它显示在指定的菜单中,你可以需要使用NSApplication的方法setServicesMenu:,相应的服务菜单项就会显示在该menu中。

services的定义放在Info.plist下的NSServices之中,NSServices对应的值是一个数组,意味着你可以同时定义多个不同的服务提供项目,每一个项目之中包含如下的属性:

NSMessage 值类型为NSString,表示服务入口方法的名称,当用户选择了提供的服务项之后,系统会自动调用yourMessageName:userData:error:,其中的yourMessageName就是该属性对应的字符串,而该方法的接收对象是你的代码中指定的实例对象(使用[NSApp setServicesProvider:instance]).

NSPortName值类型为NSString,表示服务监听端口的名称,它的值依赖于服务提供商如何申请注册,在大多数情况下,这是应用程序的名称(product Name)。

NSMenuItem值类型为NSDictionary,该字典只能有一个key为default,对应的值为NSString,该值表示服务菜单上所显示的名称,其它任何的值目前都不被允许(10.5之前有所差别,此处不作讨论)。如果需要让该菜单本地化显示,那么你可以创建一个ServicesMenu.strings,并且指定本地化key为上面default对应的值。

NSKeyEquivalent值类型为NSDictionary,该字典只能有一个key为default,对应的值为NSString,该值定义了你服务项的快捷键,但建议尽量少用快捷键,除非一定有这样的需求,因为如果你和其它的服务定义了相当的快捷键,那么结果就是不确定的,该属性是可选的。

NSSendTypes值类型为NSArray,该数组的值为服务请求时写入剪切板支持的文件类型,该文件类型由NSPasteboard定义的类型(在10.5及更老的版本中类型为UTI类型并且该属性是必须的),该属性是可选的。

NSReturnTypes同NSSendTypes一样,意为服务提供者写入剪切板支持的所有文件类型,该属性也是可选的。

NSUserData值类型为NSString,该值是可选的,该值通过服务入口函数yourMessageName:userData:error:的第二个参数返回给程序,所以你可以定义多个不同的服务菜单,但入口函数是一样的(NSMessage一样的),然后通过userData来区分不同的事务。

NSTimeout值类型为NSString,该值必须是一个数字的字符串,表示服务请求的超时时间(毫秒),它也是可选的,默认为30000 milliseconds (30 seconds)。

NSSendFileTypes值类型为NSArray,该值定义了服务所支持的UTI类型(Uniform Type Identifiers),注意不能使用pasteboard类型,当然你也可以同时指定NSSendTypes来指定pasteboard类型,它是可选的(但和NSSendTypes二者留一)

NSServiceDescription值类型为NSString,该值是对服务的一个描述,它是可能在ServicesMenu.strings中被本地化的,并且如果该值太长的话,你也最好选择使用本地化来显示,它是可选的

NSRequiredContext值类型为NSDictionary,指定该值用于限制你所提供的服务项应该在何时才会出现,该字典可以使用这些key:NSApplicationIdentifier,NSTextScript,NSTextLanguage,NSWordLimit,NSTextContent,并且每一个都是可选的(但NSRequiredContext本身是必须的),NSApplicationIdentifier的值为NSString或NSString的数组,指定了你的服务项只会在哪个应用程序中出现,比如com.apple.finder就表示只有在finder中才会出现;NSTextScript的值为NSString或NSString的数组,每个NSString为一个四个字符的脚本标识,只有当你有服务是用于处理文本并且文本中包括指定的脚本标签时才会生效;NSTextLanguage的值类型为NSString或NSString的数组,用于指定文本的语言类型,如zh,当然它也只对于接收文本的服务有效;NSWordLimit的值类型为整型,用于限制文本的最大长度,仅对于接收文件的服务有效;NSTextContent的值类型为NSString或NSString的数组,用于指定文本的特定格式(支持这几种:URL,Date,Address,Email,FilePath)。

PS:对Info.plist中NSServices的添加或属性的修改都需要注销当前用户之后,才会生效,因为系统只有刚加载时才会去检索所有的服务列表。

3.实战练习

创建一个叫ServicesTest的工程,并创建一个THServiceHelper的类,其代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
- (NSString *)encryptString:(NSString *)string
{
//加密过程省略
return resultString;
}
- (NSString *)decryptStr:(NSString *)string
{
//解密过程省略
return resultString;
}
- (void)encrypt:(NSPasteboard *)pboard userData:(NSString *)data error:(NSString **)error
{
// Test for strings on the pasteboard.
NSArray *classes = [NSArray arrayWithObject:[NSString class]];
NSDictionary *options = [NSDictionary dictionary];
if (![pboard canReadObjectForClasses:classes options:options]) {
*error = NSLocalizedString(@"Error: couldn't encrypt text.");
return;
}
// Get and encrypt the string.
NSString *pboardString = [pboard stringForType:NSPasteboardTypeString];
NSString *newString = nil;
if ([data isEqualToString:@"e"])
{
newString = [self encryptString:pboardString];
}
if ([data isEqualToString:@"d"])
{
newString = [self decryptStr:pboardString];
}
if ([newString length] == 0) {
*error = NSLocalizedString(@"Error: couldn't encrypt text.");
return;
}
// Write the encrypted string onto the pasteboard.
[pboard clearContents];
[pboard writeObjects:[NSArray arrayWithObject:newString]];
}

在delegate中添加如下的代码:

1
2
3
4
5
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification
{
serviceHelper = [[THServiceHelper alloc] init];
[NSApp setServicesProvider:serviceHelper];
}

在info.plist最尾端添加如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
<key>NSServices</key>
<array>
<dict>
<key>NSMenuItem</key>
<dict>
<key>default</key>
<string>加密字符串</string>
</dict>
<key>NSMessage</key>
<string>encrypt</string>
<key>NSPortName</key>
<string>ServicesTest</string>
<key>NSRequiredContext</key>
<dict/>
<key>NSReturnTypes</key>
<array>
<string>NSStringPboardType</string>
</array>
<key>NSSendTypes</key>
<array>
<string>NSStringPboardType</string>
</array>
<key>NSUserData</key>
<string>e</string>
</dict>
<dict>
<key>NSMenuItem</key>
<dict>
<key>default</key>
<string>解密字符串</string>
</dict>
<key>NSMessage</key>
<string>encrypt</string>
<key>NSPortName</key>
<string>ServicesTest</string>
<key>NSRequiredContext</key>
<dict/>
<key>NSReturnTypes</key>
<array>
<string>NSStringPboardType</string>
</array>
<key>NSSendTypes</key>
<array>
<string>NSStringPboardType</string>
</array>
<key>NSUserData</key>
<string>d</string>
</dict>
</array>

3.4编译运行,然后注销当前用户,然后在任何可以编辑文本的地方选中,右键菜单中你便可以看到加密/解密字符串的选项了

小工具源代码:ServicesTestCode
编译后的小工具效果:ServicesTest