程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> C語言 >> C++ >> 關於C++ >> 借助 C++ 進行 Windows 開發:Windows 運行時的呈現

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

編輯:關於C++

我的上一個專欄中討論了 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) 生成的類模板非常相似:

             typedef ITypedEventHandler<CoreWindow *, VisibilityChangedEventArgs *>
     IVisibilityChangedEventHandler;
   typedef ITypedEventHandler<CoreWindow *, WindowSizeChangedEventArgs *>
     IWindowSizeChangedEventHandler;

接下來必須實現的 COM 接口稱為 IDisplayPropertiesEventHandler,謝天謝地這個接口已經定義 了。 我只需將相關的頭文件包括在其中:

             #include <Windows.Graphics.Display.h>

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

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

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

             struct SampleWindow :
     ...
             IVisibilityChangedEventHandler,
     IWindowSizeChangedEventHandler,
     IDisplayPropertiesEventHandler

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

             EventRegistrationToken token;
   HR(m_window->add_VisibilityChanged(this, &token));
   HR(m_window->add_SizeChanged(this, &token));

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

             ComPtr<IDisplayPropertiesStatics> m_displayProperties;
   m_displayProperties = GetActivationFactory<IDisplayPropertiesStatics> (
     RuntimeClass_Windows_Graphics_Display_DisplayProperties);

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

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

稍後將返回到這三個接口的實現。 現在該是准備 DirectX 基礎結構的時候了。 我將需要標准的設 備資源處理程序集,這些在以前的專欄中已經多次討論過:

             void CreateDeviceIndependentResources() {}
   void CreateDeviceSizeResources() {}
   void CreateDeviceResources() {}
   void ReleaseDeviceResources() {}

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

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

             #include "dx.h"

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

             Factory1 m_factory;

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

             using namespace KennyKerr;
   using namespace KennyKerr::Direct2D;

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

m_factory 成員變量表示 Direct2D 1.1 工廠。 它用於創建呈現目標,並根據需要創建其他多種與 設備無關的資源。 我將創建 Direct2D 工廠,然後可以在 Load 方法的最後一步中創建與設備無關的 任意資源:

             m_factory = CreateFactory();
   CreateDeviceIndependentResources();

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

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

             bool m_visible;
   SampleWindow() : m_visible(true) {}

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

圖 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;
   }

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

             auto __stdcall Invoke(ICoreWindow *,
     IVisibilityChangedEventArgs * args) -> HRESULT override
   {
     unsigned char visible;
     HR(args->get_Visible(&visible));
     m_visible = 0 != visible;
     return S_OK;
   }

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

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

圖 2 Render 方法摘要

             void Render()
   {
     if (!m_target)
     {
       // Prepare render target ...
             }
     m_target.BeginDraw();
     Draw();
     m_target.EndDraw();
     auto const hr = m_swapChain.Present();
     if (S_OK != hr && DXGI_STATUS_OCCLUDED != hr)
     {
       ReleaseDevice();
     }
   }

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

             DeviceContext m_target;
   Dxgi::SwapChain1 m_swapChain;

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

             void ReleaseDevice()
   {
     m_target.Reset();
     m_swapChain.Reset();
     ReleaseDeviceResources();
   }

這負責釋放呈現目標和交換鏈。 它還調用 ReleaseDeviceResources 以允許釋放任何特定於設備的 資源,例如畫筆、位圖或效果。 此 ReleaseDevice 方法看上去可能無關緊要,但在 DirectX 應用程 序中對於可靠處理設備丟失非常重要。 如果不能正確釋放所有設備資源(任何由 GPU 支持的資源), 則應用程序將無法從設備丟失中恢復,並且會崩潰。

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

auto device = Direct3D::CreateDevice();

在使用 Direct3D 設備時,我可以轉到 Direct2D 工廠以創建 Direct2D 設備和 Direct2D 設備上 下文:

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

接下來,我需要創建窗口的交換鏈。 我將首先從 Direct3D 設備中檢索 DXGI 工廠:

             auto dxgi = device.GetDxgiFactory();

然後,可以為應用程序的 CoreWindow 創建一個交換鏈:

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

這裡再次強調,dx.h 庫可以自動為我填充 DXGI_SWAP_CHAIN_DESC1 結構,大幅簡化了工作。 然後 ,我將調用 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);
   }

此方法首先需要以 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 配置來縮放任意繪制命令:

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

查看本欄目

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

圖 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();
     }
     // Drawing and presentation ...
             see Figure 2

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

             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;
   }

假定目標(設備上下文)已創建,它將檢索當前邏輯 DPI 值並簡單地將其轉發到 Direct2D。 然後 調用應用程序,在重新呈現之前重新創建任何特定於設備大小的資源。 采用這種方法,我的應用程序 可以動態地響應顯示設備 DPI 配置的變化。 窗口必須動態處理的最後一種更改是對窗口大小的更改。 我已經完成事件注冊,因此只需添加 IWindowSizeChangedEventHandler Invoke 方法的實現來表示 SizeChanged 事件處理程序:

             auto __stdcall Invoke(ICoreWindow *,
     IWindowSizeChangedEventArgs *) -> HRESULT override
   {
     if (m_target)
     {
       ResizeSwapChainBitmap();
       Render();
     }
     return S_OK;
   }

唯一剩下的任務就是通過 ResizeSwapChainBitmap 方法調整交換鏈位圖的大小。 再次強調,這是 需要謹慎處理的內容。 調整交換鏈緩沖區的大小,只有在正確進行時,才會是有效的操作。 首先,要 使此操作成功,我需要確保已經釋放了對這些緩沖區的所有引用。 這些可以是應用程序直接或間接持 有的引用。 在本例中,引用由 Direct2D 設備上下文持有。 目標圖像是我創建用於包裝交換鏈的後台 緩沖區的 Direct2D 位圖。 釋放此項相當簡單:

             m_target.SetTarget();

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

圖 4 交換鏈大小調整

             void ResizeSwapChainBitmap()
   {
     m_target.SetTarget();
     if (S_OK == m_swapChain.ResizeBuffers())
     {
       CreateDeviceSwapChainBitmap();
       CreateDeviceSizeResources();
     }
     else
     {
       ReleaseDevice();
     }
   }

現在,您可以添加一些繪制命令,這些命令將由 DirectX 高效地呈現給 CoreWindow 的目標。 舉 一個簡單例子,您可能希望在 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 的應用程序所要采取的步驟。 當然,將這些內容與我 在 2013 年 5 月的專欄相比,您會驚喜地發現,得益於 dx.h 庫,這些工作相比與 DirectX 相關的代 碼編寫已經簡單了許多。 不過實際上仍有大量的樣板代碼,主要與實現 COM 接口相關。 在此處可加 入 C++/CX,來簡化應用程序中使用的 WinRT API。 它隱藏了一部分樣板 COM 代碼,我在上兩期專欄 中已經演示過。

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved