Creación de controles personalizados en Xamarin.Mac

Cuando trabaje con C# y .NET en una aplicación de Xamarin.Mac, tendrá acceso a los mismos controles de usuario que tiene un desarrollador que trabaje en Objective-C, Swift y Xcode. Dado que Xamarin.Mac se integra directamente con Xcode, puede usar Interface Builder de Xcode para crear y mantener sus controles de usuario (u opcionalmente crearlos directamente en código C#).

Aunque macOS proporciona una gran cantidad de controles de usuario incorporados, puede haber ocasiones en las que necesite crear un control personalizado para proporcionar la funcionalidad que no se proporciona de forma estándar o para adaptarse a un tema de interfaz de usuario personalizado (como la interfaz de un juego).

Example of a custom UI control

En este artículo, trataremos los conceptos básicos de la creación de un control de interfaz de usuario personalizado reutilizable en una aplicación de Xamarin.Mac. Se recomienda encarecidamente trabajar primero en el artículo Hello, Mac, específicamente en las secciones Introducción a Xcode e Interface Builder y Salidas y acciones, ya que trata conceptos clave y técnicas que usaremos en este artículo.

Es posible que quiera echar un vistazo a la sección Exponer clases o métodos de C# a Objective-C del documento sobre Aspecto internos de Xamarin.Mac, ya que explica los comandos Register y Export que se usan para conectar las clases de C# a objetos Objective-C y elementos de la interfaz de usuario.

Introducción a los controles personalizados

Como se ha indicado anteriormente, puede haber ocasiones en las que necesite crear un control de interfaz de usuario reutilizable y personalizado para proporcionar una funcionalidad única a la interfaz de usuario de su aplicación de Xamarin.Mac o para crear un tema de interfaz de usuario personalizado (como la interfaz de un juego).

En estas situaciones, puede heredar fácilmente de NSControl y crear una herramienta personalizada que podrá agregar a la interfaz de usuario de su aplicación mediante código C# o a través del Interface Builder de Xcode. Al heredar de NSControl, su control personalizado tendrá automáticamente todas las características estándar que tiene un control de interfaz de usuario incorporado (como NSButton).

Si su control de interfaz de usuario personalizado solo muestra información (como una herramienta de gráficos y diagramas personalizados), es posible que quiera heredar de NSView en lugar de NSControl.

Independientemente de la clase base que se use, los pasos básicos para crear un control personalizado son los mismos.

En este artículo, vamos a crear un componente de Flip Switch personalizado que proporciona un tema de interfaz de usuario único y un ejemplo de cómo compilar un control de interfaz de usuario personalizado totalmente funcional.

Creación del control personalizado

Dado que el control personalizado que estamos creando responderá a la entrada del usuario (clics con el botón izquierdo del ratón), vamos a heredar de NSControl. De este modo, nuestro control personalizado tendrá automáticamente todas las características estándar que tiene un control de interfaz de usuario incorporado y responderá como un control estándar de macOS.

En Visual Studio para Mac, abra el proyecto de Xamarin.Mac para el que quiere crear un control de interfaz de usuario personalizado (o cree uno nuevo). Agregue una nueva clase y llámela NSFlipSwitch:

Adding a new class

A continuación, edite la clase NSFlipSwitch.cs y haga que tenga un aspecto similar al siguiente:

using Foundation;
using System;
using System.CodeDom.Compiler;
using AppKit;
using CoreGraphics;

namespace MacCustomControl
{
    [Register("NSFlipSwitch")]
    public class NSFlipSwitch : NSControl
    {
        #region Private Variables
        private bool _value = false;
        #endregion

        #region Computed Properties
        public bool Value {
            get { return _value; }
            set {
                // Save value and force a redraw
                _value = value;
                NeedsDisplay = true;
            }
        }
        #endregion

        #region Constructors
        public NSFlipSwitch ()
        {
            // Init
            Initialize();
        }

        public NSFlipSwitch (IntPtr handle) : base (handle)
        {
            // Init
            Initialize();
        }

        [Export ("initWithFrame:")]
        public NSFlipSwitch (CGRect frameRect) : base(frameRect) {
            // Init
            Initialize();
        }

        private void Initialize() {
            this.WantsLayer = true;
            this.LayerContentsRedrawPolicy = NSViewLayerContentsRedrawPolicy.OnSetNeedsDisplay;
        }
        #endregion

        #region Draw Methods
        public override void DrawRect (CGRect dirtyRect)
        {
            base.DrawRect (dirtyRect);

            // Use Core Graphic routines to draw our UI
            ...

        }
        #endregion

        #region Private Methods
        private void FlipSwitchState() {
            // Update state
            Value = !Value;
        }
        #endregion

    }
}

La primera cosa a notar sobre nuestra clase personalizada en que estamos heredando de NSControl y usando el comando Register para exponer esta clase a Objective-C y a Interface Builder de Xcode:

