65.9K
CodeProject 正在变化。 阅读更多。
Home

简单的 WPF 条形图(水平和垂直)-第二部分

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.67/5 (21投票s)

2008年10月2日

CPOL

4分钟阅读

viewsIcon

93553

downloadIcon

2359

本文是对之前文章的扩展,演示了使用 WPF 创建水平和垂直条形图的简单步骤。

目录

“一图胜千言”。 因此,让我们先来看一些条形图控件在实际运行中的截图。

垂直条形图

Chart 1

水平条形图

Chart 1

引言

我将通过创建一个简单的图表库来学习 WPF。我从创建一个小型 WPF 条形图控件开始。您可以在 CodeProject 上查看我的第一个尝试,地址是:简单 WPF 条形图控件

这本身是一次很棒的学习经历。为了进一步加深对该主题的理解,我为这个图表库添加了更多功能,并决定与大家分享。

除了修复一些小错误之外,新添加的功能包括:

  • 修改了整个布局,以获得时尚的外观(尽管还不是非常时尚)。
  • 增加了水平条形图功能。
  • 增加了图表与宿主应用程序之间的事件通信。

当前代码仍然是为数据集编写的。您可以轻松地替换它以支持任何其他数据源。本文的目的是仅使用基本元素来让您熟悉 WPF 的图表功能,而无需涉及其他复杂性。

背景

本文假定您已具备 C# 和 WPF 的先验知识。如果您有兴趣学习 WPF,CodeProject 上有一些非常好的资源。以下是其中一些链接:

下图显示了用于表示条形图的骨架 UML 类图。与早期版本相比有一个小变化。现在,我们还有一个 BarEventArgs 对象,它在单击条形时传递给宿主。

imgUML

要运行附带的演示,请确保以下配置条目对您的环境有效:

<appSettings>
    <add key="SALES_XML" 
            value="C:\Rajesh\MyCodeProject\Lab\
            SimpleChart\SimpleChart\Data\SalesSummary.xml"/>
    <add key="SALES1_XML" 
            value="C:\Rajesh\MyCodeProject\Lab\
            SimpleChart\SimpleChart\Data\SalesSummary1.xml"/>
</appSettings>

Using the Code

让我们看看如何在您的应用程序中使用此条形图创建 HorizontalBar 图。有关详细属性,请参阅本系列的第一个部分。

chart1BarChart 的一个实例

// Set the ChartType to HorizontalBar
chart1.Type = BarChart.ChartType.HorizontalBar;

要创建 VerticalBar 图,请使用以下代码片段:

// Set the ChartType to Vertical Bar
chart2.Type = BarChart.ChartType.VerticalBar;

条形图剖析

BarChart.xaml 的 XAML 表示如下。这与该库的第一个版本不同。

<UserControl x:Class="SimpleChart.Charts.BarChart"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
     Height="640" Width="480" >

    <Grid x:Name="chartLayout">
        <Grid.RowDefinitions>
            <RowDefinition Height="10*"></RowDefinition>
            <RowDefinition Height="5*"></RowDefinition>
            <RowDefinition Height="80*"></RowDefinition>
        </Grid.RowDefinitions>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="80*"></ColumnDefinition>
            <ColumnDefinition Width="20*"></ColumnDefinition>
        </Grid.ColumnDefinitions>
            <Slider Name="zoomSlider" Grid.Row="0" 
               Grid.Column="0" Height="20" DockPanel.Dock="Top" 
               Minimum="1" Maximum="5" Value="1"/>
            <Canvas x:Name="chartTitle"  Grid.Row="1" Grid.Column="0">
                <TextBlock  x:Name="txtTopTitle" FontSize="12" Text="Title" 
                          Opacity="100" >
                    </TextBlock>
            </Canvas>
            <Canvas x:Name="chartLegendArea"  Grid.Row="2" Grid.Column="1"></Canvas>
            <ScrollViewer Grid.Row="2" Grid.Column="0" CanContentScroll="True" 
                          HorizontalScrollBarVisibility="Auto" 
                          VerticalScrollBarVisibility="Auto">
                <Canvas  x:Name="chartArea" ClipToBounds="False" Background="Black">
                    <Canvas.LayoutTransform>
                         <ScaleTransform 
                              ScaleX="{Binding ElementName=zoomSlider, Path=Value}"
                              ScaleY="{Binding ElementName=zoomSlider, Path=Value}"/>
                    </Canvas.LayoutTransform>
                    <TextBlock  x:Name="txtNoData" FontSize="12" 
                          Text="No data found." Visibility="Hidden"
                          Opacity="100" Foreground="Red">
                    </TextBlock>
                </Canvas>
        </ScrollViewer>
    </Grid>
