UQYT

矢量图形和 WPF 形状类

即使在相对乏味的二维矢量图形领域中,Windows® Presentation Foundation (WPF) 仍会要求程序员们学习许多新概念。在 WPF 中,图形对象已提升到与控件几乎*等的地位,经常参与布局并接收鼠标、键盘和笔针输入。此外,图形系统会保留这些图形对象以便不再像过去使用图形时那样频 繁地进行重绘,并且它们还可移动和用作数据绑定的目标。
开 始研究 WPF 时,我立刻想到将 System.Windows.Shapes 作为包含“婴儿”图形类的命名空间。这些类似乎适合于显示简单的线条和矩形,但我认为成熟的 WPF 程序可能希望通过重写 OnRender 方法并调用 DrawingContext 类中的方法来实现各种功能。
DrawGeometry 方法似乎特别诱人:在 WPF 中,Geometry 对象是已连接和未连接直线、弧线和 Bézier 曲线(在传统图形编程中称为“路径”)的组合。DrawGeometry 的三个参数包括 Geometry 对象、用于绘制 Geometry 的直线和曲线的 Pen 以及用于填充封闭区域的 Brush。

 

Shapes 命名空间的作用
很快,我发现自己对 WPF 矢量图形的第一印象是错误的。大多数 WPF 程序并不需要重写 OnRender 方法和调用 DrawingContext 类中的方法。虽然重写 OnRender 是个不错的培训练习,但通常在大多数主流应用程序中都不必重写它。
因 此,至少在我看来,System.Windows.Shapes 命名空间成为了用于在 WPF 中呈现二维矢量图形的命名空间。System.Windows.Shapes 命名空间包含以下类:Shape(抽象类)和 Line、Polyline、Polygon、Path、Rectangle 和 Ellipse(都是封装类)。
Shape 类自身是从 FrameworkElement 派生而来。最重要的 Shape 派生类无疑是 Path;该类与 DrawingContext 的 DrawGeometry 方法具有相同的功能,但麻烦要少得多。在 XAML 中使用 Path 类时,甚至可以使用编码绘图命令字符串来定义 Geometry 对象。
这并不表示 Shapes 类为所有应用程序构建了一个通用的矢量图形解决方案。每个类的各个实例都是一个成熟的 WPF 元素,并且可能带来更大开销。此外,每个类都只有一个画笔和一个填充画刷,而且提供的颜色可能比您需要的要少。
要 呈现包含多种颜色的复杂矢量图形,有多种方法可供选择。当然,可以创建多个 Path 对象,但如果希望将复杂图像用作自身的实体,则这种方法可能过于繁杂。此时,更好的解决方案是使用 DrawingGroup 类,它可以包含多个 GeometryDrawing 对象,而每个此类对象又都包含 Geometry、画笔和填充画刷。DrawingGroup 对象可能是 WPF 中最接*传统图形元文件的实体。DrawingGroup 对象可作为画刷的基础(通过 DrawingBrush),或者可通过 Image 类将其变成显示的 DrawingImage 对象。
如果仅需要适当数量的图形基元(尤其是当这些对象需要接收鼠标、键盘或笔针输入,或者进行自身转换时),Shapes 命名空间中的类将是理想之选。
现在,我将介绍从 Shapes 命名空间中的唯一未封装类 Shape 进行派生。可从 Shape 类进行派生以实现自定义矢量图形基元。从 Shape 派生是确保这些自定义基元使用 WPF 布局系统的协议的最简单方法。

 

