Использование визуального уровня на платформе Win32

Вы можете использовать API Composition среды выполнения Windows (также называемые визуальным уровнем) в приложениях Win32, чтобы создавать современные интерфейсы для пользователей Windows.

Полный код для этого руководства доступен на сайте GitHub: пример Win32 HelloComposition.

Приложения универсальной платформы Windows, которым необходим точный контроль над композицией пользовательского интерфейса, имеют доступ к пространству имен Windows.UI.Composition, которое позволяет точно контролировать композицию и отрисовку пользовательского интерфейса. Однако этот API Composition не ограничивается только приложениями UWP. Классические приложения Win32 могут использовать современные системы компоновки в UWP и Windows.

Необходимые компоненты

Для использования API размещения для UWP накладываются приведенные ниже предварительные требования.

Как использовать интерфейсы API Composition из классического приложения Win32

При работе с этим руководством вы создадите простое приложение Win32 на C++ и добавите в него элементы композиции UWP. Основное внимание уделяется правильной настройке проекта, созданию кода взаимодействия и отрисовке простых элементов с помощью интерфейсов API Windows Composition. Завершенное приложение выглядит следующим образом.

The running app UI

Создание проекта C++ для Win32 в Visual Studio

Первым шагом является создание проекта приложения Win32 в Visual Studio.

Чтобы создать проект приложения Win32 на C++ с именем HelloComposition, сделайте следующее.

  1. Откройте Visual Studio и выберите Файл>Создать>Проект.

    Появится диалоговое окно Создать проект.

  2. В категории Установлены разверните узел Visual C++, а затем выберите Windows Desktop.

  3. Выберите шаблон Классическое приложение Windows.

  4. Введите имя HelloComposition, а затем нажмите кнопку ОК.

    Visual Studio создаст проект и откроет редактор для основного файла приложения.

Настройка проекта для использования интерфейсов API среды выполнения Windows

Чтобы применить интерфейсы API среды выполнения Windows (WinRT) в приложении Win32, мы используем C++/WinRT. Чтобы добавить поддержку C++/WinRT, необходимо настроить проект Visual Studio.

(Дополнительные сведения см. в разделе Добавление поддержки C++/WinRT в проект классического приложения для Windows статьи "Начало работы с C++/WinRT".)

  1. В меню Проект откройте свойства проекта (Свойства HelloComposition) и убедитесь, что для следующих параметров заданы указанные значения.

    • Для параметра Конфигурации выберите Все конфигурации. Для параметра Платформа выберите Все платформы.
    • Свойства конфигурации>Общие>Версия Windows SDK = 10.0.17763.0 или более поздняя версия.

    Set SDK version

    • C/C++>Язык>Стандарт языка C++ = Стандарт ISO C++ 17 (/stf:c++17)

    Set language standard

    • Компоновщик>Ввод>Дополнительные зависимости — этот раздел должен содержать windowsapp.lib. Если эта зависимость отсутствует в списке, добавьте ее.

    Add linker dependency

  2. Изменение предварительно скомпилированного заголовка

    • Переименуйте stdafx.h и stdafx.cpp в pch.h и pch.cpp соответственно.

    • Установите для свойства проекта C/C++>Предварительно скомпилированные заголовки>Предварительно скомпилированный заголовочный файл значение pch.h.

    • Найдите #include "stdafx.h" и замените его #include "pch.h" во всех файлах.

      (Изменить>Найти и заменить>Найти в файлах.)

      Find and replace stdafx.h

    • В pch.h добавьте winrt/base.h и unknwn.h.

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

На этом этапе рекомендуется выполнить сборку проекта, чтобы убедиться в отсутствии ошибок, прежде чем продолжить.

Создание класса для размещения элементов композиции

Чтобы разместить содержимое, созданное с помощью визуального уровня, создайте класс (CompositionHost) для управления взаимодействием и создания элементов композиции. Это потребует выполнить большую часть настройки для размещения интерфейсов API Composition. Потребуется:

Мы сделаем этот класс singleton, чтобы избежать проблем с потоками. Например, можно создать только одну очередь диспетчера для каждого потока, поэтому создание второго экземпляра 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. Измените класс, чтобы использовать шаблон singleton.

    • В 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 объявите частные переменные-члены DispatcherQueueController и DesktopWindowTarget для Compositor.

    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 объявите общедоступный метод Initialize, который принимает HWND в качестве аргумента.
    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 объявите частный метод CreateDesktopWindowTarget, который принимает HWND в качестве аргумента.
    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 и интерфейсами API Win32. Теперь вы можете добавить содержимое в приложение.

Добавление элементов композиции

Теперь, когда инфраструктура настроена, можно создать содержимое Composition, которое необходимо отобразить.

В этом примере вы добавите код, создающий квадрат SpriteVisual произвольного цвета с анимацией, которая удаляет его после небольшой задержки.

  1. Добавьте элемент композиции.

    • В CompositionHost.h объявите общедоступный метод AddElement, который принимает 3 значения с плавающей запятой в качестве аргументов.
    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);
        }
    }
    

Создание и отображение окна

Теперь можно добавить кнопку и содержимое композиции UWP в пользовательский интерфейс Win32.

  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 обработайте нажатие кнопки, чтобы добавить элемент композиции в пользовательский интерфейс.

    Добавьте case BTN_ADD в блок параметров wmId в блоке WM_COMMAND.

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

Выполните сборку и запустите приложение. При необходимости сверьтесь с полным кодом в конце руководства, чтобы убедиться в том, что весь код добавлен в соответствующе места.

Если запустить приложение и нажать кнопку, в пользовательском интерфейсе должны отобразиться анимированные квадраты.

Дополнительные ресурсы

Полный код

Ниже приведен полный код класса 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 ...
// ...