Utilisation de la couche visuelle avec Win32

Vous pouvez utiliser des API de composition Windows Runtime (également appelées couche visuelle) dans vos applications Windows Win32 afin de créer des expériences modernes qui s’activent pour les utilisateurs de Windows.

Le code complet de ce tutoriel est disponible sur GitHub : Exemple Win32 HelloComposition.

Les applications Windows universelles ont accès à l’espace de noms Windows.UI.Composition, qui leur permet de contrôler avec précision la composition et l’affichage de leur interface utilisateur. Toutefois, cette API de composition n’est pas limitée aux applications UWP. Les applications de bureau Win32 peuvent tirer parti des systèmes de composition modernes dans UWP et Windows.

Prerequisites

L’API d’hébergement UWP est régie par les conditions préalables suivantes.

Guide pratique pour utiliser des API de composition à partir d’une application de bureau Win32

Dans ce tutoriel, vous allez créer une application Win32 C++ et y ajouter des éléments de composition UWP. L’objectif est de configurer correctement le projet, de créer le code d’interopérabilité et de dessiner des éléments simples à l’aide des API de composition Windows. L’application terminée ressemble à ceci.

The running app UI

Créer un projet C++ Win32 dans Visual Studio

La première étape consiste à créer le projet d’application Win32 dans Visual Studio.

Pour créer un projet d’application Win32 en C++ nommé HelloComposition :

  1. Ouvrez Visual Studio, puis sélectionnez Fichier>Nouveau>Projet.

    La boîte de dialogue Nouveau projet s’affiche.

  2. Dans la catégorie Installé, développez le nœud Visual C++, puis sélectionnez Bureau Windows.

  3. Sélectionnez le modèle Application de bureau Windows.

  4. Entrez le nom HelloComposition, puis cliquez sur OK.

    Visual Studio crée le projet et ouvre l’éditeur du fichier d’application principal.

Configurer le projet pour qu’il utilise des API Windows Runtime

Pour utiliser des API Windows Runtime (WinRT) dans votre application Win32, nous utilisons C++/WinRT. Vous devez configurer votre projet Visual Studio pour ajouter la prise en charge de C++/WinRT.

(Pour plus d’informations, consultez Bien démarrer avec C++/WinRT - Modifier un projet d’application de bureau Windows pour ajouter la prise en charge de C++/WinRT).

  1. Dans le menu Projet, ouvrez les propriétés du projet (Propriétés de HelloComposition) et assurez-vous que les paramètres suivants sont définis sur les valeurs spécifiées :

    • Pour Configuration, sélectionnez Toutes les configurations. Pour Plateforme, sélectionnez Toutes les plateformes.
    • Propriétés de la configuration>Général>Version du SDK Windows = 10.0.17763.0 ou supérieure

    Set SDK version

    • C/C++>Langage>Norme du langage C++ = ISO C++ 17 Standard (/stf:c++17)

    Set language standard

    • Éditeur de liens>Entrée>Dépendances supplémentaires doit inclure « windowsapp.lib ». Si cet élément n’est pas inclus dans la liste, ajoutez-le.

    Add linker dependency

  2. Mettez à jour l’en-tête précompilé.

    • Renommez stdafx.h et stdafx.cpp en pch.h et pch.cpp, respectivement.

    • Affectez à la propriété de projet C/C++>En-têtes précompilés>Fichier d’en-tête précompilé la valeur pch.h.

    • Recherchez #include "stdafx.h" et remplacez-le par #include "pch.h" dans tous les fichiers.

      (Modifier>Rechercher et remplacer>Chercher dans les fichiers)

      Find and replace stdafx.h

    • Dans pch.h, incluez winrt/base.h et unknwn.h.

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

Il est judicieux de générer le projet à ce stade pour vérifier qu’il n’y a pas d’erreurs avant de poursuivre.

Créer une classe pour héberger les éléments de la composition

Pour héberger le contenu que vous créez avec la couche visuelle, créez une classe (CompositionHost) afin de gérer l’interopérabilité et créer des éléments de composition. C’est là que vous effectuez la majeure partie de la configuration pour l’hébergement des API de composition, notamment :

Nous convertissons cette classe en singleton afin d’éviter les problèmes de thread. Par exemple, vous ne pouvez créer qu’une seule file d’attente de répartiteur par thread. Ainsi, l’instanciation d’une seconde instance de CompositionHost sur le même thread entraînerait une erreur.

Astuce

