Copiar y pegar en Xamarin.Mac

En este artículo se explica cómo trabajar con el panel de pegado para proporcionar copia y pegar en una aplicación de Xamarin.Mac. Muestra cómo trabajar con tipos de datos estándar que se pueden compartir entre varias aplicaciones y cómo admitir datos personalizados dentro de una aplicación determinada.

Información general

Al trabajar con C# y .NET en una aplicación de Xamarin.Mac, tiene acceso al mismo panel de pegado (copiar y pegar) que un desarrollador que trabaja en Objective-C tiene.

En este artículo se tratarán las dos formas principales de usar el pegado en una aplicación de Xamarin.Mac:

  1. Tipos de datos estándar: dado que las operaciones del panel de pegado se realizan normalmente entre dos aplicaciones no relacionadas, ninguna aplicación conoce los tipos de datos que admite el otro. Para maximizar el potencial de uso compartido, el panel de pegado puede contener varias representaciones de un elemento determinado (mediante un conjunto estándar de tipos de datos comunes), lo que permite que la aplicación de consumo elija la versión más adecuada para sus necesidades.
  2. Datos personalizados: para admitir la copia y el pegado de datos complejos dentro de Xamarin.Mac, puede definir un tipo de datos personalizado que controlará el panel de pegado. Por ejemplo, una aplicación de dibujo vectorial que permite al usuario copiar y pegar formas complejas compuestas de varios tipos de datos y puntos.

Example of the running app

En este artículo, trataremos los conceptos básicos de trabajar con el panel de pegado en una aplicación de Xamarin.Mac para admitir operaciones de copia y pegado. Se recomienda encarecidamente trabajar primero en el artículo Hello, Mac , específicamente en las secciones Introducción a Xcode y Generador de interfaces y Salidas y Acciones , ya que trata conceptos clave y técnicas que usaremos en este artículo.

Puede que quiera echar un vistazo a la sección Exponer clases o métodos de C# a Objective-C la sección del documento Xamarin.Mac Internals , también explica los Register atributos y Export que se usan para conectar las clases de C# a Objective-C objetos y elementos de la interfaz de usuario.

Introducción al panel de pegado

El panel de pegado presenta un mecanismo estandarizado para intercambiar datos dentro de una aplicación determinada o entre aplicaciones. El uso típico de un panel de pegado en una aplicación de Xamarin.Mac es controlar las operaciones de copia y pegado, pero también se admiten una serie de otras operaciones (como Drag & Drop y Application Services).

Para sacarle el suelo rápidamente, vamos a empezar con una introducción sencilla y práctica al uso de tablas de pegado en una aplicación de Xamarin.Mac. Más adelante, proporcionaremos una explicación detallada de cómo funciona el pegado y los métodos usados.

En este ejemplo, vamos a crear una aplicación sencilla basada en documentos que administra una ventana que contiene una vista de imagen. El usuario podrá copiar y pegar imágenes entre documentos de la aplicación y hacia o desde otras aplicaciones o desde varias ventanas dentro de la misma aplicación.

Creación del proyecto de Xamarin

En primer lugar, vamos a crear una nueva aplicación de Xamarin.Mac basada en documentos para la que agregaremos compatibilidad con copiar y pegar.

Haga lo siguiente:

  1. Inicie Visual Studio para Mac y haga clic en el vínculo Nuevo proyecto... .

  2. Seleccione Aplicación De Cocoa para> Mac>y, a continuación, haga clic en el botón Siguiente:

    Creating a new Cocoa app project

  3. Escriba MacCopyPaste para el nombre del proyecto y mantenga todo lo demás como predeterminado. Haga clic en Siguiente:

    Setting the name of the project

  4. Haga clic en el botón Crear :

    Confirming the new project settings

Agregar un NSDocument

A continuación, agregaremos una clase personalizada NSDocument que actuará como almacenamiento en segundo plano para la interfaz de usuario de la aplicación. Contendrá una sola vista de imagen y sabrá cómo copiar una imagen de la vista en el panel de pegado predeterminado y cómo tomar una imagen del panel de pegado predeterminado y mostrarla en la vista de imagen.

Haga clic con el botón derecho en el proyecto de Xamarin.Mac en el Panel de solución y seleccione Agregar>nuevo archivo..:

Adding an NSDocument to the project

Escriba ImageDocument para el Nombre y haga clic en el botón Nuevo. Edite la clase ImageDocument.cs y fíjela como la siguiente:

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

Echemos un vistazo a algunos de los códigos en detalle a continuación.

El código siguiente proporciona una propiedad para probar la existencia de datos de imagen en el panel de pegado predeterminado, si hay una imagen disponible, true se devuelve de otro modo 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);
    }
}

El código siguiente copia una imagen de la vista de imagen adjunta en el panel de pegado predeterminado:

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

}

Y el código siguiente pega una imagen del panel de pegado predeterminado y la muestra en la vista de imagen adjunta (si el panel de pegado contiene una imagen válida):

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

Con este documento en su lugar, crearemos la interfaz de usuario para la aplicación Xamarin.Mac.

Creación de la interfaz de usuario

Haga doble clic en el archivo Main.storyboard para abrirlo en Xcode. A continuación, agregue una barra de herramientas y una imagen bien y configúrelas de la siguiente manera:

Editing the toolbar

Agregue una copia y pegue elemento de la barra de herramientas de imagen en el lado izquierdo de la barra de herramientas. Los usaremos como accesos directos para copiar y pegar en el menú Editar. A continuación, agregue cuatro elementos de la barra de herramientas de imágenes al lado derecho de la barra de herramientas. Los usaremos para rellenar la imagen correctamente con algunas imágenes predeterminadas.

Para obtener más información sobre cómo trabajar con barras de herramientas, consulte nuestra documentación sobre barras de herramientas .

A continuación, vamos a exponer las siguientes salidas y acciones para los elementos de la barra de herramientas y el pozo de imagen:

Creating outlets and actions

Para obtener más información sobre cómo trabajar con salidas y acciones, consulte la sección Salidas y acciones de nuestra documentación Hello, Mac .

Habilitación de la interfaz de usuario

Con nuestra interfaz de usuario creada en Xcode y nuestro elemento de interfaz de usuario expuesto a través de salidas y acciones, es necesario agregar el código para habilitar la interfaz de usuario. Haga doble clic en el archivo ImageWindow.cs en el Panel de solución y fíjelo como el siguiente:

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

Echemos un vistazo a este código en detalle a continuación.

En primer lugar, exponemos una instancia de la ImageDocument clase que creamos anteriormente:

private ImageDocument _document;
...

[Export ("Document")]
public ImageDocument Document {
    get { return _document; }
    set {
        WillChangeValue ("Document");
        _document = value;
        DidChangeValue ("Document");
    }
}

ExportMediante , WillChangeValue y DidChangeValue, hemos configurado la propiedad para permitir la Document codificación de clave-valor y el enlace de datos en Xcode.

También exponemos la imagen de la imagen bien agregada a nuestra interfaz de usuario en Xcode con la siguiente propiedad:

public ViewController ImageViewController {
    get { return ContentViewController as ViewController; }
}

public NSImage Image {
    get {
        return ImageViewController.Image;
    }
    set {
        ImageViewController.Image = value;
    }
}

Cuando se carga y muestra la ventana principal, creamos una instancia de nuestra ImageDocument clase y adjuntamos bien la imagen de la interfaz de usuario con el código siguiente:

public override void AwakeFromNib ()
{
    base.AwakeFromNib ();

    // Create a new document instance
    Document = new ImageDocument ();

    // Attach to image view
    Document.ImageView = ImageViewController.ContentView;
}

Por último, en respuesta al usuario que hace clic en los elementos de la barra de herramientas copiar y pegar, llamamos a la instancia de la ImageDocument clase para realizar el trabajo real:

partial void CopyImage (NSObject sender) {
    Document.CopyImage(sender);
}

partial void PasteImage (Foundation.NSObject sender) {
    Document.PasteImage(sender);
}

Habilitación de los menús Archivo y Edición

Lo último que debemos hacer es habilitar el elemento de menú Nuevo desde el menú Archivo (para crear nuevas instancias de nuestra ventana principal) y habilitar los elementos de menú Cortar, Copiar y Pegar en el menú Editar .

Para habilitar el elemento de menú Nuevo , edite el archivo AppDelegate.cs y agregue el código siguiente:

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

Para obtener más información, consulte la sección Working with Multiple Windows (Trabajar con varias ventanas ) de nuestra documentación de Windows .

Para habilitar los elementos de menú Cortar, Copiar y Pegar , edite el archivo AppDelegate.cs y agregue el código siguiente:

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

Para cada elemento de menú, obtenemos la ventana actual, superior, clave y la conviertemos en nuestra ImageWindow clase:

var window = NSApplication.SharedApplication.KeyWindow as ImageWindow;

Desde allí llamamos a la ImageDocument instancia de clase de esa ventana para controlar las acciones de copia y pegado. Por ejemplo:

window.Document.CopyImage (sender);

Solo queremos que los elementos de menú Cortar, Copiar y Pegar sean accesibles si hay datos de imagen en el panel de pegado predeterminado o en el pozo de imagen de la ventana activa actual.

Vamos a agregar un archivo EditMenuDelegate.cs al proyecto de Xamarin.Mac y convertirlo en similar al siguiente:

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

De nuevo, obtenemos la ventana actual y superior y usamos su ImageDocument instancia de clase para ver si existen los datos de imagen necesarios. A continuación, usamos el MenuWillHighlightItem método para habilitar o deshabilitar cada elemento en función de este estado.

Edite el archivo AppDelegate.cs y haga que el DidFinishLaunching método tenga un aspecto similar al siguiente:

public override void DidFinishLaunching (NSNotification notification)
{
    // Disable automatic item enabling on the Edit menu
    EditMenu.AutoEnablesItems = false;
    EditMenu.Delegate = new EditMenuDelegate ();
}

En primer lugar, deshabilitamos la habilitación automática y deshabilitación de elementos de menú en el menú Editar. A continuación, adjuntamos una instancia de la EditMenuDelegate clase que creamos anteriormente.

Para obtener más información, consulte nuestra documentación de Menús .

Probar la aplicación

Con todo en su lugar, estamos listos para probar la aplicación. Se muestra la compilación y ejecución de la aplicación y la interfaz principal:

Running the application

Si abre el menú Editar, tenga en cuenta que Cortar, Copiar y Pegar están deshabilitados porque no hay ninguna imagen en la imagen bien o en el panel de pegado predeterminado:

Opening the Edit menu

Si agrega una imagen al área de la imagen y vuelve a abrir el menú Editar, los elementos ahora se habilitarán:

Showing the Edit menu items are enabled

Si copia la imagen y selecciona Nuevo en el menú archivo, puede pegar esa imagen en la nueva ventana:

Pasting an image into a new window

En las secciones siguientes, echaremos un vistazo detallado al trabajo con el péguelo en una aplicación de Xamarin.Mac.

Acerca del panel de pegado

En macOS (anteriormente conocido como OS X), el panel de pegado (NSPasteboard) proporciona compatibilidad con varios procesos de servidor, como Copiar y Pegar, Arrastrar y Colocar y Servicios de aplicaciones. En las secciones siguientes, veremos más detenidamente varios conceptos del panel de pegado de claves.

¿Qué es un pegado?

La NSPasteboard clase proporciona un mecanismo estandarizado para intercambiar información entre aplicaciones o dentro de una aplicación determinada. La función principal de un panel de pegado es para controlar las operaciones de copia y pegado:

  1. Cuando el usuario selecciona un elemento de una aplicación y usa el elemento de menú Cortar o Copiar , se colocan una o varias representaciones del elemento seleccionado en el panel de pegado.
  2. Cuando el usuario usa el elemento de menú Pegar (dentro de la misma aplicación o en otro diferente), la versión de los datos que puede controlar se copia del panel de pegado y se agrega a la aplicación.

Entre los usos menos obvios del panel de pegado se incluyen las operaciones de búsqueda, arrastrar, arrastrar y colocar y servicios de aplicaciones:

  • Cuando el usuario inicia una operación de arrastrar, los datos de arrastre se copian en el panel de pegado. Si la operación de arrastre finaliza con una colocación en otra aplicación, esa aplicación copia los datos del panel de pegado.
  • En el caso de los servicios de traducción, los datos que se van a traducir se copian en el panel de pegado mediante la aplicación solicitante. El servicio de aplicación, recupera los datos del panel de pegado, realiza la traducción y, a continuación, pega los datos de nuevo en el panel de pegado.

