使用 C++/WinRT 的 XAML 自定义(模板化)控件

Important

有关帮助你理解如何使用 C++/WinRT 使用和编写运行时类的基本概念和术语,请参阅 使用 C++/WinRT 使用 API使用 C++/WinRT 编写 API

Windows 应用 SDK最强大的功能之一是用户界面 (UI) 堆栈提供的灵活性,用于基于 XAML 控件类型创建自定义控件。 XAML UI 框架提供 自定义依赖属性附加属性控件模板等功能,使创建功能丰富且可自定义的控件变得容易。 本主题指导你完成使用 C++/WinRT 创建自定义(模板化)控件的步骤。

创建一个名为 BgLabelControlApp 的空白应用

首先,在 Microsoft Visual Studio 中创建新项目。 为 C++ 创建一个 空白应用,已打包(桌面版 WinUI 3) 项目,将其名称设置为 BgLabelControlApp,并且(为了使你的文件夹结构与本演练一致)确保未选中 将解决方案和项目放在同一目录中。 以最新版且正式发布的 Windows SDK(即非预览版)为目标。

在本主题的后续部分中,系统会引导你构建项目(但在那之前先不要构建)。

注释

有关为 C++/WinRT 开发设置Visual Studio(包括安装和使用 C++/WinRT Visual Studio 扩展(VSIX)和 NuGet 包(一起提供项目模板和生成支持)的信息,请参阅Visual Studio对 C++/WinRT 的支持

我们将创作一个新类来表示自定义(模板化)控件。 我们在同一个编译单元中编写并使用该类。 但我们希望能够从 XAML 标记实例化此类,因此它将是运行时类。 我们将使用 C++/WinRT 来编写和使用它。

创作新运行时类的第一步是向项目添加新 的 Midl File (.idl) 项。 将其命名为 BgLabelControl.idl。 删除 BgLabelControl.idl 的默认内容,并粘贴入此运行时类声明。

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

上面的列表显示了声明依赖属性 (DP) 时遵循的模式。 每个 DP 都有两个部分。 首先,声明 DependencyProperty 类型的只读静态属性。 它的名称是你的 DP 名称加上 属性。 你将在实现中使用此静态属性。 其次,使用 DP 的类型和名称声明读写实例属性。 如果要创作 附加属性 (而不是 DP),请参阅 自定义附加属性中的代码示例。

注释

如果需要具有浮点类型的 DP,请将其 double 设为(DoubleMIDL 3.0 中)。 声明并实现一个类型为 float 的 DP(在 MIDL 中为 Single),然后在 XAML 标记中为该 DP 设置值,会导致错误 无法从文本“<NUMBER>”创建“Windows.Foundation.Single”

保存文件。 该项目目前还无法成功完成生成,不过现在进行生成仍然很有用,因为这会生成源代码文件,你将在这些文件中实现 BgLabelControl 运行时类。 因此,现在继续生成(你预计在此阶段看到的生成错误与“未解决的外部符号”有关)。

在构建过程中,会运行 midl.exe 工具来创建用于描述运行时类的 Windows 运行时 元数据文件(\BgLabelControlApp\Debug\BgLabelControlApp\Unmerged\BgLabelControl.winmd)。 然后,该工具 cppwinrt.exe 将运行以生成源代码文件,以支持你创作和使用运行时类。 这些文件包括用于开始实现在 IDL 中声明的 BgLabelControl 运行时类的存根。 这些存根是 \BgLabelControlApp\BgLabelControlApp\Generated Files\sources\BgLabelControl.hBgLabelControl.cpp

将存根文件BgLabelControl.hBgLabelControl.cpp复制到\BgLabelControlApp\BgLabelControlApp\Generated Files\sources\项目文件夹中,即 \BgLabelControlApp\BgLabelControlApp\。 在解决方案资源管理器中,确保打开“显示所有文件”。 右键单击复制的存根文件,然后单击加入项目

你会在BgLabelControl.hBgLabelControl.cpp的顶部看到一个static_assert,你需要将其删除。 现在项目可以构建了。

实现 BgLabelControl 自定义控件类

现在,让我们打开 \BgLabelControlApp\BgLabelControlApp\BgLabelControl.hBgLabelControl.cpp 实现运行时类。 在 BgLabelControl.h中,更改构造函数以设置默认样式键,实现 LabelLabelProperty,添加一个名为 OnLabelChanged 的静态事件处理程序以处理对依赖属性的值的更改,并添加一个私有成员来存储 LabelProperty 的后备字段。

添加这些内容后,你的 BgLabelControl.h 如下所示。 可以复制并粘贴此代码列表以替换其内容 BgLabelControl.h

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

BgLabelControl.cpp中,定义如下所示的静态成员。 可以复制并粘贴此代码列表以替换其内容 BgLabelControl.cpp

// BgLabelControl.cpp
#include "pch.h"
#include "BgLabelControl.h"
#include "BgLabelControl.g.cpp"

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

在本演练中,我们不会使用 OnLabelChanged。 但是,你可以看到如何使用属性更改的回调注册依赖属性。 OnLabelChanged 的实现还显示了如何从基本投影类型获取派生的投影类型(在本例中,基投影类型为 DependencyObject)。 并演示如何获取指向实现投影类型的类型的指针。 第二个操作自然只能在实现投影类型的项目(即实现运行时类的项目)中实现。

注释

如果尚未安装 Windows SDK 版本 10.0.17763.0(Windows 10 版本 1809 或更高版本),则需要在上述依赖项属性中调用 winrt::from_abi 更改事件处理程序,而不是 winrt::get_self

设计 BgLabelControl 的默认样式

在其构造函数中,BgLabelControl 为自己设置默认样式键。 但是 什么是 默认样式? 自定义(模板化)控件需要具有默认样式(包含默认控件模板),当控件使用者未设置样式和/或模板时,该模板可用于呈现自身。 在本部分中,我们将向包含默认样式的项目添加标记文件。

确保显示所有文件解决方案资源管理器中仍处于开启状态。 在项目节点下,创建新文件夹(不是筛选器,而是文件夹),并将其命名为“主题”。 在下方 Themes,添加 Visual C++>XAML XAML>视图类型的新项,并将其命名为“Generic.xaml”。 文件夹和文件名必须如下所示,以便 XAML 框架查找自定义控件的默认样式。 删除 Generic.xaml 中的默认内容,并粘贴下面的标记。

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

在这种情况下,默认样式集的唯一属性是控件模板。 该模板由一个正方形元素(其背景绑定到 XAML Control 类型的所有实例都具有的 Background 属性)和一个文本元素(其文本绑定到 BgLabelControl::Label 依赖属性)组成。

BgLabelControl 的实例添加到主 UI 页面

打开 MainPage.xaml,其中包含主 UI 页面的 XAML 标记。 紧接在 Button 元素(StackPanel 内)之后,添加以下标记 。

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

此外,添加以下 include 指令 MainPage.h ,以便 MainPage 类型(编译 XAML 标记和命令性代码的组合)知道 BgLabelControl 自定义控件类型。 如果要从另一个 XAML 页面使用 BgLabelControl,则也会将此相同的 include 指令添加到该页的头文件中。 或者,也可以将单个 include 指令放在预编译头文件中。

// MainPage.h
...
#include "BgLabelControl.h"
...

现在构建并运行项目。 你将看到默认控件模板在标记中绑定到 BgLabelControl 实例的背景画笔和标签上。

本演练演示了 C++/WinRT 中自定义(模板化)控件的简单示例。 你可以创建自己的自定义控件,并使其内容丰富、功能齐全。 例如,自定义控件可以采用可编辑数据网格、视频播放器或三维几何图形可视化工具等复杂内容的形式。

实现 可重写 方法,例如 MeasureOverrideOnApplyTemplate

请参阅 使用 C++/WinRT 调用和重写基类型中的相关章节。

重要 API