天天看點

使用 WPF 以程式設計方式生成圖形 使用 WPF 生成圖形

<a href="http://code.msdn.microsoft.com/mag201006TestRun">下載下傳代碼示例</a>

基于一組與測試有關的資料來生成圖形是一項常見的軟體開發任務。根據我的經驗,最常用的方法是将資料導入 Excel 電子表格,然後使用 Excel 内置的繪圖功能手動生成圖形。這種做法适用于大多數情況,但是如果基礎資料頻繁更改,則手動建立圖形可能很快就變得枯燥乏味。在本月的專欄中,我将向您示範如何使用 Windows Presentation Foundation (WPF) 技術自動執行該過程。若要了解我所闡述的觀點,請看圖 1。該圖按日期顯示打開和已關閉的錯誤的計數,是使用從簡單文本檔案讀取資料的一個短小 WPF 程式動态生成的。

使用 WPF 以程式設計方式生成圖形 使用 WPF 生成圖形

圖 1 以程式設計方式生成的錯誤計數圖

打開的錯誤(用藍色線條上的紅圈表示)在開發工作開始後不久迅速增多,然後随時間推移逐漸減少(這是在估計零錯誤反彈日期時可能十分有用的資訊)。已關閉的錯誤(綠色線條上的三角形标記)則穩步增多。

雖然這些資訊可能十分有用,但在生産環境中,開發資源通常是有限的,是以手動生成這類圖形可能不太值得。但是使用我将說明的技術,可快速而輕松地建立這類圖形。

在下面幾節中,我将詳細展示和說明用于生成圖 1 中圖形的 C# 代碼。本專欄假設您已具備 C# 編碼方面的中級知識,并對 WPF 有最基本的了解。不過,即使您從前沒有接觸過這兩個領域,我認為您也能夠了解我所讨論的内容。我相信您會發現這項技術對于您的綜合技能是個有趣且有用的補充。

我首先啟動 Visual Studio 2008,并使用 WPF 應用程式模闆建立一個 C# 項目。從“建立項目”對話框右上方區域的下拉控件中選擇 .NET Framework 3.5 庫。将項目命名為 BugGraph。雖然您可以使用 WPF 基元以程式設計方式生成圖形,但我使用了友善的 DynamicDataDisplay 庫(由 Microsoft 研究院實驗室開發)。

接下來建立源資料。在生産環境中,您的資料可以位于 Excel 電子表格、SQL 資料庫或 XML 檔案中。為簡單起見,我使用簡單文本檔案。在 Visual Studio 解決方案資料總管視窗中,右鍵單擊項目名稱,然後從上下文菜單中選擇“添加”|“建立項”。然後選擇“文本檔案”項,将檔案重命名為 BugInfo.txt,并單擊“添加”按鈕。下面是虛拟資料:

01/15/2010:0:0

02/15/2010:12:5

03/15/2010:60:10

04/15/2010:88:20

05/15/2010:75:50

06/15/2010:50:70

07/15/2010:40:85

08/15/2010:25:95

09/15/2010:18:98

10/15/2010:10:99

每行中的第一個冒号分隔字段包含一個日期,第二個字段包含關聯日期的打開錯誤數,第三個字段顯示已關閉錯誤數。正如稍後您将看到的那樣,DynamicDataDisplay 庫可以處理大多數類型的資料。

接下來,我輕按兩下 Window1.xaml 檔案,以附加元件目的 UI 定義。添加對繪圖庫 DLL 的引用,并對 WPF 顯示區域的預設 Width、Height 和 Background 特性稍加修改,如下所示:

點選(此處)折疊或打開

xmlns:d3="http://research.microsoft.com/DynamicDataDisplay/1.0"

Title="Window1" WindowState="Normal" Height="500" Width="800" Background="Wheat"&gt;

然後,添加關鍵的繪圖對象,如圖 2 所示。

圖 2 添加關鍵的繪圖對象

d3:ChartPlotter Name="plotter" Margin="10,10,20,10"&gt;

            d3:ChartPlotter.HorizontalAxis&gt;

                d3:HorizontalDateTimeAxis Name="dateAxis"/&gt;

            /d3:ChartPlotter.HorizontalAxis&gt;

            d3:ChartPlotter.VerticalAxis&gt;

                d3:VerticalIntegerAxis Name="countAxis"/&gt;

            /d3:ChartPlotter.VerticalAxis&gt;

            d3:Header FontFamily="Arial" Content="Bug Information"/&gt;

            d3:VerticalAxisTitle FontFamily="Arial" Content="Count"/&gt;

            d3:HorizontalAxisTitle FontFamily="Arial" Content="Date"/&gt;

        /d3:ChartPlotter&gt;

ChartPlotter 元素是主要顯示對象。在該元素的定義中,我添加了水準日期軸和垂直整數軸的聲明。DynamicDataDisplay 庫的預設軸類型是具有小數部分的數字(在 C# 術語中稱為 double 類型);該類型無需顯式軸聲明。我還添加了一個标頭标題聲明和軸标題聲明。圖 3 顯示迄今為止的設計。

使用 WPF 以程式設計方式生成圖形 使用 WPF 生成圖形

圖 3 BugGraph 程式設計

配置了項目的靜态内容後,便已準備就緒,可以添加用于讀取源資料并以程式設計方式生成圖形的代碼。在解決方案資料總管視窗中輕按兩下 Window1.xaml.cs 檔案,以将該 C# 檔案加載到代碼編輯器中。圖 4 列出了生成圖 1 中圖形的程式的完整源代碼。

圖 4 BugGraph 項目的源代碼

using System;

using System.Collections.Generic;

using System.IO;

using System.Windows;

using System.Windows.Media;

using Microsoft.Research.DynamicDataDisplay;

using Microsoft.Research.DynamicDataDisplay.DataSources;

using Microsoft.Research.DynamicDataDisplay.PointMarkers;

namespace BugInfo

{

    public partial class Window1 : Window

    {

        public Window1()

        {

            InitializeComponent();

        }

        private void Window_Loaded(object sender, RoutedEventArgs e)

            string path = System.IO.Directory.GetCurrentDirectory();

            ListBugInfo&gt; bugInfoList = LoadBugInfo(path + @"\BugInfo.txt");

            DateTime[] dates = new DateTime[bugInfoList.Count];

            int[] numberOpen = new int[bugInfoList.Count];

            int[] numberClosed = new int[bugInfoList.Count];

            for (int i = 0; i bugInfoList.Count; ++i)

            {

                dates[i] = bugInfoList[i].date;

                numberOpen[i] = bugInfoList[i].numberOpen;

                numberClosed[i] = bugInfoList[i].numberClosed;

            }

            var datesDataSource = new EnumerableDataSourceDateTime&gt;(dates);

            datesDataSource.SetXMapping(x =&gt; dateAxis.ConvertToDouble(x));

            var numberOpenDataSource = new EnumerableDataSourceint&gt;(numberOpen);

            numberOpenDataSource.SetYMapping(y =&gt; y);

            var numberClosedDataSource = new EnumerableDataSourceint&gt;(numberClosed);

            numberClosedDataSource.SetYMapping(y =&gt; y);

            CompositeDataSource compositeDataSource1 = new

              CompositeDataSource(datesDataSource, numberOpenDataSource);

            CompositeDataSource compositeDataSource2 = new

              CompositeDataSource(datesDataSource, numberClosedDataSource);

            plotter.AddLineGraph(compositeDataSource1,

              new Pen(Brushes.Blue, 2),

              new CirclePointMarker { Size = 10.0, Fill = Brushes.Red },

              new PenDescription("Number bugs open"));

            plotter.AddLineGraph(compositeDataSource2,

              new Pen(Brushes.Green, 2),

              new TrianglePointMarker

              {

                  Size = 10.0,

                  Pen = new Pen(Brushes.Black, 2.0),

                  Fill = Brushes.GreenYellow

              },

              new PenDescription("Number bugs closed"));

            plotter.Viewport.FitToView();

        } // Window1_Loaded()

        private static ListBugInfo&gt; LoadBugInfo(string fileName)

            var result = new ListBugInfo&gt;();

            FileStream fs = new FileStream(fileName, FileMode.Open);

            StreamReader sr = new StreamReader(fs);

            string line = string.Empty;

            while ((line = sr.ReadLine()) != null &amp;&amp; !line.Equals(string.Empty))

                string[] pieces = line.Split(':');

                DateTime d = DateTime.Parse(pieces[0]);

                int numopen = int.Parse(pieces[1]);

                int numclosed = int.Parse(pieces[2]);

                BugInfo bi = new BugInfo(d, numopen, numclosed);

                result.Add(bi);

            sr.Close();

            fs.Close();

            return result;

    }

    public class BugInfo

        public DateTime date;

        public int numberOpen;

        public int numberClosed;

        public BugInfo(DateTime date, int numberOpen, int numberClosed)

            this.date = date;

            this.numberOpen = numberOpen;

            this.numberClosed = numberClosed;

}

我删除了 Visual Studio 模闆生成的不必要的 using 命名空間語句(如 System.Windows.Shapes)。然後向 DynamicDataDisplay 庫中的三個命名空間添加了 using 語句,進而不必完全限定其名稱。接下來,在 Window1 構造函數中為程式定義的主例程添加一個事件:

Loaded += new RoutedEventHandler(Window1_Loaded);

下面是該主例程的開頭部分:

private void Window_Loaded(object sender, RoutedEventArgs e)

我聲明了一個泛型清單對象 bugInfoList,并使用一個程式定義的幫助器方法(名為 LoadBugInfo)将檔案 BugInfo.txt 中的虛拟資料填充到該清單中。為了組織我的錯誤資訊,我聲明了一個小幫助器類 BugInfo,如圖 5 所示。

圖 5 幫助器類 BugInfo

public class BugInfo

為簡單起見,我将三個資料字段聲明為公共類型,而不是聲明為與 get 和 set 屬性相結合的私有類型。因為 BugInfo 隻是資料,是以我可以使用 C# 結構而不使用類。LoadBugInfo 方法打開 BugInfo.txt 檔案并周遊該檔案,分析每個字段,然後執行個體化 BugInfo 對象,并将每個 BugInfo 對象存儲到結果清單中,如圖 6 所示。

圖 6 LoadBugInfo 方法

private static ListBugInfo&gt; LoadBugInfo(string fileName)

  var result = new ListBugInfo&gt;();

  FileStream fs = new FileStream(fileName, FileMode.Open);

  StreamReader sr = new StreamReader(fs);

  string line = "";

  while ((line = sr.ReadLine()) != null)

  {

    string[] pieces = line.Split(':');

    DateTime d = DateTime.Parse(pieces[0]);

    int numopen = int.Parse(pieces[1]);

    int numclosed = int.Parse(pieces[2]);

    BugInfo bi = new BugInfo(d, numopen, numclosed);

    result.Add(bi);

  }

  sr.Close();

  fs.Close();

  return result;

我可以使用 File.ReadAllLines 方法将資料檔案中的所有行讀入一個字元串數組,而不是讀取并處理該檔案中的每一行。請注意,為了使代碼短小、清晰,我省略了正常的錯誤檢查步驟,但您在生産環境中應執行該檢查。

接下來,我對三個數組進行聲明并指派,如圖 7 所示。

圖 7 建構數組

