Este artigo foi traduzido por máquina.

Execução de teste

Gerando gráficos com o WPF

James McCaffrey

Baixe o código de exemplo

James McCaffreyA geração de um gráfico de um conjunto de dados de teste é uma tarefa comum de desenvolvimento de software. Na minha experiência, a abordagem mais comum é importar dados para uma planilha do Excel, em seguida, produzir os recursos de gráfico manualmente, usando os gráficos internos do Excel. Isso funciona bem na maioria das situações, mas se os dados subjacentes forem alterados com freqüência, a criação de gráficos de mão pode rapidamente se tornar entediante. Na coluna deste mês, mostrarei como automatizar o processo usando a tecnologia do Windows Presentation Foundation (WPF). Para ver onde eu sou do título, examine o do Figura 1. O gráfico mostra uma contagem de abrir em vez de bugs fechados por data, e foi gerada dinamicamente usando um programa pequeno do WPF que lê dados de um arquivo de texto simples.

Figure 1 Programmatically Generated Bug-Count Graph
Figura 1 do gráfico de contagem de erro gerado por meio de programação

Os bugs abertos, representados por círculos vermelhos em linha azul, aumentar rapidamente o próximo ao início do esforço de desenvolvimento, em seguida, trilha ao longo do tempo — informações que podem ser úteis quando estimar a data de repercussão do bug do zero. Os bugs fechados (Marcadores triangulares na linha verde) aumentam constantemente.

Mas Embora as informações podem ser úteis, em produção ambientes de desenvolvimento de recursos freqüentemente são limitados e gerar manualmente como um gráfico pode não ser que vale a pena o esforço. Mas usando a técnica que vou explicar, a criação de gráficos, como este é rápido e fácil.

Nas seções a seguir, irá apresentar e descrever detalhadamente o código em c# que gerou o gráfico no do Figura 1. Esta coluna pressupõe que você tenha uma familiaridade básica com o WPF e o conhecimento de nível intermediário de c# codificação. Mas mesmo se você é iniciante em ambos, acho que você será capaz de acompanhar a discussão sem muita difficultly. Tenho certeza que você encontrará a técnica uma adição útil e interessante para seu conjunto de habilidades.

Como configurar o Project

Comecei a iniciar o Visual Studio 2008 e criando um novo projeto c# usando o modelo de aplicativo do WPF. Eu selecionei a biblioteca do .NET Framework 3. 5 do controle de lista suspensa na área superior direita da caixa de diálogo New Project. Chamei o meu projeto BugGraph. Embora programaticamente você pode gerar gráficos usando primitivos do WPF, usei a biblioteca de DynamicDataDisplay conveniente desenvolvida um laboratório de pesquisa da Microsoft.

Você pode fazer o download da biblioteca gratuita da fonte aberta do CodePlex que hospeda o site em codeplex.com/dynamicdatadisplay de . Eu salvo minha cópia no diretório raiz do meu projeto BugGraph, em seguida, adicionou uma referência para a DLL no meu projeto clicando com o botão direito no nome do projeto, selecionando a opção Add Reference e apontando para o arquivo DLL no diretório raiz do meu.

Em seguida, criei meus dados de origem. Em um ambiente de produção, foi possível localizar os dados em uma planilha do Excel, um banco de dados SQL ou um arquivo XML. Para simplificar, usei um arquivo de texto simples. Na janela Solution Explorer do Visual Studio, eu right-clicked em meu nome de projeto e selecionei Add | New Item no menu de contexto. Em seguida, optei por item, o arquivo de texto renomeado o arquivo para BugInfo.txt e clicado no botão Adicionar. Eis os dados fictícios:

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

O primeiro campo delimitado por dois-pontos em cada linha contém uma data, o segundo contém o número de bugs abertos na data de associado e o terceiro campo mostra o número de bugs fechados. Como você verá em breve, a biblioteca DynamicDataDisplay pode lidar com a maioria dos tipos de dados.

Depois dois cliques no arquivo Window1.xaml para carregar as definições de interface do usuário para o projeto. Eu adicionou uma referência para a DLL da biblioteca de gráfico e modificou ligeiramente o padrão de altura, largura e atributos de plano de fundo do WPF exiba a área, da seguinte maneira:

xmlns:d3="https://research.microsoft.com/DynamicDataDisplay/1.0" 
Title="Window1" WindowState="Normal" Height="500" Width="800" Background="Wheat">

Depois disso, adicionei o objeto de plotagem chave, mostrado em do Figura 2.

De Adicionar a chave do objeto de plotagem, a Figura 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>

O elemento ChartPlotter é o objeto de vídeo principal. A definição para ela, adicionei declarações de um eixo horizontal de data e um eixo vertical inteiro. O tipo de eixo padrão para a biblioteca DynamicDataDisplay é um número com um número decimal, que é do tipo double em termos de Linguagem c#; nenhuma declaração de eixo explícita é necessária para esse tipo. Também adicionei uma declaração de título do cabeçalho e as declarações de título do eixo. A Figura 3 mostra o meu design até o momento.

Figure 3 BugGraph Program Design
Do BugGraph Program Design, a Figura 3

Vai para o código-fonte

Depois que eu tinha configurado os aspectos estáticos do meu projeto, eu estava pronto para adicionar o código que poderia ler os dados de origem e gerar programaticamente o meu gráfico. Eu clicou duas vezes em Window1.xaml.cs na janela Solution Explorer para carregar o arquivo c# no editor de código. Figura 4 lista o código-fonte inteiro para o programa que gerou o gráfico no do Figura 1.

Figura 4 do código-fonte para o Project 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

Eu excluí o desnecessários usando declarações de namespace (como, por exemplo, System.Windows.Shapes), que foram geradas com o modelo do Visual Studio. Em seguida, adicionei instruções using três espaços para nome da biblioteca DynamicDataDisplay para que eu não teria que qualificar totalmente seus nomes. Em seguida, no construtor Window1, adicionei um evento para a rotina principal definido pelo programa:

Loaded += new RoutedEventHandler(Window1_Loaded);

Aqui está como comecei a rotina principal:

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

Eu declarado um objeto de lista genérica, bugInfoList e preencher a lista com os dados fictícios no arquivo BugInfo.txt usando um método definido pelo programa auxiliar denominado LoadBugInfo. Para Organizar Minhas informações de erro, declarei uma classe auxiliar pequenos — BugInfo — como a Figura 5 de mostra.

A Figura 5 da BugInfo de classe do auxiliar

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

Declarei que os dados de três campos como o tipo de público para manter a simplicidade, em vez de tipo particular combinados com obtém e definir propriedades. Como BugInfo é apenas os dados, poderia usei um struct C# em vez de uma classe. O método LoadBugInfo abre o arquivo BugInfo.txt e itera através dele, em seguida, analisando cada campo instancia um objeto BugInfo e armazena cada objeto BugInfo em um lista de resultados, conforme mostrado no do Figura 6.

Figura 6 do Método 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;
}

Em vez de ler e processar cada linha do arquivo de dados, eu poderia ter lido todas as linhas em uma matriz de cadeia de caracteres usando o método File. ReadAllLines. Observe que, a fim de manter o tamanho do meu código pequeno e por motivos de clareza, for omitido o normal-verificação de erro você deve executar em um ambiente de produção.

Em seguida, eu declarado e atribuídos valores a três matrizes, como você pode ver no do Figura 7.

A Figura 7 Criando matrizes

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

Ao trabalhar com a biblioteca DynamicDataDisplay, organizando os dados de exibição em um conjunto de matrizes unidimensionais geral, é conveniente. Como uma alternativa para o meu design de programa que leia dados em um objeto de lista e transferidos, em seguida, os dados da lista em matrizes, pode ter ler dados diretamente nas matrizes.

Em seguida converti Minhas conjuntos de dados em tipos especiais de 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);
...

