Construir controlos XAML com templates

Este artigo guia-o como criar um controlo XAML templateado para o WinUI 3. Os controles de modelo herdam de Microsoft.UI.Xaml.Controls.Control e têm estrutura visual e comportamento visual que podem ser personalizados usando modelos de controle XAML.

Pré-requisitos

  1. Configure o seu ambiente de desenvolvimento — veja Instale ferramentas para o Windows App SDK.
  2. Siga as instruções sobre como Criar o seu primeiro WinUI project.

Para criar componentes WinUI autónomos em C# para consumo tanto em aplicações C# como em C++/WinRT, consulte o artigo Walkthrough: Crie um componente C# com controlos WinUI e consuma-o a partir de uma aplicação Windows App SDK C++.

  1. Comece a desenvolver Windows apps
  2. Descarregue e instale a versão mais recente da Extensão Visual Studio C++/WinRT (VSIX)

Crie um App WinUI em Branco (BgLabelControlApp)

Comece por criar um novo projeto no Microsoft Visual Studio. No diálogo Criar um novo projeto, selecione o modelo de projeto WinUI Blank App (Packaged) e selecione a versão de idioma apropriada. Defina o nome do project para "BgLabelControlApp" para que os nomes dos ficheiros se alinhem com o código dos exemplos abaixo.

Modelo de Projeto WinUI Blank App (Empacotado)

Modelo de Projeto WinUI Blank App (Empacotado)

Adicionar um controle de modelo ao seu aplicativo

Para adicionar um controlo templateado, clique no menu Project na barra de ferramentas ou clique com o botão direito no seu project em Solution Explorer e selecione Adicionar Novo Item.

Em Visual C#->WinUI , selecione o modelo Templated Control . Nomeie o novo controle "BgLabelControl" e clique em Adicionar.

Em Visual C++->WinUI , selecione o modelo Templated Control . Nomeie o novo controle "BgLabelControl" e clique em Adicionar. Isto vai adicionar três novos ficheiros ao seu projecto. BgLabelControl.h é o cabeçalho que contém as declarações de controle e BgLabelControl.cpp contém a implementação C++/WinRT do controle. BgLabelControl.idl é o arquivo de definição de interface que permite que o controle seja instanciado como uma classe de tempo de execução.

Implementar a classe de controle personalizado BgLabelControl

No arquivo C#, BgLabelControl.cs, observe que o construtor define a propriedade DefaultStyleKey para o nosso controle. Esta chave identifica o modelo padrão que será usado se o consumidor do controlo não especificar explicitamente um modelo. O valor-chave é o tipo do nosso controlo. Veremos essa chave em uso mais tarde quando implementarmos nosso arquivo de modelo genérico.

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

Nosso controle de modelo terá um rótulo de texto que pode ser definido programaticamente em código, em XAML ou por meio de vinculação de dados. Para que o sistema mantenha o texto do rótulo do nosso controle atualizado, ele precisa ser implementado como um DependencyPropety. To do isto, primeiro declaramos uma propriedade string e chamamos-lhe Label. Em vez de usar uma variável de suporte, definimos e obtemos o valor de nossa propriedade de dependência chamando GetValue e SetValue. Esses métodos são fornecidos pelo DependencyObject, do qual o Microsoft.UI.Xaml.Controls.Control herda.

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

Em seguida, declare a propriedade de dependência e registre-a no sistema chamando DependencyProperty.Register. Esse método especifica o nome e o tipo de nossa propriedade Label, o tipo do proprietário da propriedade, nossa classe BgLabelControl e o valor padrão para a propriedade.

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

Essas duas etapas são tudo o que é necessário para implementar uma propriedade de dependência, mas para este exemplo, adicionaremos um manipulador opcional para o evento OnLabelChanged. Este evento é gerado pelo sistema sempre que o valor do imóvel é atualizado. Neste caso, verificamos se o novo texto do rótulo é uma cadeia de caracteres vazia ou não e atualizamos uma variável de classe de acordo.

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

