本文章是由機器翻譯。

Windows 中的 C++

Direct2D 與桌面應用程式中的呈現

Kenny Kerr

 

Kenny Kerr在我最後一列,我向您展示如何其實是容易與 c + + 創建桌面應用程式,無需任何庫或框架。事實上,如果你感到特別是自虐,您可以編寫從整個桌面應用程式在您的 WinMain 函數內所做的圖 1。當然,這種做法只是不能縮放。

 

 

圖 1 狂的視窗

int __stdcall wWinMain(HINSTANCE module, HINSTANCE, PWSTR, int)
{
  WNDCLASS wc = {};
  wc.hCursor = LoadCursor(nullptr, IDC_ARROW);
  wc.hInstance = module;
  wc.lpszClassName = L"window";
  wc.lpfnWndProc = [] (HWND window, UINT message, WPARAM
    wparam, LPARAM lparam) -> LRESULT
  {
    if (WM_DESTROY == message)
    {
      PostQuitMessage(0);
      return 0;
    }
    return DefWindowProc(window, message, wparam, lparam);
  };
  RegisterClass(&wc);
  CreateWindow(wc.lpszClassName, L"Awesome?!",
    WS_OVERLAPPEDWINDOW | WS_VISIBLE, CW_USEDEFAULT, CW_USEDEFAULT,
    CW_USEDEFAULT, CW_USEDEFAULT, nullptr, nullptr, module, nullptr);
  MSG message;
  BOOL result;
  while (result = GetMessage(&message, 0, 0, 0))
  {
    if (-1 != result) DispatchMessage(&message);
  }
}

此外演示了如何活動範本庫 (ATL) 提供好的 c + + 抽象隱藏很多這種機制和 Windows 範本庫 (WTL) 如何考慮這更進一步,主要是為應用程式大量投資于使用者和GDI應用程式發展辦法 (見我 2 月列在 msdn.microsoft.com/magazine/jj891018)。

在 Windows 應用程式呈現的未來是硬體-­加速 Direct3D,但這真的就是不切實際來直接處理如果所有你想要做是呈現一個二維的應用程式或遊戲。 這就是 Direct2D 發揮作用的地方。 我簡要介紹了 Direct2D,當它首次宣佈在幾年以前,但我要花,未來幾個月考慮很多仔細看看 Direct2D 發展。 簽出我 2009 年 6 月的專欄文章,"引入 Direct2D"(msdn.microsoft.com/magazine/dd861344),有關的體系結構和基本原則的 Direct2D 的介紹。

Direct2D 設計的關鍵支柱之一是它呈現的重點和其他 Windows 應用程式開發方面留給您或其他您可能使用的庫。 雖然 Direct2D 為了在桌面視窗中呈現,它是您實際上提供此視窗並優化它的 Direct2D 呈現。 所以這個月,我會注重獨特的關係 Direct2D 和桌面應用程式視窗之間。 你可以做許多事情,以優化處理和呈現過程的視窗。 您想要減少不必要的繪畫和避免閃爍,只是為使用者提供最佳的體驗。 當然,你也會想要提供一個易於管理的框架,開發應用程式。 我會解決這些問題在這裡。

桌面視窗

在 ATL 示例中我上個月,給從 ATL 所類範本派生的視窗類的示例。 一切都很好地包含在應用程式的視窗類。 然而,什麼最終會發生的情況很多的視窗和呈現水暖告終穿插視窗的特定于應用程式的呈現和事件處理。 若要解決此問題,我傾向推此樣板化的代碼放入基類,切實可行的儘量使用編譯時多態性,達到應用程式的視窗類時此基類需要提請其注意。 由 ATL 和 WTL 有很多使用這種方法,那麼為什麼不把它擴展為您自己的類嗎?

圖 2 說明了這種分離。 基類是桌面­視窗類範本。 範本參數使基類能夠打電話到混凝土不使用虛函數的類。 在這種情況下,它使用這種技術隱藏一堆呈現特定的預處理和後處理同時到應用程式的視窗的調用來執行實際的繪圖操作。 一會兒,我就會展開 DesktopWindow 類範本上,但其視窗類註冊,首先需要有點工作。

圖 2 桌面視窗

template <typename T>
class DesktopWindow :
  public CWindowImpl<DesktopWindow<T>, CWindow,
    CWinTraits<WS_OVERLAPPEDWINDOW | WS_VISIBLE>>
{
  BEGIN_MSG_MAP(DesktopWindow)
    MESSAGE_HANDLER(WM_PAINT, PaintHandler)
    MESSAGE_HANDLER(WM_DESTROY, DestroyHandler)
  END_MSG_MAP()
  LRESULT DestroyHandler(UINT, WPARAM, LPARAM, BOOL &)
  {
    PostQuitMessage(0);
    return 0;
  }
  LRESULT PaintHandler(UINT, WPARAM, LPARAM, BOOL &)
  {
    PAINTSTRUCT ps;
    VERIFY(BeginPaint(&ps));
    Render();
    EndPaint(&ps);
    return 0;
  }
  void Render()
  {
    ...
static_cast<T *>(this)->Draw();
    ...
}
    ...
};
struct SampleWindow : DesktopWindow<SampleWindow>
{
  void Draw()
  {
    ...
}
};

優化視窗類

用於桌面應用程式的 Windows API 的現實之一是它旨在簡化與傳統的使用者和GDI資源呈現。 這些"方便"的一些需要禁用,以允許 Direct2D 接管,以避免不必要畫導致難看的閃爍。 其他這些預設值也必須予以調整,以更好地適合 Direct2D 呈現的方式開展工作。 這可以通過更改視窗類資訊之前已註冊,但您可能已經注意到 ATL 隱藏這從程式師來實現。 幸運的是,仍有方式來實現這一目標。

上個月,我表明 Windows API 期望基於其規範創建視窗之前註冊視窗類結構如何。 視窗類的特性之一是其背景的畫筆。 Windows 使用此GDI畫筆視窗開始繪畫之前清除視窗的工作區。 這是方便使用者和GDI的日子,但是是不必要的和一些閃爍的 Direct2D 應用程式的原因。 一個簡單的方法來避免這種情況是通過設置背景刷柄 nullptr 視窗類結構中。 如果您要開發相對較快的 Windows 7 或 Windows 8 的電腦上,您可能認為這是不必要的因為你沒有注意到任何閃爍。 這是只是因為現代的 Windows 桌面如此有效地組成的圖形處理單元 (GPU) 很難把它撿起來。 然而,它容易足夠應變呈現管道,誇大效果,您可能會遇到較慢的電腦上。 如果視窗的背景畫筆為白色,然後添加一點睡眠延遲來繪製視窗的用戶端區域形成了鮮明對比的黑色畫筆,— — 10 ms 和 100 ms 之間的任何地方 — — 你得撿撞擊閃爍沒有麻煩。 那麼如何避免它?

如我所述,如果您的視窗類註冊缺少背景畫筆,然後 Windows 不會有任何畫筆,以清除您的視窗。 但是,您可能已經注意到在視窗類註冊完全隱藏的 ATL 示例中的詳情。 常見的解決方案是以處理預設視窗處理控制碼的 WM_ERASEBKGND 消息 — — 提供的 DefWindowProc 函數 — — 按繪畫視窗類背景畫筆與視窗的工作區。 如果您處理此消息,返回 true,則會出現沒有繪畫。 這是一個原因­能夠解決辦法,為此消息發送到視窗無論是否該視窗類有一個有效的背景畫筆或不。 另一個解決方案是只是避免此不操作處理常式,放在第一位從視窗類中移除背景畫筆。 幸運的是,ATL 使相對簡單的重寫創建視窗的這一部分。 在創建期間,ATL 視窗來獲取此視窗類資訊上調用 GetWndClassInfo 方法。 您可以提供您自己的實現,這種方法,但 ATL 提供了一個方便的宏實現該介面為您:

DECLARE_WND_CLASS_EX(nullptr, CS_HREDRAW | CS_VREDRAW, -1);

為此宏的最後一個參數要刷的常數,但-1 值誘騙它清除此屬性的視窗類結構。 穩賺不賠的方法來確定是否已經消除了視窗的背景是檢查由 WM_PAINT 處理常式內的 BeginPaint 函數填充的 PAINTSTRUCT。 如果其轉硫酶成員是虛假的然後你知道 Windows 已清除你視窗的背景,或至少一些代碼 WM_ERASEBKGND 消息作出回應和清除它的本意是。 如果 WM_ERASEBKGND 訊息處理常式不會或不能清除背景,然後它由 WM_PAINT 訊息處理常式,若要這樣做。 不過,在這裡我們可以採用 Direct2D 完全接管是視窗客戶區的呈現,並避免這雙幅畫。 只是一定要調用 EndPaint 函數,確保 Windows 您確實畫你的視窗,否則 Windows 將繼續糾纏你不必要流的 WM_PAINT 消息。 當然,這將會損害您的應用程式性能並增加總體電力消費。

視窗類資訊值得我們注意的其他方面是視窗類樣式。 這是,事實上,以前的宏的第二個參數是什麼。 CS_HREDRAW 和 CS_VREDRAW 樣式使視窗每次在垂直方向和水準方向調整視窗大小使其無效。 當然,這沒有必要。 可以,例如,處理 WM_SIZE 消息並使不正確視窗,但我總是高興時 Windows 將保存我寫幾個多餘的程式碼。 不管怎樣,如果您忽略不正確視窗,然後 Windows 不會發送您的視窗任何 WM_PAINT 消息時視窗的大小減小。 這可能是細的如果你是快樂的視窗的內容將被剪切,但它很常見這些天來繪製各種視窗資產相對於視窗的大小。 無論你喜歡什麼,這是一項明確的決定,您需要為您的應用程式視窗進行。

雖然我對視窗背景的主題,它通常是可取明確宣佈不正確視窗。 這使您可以保留您的視窗呈現植根于 WM_PAINT 消息,而不是不得不處理畫在不同的地方和不同的代碼路徑通過您的應用程式。 你可能想要畫一些東西以回應滑鼠按一下。 當然,你可以,呈現右有訊息處理常式中。 或者,您可以只是不正確視窗,讓 WM_PAINT 處理常式使應用程式的目前狀態。 這是 InvalidateRect 函數的作用。 ATL 提供的只是換了此函數的 Invalidate 方法。 什麼往往混淆了有關此功能的開發人員是如何處理"擦除"參數。 常規的智慧似乎是說"yes"要擦除會立即重新繪製視窗和說"no"將推遲這某種程度上。 這不是真的和盡可能多的檔說。 不正確視窗,將導致它迅速進行重新繪製。 而不是 DefWindowProc 函數,它通常將清除視窗背景擦除選項。 如果擦除為 true,然後對 BeginPaint 的後續調用將清除視窗背景。 在這裡,然後,是完全避免視窗類背景畫筆,而不是依賴于 WM_ERASEBKGND 訊息處理常式的另一個原因。 無背景刷,BeginPaint 再次沒有什麼用,繪畫因此擦除選項不起作用。 如果你讓 ATL 設置背景畫筆為您的視窗類,那麼你需要作廢您的視窗,因為這將再次介紹閃爍時要小心。 為此目的向 DesktopWindow 類範本添加此受保護的成員:

void Invalidate()
{
  VERIFY(InvalidateRect(nullptr, false));
}

它也是一個好主意,若要處理 WM_DISPLAYCHANGE 消息,要使之不正確視窗。 這可以確保適當地重新繪製視窗應顯示某些東西變化影響視窗的外觀。

運行應用程式

我想使我的應用程式的 WinMain 函數相對簡單。 為了實現這一目標,我添加到要隱藏整個視窗和 Direct2D 工廠創建,以及消息迴圈的 DesktopWindow 類範本中運行的公共方法。 DesktopWindow 的運行方法所示圖 3。 這讓我很簡單地寫我的應用程式的 WinMain 函數:

int __stdcall wWinMain(HINSTANCE, HINSTANCE, PWSTR, int)
{
  SampleWindow window;
  return window.Run();
}

圖 3 DesktopWindow Run 方法

int Run()
{
  D2D1_FACTORY_OPTIONS fo = {};
  #ifdef DEBUG
  fo.debugLevel = D2D1_DEBUG_LEVEL_INFORMATION;
  #endif
  HR(D2D1CreateFactory(D2D1_FACTORY_TYPE_SINGLE_THREADED,
                       fo,
                       m_factory.GetAddressOf()));
  static_cast<T *>(this)->CreateDeviceIndependentResources();
  VERIFY(__super::Create(nullptr, nullptr, L"Direct2D"));
  MSG message;
  BOOL result;
  while (result = GetMessage(&message, 0, 0, 0))
  {
    if (-1 != result)
    {
      DispatchMessage(&message);
    }
  }
  return static_cast<int>(message.wParam);
}

在創建視窗之前, 我準備 Direct2D 廠選項通過啟用調試層為調試版本。 我強烈建議你不要是相同的因為它允許 Direct2D 出各種有用的診斷跟蹤您開發應用程式。 D2D1CreateFactory 函數返回工廠介面指標,我交給 Windows 運行時庫優秀 ComPtr 智慧指標,DesktopWindow 類的受保護成員。 我然後調用 CreateDeviceIndependentResources 方法來創建任何獨立于設備的資源 — — 幾何圖形和可重用的整個應用程式生命週期的描邊樣式的東西。 雖然我允許派生的類重寫此方法,提供一個空的存根,DesktopWindow 類範本中如果這不需要了。 Run 方法最後通過阻止同一個簡單的消息迴圈。 簽出解釋消息迴圈的最後一個月的列。

呈現目標