En su forma más sencilla, los pasteboards se usan para mover datos dentro de una aplicación determinada o entre aplicaciones y existen en un área de memoria global especial fuera del proceso de la aplicación. Aunque los conceptos de los tableros de pegado son fáciles de entender, hay varios detalles más complejos que se deben tener en cuenta. Estos se tratarán en detalle a continuación.

Tablas de pegado con nombre

Un pegado puede ser público o privado y puede usarse con diversos propósitos dentro de una aplicación o entre varias aplicaciones. macOS proporciona varias tablas de pegado estándar, cada una con un uso específico y bien definido:

  • NSGeneralPboard - El panel de pegado predeterminado para las operaciones Cortar, Copiar y Pegar .
  • NSRulerPboard - Admite las operaciones cortar, copiar y pegar en las reglas.
  • NSFontPboard - Admite operaciones cortar, copiar y pegar en NSFont objetos.
  • NSFindPboard - Admite paneles de búsqueda específicos de la aplicación que pueden compartir texto de búsqueda.
  • NSDragPboard - Admite operaciones de arrastrar y colocar .

En la mayoría de las situaciones, usará uno de los pegadores definidos por el sistema. Pero puede haber situaciones que requieran que cree sus propios pegadores. En estas situaciones, puede usar el FromName (string name) método de la NSPasteboard clase para crear un panel de pegado personalizado con el nombre especificado.

Opcionalmente, puede llamar al CreateWithUniqueName método de la NSPasteboard clase para crear un panel de pegado con nombre único.

Elementos del panel de pegado

Cada fragmento de datos que una aplicación escribe en un panel de pegado se considera un elemento pasteboard y un panel de pegado puede contener varios elementos al mismo tiempo. De este modo, una aplicación puede escribir varias versiones de los datos que se copian en un panel de pegado (por ejemplo, texto sin formato y texto con formato) y la aplicación de recuperación solo puede leer los datos que puede procesar (por ejemplo, solo texto sin formato).

Representaciones de datos e identificadores de tipo uniforme

Normalmente, las operaciones del panel de pegado tienen lugar entre dos (o más) aplicaciones que no tienen conocimiento entre sí o los tipos de datos que cada uno puede controlar. Como se indicó en la sección anterior, para maximizar la posibilidad de compartir información, un panel de pegado puede contener varias representaciones de los datos que se copian y pegan.

Cada representación se identifica a través de un identificador de tipo uniforme (UTI), que no es más que una cadena simple que identifica de forma única el tipo de fecha que se presenta (para obtener más información, consulte la documentación de información general sobre identificadores uniformes de tipos uniformes de Apple).

Si va a crear un tipo de datos personalizado (por ejemplo, un objeto de dibujo en una aplicación de dibujo vectorial), puede crear su propio UTI para identificarlo de forma única en las operaciones de copia y pegado.

Cuando una aplicación se prepara para pegar los datos copiados de un panel de pegado, debe encontrar la representación que mejor se adapte a sus capacidades (si existe alguna). Normalmente, este será el tipo más rico disponible (por ejemplo, texto con formato para una aplicación de procesamiento de texto), volviendo a los formularios más sencillos disponibles según sea necesario (texto sin formato para un editor de texto simple).

Datos prometidos

Por lo general, debe proporcionar tantas representaciones de los datos que se copian como sea posible para maximizar el uso compartido entre aplicaciones. Sin embargo, debido a las restricciones de tiempo o memoria, podría ser poco práctico escribir realmente cada tipo de datos en el panel de pegado.

En esta situación, puede colocar la primera representación de datos en el panel de pegado y la aplicación receptora puede solicitar una representación diferente, que se puede generar sobre la marcha justo antes de la operación de pegado.

Al colocar el elemento inicial en el panel de pegado, especificará que una o varias de las otras representaciones disponibles se proporcionan mediante un objeto que se ajusta a la NSPasteboardItemDataProvider interfaz. Estos objetos proporcionarán las representaciones adicionales a petición, según lo solicitado por la aplicación receptora.