Si nécessaire, consultez le code complet à la fin du tutoriel pour vous assurer que tout le code se trouve à la bonne place à mesure que vous parcourez le tutoriel.

  1. Ajoutez un nouveau fichier de classe à votre projet.

    • Dans l’Explorateur de solutions, cliquez avec le bouton droit sur le projet HelloComposition.
    • Dans le menu contextuel, sélectionnez Ajouter>Classe....
    • Dans la boîte de dialogue Ajouter une classe, nommez la classe CompositionHost.cs, puis cliquez sur Ajouter.
  2. Incluez les en-têtes et les instructions using nécessaires pour l’interopérabilité de la composition.

    • Dans CompositionHost.h, ajoutez ces instructions include au début du fichier.
    #pragma once
    #include <winrt/Windows.UI.Composition.Desktop.h>
    #include <windows.ui.composition.interop.h>
    #include <DispatcherQueue.h>
    
    • Dans CompositionHost.cpp, ajoutez ces instructions using au début du fichier, après toutes les instructions include éventuelles.
    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. Modifiez la classe pour utiliser le modèle singleton.

    • Dans CompositionHost.h, rendez le constructeur privé.
    • Déclarez une méthode GetInstance statique publique.
    class CompositionHost
    {
    public:
        ~CompositionHost();
        static CompositionHost* GetInstance();
    
    private:
        CompositionHost();
    };
    
    • Dans CompositionHost.cpp, ajoutez la définition de la méthode GetInstance.
    CompositionHost* CompositionHost::GetInstance()
    {
        static CompositionHost instance;
        return &instance;
    }
    
  4. Dans CompositionHost.h, déclarez des variables de membre privé pour le compositeur, DispatcherQueueController et 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. Ajoutez une méthode publique pour initialiser les objets d’interopérabilité de composition.

    Note

    Dans Initialize, vous appelez les méthodes EnsureDispatcherQueue, CreateDesktopWindowTarget et CreateCompositionRoot. Vous créerez ces méthodes aux étapes suivantes.

    • Dans CompositionHost.h, déclarez une méthode publique nommée Initialize qui prend un HWND comme argument.
    void Initialize(HWND hwnd);
    
    • Dans CompositionHost.cpp, ajoutez la définition de la méthode Initialize.
    void CompositionHost::Initialize(HWND hwnd)
    {
        EnsureDispatcherQueue();
        if (m_dispatcherQueueController) m_compositor = Compositor();
    
        CreateDesktopWindowTarget(hwnd);
        CreateCompositionRoot();
    }
    
  6. Créez une file d’attente de répartiteur sur le thread qui utilisera Windows Composition.

    Un compositeur doit être créé sur un thread qui a une file d’attente de répartiteur. Cette méthode est donc appelée en premier pendant l’initialisation.

    • Dans CompositionHost.h, déclarez une méthode privée nommée EnsureDispatcherQueue.
    void EnsureDispatcherQueue();
    
    • Dans CompositionHost.cpp, ajoutez la définition de la méthode 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. Inscrivez la fenêtre de votre application en tant que cible de composition.

    • Dans CompositionHost.h, déclarez une méthode privée nommée CreateDesktopWindowTarget qui prend un HWND comme argument.
    void CreateDesktopWindowTarget(HWND window);
    
    • Dans CompositionHost.cpp, ajoutez la définition de la méthode 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. Créez un conteneur visuel racine destiné à contenir des objets visuels.

    • Dans CompositionHost.h, déclarez une méthode privée nommée CreateCompositionRoot.
    void CreateCompositionRoot();
    
    • Dans CompositionHost.cpp, ajoutez la définition de la méthode CreateCompositionRoot.
    void CompositionHost::CreateCompositionRoot()
    {
        auto root = m_compositor.CreateContainerVisual();
        root.RelativeSizeAdjustment({ 1.0f, 1.0f });
        root.Offset({ 124, 12, 0 });
        m_target.Root(root);
    }
    

Générez le projet maintenant pour vous assurer qu’il n’y a pas d’erreur.

Ces méthodes configurent les composants nécessaires à l’interopérabilité entre la couche visuelle UWP et les API Win32. Vous pouvez maintenant ajouter du contenu à votre application.

Ajouter des éléments de composition

Une fois l’infrastructure en place, vous pouvez générer le contenu de composition que vous souhaitez montrer.

Pour cet exemple, vous ajoutez du code qui crée un SpriteVisual carré de couleur aléatoire avec une animation qui entraîne sa suppression après un bref délai.

  1. Ajoutez un élément de composition.

    • Dans CompositionHost.h, déclarez une méthode publique nommée AddElement qui prend 3 valeurs float comme arguments.
    void AddElement(float size, float x, float y);
    
    • Dans CompositionHost.cpp, ajoutez la définition de la méthode 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);
        }
    }
    

Créer et afficher la fenêtre

À présent, vous pouvez ajouter un bouton et le contenu de composition UWP à votre interface utilisateur Win32.

  1. Dans HelloComposition.cpp, au début du fichier, incluez CompositionHost.h, définissez BTN_ADD et récupérez une instance de CompositionHost.

    #include "CompositionHost.h"
    
    // #define MAX_LOADSTRING 100 // This is already in the file.
    #define BTN_ADD 1000
    
    CompositionHost* compHost = CompositionHost::GetInstance();
    
  2. Dans la méthode InitInstance, changez la taille de la fenêtre créée. (Dans cette ligne, remplacez CW_USEDEFAULT, 0 par 900, 672.)

    HWND hWnd = CreateWindowW(szWindowClass, szTitle, WS_OVERLAPPEDWINDOW,
        CW_USEDEFAULT, 0, 900, 672, nullptr, nullptr, hInstance, nullptr);
    
  3. Dans la fonction WndProc, ajoutez case WM_CREATE au bloc switch message. Dans ce cas, vous initialisez le CompositionHost et créez le bouton.

    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. De même, dans la fonction WndProc, gérez le clic sur le bouton pour ajouter un élément de composition à l’interface utilisateur.

    Ajoutez case BTN_ADD au bloc switch wmId à l’intérieur du bloc 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;
    }
    

À présent, vous pouvez générer et exécuter votre application. Si nécessaire, reportez-vous au code complet fourni à la fin du tutoriel pour vous assurer que tout le code se trouve à la bonne place.

Quand vous exécutez l’application et cliquez sur le bouton, vous devez voir les carrés animés ajoutés à l’interface utilisateur.

Ressources supplémentaires

Code complet

Voici le code complet de la classe CompositionHost et de la méthode 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 (partiel)

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