简单的 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 修复