在Dashboard Widget中嵌入Cocoa
Author: Michael Marmarou
译者:欧嘉蔚
对牛弹吉他翻译练习 Ver 1.0
l 导言
你是否需要在Dashboard widget中嵌入Cocoa对象呢?如果答案是需要的话,那么你就读对了文章了。这篇文章还会让你了解到Dashboard的内部是怎么工作的。通常来说,Widget是以 HTML,CSS和JavaScript的结合的形式展示的,你所看到的只是这些技术所联合起来显示的一些图像。 JavaScript建立一些控件,而他们的消息会被传回一个可以使用系统功能的Cocoa束(Cocoa Bundle)。而这个教程会为你展示一种直接访问Dashboard背后的Cocoa内核的方法。
尽管你是可以把Cocoa对象添加到Widget里面,但是我们的行为和普通的Cocoa程序会有差异。作为免责声明,这个我写这个文档是为了纯粹的教学目的,如果出了什么问题,我不会负任何责任的。Dashboard不是一个被保护的环境,我也曾经让它崩溃过很多次。我个人并不认为这些方法是“Hacks”,因为Dashboard的设计者看来是有意而且直接的开放了这能力。
因为所有的层次的程序员都可以做Dashboard Widget,所以这里会提供一点基础知识。我假设阅读这个文档的程序员都懂得Objective-C并且可以熟练地使用XCode。
l Dashboard是怎么工作的:
对于用户和程序员来说Dashboard进程是被隐藏的。真实的应用程序位置是在Dock.app包里面(/System/Library/CoreServices),它是Dock的子进程。对于程序员来说,如果你的Dashboard崩溃了,你必须重新启动Dock来重置它。在后面会有一些关于调试的章节。
最好的方式是把Dashboard看成一个巨大的浏览器。或者,很好的描述是:在一个灰色背景上的一堆浏览器。每个widget是一个被封装在一个WebFrameView(DBFrameView)里面的WebView(实际上是一个子类叫做DashboardWebView)。这些类的实际名字并不重要因为他们是私有类。因为我们没有那些头文件,我们不能知道有什么方法可调用。当然,在Cocoa中和Widget协同工作并不就是修改DashboardView那么简单。有很多东西你不能做,随着我们一步一步地研究在一个Widget里面我们有什么能够做有什么不能做,很多的限制会被讨论到。
镜像TextView的例子
l 目标:
这个例子会展示一些基本原理,如何加入一个子视图,放置一个子视图,访问它的代理。两个NSTextView会被加进一个空白的画布(canvas),并且设置一个代理,其中的一个Textview会通过代理来更新。这个例子只是为了展示如何加入Cocoa对象,并且让你了解从它们那里获取事件是十分简单的。如果你不知道怎么在通过代码(译者:不是使用Interface Builder)创建Cocoa对象,我强力建议你先花点时间在上面,因为这个工具在所有的Cocoa编程中都非常有用。
l 基础:
在Widget能被显示之前,有几样东西是必须的。缺少一个或者是一个错误的值都回让你的Widget不能显示,或者显示不正常(还会让你郁闷几个小时)。所以,在开始构建插件(plugin)之前,在Widget的目录下有一个符合标准的基础结构,是非常重要的。在这个例子里面,我们会从零开始建立一个新的Widget。尽管在开发者文档的例子里(/Developer/Examples/Dashboard/Sample Code)有一个空白的Widget。为了更好的理解一个Widget的每一部分,并且确保没有东西被忽略掉,我们还是从零开始建立一个Widget。 进入你的工作文件夹(就是你想把你要建立的那个最终会变成Widget的文件夹所放置的地方)。建立一个新的文件夹,命名为“embeddedCocoa.widget”。你可能会想:“那个扩展名不是‘.wdgt’吗?”是的,没错。但是为了避免经常要使用终端,鉴别,或者按着control点击再选显示包内容。我们还是让扩展名继续为‘.widget’。直到我们要开始使用我们的Widget。
每个Widget都需要两个plist:info.plist 和 version.plist。打开‘Property List Editor.app’ (/Developer/Applications/Utilities/),建立一个新的文件。建立一个新的根(Root),建立下面的子节点,和下面的相关联的值。
BundleVersion
String
219
CFBundleShortVersionString
String
1.0
CFBundleVersion
String
1.0
ProjectName
String
DashboardSDK
SourceVersion
String
20000
现在,并不要在意这些值的意义,但是他们是必须存在的。把文件保存为version.plist并且放置在embeddedCocoa.widget里面。建立一个新文件,建立一个根,再加入下面的子节点:
AllowMultipleInstances
Boolean
No
CFBundleIdentifier
String
com.apple.widget.embeddedcocoa
CFBundleName
String
embeddedCocoa
CFBundleShortVersionString
String
1.0
CFBundleVersion
String
1.0
DefaultImage
String
Default
Height
Number
205
MainHTML
String
embeddedCocoa.html
Plugin
String
embeddedCocoa.widgetplugin
Width
Number
300
AllowMultipleInstances 的意义非常直接。CFBundleIdentifier 对我们来说并不重要,但是要确保它的值是正确的。如果多于一个Widget有相同的值就会有问题了。DefaultImage 很重要,如果没有默认图片Widget就不会被加载了。他不会一直显示(译者:实际上,它只在主HTML没有加载的时候显示),我希望在最终版本中不再需要这个了,不过现在你还是需要一个默认图片(PNG格式的)。MainHTML 是就是WebView会频繁地加载的页面,所以也是非常重要的。Plugin 标识了最终会被放在同一个文件夹里面的插件捆束(plugin bundle)。
这文档里面会包含一些范例代码,但是,这不是必需的。如果你确实有一些范例,在随便在其中的一个范例里面找一个Default.png 文件。如果你没有这些范例,没关系,就随便建立一个图像文件,并且重命名它。任何的PNG图像都可以用,不过我会避免让它大于4GB,尽管这样的文件真的有可能存在。
现在,我们必须要建立一个HTML文件。用你手头上任意一个文本编辑器建立一个新的文件。有趣的是,这个文件可以是完全空白的。不过当前你可能会要加入一个简单的HTML和一个body标签来显示背景图片。
<html>
<body>
<img src=”Default.png”>
</body>
</html>
现在我们需要测试这Widget来确保所有东西都正常的工作。把文件夹重命名为‘embeddedCocoa.wdgt’。这个文件夹会马上把自己的图标变为Dashboard Widget的图标。双击这个图标看看Widget是否加载。如果没有加载,可能是你打错了那些plist中的一个值,缺少了一个文件或者命名不正确,又或者就是Dashboard不喜欢你了。
l 创建捆束
打开Xcode (/Developer/Applications/)。创建一个新的Cocoa捆束,在菜单下选择“New Project…”
在模版列表中,选择“Cocoa Bundle”。把捆束命名为“embeddedCocoa”。这个新建的捆束会没有基础的类,所以我们需要自己创建一个。选择类文件夹(Classes folder),双击它,然后选择‘Add’再选择‘New File…’。
从显示出来的表单中,选择‘Objective-C class’。这是一个从NSObject类派生的Objective-C类。
请确保创建了两个文件(embeddedCocoa.h 和 embeddedCocoa.m),并且他们被放置在‘Classes’文件夹。如果不是这样的,请把它们拖到那儿。有两个方法是必须被声明的,但是有一个现在还不会用到。尽管不被用到,他们必须在头文件和实现文件中出现。头文件应该像这样子的:
#import <Cocoa/Cocoa.h>
#import <WebKit/WebKit.h>
@interface embeddedCocoa : NSObject
{
WebView *webView;
NSTextView *textView;
NSTextView *textViewMirror;
}
- (id) initWithWebView:(WebView*)webView;
- (void) windowScriptObjectAvailable:(WebScriptObject* )windowScriptObject;
@end
在这里,我们导入了Cocoa和WebKit两个框架。因为WebView是在WebKit框架中声明的,所以它必须被导入。WebView是一个WebView的引用,它在initWithWebView里被传入对象。我们还声明了两个NSTextView。我们会保留对这些对象的引用,这样我们就可以在进程的生命周期中的任何时间点修改他们的内容。为了使插件工作,那两个方法必须声明。
在实现文件,有一个init方法。用户可以通过这个函数来初始化捆束、实例变量和做任何完成的工作。尽管这看来不是设计者的初衷,但是我们还是窃取它所接受的WebView,并且在上面耍小把戏。那个init函数应该像这样子的:
#import "embeddedCocoa.h"
@implementation embeddedCocoa
-(id) initWithWebView:(WebView*)wview
{
/* Top TextView */
textView = [[NSTextView alloc] initWithFrame:NSMakeRect(0,105,300,100)];
NSClipView *clipView = [[NSClipView alloc] init]; //Initialize ClipView
[clipView setDocumentView:textView]; //Add TextView as subview
[clipView setFrame:NSMakeRect(0,105,300,100)]; //Set frame
NSScrollView *scrollView = [[NSScrollView alloc] init];//Initialize ScrollView
[scrollView setDocumentView:clipView]; //Add ClipView as subview
[scrollView setFrame:NSMakeRect(0,105,300,100)]; //Set frame
/* Mirror TextView */
textViewMirror = [[NSTextView alloc] initWithFrame:NSMakeRect(0,0,300,100)];
NSClipView *clipViewMirror = [[NSClipView alloc] init];
[clipViewMirror setDocumentView:textViewMirror];
[clipViewMirror setFrame:NSMakeRect(0,0,300,100)];
NSScrollView *scrollViewMirror = [[NSScrollView alloc] init];
[scrollViewMirror setDocumentView:clipViewMirror];
[scrollViewMirror setFrame:NSMakeRect(0,0,300,100)];
[wview addSubview:scrollView]; //Adds the subview to the main WebView
[wview addSubview:scrollViewMirror]; //Adds the subview to the main Webview
[textView setString:@"Type here, and it will be mirrored..."];
[textViewMirror setString:@"...down here."];
[textView setDelegate:self]; //set self as delegate to receive notifications
webView = wview; //save the webView, in case we need to use it later
return self;
}
-(void) windowScriptObjectAvailable:(WebScriptObject *)windowScriptObject
{
}
-(void)textDidChange:(NSNotification *)notification
{
[textViewMirror setString:[textView string]]; //changes the mirror text
}
@end
因为你有可能会键入比textView能过显示的更多的字符,我在这个例子里面使用了一个scrollView。现在,plist(译者:这里指的是Cocoa捆束的plist,不是指Widget的plist)就必须修改了。找到‘info.plist’—— 它应该是在Resources那个组里面。“CFBundleIdentifier” 键应该修改为“com.apple.embeddedCocoa.plugin”,而 “NSPrincipalClass” 就修改为 “embeddedCocoa”。
<key>CFBundleIdentifier</key>
<string>com.apple.embeddedCocoa.plugin</string>
<key>NSPrincipalClass</key>
<string>embeddedCocoa</string>
接着,目标名称需要修改为包含标准的Widget扩展名。在Targets组里,选择‘embeddedCocoa’,按着Control再单击它,选择“Get info”。
应该就会出现一个带标签的面版。选择“Build”标签然后,往下滚动,在那些粗体的设定中,找到Product Name 和 Wrapper Extension。把 Wrapper Extension 定为 “widgetplugin”。
这样,这个插件应该就可以完成了。现在,构建这个项目(command-B 或者在Build菜单下选择它)。这应该不会出现什么问题,但如果有,用相应的方法解决它。进入build文件夹(通常是在项目目录下面)然后把已经构建出来的捆束拖动到Widget的目录里面。这个捆束应该命名为“embeddedCocoa.widgetplugin”。重命名Widget文件夹使扩展名为“.wdgt”。这个文件夹这应该会把自己的图标变为Dashboard Widget的图标。双击它运行。很有希望它会运行并且在Dashboard里显示。当它被完全加载,你应该会看到两个NSTextView互相在对方的上层,如果你在其中一个里面打字,在下层TextView的一个会根据上层的那个TextView的值来改变自己的值。
l 在Dashboard Widget里面的Cocoa对象
尽管我们可以加入几乎是任何是NSView子类的Cocoa对象,但是在大多数情况并不推荐这么做。一个好例子就是NSSlider。把通过Cocoa来加入的Slider并不会很好的工作,因为Widget窗口处理了所有的拖动操作。你可以单击在slider范围内的某个特殊的区域,但是它不会像你期望的那样工作。对于像这样的对象我推荐使用JavaScript作为替代。然而,在Text Field的例子里,使用NSTextView会比使用内置对象更方便。我再一次强调,使用Cocoa类应该是最后的手段。对于大多数的应用中,HTML/CSS/JS的集合就可以很好的完成了。应该还有另外一个选择就是使用Java Applet。我没有亲自试过,但是这应该是可以办到的,这样你就可以访问Java了的所有可用的对象了。 Jave和JavaScript是有非常大的区别的,不过很多人并不知道这一点。
l Cocoa的其它用法
有很多人曾经问过关于如何在内部Dahsboard调出一个打开面板、保存面板或者打印面板。这是完全有可能的,不过有一些东西需要考虑一下。首先,这三种面板都是低于Dashboard的视窗级的,这表示他们默认是不能在Dashboard层以上显示的。作为一个例子,我们有这样一个保存面板:
NSSavePanel *sp;
int runResult; // Holds the returned result button id
// pointer to a shared instance of the save panel
sp = [NSSavePanel savePanel];
// show the window
runResult = [sp runModalForDirectory:NSHomeDirectory() file:@""];
如果这些代码在Dashboard插件内部被调用,它会在Dashboard背景层的下面显示。当然,面板的层级也是可以通过下面的代码来改变的:
[sp setLevel:NSScreenSaverWindowLevel];
层级只是取一个整数值,而这个整数值必须比Dashboard的值要高。NSScreenSaverWindowLevel的值是1000,高于Dashboard的值。然而,在你单击了它以后,这个值会被重置为默认值。Dashboard的开发者推荐你使用其它替代的方法。或者在Widget内部自己做一个面板,或者找出一种可以在Dashboard内部选定文件来保存的方法。因为调出任何一个这样的面板会让用户失去身处Dashboard的感受。
在把Cocoa对象用于Widget时,你需要时常想到Dashbaord的用户体验。程序员必须把某些用户期望的Dashbaord观感纳入考虑范围。最坏的是,如果你使用非常复杂的Cocoa类来创建Widget,你还不如让它作为一个单独的应用程序。
l 改变大小和移动
当前,是没有办法在插件里面直接改变Widget的大小的。如果你在某些情况下真的需要改变Widget的大小,你只需要发送一个消息给JavaScript来改变大小(resizeTo)。如果你尝试用WebView的setFrame: ,setFrameOrigin: 或者其它类似的函数,你不会成功,因为它会在下一次重绘中被重置为原来的样子。
l 现在你可以开始了
你可以通过Cocoa对象在Dashboard里面做很多事情,不过请确保你时刻考虑到Dashboard的观感。他们只是小工具,不是大型、复杂的应用程序。添加Cocoa对象是利用Cocoa框架和它的AppKit的一个非常好的方法。这会使Dashboard变成怎样呢?这显然非常有趣。而我希望的是那些做Widget的人会最终把他们的才能用于Cocoa应用程序。在Widget里面使用少量的Cocoa对于那些对Cocoa编程感兴趣的人来说是个很好的过渡!