</UserControl>

控件按以下层次结构组织:

XAML Structure

捕获条形图事件

我们通过添加 X 轴、顶部标题和气泡文本来设置基本图表布局。

// Handle events
chart1.BarClickHandler += new EventHandler<bareventargs>(BarClickHandler);

BarChart 事件处理程序如下所示。

/// <summary />
/// This method is executed when any bar is clicked on the char.
/// </summary />
/// <param name="sender" />The sender of this event.</param />
/// <param name="e" />The BarChart event argument. This contains all the
/// information about the clicked bar.</param />
void BarClickHandler(object sender, BarEventArgs e)
{
    MessageBox.Show(e.BarObject.ToString());
}

在上面的示例中,我们只是显示条形的信息。BarObjectToString() 方法返回一个格式良好的字符串,用于调试目的。它看起来如下面的图所示:

XAML Structure

绘制图表

主要的绘图活动发生在 Generate 方法中。

  • 添加图表控件以设置基本布局。
  • 设置图表区域。
  • 获取数据源。
  • 检查数据是否存在。如果不存在,则绘制空图表。
  • 计算 Y 轴的最大值。这对于正确的缩放是必需的。
  • 获取条形数量。
  • 确定图表是单值还是多值(系列)。
  • 根据图表类型绘制 X 轴。
  • 根据图表类型绘制 Y 轴。
  • 为系列中的每个值绘制条形。
  • 计算下一个条形的位置。
  • 绘制 X 轴。
  • 绘制 Y 轴。
  • 绘制图例。
/// <summary />
/// Creates the chart based on the datasource and the charttype.
/// </summary />
public void Generate()
{
    legends.Clear();
    chartArea.Children.Clear();
    
    // Setup chart elements.
    AddChartControlsToChart();

    // Setup chart area.
    SetUpChartArea();

    // Will be made more generic in the next versions.
    DataTable dt = (DataSource as DataSet).Tables[0];

    if (null != dt)
    {
        // if no data found draw empty chart.
        if (dt.Rows.Count == 0)
        {
            DrawEmptyChart();
            return;
        }
        
        // Hide the nodate found text.
        txtNoData.Visibility = Visibility.Hidden;

        // Get the max y-value. This is used
        // to calculate the scale and y-axis.
        maxData = GetMax(dt);

        // Get the total bar count.
        int barCount = dt.Rows.Count;
        
        // If more than 1 value field, then this is a group chart.
        bool isSeries = ValueField.Count > 1;           

        // no legends added yet.
        bool legendAdded = false;  // no legends yet added.

        // For each row in the datasource
        foreach (DataRow row in dt.Rows)
        {
            // Set the barwidth. This is required to adjust
            // the size based on available no. of bars.
            SetBarWidth(barCount);

            // Draw axis label based on charttype.
            if (Type == ChartType.VerticalBar)
                DrawXAxisLabel(row);
            else if (Type == ChartType.HorizontalBar)
                DrawYAxisLabel(row);

            
            // For each row the current series is initialized
            // to 0 to indicate start of series.
            int currentSeries = 0;  

            // For each value in the datarow, draw the bar.
            foreach (string valField in ValueField)
            {
                if (null == valField)
                    continue;

                if (!row.Table.Columns.Contains(valField))
                    continue;

                // Draw bar for each value.
                switch (Type)
                {
                    case ChartType.VerticalBar:
                        DrawVerticalBar(isSeries, legendAdded, 
                                        row, ref currentSeries, valField);
                        break;
                    case ChartType.HorizontalBar:
                        DrawHorizontalBar(isSeries, legendAdded, 
                                          row, ref currentSeries, valField);
                        break;
                    default:
                        DrawVerticalBar(isSeries, legendAdded, row, 
                                        ref currentSeries, valField);
                        break;
                }
            }
            legendAdded = true;

            // Set up location for next bar in series.
            
            if (Type == ChartType.VerticalBar)
                left = left + spaceBetweenBars;
            else if (Type == ChartType.HorizontalBar)
                top = top + spaceBetweenBars + BarWidth + 10;
        }

        if (Type == ChartType.VerticalBar)
        {
            if ((left + BarWidth) > chartArea.Width)
                chartArea.Width = left + BarWidth + 20;
        }
        else if (Type == ChartType.HorizontalBar)
        {
            if ((top + BarWidth) > chartArea.Height)
                chartArea.Height = top + BarWidth + 20;

        }

        // Adjust chart element location after final chart rendering.
        AdjustChartElements();

        DrawXAxis();  // Draw x-axis
        DrawYAxis();  // Draw y-axis
        DrawLegend(); // Draw the legends
    }
}

