首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> .NET 網頁程式設計 >> C# >> 關於C#

C# GDI+ 繪製多曲線圖表:自動縮放、動態適應、完整元件化實作

     編輯:關於C#

在 WinForms/WPF 應用中繪製專業級曲線圖表,Chart 控制項雖強大但部署繁瑣。本文提供純 GDI+ 繪製的 CurveChart 元件:自動計算座標範圍與刻度、支援多曲線同屏顯示、動態適應容器大小、高效能雙緩衝繪圖、完整樣式客製化。零第三方依賴,程式碼開放可直接嵌入專案。

核心功能特性

  • 自動縮放:根據資料最小/最大值自動計算 Y 軸範圍與刻度間距
  • 多曲線支援:單次繪製多組資料,自動分配顏色,圖例顯示
  • 動態適應:容器 Resize 時自動重新計算佈局與刻度
  • 雙緩衝繪圖:消除閃爍,支援高頻更新 (即時波形圖)
  • 完整樣式控制:標題、軸標籤、刻度、網格線、曲線粗細、張力係數全可調

資料結構設計


public class CurveSeries
{
    public string Name { get; set; }
    public float[] Values { get; set; }
    public Color Color { get; set; }
    public int LineWidth { get; set; } = 2;
    public float Tension { get; set; } = 0.5f; // Catmull-Rom spline 張力
}

public class CurveChartOptions
{
    public string Title { get; set; } = "曲線圖";
    public string XAxisLabel { get; set; } = "X 軸";
    public string YAxisLabel { get; set; } = "Y 軸";
    public string[] XLabels { get; set; } // X 軸標籤 (如月份)
    public float YSliceValue { get; set; } = 20; // Y 軸刻度數值間距
    public Color BackColor { get; set; } = Color.White;
    public Color GridColor { get; set; } = Color.LightGray;
    public Color AxisColor { get; set; } = Color.Black;
    public Color TextColor { get; set; } = Color.Black;
    public int FontSize { get; set; } = 9;
    public float XRotateAngle { get; set; } = 30; // X 標籤旋轉角度
}

核心繪圖元件


using System;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Windows.Forms;

public class CurveChart : Control
{
    private CurveChartOptions _options = new CurveChartOptions();
    private CurveSeries[] _series = Array.Empty();
    private Bitmap _backBuffer;
    private readonly object _drawLock = new object();

    public CurveChartOptions Options { get => _options; set { _options = value; Invalidate(); } }
    public CurveSeries[] Series { get => _series; set { _series = value; Fit(); Invalidate(); } }

    public CurveChart()
    {
        SetStyle(ControlStyles.AllPaintingInWmPaint | ControlStyles.UserPaint | 
                 ControlStyles.OptimizedDoubleBuffer | ControlStyles.ResizeRedraw, true);
        DoubleBuffered = true;
    }

    // ===== 自動計算佈局參數 =====
    public void Fit()
    {
        if (_series.Length == 0 || _options.XLabels?.Length == 0) return;

        int padding = _options.FontSize + 5;
        float space = Math.Min(Width, Height) / 6f;
        float xSpace = space, ySpace = space;

        // X 軸刻度寬度
        float xSlice = (Width - 2 * xSpace) / (_options.XLabels.Length - 1);

        // Y 軸範圍計算
        float minVal = float.MaxValue, maxVal = float.MinValue;
        foreach (var s in _series)
            foreach (var v in s.Values)
            {
                if (v < minVal) minVal = v;
                if (v > maxVal) maxVal = v;
            }

        // 向下取整到刻度倍數
        float ySliceBegin = (float)Math.Floor(minVal / _options.YSliceValue) * _options.YSliceValue;
        int ySliceCount = (int)Math.Ceiling((maxVal - ySliceBegin) / _options.YSliceValue);
        float ySlice = (Height - 2 * ySpace) / ySliceCount;

        // 快取佈局參數供 Paint 使用
        _layout = new LayoutParams
        {
            XSpace = xSpace, YSpace = ySpace,
            XSlice = xSlice, YSlice = ySlice,
            YSliceBegin = ySliceBegin,
            YSliceCount = ySliceCount,
            FontSpace = padding
        };
    }

    private LayoutParams _layout;

    private class LayoutParams
    {
        public float XSpace, YSpace, XSlice, YSlice, YSliceBegin;
        public int YSliceCount, FontSpace;
    }

    // ===== 核心繪製邏輯 =====
    protected override void OnPaint(PaintEventArgs e)
    {
        base.OnPaint(e);
        if (_series.Length == 0 || _options.XLabels?.Length == 0) return;

        lock (_drawLock)
        {
            // 雙緩衝:先畫到 Bitmap 再一次性輸出
            if (_backBuffer == null || _backBuffer.Size != ClientSize)
                _backBuffer = new Bitmap(ClientSize.Width, ClientSize.Height);

            using (var g = Graphics.FromImage(_backBuffer))
            {
                g.SmoothingMode = SmoothingMode.AntiAlias;
                g.TextRenderingHint = System.Drawing.Text.TextRenderingHint.ClearTypeGridFit;
                g.Clear(_options.BackColor);

                DrawBorderAndBackground(g);
                DrawAxes(g);
                DrawGridLines(g);
                DrawXAxisLabels(g);
                DrawYAxisLabels(g);
                DrawTitle(g);
                DrawCurves(g);
            }

            e.Graphics.DrawImageUnscaled(_backBuffer, 0, 0);
        }
    }

    private void DrawBorderAndBackground(Graphics g)
    {
        g.DrawRectangle(new Pen(_options.AxisColor), 0, 0, Width - 1, Height - 1);
    }

    private void DrawAxes(Graphics g)
    {
        var p = new Pen(_options.AxisColor);
        float x1 = _layout.XSpace, y1 = Height - _layout.YSpace;
        float x2 = Width - _layout.XSpace + _layout.XSlice / 2, y2 = y1;
        g.DrawLine(p, x1, y1, x2, y2); // X 軸

        x1 = _layout.XSpace; y1 = Height - _layout.YSpace;
        x2 = _layout.XSpace; y2 = _layout.YSpace - _layout.YSlice / 2;
        g.DrawLine(p, x1, y1, x2, y2); // Y 軸
    }

    private void DrawGridLines(Graphics g)
    {
        var dashPen = new Pen(_options.GridColor) { DashStyle = DashStyle.Dash };
        
        // 垂直網格線 (對應 X 刻度)
        for (int i = 0; i < _options.XLabels.Length; i++)
        {
            float x = _layout.XSpace + i * _layout.XSlice;
            g.DrawLine(dashPen, x, _layout.YSpace, x, Height - _layout.YSpace);
        }

        // 水平網格線 (對應 Y 刻度)
        for (int i = 0; i <= _layout.YSliceCount; i++)
        {
            float y = Height - _layout.YSpace - i * _layout.YSlice;
            g.DrawLine(dashPen, _layout.XSpace, y, Width - _layout.XSpace, y);
        }
    }

    private void DrawXAxisLabels(Graphics g)
    {
        var font = new Font("Microsoft JhengHei", _options.FontSize);
        var brush = new SolidBrush(_options.TextColor);
        float y = Height - _layout.YSpace + 2;

        for (int i = 0; i < _options.XLabels.Length; i++)
        {
            float x = _layout.XSpace + i * _layout.XSlice;
            var size = g.MeasureString(_options.XLabels[i], font);
            
            g.TranslateTransform(x, y);
            g.RotateTransform(_options.XRotateAngle);
            g.DrawString(_options.XLabels[i], font, brush, -size.Width / 2, 0);
            g.ResetTransform();
        }
    }

    private void DrawYAxisLabels(Graphics g)
    {
        var font = new Font("Microsoft JhengHei", _options.FontSize);
        var brush = new SolidBrush(_options.TextColor);
        float x = _layout.XSpace - 5;

        for (int i = 0; i <= _layout.YSliceCount; i++)
        {
            float val = _layout.YSliceBegin + i * _options.YSliceValue;
            float y = Height - _layout.YSpace - i * _layout.YSlice;
            string text = val.ToString("0.##");
            var size = g.MeasureString(text, font);
            
            g.TranslateTransform(x - size.Width, y - size.Height / 2);
            g.RotateTransform(_options.XRotateAngle); // 同樣角度或 0
            g.DrawString(text, font, brush, 0, 0);
            g.ResetTransform();
        }
    }

    private void DrawTitle(Graphics g)
    {
        var font = new Font("Microsoft JhengHei", _options.FontSize + 2, FontStyle.Bold);
        var brush = new SolidBrush(_options.TextColor);
        var size = g.MeasureString(_options.Title, font);
        float x = (Width - size.Width) / 2;
        float y = _layout.YSpace / 2 - size.Height / 2;
        g.DrawString(_options.Title, font, brush, x, y);
    }

