Compilación de controles con plantilla XAML

En este artículo se explica cómo crear un control XAML con plantilla para WinUI 3. Los controles con plantilla se heredan de Microsoft.UI.Xaml.Controls.Control y tienen una estructura y comportamiento visual que se pueden personalizar mediante plantillas de control XAML.

Prerrequisitos

  1. Configuración del entorno de desarrollo: consulte Instalar herramientas para el Windows App SDK.
  2. Siga las instrucciones sobre cómo Crear su primer proyecto WinUI.

Para crear componentes de WinUI independientes en C# para su consumo desde aplicaciones de C# y C++/WinRT, consulte el artículo Walkthrough: Crear un componente de C# con controles WinUI y consumirlo desde una aplicación de Windows App SDK de C++.

  1. Empezar a desarrollar aplicaciones de Windows
  2. Descargue e instale la versión más reciente de la extensión de C++/WinRT Visual Studio (VSIX)

Crear una aplicación en blanco de WinUI (BgLabelControlApp)

Comience creando un nuevo project en Microsoft Visual Studio. En el cuadro de diálogo Crear un nuevo proyecto, seleccione la plantilla del proyecto WinUI Blank App (Empaquetado) y seleccione la versión de idioma adecuada. Establezca el nombre del project en "BgLabelControlApp" para que los nombres de archivo se alineen con el código de los ejemplos siguientes.

Plantilla de proyecto aplicación en blanco (empaquetada) de WinUI

Plantilla de proyecto aplicación en blanco (empaquetada) de WinUI

Adición de un control con plantilla a la aplicación

Para agregar un control con plantilla, haga clic en el menú Project de la barra de herramientas o haga clic con el botón derecho en el project en Solution Explorer y seleccione Agregar nuevo elemento.

En Visual C#->WinUI , seleccione la plantilla Control con plantilla . Asigne al nuevo control el nombre "BgLabelControl" y haga clic en Agregar.

En Visual C++->WinUI , seleccione la plantilla Control con plantilla . Asigne al nuevo control el nombre "BgLabelControl" y haga clic en Agregar. Esto agregará tres nuevos archivos a tu proyecto. BgLabelControl.h es el encabezado que contiene las declaraciones de control y BgLabelControl.cpp contiene la implementación de C++/WinRT del control. BgLabelControl.idl es el archivo de definición de interfaz que permite crear instancias del control como una clase en tiempo de ejecución.

Implementación de la clase de control personalizado BgLabelControl

En el archivo de C#, BgLabelControl.cs, tenga en cuenta que el constructor define la propiedad DefaultStyleKey para el control. Esta clave identifica la plantilla predeterminada que se usará si el consumer del control no especifica explícitamente una plantilla. El valor de clave es el tipo de nuestro control. Veremos esta clave en uso más adelante cuando implementemos el archivo de plantilla genérico.

public BgLabelControl()
{
    this.DefaultStyleKey = typeof(BgLabelControl);
}

Nuestro control con plantilla tendrá una etiqueta de texto que se puede establecer mediante programación en el código, en XAML o mediante el enlace de datos. Para que el sistema mantenga actualizado el texto de la etiqueta del control, es necesario implementarlo como DependencyPropety. To do esto, primero declaramos una propiedad de cadena y la llamamos Label. En lugar de usar una variable alternativa, establecemos y obtenemos el valor de nuestra propiedad de dependencia mediante una llamada a GetValue y SetValue. Estos métodos los proporciona DependencyObject, que hereda el elemento Microsoft.UI.Xaml.Controls.Control.

public string Label
{
    get => (string)GetValue(LabelProperty);
    set => SetValue(LabelProperty, value);
}

A continuación, declare la propiedad de dependencia y regístrela en el sistema mediante una llamada a DependencyProperty.Register. Este método especifica el nombre y el tipo de la propiedad Label, el tipo de propietario de la propiedad, nuestra clase BgLabelControl y el valor predeterminado de la propiedad.

DependencyProperty LabelProperty = DependencyProperty.Register(
    nameof(Label), 
    typeof(string),
    typeof(BgLabelControl), 
    new PropertyMetadata(default(string), new PropertyChangedCallback(OnLabelChanged)));

Estos dos pasos son todos necesarios para implementar una propiedad de dependencia, pero, en este ejemplo, vamos a agregar un controlador opcional para el evento OnLabelChanged. El sistema genera este evento cada vez que se actualiza el valor de la propiedad. En este caso, comprobamos si el nuevo texto de etiqueta es una cadena vacía o no y actualizamos una variable de clase según corresponda.

public bool HasLabelValue { get; set; }

private static void OnLabelChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
    BgLabelControl labelControl = d as BgLabelControl; //null checks omitted
    String s = e.NewValue as String; //null checks omitted
    if (s == String.Empty)
    {
        labelControl.HasLabelValue = false;
    }
    else
    {
        labelControl.HasLabelValue = true;
    }
}

Si quiere obtener más información acerca de cómo funcionan las propiedades de dependencia, consulte Introducción a las propiedades de dependencia.

En los pasos siguientes, actualizará el código en los archivos BgLabelControl.idl, BgLabelControl.h y BgLabelControl.cpp del directorio project para implementar la clase en tiempo de ejecución.

La clase de control plantilla se instanciará a partir del marcado XAML y, por ese motivo, será una clase de ejecución en tiempo real. Cuando compile el proyecto terminado, el compilador MIDL (midl.exe) usará el archivo BgLabelControl.idl para generar el archivo de metadatos de Windows Runtime (.winmd) para el control, al que harán referencia los consumidores del componente. Para obtener más información sobre la creación de clases en tiempo de ejecución, consulte Desarrollar API con C++/WinRT.

El control con plantilla que estamos creando expondrá una sola propiedad, que es una cadena que se usará como etiqueta para el control. Reemplace el contenido de BgLabelControl.idl por el código siguiente.

// BgLabelControl.idl
namespace BgLabelControlApp
{
    runtimeclass BgLabelControl : Microsoft.UI.Xaml.Controls.Control
    {
        BgLabelControl();
        static Microsoft.UI.Xaml.DependencyProperty LabelProperty{ get; };
        String Label;
    }
}

La lista anterior muestra el patrón que sigue al declarar una propiedad de dependencia (DP). Hay dos piezas en cada DP. En primer lugar, declara una propiedad estática de solo lectura de tipo DependencyProperty. Tiene el nombre de su DP plus Property. Usará esta propiedad estática en su implementación. En segundo lugar, declara una propiedad de instancia de lectura y escritura con el tipo y el nombre de tu DP. Si desea crear una propiedad adjunta (en lugar de un DP), consulte los ejemplos de código de Propiedades adjuntas personalizadas.

Reemplace el contenido de BgLabelControl.h por el código siguiente.

// BgLabelControl.h
#pragma once
#include "BgLabelControl.g.h"

namespace winrt::BgLabelControlApp::implementation
{
    struct BgLabelControl : BgLabelControlT<BgLabelControl>
    {
        BgLabelControl() { DefaultStyleKey(winrt::box_value(L"BgLabelControlApp.BgLabelControl")); }

        winrt::hstring Label()
        {
            return winrt::unbox_value<winrt::hstring>(GetValue(m_labelProperty));
        }

        void Label(winrt::hstring const& value)
        {
            SetValue(m_labelProperty, winrt::box_value(value));
        }

        static Microsoft::UI::Xaml::DependencyProperty LabelProperty() { return m_labelProperty; }

        static void OnLabelChanged(Microsoft::UI::Xaml::DependencyObject const&, Microsoft::UI::Xaml::DependencyPropertyChangedEventArgs const&);

    private:
        static Microsoft::UI::Xaml::DependencyProperty m_labelProperty;
    };
}
namespace winrt::BgLabelControlApp::factory_implementation
{
    struct BgLabelControl : BgLabelControlT<BgLabelControl, implementation::BgLabelControl>
    {
    };
}

El código anterior implementa las propiedades Label y LabelProperty, agrega un controlador de eventos estático denominado OnLabelChanged para procesar los cambios en el valor de la propiedad de dependencia y agrega un miembro privado para almacenar el campo de respaldo para LabelProperty.

A continuación, reemplace el contenido de BgLabelControl.cpp por el código siguiente.

// BgLabelControl.cpp
#include "pch.h"
#include "BgLabelControl.h"
#if __has_include("BgLabelControl.g.cpp")
#include "BgLabelControl.g.cpp"
#endif

namespace winrt::BgLabelControlApp::implementation
{
    Microsoft::UI::Xaml::DependencyProperty BgLabelControl::m_labelProperty =
        Microsoft::UI::Xaml::DependencyProperty::Register(
            L"Label",
            winrt::xaml_typename<winrt::hstring>(),
            winrt::xaml_typename<BgLabelControlApp::BgLabelControl>(),
            Microsoft::UI::Xaml::PropertyMetadata{ winrt::box_value(L"default label"), Microsoft::UI::Xaml::PropertyChangedCallback{ &BgLabelControl::OnLabelChanged } }
    );

    void BgLabelControl::OnLabelChanged(Microsoft::UI::Xaml::DependencyObject const& d, Microsoft::UI::Xaml::DependencyPropertyChangedEventArgs const& /* e */)
    {
        if (BgLabelControlApp::BgLabelControl theControl{ d.try_as<BgLabelControlApp::BgLabelControl>() })
        {
            // Call members of the projected type via theControl.

            BgLabelControlApp::implementation::BgLabelControl* ptr{ winrt::get_self<BgLabelControlApp::implementation::BgLabelControl>(theControl) };
            // Call members of the implementation type via ptr.
        }
    }
}

Este tutorial no usará el OnLabelChanged devolución de llamada, pero se proporciona para que pueda ver cómo registrar una propiedad de dependencia con una devolución de llamada modificada por propiedades. La implementación de OnLabelChanged también muestra cómo obtener un tipo proyectado derivado de un tipo proyectado base (el tipo proyectado base es DependencyObject, en este caso). Esto muestra cómo obtener un puntero al tipo que implementa el tipo proyectado. Esa segunda operación solo será posible naturalmente en el project que implementa el tipo proyectado (es decir, el project que implementa la clase en tiempo de ejecución).

La función xaml_typename es proporcionada por el espacio de nombres Windows.UI.Xaml.Interop, que no se incluye de forma predeterminada en la plantilla del proyecto WinUI. Agregue una línea al archivo de encabezado precompilado del proyecto, pch.h, para incluir el archivo de encabezado asociado con este espacio de nombres.

// pch.h
...
#include <winrt/Windows.UI.Xaml.Interop.h>
...

Definir el estilo predeterminado para BgLabelControl

Un control con plantilla debe proporcionar una plantilla de estilo predeterminada que se utilice si el usuario del control no establece explícitamente un estilo. En su constructor, el control establece una clave de estilo predeterminada para sí misma.

El archivo de plantilla genérico se denomina "Generic.xaml" y debe encontrarse en una carpeta Themes en el project. Los nombres de carpeta y archivo son obligatorios para que el marco XAML encuentre el estilo predeterminado para un control con plantilla.

El archivo de plantilla genérico se genera al agregar el control con plantilla a la aplicación.

En el nodo project, cree una nueva carpeta (no un filtro, sino una carpeta) y asígnela el nombre "Temas". En Themes, agregue un nuevo elemento de tipo Visual C++ > WinUI > Diccionario de recursos (WinUI)y asígnele el nombre "Generic.xaml".

Elimine el contenido predeterminado de Generic.xaml y pegue el marcado siguiente.

<!-- \Themes\Generic.xaml -->
<ResourceDictionary
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="using:BgLabelControlApp">

    <Style TargetType="local:BgLabelControl" >
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="local:BgLabelControl">
                    <Grid Width="100" Height="100" Background="{TemplateBinding Background}">
                        <TextBlock HorizontalAlignment="Center" VerticalAlignment="Center" Text="{TemplateBinding Label}"/>
                    </Grid>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>
</ResourceDictionary>

El atributo TargetType del elemento Style se establece en nuestro tipo BgLabelControl dentro del espacio de nombres BgLabelControlApp . Este tipo es el mismo valor especificado para la propiedad DefaultStyleKey en el constructor del control, que lo identifica como el estilo predeterminado para el control.

La propiedad Text en la plantilla de control TextBlock está enlazada a la propiedad de dependencia Label del control mediante TemplateBinding extensión de marcado. El fondo grid está enlazado a la propiedad de dependencia Background heredada de la clase Control .

Adición de una instancia de BgLabelControl a la página principal de la interfaz de usuario

Abra MainWindow.xaml, que contiene el marcado XAML para nuestra página principal de la interfaz de usuario. Inmediatamente después del elemento Button (dentro de la clase StackPanel), agrega el marcado siguiente.

<local:BgLabelControl Background="Red" Label="Hello, World!"/>

