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"中讨论的道格 · 埃里克森 (msdn.microsoft.com/­杂志/jj991975),有三种方法结合 XAML 和 DirectX 在 Windows 应用商店中应用的范围内。我会用 SwapChainBackgroundPanel 作为根子元素的 XAML 页导数的办法。此对象作为一个绘图表面 Direct2D 和 Direct3D 图形,但它也可铺 WinRT 的控件,例如应用程序栏。

Visual Studio 2012 包含这样的程序的项目模板。在新建项目对话框中,选择 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

您可以定义渐变,沿完整长度的描边,描边的形状或定位而不考虑延长如果不是很好吗? 或如何渐变那总是垂直于描边,描边的曲折怎么分?

正如他们在科幻电影中问:怎么是这种事情甚至可能?

Charles Petzold 是 MSDN 杂志和作者的"编程窗口,第六版,"(微软出版社,2012年) 长期贡献有关 Windows 8 的应用程序编写的一本书. 他的网站是 charlespetzold.com

衷心感谢以下技术专家对本文的审阅:James McNellis (Microsoft)
James McNellis 是 C++ 迷,也是 Microsoft 的 Visual C++ 团队的软件开发人员,他的工作是构建精彩的 C++ 库和维护 C 运行时库 (CRT)。他的 Tweet 是 @JamesMcNellis,也可以通过 http://jamesmcnellis.com/ 的其他位置在线联系他。