程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> .NET網頁編程 >> 關於.NET >> .net基礎:矢量圖形和WPF形狀類

.net基礎:矢量圖形和WPF形狀類

編輯:關於.NET

即使在相對乏味的二維矢量圖形領域中,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 所示。

Figure 5 PointLine.cs—the PointLine Class

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 對象。

Figure 6 CenteredEllipse.cs—the CenteredEllipse Class

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 回調初始化布局過程,如下所示:

1.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 編寫的這個簡 單方法是否適用於您自己的應用程序。

請將您想詢問的問題和提出的意見發送至 [email protected].

本文配套源碼

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved