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

WPF 兼容 MS Chart 控件

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.92/5 (12投票s)

2017 年 1 月 5 日

CPOL

6分钟阅读

viewsIcon

43095

downloadIcon

2317

本文演示了如何将 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 方法将 chart1chartSerieschartTitlexLabelyLabelbackgroundColory2Label 作为输入参数;chart1 参数直接分配给 MsChart 控件中的 myChart,所有其他参数都暴露给 MsChart 控件代码隐藏文件中定义的依赖属性。MyChart 方法还调用另一个名为 ChartStyle 的方法,该方法定义了各种与图表样式相关的属性,包括背景颜色、标签字体、图表区域外观、网格线等。在这里,我们使用 ChartBackgroundColorEnum 实现了四种背景颜色:BlueGreenRedWhite。您可以根据需要轻松添加更多图表类型和背景颜色。我们还在 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 应用程序中使用它来创建各种图表。

© . All rights reserved.