Utilizzare componenti COM con C++/WinRT

Puoi usare le funzionalità della libreria C++/WinRT per utilizzare componenti COM, ad esempio la grafica 2D e 3D ad alte prestazioni delle API DirectX. C++/ WinRT è il modo più semplice per usare DirectX senza compromettere le prestazioni. Questo argomento usa un esempio di codice Direct2D per illustrare l'uso di C++/WinRT per utilizzare classi e interfacce COM. Naturalmente, puoi combinare elementi di programmazione COM e Windows Runtime all'interno dello stesso progetto C++/WinRT.

Alla fine di questo argomento, troverai un listato di codice sorgente completo di un'applicazione Direct2D minima. Alcuni estratti di tale codice verranno usati per illustrare come utilizzare i componenti COM con C++/WinRT sfruttando diverse funzionalità della libreria C++/WinRT.

Puntatori intelligenti COM (winrt::com_ptr)

Nella programmazione con COM si lavora direttamente con le interfacce anziché con gli oggetti. Ciò è vero anche dietro le quinte per le API Windows Runtime, che sono un'evoluzione di COM. Per chiamare una funzione per una classe COM, ad esempio, si attiva la classe, si ottiene un'interfaccia e quindi si chiamano funzioni su tale interfaccia. Per accedere allo stato di un oggetto, non devi accedere direttamente ai relativi membri dati, ma chiamare funzioni di accesso e mutatore su un'interfaccia.

Per essere più specifici, si tratta di interagire con puntatori di interfaccia. Per questo, traiamo vantaggio dall'esistenza del tipo di puntatore intelligente COM in C++/WinRT — il tipo winrt::com_ptr .

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

Il codice sopra riportato mostra come dichiarare un puntatore intelligente non inizializzato a un'interfaccia COM ID2D1Factory1. Il puntatore intelligente non è inizializzato, quindi non punta ancora a un'interfaccia ID2D1Factory1 appartenente a qualsiasi oggetto effettivo (in effetti non punta ad alcuna interfaccia). Però può farlo potenzialmente e trattandosi di un puntatore intelligente ha la capacità (tramite conteggio dei riferimenti COM) di gestire la durata dell'oggetto dell'interfaccia a cui punta e di essere il mezzo tramite il quale puoi chiamare le funzioni su tale interfaccia.

Funzioni COM che restituiscono un puntatore di interfaccia come void

Puoi chiamare la funzione com_ptr::put_void per scrivere nel puntatore non elaborato sottostante di un puntatore intelligente non inizializzato.

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

Il codice precedente richiama la funzione D2D1CreateFactory, che restituisce un puntatore di interfaccia ID2D1Factory1 tramite l'ultimo parametro, con il tipo void**. Molte funzioni COM restituiscono void**. Per tali funzioni, usa com_ptr::put_void come illustrato.

Funzioni COM che restituiscono un puntatore di interfaccia specifico

La funzione D3D11CreateDevice restituisce un puntatore di interfaccia ID3D11Device tramite il terzultimo parametro, con tipo ID3D11Device**. Per le funzioni che restituiscono un puntatore di interfaccia specifico in questo modo, usa com_ptr::put.

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

L'esempio di codice nella sezione prima di questa mostra come chiamare la funzione D2D1CreateFactory non elaborata. Ma in realtà, quando l'esempio di codice in questo argomento chiama D2D1CreateFactory usa un modello di funzione helper che esegue il wrapping dell'API non elaborata e quindi l'esempio di codice usa effettivamente com_ptr::put.

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

Funzioni COM che restituiscono un puntatore di interfaccia come IUnknown

La funzione DWriteCreateFactory restituisce un puntatore di interfaccia factory DirectWrite tramite l'ultimo parametro, con tipo IUnknown. Per una funzione di questo tipo, usa com_ptr::put, ma eseguine il cast su IUnknown.

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

Ridestinare un puntatore winrt::com_ptr

Importante

Se hai un puntatore winrt::com_ptr già collocato (il relativo puntatore non elaborato interno ha già una destinazione) e destinarlo ridestinarlo in modo che punti a un oggetto diverso, devi prima di tutto assegnare nullptr al puntatore, come illustrato nell'esempio di codice seguente. In caso contrario, un com_ptr già collocato porterà il problema alla tua attenzione (quando chiami com_ptr::put oppure com_ptr:: put_void) indicando che il relativo puntatore interno non è 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()));

Gestire i codici di errore HRESULT

Per controllare il valore di un HRESULT restituito da una funzione COM e generare un'eccezione nel caso in cui rappresenti un codice di errore, chiama winrt::check_hresult.

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

Funzioni COM che accettano un puntatore di interfaccia specifico

Puoi chiamare la funzione com_ptr::get per passare com_ptr a una funzione che accetta un puntatore di interfaccia specifico dello stesso tipo.

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

Funzioni COM che accettano un puntatore di interfaccia IUnknown

Puoi usare com_ptr::get per passare com_ptr a una funzione che accetta un puntatore di interfaccia IUnknown.

Puoi usare la funzione libera winrt::get_unknown per restituire l'indirizzo dell'interfaccia IUnknown sottostante non elaborata (in altre parole, un puntatore a tale interfaccia) di un oggetto di un tipo proiettato. Puoi quindi passare tale indirizzo a una funzione che accetta un puntatore di interfaccia IUnknown.

Per informazioni sui tipi proiettati, vedi Utilizzare API con C++/WinRT.

Per un esempio di codice di get_unknown, vedi winrt::get_unknown o la sezione Listato di codice sorgente completo di un'applicazione Direct2D minima in questo argomento.

Passaggio e restituzione di puntatori intelligenti COM

Una funzione che accetta un puntatore intelligente COM in forma di winrt::com_ptr deve farlo tramite un riferimento costante o per riferimento.

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

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

Una funzione che restituisce un winrt::com_ptr deve farlo per valore.

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

Richiedere un puntatore intelligente COM per un'altra interfaccia

Puoi usare la funzione com_ptr::as per richiedere un puntatore intelligente COM per un'interfaccia diversa. La funzione genera un'eccezione se la query ha esito negativo.

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

In alternativa, usa com_ptr::try_as, che restituisce un valore che puoi confrontare con nullptr per vedere se la query è stata completata.

Listato di codice sorgente completo di un'applicazione Direct2D minima

Nota

Per informazioni sulla configurazione di Visual Studio per lo sviluppo in C++/WinRT, compresi l'installazione e l'uso dell'estensione C++/WinRT per Visual Studio (VSIX) e del pacchetto NuGet, che insieme forniscono il modello di progetto e il supporto della compilazione, vedi Supporto di Visual Studio per C++/WinRT.

Per compilare ed eseguire questo esempio di codice sorgente, installare prima o eseguire l'aggiornamento alla versione più recente di C++/WinRT Visual Studio Extension (VSIX). Vedere la nota sopra. Quindi, in Visual Studio, creare una nuova App Core (C++/WinRT). Direct2D è un nome adeguato per il progetto, ma puoi scegliere un nome qualsiasi. Specificare come destinazione la versione più recente disponibile a livello generale, ovvero non l'anteprima, di Windows SDK.

Passaggio 1: Modifica pch.h

Apri pch.h e aggiungi #include <unknwn.h> subito dopo aver incluso windows.h. Questa operazione è necessaria perché stiamo usando winrt::get_unknown. È consigliabile eseguire #include <unknwn.h> in modo esplicito ogni volta che usi winrt::get_unknown, anche se tale intestazione è stata inclusa da un'altra intestazione.

Nota

Se si omette questo passaggio, verrà visualizzato l'errore di compilazione 'get_unknown': identificatore non trovato.

Passaggio 2. Modifica App.cpp

Apri App.cpp, elimina l'intero contenuto e incolla il listato riportato di seguito.

Il codice seguente usa la funzione winrt::com_ptr::capture laddove possibile. WINRT_ASSERT è una definizione di macro e si espande in 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>());
}

Utilizzo di tipi COM, quali BSTR e VARIANT

Come puoi notare, C++/WinRT offre supporto sia per l'implementazione che per la chiamata di interfacce COM. Per usare i tipi COM, quali BSTR e VARIANT, è consigliabile usare i wrapper forniti dalle librerie di implementazione di Windows (WIL), come wil::unique_bstr e wil::unique_variant (che gestiscono la durata delle risorse).

Le librerie WIL sostituiscono framework come ATL (Active Template Library) e il supporto COM del compilatore Visual C++. Consigliamo l'uso di queste librerie rispetto alla scrittura di wrapper personalizzati oppure l'uso di tipi COM, ad esempio BSTR e VARIANT nel loro formato non elaborato (insieme alle API appropriate).

Come evitare conflitti di spazio dei nomi

In C++/WinRT è prassi comune, come illustrato nel listato di codice di questo argomento, usare numerose direttive using. In alcuni casi, tuttavia, ciò può causare un problema di importazione di nomi in conflitto nello spazio dei nomi globale. Ecco un esempio.

C++/WinRT contiene un tipo denominato winrt::Windows::Foundation::IUnknown, mentre COM definisce un tipo denominato ::IUnknown. Considera ora il codice seguente, in un progetto C++/WinRT che utilizza intestazioni COM.

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

Il nome non qualificato IUnknown è in conflitto nella spazio dei nomi globale e pertanto il compilatore restituisce un errore di simbolo ambiguo. In alternativa, puoi isolare la versione C++/WinRT del nome nello spazio dei nomi winrt, come nel codice seguente.

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

Oppure puoi anche sfruttare la comodità di using namespace winrt. Devi semplicemente qualificare la versione globale di IUnknown, come nel codice seguente.

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

Naturalmente, questo funziona con qualsiasi spazio dei nomi C++/WinRT.

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

Puoi quindi fare riferimento a winrt::Windows::Storage::StorageFile, ad esempio, usando semplicemente winrt::StorageFile.

API importanti