天天看點

借助 C++ 進行 Windows 開發 - Windows 運作時的呈現

原文位址:http://msdn.microsoft.com/zh-cn/magazine/dn451437.aspx

我的上一個專欄中讨論了 Windows 運作時 (WinRT) 應用程式模型 (msdn.microsoft.com/magazine/dn342867)。 我示範了如何通過标準 C++ 和經典 COM 來編寫 Windows 應用商店或 Windows Phone 應用程式,其中僅使用了一些 WinRT API 函數。 毫無疑問,您不必使用 C++/CX 或 C# 這樣的語言投射。 能夠繞過這些抽象概念是一種強大的功能,同時也是一種了解這項技術工作方式的很好的方法。

我在 2013 年 5 月的專欄中介紹了 Direct2D 1.1 并示範了如何使用它在桌面應用程式中進行呈現 (msdn.microsoft.com/magazine/dn198239)。 接下來的專欄介紹了 dx.codeplex.com 上提供的 dx.h 庫,這可以大幅簡化 C++ 中的 DirectX 程式設計 (msdn.microsoft.com/magazine/dn201741)。

上個專欄中的代碼對于實作基于 CoreWindow 的應用程式已經足夠,但未提供任何呈現。

本月,我将示範如何利用這種基本的架構并添加呈現支援。 WinRT 應用程式模型針對使用 DirectX 呈現進行了優化。 我将向您示範,如何利用在之前專欄中學到的有關 Direct2D 和 Direct3D 呈現的内容,将其應用到基于 CoreWindow 的 WinRT 應用程式,具體而言,通過 dx.h 庫使用 Direct2D 1.1。 大多數情況下,不論您的目标是桌面還是 Windows 運作時,需要編寫的實際 Direct2D 和 Direct3D 繪制指令是相同的。 但是,其中有一些細微的差别,當然,使其完全運轉起來從一開始就有很大差别。 是以,我将繼續上一次的内容,示範如何在螢幕上顯示一些像素!

為了正确支援呈現,視窗必須能夠意識到特定事件。 至少這包括視窗的可見性和大小的更改,以及對使用者所選擇的邏輯顯示 DPI 配置的更改。 在上次專欄中介紹的 Activated 事件中,這些新事件都通過 COM 接口回調報告給應用程式。 ICoreWindow 接口提供注冊 VisibilityChanged 和 SizeChanged 事件的方法,但首先我需要實作相應的處理程式。 我需要實作的兩個 COM 接口與 Activated 事件處理程式及其 Microsoft 接口定義語言 (MIDL) 生成的類模闆非常相似:

  1.           typedef ITypedEventHandler<CoreWindow *, VisibilityChangedEventArgs *>
  2.   IVisibilityChangedEventHandler;
  3. typedef ITypedEventHandler<CoreWindow *, WindowSizeChangedEventArgs *>
  4.   IWindowSizeChangedEventHandler;

接下來必須實作的 COM 接口稱為 IDisplayPropertiesEventHandler,謝天謝地這個接口已經定義了。 我隻需将相關的頭檔案包括在其中:

  1.           #include <Windows.Graphics.Display.h>

此外,相關類型在以下命名空間中定義:

  1.           using namespace ABI::Windows::Graphics::Display;

根據這些定義,我可以更新上次專欄中介紹的 SampleWindow 類,也從這三個接口繼承:

  1.           struct SampleWindow :
  2.   ...
  3.           IVisibilityChangedEventHandler,
  4.   IWindowSizeChangedEventHandler,
  5.   IDisplayPropertiesEventHandler

同時還需要記住更新我的 QueryInterface 實作以訓示對這些接口的支援。 這些内容将讓您自行完成。 當然,如我上次所說,Windows 運作時并不關心在哪裡實作這些 COM 接口回調。 它遵循的原則是,Windows 運作時不假定我的應用程式 IFrameworkView(SampleWindow 類實作的主要接口)也實作這些回調接口。 是以,雖然 QueryInterface 确實會正确處理這些接口的查詢,不過 Windows 運作時不會為它們進行查詢。 相反,我需要注冊相應事件,而最佳位置是在 IFrameworkView Load 方法的實作中。 提醒一下,Load 方法是應該将所有代碼粘貼到這裡的方法,以便準備應用程式進行初始呈現。 接下來在 Load 方法中注冊 VisibilityChanged 和 SizeChanged 事件:

  1.           EventRegistrationToken token;
  2. HR(m_window->add_VisibilityChanged(this, &token));
  3. HR(m_window->add_SizeChanged(this, &token));

