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

简单的 WPF 条形图控件

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.81/5 (20投票s)

2008年9月28日

CPOL

4分钟阅读

viewsIcon

131499

downloadIcon

4023

本文介绍了如何使用 WPF 创建一个简单的柱状图的分步说明。

Chart 1

引言

本文介绍了如何使用 WPF 和 C# 创建一个简单的柱状图的分步说明。您可以将 C# 替换为您选择的任何其他 .NET 语言。本文仅用于学习目的。该图表不具有专业质量。可能存在一些 bug,但也有些优点。随着 WPF 技能的逐步提高,我将不断改进此代码,并希望发布更多文章。

当前代码是为处理 XML 数据而编写的。您可以轻松地替换它。在后续文章中,我将尝试改进这一点,使其更通用,因为本文的目的是仅通过基本元素来引导您了解 WPF 的图表功能。

我感谢我目前公司中的一些同事(未提及姓名,因为我没有征得他们同意),他们以某种形式丰富了我对 WPF 的理解(尽管可能很有限)。

背景

本文假定您已具备 C# 和 WPF 的先验知识。如果您有兴趣学习 WPF,Code Project 上有一些优秀的资源。以下列出其中一些:

下面显示了表示 BarChart 控件的骨架 UML 类图

imgUML

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

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

Using the Code

让我们来看看如何在您的应用程序中使用此 BarChart 来创建单值图表。

chart1BarChart 的一个实例

// Resets the values
chart1.Reset();

// Setting this property provides angled dimension
// to the X-axis for more readability
chart1.SmartAxisLabel = true;

// Set the chart title
chart1.Title = "Sales by Region";

// Set the Y-Axis value
chart1.ValueField.Add("Amount");

// Set the chart tooltip. {field} will be replaced by current bar value.
// Need to improve in this little templating.
chart1.ToolTipText = "Sales($): {field}";

// Set the x-axis text
chart1.XAxisText = "Sales";

// Set the x-axis data field
chart1.XAxisField = "Region";

// Setting this value displays the bar value on top of the bar.
chart1.ShowValueOnBar = true;

// Get the data required for displaying the graph.
DataSet dsetAll = QueryHelper.GetDataSet(
        ConfigurationManager.AppSettings["SALES_XML"]);

// Set the datasource
chart1.DataSource = dsetAll;

// Generate the Graph
chart1.Generate();

要创建多值柱状图,请使用以下代码片段

// Reset the chart
chart2.Reset();

// Setting this property provides angled
// dimension to the X-axis for more readability
chart2.SmartAxisLabel = true;

// Set the chart title
chart2.Title = "Sales Summary by Region";

// Add the range of values to the chart.
chart2.ValueField.Add("Min");
chart2.ValueField.Add("Max");
chart2.ValueField.Add("Avg");

// Set the chart tooltip.
// {field} will be replaced by current bar value.
// Need to improve in this little templating.
chart2.ToolTipText = "Sales($): {field}";

// Set the x-axis text
chart2.XAxisText = "Sales";

// Set the x-axis field
chart2.XAxisField = "Region";

// Setting this value displays the bar value on top of the bar.
chart2.ShowValueOnBar = true;

// Get the data
dsetAll = QueryHelper.GetDataSet(
  ConfigurationManager.AppSettings["SALES1_XML"]);

// Assign to datasource
chart2.DataSource = dsetAll;

// Generate the chart
chart2.Generate();

柱状图解析

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>
        <Grid.RowDefinitions>
            <RowDefinition Height="0.05*"></RowDefinition>
            <RowDefinition></RowDefinition>
        </Grid.RowDefinitions>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="*"></ColumnDefinition>
        </Grid.ColumnDefinitions>
            <Slider Name="zoomSlider" Grid.Row="0" Grid.Column="1" Height="20"
            DockPanel.Dock="Top" Minimum="1" Maximum="5" Value="1"/>
            <ScrollViewer Grid.Row="1" Grid.Column="1" 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>

设置图表

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

/// <summary>
/// Add chart controls to chart.
/// This creates a basic layout for the chart.
/// </summary>
private void AddChartControlsToChart()
{
    chartArea.Children.Add(txtXAxis);
    chartArea.Children.Add(txtTopTitle);
    chartArea.Children.Add(bubbleText);
}

绘制图表

主要的绘图活动发生在 Generate 方法中。这张思维导图显示了绘制图表的重要活动

