System.Windows.Forms.DataVisualization.Charting 完全指南

System.Windows.Forms.DataVisualization.Charting 完全指南

第 1 章:组件概述

1.1 什么是 Chart 控件

System.Windows.Forms.DataVisualization.Charting 是 .NET Framework 中功能强大的图表控件,提供丰富的图表类型和高度可定制性。

1.2 主要特性

  • 支持 30+ 种图表类型
  • 实时数据更新
  • 丰富的样式定制
  • 交互式功能
  • 数据绑定支持

1.3 环境要求

  • .NET Framework 4.0+
  • Windows Forms 应用程序
  • 引用 System.Windows.Forms.DataVisualization.dll

第 2 章:核心对象模型

2.1 Chart 控件结构

Chart (根对象)
├── ChartAreas (图表区域集合)
│   ├── ChartArea (单个图表区域)
│   │   ├── AxisX (X轴)
│   │   │   ├── MajorGrid (主网格线)
│   │   │   ├── MinorGrid (次网格线)
│   │   │   ├── LabelStyle (标签样式)
│   │   │   └── Title (轴标题)
│   │   ├── AxisY (Y轴)
│   │   │   ├── MajorGrid (主网格线)
│   │   │   ├── MinorGrid (次网格线)
│   │   │   ├── LabelStyle (标签样式)
│   │   │   └── Title (轴标题)
│   │   ├── AxisX2 (次X轴)
│   │   ├── AxisY2 (次Y轴)
│   │   └── CursorX/CursorY (光标)
├── Series (数据系列集合)
│   ├── Series (单个数据系列)
│   │   ├── Points (数据点集合)
│   │   │   └── DataPoint (单个数据点)
│   │   ├── TrendLines (趋势线集合)
│   │   ├── EmptyPointStyle (空点样式)
│   │   └── SmartLabelStyle (智能标签样式)
├── Legends (图例集合)
│   └── Legend (单个图例)
│       ├── CustomItems (自定义图例项)
│       └── CellColumns (单元格列)
├── Titles (标题集合)
│   └── Title (单个标题)
├── Annotations (标注集合)
│   ├── TextAnnotation (文本标注)
│   ├── LineAnnotation (线条标注)
│   ├── RectangleAnnotation (矩形标注)
│   ├── ArrowAnnotation (箭头标注)
│   └── ImageAnnotation (图像标注)
├── DataManipulator (数据操作器)
│   ├── Filter (数据过滤器)
│   ├── Group (数据分组)
│   └── Sort (数据排序)
└── Palettes (调色板集合)

2.2 主要对象说明

  • ChartArea: 定义图表绘制区域
  • Series: 数据系列,包含数据点和样式
  • DataPoint: 单个数据点
  • Axis: 坐标轴配置
  • Legend: 图例显示

第 3 章:基础图表创建

3.1 基本设置步骤

  1. 添加 Chart 控件到窗体
  2. 配置 ChartArea
  3. 添加 Series
  4. 绑定数据
  5. 设置样式

3.2 代码示例:创建基础柱状图

// 初始化 Chart 控件
Chart chart1 = new Chart();
chart1.Size = new Size(600, 400);
chart1.Location = new Point(10, 10);

// 创建图表区域
ChartArea chartArea1 = new ChartArea("ChartArea1");
chartArea1.AxisX.Title = "月份";
chartArea1.AxisY.Title = "销售额";
chart1.ChartAreas.Add(chartArea1);

// 创建数据系列
Series series1 = new Series("销售数据");
series1.ChartType = SeriesChartType.Column;
series1.Points.AddXY("1月", 12000);
series1.Points.AddXY("2月", 15000);
series1.Points.AddXY("3月", 18000);
series1.Points.AddXY("4月", 22000);
series1.Points.AddXY("5月", 19000);
series1.Points.AddXY("6月", 25000);
chart1.Series.Add(series1);

// 添加图例
Legend legend1 = new Legend();
chart1.Legends.Add(legend1);

this.Controls.Add(chart1);

3.3 常用图表类型

  • Column: 柱状图 - 用于比较不同类别的数据
    image

  • Line: 折线图 - 显示数据趋势变化
    image

  • Pie: 饼图 - 显示各部分占比关系
    image

  • Bar: 条形图 - 水平显示的柱状图
    image

  • Area: 面积图 - 强调数量随时间变化的程度
    image

  • Point: 散点图 - 显示两个变量之间的关系
    image

image

  • Bubble: 气泡图 - 散点图的变体,气泡大小表示第三个变量
    image

image

  • Candlestick: K线图 - 用于金融数据展示

  • Spline: 平滑曲线图 - 使用曲线连接数据点
    image

  • StepLine: 阶梯线图 - 使用水平线和垂直线连接数据点
    image

image

  • Range: 范围图 - 显示数据范围(高低值)
    image

  • Radar: 雷达图 - 多变量比较
    image

image

  • Polar: 极坐标图 - 在极坐标系中显示数据
    image

第 4 章:图表样式定制

4.1 颜色和外观定制

// 设置图表背景色和边框
chart1.BackColor = Color.White;
chart1.BorderlineColor = Color.Gray;
chart1.BorderlineWidth = 1;
chart1.BorderlineDashStyle = ChartDashStyle.Solid;