[Register("NSFlipSwitch")]
public class NSFlipSwitch : NSControl

En las secciones siguientes, echaremos un vistazo al resto del código anterior en detalle.

Seguimiento del estado del control

Dado que nuestro control personalizado es un conmutador, necesitamos una manera de realizar un seguimiento del estado Activado/Desactivado del conmutador. Lo tratamos con el código siguiente en NSFlipSwitch:

private bool _value = false;
...

public bool Value {
    get { return _value; }
    set {
        // Save value and force a redraw
        _value = value;
        NeedsDisplay = true;
    }
}

Cuando cambia el estado del conmutador, necesitamos una manera de actualizar la interfaz de usuario. Para ello, obligamos al control a volver a dibujar su interfaz de usuario con NeedsDisplay = true.

Si nuestro controlador necesitara más que un único estado de Activado/Desactivado (por ejemplo, un conmutador multiestado con 3 posiciones), podríamos haber usado una Enumeración para supervisar el estado. Para nuestro ejemplo, bastará con un simple booleano.

También hemos agregado un método auxiliar para intercambiar el estado del conmutador entre Activado y Desactivado:

private void FlipSwitchState() {
    // Update state
    Value = !Value;
}

Más adelante, expandiremos esta clase auxiliar para informar al autor de la llamada cuando haya cambiado el estado de los conmutadores.

Dibujar la interfaz del control

Vamos a usar rutinas de dibujo de Core Graphic para dibujar la interfaz de usuario de nuestro control personalizado en runtime. Antes de poder hacerlo, tenemos que activar las capas para nuestro controlador. Lo hacemos con el siguiente método privado:

private void Initialize() {
    this.WantsLayer = true;
    this.LayerContentsRedrawPolicy = NSViewLayerContentsRedrawPolicy.OnSetNeedsDisplay;
}

Este método se llama desde cada uno de los constructores del control para asegurarse de que el control está configurado correctamente. Por ejemplo:

public NSFlipSwitch (IntPtr handle) : base (handle)
{
    // Init
    Initialize();
}

A continuación, es necesario invalidar el método DrawRect y agregar las rutinas de gráfico principal para dibujar el control:

public override void DrawRect (CGRect dirtyRect)
{
    base.DrawRect (dirtyRect);

    // Use Core Graphic routines to draw our UI
    ...

}

Ajustaremos la representación visual del control cuando cambie su estado (por ejemplo, pasar de Activado a Desactivado). Cada vez que cambia el estado, podemos usar el comando NeedsDisplay = true para forzar que el control vuelva a dibujar para el nuevo estado.

Responder a la entrada del usuario

Existen dos formas básicas de agregar entradas de usuario a nuestro control personalizado: Anular las rutinas de control del ratón o Reconocedores de gestos. El método que usamos se basará en la funcionalidad requerida por nuestro control.

Importante

Para cualquier control personalizado que cree, debería usar o bien Métodos de invalidacióno bienReconocedores de gestos, pero no ambos a la vez ya que pueden entrar en conflicto entre sí.

Control de entradas de usuario con métodos de invalidación

Los objetos que heredan de NSControl (o NSView) tienen varios métodos de invalidación para controlar la entrada del mouse o del teclado. Para nuestro control de ejemplo, queremos cambiar el estado del conmutador entre Activado y Desactivado cuando el usuario haga clic en el control con el botón izquierdo del ratón. Podemos agregar los siguientes métodos de invalidación a la clase NSFlipSwitch para controlar esto:

#region Mouse Handling Methods
// --------------------------------------------------------------------------------
// Handle mouse with Override Methods.
// NOTE: Use either this method or Gesture Recognizers, NOT both!
// --------------------------------------------------------------------------------
public override void MouseDown (NSEvent theEvent)
{
    base.MouseDown (theEvent);

    FlipSwitchState ();
}

public override void MouseDragged (NSEvent theEvent)
{
    base.MouseDragged (theEvent);
}

public override void MouseUp (NSEvent theEvent)
{
    base.MouseUp (theEvent);
}

public override void MouseMoved (NSEvent theEvent)
{
    base.MouseMoved (theEvent);
}
## endregion

En el código anterior, llamamos al método FlipSwitchState (definido anteriormente) para cambiar el estado Activado/Desactivado del conmutador en el método MouseDown. Esto también obligará a que el control se vuelva a dibujar para reflejar el estado actual.

Control de entradas de usuario con reconocedores de gestos

Opcionalmente, puede usar reconocedores de gestos para controlar la interacción del usuario con el control. Quite las invalidaciones agregadas anteriormente, edite el método Initialize y haga que tenga un aspecto similar al siguiente:

