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






4.67/5 (21投票s)
本文是对之前文章的扩展,演示了使用 WPF 创建水平和垂直条形图的简单步骤。
目录
“一图胜千言”。 因此,让我们先来看一些条形图控件在实际运行中的截图。
引言
我将通过创建一个简单的图表库来学习 WPF。我从创建一个小型 WPF 条形图控件开始。您可以在 CodeProject 上查看我的第一个尝试,地址是:简单 WPF 条形图控件。
这本身是一次很棒的学习经历。为了进一步加深对该主题的理解,我为这个图表库添加了更多功能,并决定与大家分享。
除了修复一些小错误之外,新添加的功能包括:
- 修改了整个布局,以获得时尚的外观(尽管还不是非常时尚)。
- 增加了水平条形图功能。
- 增加了图表与宿主应用程序之间的事件通信。
当前代码仍然是为数据集编写的。您可以轻松地替换它以支持任何其他数据源。本文的目的是仅使用基本元素来让您熟悉 WPF 的图表功能,而无需涉及其他复杂性。
背景
本文假定您已具备 C# 和 WPF 的先验知识。如果您有兴趣学习 WPF,CodeProject 上有一些非常好的资源。以下是其中一些链接:
下图显示了用于表示条形图的骨架 UML 类图。与早期版本相比有一个小变化。现在,我们还有一个 BarEventArgs
对象,它在单击条形时传递给宿主。
要运行附带的演示,请确保以下配置条目对您的环境有效:
<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
图。有关详细属性,请参阅本系列的第一个部分。
chart1
是 BarChart
的一个实例
// 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>
控件按以下层次结构组织:
捕获条形图事件
我们通过添加 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());
}
在上面的示例中,我们只是显示条形的信息。BarObject
的 ToString()
方法返回一个格式良好的字符串,用于调试目的。它看起来如下面的图所示:
绘制图表
主要的绘图活动发生在 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 轴的设置。
/// <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 轴的设置。
/// <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 日 - 重构并发布了水平绘图功能。