公开 Pen
虽 然我并未见过 Shape 的源代码,但我可通过基本原则了解有关该类的一些信息。由于 Shape 派生自 FrameworkElement,所以它将重写 MeasureOverride、ArrangeOverride 和 OnRender 方法。我想 OnRender 重写应该非常简单,仅需调用 DrawingContext 的 DrawGeometry 方法,将其传递给 Brush、Pen 和 Geometry 对象即可。
虽 然 DrawGeometry 调用的 Brush 参数毫无疑问是来自 Shape 的 Fill 属性,但 Shape 并未定义 Pen 类型的属性。相反,Shape 定义了九个单独的属性(内部构建在 Pen 对象中)。这九个属性均以单词 Stroke 开头,并且实际有助于在 XAML 和代码中相当轻松地使用 Shape 派生类。
例如,如果要在 XAML 中定义包含 EllipseGeometry 的 GeometryDrawing 对象,则它可能类似如下:
 
<GeometryDrawing Brush="Red">
    <GeometryDrawing.Geometry>
        <EllipseGeometry ... />
    </GeometryDrawing.Geometry>
    <GeometryDrawing.Pen>
        <Pen Brush="Blue" Thickness="3" />
    </GeometryDrawing.Pen>
</GeometryDrawing>
请注意,需在属性元素中定义 Pen 对象,并且即使使用该标记,您仍未指定 GeometryDrawing 的实际呈现方式。而在 XAML 中与 Ellipse 类等同的实现如下所示:
 
<Ellipse Fill="Red" Stroke="Blue" StrokeThickness="3" ... />
当 Shape 类调用 DrawingContext 的 DrawGeometry 方法时,它还需要 Geometry 对象。此 Geometry 对象即是本专栏剩余部分的重心。

 

两种绘图模式
Shape 类最容易引起混淆的一个方面是它能够封装两种不同的绘图模式。
第一种模式更为传统。我们姑且将其称为“坐标”模式。在使用 Line、Polyline、Polygon 和 Path 类时,可以指定实际的坐标点(这些点定义了组成图形的直线和曲线),类似如下:
 
Line line = new Line();
line.X1 = 100;
line.Y1 = 50;
line.X2 = 200;
line.Y2 = 150;
line.Stroke = Brushes.Blue;
line.StrokeThickness = 12;
结果如图 1 所示。尽管 Canvas 面板可能是 Line 元素最常见的父级,但实际上可将 Line 放到已放有另一控件或元素的任意位置。图 1 中的程序将 Line 设置为 Window 的 Content 属性。
Figure 1 Window 中的 Line 元素 
Line、Polyline、Polygon 和 Path 类从 Shape 继承 Stretch 属性,它将默认值定义为 Stretch.None。如果将该属性设置为 Stretch.Fill,Line 将填充其父级(如图 2 所示)。
Figure 2 Stretch 属性设置为 Fill 的 Line 元素 
Shape 还实现另一种绘图模式,它或许更类似 WPF 的呈现风格。我们姑且将其称为“自动调整大小”模式。此模式体现在 Rectangle 和 Ellipse 类中。图 3 显示了设置为 Window 的 Content 属性的 Ellipse 元素。Ellipse 元素的创建方法如下所示:
Figure 3 Window 中的 Ellipse 元素 
 
   Ellipse elips = new Ellipse();
   elips.Fill = Brushes.Red;
   elips.Stroke = Brushes.Blue;
   elips.StrokeThickness = 12;
并 未提供 Ellipse 元素的任何坐标或大小信息;图形默认填充其父级的内部区域。Rectangle 和 Ellipse 将 Stretch 属性的默认值设置为 Stretch.Fill。如果将该属性设置为 Stretch.None,则椭圆将缩成仅边线可见的一个小球(如图 4 所示)。
Figure 4 Stretch 属性设置为 None 的 Ellipse 元素 
还 可通过将 Rectangle 或 Ellipse 的 HorizontalAlignment 和 VerticalAlignment 属性设置为 Stretch 以外的其他值来达到这一效果。(本专栏的可下载代码中包含有名为 StretchExplore 的一个程序,可通过它查看 Line 和 Ellipse 的各种延伸选项以及我在本文中开发的两个 Shape 派生类。)
如 果希望 Rectangle 或 Ellipse 元素为特定大小,可使用 FrameworkElement 定义的 Width 和 Height 属性,或者设置 MinWidth、MaxWidth、MinHeight 和 MaxHeight 来指定值的范围。在 Canvas 面板上呈现 Rectangle 或 Ellipse 时,必须设置这些属性。
如果从 Shape 派生自定义图形基元,“坐标”模式比“自动调整大小”模式更容易实现。

 

三个只读属性
Shape 定义了 14 个供派生类继承的属性,我已经提及其中的 11 个属性:9 个以单词 Stroke 开头的画笔相关属性、Fill 属性和 Stretch。其他 3 个属性仅针对 get 访问器定义:DefiningGeometry(protected 和 abstract)、GeometryTransform(public 和 virtual)和 RenderedGeometry(public 和 virtual)。
使 用“坐标”模式从 Shape 派生以实现图形基元时,唯一需要重写的属性是 DefiningGeometry。顾名思义,需通过返回定义图形基元的 Geometry 类型的对象来实现 DefiningGeometry。很可能 Shape 派生类会包含定义图形的其他属性。在大多数情况下,您应该使用依赖属性支持这些属性,以使它们成为数据绑定和动画的目标。
由于可随时调用 DefiningGeometry(特别是在影响基元的属性发生更改时),所以重要的是在实现 DefiningGeometry 时无需定期从堆中分配内存。如果每次调用 DefiningGeometry 都会造成堆分配,则最终 Microsoft® .NET Framework 垃圾收集器必需采取行动。应尝试清除所有类实例化的 DefiningGeometry 代码,并且还应了解与某些方法相关的隐式堆分配。下面,我将向您介绍几种可用于避免在 Shape 派生时造成堆分配的技术。
首先是个简单示例,假设您倾向于使用 Line 基元的替代方法,从而可使用 Point 对象而不是double 对象对来设置开始和结束坐标点。从 Shape 派生的 PointLine 类如图 5 所示。
 
using System;
using System.Windows;
using System.Windows.Media;
using System.Windows.Shapes;

namespace Petzold.Shapes
{
    public class PointLine : Shape
    {
        LineGeometry linegeo = new LineGeometry();

        // Dependency properties
        public static readonly DependencyProperty StartPointProperty = 
            LineGeometry.StartPointProperty.AddOwner(
                typeof(PointLine),
                new FrameworkPropertyMetadata(new Point(0, 0),
                    FrameworkPropertyMetadataOptions.AffectsMeasure));

        public static readonly DependencyProperty EndPointProperty = 
            LineGeometry.EndPointProperty.AddOwner(
                typeof(PointLine),
                new FrameworkPropertyMetadata(new Point(0, 0),
                    FrameworkPropertyMetadataOptions.AffectsMeasure));

        public Point StartPoint
        {
            set { SetValue(StartPointProperty, value); }
            get { return (Point)GetValue(StartPointProperty); }
        }

        public Point EndPoint
        {
            set { SetValue(EndPointProperty, value); }
            get { return (Point)GetValue(EndPointProperty); }
        }

        // Required DefiningGeometry override
        protected override Geometry DefiningGeometry
        {
            get 
            { 
                linegeo.StartPoint = StartPoint;
                linegeo.EndPoint = EndPoint;
                return linegeo;
            }
        }
    }
}

请 注意,StartPoint 和 EndPoint 都是使用依赖关系属性定义的。我使用了与 LineGeometry 类中对应属性相同的名称,这样就可将 PointLine 类添加为这些属性的新的所有者。这两个属性都设置了 AffectsMeasure 标记,因为它们都会影响元素的大小。如果任一属性发生变化,将产生新的布局过程(最终会在 Shape 中实现对 OnRender 的调用)。如果定义的属性只影响形状的外观而不影响其大小,则可改为使用 AffectsRender 标记以避免初始化布局过程。
DefiningGeometry 属性的 get 访问器将根据 StartPoint 和 EndPoint 属性返回 LineGeometry 对象。您应当已注意到,此类重用了定义为字段的单个 LineGeometry 对象,而不是在每次调用 DefiningGeometry 时创建一个新的 LineGeometry 对象。在从 Shape 派生时为避免堆被新对象实例搅乱,这一技术至关重要。
图 6 显示了另一个相对直观的 Shape 派生类。CenteredEllipse 类允许通过指定圆心、水*和垂直半径以及旋转角度来绘制椭圆。CenteredEllipse 将自身添加为 EllipseGeometry 所定义 Center、RadiusX 和 RadiusY 属性的所有者,并且还从 ArcSegment 获得 RotationAngle 属性。请注意,CenteredEllipse 创建了作为字段存储的 RotationTransform,并重用它来关联转换和 EllipseGeometry 对象。
 
using System;
using System.Windows;
using System.Windows.Media;
using System.Windows.Shapes;

namespace Petzold.Shapes
{
  public class CenteredEllipse : Shape
  {
    EllipseGeometry elipGeo = new EllipseGeometry();
    RotateTransform xform = new RotateTransform();

    // Dependency properties
    public static readonly DependencyProperty CenterProperty =
      EllipseGeometry.CenterProperty.AddOwner(
        typeof(CenteredEllipse),
        new FrameworkPropertyMetadata(new Point(0, 0), 
          EllipsePropertyChanged));

    public static readonly DependencyProperty RadiusXProperty =
      EllipseGeometry.RadiusXProperty.AddOwner(
        typeof(CenteredEllipse),
        new FrameworkPropertyMetadata(0.0,
          EllipsePropertyChanged));

    public static readonly DependencyProperty RadiusYProperty =
      EllipseGeometry.RadiusYProperty.AddOwner(
        typeof(CenteredEllipse),
        new FrameworkPropertyMetadata(0.0,
          EllipsePropertyChanged));

    public static readonly DependencyProperty RotationAngleProperty =
      ArcSegment.RotationAngleProperty.AddOwner(
        typeof(CenteredEllipse),
        new FrameworkPropertyMetadata(0.0,
          TransformPropertyChanged));

    static void EllipsePropertyChanged(DependencyObject obj,
      DependencyPropertyChangedEventArgs args)
    {
      (obj as CenteredEllipse).EllipsePropertyChanged(args);
    }

    static void TransformPropertyChanged(DependencyObject obj,
      DependencyPropertyChangedEventArgs args)
    {
      (obj as CenteredEllipse).TransformPropertyChanged(args);
    }

    void EllipsePropertyChanged(DependencyPropertyChangedEventArgs args)
    {
      elipGeo.Center = Center;
      elipGeo.RadiusX = RadiusX;
      elipGeo.RadiusY = RadiusY;
      InvalidateMeasure();
    }

    void TransformPropertyChanged(DependencyPropertyChangedEventArgs args)
    {
      xform.Angle = RotationAngle;
      xform.CenterX = Center.X;
      xform.CenterY = Center.Y;
      InvalidateMeasure();
    }

    public CenteredEllipse()
    {
      elipGeo.Transform = xform;
    }

    // Public CLR properties
    public Point Center
    {
      set { SetValue(CenterProperty, value); }
      get { return (Point)GetValue(CenterProperty); }
    }

    public double RadiusX
    {
      set { SetValue(RadiusXProperty, value); }
      get { return (double)GetValue(RadiusXProperty); }
    }

    public double RadiusY
    {
      set { SetValue(RadiusYProperty, value); }
      get { return (double)GetValue(RadiusYProperty); }
    }

    public double RotationAngle
    {
      set { SetValue(RotationAngleProperty, value); }
      get { return (double)GetValue(RotationAngleProperty); }
    }

    // Required DefiningGeometry override
    protected override Geometry DefiningGeometry
    {
      get
      {
        return elipGeo;
      }
    }
  }
}