private void Initialize() {
    this.WantsLayer = true;
    this.LayerContentsRedrawPolicy = NSViewLayerContentsRedrawPolicy.OnSetNeedsDisplay;

    // --------------------------------------------------------------------------------
    // Handle mouse with Gesture Recognizers.
    // NOTE: Use either this method or the Override Methods, NOT both!
    // --------------------------------------------------------------------------------
    var click = new NSClickGestureRecognizer (() => {
        FlipSwitchState();
    });
    AddGestureRecognizer (click);
}

Aquí, estamos creando un nuevo NSClickGestureRecognizer y llamando al método FlipSwitchState para cambiar el estado del conmutador cuando el usuario hace clic en él con el botón izquierdo del mouse. El método AddGestureRecognizer (click) agrega el reconocedor de gestos al control.

De nuevo, el método que usamos depende de lo que intentamos lograr con nuestro control personalizado. Si necesitamos un acceso de bajo nivel a la interacción del usuario, use los Métodos de invalidación. Si necesitamos una funcionalidad predefinida, como hacer clic con el ratón, use los Reconocedores de gestos.

Responder a eventos de cambio de estado

Cuando el usuario cambia el estado de nuestro control personalizado, necesitamos una forma de responder al cambio de estado en código (como hacer algo cuando se hace clic en un botón personalizado).

Para proporcionar esta funcionalidad, edite la clase NSFlipSwitch y agregue el código siguiente:

#region Events
public event EventHandler ValueChanged;

internal void RaiseValueChanged() {
    if (this.ValueChanged != null)
        this.ValueChanged (this, EventArgs.Empty);

    // Perform any action bound to the control from Interface Builder
    // via an Action.
    if (this.Action !=null)
        NSApplication.SharedApplication.SendAction (this.Action, this.Target, this);
}
## endregion

A continuación, edite el método FlipSwitchState y haga que tenga el siguiente aspecto:

private void FlipSwitchState() {
    // Update state
    Value = !Value;
    RaiseValueChanged ();
}

En primer lugar, proporcionamos un evento ValueChanged al que podemos agregar un controlador en código de C# para que podamos realizar una acción cuando el usuario cambie el estado del modificador.

En segundo lugar, como nuestro controlador personalizado hereda de NSControl, automáticamente tiene una Acción que puede asignarse en Interface Builder de Xcode. Para llamar a esta acción cuando cambia el estado, usamos el código siguiente:

if (this.Action !=null)
    NSApplication.SharedApplication.SendAction (this.Action, this.Target, this);

En primer lugar, comprobamos si se ha asignado una acción al control. A continuación, llamamos a la acción si se ha definido.

Uso del control personalizado

Con nuestro control personalizado totalmente definido, podemos agregarlo a la interfaz de usuario de la aplicación de Xamarin.Mac mediante código C# o en Interface Builder de Xcode.

Para agregar el control usando Interface Builder, primero haga una compilación limpia del proyecto de Xamarin.Mac y después haga doble clic en el archivo Main.storyboard para abrirlo en Interface Builder y editarlo:

Editing the storyboard in Xcode

A continuación, arrastre un Custom View al diseño de la interfaz de usuario:

Selecting a Custom View from the Library

Con la vista personalizada aún seleccionada, cambie al Inspector de identidad y cambie la Clase de la vista a NSFlipSwitch:

Setting the View's class

Cambie al Editor asistente y cree una Salida para el control personalizado (asegurándose de enlazarlo en el archivo ViewController.h y no en el archivo .m):

Configuring a new Outlet

Guarde los cambios, vuelva a Visual Studio para Mac y permita que los cambios se sincronicen. Edite el archivo ViewController.cs y haga que el método ViewDidLoad se parezca a lo siguiente:

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

    // Do any additional setup after loading the view.
    OptionTwo.ValueChanged += (sender, e) => {
        // Display the state of the option switch
        Console.WriteLine("Option Two: {0}", OptionTwo.Value);
    };
}

Aquí, respondemos al evento ValueChanged que definimos anteriormente en la clase NSFlipSwitch y escribimos el Valor actual cuando el usuario hace clic en el control.

Opcionalmente, podríamos volver a Interface Builder y definir una Acción en el controlador:

Configuring a new Action

De nuevo, edite el archivo ViewController.cs y agregue el método siguiente:

partial void OptionTwoFlipped (Foundation.NSObject sender) {
    // Display the state of the option switch
    Console.WriteLine("Option Two: {0}", OptionTwo.Value);
}

Importante

Debe usar el Evento o definir una Acción en Interface Builder, pero no debe usar ambos métodos al mismo tiempo o pueden entrar en conflicto.

Resumen

En este artículo se ha echado un vistazo detallado a la creación de un control de interfaz de usuario personalizado reutilizable en una aplicación de Xamarin.Mac. Hemos visto cómo dibujar la interfaz de usuario de los controles personalizados, las dos formas principales de responder a la entrada del ratón y del usuario y cómo exponer el nuevo control a las acciones en Interface Builder de Xcode.