Consommer des composants COM avec C++/WinRT

Note

Les exemples de code de cet article utilisent le modèle d’application IFrameworkView (UWP Core App). Dans une application de bureau WinUI 3, dérivez une classe de Microsoft ::UI ::Xaml ::Application et appelez Application ::Start(...) comme point d’entrée de l’application et utilisez DispatcherQueue au lieu de CoreDispatcher. Les concepts et modèles COM/Direct2D présentés ici s’appliquent également aux applications WinUI 3.

Vous pouvez utiliser les fonctionnalités de la bibliothèque C++/WinRT pour consommer des composants COM, tels que les graphiques 2D et 3D hautes performances des API DirectX. C++/WinRT est le moyen le plus simple d’utiliser DirectX sans compromettre les performances. Cette rubrique utilise un exemple de code Direct2D pour montrer comment utiliser C++/WinRT pour consommer des classes et interfaces COM. Vous pouvez bien sûr combiner com et Windows Runtime programmation au sein du même projet C++/WinRT.

À la fin de cette rubrique, vous trouverez une liste complète de code source d’une application Direct2D minimale. Nous allons extraire des extraits de ce code et les utiliser pour illustrer comment consommer des composants COM à l’aide de C++/WinRT à l’aide de différentes installations de la bibliothèque C++/WinRT.

Pointeurs intelligents COM (winrt ::com_ptr)

Lorsque vous programmez avec COM, vous travaillez directement avec des interfaces plutôt qu'avec des objets (c'est également vrai en arrière-plan pour Windows Runtime API, qui sont une évolution de COM). Pour appeler une fonction sur une classe COM, par exemple, vous activez la classe, récupérez une interface, puis appelez des fonctions sur cette interface. Pour accéder à l’état d’un objet, vous n’accédez pas directement à ses membres de données ; au lieu de cela, vous appelez des fonctions d’assesseur et de mutateur via une interface.

Pour être plus précis, nous parlons d’interagir avec des pointeurs d’interface. Pour cela, nous profitons de l’existence du type de pointeur intelligent COM en C++/WinRT, le type winrt ::com_ptr .

#include <d2d1_1.h>
...
winrt::com_ptr<ID2D1Factory1> factory;

Le code ci-dessus montre comment déclarer un pointeur intelligent non initialisé vers une interface COM ID2D1Factory1 . Le pointeur intelligent n’est pas initialisé. Il ne pointe donc pas encore vers une interface ID2D1Factory1 appartenant à un objet réel (il ne pointe pas vers une interface du tout). Mais il a le potentiel de le faire ; et (étant un pointeur intelligent) il a la possibilité via le comptage de référence COM de gérer la durée de vie de l’objet propriétaire de l’interface vers laquelle il pointe, et d’être le support par lequel vous appelez des fonctions sur cette interface.

Fonctions COM qui retournent un pointeur d’interface sous la forme void

Vous pouvez appeler la fonction com_ptr::put_void pour écrire dans le pointeur brut sous-jacent d’un pointeur intelligent non initialisé.

D2D1_FACTORY_OPTIONS options{ D2D1_DEBUG_LEVEL_NONE };
D2D1CreateFactory(
    D2D1_FACTORY_TYPE_SINGLE_THREADED,
    __uuidof(factory),
    &options,
    factory.put_void()
);

Le code ci-dessus appelle la fonction D2D1CreateFactory , qui retourne un pointeur d’interface ID2D1Factory1 via son dernier paramètre, qui a le type void** . De nombreuses fonctions COM retournent un void**. Pour ces fonctions, utilisez com_ptr::put_void comme illustré.

Fonctions COM qui retournent un pointeur d’interface spécifique

La fonction D3D11CreateDevice retourne un pointeur d’interface ID3D11Device via son troisième paramètre de dernière date, qui a le type ID3D11Device** . Pour les fonctions qui retournent un pointeur d’interface spécifique de ce type, utilisez com_ptr::put.

winrt::com_ptr<ID3D11Device> device;
D3D11CreateDevice(
    ...
    device.put(),
    ...);

L’exemple de code de la section avant celui-ci montre comment appeler la fonction D2D1CreateFactory brute. Mais en fait, lorsque l’exemple de code de cette rubrique appelle D2D1CreateFactory, il utilise un modèle de fonction d’assistance qui encapsule l’API bas niveau, de sorte que l’exemple de code utilise en réalité com_ptr::put.

winrt::com_ptr<ID2D1Factory1> factory;
D2D1CreateFactory(
    D2D1_FACTORY_TYPE_SINGLE_THREADED,
    options,
    factory.put());

Fonctions COM qui retournent un pointeur d’interface en tant que IUnknown

La fonction DWriteCreateFactory retourne un pointeur d’interface de fabrique DirectWrite via son dernier paramètre, qui a le type IUnknown . Pour une telle fonction, utilisez com_ptr ::p ut, mais réinterpretez cela en IUnknown.

DWriteCreateFactory(
    DWRITE_FACTORY_TYPE_SHARED,
    __uuidof(dwriteFactory2),
    reinterpret_cast<IUnknown**>(dwriteFactory2.put()));

Réinitialiser un winrt::com_ptr

Important

Si vous disposez d’un winrt ::com_ptr déjà assis (son pointeur brut interne a déjà une cible) et que vous souhaitez le réinscrire pour pointer vers un autre objet, vous devez d’abord l’affecter nullptr , comme illustré dans l’exemple de code ci-dessous. Si ce n’est pas le cas, une com_ptr qui contient déjà une valeur vous le signalera (lorsque vous appelez com_ptr::put ou com_ptr::put_void) en déclenchant une assertion indiquant que son pointeur interne n’est pas null.

winrt::com_ptr<ID2D1SolidColorBrush> brush;
...
    brush.put()
...
brush = nullptr; // Important because we're about to re-seat
target->CreateSolidColorBrush(
    color_orange,
    D2D1::BrushProperties(0.8f),
    brush.put()));

Gérer les codes d’erreur HRESULT

Pour vérifier la valeur d’un HRESULT retourné à partir d’une fonction COM et lever une exception dans le cas où elle représente un code d’erreur, appelez winrt ::check_hresult.

winrt::check_hresult(D2D1CreateFactory(
    D2D1_FACTORY_TYPE_SINGLE_THREADED,
    __uuidof(factory),
    options,
    factory.put_void()));

Fonctions COM qui prennent un pointeur d’interface spécifique

Vous pouvez appeler la fonction com_ptr ::get pour passer votre com_ptr à une fonction qui prend un pointeur d’interface spécifique du même type.

... ExampleFunction(
    winrt::com_ptr<ID2D1Factory1> const& factory,
    winrt::com_ptr<IDXGIDevice> const& dxdevice)
{
    ...
    winrt::check_hresult(factory->CreateDevice(dxdevice.get(), ...));
    ...
}

Fonctions COM qui prennent un pointeur d’interface IUnknown

Vous pouvez utiliser com_ptr ::get pour passer votre com_ptr à une fonction qui prend un pointeur d’interface IUnknown .

Vous pouvez utiliser la fonction libre winrt ::get_unknown pour renvoyer l’adresse (en d’autres termes, un pointeur vers) l’interface IUnknown brute sous-jacente d’un objet d’un type projeté. Vous pouvez ensuite passer cette adresse à une fonction qui prend un pointeur d’interface IUnknown .

Pour plus d’informations sur les types projetés, consultez Consommer des API avec C++/WinRT.

Pour obtenir un exemple de code de get_unknown, consultez winrt ::get_unknown ou la liste de code source complète d’une application Direct2D minimale dans cette rubrique.

Passage et renvoi de pointeurs intelligents COM

Une fonction prenant un pointeur intelligent COM sous la forme d’un winrt ::com_ptr doit le faire par référence constante ou par référence.

... GetDxgiFactory(winrt::com_ptr<ID3D11Device> const& device) ...

... CreateDevice(..., winrt::com_ptr<ID3D11Device>& device) ...

Une fonction qui retourne un winrt ::com_ptr doit le faire par valeur.

winrt::com_ptr<ID2D1Factory1> CreateFactory() ...

Interroger un pointeur smart COM pour obtenir une autre interface

Vous pouvez utiliser la fonction com_ptr ::as pour interroger un pointeur intelligent COM pour une autre interface. La fonction lève une exception si la requête ne réussit pas.

void ExampleFunction(winrt::com_ptr<ID3D11Device> const& device)
{
    ...
    winrt::com_ptr<IDXGIDevice> const dxdevice{ device.as<IDXGIDevice>() };
    ...
}

Vous pouvez également utiliser com_ptr ::try_as, qui retourne une valeur que vous pouvez vérifier nullptr pour voir si la requête a réussi.

Liste complète du code source d’une application Direct2D minimale

Note

Pour plus d’informations sur la configuration de Visual Studio pour le développement C++/WinRT, notamment l’installation et l’utilisation de l’extension de Visual Studio C++/WinRT (VSIX) et du package NuGet (qui fournissent ensemble un modèle de projet et une prise en charge de build), consultez Visual Studio prise en charge de C++/WinRT.

