本文章是由機器翻譯。

Windows 與 C++

DirectComposition:一個保留模式 API 搞定一切

Kenny Kerr

下載代碼示例

Kenny Kerr圖形 Api 一般分為了兩個非常不同陣營。 有知名的例子,包括 Direct2D 和 Direct3D 的即時模式 Api。 然後還有Windows Presentation Foundation(WPF) 等保留模式 Api 或任何 XAML 或聲明式的 API。 現代的瀏覽器提供明確區分的兩個圖形模式,提供一個保留模式 API 和提供即時模式 API 的畫布元素的可縮放向量圖形。

保留模式假定圖形 API 將保留一些代表性的一幕,如圖形或物件,然後隨著時間的推移被操縱的樹。 這是方便,簡化了互動式應用程式的開發。 相比之下,立即模式 API 不包括內置場景圖,而是依賴于應用程式構建的場景使用的繪圖命令序列。 這有巨大的性能優勢。 即時模式 API 如 Direct2D 通常將緩衝頂點資料從多個幾何,凝聚大量的繪圖命令以及更多。 這是特別有益,與文本渲染管線,字形首先需要被寫入到一個紋理和下採樣之前清除類型篩選應用和文本使得其途徑是呈現目標。 這是為什麼很多其他圖形 Api 和越來越多的協力廠商應用程式,現在依靠 Direct2D 和 DirectWrite 用於文本渲染的原因之一。

即時模式與保留模式的選擇傳統上是下來的一種權衡性能和生產率之間。 開發者們可以選擇用於絕對性能的 Direct2D 立即模式 API 或 WPF 保留模式 API 為生產力或不方便。 DirectComposition 通過使開發人員能夠更自然地融入這兩個更改這個方程。 它模糊了立即模式和保留模式 Api 之間的界線,因為它提供了一種保留模式的圖形,但不施加任何記憶體或性能開銷。 它通過專注于點陣圖組成,而不是試圖與其他圖形 Api 競爭來實現這一壯舉。 DirectComposition 只是提供了視覺化樹和組成基礎設施這樣的點陣圖呈現與其他技術可以輕鬆地操縱和共同組成。 與 WPF 中,不同的是 DirectComposition 是 OS 圖形基礎結構的一個組成部分,並避免了所有傳統上一直困擾 WPF 應用程式的性能和空域問題。

如果你讀過我在 DirectComposition 上前面的兩個專欄 (msdn.microsoft.com/magazine/dn745861msdn.microsoft.com/magazine/dn786854),你應該已經有什麼組合引擎是有能力的一種。 現在我想要的更多明確通過向您顯示如何使用 DirectComposition 來操縱可視物件繪製帶 Direct2D 是對習慣于保留模式 Api 的開發人員很有吸引力的方式。 我要向你展示如何創建一個簡單的視窗,提出了圓圈,作為"物件",可以創建和移動,全力支援與點擊測試和更改為 Z-順序。 你可以看看這看起來像在中的示例圖 1

拖動環繞
圖 1 拖動環繞

雖然在圈子圖 1 並用 Direct2D 應用畫一圈只有一次到一個組成表面繪製。 在綁定到該視窗的視覺化樹,這組成表面然後之間組成的視覺效果的共用。 每個視覺定義偏移量相對於視窗的內容 — — 組成表面 — — 是定位並最終呈現由組合引擎。 使用者可以創建新的圈子和與滑鼠、 筆或手指移動它們。 每一次選擇了一個圓圈,它將移動到 Z 順序的頂部所以它顯示在視窗中的任何其他圈上方。 雖然我肯定不需要保留模式 API 來實現一個簡單的效果,它會作為 DirectComposition API 是如何工作的以及 Direct2D 實現一些功能強大的視覺效果很好的例子。 目標是建立一個其 WM_PAINT 處理常式並不是負責保持視窗的圖元為單位) 最新的互動式應用程式。

我將開始一個新的 SampleWindow 類從我在我以前的專欄仲介紹的視窗類範本派生的。 視窗類範本只是簡化了 c + + 中的消息分發:

struct SampleWindow : Window<SampleWindow>
{
};

任何現代的 Windows 應用程式,我需要處理動態 DPI 縮放所以我將添加兩個浮點成員來跟蹤的 DPI 比例因數為 X 軸和 Y 軸:

float m_dpiX = 0.0f;
float m_dpiY = 0.0f;

你可以初始化這些需求,在我以前的專欄中,或在您的 WM_CREATE 訊息處理常式內示。 無論哪種方式,你需要調用 MonitorFromWindow 函數來確定監視器的相交的新視窗的面積最大。 然後你只需調用 GetDpiForMonitor 函數來檢索其有效的 DPI 值。 所以我不會在這裡重申它,我已經說明了這在以前的專欄和課程的次數。

我將使用一個 Direct2D 橢圓幾何物件來描述圈要繪製以便稍後使用此相同的幾何物件進行點擊測試。 雖然它是更有效的方法畫出比幾何物件的 D2D1_ELLIPSE 結構,幾何物件提供的點擊測試和繪圖將被保留。 我會保持跟蹤的 Direct2D 工廠和橢圓幾何:

ComPtr<ID2D1Factory2> m_factory;
ComPtr<ID2D1EllipseGeometry> m_geometry;

在我以前的專欄中我將向您展示如何創建一個 Direct2D 設備物件,直接用 D2D1CreateDevice 函數,而不是通過使用一個 Direct2D 工廠。 這當然是可以接受的方式繼續下去,但這裡面有蹊蹺。 雖然他們都是獨立于設備和設備損失發生時,不需要重新創建 Direct2D 工廠資源,可以用相同的 Direct2D 工廠創建的 Direct2D 設備僅用。 因為我想要創建橢圓幾何前面,我需要一個 Direct2D 工廠物件來創建它。 我可以,或許,等到我已經用 D2D1CreateDevice 函數創建 Direct2D 設備,然後用 GetFactory 方法,檢索的底層的工廠,然後使用該工廠物件創建幾何,但那似乎相當做作。 相反,我會只是創建一個 Direct2D 工廠並使用它來創建橢圓幾何形狀和所需的設備物件。 圖 2 說明了如何創建 Direct2D 工廠和幾何物件。

圖 2 創建的 Direct2D 工廠和幾何物件

void CreateFactoryAndGeometry()
{
  D2D1_FACTORY_OPTIONS options = {};
  #ifdef _DEBUG
  options.debugLevel = D2D1_DEBUG_LEVEL_INFORMATION;
  #endif
  HR(D2D1CreateFactory(D2D1_FACTORY_TYPE_SINGLE_THREADED,
                       options,
                       m_factory.GetAddressOf()));
  D2D1_ELLIPSE const ellipse = Ellipse(Point2F(50.0f, 50.0f),
                                       49.0f,
                                       49.0f);
  HR(m_factory->CreateEllipseGeometry(ellipse,
                                      m_geometry.GetAddressOf()));
}

然後可以由 SampleWindow 的建構函式來準備這些設備-調用 CreateFactoryAndGeometry 方法­獨立資源。 正如你所看到的該橢圓定義圍繞中心點 50 圖元沿 X 軸和 Y 軸,以及 49 圖元的 X 和 Y 的半徑,使此橢圓成一個圓的半徑。 我將創建一個 100 x 100 組成的表面。 我選擇了 49 圖元的半徑範圍,因為預設的筆觸繪製由 Direct2D 橫跨周長和它否則會被剪切。

接下來是特定于設備的資源。 我需要一個支援 Direct3D 設備,組成設備以將更改提交到視覺化樹,組成目標保持視覺樹活著,將代表所有圈的視覺效果和一個共用的組成表面的父根視覺:

ComPtr<ID3D11Device> m_device3D;
ComPtr<IDCompositionDesktopDevice> m_device;
ComPtr<IDCompositionTarget> m_target;
ComPtr<IDCompositionVisual2> m_rootVisual;
ComPtr<IDCompositionSurface> m_surface;

在我前面的 DirectX 文章,尤其是,在我前面的兩個專欄在 DirectComposition 上,我已經介紹了這些不同的物件。 我也討論了,說明你應如何處理設備創建和損失所以我在這裡不會重複的。 我只是打電話給出了需要更新使用以前創建的 Direct2D 工廠的 CreateDevice2D 方法:

ComPtr<ID2D1Device> CreateDevice2D()
{
  ComPtr<IDXGIDevice3> deviceX;
  HR(m_device3D.As(&deviceX));
  ComPtr<ID2D1Device> device2D;
  HR(m_factory->CreateDevice(deviceX.Get(), 
    device2D.GetAddressOf()));
  return device2D;
}

現在我將創建的共用的表面。 我需要小心使用 ComPtr 類範本 ReleaseAndGetAddressOf 方法以確保表面可以很安全地重新創建後設備丟失或由於 DPI 縮放比例的變化。 我也需要小心,保留我的應用程式為 DirectComposition API 翻譯成物理圖元的尺寸時使用的邏輯座標系統:

HR(m_device->CreateSurface(
  static_cast<unsigned>(LogicalToPhysical(100, m_dpiX)),
  static_cast<unsigned>(LogicalToPhysical(100, m_dpiY)),
  DXGI_FORMAT_B8G8R8A8_UNORM,
  DXGI_ALPHA_MODE_PREMULTIPLIED,
  m_surface.ReleaseAndGetAddressOf()));

我然後可以調用組成表面的 BeginDraw 方法來接收 Direct2D 設備上下文,以緩衝繪圖命令:

HR(m_surface->BeginDraw(
  nullptr,
  __uuidof(dc),
  reinterpret_cast<void **>(dc.GetAddressOf()),
  &offset));

然後我需要告訴 Direct2D 如何縮放任何繪圖命令:

dc->SetDpi(m_dpiX,
           m_dpiY);

我需要轉換到的偏移量,由 DirectComposition 提供的輸出:

dc->SetTransform(Matrix3x2F::Translation(PhysicalToLogical(offset.x, m_dpiX),
                                         PhysicalToLogical(offset.y, m_dpiY)));

PhysicalToLogical 是一個 helper 函數,我經常使用的 DPI 縮放時結合過不同程度的 DPI 縮放比例 (或沒有) 支援的 Api。 你可以看到的 PhysicalToLogical 功能和對應的 LogicalToPhysical 函數在圖 3

圖 3 邏輯和物理圖元之間的轉換

template <typename T>
static float PhysicalToLogical(T const pixel,
                               float const dpi)
{
  return pixel * 96.0f / dpi;
}
template <typename T>
static float LogicalToPhysical(T const pixel,
                               float const dpi)
{
  return pixel * dpi / 96.0f;
}

現在我能簡單地畫一個藍色的圓圈與單色筆刷創建,為此:

ComPtr<ID2D1SolidColorBrush> brush;
D2D1_COLOR_F const color = ColorF(0.0f, 0.5f, 1.0f, 0.8f);
HR(dc->CreateSolidColorBrush(color,
                             brush.GetAddressOf()));

下一步,我必須在填充的橢圓幾何,然後撫摸或繪圖與修改的畫筆繪製其輪廓之前清除呈現目標:

dc->Clear();
dc->FillGeometry(m_geometry.Get(),
                 brush.Get());
brush->SetColor(ColorF(1.0f, 1.0f, 1.0f));
dc->DrawGeometry(m_geometry.Get(),
                 brush.Get());

最後,我必須調用 EndDraw 方法,以指示表面是準備組成:

HR(m_surface->EndDraw());

現在它是創建圈子的時間。 在我以前的專欄,我創建只有一個單一的根視覺,但此應用程式需要創建視覺效果上的需求,所以我會在方便的説明器方法,只是包起來:

ComPtr<IDCompositionVisual2> CreateVisual()
{
  ComPtr<IDCompositionVisual2> visual;
  HR(m_device->CreateVisual(visual.GetAddressOf()));
  return visual;
}

DirectComposition API 有趣的方面之一是它實際上是一個只寫到組合引擎介面。 雖然它保留視覺化樹,你的視窗,它不提供任何 getter 方法,您可以使用來審問的視覺化樹。 任何資訊,如視覺位置或 Z-順序,必須由應用程式直接保留。 這避免了不必要的記憶體開銷,也避免了潛在的競爭條件,世界的應用程式的視圖和組合引擎事務性狀態之間。 所以我會勇往直前,創造一個圈結構來跟蹤每個圓的位置:

struct Circle
{
  ComPtr<IDCompositionVisual2> Visual;
  float LogicalX = 0.0f;
  float LogicalY = 0.0f;
};

視覺的成分有效地表示圓的二傳手同時 LogicalX 和辯證統一欄位是 getter 方法。 我可以將與 IDCompositionVisual2 介面的視覺位置的設置,可以在保留和稍後檢索其地位與其他欄位。 這是必要的點擊測試和設備損失後恢復圈子。 為了避免這些變得不同步,我將簡單地提供更新基於的邏輯位置的可視物件的説明器的方法。 DirectComposition API 有不知道的內容可能會如何定位和縮放,所以我需要自己做出必要的 DPI 計算:

void UpdateVisualOffset(float const dpiX,
                        float const dpiY)
{
  HR(Visual->SetOffsetX(LogicalToPhysical(LogicalX, dpiX)));
  HR(Visual->SetOffsetY(LogicalToPhysical(LogicalY, dpiY)));
}

我將添加另一個説明器方法的實際設置圓的邏輯偏移量。 這一依賴于 UpdateVisualOffset 確保圈層結構和物件的可視物件的同步:

void SetLogicalOffset(float const logicalX,
                      float const logicalY,
                      float const dpiX,
                      float const dpiY)
{
  LogicalX = logicalX;
  LogicalY = logicalY;
  UpdateVisualOffset(dpiX,
                       dpiY);
}

最後,圈子裡添加到應用程式時,我需要一個簡單的建構函式來初始化結構,IDCompositionVisual2 參考的擁有權:

Circle(ComPtr<IDCompositionVisual2> && visual,
       float const logicalX,
       float const logicalY,
       float const dpiX,
       float const dpiY) :
  Visual(move(visual))
{
  SetLogicalOffset(logicalX,
                   logicalY,
                   dpiX,
                   dpiY);
}

我可以現在跟蹤的應用程式的所有圈子與標準的清單的容器:

list<Circle> m_circles;

我在這裡的時候還會成員來跟蹤任何所選的圓:

Circle * m_selected = nullptr;
float m_mouseX = 0.0f;
float m_mouseY = 0.0f;

滑鼠偏移量也將有助於產生自然的運動。 之前我看看實際的滑鼠交互,最終創造圈子,請允許我來移動它們,我去拿出方式管家。 CreateDeviceResources 方法需要重新創建任何可視的物件,應發生設備丟失,基於任何以前創建的圈子。 如果圓圈消失,它不會做。 所以右後創建或重新創建根視覺效果和共用的表面上,我就會遍歷這個清單中,創建新的視覺化物件和重新置放,以滿足現有的狀態。 圖 4 說明了這一切是如何一起使用我已經已經建立。

圖 4 創建裝置堆疊和視覺化樹

void CreateDeviceResources()
{
  ASSERT(!IsDeviceCreated());
  CreateDevice3D();
  ComPtr<ID2D1Device> const device2D = CreateDevice2D();
  HR(DCompositionCreateDevice2(
      device2D.Get(),
      __uuidof(m_device),
      reinterpret_cast<void **>(m_device.ReleaseAndGetAddressOf())));
  HR(m_device->CreateTargetForHwnd(m_window,
                                   true,
                                   m_target.ReleaseAndGetAddressOf()));
  m_rootVisual = CreateVisual();
  HR(m_target->SetRoot(m_rootVisual.Get()));
  CreateDeviceScaleResources();
  for (Circle & circle : m_circles)
  {
    circle.Visual = CreateVisual();
    HR(circle.Visual->SetContent(m_surface.Get()));
    HR(m_rootVisual->AddVisual(circle.Visual.Get(), false, nullptr));
    circle.UpdateVisualOffset(m_dpiX, m_dpiY);
  }
  HR(m_device->Commit());
}

客房部的其他一點就是要與 DPI 縮放。 作為由 Direct2D 呈現包含圓的圖元組成表面必須重新創建到的規模,和自己的視覺效果也必須被重新置放,以便及其偏移量成正比,到另一個,到所屬的視窗。 WM_DPICHANGED 訊息處理常式第一次重新創建組成表面 — — 的説明下的 CreateDeviceScaleResources 方法 — —,然後更新的內容和圈子裡的每個位置:

if (!IsDeviceCreated()) return;
CreateDeviceScaleResources();
for (Circle & circle : m_circles)
{
  HR(circle.Visual->SetContent(m_surface.Get()));
  circle.UpdateVisualOffset(m_dpiX, m_dpiY);
}
HR(m_device->Commit());