绘制条形

DrawBar() 函数负责渲染每个条形。在我们的例子中,条形由一个矩形对象表示。下图显示了绘制简单条形图所需完成的基本任务:

/// <summary />
/// Draws a bar
/// </summary />
/// <param name="isSeries" />Whether current bar is in a series or group.</param />
/// <param name="legendAdded" />Indicates whether to add legend.</param />
/// <param name="row" />The current bar row.</param />
/// <param name="currentSeries" />The current series.
//  Used to group series and color code bars.</param />
/// <param name="valField" />Value is fetched from
/// the datasource from this field.</param />
private void DrawBar(bool isSeries, bool legendAdded, DataRow row, 
                     ref int currentSeries, string valField)
{
    double val = 0.0;

    if (row[valField] == DBNull.Value)
        val = 0;
    else
        val = Convert.ToDouble(row[valField]);

    // Calculate bar value.
    double? calValue = (((float)val * 100 / maxData)) * 
            (chartArea.Height - bottomMargin - topMargin) / 100;

    Rectangle rect = new Rectangle();
    
    // Setup bar attributes.
    SetBarAttributes(calValue, rect);

    // Color the bar.
    Color stroke = Helper.GetDarkColorByIndex(currentSeries);
    rect.Fill = new SolidColorBrush(stroke);

    // Setup bar events.
    SetBarEvents(rect);

    // Add the legend if not added.
    if (isSeries && !legendAdded)
    {
        legends.Add(new Legend(stroke, ValueField[currentSeries]));
    }

    // Calculate bar top and left position.
    top = (chartArea.Height - bottomMargin) - rect.Height;
    Canvas.SetTop(rect, top);
    Canvas.SetLeft(rect, left + leftMargin);

    // Add bar to chart area.
    chartArea.Children.Add(rect);

    // Display value on bar if set to true.
    if (ShowValueOnBar)
    {
        DisplayYValueOnBar(val, rect);
    }

    // Create Bar object and assign to the rect.
    rect.Tag = new Bar(val, row, valField);

    // Calculate the new left postion for subsequent bars.
    if (isSeries)
        left = left + rect.Width;
    else
        left = left + BarWidth + spaceBetweenBars;

    // Increment the series
    currentSeries++;  
}

绘制 X 轴

下面的图显示了 C 轴的设置。

XAML Structure