CenteredEllipse 说明了一种替代结构,此结构适用于定义 Geometry 需要大量处理时间的情形。不是在依赖关系属性中设置 AffectsMeasure 标记,而是该类定义了属性更改处理程序,该程序将设置两个私有字段的属性,然后调用 InvalidateMeasure 来初始化新的布局过程。
您 会发现这些 PointLine 和 CenteredEllipse 类的行为与常规 Line 类相同。如果将 Stretch 属性设置为 Stretch.None 以外的值,则 Shape 类将计算调整几何图形大小和移动它所需的转换以填满其父级的内部区域。很显然,此转换基于 Geometry 本身的大小(可通过 Bounds 属性获取),但它还需要考虑 StrokeThickness 属性以至少使对象的大部分区域仍处于父级的矩形之内。
这 些计算是在 Shape 类中由后台执行的。Shape 计算的转换是通过公共 GeometryTransform 属性提供的。(默认值是静态属性 Transform.Identity。)Shape 还会在 GeometryTransform 属性转换 DefiningGeometry 属性时计算 RenderedGeometry 属性。Shape 中的 OnRender 方法正是使用这个 RenderedGeometry 属性来绘制图形的。
Shape 应将该转换应用于几何图形而非图像本身这一点非常重要。如果将转换应用于图像,则它还会影响用于绘制线条的画笔的宽度。
当然,好的一面就是不必担心这个转换计算。只需提供 DefiningGeometry 即可。
本专栏的源代码是一个名为 DerivingFromShape 的单一 Visual Studio 解决方案。Shape 的自我派生位于名为 Petzold.Shapes 的 DLL 工程中。该 DLL 还包括我曾在 2007 年 4 月的博客 (charlespetzold.com/blog/2007/04/191200.html) 中介绍的实现带箭头的直线和折线基元的 Shape 派生类。

 

*行线和曲线
让 我们来点儿更具挑战性的内容。假设您想要实质是 Path 增强版的一个类。Path 类定义了一个名为 Data、类型为 Geometry 的属性。Path 很可能会从其 DefiningGeometry 属性返回这个相同的 Geometry 对象,从而使 Path 成为最简单的 Shape 派生类。
新类将通过为 Path 的 Data 属性添加所有者来定义一个 Data 属性。但是,新类不仅可呈现 Geometry 对象,还会*行地呈现 Geometry(如图 7 所示)。新的 ParallelPath 类包含表示线条数量的 Number 属性(图 7 中的 5)以及表示线条之间间距的 Gap 属性。在图 7 中,StrokeThickness 设置为 3,而 Gap 设置为 4。
Figure 7 ParallelPathDemo 程序 (单击该图像获得较大视图)
但请注意,在某些情况下,ParallelPath 所用的算法可能会出错,并且有时会出现不相关的线条。但如果*行路径数量合理并使各路段之间保持*滑,那么它会非常有效。如果使用画刷填充 ParallelPath,它还可能产生意外的(尽管完全确定)结果。
生 成*行线这一概念在只涉及到直线时比较简单,但 ParallelPath 类还可用于 Bézier 曲线和弧线。与使用 Path 一样,可将 ParallelPath 的 Data 属性设置为任何 Geometry 类型的对象,以下七个封闭类均从这一抽象类派生而来:CombinedGeometry、EllipseGeometry、GeometryGroup、 LineGeometry、PathGeometry、RectangleGeometry 和 StreamGeometry。
图 7 中的图像是由以下代码所生成的:
 
<ps:ParallelPath Stroke="Black" StrokeThickness="3"
  Number="5" Gap="4" Tolerance="10">
  <ps:ParallelPath.Data>
    <PathGeometry>
      <PathFigure StartPoint="200 100">
        <PolyLineSegment Points="350 200, 450 50" />
        <PolyBezierSegment 
          Points="500 0, 600 300, 350 300, 
                  100 300, 50 0, 200 100" />
      </PathFigure>                        
    </PathGeometry>
  </ps:ParallelPath.Data>