imgMM

  • 添加图表控件以设置基本布局。
  • 设置图表区域。
  • 获取数据源。
  • 检查是否存在数据。如果没有,则绘制空图表。
  • 计算 Y 轴最大值。这对于正确的缩放是必需的。
  • 绘制 X 轴。
  • 绘制 Y 轴。
  • 绘制 X 轴标签。
  • 为每个数据值绘制条形。
  • 绘制图例。
/// <summary>
/// Creates the chart based on the datasource.
/// </summary>
public void Generate()
{
    // Reset / Clear
    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 nodata found text.
        txtNoData.Visibility = Visibility.Hidden;

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

        // Prepare the chart for rendering.
        // Does some basic setup.
        PrepareChartForRendering();

        // 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)
        {
            // Draw x-axis label based on datarow.
            DrawXAxisLabel(row);

            // Set the barwidth. This is required to adjust
            // the size based on available no. of bars.
            SetBarWidth(barCount);

            // 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.
                DrawBar(isSeries, legendAdded, row,
                        ref currentSeries, valField);

            }
            legendAdded = true;

            // Set up location for next bar in series.
            if (isSeries)
                left = left + spaceBetweenBars;
        }

        // Reset the chartarea to accommodate all the chart elements.
        if ((left + BarWidth) > chartArea.Width)
            chartArea.Width = left + BarWidth;

	  // Draw the x-axis.
        DrawXAxis();

        // Draw the y-axis.
        DrawYAxis();

        // Draw the legend.
        DrawLegend();
    }
}

绘制条形

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 position for subsequent bars.
    if (isSeries)
        left = left + rect.Width;
    else
        left = left + BarWidth + spaceBetweenBars;

    // Increment the series
    currentSeries++;
}

绘制 X 轴

设置 X 轴非常简单。只需在适当的位置添加线条控件即可

/// <summary>
/// Draws XAxis
/// </summary>
private void DrawXAxis()
{
    // Draw axis
    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);
}

绘制 Y 轴

设置 Y 轴非常简单。为了设置 Y 轴,必须完成以下活动:

  • 绘制 Y 轴。
  • 计算 Y 轴标记值。
  • 计算 Y 轴比例。
  • 为每个比例因子添加标记线和标记文本。
/// <summary>
/// Draws YAxis. Here we use the maxData value calculated earlier.
/// This method also sets up the y-axis marker.
/// </summary>
private void DrawYAxis()
{
    // Drawing yaxis is as simple as adding
    // a line control at appropriate location.
    Line yaxis = new Line();
    yaxis.X1 = leftMargin;
    yaxis.Y1 = 0;
    yaxis.X2 = leftMargin;
    yaxis.Y2 = this.Height - bottomMargin;
    yaxis.Stroke = new SolidColorBrush(Colors.Silver);
    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;
    }
}

绘制图例

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

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

    // Initialize legend location.
    double legendX1 = leftMargin + txtXAxis.Text.Length + 100;
    double legendWidth = 20;

    // Draw all legends
    foreach (Legend legend in legends)
    {
        Line legendShape = new Line();

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

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


        legendShape.X1 = legendX1;
        legendShape.Y1 = this.Height - 10;
        legendShape.X2 = legendX1 + legendWidth;
        legendShape.Y2 = this.Height - 10;

        chartArea.Children.Add(legendShape);

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

        chartArea.Children.Add(txtLegend);
        Canvas.SetTop(txtLegend, this.Height - 20);
        Canvas.SetLeft(txtLegend, legendShape.X2 + 2);

        legendX1 += legendWidth + 30 + txtLegend.Text.Length;
    }
}

反馈

上面的代码是我学习 WPF 的结果。代码在任何方面都不是最优化的或完美的。这个想法只是发布我学到的东西,以便其他人可以从中受益。请提供建设性的批评,因为这将帮助我提高 WPF 技能并编写更好的文章。

关注点

在开发此业余项目代码时,我学到的一些内容概述如下:

  • 理解基本的图表绘制
  • 理解基本 WPF 布局
  • 如何绘制智能轴标签
  • 工具提示和模板(开发中)
  • 添加图例
  • 缩放功能

开发思路

下面重点介绍我将在下一篇文章中解决的一些目标:

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

我可能无法一次性满足上述所有意图,但会在一段时间内逐步完成。

致谢

所有功劳归功于 CodeProject WPF 文章作者,因为我主要从他们的文章中学习。

历史

  • 2008 年 9 月 28 日 - 首次发布(杂乱无章,可能存在 bug,但作为学习练习)
  • 2008 年 10 月 1 日 - 次要 bug 修复
© . All rights reserved.