/// <summary />
/// Draws xAxis. For vertical bar chart it's as simple as drawing a line
/// beginning from top to bottom. For horizontal bar some calculations are
/// involved as we need to position the marker correctly along with display
/// value.
/// </summary />
private void DrawXAxis()
{
    // Draw axis
    if (Type == ChartType.VerticalBar)
    {
        Line xaxis = new Line();
        xaxis.X1 = leftMargin;
        xaxis.Y1 = this.Height - bottomMargin;
        xaxis.X2 = this.chartArea.Width;
        xaxis.Y2 = this.Height - bottomMargin;

        xaxis.Stroke = new SolidColorBrush(Colors.Silver);
        chartArea.Children.Add(xaxis);
    }
    else if (Type == ChartType.HorizontalBar)
    {
        Line xaxis = new Line();
        xaxis.X1 = leftMargin;
        xaxis.Y1 = top;
        xaxis.X2 = this.chartArea.Width;
        xaxis.Y2 = top;

        xaxis.Stroke = new SolidColorBrush(Colors.Silver);
        chartArea.Children.Add(xaxis);


        // Set the scale factor for y-axis marker.
        double scaleFactor = 10;

        // this value is used to increment the x-axis marker value.
        double xMarkerValue = Math.Ceiling(maxData.Value / scaleFactor);

        // This value is used to increment the x-axis marker location.
        double scale = 5;  // default value 5.

        // get the scale based on the current max value
        // and other chart element area adjustments.
        scale = (((float)xMarkerValue * 100 / maxData.Value)) *
            (chartArea.Width - leftMargin - rightMargin) / 100;

        double x1 = chartArea.Width - rightMargin;

        double xAxisValue = 0;

        x1 = leftMargin;
        for (double i = 0; i <= scaleFactor; i++)
        {
            // Add x-axis marker line chart.
            Line marker = AddMarkerLineToChart(x1);

            // Add the y-marker to the chart.
            AddMarkerTextToChart(x1 - scale, xAxisValue);

            // Adjust the top location for next marker.
            x1 += scale;

            // Increment the y-marker value.
            xAxisValue += xMarkerValue;
        }
    }
}

绘制 Y 轴

下面的图显示了 Y 轴的设置。

XAML Structure

/// <summary />
/// Draws YAxis. For horizantal chart it's as simple as drawing a line
/// beginning from top to bottom. For vertical bar some calculations are
/// involved as we need to position the marker correctly along with display
/// value.
/// </summary />
private void DrawYAxis()
{
    if (Type == ChartType.VerticalBar)
    {
        Line yaxis = new Line();
        yaxis.X1 = leftMargin;
        yaxis.Y1 = 0;
        yaxis.X2 = leftMargin;
        yaxis.Y2 = this.Height - bottomMargin;
        yaxis.Stroke = new SolidColorBrush(Colors.DarkBlue);
        chartArea.Children.Add(yaxis);

        // Set the scale factor for y-axis marker.
        double scaleFactor = 10;

        // this value is used to increment the y-axis marker value.
        double yMarkerValue = Math.Ceiling(maxData.Value / scaleFactor);

        // This value is used to increment the y-axis marker location.
        double scale = 5;  // default value 5.

        // get the scale based on the current max y value
        // and other chart element area adjustments.
        scale = (((float)yMarkerValue * 100 / maxData.Value)) *
            (chartArea.Height - bottomMargin - topMargin) / 100;

        double y1 = this.Height - bottomMargin;

        double yAxisValue = 0;

        for (int i = 0; i <= scaleFactor; i++)
        {
            // Add y-axis marker line chart.
            Line marker = AddMarkerLineToChart(y1);

            // Draw horizontal grid based on marker location.
            DrawHorizontalGrid(marker.X1, y1);

            // Add the y-marker to the chart.
            AddMarkerTextToChart(y1, yAxisValue);

            // Adjust the top location for next marker.
            y1 -= scale;

            // Increment the y-marker value.
            yAxisValue += yMarkerValue;
        }
    }
    else if (Type == ChartType.HorizontalBar)
    {
        Line yaxis = new Line();
        yaxis.X1 = leftMargin;
        yaxis.Y1 = 0;
        yaxis.X2 = leftMargin;
        yaxis.Y2 = top;
        yaxis.Stroke = new SolidColorBrush(Colors.DarkBlue);
        chartArea.Children.Add(yaxis);
    }
}

绘制图例

绘制图例非常简单。遍历 legends 集合,并在适当的位置绘制具有指定颜色的线条和文本。

