WPF 兼容 MS Chart 控件






4.92/5 (12投票s)
本文演示了如何将 WinForm 版本的 Microsoft 图表控件转换为 WPF 和 MVVM 兼容的图表控件,以及如何在 WPF 应用程序中使用它来创建各种图表。
引言
众所周知,Microsoft (MS) 图表控件是专门为 Windows Forms 和 ASP.NET 应用程序开发的。该控件套件提供了广泛的图表类型和图表功能,包括所有标准图表类型——折线图、条形图、饼图等——以及更专业的图表,如金字塔图、气泡图、股票图和技术指标图。它还提供了一整套全面的图表功能,包括支持多系列、可自定义的图例、趋势线和标签。
不幸的是,MS 图表控件不直接支持 WPF 和 MVVM。如果您真的想在 WPF 应用程序中使用它,您必须使用 WindowsFormsHost
元素来托管 MS 图表控件,这会破坏 WPF 数据绑定和 MVVM 规则。您可能会注意到 Microsoft 在几年前发布了一个 WPF Toolkit 图表控件。然而,这个工具包支持的图表类型有限,运行速度很慢。您无法做太多事情来提高其性能。
在本文中,我将向您展示如何将 MS 图表控件封装到 WPF UserControl
中,使其与 MVVM 兼容。然后,您可以在 WPF 应用程序中以 MVVM 模式和数据绑定方式使用此 WPF 控件,就像使用 WPF 内置控件一样。
WPF 兼容 MS Chart 控件
在这里,我将使用 WindowsFormsHost
元素将原始 MS 图表控件嵌入到名为 MsChart
的 WPF UserControl
中
<UserControl x:Class="WpfMsChart.MsChart"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mschart="clr-namespace:System.Windows.Forms.DataVisualization.Charting;
assembly=System.Windows.Forms.DataVisualization"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="300">
<Grid x:Name="grid1" Margin="10">
<WindowsFormsHost Background="{x:Null}">
<mschart:Chart x:Name="myChart"/>
</WindowsFormsHost>
</Grid>
</UserControl>
此 XAML 非常简单。首先,我们需要将 MS 图表控件的 .NET 命名空间和程序集映射到 XML 命名空间:System.Windows.Forms.DataVisualization
。使用此 XML 命名空间和 MS 图表控件类名(即 Chart
),我们将图表控件添加到 WindowsFormsHost
中,并将其命名为 myChart
。
由于原始 MS 图表控件不支持 WPF 数据绑定和 MVVM 模式,我们将使用代码隐藏文件来实现此 WPF MsChart UserControl
。以下代码片段是此控件的实现
using System.Windows;
using System.Windows.Controls;
using System.Windows.Forms.DataVisualization.Charting;
using Caliburn.Micro;
using System.Collections.Specialized;
namespace WpfMsChart
{
/// <summary>
/// Interaction logic for MsChart.xaml
/// </summary>
public partial class MsChart : UserControl
{
public MsChart()
{
InitializeComponent();
SeriesCollection = new BindableCollection<Series>();
}
public static DependencyProperty XValueTypeProperty =
DependencyProperty.Register("XValueType", typeof(string),
typeof(MsChart), new FrameworkPropertyMetadata
("Double", FrameworkPropertyMetadataOptions.BindsTwoWayByDefault));
public string XValueType
{
get { return (string)GetValue(XValueTypeProperty); }
set { SetValue(XValueTypeProperty, value); }
}
public static DependencyProperty XLabelProperty =
DependencyProperty.Register("XLabel", typeof(string),
typeof(MsChart), new FrameworkPropertyMetadata("X Axis",
FrameworkPropertyMetadataOptions.BindsTwoWayByDefault));
......
public BindableCollection<Series> SeriesCollection
{
get { return (BindableCollection<Series>)GetValue(SeriesCollectionProperty); }
set { SetValue(SeriesCollectionProperty, value); }
}
private static void OnSeriesChanged(object sender, DependencyPropertyChangedEventArgs e)
{
var ms = sender as MsChart;
var sc = e.NewValue as BindableCollection<Series>;
if (sc != null)
sc.CollectionChanged += ms.sc_CollectionChanged;
}
private void sc_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
if (SeriesCollection != null)
{
CheckCount = 0;
if (SeriesCollection.Count > 0)
CheckCount = SeriesCollection.Count;
}
}
private static DependencyProperty CheckCountProperty =
DependencyProperty.Register("CheckCount", typeof(int),
typeof(MsChart), new FrameworkPropertyMetadata(0, StartChart));
private int CheckCount
{
get { return (int)GetValue(CheckCountProperty); }
set { SetValue(CheckCountProperty, value); }
}
private static void StartChart(object sender, DependencyPropertyChangedEventArgs e)
{
var ms = sender as MsChart;
if (ms.CheckCount > 0)
{
ms.myChart.Visible = true;
ms.myChart.Series.Clear();
ms.myChart.Titles.Clear();
ms.myChart.Legends.Clear();
ms.myChart.ChartAreas.Clear();
MSChartHelper.MyChart(ms.myChart, ms.SeriesCollection,
ms.Title, ms.XLabel, ms.YLabel, ms.ChartBackground, ms.Y2Label);
if (ms.myChart.ChartAreas.Count > 0)
ms.myChart.ChartAreas[0].Area3DStyle.Enable3D = ms.IsArea3D;
ms.myChart.DataBind();
}
else
ms.myChart.Visible = false;
}
......
}
}
在这里,我们使用 Caliburn.Micro
作为我们的 MVVM 框架。我们将 MS 图表控件常用的属性转换为依赖属性。在某些情况下,我们希望在设置依赖属性的值后执行一些逻辑和计算方法。我们可以通过实现一个回调方法来执行这些任务,该回调方法在通过属性包装器或直接 SetValue
调用更改属性时触发。例如,在创建包含 MS 图表 Series
对象的 SeriesCollection
后,我们希望 MsChart
控件自动为这些 Series
对象创建相应的图表。前面代码隐藏文件中的代码片段向您展示了如何实现这样的回调方法。SeriesCollectionProperty
包含一个名为 OnSeriesChanged
的回调方法。在此回调方法内部,我们将事件处理程序添加到 CollectionChanged
属性,当 SeriesCollection
更改时,它将触发。在 CollectionChanged
处理程序中,我们将另一个私有依赖属性 CheckCount
设置为 SeriesCollection.Count
。如果 CheckCount > 0
,我们知道 SeriesCollection
确实包含 Series
对象,然后我们为 CheckCount
属性实现另一个名为 StartChart
的回调方法,通过调用在 MSChartHelper
类中实现的 MyChart
方法来创建图表。MSChartHelper
类中包含的方法只是定义了各种预定义图表样式,我将在下一节中讨论。
您可以将 DataSource
依赖属性绑定到 MS 图表控件的 DataSource
属性。如果所需的图表类型不在 MSChartHelper
类中实现的预定义图表类型中,Chart1
依赖属性允许您直接访问 MS 图表控件。
辅助类
在上一节中,我们在 WPF 中创建了一个 MsChart
控件,它封装了原始 MS 图表控件(Windows Forms 版本)。MsChart
控件中的 StartChart
方法调用 MSChartHelper
类中的 MyChart
方法。您还可以按照此处介绍的步骤根据应用程序的要求创建自己的图表类型。这样做的好处是您不需要为您创建的每个图表设置各种图表样式。将与图表样式相关的代码放在一个地方形成可重用的模板,您可以轻松更改图表样式。例如,如果您希望所有图表都具有蓝色背景,您只需在模板中更改一次,而无需对每个图表进行任何更改。
以下是 MSChartHelper
类的代码
using System.Collections.Generic;
using System.Windows.Forms.DataVisualization.Charting;
using System.Drawing;
using Caliburn.Micro;
namespace WpfMsChart
{
public static class MSChartHelper
{
public static void MyChart(Chart chart1, BindableCollection<Series> chartSeries,
string chartTitle, string xLabel, string yLabel, ChartBackgroundColor backgroundColor,
params string[] y2Label)
{
if (chart1.ChartAreas.Count < 1)
{
ChartArea area = new ChartArea();
ChartStyle(chart1, area, backgroundColor);
}
if (chartTitle != "")
chart1.Titles.Add(chartTitle);
chart1.ChartAreas[0].AxisX.Title = xLabel;
chart1.ChartAreas[0].AxisY.Title = yLabel;
if (y2Label.Length > 0)
chart1.ChartAreas[0].AxisY2.Title = y2Label[0];
foreach (var ds in chartSeries)
chart1.Series.Add(ds);
if (chartSeries.Count > 1)
{
Legend legend = new Legend();
legend.Font = new System.Drawing.Font("Trebuchet MS", 7.0F, FontStyle.Regular);
legend.BackColor = Color.Transparent;
legend.AutoFitMinFontSize = 5;
legend.LegendStyle = LegendStyle.Column;
legend.IsDockedInsideChartArea = true;
legend.Docking = Docking.Left;
legend.InsideChartArea = chart1.ChartAreas[0].Name;
chart1.Legends.Add(legend);
}
}
public static void ChartStyle
(Chart chart1, ChartArea area, ChartBackgroundColor backgroundColor)
{
int r1 = 211;
int g1 = 223;
int b1 = 240;
int r2 = 26;
int g2 = 59;
int b2 = 105;
int r3 = 165;
int g3 = 191;
int b3 = 228;
switch (backgroundColor)
{
case ChartBackgroundColor.Blue:
chart1.BackColor = Color.FromArgb(r1, g1, b1);
chart1.BorderlineColor = Color.FromArgb(r2, g2, b2);
area.BackColor = Color.FromArgb(64, r3, g3, b3);
break;
case ChartBackgroundColor.Green:
chart1.BackColor = Color.FromArgb(g1, b1, r1);
chart1.BorderlineColor = Color.FromArgb(g2, b2, r2);
area.BackColor = Color.FromArgb(64, g3, b3, r3);
break;
case ChartBackgroundColor.Red:
chart1.BackColor = Color.FromArgb(b1, r1, g1);
chart1.BorderlineColor = Color.FromArgb(b2, r2, g2);
area.BackColor = Color.FromArgb(64, b3, r3, g3);
break;
case ChartBackgroundColor.White:
chart1.BackColor = Color.White;
chart1.BorderlineColor = Color.White;
area.BackColor = Color.White;
break;
}
if (backgroundColor != ChartBackgroundColor.White)
{
chart1.BackSecondaryColor = Color.White;
chart1.BackGradientStyle = GradientStyle.TopBottom;
chart1.BorderlineDashStyle = ChartDashStyle.Solid;
chart1.BorderlineWidth = 2;
chart1.BorderSkin.SkinStyle = BorderSkinStyle.Emboss;
area.Area3DStyle.IsClustered = true;
area.Area3DStyle.Perspective = 10;
area.Area3DStyle.IsRightAngleAxes = false;
area.Area3DStyle.WallWidth = 0;
area.Area3DStyle.Inclination = 15;
area.Area3DStyle.Rotation = 10;
}
area.AxisX.IsLabelAutoFit = false;
area.AxisX.LabelStyle.Font = new Font("Trebuchet MS", 7.25F, FontStyle.Regular);
//area.AxisX.LabelStyle.IsEndLabelVisible = false;
area.AxisX.IntervalAutoMode = IntervalAutoMode.VariableCount;
area.AxisX.LineColor = Color.FromArgb(64, 64, 64, 64);
area.AxisX.MajorGrid.LineColor = Color.FromArgb(64, 64, 64, 64);
area.AxisX.IsStartedFromZero = false;
area.AxisX.RoundAxisValues();
area.AxisY.IsLabelAutoFit = false;
area.AxisY.LabelStyle.Font = new Font("Trebuchet MS", 7.25F,
System.Drawing.FontStyle.Regular);
area.AxisY.LineColor = Color.FromArgb(64, 64, 64, 64);
area.AxisY.MajorGrid.LineColor = Color.FromArgb(64, 64, 64, 64);
area.AxisY.IsStartedFromZero = false;
area.AxisY2.IsLabelAutoFit = false;
area.AxisY2.LabelStyle.Font = new Font("Trebuchet MS", 7.25F,
System.Drawing.FontStyle.Regular);
area.AxisY2.LineColor = Color.FromArgb(64, 64, 64, 64);
area.AxisY2.MajorGrid.LineColor = Color.FromArgb(15, 15, 15, 15);
area.AxisY2.IsStartedFromZero = false;
area.BackSecondaryColor = System.Drawing.Color.White;
area.BackGradientStyle = GradientStyle.TopBottom;
area.BorderColor = Color.FromArgb(64, 64, 64, 64);
area.BorderDashStyle = ChartDashStyle.Solid;
area.Position.Auto = false;
area.Position.Height = 82F;
area.Position.Width = 88F;
area.Position.X = 3F;
area.Position.Y = 10F;
area.ShadowColor = Color.Transparent;
chart1.ChartAreas.Add(area);
chart1.Invalidate();
}
public static List<System.Drawing.Color> GetColors()
{
List<Color> my_colors = new List<Color>();
my_colors.Add(Color.DarkBlue);
my_colors.Add(Color.DarkRed);
my_colors.Add(Color.DarkGreen);
my_colors.Add(Color.Black);
my_colors.Add(Color.DarkCyan);
my_colors.Add(Color.DarkViolet);
my_colors.Add(Color.DarkOrange);
my_colors.Add(Color.Maroon);
my_colors.Add(Color.SaddleBrown);
my_colors.Add(Color.DarkOliveGreen);
return my_colors;
}
}
public enum ChartBackgroundColor
{
Blue = 0,
Green = 1,
Red = 2,
White = 3,
}
}
MyChart
方法将 chart1
、chartSeries
、chartTitle
、xLabel
、yLabel
、backgroundColor
和 y2Label
作为输入参数;chart1
参数直接分配给 MsChart
控件中的 myChart
,所有其他参数都暴露给 MsChart
控件代码隐藏文件中定义的依赖属性。MyChart
方法还调用另一个名为 ChartStyle
的方法,该方法定义了各种与图表样式相关的属性,包括背景颜色、标签字体、图表区域外观、网格线等。在这里,我们使用 ChartBackgroundColorEnum
实现了四种背景颜色:Blue
、Green
、Red
和 White
。您可以根据需要轻松添加更多图表类型和背景颜色。我们还在 GetColors
方法中创建了十种预定义颜色的列表,我们可以用它们来指定图表系列的颜色。
使用 WPF MsChart 控件创建图表
在本节中,我将通过一个示例向您展示如何使用前面几节中实现的 WPF MsChart
控件创建几种不同的图表。以下是此示例中名为 MainView
的视图的 XAML 文件
<Window x:Class="WpfMsChart.MainView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:WpfMsChart"
xmlns:cal="http://www.caliburnproject.org"
mc:Ignorable="d"
Title="MainView" Height="300" Width="500">
<Grid Margin="10">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="150"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<StackPanel>
<Button x:Name="BarChart" Content="Bar Chart" Width="120" Margin="0 10 0 0"/>
<Button x:Name="LineChart" Content="Line Chart" Width="120" Margin="0 10 0 0"/>
<Button x:Name="PieChart" Content="Pie Chart" Width="120" Margin="0 10 0 0"/>
<Button x:Name="PolarChart" Content="Polar Chart" Width="120" Margin="0 10 0 0"/>
</StackPanel>
<Grid Grid.Column="1">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<local:MsChart SeriesCollection="{Binding BarSeriesCollection}"
ChartBackground="Blue" Title="Bar Chart"/>
<local:MsChart SeriesCollection="{Binding LineSeriesCollection}"
ChartBackground="Red" Title="Line Chart" Grid.Column="1"/>
<local:MsChart SeriesCollection="{Binding PieSeriesCollection}"
ChartBackground="Green" Title="Pie Chart" Grid.Row="1" IsArea3D="True"/>
<local:MsChart SeriesCollection="{Binding PolarSeriesCollection}"
ChartBackground="White" Title="Polar Chart" XLabel="" YLabel=""
Grid.Row="1" Grid.Column="1"/>
</Grid>
</Grid>
</Window>
使用 XAML 命名空间 local
和用户控件的类名 MsChart
,我们添加控件的方式与向 XAML 文件添加任何其他类型的对象完全相同,尽管 MsChart
控件包含托管在 WindowsFormsHost
中的 Windows Forms MS 图表控件。您可以看到在此示例中,我们希望创建四个简单的图表,包括条形图、折线图、饼图和极坐标图。在这种情况下,我们需要指定一个关键属性 SeriesCollection
,它保存图表系列,并且应该在视图模型中定义。
以下是相应视图模型的代码
using System;
using Caliburn.Micro;
using System.Windows.Forms.DataVisualization.Charting;
namespace WpfMsChart
{
public class MainViewModel : PropertyChangedBase
{
public BindableCollection<Series> BarSeriesCollection { get; set; }
public BindableCollection<Series> LineSeriesCollection { get; set; }
public BindableCollection<Series> PieSeriesCollection { get; set; }
public BindableCollection<Series> PolarSeriesCollection { get; set; }
public MainViewModel()
{
BarSeriesCollection = new BindableCollection<Series>();
LineSeriesCollection = new BindableCollection<Series>();
PieSeriesCollection = new BindableCollection<Series>();
PolarSeriesCollection = new BindableCollection<Series>();
}
public void BarChart()
{
double[] data1 = new double[] { 32, 56, 35, 12, 35, 6, 23 };
double[] data2 = new double[] { 67, 24, 12, 8, 46, 14, 76 };
BarSeriesCollection.Clear();
Series ds = new Series();
ds.ChartType = SeriesChartType.Column;
ds["DrawingStyle"] = "Cylinder";
ds.Points.DataBindY(data1);
BarSeriesCollection.Add(ds);
ds = new Series();
ds.ChartType = SeriesChartType.Column;
ds["DrawingStyle"] = "Cylinder";
ds.Points.DataBindY(data2);
BarSeriesCollection.Add(ds);
}
public void LineChart()
{
LineSeriesCollection.Clear();
Series ds = new Series();
ds.ChartType = SeriesChartType.Line;
ds.BorderDashStyle = ChartDashStyle.Solid;
ds.MarkerStyle = MarkerStyle.Diamond;
ds.MarkerSize = 8;
ds.BorderWidth = 2;
ds.Name = "Sine";
for (int i = 0; i < 70; i++)
{
double x = i / 5.0;
double y = 1.1 * Math.Sin(x);
ds.Points.AddXY(x, y);
}
LineSeriesCollection.Add(ds);
ds = new Series();
ds.ChartType = SeriesChartType.Line;
ds.BorderDashStyle = ChartDashStyle.Dash;
ds.MarkerStyle = MarkerStyle.Circle;
ds.MarkerSize = 8;
ds.BorderWidth = 2;
ds.Name = "Cosine";
for (int i = 0; i < 70; i++)
{
double x = i / 5.0;
double y = 1.1 * Math.Cos(x);
ds.Points.AddXY(x, y);
}
LineSeriesCollection.Add(ds);
}
public void PieChart()
{
PieSeriesCollection.Clear();
Random random = new Random();
Series ds = new Series();
for (int i = 0; i < 5; i++)
ds.Points.AddY(random.Next(10, 50));
ds.ChartType = SeriesChartType.Pie;
ds["PointWidth"] = "0.5";
ds.IsValueShownAsLabel = true;
ds["BarLabelStyle"] = "Center";
ds["DrawingStyle"] = "Cylinder";
PieSeriesCollection.Add(ds);
}
public void PolarChart()
{
PolarSeriesCollection.Clear();
Series ds = new Series();
ds.ChartType = SeriesChartType.Polar;
ds.BorderWidth = 2;
for (int i = 0; i < 360; i++)
{
double x = 1.0 * i;
double y = 0.001 + Math.Abs(Math.Sin(2.0 * x * Math.PI / 180.0) *
Math.Cos(2.0 * x * Math.PI / 180.0));
ds.Points.AddXY(x, y);
}
PolarSeriesCollection.Add(ds);
}
}
}
在这里,我们定义了四个 Series
集合和四个方法,用于创建条形图、折线图、饼图和极坐标图。如果您以前在 Windows Forms 应用程序中使用过 MS 图表控件,您应该熟悉每个方法中的代码。我们在每个方法中创建 Series
对象,并将系列对象添加到相应的系列集合中,这些集合数据绑定到视图中定义的 MsChart
控件。通过这种方式,我们可以将 MS 图表添加到我们的 WPF 应用程序中,并符合 MVVM 规范。
运行此示例生成的结果如下图所示
结论
在这里,我详细介绍了如何将 MS 图表控件转换为 WPF 和 MVVM 兼容的图表控件,以及如何在 WPF 应用程序中使用它来创建各种图表。