Para a biblioteca DynamicDataDisplay, todos os dados a ser graficamente devem ser em um formato uniforme. Eu simplesmente passado três conjuntos de dados para o construtor de EnumerableDataSource genérico. Além disso, a biblioteca deve ser informada o eixo, de x ou de y , está associado a cada fonte de dados. Os métodos SetXMapping e SetYMapping aceitam método delegados como argumentos. Em vez de definir representantes explícitos, usei as expressões lambda para criar métodos anônimos. Tipo de dados fundamentais eixo da biblioteca DynamicDataDisplay é duplo. Os métodos SetXMapping e SetYMapping mapeiam meus dados de determinado tipo de tipo duplos.

Nos x de -eixo, usei o método ConvertToDouble explicitamente converter dados de DateTime em tipo double. Sobre a de y -eixo, simplesmente escrevi y = > y (leitura como “ y vai para y ”) para se convertem implicitamente a entrada int y y dupla de saída. Eu poderia ter sido explícita com o meu mapeamento de tipo, escrevendo SetYMapping(y => Convert.ToDouble(y). Minhas opções de x de e y de para parâmetros as expressões lambda ’ foram arbitrárias, eu poderia ter usado os nomes de parâmetro.

A próxima etapa consistiu em combinar as fontes de dados x - eixo e y - eixo:

CompositeDataSource compositeDataSource1 = new
  CompositeDataSource(datesDataSource, numberOpenDataSource);

CompositeDataSource compositeDataSource2 = new
  CompositeDataSource(datesDataSource, numberClosedDataSource);

...

A captura de tela em do Figura 1 mostra duas séries de dados — o número de bugs abertos e o número de bugs de fechado – plotadas no mesmo gráfico. Cada fonte de dados compostos define uma série de dados, então, aqui eu precisava de duas origens de dados individuais — uma para o número de bugs abertos e outra para o número de bugs de fechado. Com os todos os dados preparados, uma única instrução realmente plotadas os pontos de dados:

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

...

O método AddLineGraph aceita um CompositeDataSource, que define os dados a serem plotados, junto com as informações sobre como plotá-lo. Aqui eu for instruído a plotadora objectnamed plotadora (defined in the Window1.xaml file) para fazer o seguinte: Desenhe um gráfico usando uma linha azul de espessura 2, o lugar circular marcadores de tamanho 10 que têm bordas vermelhas e o preenchimento de vermelho e adicionar série título de Abrir o número de bugs. Elegantes! Como uma das muitas alternativas, eu poderia ter usado

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

para desenhar uma linha vermelha fina com sem marcadores. Ou então, eu poderia ter criado uma linha tracejada em vez de uma linha sólida:

Pen dashedPen = new Pen(Brushes.Magenta, 3);
dashedPen.DashStyle = DashStyles.DashDot;
plotter.AddLineGraph(compositeDataSource1, dashedPen,
  new PenDescription("Open bugs"));

Meu programa concluído pela segunda seqüência de dados de plotagem:

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

Aqui eu instruído a plotadora para usar uma linha verde com Marcadores triangulares que têm uma borda preta e preenchimento verde-amarela. O método FitToView dimensiona o gráfico para o tamanho da janela do WPF.

Depois de instruir o Visual Studio para criar o projeto BugGraph, obtive um BugGraph.exe executável, que pode ser iniciado manualmente ou programaticamente a qualquer momento. Posso atualizar os dados subjacentes, simplesmente, editando o arquivo BugInfo.txt. Como todo o sistema se baseia no código do .NET Framework, pode integrar facilmente gráfico de recursos em qualquer projeto de WPF sem a necessidade de lidar com questões de tecnologia de cruz. E há uma versão do Silverlight da biblioteca DynamicDataDisplay para que eu possa adicionar por programação gráficos para aplicativos da Web, muito.

Plotar uma dispersão

A técnica que apresentei na seção anterior pode ser aplicada a qualquer tipo de dados, não apenas testes relacionados a dados. Let’s dê uma olhada rápida em outro exemplo simples, mas é bastante impressionante. A captura de tela em do Figura 8 mostra 13,509 norte-americano cidades.

Figure 8 Scatter Plot Example
De dispersão de exemplo de plotagem, a Figura 8

Provavelmente, você pode identificar onde estão os Florida, no Texas, Sul da Califórnia e os Lakes Great. Obtido a dados para o gráfico de dispersão de uma biblioteca de dados deve ser usado com o problema da traveling vendedor (www.iwr.uni-heidelberg.de/groups/comopt/software/TSPLIB95 de), um dos tópicos mais famosos e amplamente studied em ciências da computação. O arquivo que eu utilizei, usa13509.tsp.gz, é semelhante a:

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

O primeiro campo é uma ID de índice baseado em 1. Os segundo e terceiro campos representam as coordenadas derivadas de latitude e longitude dos Estados Unidos cidades com população igual a 500 ou superior. Eu criou um novo aplicativo do WPF, conforme descrito na seção anterior, adicionado a um item do arquivo de texto para o projeto e copiou os dados de cidade para o arquivo. As linhas de cabeçalho do arquivo de dados que eu comentada por anexando ao início duplo-barra (//) caracteres para essas linhas.

Para criar o gráfico de dispersão mostrado do Figura 8, somente eu precisava fazer pequenas alterações para o exemplo apresentado na seção anterior. Modifiquei os membros da classe MapInfo da seguinte maneira:

public int id;
  public double lat;
  public double lon;

A Figura 9 mostra a chave de processamento de loop no método LoadMapInfo revisado.

A Figura 9 de loop para plotar XY

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

Tive o código, verifique se a linha atual começa com tokens Meus comentário definido pelo programa e, nesse caso, pule sobre ele. Observe que eu multiplicado no campo derivado de longitude, -1,0 porque longitudes ir do Leste para o oeste (ou da direita para esquerda) ao longo de x -eixo. Sem o -1,0 fator, meu mapa seria uma imagem espelhada de corrigir a orientação.

Quando eu preenchido Meus conjuntos de dados não processados, tudo o que precisei fazer foi garantir que eu corretamente associado latitude e longitude para o eixo de - eixo e de x - y, respectivamente:

for (int i = 0; i < mapInfoList.Count; ++i)
{
  ids[i] = mapInfoList[i].id;
  xs[i] = mapInfoList[i].lon;
  ys[i] = mapInfoList[i].lat;
}

Se eu tivesse reverter a ordem das associações, o mapa resultante poderia ter sido Inclinado em sua borda. Quando eu plotados meus dados, eu precisava de apenas um ajuste pequeno para tornar uma dispersão plotagem, em vez de um gráfico de linhas:

plotter.AddLineGraph(compositeDataSource,
  new Pen(Brushes.White, 0),
  new CirclePointMarker { Size = 2.0, Fill = Brushes.Red },
  new PenDescription("U.S. cities"));

Passando um valor 0 para o construtor de caneta, especifiquei uma linha de largura de 0, que efetivamente removido a linha e criou um gráfico de dispersão em vez de um gráfico de linhas. O gráfico resultante é muito bom e o programa que gerou o gráfico levou apenas alguns minutos para escrever. Acreditar que eu tentei várias outras abordagens para plotar dados geográficos e uso do WPF com a biblioteca DynamicDataDisplay está entre as melhores soluções que descobri.

Esse gráfico agora mais fácil

As técnicas que apresentei aqui podem ser usadas para gerar através de programação de gráficos. A chave para a técnica é a biblioteca DynamicDataDisplay de pesquisa da Microsoft. Quando usado como uma técnica autônomo para gerar gráficos em um ambiente de produção de software, a abordagem é mais útil se os dados subjacentes forem alterados com freqüência. Quando usado em um aplicativo como uma técnica integrada para gerar gráficos, a abordagem é mais útil com aplicativos do WPF ou do Silverlight. E como essas duas tecnologias evoluem, tenho certeza de que veremos mais bibliotecas de exibição visual de grandes com base neles.

Dr.James McCaffreytrabalha para a Volt Information Sciences Inc. que gerencia o treinamento técnico para engenheiros de software trabalham em Redmond da Microsoft, Wash., campus. Ele trabalhou em vários produtos da Microsoft, incluindo o Internet Explorer e o MSN Busca. McCaffrey é autor do livro “ .NET Test Automation Recipes: Uma abordagem de solução de problemas ” (Apress, 2006). Ele pode ser contatado em jammc@microsoft.com.