Share via


Xamarin.Mac でのコピーと貼り付け

この記事は、ペーストボードを操作して、Xamarin.Mac アプリケーションでコピーと貼り付けを行う方法について説明しています。 複数のアプリ間で共有できる標準データ型の操作方法と、特定のアプリ内でカスタム データをサポートする方法について説明します。

概要

Xamarin.Mac アプリケーションで C# と .NET を使用する場合、開発者が Objective-C で使用するのと同じペーストボード (コピーと貼り付け) サポートにアクセスできます。

この記事では、Xamarin.Mac アプリでペーストボードを使用する 2 つの主な方法について説明します。

  1. 標準データ型 - 通常、ペーストボードの操作は関連していない 2 つのアプリ間で実行されるため、どちらのアプリも、他方のアプリがサポートするデータ型を知りません。 共有の可能性を最大限に高めるために、ペーストボードは特定の項目の複数の表現を保持できます (一般的なデータ型の標準セットを使用します)。これにより、使用しているアプリはニーズに最も適したバージョンを選択できます。
  2. カスタム データ - Xamarin.Mac で複雑なデータのコピーと貼り付けをサポートするために、ペーストボードによって処理されるカスタム データ型を定義できます。 たとえば、ユーザーが複数のデータ型とポイントで構成される複雑な図形をコピーして貼り付けできるようにするベクター描画アプリがあります。

実行中のアプリの例

この記事では、Xamarin.Mac アプリケーションでペーストボードを操作して、コピーと貼り付け操作をサポートすることに関する基本を説明します。 まず、「Hello, Mac」の記事、特に「Xcode と Interface Builder の概要」と「Outlet と Action」のセクションに取り組むことを強くお勧めします。ここでは、この記事で使用する主要な概念と手法が取り上げられています。

また、Xamarin.Mac の内部構造に関するドキュメントの「C# クラス/メソッドを Objective-C に公開する」のセクションも参照することをお勧めします。そこでは、C# クラスを Objective-C オブジェクトと UI 要素に結び付けるために使われる RegisterExport 属性について説明されています。

ペーストボードの概要

ペーストボードは、特定のアプリケーション内またはアプリケーション間でデータを交換するための標準化されたメカニズムを提供します。 Xamarin.Mac アプリケーションでのペーストボードの一般的な用途はコピー操作と貼り付け操作を処理することですが、他の多くの操作 (ドラッグ アンド ドロップ、アプリケーション サービスなど) もサポートされています。

すぐに作業を開始できるようにするために、Xamarin.Mac アプリでペーストボードを使用する簡単で実用的な入門から始めます。 その後、ペーストボードのしくみと使用するメソッドについて詳しく説明します。

この例では、イメージ ビューを含むウィンドウを管理する単純なドキュメント ベースのアプリケーションを作成します。 ユーザーは、アプリ内のドキュメント間で、アプリと他のアプリ間で、同じアプリ内で複数のウィンドウ間で画像をコピーして貼り付けることができます。

Xamarin プロジェクトを作成する

まず、ドキュメント ベースの新しい Xamarin.Mac アプリを作成します。このアプリに、コピーと貼り付けのサポートを追加します。