DateTime[] dates = new DateTime[bugInfoList.Count];

  int[] numberOpen = new int[bugInfoList.Count];

  int[] numberClosed = new int[bugInfoList.Count];

  for (int i = 0; i bugInfoList.Count; ++i)

    dates[i] = bugInfoList[i].date;

    numberOpen[i] = bugInfoList[i].numberOpen;

    numberClosed[i] = bugInfoList[i].numberClosed;

  ...

使用 DynamicDataDisplay 庫時,将顯示資料組織為一維數組集通常很友善。作為我的程式設計(即将資料讀入一個清單對象,然後将清單資料傳輸到數組)的替代方法,我可以将資料直接讀入數組。

接下來,我将資料數組轉換為特殊的 EnumerableDataSource 類型:

            var datesDataSource = new EnumerableDataSourceDateTime&gt;(dates);

對于 DynamicDataDisplay 庫,要繪制的所有資料都必須為統一格式。我隻是将三個資料數組傳遞給泛型 EnumerableDataSource 構造函數。此外,必須告知該庫與每個資料源關聯的軸(x 軸或 y 軸)。SetXMapping 和 SetYMapping 方法接受将方法委托作為參數。我使用了 lambda 表達式來建立匿名方法,而不是定義顯式委托。DynamicDataDisplay 庫的基本軸資料類型是 double。SetXMapping 和 SetYMapping 方法将我的特殊資料類型映射到 double 類型。

