zoukankan      html  css  js  c++  java
  • 使用 WPF 生成图形

     

    下载代码示例

    基于一组与测试有关的数据来生成图形是一项常见的软件开发任务。根据我的经验,最常用的方法是将数据导入 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 与他联系。

  • 相关阅读:
    LeetCode 230. Kth Smallest Element in a BST
    LeetCode 114. Flatten Binary Tree to Linked List
    LeetCode 222. Count Complete Tree Nodes
    LeetCode 129. Sum Root to Leaf Numbers
    LeetCode 113. Path Sum II
    LeetCode 257. Binary Tree Paths
    Java Convert String & Int
    Java Annotations
    LeetCode 236. Lowest Common Ancestor of a Binary Tree
    LeetCode 235. Lowest Common Ancestor of a Binary Search Tree
  • 原文地址:https://www.cnblogs.com/lvdongjie/p/5527964.html
Copyright © 2011-2022 走看看