這會明确告訴 Windows 運作時在哪裡查找前兩個接口實作。 第三個也是最後一個接口,它針對 LogicalDpiChanged 事件,但此事件注冊由 IDisplayPropertiesStatics 接口提供。 此靜态接口由 WinRT DisplayProperties 類實作。 我隻需使用 GetActivationFactory 函數模闆來擷取它(在我最近的專欄中可以找到 GetActivationFactory 的實作):

  1.           ComPtr<IDisplayPropertiesStatics> m_displayProperties;
  2. m_displayProperties = GetActivationFactory<IDisplayPropertiesStatics>(
  3.   RuntimeClass_Windows_Graphics_Display_DisplayProperties);

成員變量保留此接口指針,在視窗的生命周期中,我需要在不同點上調用它。 現在,我可以在 Load 方法中注冊 LogicalDpiChanged 事件:

  1.           HR(m_displayProperties->add_LogicalDpiChanged(this, &token));

稍後将傳回到這三個接口的實作。 現在該是準備 DirectX 基礎結構的時候了。 我将需要标準的裝置資源處理程式集,這些在以前的專欄中已經多次讨論過:

  1.           void CreateDeviceIndependentResources() {}
  2. void CreateDeviceSizeResources() {}
  3. void CreateDeviceResources() {}
  4. void ReleaseDeviceResources() {}

在第一個方法中,我可以建立或加載任何并非特定于底層 Direct3D 呈現裝置的資源。 接下來兩個用于建立特定于裝置的資源。 最好是将特定于視窗大小的資源與并非特定于視窗大小的資源分隔開。 最後,必須釋放所有裝置資源。 剩餘的 DirectX 基礎結構根據應用程式的特定需求,依賴于應用程式來正确實作這四個方法。 它在應用程式中為我提供單獨的點來管理呈現資源以及這些資源的有效建立和回收。

現在我可以引入 dx.h 來處理所有的 DirectX 繁重任務:

  1.           #include "dx.h"

每個 Direct2D 應用程式都以 Direct2D 工廠開始:

  1.           Factory1 m_factory;

您可以在 Direct2D 命名空間中找到此項,通常我采用以下方法包含它:

  1.           using namespace KennyKerr;
  2. using namespace KennyKerr::Direct2D;

dx.h 庫為 Direct2D、Direct­Write、Direct3D 和 Microsoft DirectX 圖形基礎結構 (DXGI) 等提供了獨立的命名空間。 我的大部分應用程式會頻繁使用 Direct2D,是以這對我而言是頗有意義。 當然,您可以采用任何對您的應用程式有意義的方法來管理命名空間。

m_factory 成員變量表示 Direct2D 1.1 工廠。 它用于建立呈現目标,并根據需要建立其他多種與裝置無關的資源。 我将建立 Direct2D 工廠,然後可以在 Load 方法的最後一步中建立與裝置無關的任意資源:

  1.           m_factory = CreateFactory();
  2. CreateDeviceIndependentResources();

Load 方法傳回後,WinRT CoreApplication 類立即調用 IFrameworkView Run 方法。

在我的上個專欄中,SampleWindow Run 方法的實作通過在 CoreWindow 排程程式上調用 ProcessEvents 方法即可阻止。 如果應用程式隻需要基于各種事件執行不頻繁的呈現,采用這種方法阻止便已足夠。 可能您要實作一個遊戲,或者您的應用程式隻需要一些高分辨率的動畫。 另一種極端情況是使用連續的動畫循環,不過您可能希望更為智能化一點。 我将實作一些折中處理這兩種情況的内容。 首先,我添加一個成員變量以便跟蹤視窗是否可見。 這可以在視窗實際上對使用者不可見時限制呈現:

  1.           bool m_visible;
  2. SampleWindow() : m_visible(true) {}

接下來,我可以重寫 Run 方法,如圖 1 中所示。

圖 1:動态呈現循環

  1.           auto __stdcall Run() -> HRESULT override
  2. {
  3.   ComPtr<ICoreDispatcher> dispatcher;
  4.   HR(m_window->get_Dispatcher(dispatcher.GetAddressOf()));
  5.   while (true)
  6.   {
  7.     if (m_visible)
  8.     {
  9.       Render();
  10.       HR(dispatcher->
  11.         ProcessEvents(CoreProcessEventsOption_ProcessAllIfPresent));
  12.     }
  13.     else
  14.     {
  15.       HR(dispatcher->
  16.         ProcessEvents(CoreProcessEventsOption_ProcessOneAndAllPending));
  17.     }
  18.   }
  19.   return S_OK;
  20. }

與之前一樣,Run 方法接收 CoreWindow 排程程式。 然後,它進入無限循環,連續呈現和處理隊列中可能存在的任何視窗消息(Windows 運作時稱之為“事件”)。 但是,如果視窗不可見,則将阻止,直至有消息到達。 應用程式如何得知視窗可見性的變化? 這正是使用 IVisibilityChangedEventHandler 接口的原因。 現在,我可以實作其 Invoke 方法以更新 m_visible 成員變量:

  1.           auto __stdcall Invoke(ICoreWindow *,
  2.   IVisibilityChangedEventArgs * args) -> HRESULT override
  3. {
  4.   unsigned char visible;
  5.   HR(args->get_Visible(&visible));
  6.   m_visible = 0 != visible;
  7.   return S_OK;
  8. }

MIDL 生成的接口使用 unsigned char 作為可移植的布爾資料類型。 我隻需使用提供的 IVisibilityChangedEventArgs 接口指針擷取視窗目前的可見性,然後相應地更新成員變量。 在視窗隐藏或顯示時将引發此事件,這比為桌面應用程式實作這此事件略微簡單,因為在桌面上需要考慮多種情形,包括應用程式關閉和電源管理,更不用說切換視窗。

接下來,我需要實作通過 Run 方法調用的 Render 方法,如圖 1 中所示。 在此時按需建立呈現堆棧并且實際執行繪制指令。 圖 2 中顯示了基本架構。

圖 2 Render 方法摘要

  1.           void Render()
  2. {
  3.   if (!m_target)
  4.   {
  5.     // Prepare render target ...
  6.           }
  7.   m_target.BeginDraw();
  8.   Draw();
  9.   m_target.EndDraw();
  10.   auto const hr = m_swapChain.Present();
  11.   if (S_OK != hr && DXGI_STATUS_OCCLUDED != hr)
  12.   {
  13.     ReleaseDevice();
  14.   }
  15. }

Render 方法應該比較眼熟。 它的基本表單與之前在 Direct2D 1.1 中概述的相同。 開始時根據需要建立呈現目标。 後面緊跟的是實際繪制指令,位于對 BeginDraw 和 EndDraw 的調用之間。 由于呈現目标是 Direct2D 裝置上下文,實際擷取呈現在螢幕上的像素涉及到呈現交換鍊。 說到這一點,我需要添加呈現 Direct2D 1.1 裝置上下文的 dx.h 類型以及交換鍊的 DirectX 11.1 版本。 後者在 Dxgi 命名空間中提供:

  1.           DeviceContext m_target;
  2. Dxgi::SwapChain1 m_swapChain;

最後,在呈現失敗時,Render 方法将調用 ReleaseDevice:

  1.           void ReleaseDevice()
  2. {
  3.   m_target.Reset();
  4.   m_swapChain.Reset();
  5.   ReleaseDeviceResources();
  6. }

這負責釋放呈現目标和交換鍊。 它還調用 ReleaseDeviceResources 以允許釋放任何特定于裝置的資源,例如畫筆、位圖或效果。 此 ReleaseDevice 方法看上去可能無關緊要,但在 DirectX 應用程式中對于可靠處理裝置丢失非常重要。 如果不能正确釋放所有裝置資源(任何由 GPU 支援的資源),則應用程式将無法從裝置丢失中恢複,并且會崩潰。

接下來,我需要準備呈現目标,這是我在圖 2 所示的 Render 方法中沒有涉及的一點。 首先是建立 Direct3D 裝置(dx.h 庫确實也簡化了接下來的幾個步驟):

  1.           auto device = Direct3D::CreateDevice();

在使用 Direct3D 裝置時,我可以轉到 Direct2D 工廠以建立 Direct2D 裝置和 Direct2D 裝置上下文:

  1.           m_target = m_factory.CreateDevice(device).CreateDeviceContext();

接下來,我需要建立視窗的交換鍊。 我将首先從 Direct3D 裝置中檢索 DXGI 工廠:

  1.           auto dxgi = device.GetDxgiFactory();

然後,可以為應用程式的 CoreWindow 建立一個交換鍊:

  1.           m_swapChain = dxgi.CreateSwapChainForCoreWindow(device, m_window.Get());

這裡再次強調,dx.h 庫可以自動為我填充 DXGI_SWAP_CHAIN_DESC1 結構,大幅簡化了工作。 然後,我将調用 CreateDeviceSwapChainBitmap 方法以建立 Direct2D 位圖,該位圖将呈現交換鍊的背景緩沖區:

  1.           void CreateDeviceSwapChainBitmap()
  2. {
  3.   BitmapProperties1 props(BitmapOptions::Target | BitmapOptions::CannotDraw,
  4.     PixelFormat(Dxgi::Format::B8G8R8A8_UNORM, AlphaMode::Ignore));
  5.   auto bitmap =
  6.     m_target.CreateBitmapFromDxgiSurface(m_swapChain, props);
  7.   m_target.SetTarget(bitmap);
  8. }

此方法首先需要以 Direct2D 可以了解的方法描述交換鍊的背景緩沖區。 BitmapProperties1 是 Direct2D D2D1_BITMAP_PROPERTIES1 結構的 dx.h 版本。 BitmapOptions::Target 常量訓示位圖将用作裝置上下文的目标。 Bitmap­Options::CannotDraw 常量關系到一個實際情況:交換鍊的背景緩沖區隻能用作其他繪制操作的輸出,不能用作輸入。 PixelFormat 是 Direct2D D2D1_PIXEL_FORMAT 結構的 dx.h 版本。

定義位圖屬性之後,CreateBitmapFromDxgiSurface 方法将檢索交換鍊的背景緩沖區,并建立 Direct2D 位圖來代表它。 采用這種方法,隻需通過 SetTarget 定位位圖,Direct2D 裝置上下文就可以直接呈現到交換鍊中。

回到 Render 方法,我隻需告知 Direct2D 如何根據使用者的 DPI 配置來縮放任意繪制指令:

  1.           float dpi;
  2. HR(m_displayProperties->get_LogicalDpi(&dpi));
  3. m_target.SetDpi(dpi);

然後,我将調用應用程式的裝置資源處理程式,根據需要建立任意資源。 作為總結,圖 3 提供了 Render 方法的完整裝置初始化序列。

圖 3 準備呈現目标

  1.           void Render()
  2. {
  3.   if (!m_target)
  4.   {
  5.     auto device = Direct3D::CreateDevice();
  6.     m_target = m_factory.CreateDevice(device).CreateDeviceContext();
  7.     auto dxgi = device.GetDxgiFactory();
  8.     m_swapChain = dxgi.CreateSwapChainForCoreWindow(device, m_window.Get());
  9.     CreateDeviceSwapChainBitmap();
  10.     float dpi;
  11.     HR(m_displayProperties->get_LogicalDpi(&dpi));
  12.     m_target.SetDpi(dpi);
  13.     CreateDeviceResources();
  14.     CreateDeviceSizeResources();
  15.   }
  16.   // Drawing and presentation ...
  17.           see Figure 2

雖然 DPI 縮放在 Direct2D 裝置上下文建立之後立即正确應用,在使用者更改了此設定時也需要進行更新。 可以為運作的應用程式更改 DPI 縮放的功能是 Windows 8 中的新增功能。 這正是 IDisplayPropertiesEventHandler 接口的作用。 現在,我隻需實作其 Invoke 方法并相應地更新裝置。 下面是 LogicalDpiChanged 事件處理程式:

  1.           auto __stdcall Invoke(IInspectable *) -> HRESULT override
  2. {
  3.   if (m_target)
  4.   {
  5.     float dpi;
  6.     HR(m_displayProperties->get_LogicalDpi(&dpi));
  7.     m_target.SetDpi(dpi);
  8.     CreateDeviceSizeResources();
  9.     Render();
  10.   }
  11.   return S_OK;
  12. }

假定目标(裝置上下文)已建立,它将檢索目前邏輯 DPI 值并簡單地将其轉發到 Direct2D。 然後調用應用程式,在重新呈現之前重新建立任何特定于裝置大小的資源。 采用這種方法,我的應用程式可以動态地響應顯示裝置 DPI 配置的變化。 視窗必須動态處理的最後一種更改是對視窗大小的更改。 我已經完成事件注冊,是以隻需添加 IWindowSizeChangedEventHandler Invoke 方法的實作來表示 SizeChanged 事件處理程式:

  1.           auto __stdcall Invoke(ICoreWindow *,
  2.   IWindowSizeChangedEventArgs *) -> HRESULT override
  3. {
  4.   if (m_target)
  5.   {
  6.     ResizeSwapChainBitmap();
  7.     Render();
  8.   }
  9.   return S_OK;
  10. }

唯一剩下的任務就是通過 ResizeSwapChainBitmap 方法調整交換鍊位圖的大小。 再次強調,這是需要謹慎處理的内容。 調整交換鍊緩沖區的大小,隻有在正确進行時,才會是有效的操作。 首先,要使此操作成功,我需要確定已經釋放了對這些緩沖區的所有引用。 這些可以是應用程式直接或間接持有的引用。 在本例中,引用由 Direct2D 裝置上下文持有。 目标圖像是我建立用于包裝交換鍊的背景緩沖區的 Direct2D 位圖。 釋放此項相當簡單:

  1.           m_target.SetTarget();

接下來可以調用交換鍊的 ResizeBuffers 方法以執行所有繁重的任務,然後根據需要調用應用程式的裝置資源處理程式。 圖 4 顯示了如何一起完成這些任務。

圖 4 交換鍊大小調整

  1.           void ResizeSwapChainBitmap()
  2. {
  3.   m_target.SetTarget();
  4.   if (S_OK == m_swapChain.ResizeBuffers())
  5.   {
  6.     CreateDeviceSwapChainBitmap();
  7.     CreateDeviceSizeResources();
  8.   }
  9.   else
  10.   {
  11.     ReleaseDevice();
  12.   }
  13. }

現在,您可以添加一些繪制指令,這些指令将由 DirectX 高效地呈現給 CoreWindow 的目标。 舉一個簡單例子,您可能希望在 CreateDeviceResources 處理程式中建立一個純色畫筆,并将其配置設定到成員變量,如下所示:

  1.           SolidColorBrush m_brush;
  2. m_brush = m_target.CreateSolidColorBrush(Color(1.0f, 0.0f, 0.0f));

在視窗的 Draw 方法中,我首先使用白色來清除視窗的背景:

  1.           m_target.Clear(Color(1.0f, 1.0f, 1.0f));

然後,可以使用畫筆繪制簡單的紅色矩形,如下所示:

  1.           RectF rect (100.0f, 100.0f, 200.0f, 200.0f);
  2. m_target.DrawRectangle(rect, m_brush);

為了確定應用程式可以從裝置丢失中正常恢複,我必須確定應用程式在正确時間釋放畫筆:

  1.           void ReleaseDeviceResources()
  2. {
  3.   m_brush.Reset();
  4. }

這就是使用 DirectX 呈現基于 CoreWindow 的應用程式所要采取的步驟。 當然,将這些内容與我在 2013 年 5 月的專欄相比,您會驚喜地發現,得益于 dx.h 庫,這些工作相比與 DirectX 相關的代碼編寫已經簡單了許多。 不過實際上仍有大量的樣闆代碼,主要與實作 COM 接口相關。 在此處可加入 C++/CX,來簡化應用程式中使用的 WinRT API。 它隐藏了一部分樣闆 COM 代碼,我在上兩期專欄中已經示範過。