在 x 軸上,我使用 ConvertToDouble 方法将 DateTime 資料顯式轉換為 double 類型。在 y 軸上,我隻是編寫 y =&gt; y(讀作“y 轉為 y”),将輸入 int y 隐式轉換為輸出 double y。我也可以通過編寫 SetYMapping(y =&gt; Convert.ToDouble(y) 來顯式進行類型映射。我可以任意選擇 x 和 y 作為 lambda 表達式的參數,即,我可以使用任意參數名稱。

下一步是組合 x 軸和 y 軸資料源:

CompositeDataSource compositeDataSource1 = new

圖 1 中的螢幕截圖顯示了在同一個圖形中繪制的兩個資料系列,即打開的錯誤數和已關閉的錯誤數。每個複合資料源定義一個資料系列,是以,我在此處需要兩個單獨的資料源:一個用于打開的錯誤數,一個用于已關閉的錯誤數。當資料全都準備好時,實際上隻需一條語句便可繪制資料點:

plotter.AddLineGraph(compositeDataSource1,

AddLineGraph 方法接受 CompositeDataSource,後者定義要繪制的錯誤以及有關确切的繪制方式的資訊。此處,我訓示名為 plotter 的繪圖器對象(在 Window1.xaml 檔案中定義)執行以下操作:使用粗細為 2 的藍色線條繪制一個圖形,放置具有紅色邊框和紅色填充且大小為 10 的圓圈标記,并添加系列标題 Number bugs open。太巧妙了!作為許多備選方法中的一種,我可以使用

plotter.AddLineGraph(compositeDataSource1, Colors.Red, 1, "Number Open")

來繪制不帶标記的細紅色線條。或者,我也可以建立虛線而不是實線:

Pen dashedPen = new Pen(Brushes.Magenta, 3);

dashedPen.DashStyle = DashStyles.DashDot;

plotter.AddLineGraph(compositeDataSource1, dashedPen,

  new PenDescription("Open bugs"));

我的程式最後會繪制第二個資料系列:

...

    plotter.AddLineGraph(compositeDataSource2,

    new Pen(Brushes.Green, 2),

    new TrianglePointMarker { Size = 10.0,

      Pen = new Pen(Brushes.Black, 2.0),

      Fill = Brushes.GreenYellow },

    new PenDescription("Number bugs closed"));

  plotter.Viewport.FitToView();

此處,我訓示繪圖器使用帶有三角形标記的綠色線條,這些三角形标記具有黑色邊框和黃綠色填充。FitToView 方法将圖形縮放為 WPF 視窗的大小。

訓示 Visual Studio 生成 BugGraph 項目後,我獲得 BugGraph.exe 可執行檔案,可以随時以手動方式或程式設計方式啟動該檔案。我隻需編輯 BugInfo.txt 檔案就可更新基礎資料。因為整個系統基于 .NET Framework 代碼,是以我可将繪圖功能輕松地內建到任何 WPF 項目中,而不必處理跨技術問題。DynamicDataDisplay 庫還有一個 Silverlight 版本,是以我也可以向 Web 應用程式中添加程式設計繪圖功能。

前一節中展示的技術可以應用于所有類型的資料,而不僅是與測試相關的資料。我們來簡單了解一下另一個簡單但令人印象相當深刻的示例。圖 8 中的螢幕截圖顯示了 13,509 個美國城市。

使用 WPF 以程式設計方式生成圖形 使用 WPF 生成圖形

圖 8 散點圖示例

第一個字段是從 1 開始的索引 ID。第二個和第三個字段表示從具有 500 或更多人口的美國城市的緯度和經度派生而來的坐标。我按照前一節中所述建立了一個新 WPF 應用程式,向項目中添加了一個文本檔案項,并将城市資料複制到該檔案中。我在資料檔案的标頭行前面添加了雙斜杠 (//) 字元,進而注釋掉這些行。

若要建立圖 8 中所示的散點圖,我隻需對前一節中展示的示例稍加更改即可。我修改了 MapInfo 類成員,如下所示:

public int id;

  public double lat;

  public double lon;

圖 9 顯示了修改後的 LoadMapInfo 方法中的關鍵處理循環。

圖 9 散點圖的循環

while ((line = sr.ReadLine()) != null)

   if (line.StartsWith("//"))

     continue;

   else {

     string[] pieces = line.Split(' ');

     int id = int.Parse(pieces[0]);

     double lat = double.Parse(pieces[1]);

     double lon = -1.0 * double.Parse(pieces[2]);

     MapInfo mi = new MapInfo(id, lat, lon);

     result.Add(mi);

   }

我讓代碼檢查目前行是否以程式定義的注釋标記開頭,如果是,則跳過該行。請注意,我将經度派生的字段乘以 -1.0,因為經度在 x 軸方向上是從東向西(或從右向左)。如果不使用 -1.0 因子,則我的地圖将是正确方向的鏡像圖像。

我填充原始資料數組時,隻需確定将緯度和經度分别與 y 軸和 x 軸關聯即可:  

for (int i = 0; i mapInfoList.Count; ++i)

   ids[i] = mapInfoList[i].id;

   xs[i] = mapInfoList[i].lon;

   ys[i] = mapInfoList[i].lat;

 }

如果我颠倒關聯順序,則産生的地圖會沿其邊緣傾斜。當我繪制資料時,隻需要稍微調整一下便可建立散點圖而不是折線圖:

plotter.AddLineGraph(compositeDataSource,

   new Pen(Brushes.White, 0),

   new CirclePointMarker { Size = 2.0, Fill = Brushes.Red },

   new PenDescription("U.S. cities"));

通過向 Pen 構造函數傳遞 0 值,我指定了一根寬度為 0 的線條,這可有效地删除該線條,進而建立散點圖而不是折線圖。産生的圖形效果很棒,而且隻需要幾分鐘就可編寫出生成該圖形的程式。相信我,我嘗試過其他很多種方法來繪制地理資料,将 WPF 和 DynamicDataDisplay 庫結合使用是我找到的最好的解決方案之一。

我在此處展示的技術可用于以程式設計方式生成圖形。該技術的關鍵是 Microsoft 研究院提供的 DynamicDataDisplay 庫。如果在軟體生産環境中用作獨立技術來生成圖形,則該方法在基礎資料頻繁更改時最為有用。如果在應用程式中用作內建技術來生成圖形,則該方法對于 WPF 或 Silverlight 應用程式最為有用。随着這兩種技術的演變,我确信将會看到更多基于這兩種技術的優秀視覺顯示庫。