Si vous souhaitez générer et exécuter cet exemple de code source, commencez par installer (ou mettre à jour vers) la dernière version de l’extension de Visual Studio C++/WinRT (VSIX) ; consultez la remarque ci-dessus. Ensuite, dans Visual Studio, créez une application Core (C++/WinRT). Direct2D est un nom raisonnable pour le projet, mais vous pouvez le nommer tout ce que vous aimez. Ciblez la dernière version en disponibilité générale (c’est-à-dire non en préversion) du KIT DE développement logiciel (SDK) Windows.

Étape 1. Modifier pch.h

Ouvrez pch.het ajoutez #include <unknwn.h> immédiatement après l’inclusion windows.h. Cela est dû au fait que nous utilisons winrt ::get_unknown. Il est préférable de spécifier explicitement #include <unknwn.h> à chaque fois que vous utilisez winrt::get_unknown, même si cet en-tête a été inclus par un autre en-tête.

Note

Si vous omettez cette étape, l’erreur de génération « get_unknown » s’affiche : identificateur introuvable.

Étape 2. Modifier App.cpp

Ouvrez App.cpp, supprimez son contenu entier et collez-le dans la liste ci-dessous.

Le code ci-dessous utilise la fonction winrt ::com_ptr ::capture si possible. WINRT_ASSERT est une définition de macro, et elle s’étend à _ASSERTE.

#include "pch.h"
#include <d2d1_1.h>
#include <d3d11.h>
#include <dxgi1_2.h>
#include <winrt/Windows.Graphics.Display.h>

using namespace winrt;

using namespace Windows;
using namespace Windows::ApplicationModel::Core;
using namespace Windows::UI;
using namespace Windows::UI::Core;
using namespace Windows::Graphics::Display;

namespace
{
    winrt::com_ptr<ID2D1Factory1> CreateFactory()
    {
        D2D1_FACTORY_OPTIONS options{};

#ifdef _DEBUG
        options.debugLevel = D2D1_DEBUG_LEVEL_INFORMATION;
#endif

        winrt::com_ptr<ID2D1Factory1> factory;

        winrt::check_hresult(D2D1CreateFactory(
            D2D1_FACTORY_TYPE_SINGLE_THREADED,
            options,
            factory.put()));

        return factory;
    }

    HRESULT CreateDevice(D3D_DRIVER_TYPE const type, winrt::com_ptr<ID3D11Device>& device)
    {
        WINRT_ASSERT(!device);

        return D3D11CreateDevice(
            nullptr,
            type,
            nullptr,
            D3D11_CREATE_DEVICE_BGRA_SUPPORT,
            nullptr, 0,
            D3D11_SDK_VERSION,
            device.put(),
            nullptr,
            nullptr);
    }

    winrt::com_ptr<ID3D11Device> CreateDevice()
    {
        winrt::com_ptr<ID3D11Device> device;
        HRESULT hr{ CreateDevice(D3D_DRIVER_TYPE_HARDWARE, device) };

        if (DXGI_ERROR_UNSUPPORTED == hr)
        {
            hr = CreateDevice(D3D_DRIVER_TYPE_WARP, device);
        }

        winrt::check_hresult(hr);
        return device;
    }

    winrt::com_ptr<ID2D1DeviceContext> CreateRenderTarget(
        winrt::com_ptr<ID2D1Factory1> const& factory,
        winrt::com_ptr<ID3D11Device> const& device)
    {
        WINRT_ASSERT(factory);
        WINRT_ASSERT(device);

        winrt::com_ptr<IDXGIDevice> const dxdevice{ device.as<IDXGIDevice>() };

        winrt::com_ptr<ID2D1Device> d2device;
        winrt::check_hresult(factory->CreateDevice(dxdevice.get(), d2device.put()));

        winrt::com_ptr<ID2D1DeviceContext> target;
        winrt::check_hresult(d2device->CreateDeviceContext(D2D1_DEVICE_CONTEXT_OPTIONS_NONE, target.put()));
        return target;
    }

    winrt::com_ptr<IDXGIFactory2> GetDxgiFactory(winrt::com_ptr<ID3D11Device> const& device)
    {
        WINRT_ASSERT(device);

        winrt::com_ptr<IDXGIDevice> const dxdevice{ device.as<IDXGIDevice>() };

        winrt::com_ptr<IDXGIAdapter> adapter;
        winrt::check_hresult(dxdevice->GetAdapter(adapter.put()));

        winrt::com_ptr<IDXGIFactory2> factory;
        factory.capture(adapter, &IDXGIAdapter::GetParent);
        return factory;
    }

    void CreateDeviceSwapChainBitmap(
        winrt::com_ptr<IDXGISwapChain1> const& swapchain,
        winrt::com_ptr<ID2D1DeviceContext> const& target)
    {
        WINRT_ASSERT(swapchain);
        WINRT_ASSERT(target);

        winrt::com_ptr<IDXGISurface> surface;
        surface.capture(swapchain, &IDXGISwapChain1::GetBuffer, 0);

        D2D1_BITMAP_PROPERTIES1 const props{ D2D1::BitmapProperties1(
            D2D1_BITMAP_OPTIONS_TARGET | D2D1_BITMAP_OPTIONS_CANNOT_DRAW,
            D2D1::PixelFormat(DXGI_FORMAT_B8G8R8A8_UNORM, D2D1_ALPHA_MODE_IGNORE)) };

        winrt::com_ptr<ID2D1Bitmap1> bitmap;

        winrt::check_hresult(target->CreateBitmapFromDxgiSurface(surface.get(),
            props,
            bitmap.put()));

        target->SetTarget(bitmap.get());
    }

    winrt::com_ptr<IDXGISwapChain1> CreateSwapChainForCoreWindow(winrt::com_ptr<ID3D11Device> const& device)
    {
        WINRT_ASSERT(device);

        winrt::com_ptr<IDXGIFactory2> const factory{ GetDxgiFactory(device) };

        DXGI_SWAP_CHAIN_DESC1 props{};
        props.Format = DXGI_FORMAT_B8G8R8A8_UNORM;
        props.SampleDesc.Count = 1;
        props.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT;
        props.BufferCount = 2;
        props.SwapEffect = DXGI_SWAP_EFFECT_FLIP_SEQUENTIAL;

        winrt::com_ptr<IDXGISwapChain1> swapChain;

        winrt::check_hresult(factory->CreateSwapChainForCoreWindow(
            device.get(),
            winrt::get_unknown(CoreWindow::GetForCurrentThread()),
            &props,
            nullptr, // all or nothing
            swapChain.put()));

        return swapChain;
    }

    constexpr D2D1_COLOR_F color_white{ 1.0f,  1.0f,  1.0f,  1.0f };
    constexpr D2D1_COLOR_F color_orange{ 0.92f,  0.38f,  0.208f,  1.0f };
}

struct App : implements<App, IFrameworkViewSource, IFrameworkView>
{
    winrt::com_ptr<ID2D1Factory1> m_factory;
    winrt::com_ptr<ID2D1DeviceContext> m_target;
    winrt::com_ptr<IDXGISwapChain1> m_swapChain;
    winrt::com_ptr<ID2D1SolidColorBrush> m_brush;
    float m_dpi{};

    IFrameworkView CreateView()
    {
        return *this;
    }

    void Initialize(CoreApplicationView const&)
    {
    }

    void Load(hstring const&)
    {
        CoreWindow const window{ CoreWindow::GetForCurrentThread() };

        window.SizeChanged([&](auto&&...)
        {
            if (m_target)
            {
                ResizeSwapChainBitmap();
                Render();
            }
        });

        DisplayInformation const display{ DisplayInformation::GetForCurrentView() };
        m_dpi = display.LogicalDpi();

        display.DpiChanged([&](DisplayInformation const& display, IInspectable const&)
        {
            if (m_target)
            {
                m_dpi = display.LogicalDpi();
                m_target->SetDpi(m_dpi, m_dpi);
                CreateDeviceSizeResources();
                Render();
            }
        });

        m_factory = CreateFactory();
        CreateDeviceIndependentResources();
    }

    void Uninitialize()
    {
    }

    void Run()
    {
        CoreWindow const window{ CoreWindow::GetForCurrentThread() };
        window.Activate();

        Render();
        CoreDispatcher const dispatcher{ window.Dispatcher() };
        dispatcher.ProcessEvents(CoreProcessEventsOption::ProcessUntilQuit);
    }

    void SetWindow(CoreWindow const&) {}

    void Draw()
    {
        m_target->Clear(color_white);

        D2D1_SIZE_F const size{ m_target->GetSize() };
        D2D1_RECT_F const rect{ 100.0f, 100.0f, size.width - 100.0f, size.height - 100.0f };
        m_target->DrawRectangle(rect, m_brush.get(), 100.0f);

        char buffer[1024];
        (void)snprintf(buffer, sizeof(buffer), "Draw %.2f x %.2f @ %.2f\n", size.width, size.height, m_dpi);
        ::OutputDebugStringA(buffer);
    }

