在上一期專欄中,我們探討了 Windows 運行時 (WinRT) 應用程式模型,並示範如何僅使用標準 C++ 和經典 COM 來編寫 Windows 應用商店或 Windows Phone 應用程式,而不依賴 C++/CX 或 C# 等語言投射。這種繞過抽象層的能力不僅強大,更是深入理解技術運作機制的最佳途徑。
先前我也介紹過 Direct2D 1.1 及其在桌面應用程式中的繪圖應用,以及能大幅簡化 C++ 中 DirectX 程式設計的 dx.h 函式庫。上一期的程式碼已足以實作基於 CoreWindow 的應用程式架構,但尚缺乏呈現功能。本月我將展示如何在這個基礎框架上加入呈現支援。
WinRT 應用程式模型針對 DirectX 呈現進行了優化。無論目標平台是桌面還是 Windows 運行時,實際的 Direct2D 和 Direct3D 繪圖指令大體相同,但仍存在細微差異,且完整整合的過程截然不同。我將延續上一期內容,示範如何在 CoreWindow 基礎的 WinRT 應用程式中顯示像素。
為了正確支援呈現,視窗必須感知特定事件:視窗可見性變更、大小變更,以及使用者選擇的邏輯顯示 DPI 設定變更。這些事件透過 COM 介面回呼報告給應用程式。ICoreWindow 介面提供註冊 VisibilityChanged 和 SizeChanged 事件的方法,對應的處理介面分別為:
typedef ITypedEventHandler<CoreWindow *, VisibilityChangedEventArgs *> IVisibilityChangedEventHandler; typedef ITypedEventHandler<CoreWindow *, WindowSizeChangedEventArgs *> IWindowSizeChangedEventHandler;
第三個介面 IDisplayPropertiesEventHandler 已預先定義,只需包含相關標頭檔:
#include <Windows.Graphics.Display.h> using namespace ABI::Windows::Graphics::Display;
更新 SampleWindow 類別,繼承這三個介面,並記得更新 QueryInterface 實作以支援它們。雖然 WinRT 不假設 IFrameworkView 實作也實作這些回呼介面,但我們需在 Load 方法中明確註冊事件:
EventRegistrationToken token; HR(m_window->add_VisibilityChanged(this, &token)); HR(m_window->add_SizeChanged(this, &token));
LogicalDpiChanged 事件則透過 IDisplayPropertiesStatics 介面註冊:
ComPtr<IDisplayPropertiesStatics> m_displayProperties;
m_displayProperties = GetActivationFactory<IDisplayPropertiesStatics>(
RuntimeClass_Windows_Graphics_Display_DisplayProperties);
HR(m_displayProperties->add_LogicalDpiChanged(this, &token));
需要標準的裝置資源處理常式集合:
void CreateDeviceIndependentResources() {}
void CreateDeviceSizeResources() {}
void CreateDeviceResources() {}
void ReleaseDeviceResources() {}
第一個方法建立非裝置特定資源,接著兩個建立裝置特定資源(區分視窗大小相關與否),最後釋放所有裝置資源。這四個方法提供了管理呈現資源的獨立切入點。
#include "dx.h" using namespace KennyKerr; using namespace KennyKerr::Direct2D;
每個 Direct2D 應用程式都從 Direct2D 工廠開始:
Factory1 m_factory;
在 Load 方法最後建立工廠與裝置無關資源:
m_factory = CreateFactory(); CreateDeviceIndependentResources();
Run 方法需根據視窗可見性決定呈現策略。新增成員變數追蹤可見狀態:
bool m_visible;
SampleWindow() : m_visible(true) {}
重寫 Run 方法實作動態呈現迴圈(圖 1):
auto __stdcall Run() -> HRESULT override {
ComPtr<ICoreDispatcher> dispatcher;
HR(m_window->get_Dispatcher(dispatcher.GetAddressOf()));
while (true) {
if (m_visible) {
Render();
HR(dispatcher->ProcessEvents(CoreProcessEventsOption_ProcessAllIfPresent));
} else {
HR(dispatcher->ProcessEvents(CoreProcessEventsOption_ProcessOneAndAllPending));
}
}
return S_OK;
}
視窗可見時持續呈現並處理所有待處理事件;不可見時則阻塞等待訊息到達。IVisibilityChangedEventHandler 的 Invoke 方法更新可見狀態:
auto __stdcall Invoke(ICoreWindow *, IVisibilityChangedEventArgs * args) -> HRESULT override {
unsigned char visible;
HR(args->get_Visible(&visible));
m_visible = 0 != visible;
return S_OK;
}
Render 方法按需建立呈現堆疊並執行繪圖指令(圖 2):
void Render() {
if (!m_target) {
// 準備呈現目標...
}
m_target.BeginDraw();
Draw();
m_target.EndDraw();
auto const hr = m_swapChain.Present();
if (S_OK != hr && DXGI_STATUS_OCCLUDED != hr) {
ReleaseDevice();
}
}
需要的成員變數:
DeviceContext m_target; Dxgi::SwapChain1 m_swapChain;
ReleaseDevice 方法負責釋放資源,對於可靠處理裝置遺失至關重要:
void ReleaseDevice() {
m_target.Reset();
m_swapChain.Reset();
ReleaseDeviceResources();
}
準備呈現目標包含建立 Direct3D 裝置、Direct2D 裝置與裝置內容、DXGI 工廠、交換鏈,以及 Direct2D 位圖包裝交換鏈後台緩衝區(圖 3):
void Render() {
if (!m_target) {
auto device = Direct3D::CreateDevice();
m_target = m_factory.CreateDevice(device).CreateDeviceContext();
auto dxgi = device.GetDxgiFactory();
m_swapChain = dxgi.CreateSwapChainForCoreWindow(device, m_window.Get());
CreateDeviceSwapChainBitmap();
float dpi;
HR(m_displayProperties->get_LogicalDpi(&dpi));
m_target.SetDpi(dpi);
CreateDeviceResources();
CreateDeviceSizeResources();
}
// 繪圖與呈現...
}
CreateDeviceSwapChainBitmap 方法建立包裝交換鏈後台緩衝區的 Direct2D 位圖:
void CreateDeviceSwapChainBitmap() {
BitmapProperties1 props(BitmapOptions::Target | BitmapOptions::CannotDraw,
PixelFormat(Dxgi::Format::B8G8R8A8_UNORM, AlphaMode::Ignore));
auto bitmap = m_target.CreateBitmapFromDxgiSurface(m_swapChain, props);
m_target.SetTarget(bitmap);
}
IDisplayPropertiesEventHandler 的 Invoke 方法處理 DPI 變更:
auto __stdcall Invoke(IInspectable *) -> HRESULT override {
if (m_target) {
float dpi;
HR(m_displayProperties->get_LogicalDpi(&dpi));
m_target.SetDpi(dpi);
CreateDeviceSizeResources();
Render();
}
return S_OK;
}
IWindowSizeChangedEventHandler 處理視窗大小變更:
auto __stdcall Invoke(ICoreWindow *, IWindowSizeChangedEventArgs *) -> HRESULT override {
if (m_target) {
ResizeSwapChainBitmap();
Render();
}
return S_OK;
}
ResizeSwapChainBitmap 需謹慎處理:先釋放對緩衝區的引用,再調整交換鏈緩衝區大小(圖 4):
void ResizeSwapChainBitmap() {
m_target.SetTarget();
if (S_OK == m_swapChain.ResizeBuffers()) {
CreateDeviceSwapChainBitmap();
CreateDeviceSizeResources();
} else {
ReleaseDevice();
}
}
在 CreateDeviceResources 建立純色筆刷:
SolidColorBrush m_brush; m_brush = m_target.CreateSolidColorBrush(Color(1.0f, 0.0f, 0.0f));
Draw 方法清除背景並繪製紅色矩形:
m_target.Clear(Color(1.0f, 1.0f, 1.0f)); RectF rect(100.0f, 100.0f, 200.0f, 200.0f); m_target.DrawRectangle(rect, m_brush);
確保裝置遺失時正確釋放筆刷:
void ReleaseDeviceResources() {
m_brush.Reset();
}
這就是使用 DirectX 呈現基於 CoreWindow 應用程式的完整步驟。得益於 dx.h 函式庫,DirectX 相關程式碼已大幅簡化,雖然仍有大量 COM 介面實作的樣板程式碼,但 C++/CX 可進一步簡化 WinRT API 使用。