    private void DrawCurves(Graphics g)
    {
        if (_series.Length == 0) return;
        int pointCount = _options.XLabels.Length;

        for (int s = 0; s < _series.Length; s++)
        {
            var series = _series[s];
            if (series.Values?.Length != pointCount) continue;

            var points = new PointF[pointCount];
            for (int i = 0; i < pointCount; i++)
            {
                float x = _layout.XSpace + i * _layout.XSlice;
                // Y 軸座標轉換:畫面座標系 Y 向下,數值座標系 Y 向上
                float normalizedY = (series.Values[i] - _layout.YSliceBegin) / _options.YSliceValue;
                float y = Height - _layout.YSpace - normalizedY * _layout.YSlice;
                points[i] = new PointF(x, y);
            }

            using (var pen = new Pen(series.Color, series.LineWidth))
            {
                g.DrawCurve(pen, points, series.Tension);
            }

            // 繪製資料點標記 (可選)
            foreach (var pt in points)
                g.FillEllipse(Brushes.White, pt.X - 3, pt.Y - 3, 6, 6);
        }
    }

    protected override void OnResize(EventArgs e)
    {
        base.OnResize(e);
        Fit(); // 重新計算佈局
        Invalidate();
    }
}

使用範例


public partial class MainForm : Form
{
    private CurveChart _chart;

    public MainForm()
    {
        InitializeComponent();
        InitializeChart();
        LoadSampleData();
    }

    private void InitializeChart()
    {
        _chart = new CurveChart
        {
            Dock = DockStyle.Fill,
            Options = new CurveChartOptions
            {
                Title = "月度銷售趨勢比較",
                XAxisLabel = "月份",
                YAxisLabel = "銷售額 (萬元)",
                XLabels = new[] { "1月", "2月", "3月", "4月", "5月", "6月", 
                                 "7月", "8月", "9月", "10月", "11月", "12月" },
                YSliceValue = 50,
                FontSize = 10
            }
        };
        this.Controls.Add(_chart);
    }

    private void LoadSampleData()
    {
        _chart.Series = new[]
        {
            new CurveSeries 
            { 
                Name = "2023 年", 
                Values = new float[] { 120, 135, 150, 165, 180, 195, 210, 200, 190, 185, 175, 160 },
                Color = Color.Blue 
            },
            new CurveSeries 
            { 
                Name = "2024 年", 
                Values = new float[] { 130, 145, 160, 175, 190, 210, 225, 220, 210, 200, 190, 180 },
                Color = Color.Red 
            },
            new CurveSeries 
            { 
                Name = "目標值", 
                Values = new float[] { 150, 150, 150, 150, 150, 150, 150, 150, 150, 150, 150, 150 },
                Color = Color.Green,
                LineWidth = 1,
                Tension = 0f // 直線
            }
        };
        _chart.Fit();
    }
}

效能優化要點

技術實作效益
雙緩衝ControlStyles.OptimizedDoubleBuffer + 手動 Bitmap 後台繪製消除閃爍,支援 60fps 以上更新
佈局快取Fit() 計算佈局參數存入 LayoutParams,Paint 直接使用避免每幀重複計算座標轉換
筆刷/筆重用using 區塊內建立,或靜態快取常用樣式減少 GDI 物件建立/銷毀開銷
區域失效僅資料更新時 Invalidate(),非完整重繪局部更新效能提升

擴充功能建議

  1. 工具提示:MouseMove 事件計算最近資料點,顯示 ToolTip
  2. 縮放平移:滑鼠滾輪縮放 Y 軸範圍,拖曳平移 X 軸索引
  3. 圖例:右側/底部繪製系列名稱、顏色、可見度切換
  4. 資料匯出:Bitmap.Save 輸出 PNG、複製到剪貼簿
  5. WPF 版本:改用 DrawingContext + WriteableBitmap 實現同樣 API

總結

CurveChart 元件以 純 GDI+、零依賴、全原始碼開放 實現專業級多曲線圖表:自動縮放適應資料範圍、動態響應容器大小變化、雙緩衝高效能渲染、豐富樣式客製化。程式碼結構清晰,易於擴充工具提示、縮放平移、圖例等進階互動功能,是 WinForms 應用內建圖表的理想選擇。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved