在 C++ 桌面 (Win32) 应用中托管标准 WinRT XAML 控件

本文演示了如何使用 WinRT XAML 托管 API 在新的 C++ 桌面应用中托管标准 WinRT XAML 控件(即 Windows SDK 提供的控件)。 此代码基于简单的 XAML 岛示例,本节讨论了代码的一些最重要的部分。 如果你有现成的 C++ 桌面应用项目,可以使用以下步骤和代码示例处理它。

注意

本文中演示的方案不支持直接编辑应用中托管的 WinRT XAML 控件的 XAML 标记。 此方案限制你通过代码修改所托管控件的外观和行为。 有关使你可以在托管 WinRT XAML 控件时直接编辑 XAML 标记的说明,请参阅在 C++ 桌面应用中托管自定义 WinRT XAML 控件

创建桌面应用程序项目

  1. 在安装了 Windows 10 版本 1903 SDK(版本 10.0.18362)或更高版本的 Visual Studio 2019 中,创建一个新“Windows 桌面应用程序”项目,并将其命名为“MyDesktopWin32App” 。 此项目类型在“C++” 、“Windows” 和“桌面” 项目筛选器下提供。

  2. 在“解决方案资源管理器” 中,右键单击解决方案节点,单击“重定向解决方案” ,选择“10.0.18362.0” 或更高版本的 SDK,然后单击“确定” 。

  3. 在项目中安装 Microsoft.Windows.CppWinRT NuGet 包,以启用对 C++/WinRT 的支持:

    1. 在“解决方案资源管理器” 中,右键单击你的项目并选择“管理 NuGet 包” 。
    2. 选择“浏览” 选项卡,搜索 Microsoft.Windows.CppWinRT 包,并安装此包的最新版本。

    注意

    对于新项目,你可以选择安装 C++/WinRT Visual Studio 扩展 (VSIX) 并使用该扩展中包含的 C++/WinRT 项目模板之一。 有关详细信息,请参阅 C++/WinRT 和 VSIX 的 Visual Studio 支持

  4. 在“NuGet 包管理器”窗口的“浏览”选项卡上,搜索 Microsoft.Toolkit.Win32.UI.SDK NuGet 包,并安装此包的最新稳定版 。 此包提供了多个版本和运行时资产,可使 XAML 岛在你的应用中正常工作。

  5. 应用程序清单中设置 maxversiontested 值,以指定应用程序与 Windows 10 版本 1903 兼容。

    1. 如果项目中还没有应用程序清单,请将新的 XML 文件添加到项目中,并将其命名为“app.config” 。

    2. 在应用程序清单中,包括下面的示例中所示的“compatibility” 元素和子元素。 将 maxversiontested 元素的 Id 属性替换为要面向的 Windows 的版本号(此值必须为 10.0.18362.0 或更高版本)。 请注意,设置较高的值意味着旧版本的 Windows 无法正常运行应用,因为每个 Windows 版本都只知道它之前的版本。 如果希望应用在 Windows 10 版本 1903(版本 10.0.18362)上运行,应按原样保留 10.0.18362.0 值,或为应用支持的不同值添加多个 maxversiontested 元素。

      <?xml version="1.0" encoding="UTF-8"?>
      <assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
          <compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
              <application>
                  <!-- Windows 10 -->
                  <maxversiontested Id="10.0.18362.0"/>
                  <supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}" />
              </application>
          </compatibility>
      </assembly>
      
  6. 添加对 Windows 运行时元数据的引用:

    1. 在解决方案资源管理器中,右键单击项目的“引用”节点并选择“添加引用” 。
    2. 单击页面底部的“浏览”按钮,然后导航到 SDK 安装路径中的 UnionMetadata 文件夹。 默认情况下,SDK 将安装到 C:\Program Files (x86)\Windows Kits\10\UnionMetadata
    3. 然后,选择以你面向的 Windows 版本(例如 10.0.18362.0)命名的文件夹,在该文件夹中选择 Windows.winmd 文件。
    4. 单击“确定”,关闭“添加引用”对话框 。

使用 XAML 托管 API 托管 WinRT XAML 控件

使用 XAML 托管 API 托管 WinRT XAML 控件的基本过程遵循以下常规步骤:

  1. 在你的应用创建它将托管的任何 Windows.UI.Xaml.UIElement 对象之前,为当前线程初始化 WinRT XAML 框架。 有多种方法可以执行此操作,具体取决于你计划创建的将托管控件的 DesktopWindowXamlSource 对象。

    • 如果你的应用程序在创建将托管的任何“Windows.UI.Xaml.UIElement” 对象之前创建“DesktopWindowXamlSource” 对象,则在实例化“DesktopWindowXamlSource” 对象时,会为你初始化此框架。 在这种情况下,你无需添加自己的任何代码来初始化该框架。

    • 但是,如果你的应用程序先创建 Windows.UI.Xaml.UIElement 对象,然后再创建将托管这些对象的 DesktopWindowXamlSource 对象,你的应用程序则必须调用静态 WindowsXamlManager.InitializeForCurrentThread 方法来显式初始化 WinRT XAML 框架,这样 Windows.UI.Xaml.UIElement 对象随后才能被实例化 。 在实例化托管“DesktopWindowXamlSource” 的父 UI 元素时,应用程序通常应调用此方法。

    注意

    此方法返回 WindowsXamlManager 对象,该对象包含对 WinRT XAML 框架的引用。 可以根据需要在给定线程上创建任意数量的“WindowsXamlManager” 对象。 但是,因为每个对象都包含对 WinRT XAML 框架的引用,所以你应该释放这些对象以确保最终释放 XAML 资源。

  2. 创建 DesktopWindowXamlSource 对象并将其附加到应用程序中与窗口句柄关联的父 UI 元素。

    为此,你需要执行以下步骤:

    1. 创建一个“DesktopWindowXamlSource” 对象,并将其转换为“IDesktopWindowXamlSourceNative” 或“IDesktopWindowXamlSourceNative2” COM 接口。

    2. 调用 IDesktopWindowXamlSourceNative 或 IDesktopWindowXamlSourceNative2 接口的 AttachToWindow 方法,并传入应用程序中父 UI 元素的窗口句柄。

      重要

      确保你的代码仅针对每个 DesktopWindowXamlSource 对象调用一次 AttachToWindow 方法。 对一个 DesktopWindowXamlSource 对象多次调用此方法可能会导致内存泄漏。

    3. 设置“DesktopWindowXamlSource” 中包含的内部子窗口的初始大小。 默认情况下,此内部子窗口的宽度和高度均设置为 0。 如果未设置窗口大小,则添加到 DesktopWindowXamlSource 的任何 WinRT XAML 控件都将不可见。 若要访问“DesktopWindowXamlSource” 中的内部子窗口,请使用“IDesktopWindowXamlSourceNative” 或“IDesktopWindowXamlSourceNative2” 接口的“WindowHandle” 属性。

  3. 最后,分配要托管到“DesktopWindowXamlSource” 对象的Content属性的“Windows.UI.Xaml.UIElement” 。

