Consumir componentes COM com C++/WinRT

Note

Os exemplos de código neste artigo usam o modelo de aplicativo UWP Core App (IFrameworkView). Em um aplicativo da área de trabalho do WinUI 3, derive uma classe de Microsoft::UI::Xaml::Application e chame Application::Start(...) como o ponto de entrada do aplicativo e use DispatcherQueue em vez de CoreDispatcher. Os conceitos e padrões COM/Direct2D mostrados aqui se aplicam igualmente aos aplicativos WinUI 3.

Você pode usar os recursos da biblioteca C++/WinRT para consumir componentes COM, como os gráficos 2-D e 3-D de alto desempenho das APIs DirectX. C++/WinRT é a maneira mais simples de usar o DirectX sem comprometer o desempenho. Este tópico usa um exemplo de código Direct2D para mostrar como usar C++/WinRT para consumir classes e interfaces COM. Você pode, é claro, misturar programação COM e do Windows Runtime no mesmo projeto C++/WinRT.

No final deste tópico, você encontrará uma listagem completa de código-fonte de um aplicativo Direct2D mínimo. Vamos extrair trechos desse código e usá-los para ilustrar como consumir componentes COM com C++/WinRT, valendo-nos de vários recursos da biblioteca C++/WinRT.

Ponteiros inteligentes COM (winrt::com_ptr)

Ao programar com COM, você trabalha diretamente com interfaces em vez de com objetos (isso também é verdade nos bastidores para APIs Windows Runtime, que são uma evolução do COM). Para chamar uma função em uma classe COM, por exemplo, você ativa a classe, obtém uma interface de volta e, em seguida, chama funções nessa interface. Para acessar o estado de um objeto, você não acessa seus membros de dados diretamente; Em vez disso, você chama funções de acessador e modificador em uma interface.

Para ser mais específico, estamos falando de interagir com ponteiros de interface. E, para isso, nos beneficiamos da existência do tipo de ponteiro inteligente COM no C++/WinRT— o tipo winrt::com_ptr .

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

O código acima mostra como declarar um ponteiro inteligente não inicializado para uma interface COM ID2D1Factory1 . O ponteiro inteligente não é inicializado, portanto, ele ainda não está apontando para uma interface ID2D1Factory1 pertencente a qualquer objeto real (ele não está apontando para uma interface). Mas ele tem potencial para isso; e (sendo um ponteiro inteligente) tem a capacidade de, por meio da contagem de referências do COM, gerenciar o ciclo de vida do objeto proprietário da interface para a qual ele aponta e servir como meio para chamar funções nessa interface.

Funções COM que retornam um ponteiro de interface do tipo void

Você pode chamar a função com_ptr::put_void para escrever no ponteiro bruto subjacente de um ponteiro inteligente não inicializado.

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

O código acima chama a função D2D1CreateFactory , que retorna um ponteiro de interface ID2D1Factory1 por meio de seu último parâmetro, que tem o tipo void** . Muitas funções COM retornam um void**. Para essas funções, use com_ptr::put_void, conforme mostrado.

Funções COM que retornam um ponteiro de interface específico

A função D3D11CreateDevice retorna um ponteiro de interface ID3D11Device por meio do seu terceiro parâmetro a contar do final, que tem o tipo ID3D11Device**. Para funções que retornam um ponteiro de interface específico como esse, use com_ptr::put.

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

O exemplo de código na seção anterior a esta mostra como chamar a função D2D1CreateFactory bruta. Mas, na verdade, quando o exemplo de código deste tópico chama D2D1CreateFactory, ele usa um modelo de função auxiliar que encapsula a API de baixo nível e, portanto, o exemplo de código realmente usa com_ptr::put.

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

Funções COM que retornam um ponteiro de interface como IUnknown

A função DWriteCreateFactory retorna um ponteiro para a interface de fábrica do DirectWrite por meio do seu último parâmetro, que é do tipo IUnknown. Para essa função, use com_ptr::put, mas faça um cast de reinterpret para IUnknown.

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

Reposicione um winrt::com_ptr

Important

Se você tiver um winrt::com_ptr que já está sentado (seu ponteiro bruto interno já tem um destino) e quiser reinserir para apontar para um objeto diferente, primeiro você precisará atribuí-lo nullptr , conforme mostrado no exemplo de código abaixo. Se você não fizer isso, um com_ptr já preenchido chamará o problema à sua atenção (quando você chamar com_ptr::put ou com_ptr::put_void) disparando uma asserção de que seu ponteiro interno não é nulo.

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()));

Tratar códigos de erro HRESULT

Para verificar o valor de um HRESULT retornado de uma função COM e gerar uma exceção no caso de representar um código de erro, chame winrt::check_hresult.

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

Funções COM que recebem um ponteiro para uma interface específica

Você pode chamar a função com_ptr::get para passar sua com_ptr para uma função que usa um ponteiro de interface específico do mesmo tipo.

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

Funções COM que recebem um ponteiro de interface IUnknown

Você pode usar com_ptr::get para passar seu com_ptr para uma função que usa um ponteiro de interface IUnknown .

Você pode usar a função gratuita winrt::get_unknown para retornar o endereço de (em outras palavras, um ponteiro para) a interface IUnknown bruta subjacente de um objeto de um tipo projetado. Em seguida, você pode passar esse endereço para uma função que usa um ponteiro de interface IUnknown .

Para obter informações sobre tipos projetados, consulte Consumir APIs com C++/WinRT.

Para obter um exemplo de código de get_unknown, consulte winrt::get_unknown ou a listagem de código-fonte completo de um aplicativo Direct2D mínimo neste tópico.

Passando e retornando ponteiros inteligentes COM

Uma função que recebe um ponteiro inteligente COM na forma de winrt::com_ptr deve recebê-lo por referência constante ou por referência.

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

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

Uma função que retorna um winrt::com_ptr deve fazer isso por valor.

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

Consultar um ponteiro inteligente COM para uma interface diferente

Você pode usar a função com_ptr::as para consultar um ponteiro inteligente COM para uma interface diferente. A função gerará uma exceção se a consulta não for bem-sucedida.

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

Como alternativa, use com_ptr::try_as, que retorna um valor que você pode comparar com nullptr para verificar se a consulta foi bem-sucedida.

Listagem completa de código-fonte de um aplicativo Direct2D mínimo

Note

Para obter informações sobre como configurar Visual Studio para o desenvolvimento do C++/WinRT, incluindo a instalação e o uso da VSIX (Extensão de Visual Studio) do C++/WinRT e do pacote NuGet (que, juntos, fornecem modelo de projeto e suporte de build)— consulte Visual Studio suporte para C++/WinRT.

Se você quiser compilar e executar este exemplo de código-fonte, primeiro instale (ou atualize para) a versão mais recente da VSIX (Extensão de Visual Studio) do C++/WinRT; consulte a observação acima. Em seguida, em Visual Studio, crie um novo Aplicativo Core (C++/WinRT). Direct2D é um nome razoável para o projeto, mas você pode nomeá-lo como quiser. Direcione a versão mais recente disponível (ou seja, não versão prévia) do SDK do Windows.

Etapa 1. Editar pch.h

Abra pch.h e adicione #include <unknwn.h> imediatamente após incluir windows.h. Isso ocorre porque estamos usando winrt::get_unknown. É uma boa ideia usar #include <unknwn.h> explicitamente sempre que você usar winrt::get_unknown, mesmo que esse cabeçalho tenha sido incluído por outro cabeçalho.

Note

Se você omitir essa etapa, verá o erro de build 'get_unknown': identificador não encontrado.

Etapa 2. Editar App.cpp

Abra App.cpp, exclua todo o conteúdo e cole na listagem abaixo.

O código a seguir usa a função winrt::com_ptr::capture sempre que possível. WINRT_ASSERT é uma definição de macro e se expande para _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>());
}

Trabalhando com tipos COM, como BSTR e VARIANT

Como você pode ver, o C++/WinRT fornece suporte para implementar e chamar interfaces COM. Para usar tipos COM, como BSTR e VARIANT, recomendamos que você use wrappers fornecidos pelas bibliotecas de implementação de Windows (WIL), como wil::unique_bstr e wil::unique_variant (que gerenciam tempos de vida de recursos).

O WIL substitui estruturas como a ATL (Biblioteca de Modelos Ativos) e o suporte COM do compilador do Visual C++. E recomendamos isso em vez de escrever seus próprios wrappers ou usar tipos COM como BSTR e VARIANT diretamente (juntamente com as APIs apropriadas).

Evitando colisões de namespace

É uma prática comum no C++/WinRT, como demonstra a listagem de código neste tópico, usar as diretivas de uso liberalmente. Em alguns casos, porém, isso pode levar ao problema de importar nomes colidindo para o namespace global. Veja um exemplo.

C++/WinRT contém um tipo chamado winrt::Windows::Foundation::IUnknown; enquanto COM define um tipo chamado ::IUnknown. Portanto, considere o código a seguir, em um projeto C++/WinRT que consome cabeçalhos COM.

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

O nome não qualificado IUnknown entra em conflito no espaço de nomes global, resultando no erro do compilador ambiguous symbol. Em vez disso, você pode isolar a versão C++/WinRT do nome no namespace winrt , assim.

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

Ou, se você quiser a conveniência de using namespace winrt, então você pode. Você só precisa qualificar a versão global de IUnknown, da seguinte forma.

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

Naturalmente, isso funciona com qualquer namespace C++/WinRT.

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

Em seguida, você pode consultar winrt::Windows::Storage::StorageFile, por exemplo, como apenas winrt::StorageFile.

APIs importantes