// 设置图表区域背景(渐变效果)
chart1.ChartAreas[0].BackColor = Color.LightBlue;
chart1.ChartAreas[0].BackGradientStyle = GradientStyle.DiagonalRight;
chart1.ChartAreas[0].BackSecondaryColor = Color.White;

// 启用3D效果
chart1.ChartAreas[0].Area3DStyle.Enable3D = true;
chart1.ChartAreas[0].Area3DStyle.Inclination = 30;
chart1.ChartAreas[0].Area3DStyle.Rotation = 10;

4.2 坐标轴详细配置

// X轴配置
chart1.ChartAreas[0].AxisX.Title = "时间";
chart1.ChartAreas[0].AxisX.TitleFont = new Font("Arial", 12, FontStyle.Bold);
chart1.ChartAreas[0].AxisX.Interval = 1;
chart1.ChartAreas[0].AxisX.LabelStyle.Format = "MM-dd";
chart1.ChartAreas[0].AxisX.MajorGrid.LineColor = Color.LightGray;

// Y轴配置(支持双Y轴)
chart1.ChartAreas[0].AxisY.Title = "数值";
chart1.ChartAreas[0].AxisY.Minimum = 0;
chart1.ChartAreas[0].AxisY.Maximum = 100;
chart1.ChartAreas[0].AxisY.Interval = 10;

// 次Y轴配置
chart1.ChartAreas[0].AxisY2.Enabled = AxisEnabled.True;
chart1.ChartAreas[0].AxisY2.Title = "百分比";
chart1.ChartAreas[0].AxisY2.Minimum = 0;
chart1.ChartAreas[0].AxisY2.Maximum = 1;
chart1.ChartAreas[0].AxisY2.LabelStyle.Format = "P0";

// 对数坐标轴
chart1.ChartAreas[0].AxisY.IsLogarithmic = true;
chart1.ChartAreas[0].AxisY.LogarithmBase = 10;

4.3 数据点样式高级配置

// 数据系列样式
series1.Color = Color.Red;
series1.BorderWidth = 2;
series1.BorderColor = Color.DarkRed;
series1.ShadowOffset = 2;

// 标记点样式
series1.MarkerStyle = MarkerStyle.Circle;
series1.MarkerSize = 8;
series1.MarkerColor = Color.White;
series1.MarkerBorderColor = Color.Red;
series1.MarkerBorderWidth = 2;

// 数据标签配置
series1.IsValueShownAsLabel = true;
series1.LabelFormat = "{0:F1}";
series1.LabelForeColor = Color.Black;
series1.LabelBackColor = Color.Yellow;
series1.LabelBorderColor = Color.Black;
series1.LabelBorderWidth = 1;

// 工具提示
series1.ToolTip = "数值: #VALY\
时间: #VALX";

// 自定义数据点颜色
foreach (DataPoint point in series1.Points)
{
    if (point.YValues[0] > 50)
        point.Color = Color.Green;
    else if (point.YValues[0] > 20)
        point.Color = Color.Yellow;
    else
        point.Color = Color.Red;
}

第 5 章:数据绑定与动态更新

5.1 多种数据绑定方式

5.1.1 DataSource 绑定(推荐)

// 创建数据模型
public class SalesData
{
    public string Month { get; set; }
    public decimal Amount { get; set; }
    public decimal Target { get; set; }
}

// 准备数据
List<SalesData> salesList = new List<SalesData>
{
    new SalesData { Month = "1月", Amount = 12000, Target = 10000 },
    new SalesData { Month = "2月", Amount = 15000, Target = 11000 },
    new SalesData { Month = "3月", Amount = 18000, Target = 12000 }
};

// 数据绑定
series1.Points.DataBind(salesList, "Month", "Amount", "");
series2.Points.DataBind(salesList, "Month", "Target", "");

// 或者使用 Chart.DataBindTable 方法
chart1.DataBindTable(salesList, "Month");

5.1.2 手动添加数据点

// 清空现有数据
series1.Points.Clear();

// 批量添加数据点
for (int i = 0; i < 100; i++)
{
    double value = Math.Sin(i * 0.1) * 50 + 50;
    series1.Points.AddXY(i, value);
}

// 使用 AddY 方法(自动生成X值)
series1.Points.AddY(100);
series1.Points.AddY(150);
series1.Points.AddY(200);

5.1.3 实时数据流更新

// 实时数据更新方法
private void UpdateRealTimeData(double newValue)
{
    // 限制数据点数量(性能优化)
    if (series1.Points.Count > 1000)
    {
        series1.Points.RemoveAt(0);
    }
    
    // 添加新数据点
    int xValue = series1.Points.Count > 0 ? 
        (int)series1.Points[series1.Points.Count - 1].XValue + 1 : 0;
    
    series1.Points.AddXY(xValue, newValue);
    
    // 自动调整X轴范围(显示最近100个点)
    if (xValue > 100)
    {
        chart1.ChartAreas[0].AxisX.Minimum = xValue - 100;
        chart1.ChartAreas[0].AxisX.Maximum = xValue;
    }
}

5.2 性能优化策略

5.2.1 数据点数量控制

// 数据采样:当数据点过多时进行采样
private void AddDataWithSampling(double newValue)
{
    const int maxPoints = 500;
    
    if (series1.Points.Count >= maxPoints)
    {
        // 删除前10%的数据点
        int removeCount = maxPoints / 10;
        for (int i = 0; i < removeCount; i++)
        {
            series1.Points.RemoveAt(0);
        }
    }
    
    series1.Points.AddY(newValue);
}

5.2.2 异步更新(避免UI阻塞)

private async Task UpdateChartAsync(List<double> data)
{
    await Task.Run(() =>
    {
        // 在后台线程处理数据
        var processedData = ProcessData(data);
        
        // 切换到UI线程更新图表
        chart1.Invoke(new Action(() =>
        {
            series1.Points.DataBind(processedData, "X", "Y", "");
        }));
    });
}

5.2.3 启用图表优化

// 启用双缓冲减少闪烁
chart1.DoubleBuffered = true;

// 禁用不必要的功能提升性能
chart1.ChartAreas[0].AxisX.LabelStyle.Enabled = false;
chart1.ChartAreas[0].AxisY.LabelStyle.Enabled = false;

// 使用更简单的图表类型
series1.ChartType = SeriesChartType.FastLine;

5.3 数据库数据绑定示例

// 从数据库读取数据并绑定到图表
private void BindChartFromDatabase()
{
    string connectionString = "YourConnectionString";
    string query = "SELECT Month, SalesAmount FROM SalesData WHERE Year = 2024";
    
    using (SqlConnection connection = new SqlConnection(connectionString))
    {
        SqlCommand command = new SqlCommand(query, connection);
        connection.Open();
        
        SqlDataReader reader = command.ExecuteReader();
        while (reader.Read())
        {
            string month = reader["Month"].ToString();
            decimal amount = Convert.ToDecimal(reader["SalesAmount"]);
            series1.Points.AddXY(month, amount);
        }
    }
}

第 6 章:交互功能

6.1 鼠标交互功能实现

6.1.1 缩放和平移功能

// 启用缩放功能
chart1.ChartAreas[0].AxisX.ScaleView.Zoomable = true;
chart1.ChartAreas[0].AxisY.ScaleView.Zoomable = true;

// 启用鼠标滚轮缩放
chart1.ChartAreas[0].AxisX.ScaleView.Zoom(0, 100); // 初始显示范围
chart1.MouseWheel += (sender, e) =>
{
    try
    {
        if (e.Delta > 0) // 滚轮向上,放大
        {
            chart1.ChartAreas[0].AxisX.ScaleView.ZoomReset();
            chart1.ChartAreas[0].AxisY.ScaleView.ZoomReset();
        }
        else // 滚轮向下,缩小
        {
            double xMin = chart1.ChartAreas[0].AxisX.ScaleView.ViewMinimum;
            double xMax = chart1.ChartAreas[0].AxisX.ScaleView.ViewMaximum;
            double yMin = chart1.ChartAreas[0].AxisY.ScaleView.ViewMinimum;
            double yMax = chart1.ChartAreas[0].AxisY.ScaleView.ViewMaximum;
            
            double posXStart = chart1.ChartAreas[0].AxisX.PixelPositionToValue(e.X) - (xMax - xMin) / 4;
            double posXFinish = chart1.ChartAreas[0].AxisX.PixelPositionToValue(e.X) + (xMax - xMin) / 4;
            double posYStart = chart1.ChartAreas[0].AxisY.PixelPositionToValue(e.Y) - (yMax - yMin) / 4;
            double posYFinish = chart1.ChartAreas[0].AxisY.PixelPositionToValue(e.Y) + (yMax - yMin) / 4;
            
            chart1.ChartAreas[0].AxisX.ScaleView.Zoom(posXStart, posXFinish);
            chart1.ChartAreas[0].AxisY.ScaleView.Zoom(posYStart, posYFinish);
        }
    }
    catch { }
};

6.1.2 数据点选择和右键菜单

// 启用数据点选择
chart1.Legends[0].LegendStyle = LegendStyle.Table;
chart1.Series[0].IsValueShownAsLabel = true;

// 右键菜单配置
ContextMenuStrip contextMenu = new ContextMenuStrip();
contextMenu.Items.Add("放大", null, (s, e) => ZoomIn());
contextMenu.Items.Add("缩小", null, (s, e) => ZoomOut());
contextMenu.Items.Add("重置视图", null, (s, e) => ResetView());
contextMenu.Items.Add("-"); // 分隔线
contextMenu.Items.Add("导出图片", null, (s, e) => ExportChart());

chart1.ContextMenuStrip = contextMenu;

// 数据点点击事件
chart1.GetToolTipText += (sender, e) =>
{
    if (e.HitTestResult.ChartElementType == ChartElementType.DataPoint)
    {
        int pointIndex = e.HitTestResult.PointIndex;
        DataPoint point = series1.Points[pointIndex];
        e.Text = $"数值: {point.YValues[0]:F2}\
时间: {point.XValue}";
    }
};

6.2 键盘操作实现

// 启用键盘导航
chart1.KeyDown += (sender, e) =>
{
    switch (e.KeyCode)
    {
        case Keys.Left:
            PanChart(-10, 0); // 向左平移
            break;
        case Keys.Right:
            PanChart(10, 0); // 向右平移
            break;
        case Keys.Up:
            PanChart(0, 10); // 向上平移
            break;
        case Keys.Down:
            PanChart(0, -10); // 向下平移
            break;
        case Keys.Add:
            ZoomIn(); // 放大
            break;
        case Keys.Subtract:
            ZoomOut(); // 缩小
            break;
        case Keys.Home:
            ResetView(); // 重置视图
            break;
    }
};