以下步骤和代码示例演示如何实现上述过程:

  1. 在项目“源文件” 文件夹中,打开默认的“MyDesktopWin32App.com” 文件。 删除文件的全部内容并添加以下 includeusing 语句。 除了标准 C++ 和 UWP 标头和命名空间,这些语句还包括特定于 XAML 岛的几个项。

    #include <windows.h>
    #include <stdlib.h>
    #include <string.h>
    
    #include <winrt/Windows.Foundation.Collections.h>
    #include <winrt/Windows.system.h>
    #include <winrt/windows.ui.xaml.hosting.h>
    #include <windows.ui.xaml.hosting.desktopwindowxamlsource.h>
    #include <winrt/windows.ui.xaml.controls.h>
    #include <winrt/Windows.ui.xaml.media.h>
    
    using namespace winrt;
    using namespace Windows::UI;
    using namespace Windows::UI::Composition;
    using namespace Windows::UI::Xaml::Hosting;
    using namespace Windows::Foundation::Numerics;
    
  2. 在上一部分之后复制以下代码。 此代码为应用定义 WinMain 函数。 此函数初始化一个基本窗口,并使用 XAML 托管 API 在窗口中托管简单的 UWP TextBlock 控件。

    LRESULT CALLBACK WindowProc(HWND, UINT, WPARAM, LPARAM);
    
    HWND _hWnd;
    HWND _childhWnd;
    HINSTANCE _hInstance;
    
    int CALLBACK WinMain(_In_ HINSTANCE hInstance, _In_opt_ HINSTANCE hPrevInstance, _In_ LPSTR lpCmdLine, _In_ int nCmdShow)
    {
        _hInstance = hInstance;
    
        // The main window class name.
        const wchar_t szWindowClass[] = L"Win32DesktopApp";
        WNDCLASSEX windowClass = { };
    
        windowClass.cbSize = sizeof(WNDCLASSEX);
        windowClass.lpfnWndProc = WindowProc;
        windowClass.hInstance = hInstance;
        windowClass.lpszClassName = szWindowClass;
        windowClass.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1);
    
        windowClass.hIconSm = LoadIcon(windowClass.hInstance, IDI_APPLICATION);
    
        if (RegisterClassEx(&windowClass) == NULL)
        {
            MessageBox(NULL, L"Windows registration failed!", L"Error", NULL);
            return 0;
        }
    
        _hWnd = CreateWindow(
            szWindowClass,
            L"Windows c++ Win32 Desktop App",
            WS_OVERLAPPEDWINDOW | WS_VISIBLE,
            CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT,
            NULL,
            NULL,
            hInstance,
            NULL
        );
        if (_hWnd == NULL)
        {
            MessageBox(NULL, L"Call to CreateWindow failed!", L"Error", NULL);
            return 0;
        }
    
    
        // Begin XAML Island section.
    
        // The call to winrt::init_apartment initializes COM; by default, in a multithreaded apartment.
        winrt::init_apartment(apartment_type::single_threaded);
    
        // Initialize the XAML framework's core window for the current thread.
        WindowsXamlManager winxamlmanager = WindowsXamlManager::InitializeForCurrentThread();
    
        // This DesktopWindowXamlSource is the object that enables a non-UWP desktop application 
        // to host WinRT XAML controls in any UI element that is associated with a window handle (HWND).
        DesktopWindowXamlSource desktopSource;
    
        // Get handle to the core window.
        auto interop = desktopSource.as<IDesktopWindowXamlSourceNative>();
    
        // Parent the DesktopWindowXamlSource object to the current window.
        check_hresult(interop->AttachToWindow(_hWnd));
    
        // This HWND will be the window handler for the XAML Island: A child window that contains XAML.  
        HWND hWndXamlIsland = nullptr;
    
        // Get the new child window's HWND. 
        interop->get_WindowHandle(&hWndXamlIsland);
    
        // Update the XAML Island window size because initially it is 0,0.
        SetWindowPos(hWndXamlIsland, 0, 200, 100, 800, 200, SWP_SHOWWINDOW);
    
        // Create the XAML content.
        Windows::UI::Xaml::Controls::StackPanel xamlContainer;
        xamlContainer.Background(Windows::UI::Xaml::Media::SolidColorBrush{ Windows::UI::Colors::LightGray() });
    
        Windows::UI::Xaml::Controls::TextBlock tb;
        tb.Text(L"Hello World from Xaml Islands!");
        tb.VerticalAlignment(Windows::UI::Xaml::VerticalAlignment::Center);
        tb.HorizontalAlignment(Windows::UI::Xaml::HorizontalAlignment::Center);
        tb.FontSize(48);
    
        xamlContainer.Children().Append(tb);
        xamlContainer.UpdateLayout();
        desktopSource.Content(xamlContainer);
    
        // End XAML Island section.
    
        ShowWindow(_hWnd, nCmdShow);
        UpdateWindow(_hWnd);
    
        //Message loop:
        MSG msg = { };
        while (GetMessage(&msg, NULL, 0, 0))
        {
            TranslateMessage(&msg);
            DispatchMessage(&msg);
        }
    
        return 0;
    }
    
  3. 在上一部分之后复制以下代码。 此代码定义窗口的窗口过程

    LRESULT CALLBACK WindowProc(HWND hWnd, UINT messageCode, WPARAM wParam, LPARAM lParam)
    {
        PAINTSTRUCT ps;
        HDC hdc;
        wchar_t greeting[] = L"Hello World in Win32!";
        RECT rcClient;
    
        switch (messageCode)
        {
            case WM_PAINT:
                if (hWnd == _hWnd)
                {
                    hdc = BeginPaint(hWnd, &ps);
                    TextOut(hdc, 300, 5, greeting, wcslen(greeting));
                    EndPaint(hWnd, &ps);
                }
                break;
            case WM_DESTROY:
                PostQuitMessage(0);
                break;
    
            // Create main window
            case WM_CREATE:
                _childhWnd = CreateWindowEx(0, L"ChildWClass", NULL, WS_CHILD | WS_BORDER, 0, 0, 0, 0, hWnd, NULL, _hInstance, NULL);
                return 0;
    
            // Main window changed size
            case WM_SIZE:
                // Get the dimensions of the main window's client
                // area, and enumerate the child windows. Pass the
                // dimensions to the child windows during enumeration.
                GetClientRect(hWnd, &rcClient);
                MoveWindow(_childhWnd, 200, 200, 400, 500, TRUE);
                ShowWindow(_childhWnd, SW_SHOW);
    
                return 0;
    
                // Process other messages.
    
            default:
                return DefWindowProc(hWnd, messageCode, wParam, lParam);
                break;
        }
    
        return 0;
    }
    
  4. 保存代码文件,然后生成并运行应用。 确认在应用窗口中看到 UWP TextBlock 控件。

    注意

    你可能会看到几个生成警告,包括 warning C4002: too many arguments for function-like macro invocation 'GetCurrentTime'manifest authoring warning 81010002: Unrecognized Element "maxversiontested" in namespace "urn:schemas-microsoft-com:compatibility.v1"。 这些警告是当前工具和 NuGet 包的已知问题,可将其忽略。

