简单的 WPF 条形图控件






4.81/5 (20投票s)
本文介绍了如何使用 WPF 创建一个简单的柱状图的分步说明。
 
 
引言
本文介绍了如何使用 WPF 和 C# 创建一个简单的柱状图的分步说明。您可以将 C# 替换为您选择的任何其他 .NET 语言。本文仅用于学习目的。该图表不具有专业质量。可能存在一些 bug,但也有些优点。随着 WPF 技能的逐步提高,我将不断改进此代码,并希望发布更多文章。
当前代码是为处理 XML 数据而编写的。您可以轻松地替换它。在后续文章中,我将尝试改进这一点,使其更通用,因为本文的目的是仅通过基本元素来引导您了解 WPF 的图表功能。
我感谢我目前公司中的一些同事(未提及姓名,因为我没有征得他们同意),他们以某种形式丰富了我对 WPF 的理解(尽管可能很有限)。
背景
本文假定您已具备 C# 和 WPF 的先验知识。如果您有兴趣学习 WPF,Code Project 上有一些优秀的资源。以下列出其中一些:
下面显示了表示 BarChart 控件的骨架 UML 类图
 
 
要运行附加的演示,请确保以下配置条目对您的环境有效
<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 来创建单值图表。
chart1 是 BarChart 的一个实例
// 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 方法中。这张思维导图显示了绘制图表的重要活动

- 添加图表控件以设置基本布局。
- 设置图表区域。
- 获取数据源。
- 检查是否存在数据。如果没有,则绘制空图表。
- 计算 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 修复