private void PanChart(double xOffset, double yOffset)
{
    double xMin = chart1.ChartAreas[0].AxisX.ScaleView.ViewMinimum;
    double xMax = chart1.ChartAreas[0].AxisX.ScaleView.ViewMaximum;
    double yMin = chart1.ChartAreas[0].AxisY.ScaleView.ViewMinimum;
    double yMax = chart1.ChartAreas[0].AxisY.ScaleView.ViewMaximum;
    
    chart1.ChartAreas[0].AxisX.ScaleView.Zoom(xMin + xOffset, xMax + xOffset);
    chart1.ChartAreas[0].AxisY.ScaleView.Zoom(yMin + yOffset, yMax + yOffset);
}

6.3 完整的事件处理系统

// 鼠标点击事件
chart1.MouseDown += (sender, e) =>
{
    if (e.Button == MouseButtons.Left)
    {
        HitTestResult result = chart1.HitTest(e.X, e.Y);
        
        if (result.ChartElementType == ChartElementType.DataPoint)
        {
            // 高亮选中的数据点
            foreach (DataPoint point in series1.Points)
            {
                point.Color = Color.Blue; // 重置颜色
            }
            result.Object.Color = Color.Red; // 选中点变红
            
            // 显示详细信息
            MessageBox.Show($"选中数据点: X={result.Object.XValue}, Y={result.Object.YValues[0]}");
        }
    }
};

// 鼠标移动事件(悬停效果)
chart1.MouseMove += (sender, e) =>
{
    HitTestResult result = chart1.HitTest(e.X, e.Y);
    
    if (result.ChartElementType == ChartElementType.DataPoint)
    {
        chart1.Cursor = Cursors.Hand;
    }
    else
    {
        chart1.Cursor = Cursors.Default;
    }
};

// 选择变化事件
chart1.SelectionRangeChanged += (sender, e) =>
{
    if (e.NewSelectionStart >= 0 && e.NewSelectionEnd >= 0)
    {
        // 处理选择范围变化
        double start = e.NewSelectionStart;
        double end = e.NewSelectionEnd;
        
        // 可以在这里添加选择范围的处理逻辑
    }
};

6.4 自定义标注交互

// 添加可拖动的标注
private void AddDraggableAnnotation()
{
    RectangleAnnotation annotation = new RectangleAnnotation();
    annotation.AxisX = chart1.ChartAreas[0].AxisX;
    annotation.AxisY = chart1.ChartAreas[0].AxisY;
    annotation.X = 50;
    annotation.Y = 50;
    annotation.Width = 20;
    annotation.Height = 20;
    annotation.BackColor = Color.Yellow;
    annotation.AllowMoving = true; // 允许拖动
    annotation.AllowSelecting = true; // 允许选择
    
    chart1.Annotations.Add(annotation);
    
    // 标注拖动事件
    annotation.AnnotationPositionChanged += (s, e) =>
    {
        // 标注位置改变时的处理
        Console.WriteLine($"标注移动到: X={annotation.X}, Y={annotation.Y}");
    };
}

第 7 章:高级功能

7.1 统计分析功能

7.1.1 趋势线分析

// 添加线性趋势线
series1.TrendLines.Clear();
TrendLine trendLine = new TrendLine();
trendLine.LineColor = Color.Red;
trendLine.LineWidth = 2;
trendLine.Name = "线性趋势";
trendLine.ToolTip = "线性趋势线";
trendLine.Forecast = false;
series1.TrendLines.Add(trendLine);

// 添加多项式趋势线
TrendLine polyTrend = new TrendLine();
polyTrend.PolynomialDegree = 3; // 三次多项式
polyTrend.LineColor = Color.Blue;
polyTrend.LineWidth = 2;
polyTrend.Name = "多项式趋势";
series1.TrendLines.Add(polyTrend);

// 添加移动平均线
TrendLine movingAvg = new TrendLine();
movingAvg.Name = "移动平均";
movingAvg.ToolTip = "5期移动平均";
movingAvg.Period = 5; // 5期移动平均
movingAvg.LineColor = Color.Green;
movingAvg.LineWidth = 2;
series1.TrendLines.Add(movingAvg);

7.1.2 误差线配置

// 添加误差线
series1.ErrorBarType = ErrorBarType.StandardError;
series1.ErrorBarCenterMarkerStyle = MarkerStyle.Circle;
series1.ErrorBarSeries = "ErrorSeries"; // 关联误差数据系列
series1.ErrorBarLineWidth = 1;
series1.ErrorBarLineColor = Color.Black;

// 创建误差数据系列
Series errorSeries = new Series("ErrorSeries");
errorSeries.ChartType = SeriesChartType.ErrorBar;
errorSeries.Points.AddXY(1, 100, 90, 110); // X, Y, 下限, 上限
errorSeries.Points.AddXY(2, 150, 140, 160);
chart1.Series.Add(errorSeries);

7.2 金融图表实现

7.2.1 K线图(蜡烛图)

// 创建K线图系列
Series candlestickSeries = new Series("K线图");
candlestickSeries.ChartType = SeriesChartType.Candlestick;
candlestickSeries.YValuesPerPoint = 4; // 需要4个值:高、低、开、收

// 添加K线数据(X, 高, 低, 开, 收)
candlestickSeries.Points.AddXY("2024-01", 105, 95, 100, 102);
candlestickSeries.Points.AddXY("2024-02", 110, 98, 102, 108);
candlestickSeries.Points.AddXY("2024-03", 115, 105, 108, 112);