</ps:ParallelPath>
"ps" XML 命名空间与 Petzold.Shapes CLR 命名空间相关。还可将 Data 属性设为路径微型语言中的字符串;但在这种情况下,单个坐标无法成为数据绑定或动画的目标。
Geometry 用途最广泛的派生是 PathGeometry。PathGeometry 对象是 PathFigure 对象的集合,这些对象又是作为 PathSegment 对象集合存储的已连接直线和曲线的集合。PathSegment 是一个抽象类,可从它派生出七个用于呈现直线、弧线和 Bézier 曲线的类。这些段的任意坐标点都可以作为数据绑定或动画的目标。
无 论为 Data 属性设置何种类型的 Geometry 对象,ParallelPath 都将生成一个 PathGeometry 对象来描述*行直线和曲线。但是,此算法不会尝试查找与某条 Bézier 曲线*行的另一条 Bézier 曲线。而是完全基于折线:输入是一条或多条折线,而输出包含对应于每条输入折线的多条折线。因此,ParallelPath 需要“*铺”输入几何图形 — 即表示将整个几何图形(包括弧线和 Bézier 曲线)转换成折线*似。
如 果 ParallelPath 并未提供您所预期的结果(例如,在某些段的端点上出现一些不相关的线条),可将 Tolerance 属性设置为大于 1 的某个值;比如像 ParallelPathDemo 程序那样设置为 10。该 Tolerance 值等于(*似)*铺后几何图形的每条折线中与设备无关的单元数。
幸 运的是,Geometry 类包含用于*铺几何图形的方法。该方法名为 GetFlattenedPathGeometry,并返回包含全部 PathFigure 对象和 PolyLineSegment 对象的 PathGeometry 对象。(PathFigure 类本身定义了 GetFlattenedPathFigure 方法来返回另一个 PathFigure。)
不 幸的是,GetFlattenedPathGeometry 方法需要从托管堆中分配内存,以创建 PathGeometry 对象以及组成几何图形的 PathFigure 和 PolyLineSegment 子对象。此类内存分配可能导致问题。假设 ParallelPath 的 Data 属性已设为 PathGeometry 对象,并且 PathGeometry 的其中一个坐标点为移动点。这意味着 ParallelPath 需要针对移动坐标点的每一次变化重新生成 DefiningGeometry 对象。通过调用 GetFlattenedPathGeometry,ParallelPath 隐式执行了重复的堆分配,从而最终导致需要运行 .NET 垃圾收集器。

 

最小化堆分配
由 于 GetFlattenedPathGeometry 很可能执行多个内存分配,因此我决定编写自己的路径*铺例程。通常情况下,这需要创建 PathGeometry、PathFigure 和 PolyLineSegment 对象的新实例,但我还编写了一些简单的方法用以缓存和重用这些对象。路径*铺和缓存方法位于一个名为 PathGeometryHelper 的类中。明显地,此类需要执行一些内存分配(至少在开始时),但例程内存分配不需要,所以它(我希望)能从根本上解决问题。
在 以下两种情况下,我的 PathGeometryHelper 类无法实现其*铺算法。一种情况是当 Geometry 为 StreamGeometry 类型时。原因是 StreamGeometry 构建自对 StreamGeometryContext 对象的调用,但除非通过静态 PathGeometry.CreateFromGeometry 方法,否则无法访问几何图形本身。第二种情况是当 Geometry 为 CombinedGeometry 类型时,它包含两个布尔组合的 Geometry 对象。
对 于这两种情况,PathGeometryHelper 类将不做处理,只调用 GetFlattenedPathGeometry。(实际上,由于 GeometryGroup 对象可以包含 StreamGeometry 或 CombinedGeometry 类型对象的子对象或孙子对象,因此如果路径的任何部分是这些类型的对象,我将调用 GetFlattenedPathGeometry。)这些例外情况代表某种让步,但不是非常严重。无法移动 StreamGeometry 类型的对象,而且可能很少会使用 CombinedGeometry。
为 帮助该类进一步避免内存分配,ParallelPath 类定义了两个 PathGeometry 类型的字段。一个名为 pathGeoSrc(“PathGeometry source”),负责存储*铺的路径;另一个名为 pathGeoDst(“PathGeometry destination”),负责存储展开成*行线的路径。另一字段存储名为 pathHelper 的 PathGeometryHelper 实例。
ParallelPath 定义的四个属性与名为 SourcePropertyChanged 和 DestinationPropertyChanged 的两个属性更改回调相关联。只要 Data 或 Tolerance 属性发生更改,SourcePropertyChanged 处理程序就会首先缓存上一个 PathGeometry 源,方法如下:
 