Además, agregue la siguiente directiva de inclusión en MainWindow.h para que el tipo MainWindow (una combinación de marcado XAML compilado y código imperativo) tenga en cuenta el tipo de control template BgLabelControl. Si quieres usar BgLabelControl desde otra página XAML, agrega también esta misma directiva include al archivo de encabezado de esa página. O bien, como alternativa, simplemente coloque una única directiva include en el archivo de encabezado precompilado.

//MainWindow.h
...
#include "BgLabelControl.h"
...

Ahora compile y ejecute el proyecto. Verá que la plantilla de control predeterminada está vinculada al pincel de fondo y a la etiqueta de la instancia BgLabelControl en el marcado.

resultado del control basado en plantilla

Invalidación de métodos de control

Derivas un control con plantilla de la clase en tiempo de ejecución Control, que además se deriva de clases base en tiempo de ejecución. Hay métodos sobrescribibles de Control, FrameworkElement y UIElement que puede sobrescribir en su clase derivada para personalizar el comportamiento de los controles.

En C#, los métodos reemplazables aparecen como protected override métodos. Por ejemplo, puede sobrescribir la medición de diseño, la aplicación de plantillas, el manejo de la entrada de puntero y la creación del par de automatización.

// Control overrides.
protected override void OnPointerPressed(PointerRoutedEventArgs e)
{
    base.OnPointerPressed(e);
    // Custom pointer handling...
}

// FrameworkElement overrides.
protected override Size MeasureOverride(Size availableSize)
{
    // Custom measure logic...
    return base.MeasureOverride(availableSize);
}

protected override void OnApplyTemplate()
{
    base.OnApplyTemplate();
    // Retrieve template parts and set up event handlers...
}

// UIElement overrides.
protected override AutomationPeer OnCreateAutomationPeer()
{
    return new BgLabelControlAutomationPeer(this);
}

En C++/WinRT, las funciones reemplazables no son virtuales ni protegidas, pero todavía puede invalidarlos y proporcionar su propia implementación:

// Control overrides.
void OnPointerPressed(Microsoft::UI::Xaml::Input::PointerRoutedEventArgs const& /* e */) const { ... };

// FrameworkElement overrides.
Windows::Foundation::Size MeasureOverride(Windows::Foundation::Size const& /* availableSize */) const { ... };
void OnApplyTemplate() const { ... };

// UIElement overrides.
Microsoft::UI::Xaml::Automation::Peers::AutomationPeer OnCreateAutomationPeer() const { ... };

Generación de los archivos de código fuente de control sin usar una plantilla

En esta sección se muestra cómo puede generar los archivos de código fuente necesarios para crear el control personalizado sin usar la plantilla de elemento Control con plantilla .

En primer lugar, agregue un nuevo elemento Midl File (.idl) al project. En el menú Project, seleccione Agregar nuevo elemento... y escriba "MIDL" en el cuadro de búsqueda para buscar el elemento de archivo .idl. Asigne un nombre al nuevo archivo BgLabelControl.idl para que el nombre sea coherente con los pasos descritos en este artículo. Elimine el contenido predeterminado de BgLabelControl.idly pegue la declaración de clase en tiempo de ejecución que se muestra en los pasos anteriores.

Después de guardar el nuevo archivo .idl, el siguiente paso consiste en generar el archivo de metadatos de Windows Runtime (.winmd) y códigos auxiliares para los archivos de implementación .cpp y .h que usará para implementar el control con plantilla. Genere estos archivos mediante la compilación de la solución, lo que hará que el compilador MIDL (midl.exe) compile el archivo .idl que creó. Tenga en cuenta que la solución no se compilará correctamente y Visual Studio mostrará errores de compilación en la ventana de salida, pero se generarán los archivos necesarios.

Copie los archivos auxiliares BgLabelControl.h y BgLabelControl.cpp de \BgLabelControlApp\BgLabelControlApp\Generated Files\sources\ en la carpeta project. En Solution Explorer, asegúrese de que Mostrar todos los archivos está activado. Haga clic con el botón derecho en los archivos auxiliares que copió y haga clic en Incluir en Project.

El compilador coloca una línea de static_assert en la parte superior de BgLabelControl.h y BgLabelControl.cpp para evitar que se compilen los archivos generados. Al implementar el control, debe quitar estas líneas de los archivos que ha colocado en el directorio project. En esta guía, puedes sobrescribir todo el contenido de los archivos con el código proporcionado anteriormente.

Consulte también