/// <summary />
/// Draw chart legends.
/// </summary />
private void DrawLegend()
{
    if (legends == null || legends.Count == 0)
    {
        chartLegendArea.Visibility = Visibility.Hidden;
        return;
    }

    chartLegendArea.Visibility = Visibility.Visible;

    double x1 = 5;
    double y1 = 5;
    double legendWidth = 20;
    TextBlock tb;

    // Draw all legends
    foreach (Legend legend in legends)
    {
        Line legendShape = new Line();
      Size size = new Size(0, 0);
        
        // Calculate the legend width.
        for (int i = 0; i < legends.Count; i++)
        {
            tb = new TextBlock();
            tb.Text = legends[i].LegendText;
            tb.Measure(new Size(Double.PositiveInfinity, 
                                Double.PositiveInfinity));
            size = tb.DesiredSize;
            if (legendWidth < size.Width)
                legendWidth = size.Width;
        }

        legendShape.Stroke = new SolidColorBrush(legend.LegendColor);
        legendShape.StrokeDashCap = PenLineCap.Round;
        legendShape.StrokeThickness = 2;

        legendShape.StrokeStartLineCap = PenLineCap.Round;
        legendShape.StrokeEndLineCap = PenLineCap.Triangle;

        legendShape.X1 = x1;
        legendShape.Y1 = y1;
        legendShape.X2 = x1 + legendWidth;
        legendShape.Y2 = y1;

        chartLegendArea.Children.Add(legendShape);

        TextBlock txtLegend = new TextBlock();
        txtLegend.Text = legend.LegendText;
        txtLegend.Foreground = legendTextColor;

        chartLegendArea.Children.Add(txtLegend);
        
        Canvas.SetTop(txtLegend, y1 - size.Height /2);
        Canvas.SetLeft(txtLegend, legendShape.X1 + legendShape.X2 + 5);

        y1 += 15;
    }
}

绘制 X 轴标签

/// <summary />
/// Draw the x-axis label.
/// </summary />
/// <param name="row" />The bar data row.</param />
private void DrawXAxisLabel(DataRow row)
{
    // Setup XAxis label
    TextBlock markText = new TextBlock();
    markText.Text = row[XAxisField].ToString();
    markText.Width = 80;
    markText.HorizontalAlignment = HorizontalAlignment.Stretch;

    markText.Foreground = TextColor;
    markText.TextAlignment = TextAlignment.Center;
    markText.FontSize = 8;

    markText.MouseEnter += new MouseEventHandler(XText_MouseEnter);
    markText.MouseLeave += new MouseEventHandler(XText_MouseLeave);

    if (SmartAxisLabel)
    {
        Transform st = new SkewTransform(0, 20);
        markText.RenderTransform = st;
    }

    chartArea.Children.Add(markText);
    Canvas.SetTop(markText, this.Height - bottomMargin);  // adjust y location
    Canvas.SetLeft(markText, left + leftMargin / 2);
}

绘制 Y 轴标签

/// <summary />
/// Draw the y-axis label.
/// </summary />
/// <param name="row" />The bar data row.</param />
private void DrawYAxisLabel(DataRow row)
{
    TextBlock markText = new TextBlock();
    markText.Text = row[XAxisField].ToString();
    markText.Width = 80;
    markText.HorizontalAlignment = HorizontalAlignment.Stretch;
    markText.Foreground = TextColor;
    markText.TextAlignment = TextAlignment.Center;
    markText.FontSize = 8;

    markText.MouseEnter += new MouseEventHandler(XText_MouseEnter);
    markText.MouseLeave += new MouseEventHandler(XText_MouseLeave);

    if (SmartAxisLabel)
    {
        Transform st = new SkewTransform(0, 20);
        markText.RenderTransform = st;
    }

    chartArea.Children.Add(markText);
    // adjust y location
    Canvas.SetTop(markText, (top + (BarWidth * ValueField.Count)/2));
    Canvas.SetLeft(markText, left-leftMargin+10);
}

反馈

我认为代码正在根据您的反馈得到改进,因此请提供建设性的批评,因为这将帮助我提高 WPF 技能,并可能让我能够撰写更好的文章。

关注点

我在开发这个业余代码时学到的一些东西概述如下:

  • 理解绘制简单的垂直和水平条形图。
  • 捕获条形图上的事件。这可以用于钻取功能。
  • 根据可用区域动态调整控件大小。

开发思路

下面是一些我希望在下一篇文章中解决的目标。这仍然是第一个版本遗留的,但希望我能在一段时间内将其涵盖。

  • 使图表组件可重用
  • 支持任何数据源
  • 增强的工具提示模板
  • 钻取功能
  • 内置图表过滤功能

这些目标可能会随着时间的推移而重新排序。根据需要可能会添加或删除新的目标。

致谢

所有功劳归于 CodeProject WPF 文章作者,因为我从他们的文章中学到了很多。

历史

  • 2008 年 10 月 1 日 - 重构并发布了水平绘图功能。
© . All rights reserved.