次の操作を行います。

  1. Visual Studio for Mac を起動し、[新しいプロジェクト...] リンクをクリックします。

  2. [Mac]>[アプリ]>[Cocoa アプリ] の順に選び、[次へ] ボタンをクリックします。

    新しい Cocoa アプリ プロジェクトの作成

  3. [プロジェクト名] に「MacCopyPaste」と入力し、それ以外はすべて既定値のままにします。 [Next:

    プロジェクトの名前の設定

  4. [作成] ボタンをクリックします。

    新しいプロジェクトの設定の確認

NSDocument を追加する

次に、アプリケーションのユーザー インターフェイスのバックグラウンド ストレージとして機能するカスタム NSDocument クラスを追加します。 1 つの画像ビューが含まれており、ビューから既定のペーストボードに画像をコピーする方法と、既定のペーストボードから画像を取得して画像ビューに表示する方法を理解しています。

Solution Pad で Xamarin.Mac プロジェクトを右クリックし、[追加]>[新しいファイル...] の順に選びます。

プロジェクトへの NSDocument の追加

[名前] に「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 で開きます。 次に、ツール バーとイメージ ウェルを追加し、次のように構成します。

ツール バーの編集

コピーと貼り付けの画像ツール バーの項目をツールバーの左側に追加します。 これは、[編集] メニューからコピーして貼り付けるためのショートカットとして使用します。 次に、ツール バーの右側に画像ツール バーの項目を 4 つ追加します。 これらを使用して、いくつかの既定の画像をイメージ ウェルに追加します。

ツール バーの操作の詳細については、ツール バーに関するドキュメントをご覧ください。

次に、ツール バーの項目とイメージ ウェルのために次の Outlet と Action を公開しましょう。

アウトレットとアクションの作成

Outlet と Action の操作の詳細については、「Hello, Mac」ドキュメントの「Outlet と Action」セクションをご覧ください。

ユーザー インターフェイスを有効にする

Xcode でユーザー インターフェイスを作成し、Outlet と Action を使用して UI 要素を公開したので、UI を有効にするためのコードを追加する必要があります。 Solution PadImageWindow.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");
    }
}

ExportWillChangeValueDidChangeValue を使用して 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);
}

詳細については、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);

[切り取り][コピー][貼り付け] メニュー項目は、既定のペーストボードまたは現在アクティブなウィンドウのイメージ ウェルに画像データがある場合にのみアクセスできるようにする必要があります。

Xamarin.Mac プロジェクトに EditMenuDelegate.cs ファイルを追加し、次のようにしましょう。

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 クラスのインスタンスをアタッチします。

詳細については、Microsoft のメニューに関するドキュメントをご覧ください。

アプリのテスト

すべてが整ったので、アプリケーションをテストする準備ができました。 アプリをビルドして実行すると、メイン インターフェイスが表示されます。

アプリケーションの実行

[編集] メニューを開くと、[切り取り][コピー][貼り付け] が無効になっていることにご注意ください。これは、イメージ ウェルまたは既定のペーストボードに画像がないためです。

[編集] メニューを開く

画像をイメージ ウェルに追加して [編集] メニューをもう一度開くと、これらの項目が有効になります。

有効になっている [編集] メニューの項目

画像をコピーし、[ファイル] メニューから [新規] を選ぶと、その画像を新しいウィンドウに貼り付けることができます。

イメージを新しいウィンドウに貼り付ける

以降のセクションでは、Xamarin.Mac アプリケーションでのペーストボードの操作について詳しく説明します。

ペーストボードの概要

macOS (旧称 OS X) のペーストボード (NSPasteboard) では、コピーと貼り付け、ドラッグ アンド ドロップ、アプリケーション サービスなどの複数のサーバー プロセスがサポートされています。 以降のセクションでは、ペーストボードの主要な概念のいくつかを詳しく説明します。

ペーストボードとは?

NSPasteboard クラスは、アプリケーション間または特定のアプリ内で情報を交換するための標準化されたメカニズムを提供します。 ペーストボードの主要な機能は、コピー操作と貼り付け操作を処理することです。

  1. ユーザーがアプリ内の項目を選択し、[切り取り] または [コピー] メニュー項目を使用すると、選択した項目の 1 つ以上の表現がポーストボードに配置されます。
  2. ユーザーが (同じアプリ内または別のアプリ内で) [貼り付け] メニュー項目を使用すると、処理できるデータのバージョンがペーストボードからコピーされ、アプリに追加されます。

あまり明確でないペーストボードの使用には、検索、ドラッグ アンド ドロップ、アプリケーション サービス操作があります。

  • ユーザーがドラッグ操作を開始すると、ドラッグ データがペーストボードにコピーされます。 ドラッグ操作が別のアプリへのドロップで終了すると、そのアプリはペーストボードからデータをコピーします。
  • 移動サービスの場合、移動するデータは、要求元のアプリによってペーストボードにコピーされます。 アプリケーション サービスは、ペーストボードからデータを取得し、移動を行い、データをペーストボードに貼り付けます。