Recuento de cambios

Cada panel de pegado mantiene un recuento de cambios que aumenta cada vez que se declara un nuevo propietario. Una aplicación puede determinar si el contenido del panel de pegado ha cambiado desde la última vez que lo examinó comprobando el valor del recuento de cambios.

Use los métodos y ClearContents de la NSPasteboard clase para modificar el ChangeCount recuento de cambios de un panel de pegado determinado.

Copiar datos en un panel de pegado

Para realizar una operación de copia, primero se accede a un panel de pegado, se borra cualquier contenido existente y se escriben tantas representaciones de los datos como sea necesario en el panel de pegado.

Por ejemplo:

// 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});

Normalmente, solo escribirá en el pegado general, como hemos hecho en el ejemplo anterior. Cualquier objeto que envíe al WriteObjects método debe ajustarse a la INSPasteboardWriting interfaz . Varias clases integradas (como NSString, NSImage, NSURL, NSColor, NSAttributedStringy NSPasteboardItem) se ajustan automáticamente a esta interfaz.

Si va a escribir una clase de datos personalizada en el panel de pegado, debe ajustarse a la INSPasteboardWriting interfaz o encapsularse en una instancia de la NSPasteboardItem clase (consulte la sección Tipos de datos personalizados a continuación).

Lectura de datos desde un panel de pegado

Como se indicó anteriormente, para maximizar la posibilidad de compartir datos entre aplicaciones, se pueden escribir varias representaciones de los datos copiados en el panel de pegado. Es hasta la aplicación receptora seleccionar la versión más rica posible para sus funcionalidades (si existe alguna).

Operación de pegado simple

Los datos se leen desde el panel de pegado mediante el ReadObjectsForClasses método . Necesitará dos parámetros:

  1. Matriz de tipos de NSObject clase basados que desea leer del panel de pegado. Primero debe ordenar esto con el tipo de datos más deseado, con los tipos restantes en la preferencia decreciente.
  2. Diccionario que contiene restricciones adicionales (como limitar a tipos de contenido de dirección URL específicos) o un diccionario vacío si no se requieren restricciones adicionales.

El método devuelve una matriz de elementos que cumplen los criterios que hemos pasado y, por tanto, contiene como máximo el mismo número de tipos de datos que se solicitan. también es posible que ninguno de los tipos solicitados esté presente y se devolverá una matriz vacía.

Por ejemplo, el código siguiente comprueba si existe en NSImage el panel de pegado general y lo muestra en una imagen bien si lo hace:

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

Solicitud de varios tipos de datos

En función del tipo de aplicación de Xamarin.Mac que se va a crear, puede controlar varias representaciones de los datos que se pegan. En esta situación, hay dos escenarios para recuperar datos del panel de pegado:

  1. Realice una sola llamada al ReadObjectsForClasses método y proporcione una matriz de todas las representaciones que desee (en el orden preferido).
  2. Realice varias llamadas al ReadObjectsForClasses método solicitando una matriz de tipos diferente cada vez.

Consulte la sección Operación de pegado simple anterior para obtener más información sobre cómo recuperar datos de un panel de pegado.

Comprobación de los tipos de datos existentes

Hay veces que es posible que desee comprobar si un panel de pegado contiene una representación de datos determinada sin leer realmente los datos del panel de pegado (por ejemplo, habilitar el elemento de menú Pegar solo cuando existen datos válidos).

Llame al CanReadObjectForClasses método del panel de pegado para ver si contiene un tipo determinado.

Por ejemplo, el código siguiente determina si el panel de pegado general contiene una NSImage instancia:

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

Lectura de direcciones URL desde el panel de pegado

En función de la función de una aplicación de Xamarin.Mac determinada, es posible que sea necesario leer las direcciones URL de un panel de pegado, pero solo si cumplen un conjunto determinado de criterios (como apuntar a archivos o direcciones URL de un tipo de datos específico). En esta situación, puede especificar criterios de búsqueda adicionales mediante el segundo parámetro de los CanReadObjectForClasses métodos o ReadObjectsForClasses .

Tipos de datos personalizados