pathHelper.CacheAll(pathGeoSrc);
CacheAll 方法缓存 PathGeometry 对象自身以及 PathGeometry 中包含的所有 PathFigure 和 PolyLineSegment 对象。但是,如果上一个 PathGeometry 是从对 Geometry 的 GetFlattenedPathGeometry 方法的调用获得,则对象将冻结且无法再进行修改。因此不会缓存此类对象。
随后,SourcePropertyChanged 处理程序*铺传入几何图形,并使用以下代码将其存储为 pathGeoSrc:
 
pathGeoSrc = pathHelper.FlattenGeometry(Data, Tolerance);
只 要可能,FlattenGeometry 方法将使用缓存中的 PathGeometry、PathFigure 和 PolyLineSegment 对象。最后,SourcePropertyChanged 调用与 Number 和 Gap 属性关联的属性更改回调,如下所示:
 
DestinationPropertyChanged(args);
DestinationPropertyChanged 回调首先将包含*行线的 PathGeometry 组件返回到缓存中,如下所示:
 
pathHelper.CacheAll(pathGeoDst);
然后,回调调用 ParallelPath 中的 GenerateGeometry 方法生成多条*行路径,如下所示:
 
pathGeoDst = GenerateGeometry(pathGeoSrc);
GenerateGeometry 方法还将使用缓存中的 PathGeometry、PathFigure 和 PolyLineSegment 对象,以及作为字段存储并在每次调用时重用的 List<Point> 类型的对象。如果缓存中存在足够多的对象,整个过程将不需要进行任何堆分配。最后,DestinationPropertyChanged 回调初始化布局过程,如下所示:
 
InvalidateMeasure();
DefiningGeometry 属性仅返回 pathGeoDst。

 

更好的加宽路径
写完 ParallelPath 类之后,另一个类又来了。这个类使我有机会解决困扰我长达 15 年的一个问题。
有时,当程序员开始使用宽线条时,他们很想知道有无方法可以描画轮廓线。除非亲眼看到,否则整个概念显得完全不可思议。但是,Win32® 一开始就可以实现并支持这一概念。在 WPF 中,该工具以 GetWidenedPathGeometry 方法的形式内置于 Geometry 类中。
我们来看一个示例。假设您的 Geometry 名为 geo,并将它用于 Path 对象,如以下代码所示:
 
path = new Path();
path.Data = geo;
path.Stroke = Brushes.Blue;
path.StrokeThickness = 40;
图 8 显示了该 Path 对象。
Figure 8 使用 Path 绘制加宽线 
Geometry 类的 GetWidenedPathGeometry 需要 Pen 对象,但它将忽略 Brush 属性并仅使用 Pen 的物理尺寸和特征,如下所示:
 
Pen pen = new Pen(null, 40);
PathGeometry pathGeo = geo.GetWidenedPathGeometry(pen);
请 注意,我指定了与 Path 代码中相同的画笔粗细(即 40 单位)。该新 PathGeometry 指出了 Geometry 的轮廓(就像使用 Pen 绘制一样)。现在,使用这个新 Geometry 而非通过指定一个较细的画笔和填充画刷来绘制一个新的 Path 图形:
 
path = new Path();
path.Data = pathGeo;
path.Stroke = Brushes.Red;
path.StrokeThickness = 6;
path.Fill = Brushes.Blue;
结果如图 9 所示。实质上已描绘出原始线条。
Figure 9 使用 Path 绘制加宽路径 
很显然,这个加宽路径方法很奏效,但却存在着两个问题。第一个问题是 GetWidenedPathGeometry 是一个方法。如果创建带有一些可移动点的 PathGeometry,则必须重复调用 GetWidenedPathGeometry 来移动加宽路径。
路径加宽存在的第二个问题是明显的人为错误。很容易就可以看出错误来源 — 曾在 Windows 中使用过加宽路径的人都非常熟悉此类错误。
鉴于这些原因,我创建了一个 WidenedPath 类,它包含类似 Path 的 Data 属性和作为加宽参数的 WideningPen 属性。图 10 中的图像是由以下代码所创建:
Figure 10 Window 中的 Line 元素 
 
   WidenedPath widePath = new WidenedPath();
   widePath.Data = geo;
   widePath.WideningPen = pen;
   widePath.Stroke = Brushes.Red;
   widePath.StrokeThickness = 6;
   widePath.Fill = Brushes.Blue;
您 可能会认为 WidenedPath 只是非常简单地修改了一下 ParallelPath,其实还有其他一些复杂之处。正确完成这个类后,路径加宽必须考虑线头和连结点 — Pen 的 StartLineCap、EndLineCap 和 LineJoin 属性。它要求路径加宽算法潜在地向路径添加曲线(如图 11 所示)。
Figure 11 带有端点和连结点的 WidenedPath 类 
但 是,WidenedPath 并不会考虑 Pen 的 DashStyle、DashCap 和 MiterLimit 属性。尽管 WidenedPath 比 GetWidenedPathGeometry 的人为错误要少,但仍不可完全避免。与 ParallelPath 一样,可通过增加 Tolerance 属性来最小化这些人为错误。

 

其他模式
尽管我重点介绍的是使用由 Shape 类实现的“坐标”模式的类,但我相信您应该还没有忘记“自动调整大小”模式吧。
通 过对 Rectangle 和 Ellipse 的实验得知,这些类始终从 GeometryTransform 方法返回 Transform.Identity,而非根据通过重载 MeasureOverride 和 ArrangeOverride 方法所获得的信息来确定 DefiningGeometry 的大小。
在创建 RegularPolygon 类时对该方法进行实验之后,我尝试就像使用“坐标”模式一样编写该类的代码。我并未尝试使用任何坐标或大小,而是将折线放在圆心为 (0, 0) 且半径为 1 的圆上,然后让 WPF 处理剩下的工作。
结 果肯定会与 Rectangle 或 Ellipse 有所不同,但当 Width 和 Height 属性设为 NaN 且 Stretch 设为 Uniform 或 Fill 时差异最大。在许多此类情况下,RegularPolygon 可见,而 Ellipse 则成为一个小球。
源 代码中包含一个名为 StretchExplore 的程序,可使用它来查看各种 Stretch、HorizontalAlignment、VerticalAlignment、Width 和 Height 设置组合下的 Ellipse、 RegularPolygon、Line 和 PointLine 图形。然后,您可以自行确定我在 RegularPolygon 编写的这个简单方法是否适用于您自己的应用程序。

分类:

技术点:

相关文章:

  • 2021-12-22
  • 2021-12-22
  • 2021-11-30
  • 2021-10-28
  • 2021-11-21
  • 2021-12-22
  • 2018-06-06
  • 2019-01-23
猜你喜欢
  • 2021-12-22
  • 2021-12-19
  • 2021-11-11
  • 2021-12-22
  • 2021-09-21
  • 2021-12-19
  • 2021-12-13
相关资源
相似解决方案