最も単純な形式でも、ペーストボードは、特定のアプリ内またはアプリ間でデータを移動するために使用します。そのため、アプリのプロセス外の特別なグローバル メモリ領域に存在します。 ペーストボードの概念は簡単に把握できるものですが、さらに複雑な詳細をいくつか考慮する必要があります。 これについては、以下で詳しく説明します。

名前付きペーストボード

ペーストボードはパブリックまたはプライベートにすることができ、アプリケーション内または複数のアプリ間でさまざまな目的に使用できます。 macOS には、いくつかの標準的なペーストボードが用意されており、それぞれに明確に定義された特定の用途があります。

  • NSGeneralPboard - [切り取り][コピー][貼り付け] 操作のための既定のペーストボード。
  • NSRulerPboard - ルーラーでの[切り取り][コピー][貼り付け] 操作をサポートします。
  • NSFontPboard - NSFont オブジェクトでの[切り取り][コピー][貼り付け] 操作をサポートします。
  • NSFindPboard - 検索テキストを共有できるアプリケーション固有の検索パネルをサポートします。
  • NSDragPboard - [ドラッグ アンド ドロップ] 操作をサポートします。

ほとんどの場合、システム定義のペーストボードのいずれかを使用します。 ただし、独自のペーストボードを作成する必要がある場合もあります。 そのような状況では、NSPasteboard クラスの FromName (string name) メソッドを使用して、指定された名前でカスタム ペーストボードを作成できます。

必要に応じて、NSPasteboard クラスの CreateWithUniqueName メソッドを呼び出して、一意の名前を持つペーストボードを作成できます。

ペーストボード項目

アプリケーションがペーストボードに書き込む各データは "ペーストボード項目" と見なされ、ペーストボードは複数の項目を同時に保持できます。 そのため、アプリは、ペーストボードにコピーされるデータの複数のバージョン (プレーン テキストと書式設定されたテキストなど) を書き込むことができ、取得するアプリは処理できるデータ (プレーン テキストのみなど) のみを読み取ることができます。

データ表現と Uniform Type Identifier

通常、ペーストボードの操作は、互いに関する知識や、他方が処理できるデータ型に関する知識がない 2 つ (またはそれ以上) のアプリケーション間で行われます。 上記のセクションで説明したように、情報共有の可能性を最大限に高めるために、ペーストボードには、コピーおよび貼り付けするデータの複数の表現を保持できます。

各表現は Uniform Type Identifier (UTI) を使用して識別されます。これは、表示されるデータ型を一意に識別する単純な文字列に過ぎません (詳細については、Apple の Uniform Type Identifiers の概要に関するドキュメントをご覧ください)。

カスタム データ型 (ベクター描画アプリの描画オブジェクトなど) を作成する場合は、独自の UTI を作成して、コピーおよび貼り付け操作で一意に識別できます。

アプリがペーストボードからコピーしたデータを貼り付ける準備をするときに、(存在する場合) その機能に最適な表現を見つける必要があります。 通常、これは、使用可能な最もリッチな型 (ワープロ アプリの書式設定されたテキストなど) であり、必要に応じて使用できる最も単純な形式 (単純なテキスト エディターのプレーン テキスト) にフォールバックします。

保証されたデータ

一般に、アプリ間の共有を最大化するために、コピーするデータの表現をできるだけ多く指定する必要があります。 ただし、時間やメモリの制約のため、各データ型を実際にペーストボードに書き込むのは実用的ではない可能性があります。

このような状況では、最初のデータ表現をペーストボードに配置し、受信側のアプリは、貼り付け操作の直前にすぐに生成できる別の表現を要求できます。

最初の項目をペーストボードに配置するときに、NSPasteboardItemDataProvider インターフェイスに準拠するオブジェクトによって、使用可能な他の表現の 1 つ以上が提供されるように指定します。 これらのオブジェクトは、受信側のアプリからの要求に応じて、追加の表現をオンデマンドで提供します。

変更カウント

各ペーストボードは、新しい所有者が宣言されるたびにインクリメントされる "変更カウント" を保持します。 アプリは、変更カウントの値を確認することで、前回の検証以降にペーストボードの内容が変更されたかどうかを判断できます。

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 インターフェイスに準拠している必要があります。 いくつかの組み込みのクラス (NSStringNSImageNSURLNSColorNSAttributedStringNSPasteboardItem など) は、このインターフェイスに自動的に準拠しています。