// 设置K线颜色(涨为红,跌为绿)
candlestickSeries.CustomProperties = "PriceDownColor=Green,PriceUpColor=Red";
candlestickSeries["ShowOpenClose"] = "Both"; // 显示开盘收盘线

chart1.Series.Add(candlestickSeries);

7.2.2 成交量图(配合K线图)

// 创建成交量系列(使用次Y轴)
Series volumeSeries = new Series("成交量");
volumeSeries.ChartType = SeriesChartType.Column;
volumeSeries.YAxisType = AxisType.Secondary; // 使用次Y轴

// 添加成交量数据
volumeSeries.Points.AddXY("2024-01", 1000000);
volumeSeries.Points.AddXY("2024-02", 1500000);
volumeSeries.Points.AddXY("2024-03", 1200000);

// 配置次Y轴
chart1.ChartAreas[0].AxisY2.Enabled = AxisEnabled.True;
chart1.ChartAreas[0].AxisY2.Title = "成交量";
chart1.ChartAreas[0].AxisY2.LabelStyle.Format = "#,##0";

chart1.Series.Add(volumeSeries);

7.2.3 技术指标计算

// 计算移动平均线(MA)
private Series CalculateMovingAverage(Series sourceSeries, int period, string name)
{
    Series maSeries = new Series(name);
    maSeries.ChartType = SeriesChartType.Line;
    maSeries.Color = Color.Blue;
    maSeries.BorderWidth = 2;
    
    for (int i = period - 1; i < sourceSeries.Points.Count; i++)
    {
        double sum = 0;
        for (int j = i - period + 1; j <= i; j++)
        {
            sum += sourceSeries.Points[j].YValues[0];
        }
        double ma = sum / period;
        maSeries.Points.AddXY(sourceSeries.Points[i].XValue, ma);
    }
    
    return maSeries;
}

// 计算相对强弱指数(RSI)
private Series CalculateRSI(Series sourceSeries, int period, string name)
{
    Series rsiSeries = new Series(name);
    rsiSeries.ChartType = SeriesChartType.Line;
    rsiSeries.Color = Color.Purple;
    
    // RSI计算逻辑
    // ... 实现RSI算法
    
    return rsiSeries;
}

7.3 自定义绘制功能

7.3.1 自定义标注

// 添加文本标注
TextAnnotation textAnnotation = new TextAnnotation();
textAnnotation.Text = "重要数据点";
textAnnotation.X = 50;
textAnnotation.Y = 75;
textAnnotation.ForeColor = Color.Red;
textAnnotation.Font = new Font("Arial", 10, FontStyle.Bold);
textAnnotation.Visible = true;
chart1.Annotations.Add(textAnnotation);

// 添加箭头标注
ArrowAnnotation arrowAnnotation = new ArrowAnnotation();
arrowAnnotation.AnchorDataPoint = series1.Points[2];
arrowAnnotation.Width = 5;
arrowAnnotation.Height = 10;
arrowAnnotation.LineColor = Color.Blue;
arrowAnnotation.ArrowStyle = ArrowStyle.Simple;
chart1.Annotations.Add(arrowAnnotation);

// 添加图像标注
ImageAnnotation imageAnnotation = new ImageAnnotation();
imageAnnotation.Image = Image.FromFile("warning.png");
imageAnnotation.X = 30;
imageAnnotation.Y = 80;
imageAnnotation.Width = 20;
imageAnnotation.Height = 20;
chart1.Annotations.Add(imageAnnotation);

7.3.2 自定义图例

// 创建自定义图例项
LegendItem legendItem = new LegendItem();
legendItem.Name = "自定义图例";
legendItem.Color = Color.Gold;
legendItem.ImageStyle = LegendImageStyle.Rectangle;
legendItem.BorderColor = Color.Black;
legendItem.BorderWidth = 1;
legendItem.MarkerStyle = MarkerStyle.Star5;
legendItem.MarkerSize = 15;

// 添加到图例
chart1.Legends[0].CustomItems.Add(legendItem);

// 自定义图例文本
chart1.Legends[0].LegendStyle = LegendStyle.Table;
chart1.Legends[0].TableStyle = LegendTableStyle.Auto;
chart1.Legends[0].Docking = Docking.Bottom;

7.3.3 自定义绘制事件

// 自定义绘制数据点
chart1.Customize += (sender, e) =>
{
    // 在绘制前进行自定义处理
};

chart1.PostPaint += (sender, e) =>
{
    // 在绘制完成后添加自定义图形
    if (e.ChartElement is ChartArea)
    {
        Graphics graphics = e.ChartGraphics.Graphics;
        
        // 绘制自定义网格线
        Pen customPen = new Pen(Color.LightGray, 1);
        customPen.DashStyle = System.Drawing.Drawing2D.DashStyle.Dot;
        
        // 在图表区域绘制自定义内容
        RectangleF chartAreaRect = e.ChartGraphics.GetAbsoluteRectangle(
            e.ChartElement.GetType().GetProperty("Position")?.GetValue(e.ChartElement) as ElementPosition
        );
        
        // 绘制对角线
        graphics.DrawLine(customPen, 
            chartAreaRect.Left, chartAreaRect.Top,
            chartAreaRect.Right, chartAreaRect.Bottom);
    }
};

第 8 章:实战案例

8.1 实时串口波形监控(结合 SerialPort)

8.1.1 完整实现代码

public partial class SerialWaveMonitor : Form
{
    private SerialPort serialPort;
    private Chart waveChart;
    private Series[] channelSeries;
    private const int MAX_POINTS = 1000;
    private double[] alarmThresholds = new double[4] { 3.0, 3.0, 3.0, 3.0 };

    public SerialWaveMonitor()
    {
        InitializeComponent();
        InitializeChart();
        InitializeSerialPort();
    }

    private void InitializeChart()
    {
        waveChart = new Chart();
        waveChart.Dock = DockStyle.Fill;
        
        // 创建图表区域
        ChartArea chartArea = new ChartArea("WaveArea");
        chartArea.AxisX.Title = "时间 (s)";
        chartArea.AxisY.Title = "电压 (V)";
        chartArea.AxisX.Minimum = 0;
        chartArea.AxisX.Maximum = 10;
        chartArea.AxisY.Minimum = -5;
        chartArea.AxisY.Maximum = 5;
        chartArea.CursorX.IsUserEnabled = true;
        chartArea.CursorX.IsUserSelectionEnabled = true;
        
        waveChart.ChartAreas.Add(chartArea);

        // 创建4个通道的数据系列
        channelSeries = new Series[4];
        Color[] channelColors = { Color.Red, Color.Blue, Color.Green, Color.Orange };
        
        for (int i = 0; i < 4; i++)
        {
            channelSeries[i] = new Series($"通道{i+1}");
            channelSeries[i].ChartType = SeriesChartType.FastLine;
            channelSeries[i].Color = channelColors[i];
            channelSeries[i].BorderWidth = 2;
            channelSeries[i].LegendText = $"通道{i+1}";
            waveChart.Series.Add(channelSeries[i]);
        }

        // 添加图例
        Legend legend = new Legend();
        legend.Docking = Docking.Top;
        waveChart.Legends.Add(legend);

        this.Controls.Add(waveChart);
    }

    private void InitializeSerialPort()
    {
        serialPort = new SerialPort("COM3", 9600, Parity.None, 8, StopBits.One);
        serialPort.DataReceived += SerialPort_DataReceived;
    }

    private void SerialPort_DataReceived(object sender, SerialDataReceivedEventArgs e)
    {
        try
        {
            string data = serialPort.ReadLine();
            ProcessSerialData(data);
        }
        catch (Exception ex)
        {
            // 错误处理
        }
    }

    private void ProcessSerialData(string data)
    {
        // 解析串口数据格式:CH1:1.23,CH2:2.34,CH3:0.56,CH4:3.21
        string[] channels = data.Split(',');
        double[] values = new double[4];
        
        for (int i = 0; i < Math.Min(4, channels.Length); i++)
        {
            if (channels[i].Contains(":"))
            {
                string[] parts = channels[i].Split(':');
                if (parts.Length == 2 && double.TryParse(parts[1], out double value))
                {
                    values[i] = value;
                    
                    // 检查报警阈值
                    if (Math.Abs(value) > alarmThresholds[i])
                    {
                        TriggerAlarm(i, value);
                    }
                }
            }
        }

        // 更新图表(确保在UI线程执行)
        if (waveChart.InvokeRequired)
        {
            waveChart.Invoke(new Action<double[]>(UpdateWaveChart), values);
        }
        else
        {
            UpdateWaveChart(values);
        }
    }

    private void UpdateWaveChart(double[] values)
    {
        double currentTime = DateTime.Now.TimeOfDay.TotalSeconds;
        
        for (int i = 0; i < values.Length; i++)
        {
            // 限制数据点数量
            if (channelSeries[i].Points.Count > MAX_POINTS)
            {
                channelSeries[i].Points.RemoveAt(0);
            }
            
            // 添加新数据点
            channelSeries[i].Points.AddXY(currentTime, values[i]);
            
            // 自动调整X轴范围(显示最近10秒数据)
            if (currentTime > waveChart.ChartAreas[0].AxisX.Maximum)
            {
                waveChart.ChartAreas[0].AxisX.Minimum = currentTime - 10;
                waveChart.ChartAreas[0].AxisX.Maximum = currentTime;
            }
        }
        
        waveChart.Invalidate(); // 强制重绘
    }

    private void TriggerAlarm(int channel, double value)
    {
        // 报警处理逻辑
        Console.WriteLine($"通道{channel+1}报警!当前值: {value}, 阈值: {alarmThresholds[channel]}");
        
        // 可以添加声音报警、闪烁提示等
    }

    public void StartMonitoring()
    {
        if (!serialPort.IsOpen)
        {
            serialPort.Open();
        }
    }

    public void StopMonitoring()
    {
        if (serialPort.IsOpen)
        {
            serialPort.Close();
        }
    }
}

8.2 销售数据报表(柱状图 + 折线图组合)

8.2.1 完整实现代码

public class SalesReportChart
{
    private Chart salesChart;
    
    public SalesReportChart()
    {
        InitializeChart();
    }

