面向初学者的 Cocoa Mac OS X 编程 / 归档
我们已经讨论了一些关于归档的一般原则,现在我们将为我们的绘图文档实现它。一旦我们有了归档形状的能力,就会发现实现诸如剪切和粘贴之类的操作变得直截了当。
您会记得,当我们在 MyDocument 中添加代码以将文档读写到文件时,我们使用了一对名为 NSKeyedArchiver 和 NSKeyedUnarchiver 的对象。在那时,我们只是将整个 'objects' 数组作为所谓的 _根_ 对象进行归档。我们并不关心幕后发生了什么 - 归档器将数组转换为一个 NSData 对象,我们将其传递给 Cocoa,Cocoa 将数据写入文件。当我们打开文件时,解归档器重建了我们的数组,然后我们将其用作我们的 objects 数组。所以我们应该能够将绘图保存为文件并再次打开它,对吗?不,还没。问题是到目前为止我们还没有归档形状的实际属性。WKDShape 是我们设计的类,因此 Cocoa 还不知道如何对其进行归档。因此,如果我们尝试保存我们的绘图,我们会很失望地发现再次打开它们时会得到空白文档。
我们通过编写一些代码来告诉 Cocoa 如何归档我们的形状。我们需要以下两种方法
- (void) encodeWithCoder:(NSCoder*) coder; - (id) initWithCoder:(NSCoder*) coder;
这两个方法是 _正式_ 协议 NSCoding 的一部分。我们已经遇到过非正式协议,它只是我们之间达成一致要实现的一些方法。正式协议类似,除了它被严格执行 - 实现正式协议的对象_必须_这样做。为了告诉 Cocoa 我们想要成为正式协议的一部分,我们修改我们的类声明如下
@interface WKDShape : NSObject <NSCoding>
这表明我们将要实现正式协议 _NSCoding_。如果我们没有这样做,我们将在编译时得到一个错误,并且将无法构建我们的应用程序。
之前我们使用了 NSKeyedArchiver 及其对应的 NSKeyedUnarchiver。检查这两个类的头文件将显示它们实际上是更通用类 NSCoder 的子类。这就是传递给上面两种方法的内容,这两个方法是实现 NSCoding 协议所必需的。我们只需要告诉编码器要添加哪些属性到其归档中,它会完成其余工作。我们给每个属性一个名称或 _键_,然后使用该键稍后检索相同的属性。我们需要为每个形状保存的属性是
- 它的尺寸
- 它的位置
- 它的填充和描边颜色
- 它的描边宽度
NSCoder 有 encodeObject:ForKey: 和 encodeFloat:forKey: 之类的方法可以处理繁重的工作。所以这里是我们归档方法
- (void) encodeWithCoder:(NSCoder*) coder { [coder encodeRect:[self bounds] forKey:@"bounds"]; [coder encodeObject:[self fillColour] forKey:@"fill_colour"]; [coder encodeObject:[self strokeColour] forKey:@"stroke_colour"]; [coder encodeFloat:[self strokeWidth] forKey:@"stroke_width"]; } - (id) initWithCoder:(NSCoder*) coder { [self init]; [self setBounds:[coder decodeRectForKey:@"bounds"]]; [self setFillColour:[coder decodeObjectForKey:@"fill_colour"]]; [self setStrokeColour:[coder decodeObjectForKey:@"stroke_colour"]]; [self setStrokeWidth:[coder decodeFloatForKey:@"stroke_width"]]; return self; }
第一个,encodeWithCoder,在保存时使用。属性使用这里给出的键保存到编码器中,这些键只是标识属性的字符串。解码方法相反。请注意,它是一个 init 方法,这是有道理的 - 对象由编码器从数据流中创建,因此它需要使用保存的属性进行初始化。通常,对象将在做任何其他事情之前调用 [super initWithCoder:coder] 和 [super encodeWithCoder:coder],以便所有子类的属性也按需保存。在本例中,因为我们从 NSObject 继承而来,而 NSObject 没有实现 <NSCoding>,所以我们不需要这样做(实际上这样做会出错)。这就是为什么在本例中我们调用 [self init] 而不是 [super init] 的原因。
现在,如果您编译并运行,您会发现将绘图保存到文件是可行的 - 当您再次打开该文件时,您的绘图会完全按保存时的样子重新出现,包括所有属性,例如颜色等。
剪切和粘贴利用归档,以便我们能够在文档之间移动对象等。现在我们能够归档 WKDShape,我们也能够可靠且轻松地归档任何这些形状集。回想一下,NSKeyedArchiver 将某种“根对象”转换为 NSData 对象,这只是一块包含该对象编码版本的二进制数据。NSData 在保存文档的情况下写入文件,但如果我们将其写入名为 _剪贴板_ 的对象,我们就将该数据放到剪贴板,从而实现剪切或复制。粘贴是逆向操作,类似于解归档文件;我们解归档剪贴板数据并将由此创建的对象添加到文档中。
剪切和粘贴是命令,因此我们在 MyDocument 中将其实现为操作方法。将以下内容添加到 MyDocument.h 中的类定义中并保存该文件
- (IBAction) cut:(id) sender; - (IBAction) copy:(id) sender; - (IBAction) paste:(id) sender; - (IBAction) delete:(id) sender;
我们将实现所有四个方法。但是请注意,剪切等效于复制后删除,而删除只是删除选择中的对象,因此唯一“困难”的工作是在 copy: 和 paste: 方法中。在我们实现它们之前,让我们在 IB 中将它们连接起来。
双击 'MainMenu.nib' 以打开它(如果它尚未在 IB 中打开),然后双击 'FirstResponder' 图标。回想一下,每当可以在依赖于上下文的各种地方实现命令时,我们都会使用 FirstResponder。这正是这种情况,因为剪切和粘贴等可以在各种对象中以各种方式实现。碰巧的是,您会发现我们不需要做任何事情 - 当创建 Mainmenu.nib 时,剪切、粘贴等都是为您预先连接的。验证菜单命令是否确实连接到这些方法。
在 Xcode 中,在 MyDocument.m 中填充这四个方法。现在,不要添加任何代码。编译并运行项目。您现在会看到“编辑”菜单中提供了“剪切”和“粘贴”命令。实际上,我们希望这些命令仅在有东西可以选择实际剪切时才可用,但现在我们将接受默认行为,直到我们构建并测试代码。
在 Cocoa 中负责剪贴板行为的对象是 NSPasteboard。实际上,存在用于不同任务的多个剪贴板,但我们需要的是基本剪贴板,称为“通用”剪贴板。当我们收到“剪切”命令时,我们将选择归档到 NSData 对象中,然后将此数据放到通用剪贴板。因为此数据是我们应用程序私有的,所以我们不必担心在此阶段将其转换为其他应用程序可读的形式,例如 TIFF 或 PDF。
以下是我们编辑方法的实现
- (IBAction) cut:(id) sender { [self copy:sender]; [self delete:sender]; } - (IBAction) copy:(id) sender { if ([[self selection] count] > 0 ) { NSData* clipData = [NSKeyedArchiver archivedDataWithRootObject:[self selection]]; NSPasteboard* cb = [NSPasteboard generalPasteboard]; [cb declareTypes:[NSArray arrayWithObjects:@"wikidrawprivate", nil] owner:self]; [cb setData:clipData forType:@"wikidrawprivate"]; } } - (IBAction) paste:(id) sender { NSPasteboard* cb = [NSPasteboard generalPasteboard]; NSString* type = [cb availableTypeFromArray:[NSArray arrayWithObjects:@"wikidrawprivate", nil]]; if ( type ) { NSData* clipData = [cb dataForType:type]; NSArray* objects = [NSKeyedUnarchiver unarchiveObjectWithData:clipData]; NSEnumerator* iter = [objects objectEnumerator]; id obj; [self deselectAll]; while( obj = [iter nextObject]) { [self addObject:obj]; [self selectObject:obj]; [(WKDShape*)obj repaint]; } } } - (IBAction) delete:(id) sender { NSArray* sel = [[self selection] copy]; NSEnumerator* iter = [sel objectEnumerator]; id obj; while ( obj = [iter nextObject]) [self removeObject:obj]; [sel release]; }
cut: 只是 copy: 后跟 delete
copy: 首先检查选择是否为空,如果不是,它使用键控归档器将选择归档到 NSData 对象中。然后,它获取通用剪贴板。要将数据放到剪贴板上,您首先要声明数据的类型。这允许将多个表示形式放置在同一数据的剪贴板中 - 接收方可以决定哪种表示形式最适合它 - 例如,图形程序可能想要数据的 TIFF 图像,而文字处理器可能想要文本表示形式等。这里,我们只有一个表示形式,它是一种名为“wikidrawprivate”的私有格式。因此,实际上“列表”中只有一项。一旦我们声明了类型,我们就可以将数据放到与该类型关联的剪贴板上。
paste: 反转此过程。首先,它获取通用剪贴板并检查是否确实存在类型为“wikidrawprivate”的数据。如果没有,则什么也不做,因此它不会尝试粘贴它无法理解的来自另一个应用程序的数据。然后,我们从剪贴板下载数据并解归档它。我们知道它是一个形状对象的数组 - 实际上复制的选择包含的内容是什么。我们需要将这些对象添加到文档中,所以我们只需遍历数组并添加和选择遇到的对象。我们还首先取消选择文档中的所有对象,以便用户获得预期的结果 - 粘贴后的对象在粘贴后被选中。
delete: 方法从文档中删除所选对象。它通过遍历选择数组的副本来做到这一点。这里需要一个副本,因为删除一个对象也会取消选择它 - 如果我们没有创建副本,我们将在遍历的同一个数组上进行修改,并且事情不会正常工作。一旦我们将对象从文档中删除,我们就可以完成副本,因此我们可以释放它。
编译并运行项目,并确认您有能力在不同的文档之间剪切和粘贴对象。
进一步练习
- 修改代码,使其还导出选择的 PDF 表示形式。这有点复杂,但并不难。提示:查看 NSView 的 writePDFInsideRect:toPasteboard: 方法
- 更复杂:添加拖放行为。这与剪切和粘贴几乎相同,并使用“拖放”剪贴板。覆盖 NSView 的方法以在创建拖放数据时以及在接收拖放到视图的数据时收到通知。