    void Render()
    {
        if (!m_target)
        {
            winrt::com_ptr<ID3D11Device> const device{ CreateDevice() };
            m_target = CreateRenderTarget(m_factory, device);
            m_swapChain = CreateSwapChainForCoreWindow(device);

            CreateDeviceSwapChainBitmap(m_swapChain, m_target);

            m_target->SetDpi(m_dpi, m_dpi);

            CreateDeviceResources();
            CreateDeviceSizeResources();
        }

        m_target->BeginDraw();
        Draw();
        m_target->EndDraw();

        HRESULT const hr{ m_swapChain->Present(1, 0) };

        if (S_OK != hr && DXGI_STATUS_OCCLUDED != hr)
        {
            ReleaseDevice();
        }
    }

    void ReleaseDevice()
    {
        m_target = nullptr;
        m_swapChain = nullptr;

        ReleaseDeviceResources();
    }

    void ResizeSwapChainBitmap()
    {
        WINRT_ASSERT(m_target);
        WINRT_ASSERT(m_swapChain);

        m_target->SetTarget(nullptr);

        if (S_OK == m_swapChain->ResizeBuffers(0, // all buffers
            0, 0, // client area
            DXGI_FORMAT_UNKNOWN, // preserve format
            0)) // flags
        {
            CreateDeviceSwapChainBitmap(m_swapChain, m_target);
            CreateDeviceSizeResources();
        }
        else
        {
            ReleaseDevice();
        }
    }

    void CreateDeviceIndependentResources()
    {
    }

    void CreateDeviceResources()
    {
        winrt::check_hresult(m_target->CreateSolidColorBrush(
            color_orange,
            D2D1::BrushProperties(0.8f),
            m_brush.put()));
    }

    void CreateDeviceSizeResources()
    {
    }

    void ReleaseDeviceResources()
    {
        m_brush = nullptr;
    }
};

int __stdcall wWinMain(HINSTANCE, HINSTANCE, PWSTR, int)
{
    CoreApplication::Run(winrt::make<App>());
}

Utilisation de types COM, tels que BSTR et VARIANT

Comme vous pouvez le voir, C++/WinRT prend en charge l’implémentation et l’appel d’interfaces COM. Pour utiliser des types COM, tels que BSTR et VARIANT, nous vous recommandons d’utiliser des wrappers fournis par les bibliothèques d’implémentation de Windows (WIL) telles que wil ::unique_bstr et wil ::unique_variant (qui gèrent les durées de vie des ressources).

WIL remplace les frameworks tels que la bibliothèque de modèles actifs (ATL) et la prise en charge COM du compilateur Visual C++. Et nous le recommandons plutôt que d’écrire vos propres encapsulations ou d’utiliser des types COM tels que BSTR et VARIANT à l’état brut (avec les API appropriées).

Éviter les collisions d’espaces de noms

Il est courant dans C++/WinRT, comme le montre la liste de code de cette rubrique, d’utiliser des directives d’utilisation libéralement. Dans certains cas, cela peut entraîner le problème d’importation de noms en collision dans l’espace de noms global. Voici un exemple.

C++/WinRT contient un type nommé winrt ::Windows ::Foundation ::IUnknown ; tandis que COM définit un type nommé ::IUnknown. Prenons donc le code suivant dans un projet C++/WinRT qui consomme des en-têtes COM.

using namespace winrt::Windows::Foundation;
...
void MyFunction(IUnknown*); // error C2872:  'IUnknown': ambiguous symbol

Le nom non qualifié IUnknown est en conflit dans l’espace de noms global, d’où l’erreur du compilateur symbole ambigu. Au lieu de cela, vous pouvez isoler la version C++/WinRT du nom dans l’espace de noms winrt , comme ceci.

namespace winrt
{
    using namespace Windows::Foundation;
}
...
void MyFunctionA(IUnknown*); // Ok.
void MyFunctionB(winrt::IUnknown const&); // Ok.

Ou, si vous voulez la commodité de using namespace winrt, alors vous pouvez. Vous devez simplement qualifier la version globale d’IUnknown, comme ceci.

using namespace winrt;
namespace winrt
{
    using namespace Windows::Foundation;
}
...
void MyFunctionA(::IUnknown*); // Ok.
void MyFunctionB(winrt::IUnknown const&); // Ok.

Naturellement, cela fonctionne avec n’importe quel espace de noms C++/WinRT.

namespace winrt
{
    using namespace Windows::Storage;
    using namespace Windows::System;
}

Vous pouvez ensuite faire référence à winrt ::Windows ::Storage ::StorageFile, par exemple, comme juste winrt ::StorageFile.

API importantes