将视觉层与 Win32 结合使用

可以在 Win32 应用中使用 Windows 运行时 Composition API(也称为视觉层)来创建面向 Windows 用户的现代特色体验。

GitHub 上提供了本教程的完整代码:Win32 HelloComposition 示例

需要精确控制其 UI 合成的通用 Windows 应用程序可以访问 Windows.UI.Composition 命名空间,以便对其 UI 的合成和呈现方式进行精细控制。 但是,此 Composition API 并不局限于 UWP 应用。 Win32 桌面应用程序可以利用 UWP 和 Windows 中的现代合成系统。

必备条件

UWP 宿主 API 的先决条件如下。

如何从 Win32 桌面应用程序使用 Composition API

在本教程中,你将创建一个简单的 Win32 C++ 应用,并在其中添加 UWP 合成元素。 重点在于正确配置项目、创建互操作代码,并使用 Windows Composition API 绘制一些简单内容。 完成的应用如下所示。

正在运行的应用 UI

在 Visual Studio 中创建 C++ Win32 项目

第一步是在 Visual Studio 中创建 Win32 应用项目。

若要在 C++ 中创建名为 HelloComposition 的新 Win32 应用程序项目:

  1. 打开 Visual Studio 并选择“文件”>“新建”>“项目”。

    此时会打开“新建项目”对话框。

  2. 在“安装”类别下展开“Visual C++”节点,然后选择“Windows 桌面”。

  3. 选择“Windows 桌面应用程序”模板。

  4. 输入名称 HelloComposition,然后单击“确定”。

    Visual Studio 将创建该项目,并打开主应用文件的编辑器。

将项目配置为使用 Windows 运行时 API

为了在 Win32 应用中使用 Windows 运行时 (WinRT) API,我们将使用 C++/WinRT。 需要配置 Visual Studio 项目才能添加 C++/WinRT 支持。

(有关详细信息,请参阅 C++/WinRT 入门 - 修改 Windows 桌面应用程序项目以添加 C++/WinRT 支持)。

  1. 在“项目”菜单中,打开项目属性(HelloComposition 属性),并确保对以下设置使用指定值:

    • 对于“配置”,请选择“所有配置”。 对于“平台”,请选择“所有平台”。
    • “配置属性”>“常规”>“Windows SDK 版本” = “10.0.17763.0 或更高版本”

    设置 SDK 版本

    • “C/C++”>“语言”>“C++ 语言标准” = “ISO C++ 17 标准(/stf:c++17)”

    设置语言标准

    • “链接器”>“输入”>“其他依赖项”必须包括“windowsapp.lib”。 如果列表中不包括此项,请添加它。

    添加链接器依赖项

  2. 更新预编译的标头

    • stdafx.hstdafx.cpp 分别重命名为 pch.hpch.cpp

    • 将项目属性“C/C++”>“预编译的标头”>“预编译的标头文件”设置为 pch.h。

    • 在所有文件中找到 #include "stdafx.h" 并将其替换为 #include "pch.h"

      (“编辑”>“查找和替换”>“在文件中查找”)

      查找并替换 stdafx.h

    • pch.h 中包含 winrt/base.hunknwn.h

      // reference additional headers your program requires here
      #include <unknwn.h>
      #include <winrt/base.h>
      

此时最好是生成项目,以确保在继续操作之前不会出错。

创建一个类用于承载合成元素

若要使用视觉层承载创建的内容,请创建一个类 (CompositionHost) 用于管理互操作并创建合成元素。 将在此类中完成用于承载 Composition API 的大部分配置,包括:

我们将此类设为单一实例,以免出现线程问题。 例如,只能为每个线程创建一个调度程序队列,因此,在同一线程中实例化 CompositionHost 的另一个实例会导致出错。

提示

如果需要,请检查本教程末尾的完整代码,确保在完成本教程的过程中,所有代码位于正确的位置。

  1. 向你的项目中添加一个新类文件。

    • 在“解决方案资源管理器”中右键单击“HelloComposition”项目。
    • 在上下文菜单中,选择“添加”>“类...”。
    • 在“添加类”对话框中,将类命名为 CompositionHost.cs,然后单击“添加”。
  2. 包含合成互操作所需的标头和 using 语句。

    • 在 CompositionHost.h 文件的顶部,添加这些 include 语句。
    #pragma once
    #include <winrt/Windows.UI.Composition.Desktop.h>
    #include <windows.ui.composition.interop.h>
    #include <DispatcherQueue.h>
    
    • 在 CompositionHost.cpp 文件顶部的任何 include 语句后面添加这些 using 语句。
    using namespace winrt;
    using namespace Windows::System;
    using namespace Windows::UI;
    using namespace Windows::UI::Composition;
    using namespace Windows::UI::Composition::Desktop;
    using namespace Windows::Foundation::Numerics;
    
  3. 编辑该类以使用单一实例模式。

    • 在 CompositionHost.h 中,将构造函数设为专用构造函数。
    • 声明公共静态 GetInstance 方法。
    class CompositionHost
    {
    public:
        ~CompositionHost();
        static CompositionHost* GetInstance();
    
    private:
        CompositionHost();
    };
    
    • 在 CompositionHost.cpp 中,添加 GetInstance 方法的定义。
    CompositionHost* CompositionHost::GetInstance()
    {
        static CompositionHost instance;
        return &instance;
    }
    
  4. 在 CompositionHost.h 中,声明 Compositor、DispatcherQueueController 和 DesktopWindowTarget 的专用成员变量。

    winrt::Windows::UI::Composition::Compositor m_compositor{ nullptr };
    winrt::Windows::System::DispatcherQueueController m_dispatcherQueueController{ nullptr };
    winrt::Windows::UI::Composition::Desktop::DesktopWindowTarget m_target{ nullptr };
    
  5. 添加一个公共方法用于初始化合成互操作对象。

    注意

    在 Initialize 中,调用 EnsureDispatcherQueue、CreateDesktopWindowTarget 和 CreateCompositionRoot 方法。 将在后续步骤中创建这些方法。

    • 在 CompositionHost.h 中,声明采用 HWND 作为参数的、名为 Initialize 的公共方法。
    void Initialize(HWND hwnd);
    
    • 在 CompositionHost.cpp 中,添加 Initialize 方法的定义。
    void CompositionHost::Initialize(HWND hwnd)
    {
        EnsureDispatcherQueue();
        if (m_dispatcherQueueController) m_compositor = Compositor();
    
        CreateDesktopWindowTarget(hwnd);
        CreateCompositionRoot();
    }
    
  6. 在要使用 Windows Composition 的线程中创建调度程序队列。

    必须在包含调度程序队列的线程中创建 Compositor,因此在初始化期间首先会调用此方法。

    • 在 CompositionHost.h 中,声明名为 EnsureDispatcherQueue 的专用方法。
    void EnsureDispatcherQueue();
    
    • 在 CompositionHost.cpp 中,添加 EnsureDispatcherQueue 方法的定义。
    void CompositionHost::EnsureDispatcherQueue()
    {
        namespace abi = ABI::Windows::System;
    
        if (m_dispatcherQueueController == nullptr)
        {
            DispatcherQueueOptions options
            {
                sizeof(DispatcherQueueOptions), /* dwSize */
                DQTYPE_THREAD_CURRENT,          /* threadType */
                DQTAT_COM_ASTA                  /* apartmentType */
            };
    
            Windows::System::DispatcherQueueController controller{ nullptr };
            check_hresult(CreateDispatcherQueueController(options, reinterpret_cast<abi::IDispatcherQueueController**>(put_abi(controller))));
            m_dispatcherQueueController = controller;
        }
    }
    
  7. 将应用的窗口注册为合成目标。

    • 在 CompositionHost.h 中,声明采用 HWND 作为参数的、名为 CreateDesktopWindowTarget 的专用方法。
    void CreateDesktopWindowTarget(HWND window);
    
    • 在 CompositionHost.cpp 中,添加 CreateDesktopWindowTarget 方法的定义。
    void CompositionHost::CreateDesktopWindowTarget(HWND window)
    {
        namespace abi = ABI::Windows::UI::Composition::Desktop;
    
        auto interop = m_compositor.as<abi::ICompositorDesktopInterop>();
        DesktopWindowTarget target{ nullptr };
        check_hresult(interop->CreateDesktopWindowTarget(window, false, reinterpret_cast<abi::IDesktopWindowTarget**>(put_abi(target))));
        m_target = target;
    }
    
  8. 创建一个根视觉容器用于保存视觉对象。

    • 在 CompositionHost.h 中,声明名为 CreateCompositionRoot 的专用方法。
    void CreateCompositionRoot();
    
    • 在 CompositionHost.cpp 中,添加 CreateCompositionRoot 方法的定义。
    void CompositionHost::CreateCompositionRoot()
    {
        auto root = m_compositor.CreateContainerVisual();
        root.RelativeSizeAdjustment({ 1.0f, 1.0f });
        root.Offset({ 124, 12, 0 });
        m_target.Root(root);
    }
    

立即生成项目以确保不会出错。

这些方法将设置 UWP 视觉层与 Win32 API 之间的互操作所需的组件。 现在,可将内容添加到应用。

添加合成元素

准备好基础结构后,接下来可以生成要显示的合成内容。

对于本示例,你将添加用于创建随机色彩的正方形 SpriteVisual 的代码,经过短暂的延迟后,该正方形将通过动画效果落下。

  1. 添加合成元素。

    • 在 CompositionHost.h 中,声明采用 3 个浮点值作为参数的、名为 AddElement 的公共方法。
    void AddElement(float size, float x, float y);
    
    • 在 CompositionHost.cpp 中,添加 AddElement 方法的定义。
    void CompositionHost::AddElement(float size, float x, float y)
    {
        if (m_target.Root())
        {
            auto visuals = m_target.Root().as<ContainerVisual>().Children();
            auto visual = m_compositor.CreateSpriteVisual();
    
            auto element = m_compositor.CreateSpriteVisual();
            uint8_t r = (double)(double)(rand() % 255);;
            uint8_t g = (double)(double)(rand() % 255);;
            uint8_t b = (double)(double)(rand() % 255);;
    
            element.Brush(m_compositor.CreateColorBrush({ 255, r, g, b }));
            element.Size({ size, size });
            element.Offset({ x, y, 0.0f, });
    
            auto animation = m_compositor.CreateVector3KeyFrameAnimation();
            auto bottom = (float)600 - element.Size().y;
            animation.InsertKeyFrame(1, { element.Offset().x, bottom, 0 });
    
            using timeSpan = std::chrono::duration<int, std::ratio<1, 1>>;
    
            std::chrono::seconds duration(2);
            std::chrono::seconds delay(3);
    
            animation.Duration(timeSpan(duration));
            animation.DelayTime(timeSpan(delay));
            element.StartAnimation(L"Offset", animation);
            visuals.InsertAtTop(element);
    
            visuals.InsertAtTop(visual);
        }
    }
    

创建并显示窗口

现在,可以在 Win32 UI 中添加一个按钮和 UWP 合成内容。

  1. 在 HelloComposition.cpp 文件的顶部包含 CompositionHost.h,定义 BTN_ADD,并获取 CompositionHost 的实例。

    #include "CompositionHost.h"
    
    // #define MAX_LOADSTRING 100 // This is already in the file.
    #define BTN_ADD 1000
    
    CompositionHost* compHost = CompositionHost::GetInstance();
    
  2. InitInstance 方法中,更改创建的窗口的大小。 (在此行中,将 CW_USEDEFAULT, 0 更改为 900, 672。)

    HWND hWnd = CreateWindowW(szWindowClass, szTitle, WS_OVERLAPPEDWINDOW,
        CW_USEDEFAULT, 0, 900, 672, nullptr, nullptr, hInstance, nullptr);
    
  3. 在 WndProc 函数中,将 case WM_CREATE 添加到 message 开关块。 在本例中,你将初始化 CompositionHost 并创建按钮。

    case WM_CREATE:
    {
        compHost->Initialize(hWnd);
        srand(time(nullptr));
    
        CreateWindow(TEXT("button"), TEXT("Add element"),
            WS_VISIBLE | WS_CHILD | BS_PUSHBUTTON,
            12, 12, 100, 50,
            hWnd, (HMENU)BTN_ADD, nullptr, nullptr);
    }
    break;
    
  4. 另外,在 WndProc 函数中,处理按钮单击以将合成元素添加到 UI。

    case BTN_ADD 添加到 WM_COMMAND 块中的 wmId 开关块。

    case BTN_ADD: // addButton click
    {
        double size = (double)(rand() % 150 + 50);
        double x = (double)(rand() % 600);
        double y = (double)(rand() % 200);
        compHost->AddElement(size, x, y);
        break;
    }
    

现在,可以生成并运行你的应用。 如果需要,请检查本教程末尾的完整代码,确保所有代码位于正确的位置。

运行应用并单击按钮时,应会看到已添加到 UI 中的动画正方形。

其他资源

完成代码

下面是 CompositionHost 类和 InitInstance 方法的完整代码。

CompositionHost.h

#pragma once
#include <winrt/Windows.UI.Composition.Desktop.h>
#include <windows.ui.composition.interop.h>
#include <DispatcherQueue.h>

class CompositionHost
{
public:
    ~CompositionHost();
    static CompositionHost* GetInstance();

    void Initialize(HWND hwnd);
    void AddElement(float size, float x, float y);

private:
    CompositionHost();

    void CreateDesktopWindowTarget(HWND window);
    void EnsureDispatcherQueue();
    void CreateCompositionRoot();

    winrt::Windows::UI::Composition::Compositor m_compositor{ nullptr };
    winrt::Windows::UI::Composition::Desktop::DesktopWindowTarget m_target{ nullptr };
    winrt::Windows::System::DispatcherQueueController m_dispatcherQueueController{ nullptr };
};

CompositionHost.cpp

#include "pch.h"
#include "CompositionHost.h"

using namespace winrt;
using namespace Windows::System;
using namespace Windows::UI;
using namespace Windows::UI::Composition;
using namespace Windows::UI::Composition::Desktop;
using namespace Windows::Foundation::Numerics;

CompositionHost::CompositionHost()
{
}

CompositionHost* CompositionHost::GetInstance()
{
    static CompositionHost instance;
    return &instance;
}

CompositionHost::~CompositionHost()
{
}

void CompositionHost::Initialize(HWND hwnd)
{
    EnsureDispatcherQueue();
    if (m_dispatcherQueueController) m_compositor = Compositor();

    if (m_compositor)
    {
        CreateDesktopWindowTarget(hwnd);
        CreateCompositionRoot();
    }
}

void CompositionHost::EnsureDispatcherQueue()
{
    namespace abi = ABI::Windows::System;

    if (m_dispatcherQueueController == nullptr)
    {
        DispatcherQueueOptions options
        {
            sizeof(DispatcherQueueOptions), /* dwSize */
            DQTYPE_THREAD_CURRENT,          /* threadType */
            DQTAT_COM_ASTA                  /* apartmentType */
        };

        Windows::System::DispatcherQueueController controller{ nullptr };
        check_hresult(CreateDispatcherQueueController(options, reinterpret_cast<abi::IDispatcherQueueController**>(put_abi(controller))));
        m_dispatcherQueueController = controller;
    }
}

void CompositionHost::CreateDesktopWindowTarget(HWND window)
{
    namespace abi = ABI::Windows::UI::Composition::Desktop;

    auto interop = m_compositor.as<abi::ICompositorDesktopInterop>();
    DesktopWindowTarget target{ nullptr };
    check_hresult(interop->CreateDesktopWindowTarget(window, false, reinterpret_cast<abi::IDesktopWindowTarget**>(put_abi(target))));
    m_target = target;
}