現在,我會處理指標的相互作用。 我會讓使用者創建新的圈子,如果按下控制鍵時按一下滑鼠左鍵。 WM_LBUTTONDOWN 訊息處理常式看起來像這樣:

if (wparam & MK_CONTROL)
{
  // Create new circle
}
else
{
  // Look for existing circle
}
HR(m_device->Commit());

假設需要創建的一個新的圓圈,先通過創建一個新的視覺和添加作為一個孩子的根視覺效果之前設置的共用的內容:

ComPtr<IDCompositionVisual2> visual = CreateVisual();
HR(visual->SetContent(m_surface.Get()));
HR(m_rootVisual->AddVisual(visual.Get(), false, nullptr));

新視覺被添加在任何現有的視覺效果。 這是 AddVisual 方法的第二個參數在工作。 如果我已將此設置為 true 然後新視覺會已經被放置在任何現有的兄弟姐妹。 接下來,我需要添加的清單,以便以後可以支援的圈層結構的點擊測試,設備損失和 DPI 縮放:

m_circles.emplace_front(move(visual),
       PhysicalToLogical(LOWORD(lparam), m_dpiX) - 50.0f,
       PhysicalToLogical(HIWORD(lparam), m_dpiY) - 50.0f,
       m_dpiX,
       m_dpiY);

我很小心,所以我可以自然地支援點擊測試的視覺化樹意味著相同的順序在清單的前面放置新創建的圈子。 我起初也定位視覺,所以它在滑鼠位置上居中。 最後,假設使用者不會立即釋放滑鼠,我也捕獲滑鼠和跟蹤哪一個圓圈將有可能被移動:

SetCapture(m_window);
m_selected = &m_circles.front();
m_mouseX = 50.0f;
m_mouseY = 50.0f;

滑鼠偏移量可以讓我順利地將任何圈子無論在哪裡拖上滑鼠指標開始落下的圓圈。 尋找一個現有的圈子是有點更多的參與。 在這裡,再一次,我需要手動應用 DPI 的認識。 幸運的是,Direct2D 使得這一陣微風。 首先,我需要遍歷的自然的 Z-順序的圈子。 幸運的是,我已經把新的圈子,在清單的前面所以這是一個簡單的從開始到結束反覆運算問題:

for (auto circle = begin(m_circles); circle != end(m_circles); ++circle)
{
}

我沒有使用基於範圍為語句因為它會更方便,其實在這種情況下有反覆運算器派上用場。 圈子在哪裡呢? 好吧,每個圓圈跟蹤的邏輯位置視窗的左上角。 滑鼠消息 LPARAM 還包含指標的物理位置在視窗左上角。 但它不是不夠的將它們轉換成一個共同的座標系統,因為我需要進行點擊測試的形狀並不是一個簡單的矩形。 由幾何物件定義形狀並 Direct2D 提供的 FillContainsPoint 方法來執行點擊測試。 訣竅是幾何物件提供唯一的形狀圓和沒有它的位置。 進行點擊測試有效地工作,需要第一次翻譯滑鼠的位置,這樣它是相對於的幾何物件。 也很簡單,

D2D1_POINT_2F const point =
  Point2F(LOWORD(lparam) - LogicalToPhysical(circle->LogicalX, m_dpiX),
          HIWORD(lparam) - LogicalToPhysical(circle->LogicalY, m_dpiY));

但我不是準備好要調用 FillContainsPoint 方法。 另一個問題是幾何物件不知道任何關於渲染的目標。 當我用幾何物件來繪製圓時,它是呈現目標,按比例的幾何形狀,以匹配目標的 DPI 值。 所以我需要的方式擴大執行點擊測試,所以它將反映出大小的圓之前, 的幾何對應于使用者實際上看到在螢幕上。 再次,Direct2D 前來搭救。 FillContainsPoint 接受可選的 3 × 2 矩陣來變換的幾何測試給定的點包含在形狀內前。 我可以簡單地定義給定視窗的 DPI 值尺度變換:

D2D1_MATRIX_3X2_F const transform = Matrix3x2F::Scale(m_dpiX / 96.0f,
                                                      m_dpiY / 96.0f);

FillContainsPoint 方法將然後告訴我該點是否包含在圈子內:

BOOL contains = false;
HR(m_geometry->FillContainsPoint(point,
                                 transform,
                                 &contains));
if (contains)
{
  // Reorder and select circle
  break;
}

如果該點包含在圈內,我需要重新排列組成的視覺效果,這樣所選的圓視覺是頂部的 Z-順序。 通過刪除的子可視並將其添加到任何現有的視覺效果的前面,我可以這樣做:

HR(m_rootVisual->RemoveVisual(circle->Visual.Get()));
HR(m_rootVisual->AddVisual(circle->Visual.Get(), false, nullptr));

我也需要保持我的清單最新的通過將圓圈移動到前面的清單:

m_circles.splice(begin(m_circles), m_circles, circle);

然後,我假定使用者希望拖動周圍的圓圈:

SetCapture(m_window);
m_selected = &*circle;
m_mouseX = PhysicalToLogical(point.x, m_dpiX);
m_mouseY = PhysicalToLogical(point.y, m_dpiY);

在這裡,我很小心,計算相對於所選圓的滑鼠位置的偏移量。 在這種圈子並不直觀地"對齊"到滑鼠指標的中心是拖動的方式,提供無縫運動。 回應 WM_MOUSEMOVE 消息允許任何所選的圓繼續這項運動,只要選擇了一圈:

if (!m_selected) return;
m_selected->SetLogicalOffset(
  PhysicalToLogical(GET_X_LPARAM(lparam), m_dpiX) - m_mouseX,
  PhysicalToLogical(GET_Y_LPARAM(lparam), m_dpiY) - m_mouseY,
  m_dpiX,
  m_dpiY);
HR(m_device->Commit());

圈層結構的 SetLogicalOffset 方法更新由圈子,維護的邏輯位置一樣的物理位置的視覺組成。 我也是小心使用的 GET_X_LPARAM 和 GET_Y_LPARAM 的宏來破解 LPARAM,而不是通常的整型和高字的宏。 而由 WM_MOUSEMOVE 消息報告的位置是相對於視窗的左上角,這將包括消極的座標,如果捕獲滑鼠和圈子拖上方或左側的視窗。像往常一樣,對視覺化樹的更改必須致力為他們去實現。 任何運動在 WM_LBUTTONUP 訊息處理常式結束時釋放滑鼠和重置 m_selected 指標:

ReleaseCapture();
m_selected = nullptr;

最後,我會用最好的部分。 最令人信服的證據這是指示性的保留模式圖形是當你考慮在 WM_PAINT 訊息處理常式圖 5

圖 5 保留模式 WM_PAINT 訊息處理常式

void PaintHandler()
{
  try
  {
    if (IsDeviceCreated())
    {
      HR(m_device3D->GetDeviceRemovedReason());
    }
    else
    {
      CreateDeviceResources();
    }
    VERIFY(ValidateRect(m_window, nullptr));
  }
  catch (ComException const & e)
  {
    ReleaseDeviceResources();
  }
}

CreateDeviceResources 方法創建裝置堆疊前面。 只要沒有什麼不妥,沒有進一步的工作是通過 WM_PAINT 訊息處理常式中,除了要驗證視窗。 如果檢測到設備損失,各種的 catch 塊將釋放裝置和失效作為必要的視窗。 下一個 WM_PAINT 消息到來再一次將重新創建的設備資源。 在我下一個專欄中,我將展示你如何你可以產生並不直接驅動的使用者輸入的視覺效果。 組合引擎執行更多的渲染而不涉及應用程式,就可能出現設備損失沒有即使知道它的應用。 這就是為什麼 GetDeviceRemoved­調用方法的原因。 如果組合引擎檢測到設備損失純粹以便它可以通過調用 GetDeviceRemovedReason 方法在 Direct3D 設備檢查設備損失,它將發送應用程式視窗 WM_PAINT 消息。 帶一個隨附的示例專案的測試磁碟機 DirectComposition !


Kenny克爾 是一個基於在加拿大,以及作者的 Pluralsight 和微軟最有價值球員的電腦程式員。他的博客 kennykerr.ca ,你可以跟著他在 Twitter 上 twitter.com/kennykerr

感謝以下微軟技術專家對本文的審閱:Leonardo 布蘭科 (Leonardo.Blanco@microsoft.com) 和James克拉克 (James。Clarke@microsoft.com