Использование COM-компонентов в C++/WinRT

Note

Примеры кода в этой статье используют модель приложения UWP Core App (IFrameworkView). В настольном приложении WinUI 3 унаследуйте класс от Microsoft::UI::Xaml::Application и вызовите Application::Start(...) в качестве точки входа приложения, а также используйте DispatcherQueue вместо CoreDispatcher. Основные понятия и шаблоны COM/Direct2D, показанные здесь, применяются одинаково к приложениям WinUI 3.

Вы можете использовать средства библиотеки C++/WinRT для использования COM-компонентов, таких как высокопроизводительная графика 2-D и 3-D API DirectX. C++/WinRT — это самый простой способ использования DirectX без ущерба для производительности. В этом разделе на примере кода Direct2D показано, как использовать C++/WinRT для работы с COM-классами и интерфейсами. Вы, конечно, можете сочетать программирование с COM и для среда выполнения Windows в рамках одного проекта C++/WinRT.

В конце этого раздела вы найдете полный список исходного кода минимального приложения Direct2D. Мы возьмём фрагменты из этого кода и с их помощью покажем, как использовать COM-компоненты в C++/WinRT, задействуя различные возможности библиотеки C++/WinRT.

Интеллектуальные указатели COM (winrt::com_ptr)

При программе с помощью COM вы работаете непосредственно с интерфейсами, а не с объектами (это также верно для api среда выполнения Windows, которые являются эволюцией COM). Чтобы вызвать функцию COM-класса, нужно, например, активировать класс, получить интерфейс, а затем вызвать функции через этот интерфейс. Чтобы получить доступ к состоянию объекта, вы напрямую не обращаетесь к его членам данных; Вместо этого вы вызываете функции доступа и мутатора в интерфейсе.

Чтобы быть более конкретным, мы говорим о взаимодействии с указателями интерфейса. И для этого мы пользуемся наличием типа смарт-указателя COM в C++/WinRT — тип winrt::com_ptr .

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

Приведенный выше код показывает, как объявить неинициализированный смарт-указатель на COM-интерфейс ID2D1Factory1 . Смарт-указатель неинициализирован, поэтому он еще не указывает на интерфейс ID2D1Factory1 , принадлежащий любому фактическому объекту (он вообще не указывает на интерфейс). Но у него есть потенциал для этого; и, будучи умным указателем, он способен с помощью подсчёта ссылок COM управлять временем жизни объекта-владельца интерфейса, на который указывает, а также служить средством вызова функций этого интерфейса.

ФУНКЦИИ COM, возвращающие указатель интерфейса в виде void

Вы можете вызвать функцию com_ptr::put_void, чтобы записать значение в лежащий в основе неинициализированного смарт-указателя сырой указатель.

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

Приведенный выше код вызывает функцию D2D1CreateFactory , которая возвращает указатель интерфейса ID2D1Factory1 через его последний параметр, имеющий тип void** . Многие функции COM возвращают void**. Для таких функций используйте com_ptr::put_void, как показано.

ФУНКЦИИ COM, возвращающие определенный указатель интерфейса

Функция D3D11CreateDevice возвращает указатель интерфейса ID3D11Device через третий из последнего параметра, который имеет тип ID3D11Device** . Для функций, которые возвращают указатель на определённый интерфейс такого типа, используйте com_ptr::put.

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

Пример кода в предыдущем разделе показывает, как вызвать функцию D2D1CreateFactory в исходном виде. Но на самом деле, когда пример кода в этом разделе вызывает D2D1CreateFactory, он использует шаблон вспомогательной функции, который оборачивает низкоуровневый API, поэтому этот пример кода фактически использует com_ptr::put.

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

ФУНКЦИИ COM, возвращающие указатель интерфейса в качестве IUnknown

Функция DWriteCreateFactory возвращает указатель интерфейса фабрики DirectWrite с помощью последнего параметра, который имеет тип IUnknown . Для такой функции используйте com_ptr::put, но выполните reinterpret_cast к IUnknown.

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

Переустановить winrt::com_ptr

Это важно

Если у вас есть winrt::com_ptr, который уже инициализирован (то есть его внутренний сырой указатель уже указывает на некоторый объект), и вы хотите переназначить его так, чтобы он указывал на другой объект, то сначала нужно присвоить ему nullptr, как показано в примере кода ниже. Если вы этого не сделаете, уже инициализированный com_ptr укажет на проблему (при вызове com_ptr::put или com_ptr::put_void), сгенерировав утверждение о том, что его внутренний указатель не равен 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()));

Обработка кодов ошибок HRESULT

Чтобы проверить значение HRESULT, возвращенное из com-функции, и вызвать исключение в случае, если он представляет код ошибки, вызовите winrt::check_hresult.

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

