下载代码示例

基于一组与测试有关的数据来生成图形是一项常见的软件开发任务。根据我的经验,最常用的方法是将数据导入 Excel 电子表格,然后使用 Excel 内置的绘图功能手动生成图形。这种做法适用于大多数情况,但是如果基础数据频繁更改,则手动创建图形可能很快就变得枯燥乏味。在本月的专栏中,我将向您演示如何使用 Windows Presentation Foundation (WPF) 技术自动执行该过程。若要了解我所阐述的观点,请看图 1。该图按日期显示打开和已关闭的错误的计数,是使用从简单文本文件读取数据的一个短小 WPF 程序动态生成的。


图 1 以编程方式生成的错误计数图

打开的错误(用蓝色线条上的红圈表示)在开发工作开始后不久迅速增多,然后随时间推移逐渐减少(这是在估计零错误反弹日期时可能十分有用的信息)。已关闭的错误(绿色线条上的三角形标记)则稳步增多。

虽然这些信息可能十分有用,但在生产环境中,开发资源通常是有限的,因此手动生成这类图形可能不太值得。但是使用我将说明的技术,可快速而轻松地创建这类图形。

在下面几节中,我将详细展示和说明用于生成图 1 中图形的 C# 代码。本专栏假设您已具备 C# 编码方面的中级知识,并对 WPF 有最基本的了解。不过,即使您从前没有接触过这两个领域,我认为您也能够理解我所讨论的内容。我相信您会发现这项技术对于您的综合技能是个有趣且有用的补充。

建立项目

我首先启动 Visual Studio 2008,并使用 WPF 应用程序模板新建一个 C# 项目。从“新建项目”对话框右上方区域的下拉控件中选择 .NET Framework 3.5 库。将项目命名为 BugGraph。虽然您可以使用 WPF 基元以编程方式生成图形,但我使用了方便的 DynamicDataDisplay 库(由 Microsoft 研究院实验室开发)。

您可以从位于 codeplex.com/dynamicdatadisplay 的 CodePlex 开源托管站点下载该库。我将副本保存在 BugGraph 项目的根目录中,然后右键单击项目名称,选择“添加引用”选项并指向根目录中的 DLL 文件,从而在项目中添加对 DLL 的引用。

接下来创建源数据。在生产环境中,您的数据可以位于 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.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

我删除了 Visual Studio 模板生成的不必要的 using 命名空间语句(如 System.Windows.Shapes)。然后向 DynamicDataDisplay 库中的三个命名空间添加了 using 语句,从而不必完全限定其名称。接下来,在 Window1 构造函数中为程序定义的主例程添加一个事件:

 
复制
Loaded += new RoutedEventHandler(Window1_Loaded);

下面是该主例程的开头部分:

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

我声明了一个泛型列表对象 bugInfoList,并使用一个程序定义的帮助器方法(名为 LoadBugInfo)将文件 BugInfo.txt 中的虚拟数据填充到该列表中。为了组织我的错误信息,我声明了一个小帮助器类 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;
}
}

为简单起见,我将三个数据字段声明为公共类型,而不是声明为与 get 和 set 属性相结合的私有类型。因为 BugInfo 只是数据,所以我可以使用 C# 结构而不使用类。LoadBugInfo 方法打开 BugInfo.txt 文件并遍历该文件,分析每个字段,然后实例化 BugInfo 对象,并将每个 BugInfo 对象存储到结果列表中,如图 6 所示。

图 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 库,要绘制的所有数据都必须为统一格式。我只是将三个数据数组传递给泛型 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) 来显式进行类型映射。我可以任意选择 xy 作为 lambda 表达式的参数,即,我可以使用任意参数名称。

下一步是组合 x 轴和 y 轴数据源:

 
复制
CompositeDataSource compositeDataSource1 = new
CompositeDataSource(datesDataSource, numberOpenDataSource); CompositeDataSource compositeDataSource2 = new
CompositeDataSource(datesDataSource, numberClosedDataSource); ...

图 1 中的屏幕截图显示了在同一个图形中绘制的两个数据系列,即打开的错误数和已关闭的错误数。每个复合数据源定义一个数据系列,因此,我在此处需要两个单独的数据源:一个用于打开的错误数,一个用于已关闭的错误数。当数据全都准备好时,实际上只需一条语句便可绘制数据点:

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

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(); } // Window1_Loaded()

此处,我指示绘图器使用带有三角形标记的绿色线条,这些三角形标记具有黑色边框和黄绿色填充。FitToView 方法将图形缩放为 WPF 窗口的大小。

指示 Visual Studio 生成 BugGraph 项目后,我获得 BugGraph.exe 可执行文件,可以随时以手动方式或编程方式启动该文件。我只需编辑 BugInfo.txt 文件就可更新基础数据。因为整个系统基于 .NET Framework 代码,所以我可将绘图功能轻松地集成到任何 WPF 项目中,而不必处理跨技术问题。DynamicDataDisplay 库还有一个 Silverlight 版本,因此我也可以向 Web 应用程序中添加编程绘图功能。

散点图

前一节中展示的技术可以应用于所有类型的数据,而不仅是与测试相关的数据。我们来简单了解一下另一个简单但令人印象相当深刻的示例。图 8 中的屏幕截图显示了 13,509 个美国城市。


图 8 散点图示例