Para obter mais informações sobre como as propriedades de dependência funcionam, consulte Visão geral das propriedades de dependência.

Nos passos seguintes, irá atualizar o código nos ficheiros BgLabelControl.idl, BgLabelControl.h e BgLabelControl.cpp no diretório project para implementar a classe de runtime.

A classe de controle templated será instanciada a partir da marcação XAML e, por esse motivo, será uma classe de tempo de execução. Quando você constrói o projeto finalizado, o compilador MIDL (midl.exe) utilizará o ficheiro BgLabelControl.idl para criar o ficheiro de metadados do Windows Runtime (.winmd) para o seu controlo, que será consultado pelos consumidores do seu componente. Para mais informações sobre a criação de classes de execução, consulte Author APIs with C++/WinRT.

O controle templated que estamos criando irá expor uma única propriedade que é uma cadeia de caracteres que será usada como um rótulo para o controle. Substitua o conteúdo de BgLabelControl.idl pelo seguinte código.

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

A listagem acima mostra o padrão que você segue ao declarar uma propriedade de dependência (DP). Há duas peças para cada DP. Primeiro, você declara uma propriedade estática somente leitura do tipo DependencyProperty. Tem o nome do seu DP mais Propriedade. Você usará essa propriedade estática em sua implementação. Em segundo lugar, declara-se uma propriedade de instância de leitura e escrita com o tipo e o nome do seu DP. Se você deseja criar uma propriedade anexada (em vez de um DP), consulte os exemplos de código em Propriedades anexadas personalizadas.

Substitua o conteúdo de BgLabelControl.h com o código a seguir.

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

O código mostrado acima implementa as propriedades Label e LabelProperty, adiciona um manipulador de eventos estático chamado OnLabelChanged para processar alterações no valor da propriedade de dependência e adiciona um membro privado para armazenar o campo de apoio para LabelProperty.

Em seguida, substitua o conteúdo do BgLabelControl.cpp pelo código a seguir.

// 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 passo a passo não usará o retorno de chamada OnLabelChanged, mas é fornecido para que possa ver como registar uma propriedade de dependência com um retorno de chamada de alteração de propriedade. A implementação de OnLabelChanged também mostra como obter um tipo projetado derivado de um tipo projetado base (o tipo projetado base é DependencyObject, neste caso). E mostra como então obter um ponteiro para o tipo que implementa o tipo projetado. Essa segunda operação será naturalmente possível apenas no project que implementa o tipo projetado (isto é, no project que implementa a classe de runtime).

A função xaml_typename é fornecida pelo espaço de nomes Windows.UI.Xaml.Interop, que não está incluído por padrão no modelo de projeto WinUI. Adicione uma linha ao ficheiro de cabeçalho pré-compilado do seu project, pch.h, para incluir o ficheiro de cabeçalho associado a este namespace.

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

Definir o estilo padrão para BgLabelControl

Um controle de modelo deve fornecer um modelo de estilo padrão que é usado se o usuário do controle não definir explicitamente um estilo. No seu construtor, o controlo define uma chave de estilo padrão para si próprio.

O ficheiro modelo genérico chama-se "Generic.xaml" e deve estar localizado numa pasta Themes na sua project. Os nomes de pasta e arquivo são necessários para que a estrutura XAML encontre o estilo padrão para um controle de modelo.

O ficheiro de modelo genérico é gerado quando adiciona o Controlo com Modelo à sua aplicação.

No nó do teu project, cria uma nova pasta (não um filtro, mas uma pasta) e chama-lhe "Themes". Em Themes, adicione um novo item do tipo Visual C++ > WinUI > Resource Dictionary (WinUI) e nomeie-o "Generic.xaml".

Exclua o conteúdo padrão de Generic.xaml e cole a marcação abaixo.

<!-- \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>