ФУНКЦИИ COM, использующие определенный указатель интерфейса

Вы можете вызвать функцию com_ptr::get , чтобы передать com_ptr в функцию, которая принимает определенный указатель интерфейса одного типа.

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

COM-функции, принимаюющие указатель интерфейса IUnknown

Вы можете использовать com_ptr::get, чтобы передать объект com_ptr в функцию, которая принимает указатель на интерфейс IUnknown.

Вы можете использовать бесплатную функцию winrt::get_unknown , чтобы вернуть адрес (другими словами, указатель на базовый необработанный интерфейс IUnknown объекта проецируемого типа. Затем этот адрес можно передать в функцию, которая принимает указатель интерфейса IUnknown .

Сведения о проецируемых типах см. в разделе "Использование API с помощью C++/WinRT".

Пример кода get_unknown см. в статье winrt::get_unknown или полный список исходного кода минимального приложения Direct2D в этом разделе.

Передача и возврат смарт-указателей COM

Функция, принимаюая смарт-указатель COM в виде winrt::com_ptr должна делать это по постоянной ссылке или по ссылке.

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

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

Функция, возвращающая winrt::com_ptr, должна возвращать его по значению.

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

Запрос интеллектуального указателя COM для другого интерфейса

Вы можете использовать функцию com_ptr::as для запроса интеллектуального указателя COM для другого интерфейса. Функция создает исключение, если запрос не выполнен.

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

Кроме того, используйте com_ptr::try_as, которое возвращает значение, которое можно проверить на nullptr наличие успешного выполнения запроса.

Полный список исходного кода минимального приложения Direct2D

Note

Сведения о настройке Visual Studio для разработки C++/WinRT, включая установку и использование расширения C++/WinRT Visual Studio (VSIX) и пакета NuGet (которые вместе предоставляют поддержку шаблона проекта и сборки),см. Visual Studio поддержку C++/WinRT.

Если вы хотите создать и запустить этот пример исходного кода, сначала установите (или обновите до) последнюю версию расширения C++/WinRT Visual Studio (VSIX); см. примечание выше. Затем в Visual Studio создайте новое основное приложение (C++/WinRT). Direct2D является разумным именем для проекта, но его можно назвать как угодно. Нацелимся на последнюю общедоступную версию (т. е. не предварительную) пакета SDK Windows.

Шаг 1. Измените pch.h.

Откройте pch.hи добавьте #include <unknwn.h> сразу после включения windows.h. Это связано с тем, что мы используем winrt::get_unknown. #include <unknwn.h> Рекомендуется явно использовать winrt::get_unknown, даже если этот заголовок был включен другим заголовком.

Note

Если этот шаг не указан, появится ошибка сборки "get_unknown": идентификатор не найден.

Шаг 2. Измените App.cpp.

Откройте App.cpp, удалите всё его содержимое и вставьте туда приведённый ниже листинг.

Приведенный ниже код использует функцию winrt::com_ptr::capture , где это возможно. WINRT_ASSERT — это определение макроса, и оно расширяется до _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>());
}

Работа с типами COM, такими как BSTR и VARIANT

Как видно, C++/WinRT обеспечивает поддержку реализации и вызова COM-интерфейсов. Для использования типов COM, таких как BSTR и VARIANT, рекомендуется использовать оболочки, предоставляемые библиотеками реализации Windows (WIL), например wil::unique_bstr и wil::unique_variant (которые управляют временем существования ресурсов).

УИЛ заменяет платформы, такие как библиотека активных шаблонов (ATL) и com-поддержка компилятора Visual C++. И мы рекомендуем этот подход вместо написания собственных оболочек или прямого использования типов COM, таких как BSTR и VARIANT, вместе с соответствующими API.

Предотвращение конфликтов пространства имен

В C++/WinRT обычно широко используются директивы using, как показывает листинг кода в этом разделе. Однако в некоторых случаях это может привести к проблеме импорта конфликтующих имён в глобальное пространство имён. Приведем пример.

C++/WinRT содержит тип winrt::Windows::Foundation::IUnknown; в то время как COM определяет тип ::IUnknown. Поэтому рассмотрим следующий код в проекте C++/WinRT, который использует заголовки COM.

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

Неквалифицированное имя IUnknown вступает в конфликт в глобальном пространстве имен, что приводит к ошибке компилятора неоднозначный символ. Вместо этого можно изолировать версию имени C++/WinRT в пространстве имен winrt , как показано ниже.

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

Или, если вы хотите удобство using namespace winrt, то можете. Вам просто нужно указать полное глобальное имя IUnknown вот так.

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

Естественно, это работает с любым пространством имен C++/WinRT.

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

Затем можно обращаться, например, к winrt::Windows::Storage::StorageFile просто как к winrt::StorageFile.

Важные API