Direct2D 呈現目標應創建作為 WM_PAINT 訊息處理常式的一部分進行調用的 Render 方法內部的需求。 有別于某些其他 Direct2D 呈現目標,它是完全有可能的設備 — — 在大多數情況下 GPU — — 提供硬體加速呈現為一個桌面視窗可以消失或更改某些方式使呈現目標所分配的任何資源無效。 由於在 Direct2D 即時模式呈現的性質,應用程式負責跟蹤資源的是特定于設備的和可能需要不時重新創建。 幸運的是,這很容易管理。 圖 4 提供了完整的 DesktopWindow 渲染方法。

圖 4 DesktopWindow 渲染方法

void Render()
{
  if (!m_target)
  {
    RECT rect;
    VERIFY(GetClientRect(&rect));
    auto size = SizeU(rect.right, rect.bottom);
    HR(m_factory->CreateHwndRenderTarget(RenderTargetProperties(),
      HwndRenderTargetProperties(m_hWnd, size),
      m_target.GetAddressOf()));
    static_cast<T *>(this)->CreateDeviceResources();
  }
  if (!(D2D1_WINDOW_STATE_OCCLUDED & m_target->CheckWindowState()))
  {
    m_target->BeginDraw();
    static_cast<T *>(this)->Draw();
    if (D2DERR_RECREATE_TARGET == m_target->EndDraw())
    {
      m_target.Reset();
    }
  }
}

Render 方法首先檢查是否有效的管理呈現目標 COM 介面的 ComPtr。 這種方式,它只將重新創建呈現目標在必要時。 這會發生至少一次呈現在視窗第一次。 如果有事情發生到基礎設備,或不論何種原因,呈現目標需要重新創建,那麼最後的 Render 方法中的 EndDraw 方法圖 4 將返回 D2DERR_RECREATE_TARGET 常量。 然後使用 ComPtr 重置方法只是釋放該呈現器目標。 該視窗要求繪製自身,在下一次的渲染方法會創建新的 Direct2D 的議案通過呈現目標。

它開始通過獲取物理圖元中的視窗的工作區。 Direct2D 大部分使用邏輯圖元以獨佔方式,使它能夠支援高 DPI 顯示自然。 這一點,它將啟動物理顯示和其邏輯座標系統之間的關係。 然後,它調用 Direct2D 工廠創建呈現目標物件。 此時它將調用到派生的應用程式視窗類創建任何特定于設備的資源 — — 畫筆和所依賴的基本呈現目標設備上的點陣圖的事情。 再次,空的存根 (stub) 是由 DesktopWindow 類提供如果這不需要。

在繪圖中之前, 的 Render 方法檢查視窗是實際可見的和不完全阻塞。 這樣可以避免任何不必要的呈現。 通常情況下,這僅發生在底層的 DirectX 交換鏈是看不見的例如,當使用者鎖定或切換桌面時。 BeginDraw 和 EndDraw 方法然後跨越對應用程式視窗繪製方法的調用。 Direct2D 願借此機會批次處理頂點緩衝區中的幾何圖形、 合併繪製命令,以在 GPU 上提供的最大輸送量和性能。

最後的關鍵一步以正確結合一個桌面視窗的 Direct2D 是時調整視窗的大小調整呈現目標。 我已經講到如何在視窗是自動失效,以確保它迅速重新繪製,但呈現目標本身已經沒有視窗的大小已經改變了的想法。 幸運的是,這很容易做,作為圖 5 所示。

圖 5 調整呈現目標

MESSAGE_HANDLER(WM_SIZE, SizeHandler)
LRESULT SizeHandler(UINT, WPARAM, LPARAM lparam, BOOL &)
{
  if (m_target)
  {
    if (S_OK != m_target->Resize(SizeU(LOWORD(lparam),
      HIWORD(lparam))))
    {
      m_target.Reset();
    }
  }
  return 0;
}

假設 ComPtr 目前擁有一個有效的呈現目標 COM 介面指標,呈現目標調整大小方法稱為新的大小,與提供的視窗消息需要 LPARAM 的。如果然後呈現目標不能調整其內部資源的所有由於某種原因,ComPtr 只重定,迫使要重新創建的下一個時間呈現的呈現目標要求。

而這正是我已在本月的專欄中的餘地。您現在可以創建和管理一個桌面視窗以及使用 GPU 來呈現您的應用程式視窗中您所需要的一切。隨著探索 Direct2D 加入我下個月。

Kenny 克爾 是設在加拿大的 Pluralsight 和 Microsoft MVP 的作者一個電腦程式員。在他的博客 kennykerr.ca 你可以跟隨他在 Twitter 上和 twitter.com/kennykerr

感謝以下技術專家對本文的審閱中: Worachai Chaoweeraprasit