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, não acesse seus membros de dados diretamente; em vez disso, chame as funções de acessador e modificador em uma interface.

Para ser mais específico, estamos falando sobre como interagir com os ponteiros da 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 gravar em um ponteiro bruto subjacente do 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 reinterprete a transmissão disso para IUnknown.

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

Restabelecer um winrt::com_ptr

Important

Se você tiver um winrt::com_ptr que já esteja estabelecido (seu ponteiro bruto interno já tem um destino) e deseja restabelecê-lo para apontar para outro objeto, primeiramente, será preciso atribuir nullptr a ele, conforme mostrado no exemplo de código abaixo. Se não o fizer, um com_ptr já estabelecido chamará sua atenção para o problema (quando você chamar com_ptr::put ou com_ptr::put_void) afirmando 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

É possível 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 usando um ponteiro inteligente COM na forma de um winrt::com_ptr deve fazer isso por referência de 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 é recomendável substituir seus próprios wrappers ou usar tipos de COM como BSTR e VARIANT na respectiva forma bruta (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 quiser a conveniência de using namespace winrt, 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