复制并粘贴 Xamarin.Mac
本文介绍如何使用粘贴板在 Xamarin.Mac 应用程序中提供复制和粘贴。 它演示如何使用可在多个应用之间共享的标准数据类型,以及如何在给定应用中支持自定义数据。
概述
在 Xamarin.Mac 应用程序中使用 C# 和 .NET 时,你可以获得的粘贴板(复制和粘贴)支持与在 Objective-C 中工作的开发人员相同。
本文将介绍在 Xamarin.Mac 应用中使用粘贴板的两种主要方法:
- 标准数据类型 - 由于粘贴板操作通常在两个不相关的应用之间执行,因此两个应用都不知道其他支持的数据类型。 为了最大限度地发挥共享潜力,粘贴板可以保存给定项目的多个表示形式(使用一组标准通用数据类型),这允许使用的应用选取最适合其需求的版本。
- 自定义数据 - 为了支持在 Xamarin.Mac 中复制和粘贴复杂数据,可以定义将由粘贴板处理的自定义数据类型。 例如,一个矢量绘图应用,允许用户复制和粘贴由多种数据类型和点组成的复杂图形。
本文将介绍在 Xamarin.Mac 应用程序中使用粘贴板以支持复制和粘贴操作的基础知识。 强烈建议先阅读 Hello, Mac 一文,特别是 Xcode 和 Interface Builder 简介和输出口和操作部分,因为其中介绍了我们将在本文中使用的关键概念和技术。
你可能还需要查看 Xamarin.Mac 内部机制文档的向 Objective-C 公开 C# 类/方法部分,其中介绍了用于将 C# 类连接到 Objective-C 对象和 UI 元素的 Register
和 Export
属性。
粘贴板入门
粘贴板提供一种标准化机制,用于在给定的应用程序或应用程序之间交换数据。 Xamarin.Mac 应用程序中粘贴板的典型用途是处理复制和粘贴操作,但还支持许多其他操作(如拖放和应用程序服务)。
为了让你快速上手,我们将从使用 Xamarin.Mac 应用中的粘贴板开始,为你提供一个简单、实用的介绍。 稍后,我们将提供有关粘贴板的工作原理和所用方法的深入说明。
在本示例中,我们将创建一个基于文档的简单应用程序,用于管理包含图像视图的窗口。 用户将能够在同一应用中的文档之间复制和粘贴图像,以及从其他应用或多个窗口复制和粘贴图像。
创建 Xamarin 项目
首先创建一个新的基于文档的 Xamarin.Mac 应用,我们将添加复制和粘贴支持。
请执行以下操作:
启动 Visual Studio for Mac,然后单击“新建项目...”链接。
选择 Mac>App>Cocoa App,然后单击“下一步”按钮:
为“项目名称”输入
MacCopyPaste
,并将其他所有内容保留为默认值。 单击“下一步:单击“创建”按钮:
添加 NSDocument
接下来添加将充当应用程序用户界面后台存储的自定义 NSDocument
类。 它将包含单个图像视图,并知道如何将图像从视图复制到默认粘贴板,以及如何从默认粘贴板中获取图像,并将其显示在图像视图中。
右键单击 Solution Pad 中的 Xamarin.Mac 项目,然后选择“添加”>“新建文件...”:
对“名称”输入 ImageDocument
,然后单击“新建”按钮。 编辑 ImageDocument.cs 类,使其如下所示:
using System;
using AppKit;
using Foundation;
using ObjCRuntime;
namespace MacCopyPaste
{
[Register("ImageDocument")]
public class ImageDocument : NSDocument
{
#region Computed Properties
public NSImageView ImageView {get; set;}
public ImageInfo Info { get; set; } = new ImageInfo();
public bool ImageAvailableOnPasteboard {
get {
// Initialize the pasteboard
NSPasteboard pasteboard = NSPasteboard.GeneralPasteboard;
Class [] classArray = { new Class ("NSImage") };
// Check to see if an image is on the pasteboard
return pasteboard.CanReadObjectForClasses (classArray, null);
}
}
#endregion
#region Constructor
public ImageDocument ()
{
}
#endregion
#region Public Methods
[Export("CopyImage:")]
public void CopyImage(NSObject sender) {
// Grab the current image
var image = ImageView.Image;
// Anything to process?
if (image != null) {
// Get the standard pasteboard
var pasteboard = NSPasteboard.GeneralPasteboard;
// Empty the current contents
pasteboard.ClearContents();
// Add the current image to the pasteboard
pasteboard.WriteObjects (new NSImage[] {image});
// Save the custom data class to the pastebaord
pasteboard.WriteObjects (new ImageInfo[] { Info });
// Using a Pasteboard Item
NSPasteboardItem item = new NSPasteboardItem();
string[] writableTypes = {"public.text"};
// Add a data provier to the item
ImageInfoDataProvider dataProvider = new ImageInfoDataProvider (Info.Name, Info.ImageType);
var ok = item.SetDataProviderForTypes (dataProvider, writableTypes);
// Save to pasteboard
if (ok) {
pasteboard.WriteObjects (new NSPasteboardItem[] { item });
}
}
}
[Export("PasteImage:")]
public void PasteImage(NSObject sender) {
// Initialize the pasteboard
NSPasteboard pasteboard = NSPasteboard.GeneralPasteboard;
Class [] classArray = { new Class ("NSImage") };
bool ok = pasteboard.CanReadObjectForClasses (classArray, null);
if (ok) {
// Read the image off of the pasteboard
NSObject [] objectsToPaste = pasteboard.ReadObjectsForClasses (classArray, null);
NSImage image = (NSImage)objectsToPaste[0];
// Display the new image
ImageView.Image = image;
}
Class [] classArray2 = { new Class ("ImageInfo") };
ok = pasteboard.CanReadObjectForClasses (classArray2, null);
if (ok) {
// Read the image off of the pasteboard
NSObject [] objectsToPaste = pasteboard.ReadObjectsForClasses (classArray2, null);
ImageInfo info = (ImageInfo)objectsToPaste[0];
}
}
#endregion
}
}
下面详细介绍一些代码。
以下代码提供一个属性,用于测试默认粘贴板上是否存在图像数据,如果图像可用,true
返回其他 false
:
public bool ImageAvailableOnPasteboard {
get {
// Initialize the pasteboard
NSPasteboard pasteboard = NSPasteboard.GeneralPasteboard;
Class [] classArray = { new Class ("NSImage") };
// Check to see if an image is on the pasteboard
return pasteboard.CanReadObjectForClasses (classArray, null);
}
}
以下代码将附加图像视图中的图像复制到默认粘贴板中:
[Export("CopyImage:")]
public void CopyImage(NSObject sender) {
// Grab the current image
var image = ImageView.Image;
// Anything to process?
if (image != null) {
// Get the standard pasteboard
var pasteboard = NSPasteboard.GeneralPasteboard;
// Empty the current contents
pasteboard.ClearContents();
// Add the current image to the pasteboard
pasteboard.WriteObjects (new NSImage[] {image});
// Save the custom data class to the pastebaord
pasteboard.WriteObjects (new ImageInfo[] { Info });
// Using a Pasteboard Item
NSPasteboardItem item = new NSPasteboardItem();
string[] writableTypes = {"public.text"};
// Add a data provider to the item
ImageInfoDataProvider dataProvider = new ImageInfoDataProvider (Info.Name, Info.ImageType);
var ok = item.SetDataProviderForTypes (dataProvider, writableTypes);
// Save to pasteboard
if (ok) {
pasteboard.WriteObjects (new NSPasteboardItem[] { item });
}
}
}
下面的代码粘贴默认粘贴板中的图像,并将其显示在附加的图像视图中(如果粘贴板包含有效图像):
[Export("PasteImage:")]
public void PasteImage(NSObject sender) {
// Initialize the pasteboard
NSPasteboard pasteboard = NSPasteboard.GeneralPasteboard;
Class [] classArray = { new Class ("NSImage") };
bool ok = pasteboard.CanReadObjectForClasses (classArray, null);
if (ok) {
// Read the image off of the pasteboard
NSObject [] objectsToPaste = pasteboard.ReadObjectsForClasses (classArray, null);
NSImage image = (NSImage)objectsToPaste[0];
// Display the new image
ImageView.Image = image;
}
Class [] classArray2 = { new Class ("ImageInfo") };
ok = pasteboard.CanReadObjectForClasses (classArray2, null);
if (ok) {
// Read the image off of the pasteboard
NSObject [] objectsToPaste = pasteboard.ReadObjectsForClasses (classArray2, null);
ImageInfo info = (ImageInfo)objectsToPaste[0]
}
}
完成本文档后,我们将为 Xamarin.Mac 应用创建用户界面。
生成用户界面
双击 Main.storyboard 文件,在 Xcode 中将其打开。 接下来,添加工具栏和图像,并如下所示对其进行配置:
添加副本并将图像工具栏项粘贴到工具栏左侧。 我们将使用这些快捷方式从“编辑”菜单复制和粘贴。 接下来,将四个图像工具栏项添加到工具栏右侧。 我们将使用这些和一些默认图像来填充图像。
有关使用工具栏的详细信息,请参阅我们的工具栏文档。
接下来,让我们公开工具栏项和图像的以下出口和操作:
有关使用出口和操作的详细信息,请参阅 Hello, Mac 文档的“出口和操作”部分。
启用用户界面
我们已经在 Xcode 中创建了用户界面,并且通过出口和操作公开了用户界面元素,接下来我们需要添加代码以启用用户界面。 双击 Solution Pad 中的 ImageWindow.cs 文件,使其如下所示:
using System;
using Foundation;
using AppKit;
namespace MacCopyPaste
{
public partial class ImageWindow : NSWindow
{
#region Private Variables
ImageDocument document;
#endregion
#region Computed Properties
[Export ("Document")]
public ImageDocument Document {
get {
return document;
}
set {
WillChangeValue ("Document");
document = value;
DidChangeValue ("Document");
}
}
public ViewController ImageViewController {
get { return ContentViewController as ViewController; }
}
public NSImage Image {
get {
return ImageViewController.Image;
}
set {
ImageViewController.Image = value;
}
}
#endregion
#region Constructor
public ImageWindow (IntPtr handle) : base (handle)
{
}
#endregion
#region Override Methods
public override void AwakeFromNib ()
{
base.AwakeFromNib ();
// Create a new document instance
Document = new ImageDocument ();
// Attach to image view
Document.ImageView = ImageViewController.ContentView;
}
#endregion
#region Public Methods
public void CopyImage (NSObject sender)
{
Document.CopyImage (sender);
}
public void PasteImage (NSObject sender)
{
Document.PasteImage (sender);
}
public void ImageOne (NSObject sender)
{
// Load image
Image = NSImage.ImageNamed ("Image01.jpg");
// Set image info
Document.Info.Name = "city";
Document.Info.ImageType = "jpg";
}
public void ImageTwo (NSObject sender)
{
// Load image
Image = NSImage.ImageNamed ("Image02.jpg");
// Set image info
Document.Info.Name = "theater";
Document.Info.ImageType = "jpg";
}
public void ImageThree (NSObject sender)
{
// Load image
Image = NSImage.ImageNamed ("Image03.jpg");
// Set image info
Document.Info.Name = "keyboard";
Document.Info.ImageType = "jpg";
}
public void ImageFour (NSObject sender)
{
// Load image
Image = NSImage.ImageNamed ("Image04.jpg");
// Set image info
Document.Info.Name = "trees";
Document.Info.ImageType = "jpg";
}
#endregion
}
}
下面详细介绍此代码。
首先,我们公开上面创建的 ImageDocument
类的实例:
private ImageDocument _document;
...
[Export ("Document")]
public ImageDocument Document {
get { return _document; }
set {
WillChangeValue ("Document");
_document = value;
DidChangeValue ("Document");
}
}
通过使用 Export
、WillChangeValue
和 DidChangeValue
,我们设置了 Document
属性,以允许 Xcode 中的键值编码和数据绑定。
我们还使用以下属性在 Xcode 中向 UI 添加的图像公开图像:
public ViewController ImageViewController {
get { return ContentViewController as ViewController; }
}
public NSImage Image {
get {
return ImageViewController.Image;
}
set {
ImageViewController.Image = value;
}
}
加载并显示主窗口时,我们将创建 ImageDocument
类的实例,并使用以下代码将 UI 的图像附加到该实例:
public override void AwakeFromNib ()
{
base.AwakeFromNib ();
// Create a new document instance
Document = new ImageDocument ();
// Attach to image view
Document.ImageView = ImageViewController.ContentView;
}
最后,为了响应用户单击复制和粘贴工具栏项,我们调用 ImageDocument
类的实例来执行实际工作:
partial void CopyImage (NSObject sender) {
Document.CopyImage(sender);
}
partial void PasteImage (Foundation.NSObject sender) {
Document.PasteImage(sender);
}
启用“文件”和“编辑”菜单
我们需要执行的最后一项操作是从“文件”菜单(若要创建新主窗口实例)中启用“新建”菜单项,并从“编辑”菜单启用“剪切”、“复制”和“粘贴”菜单项。
若要启用“新建”菜单项,请编辑 AppDelegate.cs 文件并添加以下代码:
public int UntitledWindowCount { get; set;} =1;
...
[Export ("newDocument:")]
void NewDocument (NSObject sender) {
// Get new window
var storyboard = NSStoryboard.FromName ("Main", null);
var controller = storyboard.InstantiateControllerWithIdentifier ("MainWindow") as NSWindowController;
// Display
controller.ShowWindow(this);
// Set the title
controller.Window.Title = (++UntitledWindowCount == 1) ? "untitled" : string.Format ("untitled {0}", UntitledWindowCount);
}
若要启用“剪切”、“复制”和“粘贴”菜单项,编辑 AppDelegate.cs 文件并添加以下代码:
[Export("copy:")]
void CopyImage (NSObject sender)
{
// Get the main window
var window = NSApplication.SharedApplication.KeyWindow as ImageWindow;
// Anything to do?
if (window == null)
return;
// Copy the image to the clipboard
window.Document.CopyImage (sender);
}
[Export("cut:")]
void CutImage (NSObject sender)
{
// Get the main window
var window = NSApplication.SharedApplication.KeyWindow as ImageWindow;
// Anything to do?
if (window == null)
return;
// Copy the image to the clipboard
window.Document.CopyImage (sender);
// Clear the existing image
window.Image = null;
}
[Export("paste:")]
void PasteImage (NSObject sender)
{
// Get the main window
var window = NSApplication.SharedApplication.KeyWindow as ImageWindow;
// Anything to do?
if (window == null)
return;
// Paste the image from the clipboard
window.Document.PasteImage (sender);
}
对于每个菜单项,我们获取当前、最顶部、键窗口并将其强制转换为 ImageWindow
类:
var window = NSApplication.SharedApplication.KeyWindow as ImageWindow;
在此处调用该窗口的 ImageDocument
类实例来处理复制和粘贴操作。 例如:
window.Document.CopyImage (sender);
只有当默认剪贴板或当前活动窗口的图片框中有图像数据时,我们才希望用户能够访问“剪切”、“复制”和“粘贴”菜单项。
让我们将 EditMenuDelegate.cs 文件添加到 Xamarin.Mac 项目,使其如下所示:
using System;
using AppKit;
namespace MacCopyPaste
{
public class EditMenuDelegate : NSMenuDelegate
{
#region Override Methods
public override void MenuWillHighlightItem (NSMenu menu, NSMenuItem item)
{
}
public override void NeedsUpdate (NSMenu menu)
{
// Get list of menu items
NSMenuItem[] Items = menu.ItemArray ();
// Get the key window and determine if the required images are available
var window = NSApplication.SharedApplication.KeyWindow as ImageWindow;
var hasImage = (window != null) && (window.Image != null);
var hasImageOnPasteboard = (window != null) && window.Document.ImageAvailableOnPasteboard;
// Process every item in the menu
foreach(NSMenuItem item in Items) {
// Take action based on the menu title
switch (item.Title) {
case "Cut":
case "Copy":
case "Delete":
// Only enable if there is an image in the view
item.Enabled = hasImage;
break;
case "Paste":
// Only enable if there is an image on the pasteboard
item.Enabled = hasImageOnPasteboard;
break;
default:
// Only enable the item if it has a sub menu
item.Enabled = item.HasSubmenu;
break;
}
}
}
#endregion
}
}
同样,我们获取当前最顶层的窗口,并使用其 ImageDocument
类实例来查看所需的图像数据是否存在。 然后,我们使用 MenuWillHighlightItem
方法基于此状态启用或禁用每个项。
编辑 AppDelegate.cs 文件,使 DidFinishLaunching
方法如下所示:
public override void DidFinishLaunching (NSNotification notification)
{
// Disable automatic item enabling on the Edit menu
EditMenu.AutoEnablesItems = false;
EditMenu.Delegate = new EditMenuDelegate ();
}
首先,禁用自动启用和禁用“编辑”菜单中的菜单项。 接下来,附加上面创建的 EditMenuDelegate
类的实例。
有关详细信息,请参阅我们的菜单文档。
测试应用
准备好一切后,即可测试应用程序。 生成并运行应用,并显示主界面:
如果打开“编辑”菜单,请注意,剪切、复制和粘贴处于禁用状态,因为图像中或默认粘贴板上没有图像:
如果将图像添加到图像并重新打开“编辑”菜单,则会立即启用这些项:
如果复制映像并从文件菜单中选择“新建”,则可以将该图像粘贴到新窗口中:
在以下部分中,我们将详细介绍如何使用 Xamarin.Mac 应用程序中的粘贴板。
关于粘贴板
在 macOS(以前称为 OS X)中,粘贴板 (NSPasteboard
) 为多个服务器进程(如复制和粘贴、拖放和应用程序服务)提供支持。 在以下部分中,我们将仔细探讨几个重要的粘贴板概念。
什么是粘贴板?
NSPasteboard
类提供了一种标准化机制,用于在应用程序之间或在给定应用中交换信息。 粘贴板的主要功能是处理复制和粘贴操作:
- 当用户在应用中选择项目并使用“剪切”或“复制”菜单项时,所选项目的一个或多个表示形式将放在粘贴板上。
- 当用户使用“粘贴”菜单项(在同一应用或不同应用内)时,可以处理的数据版本将从粘贴板复制并添加到应用。
不太明显的粘贴板使用包括查找、拖动、拖放以及应用程序服务操作:
- 当用户启动拖动操作时,拖动数据将复制到粘贴板。 如果拖动操作以拖放到另一个应用结束,该应用将从粘贴板复制数据。
- 对于翻译服务,要翻译的数据将通过请求的应用复制到粘贴板。 应用程序服务从粘贴板检索数据,执行转换,然后将数据粘贴回粘贴板。
在它们最简单的形式中,粘贴板用于在给定应用内部或应用之间移动数据,因此存在于应用进程之外的特殊全局内存区域中。 虽然粘贴板的概念很容易掌握,但必须考虑几个较为复杂的细节。 下面将详细介绍这些内容。
命名粘贴板
粘贴板可以是公共的,也可以是专用的,可用于应用程序内或多个应用之间的各种用途。 macOS提供了几个标准的粘贴板,每个粘贴板都有特定且定义明确的使用方式:
NSGeneralPboard
- 剪切、复制和粘贴操作的默认粘贴板。NSRulerPboard
- 支持对标尺执行剪切、复制和粘贴操作。NSFontPboard
- 支持对NSFont
对象执行剪切、复制和粘贴操作。NSFindPboard
- 支持可共享搜索文本的应用程序特定的查找面板。NSDragPboard
- 支持拖放操作。
在大多数情况下,你将使用系统定义的粘贴板之一。 但在某些情况下,可能需要创建自己的粘贴板。 在这些情况下,可以使用 NSPasteboard
类的 FromName (string name)
方法创建具有给定名称的自定义粘贴板。
(可选)可以调用 NSPasteboard
类的 CreateWithUniqueName
方法来创建唯一命名的粘贴板。
粘贴板项
应用程序写入粘贴板的每个数据都被视为粘贴板项,粘贴板可以同时保存多个项。 这样,应用就可以写入复制到粘贴板(例如纯文本和带格式文本)的多个版本,检索应用只能读取可以处理的数据(如纯文本)。
数据表示形式和统一类型标识符
粘贴板操作通常发生在两个(或更多)应用程序之间,这些应用程序不知道彼此或每个应用程序可以处理的数据类型。 如上述部分所述,为了最大限度地发挥共享信息的潜力,粘贴板可以保存复制和粘贴数据的多个表示形式。
每种表示都是通过统一类型标识符 (UTI) 来识别的,UTI 只不过是一个简单的字符串,用于唯一标识呈现的数据类型(有关详细信息,请参阅 Apple 的统一类型标识符概述文档)。
如果要创建自定义数据类型(例如,矢量绘图应用中的绘图对象),则可以创建自己的 UTI 以在复制和粘贴操作中唯一标识它。
当应用准备粘贴从粘贴板复制的数据时,它必须找到最符合其功能(如果有)的表示形式。 通常,这将是可用的最丰富的类型(例如,对于文字处理应用而言,是格式化文本),然后按需回退到最简单的可用形式(对于简单的文本编辑器而言,是纯文本)。
承诺的数据
一般来说,应尽可能多地提供要复制的数据的表示形式,以最大限度地在应用之间共享。 但是,由于时间或内存限制,实际将每种数据类型写入粘贴板可能不切实际。
在这种情况下,可以将第一种数据表示形式放在粘贴板上,接收应用可以请求另一种表示形式,该表示形式可以在粘贴操作之前立即生成。
在粘贴板中放置初始项时,将指定一个或多个其他表示形式由符合 NSPasteboardItemDataProvider
接口的对象提供。 这些对象将根据需要提供额外的表示形式,以满足接收应用的要求。
更改计数
每个粘贴板都维护一个更改计数,每次声明新所有者时,该计数都会递增。 应用可以通过检查更改计数的值来确定粘贴板的内容自上次检查以来是否有更改。
使用 NSPasteboard
类的 ChangeCount
和 ClearContents
方法修改给定粘贴板的更改计数。
将数据复制到粘贴板
执行复制操作,首先访问粘贴板,清除任何现有内容,然后将数据所需的多个表示形式写入粘贴板。
例如:
// Get the standard pasteboard
var pasteboard = NSPasteboard.GeneralPasteboard;
// Empty the current contents
pasteboard.ClearContents();
// Add the current image to the pasteboard
pasteboard.WriteObjects (new NSImage[] {image});
通常,只需写入常规粘贴板,如上例所示。 发送到 WriteObjects
方法的任何对象都必须符合 INSPasteboardWriting
接口。 多个内置类(如 NSString
、NSImage
、NSURL
、NSColor
、NSAttributedString
和 NSPasteboardItem
)自动符合此接口。
如果要将自定义数据类写入粘贴板,它必须符合 INSPasteboardWriting
接口,或包装在 NSPasteboardItem
类的实例中(请参阅下面的自定义数据类型部分)。
从粘贴板读取数据
如上所述,为了最大化应用之间共享数据的可能性,可以将复制数据的多个表示形式写入粘贴板。 接收应用可以为其功能选择最丰富的版本(如果有)。
简单粘贴操作
使用 ReadObjectsForClasses
方法从粘贴板读取数据。 它将需要两个参数:
- 要从粘贴板读取的基于
NSObject
类类型的数组。 应首先按照最期望的数据类型排序,然后将剩余类型按偏好递减顺序排列。 - 包含其他约束(例如限制为特定 URL 内容类型)或空字典(如果不需要进一步的约束)的字典。
该方法返回一个符合我们传入条件的项目数组,因此最多包含所请求的数据类型数量。 也有可能不存在任何请求的类型,此时将返回一个空数组。
例如,以下代码检查常规剪贴板中是否存在 NSImage
,如果存在,则将其显示在图像框中:
[Export("PasteImage:")]
public void PasteImage(NSObject sender) {
// Initialize the pasteboard
NSPasteboard pasteboard = NSPasteboard.GeneralPasteboard;
Class [] classArray = { new Class ("NSImage") };
bool ok = pasteboard.CanReadObjectForClasses (classArray, null);
if (ok) {
// Read the image off of the pasteboard
NSObject [] objectsToPaste = pasteboard.ReadObjectsForClasses (classArray, null);
NSImage image = (NSImage)objectsToPaste[0];
// Display the new image
ImageView.Image = image;
}
Class [] classArray2 = { new Class ("ImageInfo") };
ok = pasteboard.CanReadObjectForClasses (classArray2, null);
if (ok) {
// Read the image off of the pasteboard
NSObject [] objectsToPaste = pasteboard.ReadObjectsForClasses (classArray2, null);
ImageInfo info = (ImageInfo)objectsToPaste[0];
}
}
请求多个数据类型
根据创建的 Xamarin.Mac 应用程序的类型,它或能处理粘贴数据的多个表示形式。 在这种情况下,从粘贴板检索数据有两种情况:
- 调用
ReadObjectsForClasses
方法一次,并提供你希望的所有表示形式的数组(按优先顺序排列)。 - 每次调用
ReadObjectsForClasses
方法,要求使用不同的类型数组。
有关从粘贴板检索数据的更多详细信息,请参阅上面的简单粘贴操作部分。
检查现有数据类型
有时,你可能想要检查粘贴板是否包含给定的数据表示形式,而不实际从粘贴板读取数据(例如仅在有效数据存在时启用“粘贴”菜单项)。
调用粘贴板的 CanReadObjectForClasses
方法以查看它是否包含给定类型。
例如,以下代码确定常规粘贴板是否包含 NSImage
实例:
public bool ImageAvailableOnPasteboard {
get {
// Initialize the pasteboard
NSPasteboard pasteboard = NSPasteboard.GeneralPasteboard;
Class [] classArray = { new Class ("NSImage") };
// Check to see if an image is on the pasteboard
return pasteboard.CanReadObjectForClasses (classArray, null);
}
}
从粘贴板读取 URL
基于给定 Xamarin.Mac 应用程序的功能,可能需要从粘贴板中读取 URL,但只有当它们满足特定条件集时(例如指向文件或特定数据类型的 URL)才读取。 在这种情况下,可以使用 CanReadObjectForClasses
或 ReadObjectsForClasses
方法的第二个参数指定其他搜索条件。
自定义数据类型
有时需要将自己的自定义类型保存到 Xamarin.Mac 应用中的粘贴板。 例如,允许用户复制和粘贴绘图对象的矢量绘图应用。
在这种情况下,需要设计数据自定义类,使其继承自 NSObject
,并符合几个接口(INSCoding
、INSPasteboardWriting
和 INSPasteboardReading
)。 (可选)可以使用 NSPasteboardItem
封装要复制或粘贴的数据。
下面将详细介绍这两个选项。
使用自定义类
在本部分中,我们将扩展我们在本文档开头创建的简单示例应用,并添加自定义类来跟踪我们在窗口之间复制和粘贴的图像的相关信息。
向项目中添加一个新类,并将其命名为 ImageInfo.cs。 编辑文件,使其如下所示:
using System;
using AppKit;
using Foundation;
namespace MacCopyPaste
{
[Register("ImageInfo")]
public class ImageInfo : NSObject, INSCoding, INSPasteboardWriting, INSPasteboardReading
{
#region Computed Properties
[Export("name")]
public string Name { get; set; }
[Export("imageType")]
public string ImageType { get; set; }
#endregion
#region Constructors
[Export ("init")]
public ImageInfo ()
{
}
public ImageInfo (IntPtr p) : base (p)
{
}
[Export ("initWithCoder:")]
public ImageInfo(NSCoder decoder) {
// Decode data
NSString name = decoder.DecodeObject("name") as NSString;
NSString type = decoder.DecodeObject("imageType") as NSString;
// Save data
Name = name.ToString();
ImageType = type.ToString ();
}
#endregion
#region Public Methods
[Export ("encodeWithCoder:")]
public void EncodeTo (NSCoder encoder) {
// Encode data
encoder.Encode(new NSString(Name),"name");
encoder.Encode(new NSString(ImageType),"imageType");
}
[Export ("writableTypesForPasteboard:")]
public virtual string[] GetWritableTypesForPasteboard (NSPasteboard pasteboard) {
string[] writableTypes = {"com.xamarin.image-info", "public.text"};
return writableTypes;
}
[Export ("pasteboardPropertyListForType:")]
public virtual NSObject GetPasteboardPropertyListForType (string type) {
// Take action based on the requested type
switch (type) {
case "com.xamarin.image-info":
return NSKeyedArchiver.ArchivedDataWithRootObject(this);
case "public.text":
return new NSString(string.Format("{0}.{1}", Name, ImageType));
}
// Failure, return null
return null;
}
[Export ("readableTypesForPasteboard:")]
public static string[] GetReadableTypesForPasteboard (NSPasteboard pasteboard){
string[] readableTypes = {"com.xamarin.image-info", "public.text"};
return readableTypes;
}
[Export ("readingOptionsForType:pasteboard:")]
public static NSPasteboardReadingOptions GetReadingOptionsForType (string type, NSPasteboard pasteboard) {
// Take action based on the requested type
switch (type) {
case "com.xamarin.image-info":
return NSPasteboardReadingOptions.AsKeyedArchive;
case "public.text":
return NSPasteboardReadingOptions.AsString;
}
// Default to property list
return NSPasteboardReadingOptions.AsPropertyList;
}
[Export ("initWithPasteboardPropertyList:ofType:")]
public NSObject InitWithPasteboardPropertyList (NSObject propertyList, string type) {
// Take action based on the requested type
switch (type) {
case "com.xamarin.image-info":
return new ImageInfo();
case "public.text":
return new ImageInfo();
}
// Failure, return null
return null;
}
#endregion
}
}
在以下部分中,我们将详细查看此类。
继承和接口
在自定义数据类可以写入剪贴板或从剪贴板读取之前,它必须符合 INSPastebaordWriting
和 INSPasteboardReading
接口。 此外,它必须继承自 NSObject
,并符合 INSCoding
接口:
[Register("ImageInfo")]
public class ImageInfo : NSObject, INSCoding, INSPasteboardWriting, INSPasteboardReading
...
该类还必须使用 Register
指令向 Objective-C 公开,并且必须使用 Export
公开任何必需的属性或方法。 例如:
[Export("name")]
public string Name { get; set; }
[Export("imageType")]
public string ImageType { get; set; }
我们将公开这个类将要包含的两个数据字段 - 图像的名称和类型(jpg、png 等)。
有关详细信息,请参阅 Xamarin.Mac 内部文档中的“将 C# 类/方法公开给 Objective-C”部分,该部分解释了用于将 C# 类与 Objective-C 对象和 UI 元素连接起来的 Register
和 Export
属性。
构造函数
自定义数据类需要两个构造函数(正确公开到 Objective-C),以便可以从粘贴板读取它:
[Export ("init")]
public ImageInfo ()
{
}
[Export ("initWithCoder:")]
public ImageInfo(NSCoder decoder) {
// Decode data
NSString name = decoder.DecodeObject("name") as NSString;
NSString type = decoder.DecodeObject("imageType") as NSString;
// Save data
Name = name.ToString();
ImageType = type.ToString ();
}
首先,我们在 init
的默认 Objective-C 方法下公开空构造函数。
接下来,我们公开一个符合 NSCoding
的构造函数,它将在粘贴操作下使用导出的名称 initWithCoder
从剪贴板中创建对象的新实例。
这个构造函数采用一个NSCoder
(由 NSKeyedArchiver
在写入剪贴板时创建),提取键值对的数据,并将其保存到数据类的属性字段中。
写入粘贴板
通过符合 INSPasteboardWriting
接口,我们需要公开两个方法,以及可选的第三个方法,以便可以将类写入粘贴板。
首先,我们需要告知粘贴板可以写入自定义类的数据类型表示形式:
[Export ("writableTypesForPasteboard:")]
public virtual string[] GetWritableTypesForPasteboard (NSPasteboard pasteboard) {
string[] writableTypes = {"com.xamarin.image-info", "public.text"};
return writableTypes;
}
每种表示都是通过统一类型标识符 (UTI) 来识别的,UTI 只不过是一个简单的字符串,用于唯一标识呈现的数据类型(有关详细信息,请参阅 Apple 的统一类型标识符概述文档)。
对于自定义格式,我们将创建自己的 UTI:"com.xamarin.image-info"(请注意,反向表示法与应用标识符一样)。 我们的类还能够将标准字符串写入粘贴板 (public.text
)。
接下来,我们需要以实际写入粘贴板的请求格式创建对象:
[Export ("pasteboardPropertyListForType:")]
public virtual NSObject GetPasteboardPropertyListForType (string type) {
// Take action based on the requested type
switch (type) {
case "com.xamarin.image-info":
return NSKeyedArchiver.ArchivedDataWithRootObject(this);
case "public.text":
return new NSString(string.Format("{0}.{1}", Name, ImageType));
}
// Failure, return null
return null;
}
对于 public.text
类型,我们将返回一个简单的格式化 NSString
对象。 对于自定义 com.xamarin.image-info
类型,我们使用 NSKeyedArchiver
和 NSCoder
接口将自定义数据类编码为键/值配对存档。 我们需要实现以下方法来实际处理编码:
[Export ("encodeWithCoder:")]
public void EncodeTo (NSCoder encoder) {
// Encode data
encoder.Encode(new NSString(Name),"name");
encoder.Encode(new NSString(ImageType),"imageType");
}
单个键/值对将写入编码器,并使用上面添加的第二个构造函数进行解码。
(可选)我们可以包括以下方法,用于在将数据写入粘贴板时定义任何选项:
[Export ("writingOptionsForType:pasteboard:"), CompilerGenerated]
public virtual NSPasteboardWritingOptions GetWritingOptionsForType (string type, NSPasteboard pasteboard) {
return NSPasteboardWritingOptions.WritingPromised;
}
目前仅提供 WritingPromised
选项,当给定类型仅被承诺而未实际写入剪贴板时,应使用此选项。 有关详细信息,请参阅上面的承诺的数据部分。
使用这些方法,可以使用以下代码将自定义类写入粘贴板:
// Get the standard pasteboard
var pasteboard = NSPasteboard.GeneralPasteboard;
// Empty the current contents
pasteboard.ClearContents();
// Add info to the pasteboard
pasteboard.WriteObjects (new ImageInfo[] { Info });
从粘贴板读取
通过符合 INSPasteboardReading
接口,我们需要公开三种方法,以便从粘贴板读取自定义数据类。
首先,我们需要告诉粘贴板,自定义类可以从剪贴板读取哪些数据类型表示形式:
[Export ("readableTypesForPasteboard:")]
public static string[] GetReadableTypesForPasteboard (NSPasteboard pasteboard){
string[] readableTypes = {"com.xamarin.image-info", "public.text"};
return readableTypes;
}
同样,这些定义为简单的 UTI,也是我们在上面写入粘贴板部分中定义的相同类型。
接下来,我们需要告知粘贴板如何使用以下方法读取每个 UTI 类型:
[Export ("readingOptionsForType:pasteboard:")]
public static NSPasteboardReadingOptions GetReadingOptionsForType (string type, NSPasteboard pasteboard) {
// Take action based on the requested type
switch (type) {
case "com.xamarin.image-info":
return NSPasteboardReadingOptions.AsKeyedArchive;
case "public.text":
return NSPasteboardReadingOptions.AsString;
}
// Default to property list
return NSPasteboardReadingOptions.AsPropertyList;
}
对于 com.xamarin.image-info
类型,我们将通过调用添加到类的 initWithCoder:
构造函数,告知粘贴板在将类写入粘贴板时,使用 NSKeyedArchiver
创建的键/值对解码。
最后,我们需要添加以下方法来从粘贴板读取其他 UTI 数据表示形式:
[Export ("initWithPasteboardPropertyList:ofType:")]
public NSObject InitWithPasteboardPropertyList (NSObject propertyList, string type) {
// Take action based on the requested type
switch (type) {
case "public.text":
return new ImageInfo();
}
// Failure, return null
return null;
}
有了所有这些方法,可以使用以下代码从粘贴板读取自定义数据类:
// Initialize the pasteboard
NSPasteboard pasteboard = NSPasteboard.GeneralPasteboard;
var classArrayPtrs = new [] { Class.GetHandle (typeof(ImageInfo)) };
NSArray classArray = NSArray.FromIntPtrs (classArrayPtrs);
// NOTE: Sending messages directly to the base Objective-C API because of this defect:
// https://bugzilla.xamarin.com/show_bug.cgi?id=31760
// Check to see if image info is on the pasteboard
ok = bool_objc_msgSend_IntPtr_IntPtr (pasteboard.Handle, Selector.GetHandle ("canReadObjectForClasses:options:"), classArray.Handle, IntPtr.Zero);
if (ok) {
// Read the image off of the pasteboard
NSObject [] objectsToPaste = NSArray.ArrayFromHandle<Foundation.NSObject>(IntPtr_objc_msgSend_IntPtr_IntPtr (pasteboard.Handle, Selector.GetHandle ("readObjectsForClasses:options:"), classArray.Handle, IntPtr.Zero));
ImageInfo info = (ImageInfo)objectsToPaste[0];
}
使用 NSPasteboardItem
有时你可能需要将自定义项写入剪贴板,而这些项并不需要创建自定义类,或者你只想以通用格式提供数据,仅按需提供。 对于这些情况,可以使用 NSPasteboardItem
。
NSPasteboardItem
提供对写入剪贴板的数据的精细控制,并且它是为临时访问而设计的 - 在将其写入剪贴板后应予弃置。
写入数据
若要将自定义数据写入 NSPasteboardItem
,需要提供自定义 NSPasteboardItemDataProvider
。 向项目中添加一个新类,并将其命名为 ImageInfoDataProvider.cs。 编辑文件,使其如下所示:
using System;
using AppKit;
using Foundation;
namespace MacCopyPaste
{
[Register("ImageInfoDataProvider")]
public class ImageInfoDataProvider : NSPasteboardItemDataProvider
{
#region Computed Properties
public string Name { get; set;}
public string ImageType { get; set;}
#endregion
#region Constructors
[Export ("init")]
public ImageInfoDataProvider ()
{
}
public ImageInfoDataProvider (string name, string imageType)
{
// Initialize
this.Name = name;
this.ImageType = imageType;
}
protected ImageInfoDataProvider (NSObjectFlag t){
}
protected internal ImageInfoDataProvider (IntPtr handle){
}
#endregion
#region Override Methods
[Export ("pasteboardFinishedWithDataProvider:")]
public override void FinishedWithDataProvider (NSPasteboard pasteboard)
{
}
[Export ("pasteboard:item:provideDataForType:")]
public override void ProvideDataForType (NSPasteboard pasteboard, NSPasteboardItem item, string type)
{
// Take action based on the type
switch (type) {
case "public.text":
// Encode the data to string
item.SetStringForType(string.Format("{0}.{1}", Name, ImageType),type);
break;
}
}
#endregion
}
}
与自定义数据类一样,我们需要使用 Register
和 Export
指令将其公开给 Objective-C。 该类必须继承自 NSPasteboardItemDataProvider
,并且必须实现 FinishedWithDataProvider
和 ProvideDataForType
方法。
使用 ProvideDataForType
方法提供将在 NSPasteboardItem
中包装的数据,如下所示:
[Export ("pasteboard:item:provideDataForType:")]
public override void ProvideDataForType (NSPasteboard pasteboard, NSPasteboardItem item, string type)
{
// Take action based on the type
switch (type) {
case "public.text":
// Encode the data to string
item.SetStringForType(string.Format("{0}.{1}", Name, ImageType),type);
break;
}
}
在这种情况下,我们存储了关于图像的两条信息(名称和图像类型),并将它们写入一个简单的字符串 (public.text
)。
将数据键入粘贴板,请使用以下代码:
// Get the standard pasteboard
var pasteboard = NSPasteboard.GeneralPasteboard;
// Using a Pasteboard Item
NSPasteboardItem item = new NSPasteboardItem();
string[] writableTypes = {"public.text"};
// Add a data provider to the item
ImageInfoDataProvider dataProvider = new ImageInfoDataProvider (Info.Name, Info.ImageType);
var ok = item.SetDataProviderForTypes (dataProvider, writableTypes);
// Save to pasteboard
if (ok) {
pasteboard.WriteObjects (new NSPasteboardItem[] { item });
}
读取数据
若要从粘贴板中读取数据,请使用以下代码:
// Initialize the pasteboard
NSPasteboard pasteboard = NSPasteboard.GeneralPasteboard;
Class [] classArray = { new Class ("NSImage") };
bool ok = pasteboard.CanReadObjectForClasses (classArray, null);
if (ok) {
// Read the image off of the pasteboard
NSObject [] objectsToPaste = pasteboard.ReadObjectsForClasses (classArray, null);
NSImage image = (NSImage)objectsToPaste[0];
// Do something with data
...
}
Class [] classArray2 = { new Class ("ImageInfo") };
ok = pasteboard.CanReadObjectForClasses (classArray2, null);
if (ok) {
// Read the image off of the pasteboard
NSObject [] objectsToPaste = pasteboard.ReadObjectsForClasses (classArray2, null);
// Do something with data
...
}
总结
本文详细介绍了如何使用 Xamarin.Mac 应用程序中的粘贴板来支持复制和粘贴操作。 首先,它引入了一个简单的示例,让你熟悉标准粘贴板操作。 然后,详细介绍了粘贴板以及如何从粘贴板读取和写入数据。 最后,它考虑了如何使用自定义数据类型来支持在应用中复制和粘贴复杂数据类型。