    private void InitializeChart()
    {
        salesChart = new Chart();
        salesChart.Size = new Size(800, 500);
        
        // 配置图表区域
        ChartArea chartArea = new ChartArea("SalesArea");
        chartArea.AxisX.Title = "月份";
        chartArea.AxisY.Title = "销售额 (万元)";
        chartArea.AxisY2.Title = "增长率 (%)";
        chartArea.AxisY2.Enabled = AxisEnabled.True;
        
        // 配置次Y轴
        chartArea.AxisY2.MajorGrid.Enabled = false;
        chartArea.AxisY2.LabelStyle.Format = "P0";
        chartArea.AxisY2.Minimum = -0.5;
        chartArea.AxisY2.Maximum = 1.5;
        
        salesChart.ChartAreas.Add(chartArea);

        // 创建销售数据系列(柱状图)
        Series salesSeries = new Series("销售额");
        salesSeries.ChartType = SeriesChartType.Column;
        salesSeries.Color = Color.SteelBlue;
        salesSeries.YAxisType = AxisType.Primary;
        salesSeries.IsValueShownAsLabel = true;
        salesSeries.LabelFormat = "{0:F0}";
        salesSeries["DrawingStyle"] = "Cylinder"; // 圆柱效果
        
        // 创建增长率系列(折线图,使用次Y轴)
        Series growthSeries = new Series("增长率");
        growthSeries.ChartType = SeriesChartType.Line;
        growthSeries.Color = Color.Red;
        growthSeries.YAxisType = AxisType.Secondary;
        growthSeries.BorderWidth = 3;
        growthSeries.MarkerStyle = MarkerStyle.Circle;
        growthSeries.MarkerSize = 8;
        growthSeries.MarkerColor = Color.White;
        growthSeries.MarkerBorderColor = Color.Red;
        growthSeries.MarkerBorderWidth = 2;
        growthSeries.IsValueShownAsLabel = true;
        growthSeries.LabelFormat = "{0:P0}";

        salesChart.Series.Add(salesSeries);
        salesChart.Series.Add(growthSeries);

        // 添加图例
        Legend legend = new Legend();
        legend.Docking = Docking.Bottom;
        salesChart.Legends.Add(legend);

        // 添加标题
        Title title = new Title("2024年销售数据报表");
        title.Font = new Font("微软雅黑", 14, FontStyle.Bold);
        salesChart.Titles.Add(title);
    }

    public void LoadSalesData(List<SalesData> data)
    {
        salesChart.Series["销售额"].Points.Clear();
        salesChart.Series["增长率"].Points.Clear();

        foreach (var item in data)
        {
            salesChart.Series["销售额"].Points.AddXY(item.Month, item.Amount / 10000); // 转换为万元
            salesChart.Series["增长率"].Points.AddXY(item.Month, item.GrowthRate);
        }
    }

    // 数据模型
    public class SalesData
    {
        public string Month { get; set; }
        public decimal Amount { get; set; }
        public double GrowthRate { get; set; }
    }

    public Chart GetChart()
    {
        return salesChart;
    }
}

// 使用示例
public void CreateSalesReport()
{
    SalesReportChart report = new SalesReportChart();
    
    List<SalesReportChart.SalesData> data = new List<SalesReportChart.SalesData>
    {
        new SalesReportChart.SalesData { Month = "1月", Amount = 1200000, GrowthRate = 0.0 },
        new SalesReportChart.SalesData { Month = "2月", Amount = 1500000, GrowthRate = 0.25 },
        new SalesReportChart.SalesData { Month = "3月", Amount = 1800000, GrowthRate = 0.20 },
        new SalesReportChart.SalesData { Month = "4月", Amount = 2200000, GrowthRate = 0.22 },
        new SalesReportChart.SalesData { Month = "5月", Amount = 1900000, GrowthRate = -0.14 },
        new SalesReportChart.SalesData { Month = "6月", Amount = 2500000, GrowthRate = 0.32 }
    };
    
    report.LoadSalesData(data);
    
    // 将图表添加到窗体
    this.Controls.Add(report.GetChart());
}

8.3 设备状态监控仪表盘(Gauge 图表)

8.3.1 完整实现代码

public class GaugeDashboard
{
    private Chart gaugeChart;
    private Series[] gaugeSeries;
    private const int GAUGE_COUNT = 4;

    public GaugeDashboard()
    {
        InitializeGaugeChart();
    }

    private void InitializeGaugeChart()
    {
        gaugeChart = new Chart();
        gaugeChart.Size = new Size(600, 400);
        
        // 创建2x2的仪表盘布局
        for (int i = 0; i < GAUGE_COUNT; i++)
        {
            ChartArea gaugeArea = new ChartArea($"GaugeArea{i}");
            gaugeArea.Position = new ElementPosition(
                (i % 2) * 50,      // X位置
                (i / 2) * 50,      // Y位置
                45,                // 宽度
                45                 // 高度
            );
            
            // 配置为雷达图模拟仪表盘
            gaugeArea.Area3DStyle.Enable3D = true;
            gaugeArea.BackColor = Color.LightGray;
            
            gaugeChart.ChartAreas.Add(gaugeArea);
        }

        // 创建仪表盘系列
        gaugeSeries = new Series[GAUGE_COUNT];
        string[] gaugeNames = { "CPU使用率", "内存使用", "磁盘IO", "网络流量" };
        Color[] gaugeColors = { Color.Red, Color.Blue, Color.Green, Color.Orange };
        
        for (int i = 0; i < GAUGE_COUNT; i++)
        {
            gaugeSeries[i] = new Series(gaugeNames[i]);
            gaugeSeries[i].ChartType = SeriesChartType.Radar;
            gaugeSeries[i].ChartArea = $"GaugeArea{i}";
            gaugeSeries[i].Color = gaugeColors[i];
            gaugeSeries[i].BorderWidth = 3;
            
            // 添加仪表盘刻度数据点
            gaugeSeries[i].Points.AddY(0);   // 最小值
            gaugeSeries[i].Points.AddY(25);  // 25%
            gaugeSeries[i].Points.AddY(50);  // 50%
            gaugeSeries[i].Points.AddY(75);  // 75%
            gaugeSeries[i].Points.AddY(100); // 最大值
            
            gaugeChart.Series.Add(gaugeSeries[i]);
        }

        // 添加指针系列(使用第二个数据点作为指针)
        for (int i = 0; i < GAUGE_COUNT; i++)
        {
            Series pointerSeries = new Series($"{gaugeNames[i]}指针");
            pointerSeries.ChartType = SeriesChartType.Line;
            pointerSeries.ChartArea = $"GaugeArea{i}";
            pointerSeries.Color = Color.Black;
            pointerSeries.BorderWidth = 2;
            pointerSeries.Points.AddY(0); // 指针起始点
            pointerSeries.Points.AddY(50); // 指针当前位置(初始50%)
            
            gaugeChart.Series.Add(pointerSeries);
        }
    }

