本文章是由機器翻譯。

測試回合

使用 WPF 產生圖形

James McCaffrey

下載程式碼範例

從一組相關的測試資料產生圖形是常見的軟體開發工作。我的經驗最常見的方法是將資料匯入的 Excel 試算表,然後產生以手動方式使用 [在 Excel 內建圖形的圖表功能。這在大多數的情況下運作,但如果基礎資料經常變更,以手動方式建立圖形可以迅速成為繁瑣。本月 ’s] 欄中我告訴您如何使用 Windows Presentation Foundation (WPF) 技術的程序自動化。若要知道,我標題,查看 的 圖 1。依日期,開啟與關閉 Bug,並產生即時使用簡短的 WPF 程式,從簡單的文字檔讀取資料,則圖表會顯示的計數。


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

開啟 Bug 由紅色圓圈在藍色的行上迅速增加開發] 工作開端附近,然後軌跡經過一段時間 — 估計零錯誤彈回日期時可能有用的資訊。已關閉的 Bug (綠色線條上的三角形標記) 持續增加。

但是,雖然這些資訊可能會很有用,在實際執行環境開發資源通常是限制,並手動產生這類圖形可能不是值得努力。但使用 [我說明的技術建立像這樣的圖形是快速且容易。

下列的區段中,我呈現,並詳細說明 C# 碼產生 的 圖 1 中的圖形。此資料行假設您擁有的 C# 程式碼撰寫的中間層級知識和非常基本的熟悉 WPF。但是,即使您 ’re 兩者的新,我認為您能夠依照太多不討論困難。我 ’m 確信您尋找技術有趣且有用的加法技術集。

設定專案

我開始啟動 Visual Studio 2008,並建立新的 C# 專案使用 WPF 應用程式範本。我可以從下拉式控制項中的 [新增專案] 對話方塊上方的右手邊區域選取.NET Framework 3.5 程式庫。我名為 [我的專案 BugGraph。雖然以程式設計的方式,您可以產生使用 WPF 的基本圖表,我使用方便 DynamicDataDisplay 媒體櫃由 Microsoft 研究實驗室開發。

您可以從 CodePlex 開放原始碼裝載站台在 codeplex.com/dynamicdatadisplay 的免費下載文件庫。我在我 BugGraph] 專案的根目錄中儲存我的複本,然後 DLL 的參考加入專案中,藉由在專案名稱上按一下滑鼠右鍵、 選取 [加入參考] 選項,然後指向 [我的根目錄中的 DLL 檔案。

接下來,我建立我的來源資料。在實際執行的環境中可能位於您的資料在 Excel 工作表,SQL 資料庫或 XML 檔案中。為了簡單起見,我使用簡單的文字檔。我在 Visual Studio 方案總管] 視窗中用滑鼠右鍵按一下 [我的專案名稱,選取 [新增] | 新的項目從內容功能表。然後選取了文字檔案項目,BugInfo.txt 重新命名檔案,然後按一下 [新增] 按鈕。這裡 ’s 虛設資料:

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

在每一行中的,第一個冒號分隔欄位保留日期、 第二個相關聯的日期上包含開啟的 Bug 數和第三個欄位會顯示已關閉的 Bug 數。您很快就發現,DynamicDataDisplay 程式庫可以處理大部分的資料類型。

接下來,我按兩下檔案 Window1.xaml 載入專案的使用者介面定義上。我新增圖形的程式庫 DLL 的參考,然後稍微修改預設高度,寬度和背景屬性 WPF 的顯示區域,如下所示:

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# 詞彙中的雙精度浮點型別為小數的數字 ; 沒有明確的座標軸宣告為該型別所需。我也會加入標頭標題宣告及座標軸標題的宣告。圖 3 為止顯示我的設計。


圖 3 的 BugGraph 程式設計

移至來源

一旦我設定靜態的方面的專案,我已準備好要加入程式碼,會讀取來源資料,並以程式設計方式產生 [我的圖形。我在 [方案總管] 視窗中的 Window1.xaml.cs 載入程式碼編輯器中的 C# 檔案上連按兩下。圖 4 列出整個原始程式碼產生圖形 的 圖 1 中的程式。

圖 4 的 BugGraph 專案的原始程式碼

using System;
using System.Collections.Generic;
using System.Windows;
using System.Windows.Media; // Pen

using System.IO;
using Microsoft.Research.DynamicDataDisplay; // Core functionality
using Microsoft.Research.DynamicDataDisplay.DataSources; // EnumerableDataSource
using Microsoft.Research.DynamicDataDisplay.PointMarkers; // CirclePointMarker

namespace BugGraph
{
  public partial class Window1 : Window
  {
    public Window1()
    {
      InitializeComponent();
      Loaded += new RoutedEventHandler(Window1_Loaded);
    }

    private void Window1_Loaded(object sender, RoutedEventArgs e)
    {
      List<BugInfo> bugInfoList = LoadBugInfo("..\\..\\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 EnumerableDataSource<DateTime>(dates);
      datesDataSource.SetXMapping(x => dateAxis.ConvertToDouble(x));

      var numberOpenDataSource = new EnumerableDataSource<int>(numberOpen);
      numberOpenDataSource.SetYMapping(y => y);

      var numberClosedDataSource = new EnumerableDataSource<int>(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 List<BugInfo> LoadBugInfo(string fileName)
    {
      var result = new List<BugInfo>();
      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;
    }

  } // class Window1

  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;
  }

}} // ns

已刪除,不需要使用命名空間 (例如 System.Windows.Shapes) 之陳述式所產生的 Visual Studio 範本。 然後我會加入使用 SQL 陳述式至三個命名空間,從 DynamicDataDisplay 程式庫,wouldn’t 需要完整限定其名稱。 接下來,Window1 建構函式中新增主要的程式自訂常式的事件:

Loaded += new RoutedEventHandler(Window1_Loaded);

這裡 ’s 我開始主常式的方式:

private void Window1_Loaded(object sender, RoutedEventArgs e)
{
  List<BugInfo> bugInfoList = LoadBugInfo("..\\..\\BugInfo.txt");
  ...

我宣告泛型清單物件 bugInfoList,並使用名為 LoadBugInfo 程式定義的協助程式方法填入與檔案 BugInfo.txt 空的資料清單。 若要組織我的 Bug 資訊,我宣告很小的協助程式類別 — BugInfo — 為 圖 5 顯示。

圖 5 的協助程式類別 BugInfo

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;
  }
}

我宣告三個資料欄位為為了簡單起見,公用的型別,而不是私用的型別為加上取得和設定屬性。 因為 BugInfo 資料只是我可以而非類別使用 C# 結構。 LoadBugInfo 方法開啟 BugInfo.txt 檔案,並逐一它,剖析每個欄位中,然後 BugInfo 物件具現化和 的 圖 6 所示,儲存至結果清單,BugInfo 的每個物件。

圖 6 的 LoadBugInfo 方法

private static List<BugInfo> LoadBugInfo(string fileName)
{
  var result = new List<BugInfo>();
  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 EnumerableDataSource<DateTime>(dates);
datesDataSource.SetXMapping(x => dateAxis.ConvertToDouble(x));

var numberOpenDataSource = new EnumerableDataSource<int>(numberOpen);
numberOpenDataSource.SetYMapping(y => y);

var numberClosedDataSource = new EnumerableDataSource<int>(numberClosed);
numberClosedDataSource.SetYMapping(y => y);
...

DynamicDataDisplay 文件庫 graphed 的所有資料必須都是統一的格式。 我只會傳遞至泛型 EnumerableDataSource 建構函式的三個陣列資料。 此外,您必須哪一個座標軸、 xy ,是每個資料來源相關聯會告知文件庫。 SetXMapping 和 SetYMapping 方法接受做為引數的方法的委派。 而不是定義明確的委派,我會用 Lambda 運算式,來建立匿名方法。 雙 DynamicDataDisplay 程式庫 ’s fundamental 座標軸的資料型別。 SetXMapping 和 SetYMapping 方法對應我特別的資料型別,型別雙。

x 上-軸,我用 ConvertToDouble 方法來明確地將日期時間資料轉換雙精度浮點型別。 y 上-軸,我只需撰寫 y = > y (如 「 y 前往 y 」 讀取) 將隱含地轉換成輸出雙 y 的 [輸入的 int y。 我可能已經明確我的型別對應藉由撰寫 SetYMapping(y => Convert.ToDouble(y). 我的 Lambda 運算式 ’ 參數的 yx 的選擇是任意 — 我也可以使用任何的參數名稱。

下一個步驟,就是結合 x -軸和 y -軸的資料來源:

CompositeDataSource compositeDataSource1 = new
  CompositeDataSource(datesDataSource, numberOpenDataSource);

CompositeDataSource compositeDataSource2 = new
  CompositeDataSource(datesDataSource, numberClosedDataSource);

...

圖 1 中螢幕擷取畫面會顯示兩個資料數列 — 開啟的 Bug 數和數目關閉 Bug — 在同一圖形上繪製。每個複合資料來源定義一個資料] 數列因此這裡我需要兩個個別的資料來源 — 其中一個開啟的 Bug 數,一個用於的數目已關閉的 Bug。與資料所有準備,單一陳述式實際上會繪製資料點:

plotter.AddLineGraph(compositeDataSource1,
  new Pen(Brushes.Blue, 2),
  new CirclePointMarker { Size = 10.0, Fill = Brushes.Red },
  new PenDescription("Number bugs open"));

...

AddLineGraph 方法接受一項 CompositeDataSource 定義資料會繪製以及關於如何確切繪製其相關資訊。 我在這裡指示繪圖機 objectnamed 繪圖機 (defined in the Window1.xaml file) 來執行下列動作:繪製圖形,使用藍色線條的粗細 2,位置循環標記的大小 10 (有紅色框線和紅色的填滿,並加入 開啟數字錯誤 系列標題。 不錯! 其中許多替代方案我也可以使用

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();

} // Window1_Loaded()

此處我會指示要使用有黑色框線和填滿綠色黃色的三角形資料標記的綠線繪圖機。FitToView 方法按比例調整圖形的 WPF 視窗大小。

Visual Studio 建置 BugGraph 專案,讓他們之後, 得到一個 BugGraph.exe 可執行,這可啟動手動或以程式設計的方式在任何時間。我可以藉由直接編輯 BugInfo.txt 檔案更新基礎資料。因為整個系統以.NET Framework 程式碼為基礎的我可以輕易地而不必處理交叉技術問題將圖形的功能整合到任何 WPF 專案。和有 ’s Silverlight DynamicDataDisplay 程式庫版本,所以我可以加入程式設計圖形至 Web 的應用程式太。

散佈圖繪圖區

我提出的技術,在前一節中可以套用到任何種類的不只是測試相關資料的資料。let’s 採取簡短看另一個簡單但而是令人印象深刻的範例。的 圖 8 中螢幕擷取畫面顯示 13,509 美國城市。


圖 8 散佈繪圖範例

您可能可以識別 Florida、 德州、 南加利福尼亞和大湖的位置。我的散佈圖繪圖資料取自 traveling salesman 問題 ( www.iwr.uni-heidelberg.de/groups/comopt/software/TSPLIB95 ),搭配資料的文件庫在電腦科學領域中最著名的和廣泛 studied 主題之一。我所使用的檔案,usa13509.tsp.gz,看起來就像:

NAME : usa13509
(other header information)
1 245552.778 817827.778
2 247133.333 810905.556
3 247205.556 810188.889
...

13507 489663.889 972433.333
13508 489938.889 1227458.333
13509 490000.000 1222636.111

第一個欄位是以 1 起始的索引識別碼。第二個和第三個欄位代表衍生自美國經緯度座標大於或等於 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 因為 longitudes 從東向西 (或從右至左) 以及 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"));

藉由傳遞給畫筆的建構函式的值 0,我指定 0 寬度線條會有效地移除行並建立散佈圖繪圖,而不是一個折線圖。產生的圖表是相當酷炫和產生圖形的程式所花費的只有幾分鐘的時間來寫入。相信我,我試著許多其他方法來繪製地理的資料,並使用 WPF DynamicDataDisplay 程式庫是在我找到的最佳解決方案。

圖形進行簡易

我此處介紹的技巧可用來以程式設計方式產生圖形。此項技術的關鍵是 DynamicDataDisplay 程式庫,從 Microsoft 的參考資料。軟體的實際執行環境中產生圖形,作為獨立技術時方法是基礎資料變更頻繁的最有用的。時您可以使用整合的技術為應用程式中產生圖形,方法會是最有用的 WPF 或 Silverlight 應用程式。與這些這兩項技術發展過程,我 ’m 確定我們看到多個很棒的視覺顯示庫根據它們。

Dr。James McCaffrey *適用於 Volt 資訊科學 Inc.他負責管理技術的訓練的軟體工程師使用 Microsoft 台北市,Wash.,在 校園。他曾在數個的 Microsoft 產品包括 Internet Explorer] 和 [MSN 搜尋。McCaffrey 是作者的 「.NET 測試自動化食譜:問題方案方法中 」 (Apress 2006)。他可以達到在 jammc@microsoft.com.                     *