<a href="http://code.msdn.microsoft.com/mag201006TestRun">下載下傳代碼示例</a>
基于一組與測試有關的資料來生成圖形是一項常見的軟體開發任務。根據我的經驗,最常用的方法是将資料導入 Excel 電子表格,然後使用 Excel 内置的繪圖功能手動生成圖形。這種做法适用于大多數情況,但是如果基礎資料頻繁更改,則手動建立圖形可能很快就變得枯燥乏味。在本月的專欄中,我将向您示範如何使用 Windows Presentation Foundation (WPF) 技術自動執行該過程。若要了解我所闡述的觀點,請看圖 1。該圖按日期顯示打開和已關閉的錯誤的計數,是使用從簡單文本檔案讀取資料的一個短小 WPF 程式動态生成的。
![](https://img.laitimes.com/img/_0nNw4CM6IyYiwiM6ICdiwiIn5GcuAjY1ITO2UWZwADNhdDO1QzN4MWZ1ADN4gDNkJWZ5kDOfdWbp9CXt92Yu4GZjlGbh5SZslmZxl3Lc9CX6MHc0RHaiojIsJye.png)
圖 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">
然後,添加關鍵的繪圖對象,如圖 2 所示。
圖 2 添加關鍵的繪圖對象
d3:ChartPlotter Name="plotter" Margin="10,10,20,10">
d3:ChartPlotter.HorizontalAxis>
d3:HorizontalDateTimeAxis Name="dateAxis"/>
/d3:ChartPlotter.HorizontalAxis>
d3:ChartPlotter.VerticalAxis>
d3:VerticalIntegerAxis Name="countAxis"/>
/d3:ChartPlotter.VerticalAxis>
d3:Header FontFamily="Arial" Content="Bug Information"/>
d3:VerticalAxisTitle FontFamily="Arial" Content="Count"/>
d3:HorizontalAxisTitle FontFamily="Arial" Content="Date"/>
/d3:ChartPlotter>
ChartPlotter 元素是主要顯示對象。在該元素的定義中,我添加了水準日期軸和垂直整數軸的聲明。DynamicDataDisplay 庫的預設軸類型是具有小數部分的數字(在 C# 術語中稱為 double 類型);該類型無需顯式軸聲明。我還添加了一個标頭标題聲明和軸标題聲明。圖 3 顯示迄今為止的設計。
圖 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> 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>(dates);
datesDataSource.SetXMapping(x => dateAxis.ConvertToDouble(x));
var numberOpenDataSource = new EnumerableDataSourceint>(numberOpen);
numberOpenDataSource.SetYMapping(y => y);
var numberClosedDataSource = new EnumerableDataSourceint>(numberClosed);
numberClosedDataSource.SetYMapping(y => 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> LoadBugInfo(string fileName)
var result = new ListBugInfo>();
FileStream fs = new FileStream(fileName, FileMode.Open);
StreamReader sr = new StreamReader(fs);
string line = string.Empty;
while ((line = sr.ReadLine()) != null && !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> LoadBugInfo(string fileName)
var result = new ListBugInfo>();
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>(dates);
對于 DynamicDataDisplay 庫,要繪制的所有資料都必須為統一格式。我隻是将三個資料數組傳遞給泛型 EnumerableDataSource 構造函數。此外,必須告知該庫與每個資料源關聯的軸(x 軸或 y 軸)。SetXMapping 和 SetYMapping 方法接受将方法委托作為參數。我使用了 lambda 表達式來建立匿名方法,而不是定義顯式委托。DynamicDataDisplay 庫的基本軸資料類型是 double。SetXMapping 和 SetYMapping 方法将我的特殊資料類型映射到 double 類型。
在 x 軸上,我使用 ConvertToDouble 方法将 DateTime 資料顯式轉換為 double 類型。在 y 軸上,我隻是編寫 y => y(讀作“y 轉為 y”),将輸入 int y 隐式轉換為輸出 double y。我也可以通過編寫 SetYMapping(y => 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 個美國城市。
圖 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 應用程式最為有用。随着這兩種技術的演變,我确信将會看到更多基于這兩種技術的優秀視覺顯示庫。