    public void UpdateGaugeValue(int gaugeIndex, double value)
    {
        if (gaugeIndex >= 0 && gaugeIndex < GAUGE_COUNT)
        {
            // 更新指针位置
            Series pointerSeries = gaugeChart.Series[$"{gaugeSeries[gaugeIndex].Name}指针"];
            pointerSeries.Points[1].YValues[0] = Math.Max(0, Math.Min(100, value));
            
            // 根据数值改变颜色
            if (value > 80)
                gaugeSeries[gaugeIndex].Color = Color.Red;
            else if (value > 60)
                gaugeSeries[gaugeIndex].Color = Color.Orange;
            else
                gaugeSeries[gaugeIndex].Color = Color.Green;
                
            gaugeChart.Invalidate();
        }
    }

    public Chart GetChart()
    {
        return gaugeChart;
    }
}

// 使用示例:模拟实时监控
public class DeviceMonitor : Form
{
    private GaugeDashboard dashboard;
    private Timer updateTimer;

    public DeviceMonitor()
    {
        dashboard = new GaugeDashboard();
        this.Controls.Add(dashboard.GetChart());
        
        // 启动定时器模拟数据更新
        updateTimer = new Timer();
        updateTimer.Interval = 1000; // 1秒更新一次
        updateTimer.Tick += UpdateTimer_Tick;
        updateTimer.Start();
    }

    private void UpdateTimer_Tick(object sender, EventArgs e)
    {
        Random rand = new Random();
        
        // 模拟4个仪表盘的实时数据
        for (int i = 0; i < 4; i++)
        {
            double value = rand.NextDouble() * 100;
            dashboard.UpdateGaugeValue(i, value);
        }
    }
}

第 9 章:常见问题与解决方案

9.1 性能优化问题

问题:图表卡顿

  • 解决方案1: 减少数据点数量,使用数据采样
  • 解决方案2: 优化刷新频率,使用双缓冲
  • 解决方案3: 启用图表虚拟化

代码示例:

chart1.Series[0].Points.DataBind(data.Take(1000)); // 限制数据点数量
chart1.ChartAreas[0].AxisX.Interval = 10; // 设置坐标轴间隔

9.2 线程安全问题

问题:跨线程更新图表

  • 解决方案: 使用 Control.Invoke 方法

代码示例:

private void UpdateChartFromThread()
{
    if (chart1.InvokeRequired)
    {
        chart1.Invoke(new Action(UpdateChartFromThread));
        return;
    }
    // 更新图表代码
}

9.3 样式配置问题

问题:样式不生效

  • 检查点1: ChartArea 与 Series 的关联关系
  • 检查点2: 坐标轴范围设置
  • 检查点3: 数据点索引正确性

9.4 数据绑定问题

问题:数据绑定失败

  • 解决方案1: 检查数据源类型
  • 解决方案2: 验证数据格式
  • 解决方案3: 使用 DataBind 方法

第 10 章:最佳实践与性能优化

10.1 内存管理

  • 及时清理不需要的数据点
  • 使用数据绑定而非手动添加
  • 避免频繁的图表重绘

10.2 用户体验

  • 合理的图表缩放比例
  • 清晰的图例说明
  • 响应式布局设计

10.3 代码组织

  • 模块化的图表配置
  • 可重用的图表模板
  • 统一的样式管理

附录:常用属性速查表

图表类型 (ChartType)

  • Column, Line, Pie, Bar, Area, Point, Bubble
  • Spline, StepLine, Range, RangeColumn
  • Stock, Candlestick, Radar, Polar

坐标轴属性

  • Interval: 刻度间隔
  • Minimum/Maximum: 范围设置
  • LabelStyle.Format: 标签格式
  • MajorGrid: 主网格线

数据系列属性

  • Color: 系列颜色
  • BorderWidth: 边框宽度
  • MarkerStyle: 标记点样式
  • ChartType: 图表类型

本指南涵盖了 System.Windows.Forms.DataVisualization.Charting 控件的所有核心功能和实践技巧,可作为开发参考手册使用。

posted @ 2025-10-18 13:25  【唐】三三  阅读(63)  评论(0)    收藏  举报