void CompositionHost::CreateCompositionRoot()
{
    auto root = m_compositor.CreateContainerVisual();
    root.RelativeSizeAdjustment({ 1.0f, 1.0f });
    root.Offset({ 124, 12, 0 });
    m_target.Root(root);
}

void CompositionHost::AddElement(float size, float x, float y)
{
    if (m_target.Root())
    {
        auto visuals = m_target.Root().as<ContainerVisual>().Children();
        auto visual = m_compositor.CreateSpriteVisual();

        auto element = m_compositor.CreateSpriteVisual();
        uint8_t r = (double)(double)(rand() % 255);;
        uint8_t g = (double)(double)(rand() % 255);;
        uint8_t b = (double)(double)(rand() % 255);;

        element.Brush(m_compositor.CreateColorBrush({ 255, r, g, b }));
        element.Size({ size, size });
        element.Offset({ x, y, 0.0f, });

        auto animation = m_compositor.CreateVector3KeyFrameAnimation();
        auto bottom = (float)600 - element.Size().y;
        animation.InsertKeyFrame(1, { element.Offset().x, bottom, 0 });

        using timeSpan = std::chrono::duration<int, std::ratio<1, 1>>;

        std::chrono::seconds duration(2);
        std::chrono::seconds delay(3);

        animation.Duration(timeSpan(duration));
        animation.DelayTime(timeSpan(delay));
        element.StartAnimation(L"Offset", animation);
        visuals.InsertAtTop(element);

        visuals.InsertAtTop(visual);
    }
}

HelloComposition.cpp(部分)

#include "pch.h"
#include "HelloComposition.h"
#include "CompositionHost.h"

#define MAX_LOADSTRING 100
#define BTN_ADD 1000

CompositionHost* compHost = CompositionHost::GetInstance();

// Global Variables:

// ...
// ... code not shown ...
// ...

BOOL InitInstance(HINSTANCE hInstance, int nCmdShow)
{
   hInst = hInstance; // Store instance handle in our global variable

   HWND hWnd = CreateWindowW(szWindowClass, szTitle, WS_OVERLAPPEDWINDOW,
      CW_USEDEFAULT, 0, 900, 672, nullptr, nullptr, hInstance, nullptr);

// ...
// ... code not shown ...
// ...
}

// ...

LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
    switch (message)
    {
// Add this...
    case WM_CREATE:
    {
        compHost->Initialize(hWnd);
        srand(time(nullptr));

        CreateWindow(TEXT("button"), TEXT("Add element"),
            WS_VISIBLE | WS_CHILD | BS_PUSHBUTTON,
            12, 12, 100, 50,
            hWnd, (HMENU)BTN_ADD, nullptr, nullptr);
    }
    break;
// ...
    case WM_COMMAND:
    {
        int wmId = LOWORD(wParam);
        // Parse the menu selections:
        switch (wmId)
        {
        case IDM_ABOUT:
            DialogBox(hInst, MAKEINTRESOURCE(IDD_ABOUTBOX), hWnd, About);
            break;
        case IDM_EXIT:
            DestroyWindow(hWnd);
            break;
// Add this...
        case BTN_ADD: // addButton click
        {
            double size = (double)(rand() % 150 + 50);
            double x = (double)(rand() % 600);
            double y = (double)(rand() % 200);
            compHost->AddElement(size, x, y);
            break;
        }
// ...
        default:
            return DefWindowProc(hWnd, message, wParam, lParam);
        }
    }
    break;
    case WM_PAINT:
    {
        PAINTSTRUCT ps;
        HDC hdc = BeginPaint(hWnd, &ps);
        // TODO: Add any drawing code that uses hdc here...
        EndPaint(hWnd, &ps);
    }
    break;
    case WM_DESTROY:
        PostQuitMessage(0);
        break;
    default:
        return DefWindowProc(hWnd, message, wParam, lParam);
    }
    return 0;
}

// ...
// ... code not shown ...
// ...