DirectX 要素

利用 Direct2D 幾何進行指畫

Charles Petzold

下載代碼示例

Charles Petzold隨著作業系統的發展多年來,所以有了基本的原型應用程式每個開發者應該知道如何的代碼。 對於舊的命令列環境,共同行使是十六進位轉儲 — — 一種程式,列出的內容以十六進位位元組的檔。 對於圖形的滑鼠和鍵盤介面,計算機和筆記本是普遍的。

在多點觸控像 Windows 8 的環境中,我將提名兩個原型應用程式:照片散佈圖和手指的油漆。照片散佈圖是好的方法,若要瞭解如何使用兩個手指可以縮放和旋轉的視覺化物件,雖然手指畫涉及跟蹤單個手指在螢幕上繪製線條。

探討各種辦法了我的書,"Windows 程式設計,第六版"的第 13 章中的 Windows 8 手指畫 (微軟出版社,2012年)。這些程式可用於僅 Windows 運行時 (WinRT) 繪製線條,但現在想重溫行使,而是使用 DirectX。這將是一個好的辦法,以熟悉的 DirectX,一些重要方面,但我懷疑它最終也將使我們一些額外的靈活性,在 Windows 運行時不可用。

Visual Studio範本

作為他 2013 年 3 月的文章,"使用 XAML 與 DirectX 和 c + + 在 Windows 商店 Apps"中討論的Doug· 埃裡克森 (msdn.microsoft.com/­雜誌/jj991975),有三種方法結合 XAML 和 DirectX 在 Windows 應用商店中應用的範圍內。我會用 SwapChainBackgroundPanel 作為根子項目的 XAML 頁導數的辦法。此物件作為一個繪圖表面 Direct2D 和 Direct3D 圖形,但它也可鋪 WinRT 的控制項,例如應用程式欄。

Visual Studio2012年包含這樣的程式的專案範本。在新建專案對話方塊中,選擇 Visual c + + 和 Windows 應用商店在左邊,然後調用 Direct2D App (XAML) 的範本。其他的 DirectX 範本稱為 Direct3D 應用程式,創建僅 DirectX 程式無需任何 WinRT 控制項或圖形。然而,這兩個範本都有些命名因為你可以與他們其中之一的 2D 或 3D 圖形。

Direct2D App (XAML) 範本創建一個簡單的 Windows 應用商店應用程式邏輯劃分之間基於 XAML UI 和 DirectX 圖形輸出。使用者介面包括一個名為 DirectXPage (就像正常的 Windows 應用商店中應用) 從頁派生並包含 XAML 檔、 標頭檔和代碼檔的類。DirectXPage 用於處理使用者輸入,與 WinRT 的控制項的交互和顯示基於 XAML 的圖形和文本。DirectXPage 的根項目是的 SwapChainBackgroundPanel,你可以對待作為 XAML 中經常網格元素和 DirectX 呈現圖面。

專案範本還會創建名為可處理的 DirectX 開銷,大部分的 DirectXBase 類和類命名為 SimpleTextRenderer,從 DirectXBase 派生並執行特定于應用程式 DirectX 圖形輸出。名稱 SimpleTextRenderer 是指此類內從專案範本創建的應用程式是什麼。要重命名此類或取代它的東西有一個更合適的名稱。

從應用程式的範本

此列的可下載代碼之間Visual Studio專案被命名為 BasicFingerPaint 創建使用 Direct2D (XAML) 範本。我改名為 SimpleTextRenderer 到 FingerPaint­渲染器和添加一些其他類。

Direct2D (XAML) 範本意味著分離的 XAML 和 DirectX 部分的應用程式的體系結構:該應用程式的所有 DirectX 代碼應限於 DirectXBase (這你不需要改變),從 DirectXBase (在本例中 FingerPaintRenderer),和任何其他類或結構可能需要這兩個類派生的渲染器類。儘管它的名字,DirectXPage 應該不需要包含任何 DirectX 代碼。相反,DirectXPage 將為它保存作為私有資料成員名為 m_renderer 的渲染器類具現化。DirectXPage 使得許多調用到渲染器類 (和間接 DirectXBase) 來顯示圖形輸出和 DirectX 視窗大小的變化和其他重要事件的通知。渲染器類不會調用 DirectXPage。

在 DirectXPage.xaml 檔中添加到讓你的應用程式欄的下拉式列示方塊中選擇繪圖顏色和線寬和按鈕,保存,載入,清除繪圖。(檔 I/O 邏輯是極其簡陋,不包括設施和服務,如警告你如果你要清楚你沒保存的繪圖。

當你觸摸到螢幕上,手指移動它,並提起,PointerPressed,PointerMoved 和 PointerReleased 的事件生成說明手指的進展。每個事件被伴隨著 ID 號,它允許您跟蹤個人的手指和點的值,該值指示當前的手指的位置。保留和連接這些點,並且你已經呈現單個筆劃。呈現多個筆劃,和你有一個完整的繪圖。圖 1 組成的九招 BasicFingerPaint 繪圖顯示。

A BasicFingerPaint Drawing圖 1 BasicFingerPaint 繪圖

在 DirectXPage 代碼隱藏檔中,添加指標的事件方法的重寫。這些方法調用相應的方法我命名為 BeginStroke、 ContinueStroke、 EndStroke、 CancelStroke、 FingerPaintRenderer 中所示圖 2

圖 2 指標事件方法調用的渲染器類

void DirectXPage::OnPointerPressed(PointerRoutedEventArgs^ args)
{
  NamedColor^ namedColor = 
    dynamic_cast<NamedColor^>(colorComboBox->SelectedItem);
  Color color = 
    namedColor != nullptr ?
namedColor->Color : Colors::Black;
  int width = widthComboBox->SelectedIndex !=
    -1 ?
(int)widthComboBox->SelectedItem : 5;
  m_renderer->BeginStroke(args->Pointer->PointerId,
                          args->GetCurrentPoint(this)->Position,
                          float(width), color);
  CapturePointer(args->Pointer);
}
void DirectXPage::OnPointerMoved(PointerRoutedEventArgs^ args)
{
  IVector<PointerPoint^>^ pointerPoints = 
    args->GetIntermediatePoints(this);
  // Loop backward through intermediate points
  for (int i = pointerPoints->Size - 1; i >= 0; i--)
    m_renderer->ContinueStroke(args->Pointer->PointerId,
                               pointerPoints->GetAt(i)->Position);
}
void DirectXPage::OnPointerReleased(PointerRoutedEventArgs^ args)
{
  m_renderer->EndStroke(args->Pointer->PointerId,
                        args->GetCurrentPoint(this)->Position);
}
void DirectXPage::OnPointerCaptureLost(PointerRoutedEventArgs^ args)
{
  m_renderer->CancelStroke(args->Pointer->PointerId);
}
void DirectXPage::OnKeyDown(KeyRoutedEventArgs^ args)
{
  if (args->Key == VirtualKey::Escape)
      ReleasePointerCaptures();
}

PointerId 物件是區分手指、 滑鼠和筆的唯一整數。 點和顏色值傳遞給這些方法都是基本的 WinRT 類型,但他們不是 DirectX 類型。 DirectX 有其自己點和顏色的結構,命名為 D2D1_POINT_2F 和 D2D1::ColorF。 DirectXPage 不知道任何有關 DirectX,所以 FingerPaintRenderer 類有責任執行所有的 WinRT 的資料類型和 DirectX 資料類型之間的轉換。

構建路徑幾何圖形

在 BasicFingerPaint,每個筆劃是從跟蹤一系列指標事件構造的連接短行的集合。 通常,半吊子應用將呈現一幅點陣圖,然後可以將其保存到一個檔上的這些線。 我決定不這樣做。 您保存和載入從 BasicFingerPaint 檔是筆劃,本身的點的集合的集合。

如何使用 Direct2D 來呈現這些筆劃在螢幕上的? 如果你看通過 ID2D1DeviceCoNtext (這是主要是由 ID2D1RenderTarget 定義的方法) 所定義的繪圖方法,三個候選人跳出來:DrawLine、 DrawGeometry 和 FillGeometry。

DrawLine 與特定寬度、 畫筆和風格的兩個點之間繪製一條直線。 是合理呈現中風與一系列 DrawLine 調用,但它可能是更有效鞏固單折線中的單個行。 為此,您需要 DrawGeometry。

在 Direct2D,幾何基本上是定義直線、 貝茲曲線和弧線的點的集合。 沒有線寬、 顏色或樣式在幾何中的概念。 雖然 Direct2D 支援多種類型的簡單的幾何圖形 (矩形、 圓角的矩形、 橢圓)、 最多才多藝的幾何形狀由 ID2D1PathGeometry 物件表示。

路徑幾何圖形組成的一個或多個"數位"。每個圖都是一系列相互連接的直線和曲線。 圖的各個元件,稱為"段"。可能關閉圖 — — 就是最後一點可能連接與第一點 — — 但它不需要。

要呈現幾何,打電話 DrawGeometry 與特定的線寬、 筆刷和樣式在設備上下文。 FillGeometry 方法填充的閉合的區域,用畫筆幾何形狀的內部。

封裝的筆觸,FingerPaintRenderer 中所示定義私有的結構,稱為 StrokeInfo, 圖 3

圖 3 渲染器的 StrokeInfo 結構和兩個集合

struct StrokeInfo
{
  StrokeInfo() : Color(0, 0, 0),
                 Geometry(nullptr)
  {
  };
  std::vector<D2D1_POINT_2F> Points;
  Microsoft::WRL::ComPtr<ID2D1PathGeometry> Geometry;
  float Width;
  D2D1::ColorF Color;
};
std::vector<StrokeInfo> completedStrokes;
std::map<unsigned int, StrokeInfo> strokesInProgress;

圖 3 也顯示了兩個集合,用於保存 StrokeInfo 物件:CompletedStrokes 集合是一個向量集合,而 strokesInProgress 是作為金鑰使用指標 ID 映射集合。

StrokeInfo 結構的點成員積累彌補腦卒中的所有點。 從這些點,可以構造一個 ID2D1PathGeometry 物件。 圖 4 顯示了執行這項工作的方法。 (為清楚起見,上市不會顯示檢查代碼,四處遊蕩的 HRESULT 值。

圖 4 從點創建路徑幾何圖形

ComPtr<ID2D1PathGeometry>
  FingerPaintRenderer::CreatePolylinePathGeometry
    (std::vector<D2D1_POINT_2F> points)
{
  // Create the PathGeometry
  ComPtr<ID2D1PathGeometry> pathGeometry;
  HRESULT hresult = 
    m_d2dFactory->CreatePathGeometry(&pathGeometry);
  // Get the GeometrySink of the PathGeometry
  ComPtr<ID2D1GeometrySink> geometrySink;
  hresult = pathGeometry->Open(&geometrySink);
  // Begin figure, add lines, end figure, and close
  geometrySink->BeginFigure(points.at(0), D2D1_FIGURE_BEGIN_HOLLOW);
  geometrySink->AddLines(points.data() + 1, points.size() - 1);
  geometrySink->EndFigure(D2D1_FIGURE_END_OPEN);
  hresult = geometrySink->Close();
  return pathGeometry;
}

ID2D1PathGeometry 物件是數位和線段的集合。 要定義的路徑幾何圖形內容,第一次調用 Open 獲得 ID2D1GeometrySink 的物件上。 此幾何圖形接收器,在您調用了 BeginFigure 和 EndFigure 來分隔每個圖中,和那些電話、 AddLines、 AddArc、 AddBezier 和其他人將添加到該圖的線段之間。 (由 FingerPaintRenderer 創建的路徑幾何圖形有只有單一的圖包含多個直線段。後在幾何接收器上調用 Close,路徑幾何圖形準備使用,但已成為不可變的。 您不能重新打開它或改變什麼在它。

出於此原因,以及你的手指在螢幕上移動程式積累點和顯示筆劃中取得了進展,新路徑幾何圖形必須不斷地修造和老那些被遺棄。

這些新的路徑幾何圖形被創建時? 請記住,應用程式可以比視頻刷新率,更快地接收 PointerMoved 事件,所以它沒有在 PointerMoved 處理常式中創建的路徑幾何圖形的感覺。 相反,該程式處理該事件時通過只保存新的點,但如果它重複前面的點 (其中有時會發生)。

圖 5 顯示的三種主要方法在 FingerPaintRenderer 中涉及的筆劃構成點的積累。 新的 StrokeInfo 被添加到 BeginStroke ; 期間 strokeInProgress 集合 它在 ContinueStroke,更新並轉移到 EndStroke 中的 completedStrokes 集合。

圖 5 積累在 FingerPaintRenderer 中的筆劃

void FingerPaintRenderer::BeginStroke(unsigned int id, Point point,
                                      float width, Color color)
{
  // Save stroke information in StrokeInfo structure
  StrokeInfo strokeInfo;
  strokeInfo.Points.push_back(Point2F(point.X, point.Y));
  strokeInfo.Color = ColorF(color.R / 255.0f, color.G / 255.0f,
                            color.B / 255.0f, color.A / 255.0f);
  strokeInfo.Width = width;
  // Store in map with ID number
  strokesInProgress.insert(std::pair<unsigned int, 
    StrokeInfo>(id, strokeInfo));
  this->IsRenderNeeded = true;
}
void FingerPaintRenderer::ContinueStroke(unsigned int id, Point point)
{
  // Never started a stroke, so skip
  if (strokesInProgress.count(id) == 0)
      return;
  // Get the StrokeInfo object for this finger
  StrokeInfo strokeInfo = strokesInProgress.at(id);
  D2D1_POINT_2F previousPoint = strokeInfo.Points.back();
  // Skip duplicate points
  if (point.X != previousPoint.x || point.Y != previousPoint.y)
  {
    strokeInfo.Points.push_back(Point2F(point.X, point.Y));
    strokeInfo.Geometry = nullptr;          // Because now invalid
    strokesInProgress[id] = strokeInfo;
    this->IsRenderNeeded = true;
  }
}
void FingerPaintRenderer::EndStroke(unsigned int id, Point point)
{
  if (strokesInProgress.count(id) == 0)
      return;
  // Get the StrokeInfo object for this finger
  StrokeInfo strokeInfo = strokesInProgress.at(id);
  // Add the final point and create final PathGeometry
  strokeInfo.Points.push_back(Point2F(point.X, point.Y));
  strokeInfo.Geometry = CreatePolylinePathGeometry(strokeInfo.Points);
  // Remove from map, save in vector
  strokesInProgress.erase(id);
  completedStrokes.push_back(strokeInfo);
  this->IsRenderNeeded = true;
}

注意每一種方法將 IsRenderNeeded 設置為 true,指示螢幕需要重繪。 這代表不得不到專案結構的變化之一。 在新創建的專案,基於 Direct2D (XAML) 範本,DirectXPage 和 SimpleTextRenderer 聲明私有 Boolean 資料成員命名為 m_renderNeeded。 然而,只有在 DirectXPage 是實際使用的資料成員。 這不是像它應該是:往往呈現代碼需要確定時必須重畫屏幕。 我取代那些兩個 m_renderNeeded 的資料成員,單一的公共屬性中命名為 IsRender FingerPaintRenderer­需要。 IsRenderNeeded 屬性可以設置從 DirectXPage 和 FingerPaintRenderer,但它僅由 DirectXPage 使用。

在呈現迴圈

一般情況下,一個 DirectX 程式可以重繪其整個螢幕以視頻刷新率,這往往是 60 幀每秒左右。 這一設施給顯示涉及動畫或透明度的圖形中的程式最大的靈活性。 而不是搞什麼螢幕的一部分需要更新以及如何避免擾亂了現有的圖形,只重繪整個螢幕。

在程式如 BasicFingerPaint,只需要重繪時一些變化,這是由真實的 IsRenderNeeded 屬性設置螢幕。 此外,重繪可能想像只限于某些地區的螢幕上,但這並不那麼容易與 Direct2D (XAML) 範本創建的應用程式。

要刷新螢幕,DirectXPage 使用方便的組成­Target::Rendering 事件,該事件在同步過程中與硬體視頻刷新。 在 DirectX 程式中,此事件的處理常式有時被稱為呈現迴圈,和所示圖 6

圖 6 渲染迴圈中 DirectXPage

void DirectXPage::OnRendering(Object^ sender, Object^ args)
{
  if (m_renderer->IsRenderNeeded)
  {
    m_timer->Update();
    m_renderer->Update(m_timer->Total, m_timer->Delta);
    m_renderer->Render();
    m_renderer->Present();
    m_renderer->IsRenderNeeded = false;
  }
}

渲染器的定義是更新方法。 這是視覺物件準備呈現,尤其是如果他們要求提供的由專案範本創建 timer 類的計時資訊。 FingerPaintRenderer 使用 Update 方法來創建路徑幾何圖形從集合點,如果必要。 Render 方法通過 DirectXBase 聲明中定義的 FingerPaintRenderer,但和負責呈現所有的圖形。 命名方法的禮物 — — 它是一個動詞,不是一個名詞 — — DirectXBase,由定義,並且轉移到視頻硬體的複合的視覺效果。

Render 方法開始通過該程式的 ID3D11DeviceCoNtext 物件上調用 BeginDraw,並通過調用 EndDraw 的結論。 之間,它可以調用繪圖函數。 只是每個筆觸的 Render 方法期間呈現:

m_solidColorBrush->SetColor(strokeInfo.Color);
m_d2dContext->DrawGeometry(strokeInfo.Geometry.Get(),
                           m_solidColorBrush.Get(),
                           strokeInfo.Width,
                           m_strokeStyle.Get());

M_solidColorBrush 和 m_strokeStyle 的物件的資料成員。

下一步是什麼?

顧名思義,BasicFingerPaint 是一個非常簡單的應用程式。 因為它不會呈現為點陣圖的筆劃,渴望和持久性的手指畫家可能導致該程式生成並呈現數以千計的幾何圖形。 在一些點,螢幕刷新可能受到影響。

然而,因為程式會保持離散幾何形狀,而不是混合在一起在點陣圖上的一切,該程式可能允許個別筆劃以後刪除編輯,或許通過更改的顏色或寬度,或甚至在螢幕上移動到不同的位置。

因為每個筆劃是單一路徑幾何圖形,運用不同的造型是相當容易。 例如,嘗試更改一行在創建­DeviceIndependentResources FingerPaintRenderer 中的方法:

strokeStyleProps.dashStyle = D2D1_DASH_STYLE_DOT;

現在該程式繪製而不是實線、 虛線所示,結果圖 7。 這種技術只能因為每個筆劃是單個幾何; 如果單個線段組成的筆劃了所有單獨的行,它不會工作。

Rendering a Path Geometry with a Dotted Line
圖 7 渲染帶有虛線路徑幾何圖形

另一個可能的增強是漸層筆刷。 GradientFingerPaint 程式是非常類似于 BasicFingerPaint 除外,它有兩個下拉式列示方塊的顏色,並使用線性漸層畫筆來呈現路徑幾何圖形。 結果就如 [圖 8] 所示,

The GradientFingerPaint Program
圖 8 GradientFingerPaint 程式

雖然每個筆觸有它自己的線性漸層畫筆,漸變的起始點是始終設置為中風界限,起點和終點到右下角的左上角。 描邊用手指在繪製時,您可以經常看到的梯度變化如描邊變長。 但取決於如何繪製筆觸,有時漸變沿著筆劃的長度的和有時你勉強看到漸變在所有與 X 中的兩個筆劃可以明顯看出圖 8

您可以定義漸變,沿完整長度的描邊,描邊的形狀或定位而不考慮延長如果不是很好嗎? 或如何漸變那總是垂直于描邊,描邊的曲折怎麼分?

正如他們在科幻電影中問:怎麼是這種事情甚至可能?

CharlesPetzold 是 MSDN 雜誌和作者的"程式設計視窗,第六版,"(微軟出版社,2012年) 長期貢獻有關 Windows 8 的應用程式編寫的一本書. 他的網站是 charlespetzold.com

感謝以下技術專家對本文的審閱:JamesMcNellis (Microsoft)
JamesMcNellis 是一個 c + + 愛好者和微軟的 Visual c + + 團隊的軟體發展人員在他那裡他生成 c + + 庫和維護的 C 運行時庫 (CRT)。他在微博 @JamesMcNellis,並通過線上其他地方可以發現 HTTP://jamesmcnellis.com/