Hay ocasiones en las que tendrá que guardar sus propios tipos personalizados en el panel de pegado desde una aplicación de Xamarin.Mac. Por ejemplo, una aplicación de dibujo vectorial que permite al usuario copiar y pegar objetos de dibujo.

En esta situación, deberá diseñar la clase personalizada de datos para que herede de NSObject y se ajuste a algunas interfaces (INSCodingy INSPasteboardWritingINSPasteboardReading). Opcionalmente, puede usar para NSPasteboardItem encapsular los datos que se van a copiar o pegar.

Ambas opciones se tratarán en detalle a continuación.

Uso de una clase personalizada

En esta sección se expandirá la aplicación de ejemplo simple que creamos al principio de este documento y agregaremos una clase personalizada para realizar un seguimiento de la información sobre la imagen que estamos copiando y pegando entre ventanas.

Agregue una nueva clase al proyecto y llámela ImageInfo.cs. Edite el archivo y fíjelo como el siguiente:

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

En las secciones siguientes, echaremos un vistazo detallado a esta clase.

Herencia e interfaces

Antes de que se pueda escribir o leer una clase de datos personalizada desde un panel de pegado, debe ajustarse a las INSPastebaordWriting interfaces y INSPasteboardReading . Además, debe heredar de NSObject y también cumplir con la INSCoding interfaz:

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

La clase también debe exponerse al Objective-C uso de la Register directiva y debe exponer las propiedades o métodos necesarios mediante Export. Por ejemplo:

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

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

Estamos exponiendo los dos campos de datos que esta clase contendrá: el nombre de la imagen y su tipo (jpg, png, etc.).

Para obtener más información, consulte la sección Exposición de clases y métodos de C# aObjective-Cla sección de la documentación de Xamarin.Mac Internals, explica los Register atributos y Export que se usan para conectar las clases de C# a Objective-C objetos y elementos de la interfaz de usuario.

Constructores

Se requerirán dos constructores (expuestos correctamente a Objective-C) para nuestra clase de datos personalizada para que se pueda leer desde un panel de pegado:

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

En primer lugar, exponemos el constructor vacío bajo el método predeterminado Objective-C de init.

A continuación, se expone un NSCoding constructor compatible que se usará para crear una nueva instancia del objeto desde el panel de pegado al pegar en el nombre exportado de initWithCoder.

Este constructor toma NSCoder (tal como lo crea un NSKeyedArchiver cuando se escribe en el panel de pegado), extrae los datos emparejados de clave-valor y lo guarda en los campos de propiedad de la clase de datos.

Escribir en el panel de pegado

Al ajustarse a la INSPasteboardWriting interfaz, es necesario exponer dos métodos y, opcionalmente, un tercer método, para que la clase se pueda escribir en el panel de pegado.

En primer lugar, es necesario indicar al panel de pegado qué representaciones de tipo de datos se pueden escribir en la clase personalizada:

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

Cada representación se identifica a través de un identificador uniforme de tipo (UTI), que no es más que una cadena simple que identifica de forma única el tipo de datos que se presentan (para obtener más información, consulte la documentación de información general sobre identificadores uniformes de tipos uniformes de Apple).

Para nuestro formato personalizado, estamos creando nuestra propia UTI: "com.xamarin.image-info" (tenga en cuenta que está en notación inversa como un identificador de aplicación). Nuestra clase también es capaz de escribir una cadena estándar en el panel de pegado (public.text).

A continuación, es necesario crear el objeto en el formato solicitado que realmente se escribe en el panel de pegado:

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

Para el public.text tipo, se devuelve un objeto simple con formato NSString . Para el tipo personalizado com.xamarin.image-info , usamos y NSKeyedArchiver la NSCoder interfaz para codificar la clase de datos personalizada en un archivo emparejado clave-valor. Tendremos que implementar el método siguiente para controlar realmente la codificación:

[Export ("encodeWithCoder:")]
public void EncodeTo (NSCoder encoder) {

    // Encode data
    encoder.Encode(new NSString(Name),"name");
    encoder.Encode(new NSString(ImageType),"imageType");
}

Los pares clave-valor individuales se escriben en el codificador y se descodificarán mediante el segundo constructor que hemos agregado anteriormente.

Opcionalmente, podemos incluir el método siguiente para definir cualquier opción al escribir datos en el panel de pegado:

[Export ("writingOptionsForType:pasteboard:"), CompilerGenerated]
public virtual NSPasteboardWritingOptions GetWritingOptionsForType (string type, NSPasteboard pasteboard) {
    return NSPasteboardWritingOptions.WritingPromised;
}

Actualmente solo está disponible la WritingPromised opción y debe usarse cuando solo se promete un tipo determinado y no se escribe realmente en el pegado. Para obtener más información, consulte la sección Datos prometidos anterior.

Con estos métodos implementados, se puede usar el código siguiente para escribir nuestra clase personalizada en el panel de pegado:

// Get the standard pasteboard
var pasteboard = NSPasteboard.GeneralPasteboard;

// Empty the current contents
pasteboard.ClearContents();

// Add info to the pasteboard
pasteboard.WriteObjects (new ImageInfo[] { Info });

Lectura desde el panel de pegado

Al ajustarse a la INSPasteboardReading interfaz, es necesario exponer tres métodos para que la clase de datos personalizada se pueda leer desde el panel de pegado.

En primer lugar, es necesario indicar al panel de pegado qué representaciones de tipo de datos puede leer la clase personalizada del Portapapeles:

[Export ("readableTypesForPasteboard:")]
public static string[] GetReadableTypesForPasteboard (NSPasteboard pasteboard){
    string[] readableTypes = {"com.xamarin.image-info", "public.text"};
    return readableTypes;
}

De nuevo, se definen como URI simples y son los mismos tipos que definimos en la sección Escritura en el Panel de pegado anterior.

A continuación, es necesario indicar al panel de pegado cómo se leerá cada uno de los tipos de UTI mediante el método siguiente:

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

Para el com.xamarin.image-info tipo, se indica al panel de pegado que descodifique el par clave-valor que creamos con al NSKeyedArchiver escribir la clase en el panel de pegado llamando al initWithCoder: constructor que agregamos a la clase .

Por último, es necesario agregar el método siguiente para leer las otras representaciones de datos UTI desde el panel de pegado:

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

Con todos estos métodos implementados, la clase de datos personalizada se puede leer desde el panel de pegado mediante el código siguiente:

// 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];
}

Uso de un objeto NSPasteboardItem

Puede haber ocasiones en las que necesite escribir elementos personalizados en el panel de pegado que no garantizan la creación de una clase personalizada o si desea proporcionar datos en un formato común, solo según sea necesario. En estas situaciones, puede usar .NSPasteboardItem

Proporciona NSPasteboardItem un control específico sobre los datos que se escriben en el panel de pegado y está diseñado para el acceso temporal; se debe eliminar después de que se haya escrito en el panel de pegado.

Escritura de datos

Para escribir los datos personalizados en , NSPasteboardItem deberá proporcionar un personalizado NSPasteboardItemDataProvider. Agregue una nueva clase al proyecto y llámela ImageInfoDataProvider.cs. Edite el archivo y fíjelo como el siguiente:

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

Como hicimos con la clase de datos personalizada, es necesario usar las Register directivas y Export para exponerla a Objective-C. La clase debe heredar de NSPasteboardItemDataProvider y debe implementar los FinishedWithDataProvider métodos y ProvideDataForType .

Use el ProvideDataForType método para proporcionar los datos que se ajustarán en la NSPasteboardItem siguiente:

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

}

En este caso, estamos almacenando dos fragmentos de información sobre nuestra imagen (Name e ImageType) y escribiendolos en una cadena simple (public.text).

Escriba escribir los datos en el panel de pegado y use el código siguiente:

// 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 });
}

Lectura de datos

Para volver a leer los datos desde el panel de pegado, use el código siguiente:

// 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
    ...
}

Resumen

En este artículo se ha tomado un vistazo detallado al trabajo con el panel de pegado en una aplicación de Xamarin.Mac para admitir operaciones de copia y pegado. En primer lugar, introdujo un ejemplo sencillo para familiarizarse con las operaciones estándar de los pasteboards. A continuación, se tomó un vistazo detallado al panel de pegado y cómo leer y escribir datos de él. Por último, se ha examinado el uso de un tipo de datos personalizado para admitir la copia y el pegado de tipos de datos complejos dentro de una aplicación.