复制并粘贴 Xamarin.Mac

本文介绍如何使用粘贴板在 Xamarin.Mac 应用程序中提供复制和粘贴。 它演示如何使用可在多个应用之间共享的标准数据类型,以及如何支持给定应用中的自定义数据。

概述

在 Xamarin.Mac 应用程序中使用 C# 和 .NET 时,可以访问开发人员使用的 Objective-C 同一粘贴板(复制和粘贴)支持。

在本文中,我们将介绍在 Xamarin.Mac 应用中使用粘贴板的两种主要方法:

  1. 标准数据类型 - 由于粘贴板操作通常在两个不相关的应用之间执行,应用都不知道另一个应用支持的数据类型。 为了最大限度地发挥共享潜力,粘贴板可以保存给定项目的多个表示形式(使用一组标准通用数据类型),这允许使用的应用选取最适合其需求的版本。
  2. 自定义数据 - 为了支持在 Xamarin.Mac 中复制和粘贴复杂数据,可以定义将由粘贴板处理的自定义数据类型。 例如,一个矢量绘图应用,允许用户复制和粘贴由多个数据类型和点组成的复杂形状。

Example of the running app

在本文中,我们将介绍在 Xamarin.Mac 应用程序中使用粘贴板以支持复制和粘贴操作的基础知识。 强烈建议先浏览 Hello、Mac 文章,特别是 Xcode 和 Interface Builder 和输出口和 操作 简介部分,因为它介绍本文中将要使用的关键概念和技术。

你可能还需要查看 Xamarin.Mac Internals 文档的“公开 C# 类/方法Objective-C”部分,其中还介绍了ExportRegister用于将 C# 类与Objective-C对象和 UI 元素连接起来的特性。

粘贴板入门

粘贴板提供一种标准化机制,用于在给定的应用程序或应用程序之间交换数据。 Xamarin.Mac 应用程序中粘贴板的典型用途是处理复制和粘贴操作,但还支持许多其他操作(如拖放和应用程序服务)。

为了快速离开地面,我们将从一个简单的实用介绍开始,了解如何在 Xamarin.Mac 应用中使用粘贴板。 稍后,我们将提供有关粘贴板的工作原理和所用方法的深入说明。

在本示例中,我们将创建一个基于文档的简单应用程序,用于管理包含图像视图的窗口。 用户将能够在同一应用中的文档之间复制和粘贴图像,以及从其他应用或多个窗口复制和粘贴图像。

创建 Xamarin 项目

首先,我们将创建一个新的基于文档的 Xamarin.Mac 应用,我们将添加复制和粘贴支持。

请执行以下操作:

  1. 启动 Visual Studio for Mac,然后单击“ 新建项目...” 链接。

  2. 选择 Mac>应用 Cocoa 应用>,然后单击“下一步”按钮:

    Creating a new Cocoa app project

  3. 输入MacCopyPaste项目名称,并将其他所有内容保留为默认值。 单击“下一步:

    Setting the name of the project

  4. 单击“创建”按钮:

    Confirming the new project settings

添加 NSDocument

接下来,我们将添加将充当应用程序用户界面后台存储的自定义 NSDocument 类。 它将包含单个图像视图,并知道如何将图像从视图复制到默认粘贴板,以及如何从默认粘贴板中获取图像,并将其显示在图像视图中。

右键单击 Solution Pad 中的 Xamarin.Mac 项目,然后选择“添加新>文件”。.

Adding an NSDocument to the project

对“名称”输入 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 中打开该文件。 接下来,添加工具栏和图像,并按如下所示对其进行配置:

Editing the toolbar

将图像工具栏项复制并粘贴到工具栏左侧。 我们将使用这些快捷方式从“编辑”菜单复制和粘贴。 接下来,将四个 图像工具栏项 添加到工具栏右侧。 我们将使用这些图像来很好地填充图像和一些默认图像。

有关使用工具栏的详细信息,请参阅我们的 工具栏 文档。

接下来,让我们公开工具栏项和图像的以下出口和操作:

Creating outlets and actions

有关使用出口和操作的详细信息,请参阅 Hello, Mac 文档的“出口和操作”部分。

启用用户界面

通过在 Xcode 中创建的用户界面以及通过出口和操作公开的 UI 元素,我们需要添加代码以启用 UI。 双击 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");
    }
}

通过使用 ExportWillChangeValue> 设置DidChangeValueDocument属性,以便在 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);
}

有关详细信息,请参阅 Windows 文档的“使用多个 Windows”部分。

若要启用剪切、复制和粘贴菜单项,请编辑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 实例。

有关详细信息,请参阅菜单文档。

测试应用

准备好一切后,即可测试应用程序。 生成并运行应用,并显示主界面:

Running the application

如果打开“编辑”菜单,请注意 “剪切”、“ 复制 ”和 “粘贴 ”处于禁用状态,因为图像中或默认粘贴板中没有图像:

Opening the Edit menu

如果将图像添加到图像并重新打开“编辑”菜单,则现在将启用这些项:

Showing the Edit menu items are enabled

如果复制映像并从文件菜单中选择“ 新建 ”,则可以将该图像粘贴到新窗口中:

Pasting an image into a new window

在以下部分中,我们将详细介绍如何使用 Xamarin.Mac 应用程序中的粘贴板。

关于粘贴板

在 macOS(以前称为 OS X)中,粘贴板(NSPasteboard)为多个服务器进程(如复制和粘贴、拖放和应用程序服务)提供支持。 在以下部分中,我们将仔细了解几个关键粘贴板概念。

什么是粘贴板?

NSPasteboard 类提供了一种标准化机制,用于在应用程序之间或在给定应用中交换信息。 粘贴板的主要功能是处理复制和粘贴操作:

  1. 当用户在应用中选择某个项并使用 “剪切 ”或 “复制 ”菜单项时,所选项目的一个或多个表示形式将放在粘贴板上。
  2. 当用户使用 “粘贴” 菜单项(在同一应用或不同应用内),可以处理的数据版本将从粘贴板复制并添加到应用。

不太明显的粘贴板使用包括查找、拖动、拖放以及应用程序服务操作:

  • 当用户启动拖动操作时,拖动数据将复制到粘贴板。 如果拖动操作以拖放到另一个应用结束,该应用将从粘贴板复制数据。
  • 对于翻译服务,要翻译的数据将通过请求的应用复制到粘贴板。 应用程序服务从粘贴板检索数据,执行转换,然后将数据粘贴回粘贴板。

在最简单的形式中,粘贴板用于在给定应用内移动数据,或在应用之间移动数据,并在应用进程外的特殊全局内存区域中存在。 虽然粘贴板的概念很容易掌握,但必须考虑几个更复杂的细节。 下面将详细介绍这些内容。

命名粘贴板

粘贴板可以是公共的,也可以是专用的,可用于应用程序内或多个应用之间的各种用途。 macOS 提供了多个标准粘贴板,每个粘贴板都有一个特定的定义完善的用法:

  • NSGeneralPboard- 剪切、复制粘贴操作的默认粘贴
  • NSRulerPboard- 支持对标尺执行剪切复制粘贴操作。
  • NSFontPboard- 支持对对象执行NSFont剪切复制粘贴操作。
  • NSFindPboard - 支持可共享搜索文本的应用程序特定的查找面板。
  • NSDragPboard - 支持 拖放 操作。

在大多数情况下,你将使用系统定义的粘贴板之一。 但在某些情况下,可能需要创建自己的粘贴板。 在这些情况下,可以使用 FromName (string name) 类的方法 NSPasteboard 创建具有给定名称的自定义粘贴板。

(可选)可以调用 CreateWithUniqueName 类的方法 NSPasteboard 来创建唯一命名的粘贴板。

粘贴板项

应用程序写入粘贴板的每个数据都被视为 粘贴板项 ,粘贴板可以同时保存多个项目。 这样,应用就可以写入复制到粘贴板(例如纯文本和带格式文本)的多个版本,检索应用只能读取可以处理的数据(如纯文本)。

数据表示形式和统一类型标识符

粘贴板操作通常发生在两个(或更多)应用程序之间,这些应用程序不知道彼此或每个应用程序可以处理的数据类型。 如上述部分所述,为了最大限度地发挥共享信息的潜力,粘贴板可以保存复制和粘贴数据的多个表示形式。

每个表示形式都通过统一类型标识符(UTI)进行标识,这不仅仅是一个简单的字符串,用于唯一标识所显示的日期类型(有关详细信息,请参阅 Apple 的 统一类型标识符概述 文档)。

如果要创建自定义数据类型(例如,矢量绘图应用中的绘图对象),则可以创建自己的 UTI 以在复制和粘贴操作中唯一标识它。

当应用准备粘贴从粘贴板复制的数据时,它必须找到最符合其功能(如果有)的表示形式。 通常,这是可用的最丰富的类型(例如字处理应用的格式化文本),回退到最简单形式的可用(简单文本编辑器的纯文本)。

承诺的数据

一般来说,应尽可能多地提供要复制的数据的表示形式,以最大限度地在应用之间共享。 但是,由于时间或内存限制,实际将每种数据类型写入粘贴板可能不切实际。

在这种情况下,可以将第一个数据表示形式放在粘贴板上,接收应用可以请求不同的表示形式,该表示形式可以在粘贴操作之前立即生成。

在粘贴板中放置初始项时,将指定一个或多个可用的其他表示形式由符合 NSPasteboardItemDataProvider 接口的对象提供。 这些对象将按需提供额外的表示形式,由接收应用请求。

更改计数

每个粘贴板都维护每次 声明新所有者时递增的更改计数 。 应用可以通过检查更改计数的值来确定粘贴板的内容自上次检查以来是否发生了更改。

ChangeCount使用类的NSPasteboardClearContents方法修改给定粘贴板的更改计数。

将数据复制到粘贴板