カスタム データ クラスをペーストボードに書き込む場合は、INSPasteboardWriting インターフェイスに準拠するか、NSPasteboardItem クラスのインスタンスにラップする必要があります (後述の「カスタム データ型」セクションをご覧ください)。

ペーストボードからデータを読み取る

前述のように、アプリ間でデータ共有の可能性を最大限に高めるために、コピーしたデータの複数の表現をペーストボードに書き込む場合があります。 機能に対して可能な限りリッチなバージョン (存在する場合) を選択するかどうかは、受信側のアプリにかかっています。

単純な貼り付け操作

ReadObjectsForClasses メソッドを使用して、ペーストボードからデータを読み取ります。 次の 2 つのパラメーターが必要です。

  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 アプリケーションの種類に基づいて、貼り付けるデータの複数の表現を処理できる場合があります。 そのような状況では、ペーストボードからデータを取得する 2 つのシナリオがあります。

  1. ReadObjectsForClasses メソッドを 1 回呼び出し、必要なすべての表現の配列を指定します (優先順位の高い順)。
  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 を指すなど)。 このような場合、CanReadObjectForClasses または ReadObjectsForClasses メソッドの 2 番目のパラメーターを使用して、追加の検索条件を指定できます。

カスタム データ型

Xamarin.Mac アプリから独自のカスタム型をペーストボードに保存する必要がある場合があります。 たとえば、ユーザーが描画オブジェクトをコピーして貼り付けできるようにするベクター描画アプリなどです。

このような場合、データ カスタム クラスが NSObject から継承し、いくつかのインターフェイス (INSCodingINSPasteboardWritingINSPasteboardReading) に準拠するようにデータ カスタム クラスを設計する必要があります。 必要に応じて 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
...

また、クラスは Export を使用して Objective-C に公開され、Register を使用して必要なすべてのプロパティまたはメソッドを公開する必要があります。 次に例を示します。

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

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

ここでは、このクラスに含まれるデータの 2 つのフィールド (画像の名前とその型 (jpg、png など)) を公開しています。

詳細については、Xamarin.Mac の内部構造に関するドキュメントの「C# クラス/メソッドを Objective-C に公開する」のセクションをご覧ください。そこでは、C# クラスを Objective-C オブジェクトと UI 要素に結び付けるために使われる RegisterExport 属性について説明されています。

コンストラクター

カスタム データ クラスをペーストボードから読み取ることができるように、(Objective-C に適切に公開されている) 2 つのコンストラクターが必要です。

[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 インターフェイスに準拠することで、クラスをペーストボードに書き込むことができるように、2 つのメソッド (および必要に応じて 3 つ目のメソッド) を公開する必要があります。

まず、カスタム クラスの書き込み先となることのできるデータ型表現をペーストボードに伝える必要があります。

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

各表現は Uniform Type Identifier (UTI) を使用して識別されます。これは、表示されるデータ型を一意に識別する単純な文字列に過ぎません (詳細については、Apple の Uniform Type Identifiers の概要に関するドキュメントをご覧ください)。

カスタム形式には、独自の 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");
}

個々のキーと値のペアはエンコーダーに書き込まれ、上記で追加した 2 番目のコンストラクターを使用してデコードされます。

必要に応じて、次のメソッドを含めることで、ペーストボードにデータを書き込むときにオプションを定義できます。

[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 インターフェイスに準拠することで、カスタム データ クラスをペーストボードから読み取ることができるように、3 つのメソッドを公開する必要があります。

まず、カスタム クラスがクリップボードから読み取ることのできるデータ型表現をペーストボードに伝える必要があります。

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

カスタム データ クラスと同様に、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;
    }

}

この場合、画像に関する 2 つの情報 (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 アプリケーションでペーストボードを操作して、コピーと貼り付け操作をサポートすることについて詳しく解説しました。 最初に、標準のペーストボードの操作を理解するための簡単な例を紹介しました。 次に、ペーストボードと、そこからデータの読み取りと書き込みを行う方法について詳しく説明しました。 最後に、カスタム データ型を使用して、アプリ内の複合データ型のコピーと貼り付けをサポートすることについて確認しました。