O atributo TargetType do elemento Style está definido para o nosso tipo BgLabelControl dentro do namespace BgLabelControlApp. Este tipo é o mesmo valor especificado para a propriedade DefaultStyleKey no construtor do controlo, que identifica este como o estilo padrão do controlo.

A propriedade Text do TextBlock no template de controlo está ligada à propriedade de dependência Label do nosso controlo usando a marcação TemplateBinding. O fundo Grid está ligado à propriedade de dependência Background herdada da classe Control .

Adicionar uma instância de BgLabelControl à página principal da interface do usuário

Abra o MainWindow.xaml, que contém a marcação XAML para a nossa página principal da interface de utilizador. Imediatamente após o elemento Button (dentro do StackPanel), adicione a seguinte marcação.

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

Além disso, adicione a seguinte directiva de inclusão no MainWindow.h para que o tipo MainWindow (uma combinação de compilação de marcação XAML e código imperativo) reconheça o tipo de controlo de modelo BgLabelControl. Se você quiser usar BgLabelControl de outra página XAML, adicione essa mesma diretiva include ao arquivo de cabeçalho dessa página também. Ou, alternativamente, basta colocar uma única diretiva include em seu arquivo de cabeçalho pré-compilado.

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

Agora, construa e execute o projeto. Você verá que o modelo de controle padrão está associado ao pincel de fundo e ao rótulo da instância do BgLabelControl na marcação.

Resultado do controle templatizado

Métodos de controlo de substituição

Você deriva um controle modelado da classe de tempo de execução Control, que por sua vez deriva de classes base de tempo de execução. Existem métodos sobrescritíveis de Control, FrameworkElement e UIElement que podes sobrepor na tua classe derivada para personalizar o comportamento do controlo.

Em C#, métodos substituíveis aparecem como protected override métodos. Por exemplo, pode sobrescrever a medição de layout, aplicação de templates, gestão de entradas de ponteiros e criação de pares de automação:

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

Em C++/WinRT, as funções substituíveis não são nem virtuais, nem protegidas, mas pode ainda assim substituí-las e fornecer a sua própria implementação.

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

Gerar os ficheiros fonte de controlo sem usar um template

Esta secção mostra como pode gerar os ficheiros de origem necessários para criar o seu controlo personalizado sem usar o modelo de controlo de item Templated Control.

Primeiro, adicione um novo item Midl File (.idl) ao projecto. No menu Project, selecione Adicionar Novo Item... e escreva "MIDL" na caixa de pesquisa para encontrar o item do ficheiro .idl. Nomeie o novo arquivo BgLabelControl.idl para que o nome seja consistente com as etapas neste artigo. Exclua o conteúdo padrão do BgLabelControl.idle cole na declaração de classe em tempo de execução mostrada nas etapas acima.

Depois de guardar o novo ficheiro .idl, o passo seguinte é gerar o ficheiro de metadados Windows Runtime (.winmd) e stubs para os ficheiros de implementação .cpp e .h que irá usar para implementar o controlo templateado. Gere esses arquivos criando a solução, o que fará com que o compilador MIDL (midl.exe) compile o arquivo .idl que você criou. Note que a Solução não irá compilar com sucesso e o Visual Studio mostrará erros de compilação na janela de saída, mas os ficheiros necessários serão gerados.

Copie os ficheiros stub BgLabelControl.h e BgLabelControl.cpp de \BgLabelControlApp\BgLabelControlApp\Generated Files\sources\ para a pasta project. Em Solution Explorer, certifique-se de que Mostrar Todos os Ficheiros está ativado. Clique com o botão direito nos ficheiros stub que copiou e clique em Incluir em Project.

O compilador coloca uma linha static_assert na parte superior de BgLabelControl.h e BgLabelControl.cpp para impedir que os arquivos gerados sejam compilados. Ao implementar o seu controlo, deve remover essas linhas dos ficheiros que colocou no diretório do projeto. Para este tutorial, podes simplesmente substituir todo o conteúdo dos arquivos pelo código fornecido acima.

Consulte também