文章来源:苹果官方文档
介绍拖放
Cocoa 让我们能够实现时髦高雅的拖放功能,在应用内部或者在应用之间。这个编程主题讲解了如何用少数几个方法来实现拖放。
本文结构
在此处和 dragging 协议讲解里的文字里,术语 dragging session 是指整个过程,包括一张图片的选择、拖、放和目的地的接收或驳回。一个 dragging 操作就是目的地在图片被释放的时候接收了图片的行为。dragging 源是“拥有”被拖的图片的对象;它是进行 dragging session 的方法的参数。
dragging 是一个可视化的操作。要成为 dragging 操作的源或目的地,这个对象必须表示为屏幕的一部分真实区域;因此,只有 window 和 view 对象可以成为拖的源和目的地。(注意源视图和没必要和上面定义的 dragging 源是相同的对象。)NSWindow 和 NSView 提供方法来管理拖一个对象的用户界面。只需要实现 NSDraggingSource 或 NSDraggingDestination 协议中的少数几个方法,具体实现哪个要看你的 window 或 view 子类是源还是目的地。
dragging 协议在这些章节中讲解:
- Dragging 源
- Dragging 目的地
如何接收一次 drag 在这些章节中讲解:
- 接收 drag 操作
- Dragging 文件
关于拖放经常被问到的问题在这个章节中说明:
- 经常被问到的问题
Dragging 源
一个 dragging session 通过用户在 window 或 view 里点击和移动鼠标来初始化。NSView 和 NSWindow 实现了方法 dragImage:at:offset:event:pasteboard:source:slideBack: 来管理 dragging session。在 NSView 或 NSWindow 子类里的 mouseDown: 或 mouseDragged: 方法里调用这个方法。提供一个图片以在拖的过程中显示,一个 pasteboard 保管了数据,还有一个对象作为数据的“拥有者”或 dragging 源。在 dragging session 中,dragging 源被发送由 NSDraggingSource 协议定义的消息来执行所有必要的操作,如下所述。
注意:NSView 也实现了 convenience 方法 dragFile:fromRect:slideBack:event:,用于被拖的对象是文件的时候。NSView 然后自己管理图片、pasteboard 和 源消息。
拖操作
NSDraggingSource 方法中只有一个需要被实现:draggingSourceOperationMaskForLocal:。这个方法声明了源可以被实施的操作类型。表格 1 列出了可用的拖操作。(在 Java 中,常量被定义在 NSDraggingInfo 命名空间,以 NS 为前缀。)方法应该返回一个允许的类型按位或组合或 NSDragOperationNone,在没有操作被允许的情况下。
表格 1 可用的拖操作
dragging 操作 | 含义 |
---|---|
NSDragOperationCopy | 数据由可以被拷贝的图像表示 |
NSDragOperationLink | 数据可以被分享 |
NSDragOperationGeneric | 操作可以被目的地定义 |
NSDragOperationPrivate | 操作被私人地在源和目的地之间达成。 |
NSDragOperationMove | 数据可以被移动 |
NSDragOperationDelete | 数据可以被删除 |
NSDragOperationEvery | 以上全部 |
NSDragOperationAll | 弃用。用 NSDragOperationEvery 替代。 |
NSDragOperationNone | 一个拖操作都不给。 |
如果这个拖是全部在自己的应用中进行的或者是在自己的和另一个应用中间进行,允许的操作可能会有不同。传递给 draggingSourceOperationMaskForLocal: 的标记指明了是邻近或内部的拖。
用户可以按下辅助按键来进一步选择要执行哪个操作。如果 control、option 或 command 键被按下,源的操作 mask 被过滤到只包含表格 2 中给定的操作。要阻止辅助键修改 mask,你的 dragging 源应该实现 ignoreModifierKeysWhileDragging 然后返回 YES。
表格 2 modifier 键选择的拖操作
辅助按键 | dragging 操作 |
---|---|
Control | NSDragOperationLink |
Option | NSDragOperationCopy |
Command | NSDragOperationGeneric |
拖消息
在拖的过程中,源对象被发送一系列消息来通知它拖操作的状态。在拖最开始的时候,源被发送 draggedImage:beganAt: 消息。每次被拖的对象移动了,源就被发送一个 draggedImage:movedTo: 消息。最终,当用户已经释放了鼠标按钮并且目的地执行了放操作或拒收了,源会被发送 draggedImage:endedAt:operation: 消息。operation 参数是目的地执行的拖操作,或是 NSDragOperationNone 如果拖失败了的话。(在 Java 里,这些方法的名字为 startedDraggingImage、movedDraggingImage 和 finishedDraggingImage。)
dragging 源通常不需要实现这些方法里的每一个。但如果你要支持 NSDragOperationMove 或 NSDragOperationDelete 操作,需要实现 draggedImage:endedAt:operation: 来从源中移除被拖的数据。(注意一个 NSDragOperationDelete 操作在拖动任意对象到dock中的垃圾桶图标的时候被触发。)
被拖的图片
一个 dragging session 中被拖的图片只是简单地表示 pasteboard 上的数据。尽管 dragging 目的地可以访问图片,它主要还是关心图片表示的 pasteboard 中的数据 —— 目的地最终执行的 dragging 操作还是基于 pasteboard 数据,而不是图片本身。
当 dragging session 由 NSView 方法 dragFile:fromRect:slideBack:event: 启动的时候,NSView 使用该文件的 Finder 图标作为图片。对于你自定义的拖,需要配一个合适的图片。可能包括显示的数据半透明的截图,比如被选中的文字部分,或是数据的符号化表示,比如拖电子表格数据的时候用一个表格图标。
Dragging 目的地
要接收拖操作,必须注册你的 window 或 视图会接收的 pasteboard 类型,通过发送 registerForDraggedTypes: 消息,在 NSWindow 和 NSView 中都定义了,然后实现几个 NSDraggingDestination 协议中的方法。dragging session 中,候选目的地只有在注册了可以匹配被拖的 pasteboard 数据类型的时候才会接收 NSDraggingDestination。目的地在图片进入、内部移动以及退出或释放内部边界的时候接收这些消息。
尽管 NSDraggingDestination 被声明为一个 informal 协议,你创建用来实现协议的 NSWindow 和 NSView 子类只需要实现相关的方法即可。(NSWindow 和 NSView 类为全部这些方法都提供了私有实现。)一个 window 对象或它的 delegate 可以实现这些方法;但是,delegate 的实现级别更高,如果两个地方都有实现的话。
目的地消息的sender
NSDraggingDestination 方法的每一个都只有一个参数:sender,调用方法的对象。在 NSDraggingDestination 方法的实现中,目的地可以发送 NSDraggingInfo 协议消息给 sender 来获取当前 dragging session 上的更多信息,比如查询 dragging pasteboard 或者源的操作 mask。在 Java 里,sender 是一个 NSDragDestination 对象,实现了 NSDraggingInfo 接口。
Dragging Pasteboard
尽管一个标准的 dragging pasteboard(用 [NSPasteboard pasteboardWithName:NSDragPboard] 获得)被提供为获得 dragging 数据的一种便捷方式,但无法保证这会是一次跨进程拖使用的 pasteboard。因此,要确保获得正确的 pasteboard,你的代码应该使用 [sender draggingPasteboard]。
目的地消息的顺序
六个 NSDraggingDestination 方法被调用在一个确定的顺序中:
- 当图片被拖进目的地的边界时,目的地被发送一个 draggingEntered: 方法。这个方法会返回一个值,指明目的地将要执行的 dragging 操作。
- 图片留在目的地的时候,一系列 draggingUpdated: 消息被发送。方法会返回一个值,指明目的地将要执行的 dragging 操作。
- 如果图片被拖出目的地,draggingExited: 被发送,并且 NSDraggingDestination 消息队列会停止。如果它再次进入,队列会重新开始(用一个新的 draggingEntered: 消息)。
- 如果图片被释放了,它会滑回源(然后打断队列)或一个 prepareForDragOperation: 消息被发送到目的地,根据最近最近一次调用 draggingEntered: 或 draggingUpdated: 的返回值。
- 如果 prepareForDragOperation: 消息返回了 YES,一个 performDragOperation: 消息被发送。
- 最终,如果 performDragOperation: 返回了 YES,concludeDragOperation: 被发送。
接收拖操作
这篇文档展示了一段示例代码,可以让一个 view(或 window)接收几种数据类型的 dragging session,基于被拖的类型执行不同的拖操作。样例实现可以接收一种颜色或是一个文件。被拖的颜色会被拷贝,文件会被链接或拷贝。
在一个 view 可以接收拖操作之前,需要注册可以接收的数据类型,通过调用它的 registerForDraggedTypes:,像这样:
[self registerForDraggedTypes:[NSArray arrayWithObjects:
NSColorPboardType, NSFilenamesPboardType, nil]];
现在,不管什么时间只要一个由这些数据类型之一的数据类型 dragging session 进入了 view 的边界,view 就会被发送一系列 NSDraggingDestination 消息。下面的代码是一个得到 draggingEntered: 发送的初始化简单例子。方法从 sender 获得了 dragging pasteboard 和可用的拖操作。如果 pasteboard 包含颜色数据并且源对象许可了拖动,方法会返回 NSDragOperationGeneric,指示目的地对象许可 pasteboard 上的颜色数据的拖动。如果 pasteboard 包含了一个文件名并且源许可了链接,方法会返回 NSDragOperationLink,指示目的地许可链接。如果源不允许链接,目的地也会检查是不是有拷贝操作可以作替代,如果是就返回 NSDragOperationCopy。如果这些测试都失败了,方法返回 NSDragOperationNone。
- (NSDragOperation)draggingEntered:(id )sender {
NSPasteboard *pboard;
NSDragOperation sourceDragMask;
sourceDragMask = [sender draggingSourceOperationMask];
pboard = [sender draggingPasteboard];
if ( [[pboard types] containsObject:NSColorPboardType] ) {
if (sourceDragMask & NSDragOperationGeneric) {
return NSDragOperationGeneric;
}
}
if ( [[pboard types] containsObject:NSFilenamesPboardType] ) {
if (sourceDragMask & NSDragOperationLink) {
return NSDragOperationLink;
} else if (sourceDragMask & NSDragOperationCopy) {
return NSDragOperationCopy;
}
}
return NSDragOperationNone;
}
随着 dragging session 的继续,目的地被发送 draggingUpdated: 消息。你只需要在目的地需要了解被拖拽图片当前位置的时候实现它,可能是改变 dragging 操作或更新你正在提供的某个视觉反馈,例如插入光标。如果没有实现,NSView 假设 dragging 操作从调用 draggingEntered: 起就没有被改变。如果 dragging session 离开了 view 的边框,draggingExited: 方法被调用。如果你需要清理之前的某一个消息,就实现它,比如移除视觉反馈。
当图片被用一个拖操作放下而不是 NSDragOperationNone,目的地被发送 prepareForDragOperation: 紧接着 performDragOperation: 和 concludeDragOperation:。你可以通过在前两个方法中的任一个返回 NO 来取消拖。
在 performDragOperation: 方法里做大量的数据处理;其他两个方法只在必要的时候声明。接下来的代码示例展示了这个方法可能的实现。这个方法再次检查了 pasteboard 的可用数据,并且如果必要的话,它也检查了 dragging 源的操作 mask 里的可用操作。
- (BOOL)performDragOperation:(id )sender {
NSPasteboard *pboard;
NSDragOperation sourceDragMask;
sourceDragMask = [sender draggingSourceOperationMask];
pboard = [sender draggingPasteboard];
if ( [[pboard types] containsObject:NSColorPboardType] ) {
// Only a copy operation allowed so just copy the data
NSColor *newColor = [NSColor colorFromPasteboard:pboard];
[self setColor:newColor];
} else if ( [[pboard types] containsObject:NSFilenamesPboardType] ) {
NSArray *files = [pboard propertyListForType:NSFilenamesPboardType];
// Depending on the dragging source and modifier keys,
// the file data may be copied or linked
if (sourceDragMask & NSDragOperationLink) {
[self addLinkToFiles:files];
} else {
[self addDataFromFiles:files];
}
}
return YES;
}