使用 C++/WinRT 生成 XAML 控件

本文介绍如何使用 C++/WinRT 为 WinUI 3 创建模板化 XAML 控件。 模板化控件继承自 Microsoft.UI.Xaml.Controls.Control ,具有可以使用 XAML 控件模板自定义的视觉结构和视觉行为。 本文介绍与使用 C++/WinRT 但已改编为使用 WinUI 3 的文章 XAML 自定义(模板化)控件相同的方案。

先决条件

  1. 开始开发 Windows 应用
  2. 下载并安装最新版本的 C++/WinRT Visual Studio 扩展 (VSIX)

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

首先,在 Microsoft Visual Studio 中创建新项目。 在 Create a new project 对话框中,选择 空白应用(UWP 中的 WinUI) 项目模板,确保选择C++语言版本。 将项目名称设置为“BgLabelControlApp”,使文件名与以下示例中的代码保持一致。 将 目标版本 设置为 Windows 10 版本 1903(内部版本 18362), 并将最低版本 设置为 Windows 10 版本 1803(内部版本 17134)。 本演练也适用于使用 空白应用、打包(桌面中的 WinUI) 项目模板创建的桌面应用,只需确保执行 BgLabelControlApp (Desktop) 项目中的所有步骤。

空白应用项目模板

向应用添加模板化控件

若要添加模板化控件,请单击工具栏中的 “项目” 菜单,或在 解决方案资源管理器 中右键单击项目,然后选择“ 添加新项 ”。 在 Visual C++->WinUI 下,选择 自定义控件(WinUI) 模板。 将新控件命名为“BgLabelControl”,然后单击 添加。 这将向项目添加三个新文件。 BgLabelControl.h 是包含控件声明的标头,包含 BgLabelControl.cpp 控件的 C++/WinRT 实现。 BgLabelControl.idl 是允许控件实例化为运行时类的接口定义文件。

实现 BgLabelControl 自定义控件类

在以下步骤中,您将在项目目录中的BgLabelControl.idlBgLabelControl.hBgLabelControl.cpp文件中更新代码,以实现运行时类。

模板化控件类将从 XAML 标记实例化,因此它将是运行时类。 生成完成的项目时,MIDL 编译器(midl.exe)将使用 BgLabelControl.idl 该文件为控件生成 Windows 运行时元数据文件(.winmd),组件使用者将引用该文件。 有关创建运行时类的详细信息,请参阅 使用 C++/WinRT 开发 API

我们创建的模板化控件将公开一个属性,该属性是一个字符串,该字符串将用作控件的标签。 将 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),请参阅 自定义附加属性中的代码示例。

请注意,上面代码中引用的 XAML 类位于 Microsoft.UI.Xaml 命名空间中。 这就是将它们区分为 WinUI 控件而不是在 Windows.UI.XAML 命名空间中定义的 UWP XAML 控件。

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

上面显示的代码实现 LabelLabelProperty 属性,添加一个名为 OnLabelChanged 的静态事件处理程序,以处理对依赖属性值所做的更改,并添加一个私有成员来存储 LabelProperty 的后备字段。 同样,请注意,头文件中引用的 XAML 类位于属于 WinUI 3 框架的 Microsoft.UI.Xaml 命名空间中,而不是 UWP UI 框架使用的 Windows.UI.Xaml 命名空间。

接下来,将BgLabelControl.cpp的内容替换为以下代码。

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

本演练不使用 OnLabelChanged 回调,但已提供该回调,以便了解如何使用属性更改的回调注册依赖属性。 OnLabelChanged 的实现还展示了如何从基础投影类型获取派生的投影类型(在本例中,基础投影类型是 DependencyObject)。 并演示如何获取指向实现投影类型的类型的指针。 第二个操作自然只能在实现投影类型的项目中(即实现运行时类的项目)中实现。

xaml_typename函数由默认情况下未包含在 WinUI 3 项目模板中的 Windows.UI.Xaml.Interop 命名空间提供。 将一行添加到项目的预编译头文件(pch.h)以包含与此命名空间关联的头文件。

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

定义 BgLabelControl 的默认样式

在其构造函数中,BgLabelControl 为自己设置默认样式键。 模板化控件需要具有一个默认样式,其中包含默认的控件模板,以便在控件使用者未设置样式和/或模板时用于自身的呈现。 在本部分中,我们将向包含默认样式的项目添加标记文件。

确保 显示所有文件 仍保持打开状态(在 解决方案资源管理器中)。 在项目节点下,创建新文件夹(不是筛选器,而是文件夹),并将其命名为“主题”。 在 Themes下,添加一个新项,类型为 Visual C++ > WinUI > 资源字典(WinUI),并将其命名为“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 页面

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

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

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

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

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

模板化控制结果

实现可替代函数,例如 MeasureOverride 和 onApplyTemplate

Control 运行时类派生模板化控件,该控件本身进一步派生自基运行时类。 ControlFrameworkElement以及可在派生类中重写的 UIElement,有可替代的方法。 下面是演示如何执行此操作的代码示例。

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

可重写 函数在不同语言的投影中以不同方式呈现。 例如,在 C# 中,可重写函数通常显示为受保护的虚拟函数。 在 C++/WinRT 中,它们既不是虚拟的,也不是受保护的,但你仍然可以替代它们并提供自己的实现,如上所示。

在不使用模板的情况下生成控件源文件。

本部分介绍如何在不使用 自定义控件 项模板的情况下生成用于创建自定义控件所需的源文件。

首先,向项目添加新的 Midl 文件 (.idl) 项。 在 “项目 ”菜单中,选择“ 添加新项...” 并在搜索框中键入“MIDL”以查找 .idl 文件项。 为新文件 BgLabelControl.idl 命名,以便名称与本文中的步骤一致。 删除 BgLabelControl.idl 的默认内容,并粘贴上述步骤中显示的运行时类声明。

保存新的 .idl 文件后,下一步是为用于实现模板化控件的 .cpp 和 .h 实现文件生成 Windows 运行时元数据文件(.winmd)和存根。 通过生成解决方案来生成这些文件,这将导致 MIDL 编译器(midl.exe)编译创建的 .idl 文件。 请注意,解决方案不会成功生成,Visual Studio 将在输出窗口中显示生成错误,但将生成必要的文件。

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

编译器在 BgLabelControl.h 和 BgLabelControl.cpp 的顶部放置 static_assert 行,以防止编译生成的文件。 实现控件时,应从项目目录中放置的文件中删除这些行。 对于本次演练,你可以用上述提供的代码覆盖文件的整个内容。

另请参阅