Note
本文中的程式碼範例使用 UWP 核心應用程式(IFrameworkView)應用模型。 在 WinUI 3 桌面應用程式中,從 Microsoft::UI::Xaml::Application 衍生一個類別,並呼叫 Application::Start(...) 作為應用程式的入口,並使用 DispatcherQueue 取代 CoreDispatcher。 這裡展示的 COM/Direct2D 概念和模式同樣適用於 WinUI 3 應用程式。
你可以利用 C++/WinRT 函式庫的功能來整合 COM 元件,例如 DirectX API 的高效能 2D 與 3D 圖形。 C++/WinRT 是使用 DirectX 最簡單的方式,且不犧牲效能。 本主題使用一個 Direct2D 程式碼範例,示範如何使用 C++/WinRT 來處理 COM 類別與介面。 當然,你可以在同一個 C++/WinRT 專案中混合使用 COM 和 Windows 執行階段 程式設計。
在本主題的結尾,你會看到一份簡約 Direct2D 應用程式的完整原始碼清單。 我們將從這些程式碼中擷取內容,並用它們來說明如何利用 C++/WinRT 函式庫的各種功能來處理 COM 元件。
COM 智慧指標(winrt::com_ptr)
用 COM 程式設計時,你是直接處理介面,而不是物件(這在 Windows 執行階段 API 的幕後也是如此,因為它是 COM 的演進)。 舉例來說,要呼叫 COM 類別的函式,你啟動該類別,取得介面,然後呼叫該介面上的函式。 要存取物件的狀態,你不會直接存取其資料成員;相反地,你是在介面上呼叫存取器和變異器函式。
更具體來說,我們談的是與介面 指標的互動。 因此,我們受益於 C++/WinRT 中 COM 智慧指標型別的存在—— winrt::com_ptr 型態。
#include <d2d1_1.h>
...
winrt::com_ptr<ID2D1Factory1> factory;
上述程式碼展示了如何宣告未初始化的智慧指標到 ID2D1Factory1 COM 介面。 智慧型指標尚未初始化,因此尚未指向任何實際物件所屬的 ID2D1Factory1 介面(事實上,它根本沒有指向任何介面)。 但它確實有這樣做的潛力;而且(作為智慧指標)它能夠透過 COM 參考計數來管理其所指向介面之擁有物件的生命週期,並作為呼叫該介面上函式的媒介。
COM函式將介面指標回傳為空值
你可以呼叫 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,但要將其重新詮釋轉型為 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 錯誤代碼
若要檢查從 COM 函式回傳的 HRESULT 值,並在該結果代表錯誤碼時拋出例外,請呼叫 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 介面指標的函式。
如需投影型別的相關資訊,請參閱使用 C++/WinRT 取用 API。
get_unknown範例請參見 winrt::get_unknown,或本主題中最小 Direct2D 應用程式的完整原始碼列表。
傳遞與回傳 COM 智慧指標
一個以 winrt::com_ptr 形式取得 COM 智慧指標的函式,應該透過持續引用或引用來執行。
... 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 建立一個新的 Core App(C++/WinRT)。
Direct2D 這是個合理的專案名稱,但你可以隨意命名。 鎖定最新普遍可用(非預覽版)的 Windows SDK 版本。
步驟 1。 編輯 pch.h
打開 pch.h,並在包含 #include <unknwn.h>後立即加入windows.h。 這是因為我們用的是 winrt::get_unknown。 每次使用 #include <unknwn.h> 時,即使該標頭已被其他標頭包含,也應該明確標示。
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(管理資源生命週期)。
WIL 取代了如主動模板函式庫(Active Template Library,ATL)以及 Visual C++ 編譯器的 COM 支援等框架。 我們也建議它勝過自行撰寫包裝器,或使用像 BSTR 和 VARIANT 這類原始形式的 COM 類型(搭配適當的 API)。
避免命名空間碰撞
在 C++/WinRT 中,正如本主題程式碼列表所示,廣泛使用 useing-directive 是常見做法。 不過在某些情況下,這可能導致將碰撞名稱匯入全域命名空間的問題。 以下是範例。
C++/WinRT 包含一個名為 winrt::Windows::Foundation::IUnknown 的型別;而 COM 定義了一個名為 ::IUnknown 的型別。 所以請考慮以下程式碼,在一個使用 COM 標頭的 C++/WinRT 專案中。
using namespace winrt::Windows::Foundation;
...
void MyFunction(IUnknown*); // error C2872: 'IUnknown': ambiguous symbol
未限定名稱 IUnknown 在全域命名空間中發生衝突,因此會產生 ambiguous symbol 編譯器錯誤。 相反地,你可以將 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。