有关演示使用 XAML 托管 API 托管 WinRT XAML 控件的完整示例,请参阅以下代码文件:

打包应用

可以选择在 MSIX 包中打包应用以供部署。 MSIX 是适用于 Windows 的新式应用打包技术,并且基于 MSI、.appx、App-V 和 ClickOnce 安装技术的组合。

下面的说明介绍了如何在 Visual Studio 2019 中使用 Windows 应用程序打包项目将解决方案中的所有组件打包到 MSIX 包。 只有在需要将应用打包到 MSIX 包时,才需要使用这些步骤。

注意

如果选择不在 MSIX 包中打包应用程序以供部署,则运行应用的计算机必须安装有 Visual C++ 运行时

  1. 向解决方案中添加一个新的 Windows 应用程序打包项目。 创建项目时,针对“目标版本”和“最低版本”选择“Windows 10 版本 1903 (10.0;版本 18362)” 。

  2. 在打包项目中,右键单击“应用程序”节点,然后选择“添加引用” 。 在项目列表中,选择解决方案中的 C++ 桌面应用程序项目,然后单击“确认”。

  3. 生成并运行打包项目。 确认应用运行并按预期显示 WinRT XAML 控件。

后续步骤

本文中的代码示例可帮助你熟悉在 C++ 桌面应用中托管标准 WinRT XAML 控件的基本方案。 以下部分介绍应用程序可能需要支持的其他方案。

托管自定义 WinRT XAML 控件

在许多场景下,你可能需要托管一个自定义 WinRT XAML 控件,其中包含多个协同工作的单独控件。 在 C++ 桌面应用中托管自定义控件(你自己定义的控件或第三方提供的控件)的过程比托管标准控件更复杂,需要其他代码。

有关完整的演练,请参阅使用 XAML 托管 API 在 C++ 桌面应用中托管自定义 WinRT XAML 控件

高级方案

许多托管 XAML 岛的桌面应用程序需要处理其他方案以提供流畅的用户体验。 例如,桌面应用程序可能需要处理 XAML 岛中的键盘输入、XAML 岛与其他 UI 元素之间的焦点导航以及布局更改。

有关处理这些场景的详细信息和指向相关代码示例的指针,请参阅 C++ 桌面应用中 XAML Island 的高级应用场景