您可能可以识别出福罗里达州、德克萨斯州、南加利福尼亚州以及五大湖的位置。我从一个库获得了该散点图的数据,该库中的数据旨在用于旅行商问题 (www.iwr.uni-heidelberg.de/groups/comopt/software/TSPLIB95),这在计算机科学领域是一个最有名且广为研究的主题之一。我使用的文件 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 开始的索引 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 应用程序最为有用。随着这两种技术的演变,我确信将会看到更多基于这两种技术的优秀视觉显示库。

 

James McCaffrey 博士供职于 Volt Information Sciences, Inc.,在该公司他负责管理对华盛顿州雷蒙德市沃什湾 Microsoft 总部园区的软件工程师进行的技术培训。他曾参与过多项 Microsoft 产品的研发工作,其中包括 Internet Explorer 和 MSN Search。McCaffrey 是《.NET Test Automation Recipes:A Problem-Solution Approach》(Apress,2006 年)一书的作者。可通过 jammc@microsoft.com 与他联系。

使用 WPF 生成图形的更多相关文章

  1. WPF三维图形

    原文:WPF三维图形 wpf 三维图形基础生成三维图形的基本思想是能得到一个物体的三维立体模型(model).由于我们的屏幕只有二维,因而我们定义了一个用于给物体拍照的照相机(Camera).拍到的照 ...

  2. eclipse绘制activiti无法生成图形

    今天使用eclipse绘制acitiviti,发现无法绘制图形,设置里面勾选也勾选了,同时activiti插件也是最新的,最后发现同时打开了两个eclipse窗口就不支持生成图形,看来只能开一个窗口了 ...

  3. C#生成图形验证码

    先看效果: 再上代码 public class CaptchaHelper { private static Random rand = new Random(); private static in ...

  4. PHP5 GD库生成图形验证码(汉字)

    PHP5 GD库生成图形验证码且带有汉字的实例分享. 1,利用GD库函数生成图片,并在图片上写指定字符imagecreatetruecolor 新建一个真彩色图像imagecolorallocate ...

  5. PHP5生成图形验证码(有汉字)

    利用PHP5中GD库生成图形验证码 类似于下面这样 1.利用GD库函数生成图片,并在图片上写指定字符 imagecreatetruecolor   新建一个真彩色图像      imagecolora ...

  6. 给WPF示例图形加上方便查看大小的格子之完善版本

    原文:给WPF示例图形加上方便查看大小的格子之完善版本 去年10月份, 我曾写过一篇"给WPF示例图形加上方便查看大小的格子"的BLOG(http://blog.csdn.net/ ...

  7. 给WPF示例图形加上方便查看大小的格子

    原文:给WPF示例图形加上方便查看大小的格子 有时,我们为了方便查看WPF图形的样式及比例等,需要一些辅助性的格线,置于图形.图像的背景中. 比如下图,就是为了更清晰地查看折线的图形,我们画了用于标示 ...

  8. java生成图形验证码

    效果图 import java.awt.Color; import java.awt.Font; import java.awt.Graphics; import java.awt.image.Buf ...

  9. JavaScript图形实例:再谈IFS生成图形

    在“JavaScript图形实例:迭代函数系统生成图形”一文中,我们介绍了采用迭代函数系统(Iterated Function System,IFS)创建分形图案的一些实例.在该文中,仿射变换函数W的 ...

随机推荐

  1. Python全栈开发:socket代码实例

    客户端与服务端交互的基本流程 服务端server #!/usr/bin/env python # -*- coding;utf-8 -*- import socket sk = socket.sock ...

  2. Python全栈开发:冒泡排序

    #!/usr/bin/env python # -*- coding;utf-8 -*- """ 第一次对比:找到最大值,放到最后 对比是两两对比,对比的两个数组合共有l ...

  3. loj2494 [hnoi2018]寻宝游戏

    题意:给你n个元素的数组a.你可以在每个元素之前添加and和or的符号.每次询问最后变成r有多少种添号情况. n<=1000,m<=5000,q<=1000. 标程: #includ ...

  4. [复习]平衡树splay

    明天要考试了…… 出来写一个splay的复习总结. 怕忘…… ^废话^ 以下内容学习自yyb大神的博客, 由于yyb大神内容不全, 部分是博主本人自行脑补... 这个模板还是比较全的^-^ ^又是一堆 ...

  5. slam课程

    美国宾夕法尼亚大学最近有录制一套 无人机视觉定位导航相关的视频课程,2019年3月份在YouTube上更新完毕,质量非常高,名字叫Robotics,视频课程列表:https://www.youtube ...

  6. springboot下slf4j配置

    我们在引用的时候直接写 private static final Logger logger = LoggerFactory.getLogger(XXXServiceImpl.class); log. ...

  7. windows IIS 部署dotnetcore

    1.安装windows IIS 程序 ,详细过程略. 2.下载dotnet core SDK runtime 链接地址:https://dotnet.microsoft.com/download/th ...

  8. vuex基础知识总结

    项目中要求添加vuex,根据学习我这个小白总结了一点自己的心得,供大家参考 在学习之前,要知道两件事 为什么要用vuex?vuex要什么场景下应用? 简单点解释一下 1.项目中应用了vue脚手架之后, ...

  9. NAT后的FTP服务器部署笔记

    (2019年2月19日注:这篇文章原先发在自己github那边的博客,时间是2017年2月5日) 寒假开始以后,过年之前有一个任务,为实验室的人搭建一个FTP,用之前部署好的物理服务器.这本就是网管干 ...

  10. 《DSP using MATLAB》Problem 8.22

    时光飞逝,亲朋会一个一个离我们远去,孤独漂泊一阵子后,我们自己也要离开, 代码: %% -------------------------------------------------------- ...