首先访问粘贴板、清除任何现有内容并写入粘贴板所需的任意数据表示形式来执行复制操作。

例如:

// 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,、NSImageNSURLNSColorNSAttributedStringNSPasteboardItem)自动符合此接口。

如果要将自定义数据类写入粘贴板,则它必须符合 INSPasteboardWriting 接口或包装在类实例 NSPasteboardItem 中(请参阅 下面的“自定义数据类型 ”部分)。

从粘贴板读取数据

如前所述,为了最大限度地在应用之间共享数据的可能性,可能会将复制数据的多个表示形式写入粘贴板。 接收应用可以为其功能选择最丰富的版本(如果有)。

简单粘贴操作

使用 ReadObjectsForClasses 该方法从粘贴板读取数据。 它将需要两个参数:

  1. 要从粘贴板读取的 NSObject 基于类类型的数组。 首先应使用最所需的数据类型对此项进行排序,而剩余的任何类型都以递减首选项进行排序。
  2. 包含其他约束(例如限制为特定 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 应用程序的类型,它可能能够处理粘贴数据的多个表示形式。 在这种情况下,有两种从粘贴板检索数据的方案:

  1. 对方法进行单个调用 ReadObjectsForClasses ,并提供所需的所有表示形式的数组(按首选顺序)。
  2. 每次对请求不同类型数组的方法进行多次调用 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)。 在这种情况下,可以使用或ReadObjectsForClasses方法的第CanReadObjectForClasses二个参数指定其他搜索条件。

自定义数据类型

有时需要将自己的自定义类型保存到 Xamarin.Mac 应用中的粘贴板。 例如,允许用户复制和粘贴绘图对象的矢量绘图应用。

在这种情况下,需要设计数据自定义类,使其继承NSObject并符合几个接口(INSCodingINSPasteboardWritingINSPasteboardReading)。 (可选)可以使用 a 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
    }
}
    

在以下部分中,我们将详细查看此类。

继承和接口

在自定义数据类可以写入粘贴板或从粘贴板读取之前,它必须符合 INSPastebaordWritingINSPasteboardReading 接口。 此外,它必须继承自 NSObject 并符合 INSCoding 接口:

[Register("ImageInfo")]
public class ImageInfo : NSObject, INSCoding, INSPasteboardWriting, INSPasteboardReading
...

该类还必须公开给 Objective-C 使用 Register 指令,并且必须使用该指令公开任何必需的属性或方法 Export。 例如:

[Export("name")]
public string Name { get; set; }

[Export("imageType")]
public string ImageType { get; set; }

我们将公开此类将包含的两个数据字段 - 图像的名称及其类型(jpg、png 等)。

有关详细信息,请参阅 Xamarin.Mac 内部文档“公开 C# 类/方法”Objective-C部分,其中介绍了RegisterExport用于将 C# 类连接到Objective-C对象和 UI 元素的属性。

构造函数

自定义数据类需要两个构造函数(正确公开到 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 ();
}

首先,我们在默认Objective-C方法init下公开构造函数。

接下来,我们公开一个 NSCoding 合规的构造函数,该构造函数将用于在粘贴到导出名称 initWithCoder下时从粘贴板创建新对象的实例。

此构造函数采用( NSCoder 由写入粘贴板时创建 NSKeyedArchiver )提取键/值配对数据并将其保存到数据类的属性字段。

写入粘贴板

通过符合 INSPasteboardWriting 接口,我们需要公开两种方法,并选择性地公开第三种方法,以便可以将类写入粘贴板。

首先,我们需要告知粘贴板可以写入自定义类的数据类型表示形式:

[Export ("writableTypesForPasteboard:")]
public virtual string[] GetWritableTypesForPasteboard (NSPasteboard pasteboard) {
    string[] writableTypes = {"com.xamarin.image-info", "public.text"};
    return writableTypes;
}

每个表示形式都通过统一类型标识符(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类型,我们将使用和NSKeyedArchiverNSCoder接口将自定义数据类编码为键/值配对存档。 我们需要实现以下方法来实际处理编码:

[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;
}

同样,这些定义为简单的 UTIs,也是我们在上面“粘贴板写入”部分中定义的相同类型。

接下来,我们需要告知粘贴板 如何使用 以下方法读取每个 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
    }
}

与自定义数据类一样,我们需要使用 RegisterExport 指令将其公开给 Objective-C。 该类必须继承自 NSPasteboardItemDataProvider 并且必须实现 FinishedWithDataProviderProvideDataForType 方法。

使用该方法 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;
    }

}

在这种情况下,我们将存储有关图像(Name 和 ImageType)的两段信息,并将这些信息写入简单字符串(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 应用程序中的粘贴板来支持复制和粘贴操作。 首先,它引入了一个简单的示例,让你熟悉标准粘贴板操作。 接下来,详细介绍了粘贴板以及如何从粘贴板读取和写入数据。 最后,它考虑了如何使用自定义数据类型来支持在应用中复制和粘贴复杂数据类型。