使用SkiaSharp打造专业级12导联心电图查看器:性能与美观兼具的可视化实践

在这里插入图片描述

前言

欢迎关注dotnet研习社,今天我们研究的Google Skia图形库的.NET绑定SkiaSharp图形库

在医疗软件开发领域,心电图(ECG)数据的可视化是一个既有挑战性又极其重要的任务。作为开发者,我们需要创建既专业又直观的界面来展示复杂的生物医学数据。本文将分享我使用SkiaSharp在.NET平台上构建12导联心电图查看器的完整过程,希望能为医疗软件开发者提供一些有价值的参考。

项目背景

心电图是临床诊断中最重要的工具之一,标准的12导联心电图能够从不同角度观察心脏的电活动。传统的心电图显示软件往往依赖于专有的图形库或复杂的绘图框架,这不仅增加了开发成本,也限制了定制化的可能性。

SkiaSharp作为Google Skia图形库的.NET绑定,为我们提供了一个强大而灵活的2D图形渲染解决方案。它不仅性能优异,还支持硬件加速,非常适合用于构建高质量的医学数据可视化应用。

技术选型与架构设计

技术栈

  • .NET 8.0: 最新的.NET框架,提供优异的性能和丰富的API
  • Windows Forms: 成熟稳定的桌面应用框架
  • SkiaSharp: 高性能2D图形渲染引擎
  • SkiaSharp.Views.WindowsForms: Windows Forms集成包

在这里插入图片描述

架构设计

项目采用分层架构,清晰地分离了数据模型、渲染逻辑和用户界面:
在这里插入图片描述

ECG12LeadViewer/
├── ECGData.cs              # 数据模型层
├── ECGRenderer.cs          # 渲染引擎层
├── ECGViewerControl.cs     # 控件封装层
├── MainForm.cs            # 用户界面层
└── Program.cs             # 程序入口

核心技术实现

1.新建项目引用SkiaSharp相关的库

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>WinExe</OutputType>
    <TargetFramework>net8.0-windows10.0.19041.0</TargetFramework>
    <UseWindowsForms>true</UseWindowsForms>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
  </PropertyGroup>

  <ItemGroup>
    
    <PackageReference Include="SkiaSharp" Version="3.119.0" />
    <PackageReference Include="SkiaSharp.Views.Desktop.Common" Version="3.119.0" />
    <PackageReference Include="SkiaSharp.Views.WindowsForms" Version="3.119.0" />
  </ItemGroup>

</Project> 

2. 数据模型设计

首先,我们需要定义一个强类型的数据模型来表示12导联心电图数据:

using System;

namespace ECG12LeadViewer
{
    /// <summary>
    /// 12导联心电图数据
    /// </summary>
    public class ECGData
	{
	    public float[] LeadI { get; set; } = Array.Empty<float>();
	    public float[] LeadII { get; set; } = Array.Empty<float>();
	    public float[] LeadIII { get; set; } = Array.Empty<float>();
	    public float[] aVR { get; set; } = Array.Empty<float>();
	    public float[] aVL { get; set; } = Array.Empty<float>();
	    public float[] aVF { get; set; } = Array.Empty<float>();
	    public float[] V1 { get; set; } = Array.Empty<float>();
	    public float[] V2 { get; set; } = Array.Empty<float>();
	    public float[] V3 { get; set; } = Array.Empty<float>();
	    public float[] V4 { get; set; } = Array.Empty<float>();
	    public float[] V5 { get; set; } = Array.Empty<float>();
	    public float[] V6 { get; set; } = Array.Empty<float>();
	
	    public int SampleRate { get; set; } = 360; // Hz
	    public float Duration { get; set; } = 10.0f; // seconds
	
		/// <summary>
		/// 获取所有导联的数据
		/// </summary>
		public Dictionary<string, float[]> GetAllLeads()
		{
		    return new Dictionary<string, float[]>
		    {
		        { "I", LeadI },
		        { "II", LeadII },
		        { "III", LeadIII },
		        { "aVR", aVR },
		        { "aVL", aVL },
		        { "aVF", aVF },
		        { "V1", V1 },
		        { "V2", V2 },
		        { "V3", V3 },
		        { "V4", V4 },
		        { "V5", V5 },
		        { "V6", V6 }
		    };
		}
	}
}

这个设计考虑了医学标准,支持360Hz的采样率,这是心电图设备的常见采样频率。以及通过字典数据获取所有导联的数据。

3. 模拟心电图数据生成

为了测试和演示,我们实现了一个逼真的心电图数据生成器:

using System;

namespace ECG12LeadViewer
{
    /// <summary>
    /// 12导联心电图数据
    /// </summary>
    public class ECGData
    {
    	//...数据模型设计
    	
        /// <summary>
        /// 生成模拟的12导联心电图数据
        /// </summary>
        public static ECGData GenerateSimulatedData()
        {
            var ecg = new ECGData();
            int sampleCount = (int)(ecg.SampleRate * ecg.Duration);
            
            // 心率约75 BPM
            double heartRate = 75.0;
            double rPeakInterval = 60.0 / heartRate; // seconds between R peaks
            int samplesPerBeat = (int)(ecg.SampleRate * rPeakInterval);

            var random = new Random(42); // 固定种子以获得一致的结果

            // 为每个导联生成数据
            ecg.LeadI = GenerateLeadData(sampleCount, samplesPerBeat, 1.0f, random);
            ecg.LeadII = GenerateLeadData(sampleCount, samplesPerBeat, 1.2f, random);
            ecg.LeadIII = GenerateLeadData(sampleCount, samplesPerBeat, 0.8f, random);
            ecg.aVR = GenerateLeadData(sampleCount, samplesPerBeat, -0.5f, random);
            ecg.aVL = GenerateLeadData(sampleCount, samplesPerBeat, 0.6f, random);
            ecg.aVF = GenerateLeadData(sampleCount, samplesPerBeat, 1.0f, random);
            ecg.V1 = GenerateLeadData(sampleCount, samplesPerBeat, 0.4f, random);
            ecg.V2 = GenerateLeadData(sampleCount, samplesPerBeat, 1.5f, random);
            ecg.V3 = GenerateLeadData(sampleCount, samplesPerBeat, 1.8f, random);
            ecg.V4 = GenerateLeadData(sampleCount, samplesPerBeat, 1.6f, random);
            ecg.V5 = GenerateLeadData(sampleCount, samplesPerBeat, 1.2f, random);
            ecg.V6 = GenerateLeadData(sampleCount, samplesPerBeat, 0.8f, random);

            return ecg;
        }

        /// <summary>
        /// 为单个导联生成模拟数据
        /// </summary>
        private static float[] GenerateLeadData(int sampleCount, int samplesPerBeat, float amplitude, Random random)
        {
            var data = new float[sampleCount];
            
            for (int i = 0; i < sampleCount; i++)
            {
                double time = (double)i / 360.0; // 时间(秒)
                int beatPosition = i % samplesPerBeat;
                double beatPhase = (double)beatPosition / samplesPerBeat;

                // 基线噪声
                float noise = (float)(random.NextDouble() - 0.5) * 0.05f;
                
                // 生成ECG波形
                float ecgValue = GenerateECGWaveform(beatPhase) * amplitude + noise;
                
                data[i] = ecgValue;
            }

            return data;
        }

        /// <summary>
        /// 生成单个心跳的ECG波形
        /// </summary>
        private static float GenerateECGWaveform(double phase)
        {
            // P波 (0.0 - 0.2)
            if (phase < 0.2)
            {
                double pPhase = phase / 0.2;
                return (float)(0.2 * Math.Sin(Math.PI * pPhase));
            }
            // PR间期 (0.2 - 0.35)
            else if (phase < 0.35)
            {
                return 0.0f;
            }
            // QRS复合波 (0.35 - 0.45)
            else if (phase < 0.45)
            {
                double qrsPhase = (phase - 0.35) / 0.1;
                if (qrsPhase < 0.3) // Q波
                    return (float)(-0.3 * Math.Sin(Math.PI * qrsPhase / 0.3));
                else if (qrsPhase < 0.7) // R波
                    return (float)(2.0 * Math.Sin(Math.PI * (qrsPhase - 0.3) / 0.4));
                else // S波
                    return (float)(-0.5 * Math.Sin(Math.PI * (qrsPhase - 0.7) / 0.3));
            }
            // ST段 (0.45 - 0.6)
            else if (phase < 0.6)
            {
                return 0.0f;
            }
            // T波 (0.6 - 0.9)
            else if (phase < 0.9)
            {
                double tPhase = (phase - 0.6) / 0.3;
                return (float)(0.4 * Math.Sin(Math.PI * tPhase));
            }
            // 基线 (0.9 - 1.0)
            else
            {
                return 0.0f;
            }
        }
    }
} 

这个算法基于真实的心电图生理学原理,生成包含P波、QRS复合波和T波的完整心跳周期。

4. SkiaSharp渲染引擎

渲染引擎是整个项目的核心,负责将数据转换为可视化的心电图。以下是关键的渲染逻辑:

布局类设计
/// <summary>
/// ECG布局参数
/// </summary>
public class ECGLayout
{
    public int Margin { get; set; }
    public int LeadWidth { get; set; }
    public int LeadHeight { get; set; }
    public int Rows { get; set; }
    public int Cols { get; set; }
    public int GridSpacing { get; set; }
}
ECG渲染器设计
public class ECGRenderer
{
    private readonly SKPaint _gridPaint;
    private readonly SKPaint _waveformPaint;
    private readonly SKPaint _textPaint;
    private readonly SKFont _skFont;
    
    /// <summary>
	/// 渲染12导联心电图
	/// </summary>
	public void Render(SKSurface surface, ECGData ecgData, int width, int height)
	{
	    var canvas = surface.Canvas;
	    canvas.Clear(SKColors.White);	
	    // 计算布局
	    var layout = CalculateLayout(width, height);
	    // 绘制背景
	    canvas.DrawRect(0, 0, width, height, _backgroundPaint);	
	    // 绘制网格
	    DrawGrid(canvas, layout);	
	    // 绘制导联
	    DrawLeads(canvas, ecgData, layout);	
	    // 绘制标签
	    DrawLabels(canvas, layout);
	}
}
计算布局设计
/// <summary>
/// 计算布局参数
/// </summary>
private ECGLayout CalculateLayout(int width, int height)
{
    const int margin = 40;
    const int rows = 4;
    const int cols = 3;
    
    int availableWidth = width - 2 * margin;
    int availableHeight = height - 2 * margin;
    
    int leadWidth = availableWidth / cols;
    int leadHeight = availableHeight / rows;

    return new ECGLayout
    {
        Margin = margin,
        LeadWidth = leadWidth,
        LeadHeight = leadHeight,
        Rows = rows,
        Cols = cols,
        GridSpacing = 20 // 网格间距,代表200ms或0.2mV
    };
}
医学标准网格绘制

心电图的网格有特定的医学标准:细网格代表0.04秒和0.1mV,粗网格代表0.2秒和0.5mV。我们用SkiaSharp精确实现了这些标准:

/// <summary>
/// 绘制网格
/// </summary>
private void DrawGrid(SKCanvas canvas, ECGLayout layout)
{
    // 绘制细网格线
    var fineGridPaint = new SKPaint
    {
        Color = SKColor.Parse("#FFE0E0"),
        StrokeWidth = 0.5f,
        Style = SKPaintStyle.Stroke
    };

    // 绘制粗网格线
    var coarseGridPaint = new SKPaint
    {
        Color = SKColor.Parse("#FFCCCC"),
        StrokeWidth = 1.0f,
        Style = SKPaintStyle.Stroke
    };

    for (int row = 0; row < layout.Rows; row++)
    {
        for (int col = 0; col < layout.Cols; col++)
        {
            float x = layout.Margin + col * layout.LeadWidth;
            float y = layout.Margin + row * layout.LeadHeight;
            float w = layout.LeadWidth;
            float h = layout.LeadHeight;

            // 绘制导联边框
            canvas.DrawRect(x, y, w, h, _gridPaint);

            // 绘制细网格
            for (float gx = x; gx <= x + w; gx += layout.GridSpacing / 5)
            {
                canvas.DrawLine(gx, y, gx, y + h, fineGridPaint);
            }
            for (float gy = y; gy <= y + h; gy += layout.GridSpacing / 5)
            {
                canvas.DrawLine(x, gy, x + w, gy, fineGridPaint);
            }

            // 绘制粗网格
            for (float gx = x; gx <= x + w; gx += layout.GridSpacing)
            {
                canvas.DrawLine(gx, y, gx, y + h, coarseGridPaint);
            }
            for (float gy = y; gy <= y + h; gy += layout.GridSpacing)
            {
                canvas.DrawLine(x, gy, x + w, gy, coarseGridPaint);
            }
        }
    }

    fineGridPaint.Dispose();
    coarseGridPaint.Dispose();
}
绘制导联标签
/// <summary>
/// 绘制导联标签
/// </summary>
private void DrawLabels(SKCanvas canvas, ECGLayout layout)
{
    var leadNames = new[] { "I", "aVR", "V1", "V4", "II", "aVL", "V2", "V5", "III", "aVF", "V3", "V6" };

    for (int i = 0; i < leadNames.Length; i++)
    {
        int row = i / layout.Cols;
        int col = i % layout.Cols;
        
        float x = layout.Margin + col * layout.LeadWidth + 10;
        float y = layout.Margin + row * layout.LeadHeight + 20;
        
        canvas.DrawText(leadNames[i], x, y,_skFont,_textPaint);
    }
}
波形路径优化

对于大量的心电图数据点,我们使用SKPath来优化绘制性能:

/// <summary>
/// 绘制单个导联
/// </summary>
private void DrawSingleLead(SKCanvas canvas, float[] data, float x, float y, float width, float height, int sampleRate)
{
    if (data.Length == 0) return;

    var path = new SKPath();
    
    // 计算显示的数据范围(显示2.5秒的数据)
    float displayDuration = 2.5f; // seconds
    int displaySamples = (int)(sampleRate * displayDuration);
    int startIndex = Math.Max(0, data.Length - displaySamples);
    int endIndex = data.Length;
    
    // 计算缩放参数
    float timeScale = width / displayDuration;
    float amplitudeScale = height / 4.0f; // 假设±2mV的范围
    
    // 基线位置
    float baseline = y + height / 2;
    
    bool firstPoint = true;
    for (int i = startIndex; i < endIndex; i++)
    {
        float time = (float)(i - startIndex) / sampleRate;
        float px = x + time * timeScale;
        float py = baseline - data[i] * amplitudeScale;
        
        if (firstPoint)
        {
            path.MoveTo(px, py);
            firstPoint = false;
        }
        else
        {
            path.LineTo(px, py);
        }
    }
    
    canvas.DrawPath(path, _waveformPaint);
    path.Dispose();
}

5. 实时动画系统

为了模拟真实的心电监护仪效果,我们实现了一个基于滑动窗口的实时动画系统:

using SkiaSharp;
using SkiaSharp.Views.Desktop;
using System.ComponentModel;

namespace ECG12LeadViewer
{
    /// <summary>
    /// ECG查看器控件
    /// </summary>
    public class ECGViewerControl : SKControl
    {
        private ECGData? _ecgData;
        private ECGRenderer? _renderer;
        private System.Windows.Forms.Timer? _animationTimer;
        private int _currentSample = 0;

        [Browsable(false)]
        public ECGData? ECGData
        {
            get => _ecgData;
            set
            {
                _ecgData = value;
                _currentSample = 0;
                Invalidate();
            }
        }

        [Browsable(true)]
        [Description("是否启用实时动画效果")]
        public bool EnableAnimation { get; set; } = true;

        [Browsable(true)]
        [Description("动画更新间隔(毫秒)")]
        public int AnimationInterval { get; set; } = 50;

        public ECGViewerControl()
        {
            _renderer = new ECGRenderer();
            InitializeAnimation();
        }

        private void InitializeAnimation()
        {
            _animationTimer = new System.Windows.Forms.Timer();
            _animationTimer.Interval = AnimationInterval;
            _animationTimer.Tick += OnAnimationTick;
        }

        private void OnAnimationTick(object? sender, EventArgs e)
        {
            if (_ecgData != null && EnableAnimation)
            {
                _currentSample += 5; // 每次前进5个样本点
                if (_currentSample >= _ecgData.LeadI.Length)
                {
                    _currentSample = 0; // 循环播放
                }
                Invalidate();
            }
        }

        protected override void OnPaintSurface(SKPaintSurfaceEventArgs e)
        {
            base.OnPaintSurface(e);

            if (_renderer == null) return;

            var surface = e.Surface;
            var canvas = surface.Canvas;
            var info = e.Info;

            canvas.Clear(SKColors.White);

            if (_ecgData != null)
            {
                // 如果启用动画,创建当前时刻的数据快照
                var displayData = EnableAnimation ? CreateAnimatedData(_ecgData, _currentSample) : _ecgData;
                _renderer.Render(surface, displayData, info.Width, info.Height);
            }
            else
            {
                // 绘制占位符
                DrawPlaceholder(canvas, info.Width, info.Height);
            }
        }

        /// <summary>
        /// 创建动画数据(模拟实时显示)
        /// </summary>
        private ECGData CreateAnimatedData(ECGData originalData, int currentSample)
        {
            // 显示2.5秒的数据窗口
            int windowSize = (int)(originalData.SampleRate * 2.5f);
            int startIndex = Math.Max(0, currentSample - windowSize);
            int endIndex = Math.Min(originalData.LeadI.Length, currentSample);

            var animatedData = new ECGData
            {
                SampleRate = originalData.SampleRate,
                Duration = (float)(endIndex - startIndex) / originalData.SampleRate
            };

            // 复制窗口内的数据
            animatedData.LeadI = CopyDataWindow(originalData.LeadI, startIndex, endIndex);
            animatedData.LeadII = CopyDataWindow(originalData.LeadII, startIndex, endIndex);
            animatedData.LeadIII = CopyDataWindow(originalData.LeadIII, startIndex, endIndex);
            animatedData.aVR = CopyDataWindow(originalData.aVR, startIndex, endIndex);
            animatedData.aVL = CopyDataWindow(originalData.aVL, startIndex, endIndex);
            animatedData.aVF = CopyDataWindow(originalData.aVF, startIndex, endIndex);
            animatedData.V1 = CopyDataWindow(originalData.V1, startIndex, endIndex);
            animatedData.V2 = CopyDataWindow(originalData.V2, startIndex, endIndex);
            animatedData.V3 = CopyDataWindow(originalData.V3, startIndex, endIndex);
            animatedData.V4 = CopyDataWindow(originalData.V4, startIndex, endIndex);
            animatedData.V5 = CopyDataWindow(originalData.V5, startIndex, endIndex);
            animatedData.V6 = CopyDataWindow(originalData.V6, startIndex, endIndex);

            return animatedData;
        }

        private float[] CopyDataWindow(float[] sourceData, int startIndex, int endIndex)
        {
            if (startIndex >= endIndex || startIndex >= sourceData.Length)
                return Array.Empty<float>();

            int length = Math.Min(endIndex - startIndex, sourceData.Length - startIndex);
            var result = new float[length];
            Array.Copy(sourceData, startIndex, result, 0, length);
            return result;
        }

        /// <summary>
        /// 绘制占位符
        /// </summary>
        private void DrawPlaceholder(SKCanvas canvas, int width, int height)
        {
            SKFont skFont = new SKFont
            {
                Size = 24,
            };
            SKTextAlign textAlign = SKTextAlign.Center;

            using var paint = new SKPaint
            {
                Color = SKColors.Gray,
                IsAntialias = true,
            };

            string text = "加载ECG数据...";
            canvas.DrawText(text, width / 2, height / 2,textAlign, skFont, paint);
        }

        /// <summary>
        /// 开始播放动画
        /// </summary>
        public void StartAnimation()
        {
            if (EnableAnimation && _animationTimer != null)
            {
                _animationTimer.Start();
            }
        }

        /// <summary>
        /// 停止播放动画
        /// </summary>
        public void StopAnimation()
        {
            if (_animationTimer != null)
            {
                _animationTimer.Stop();
            }
        }

        /// <summary>
        /// 重置到开始位置
        /// </summary>
        public void Reset()
        {
            _currentSample = 0;
            Invalidate();
        }

        protected override void Dispose(bool disposing)
        {
            if (disposing)
            {
                _animationTimer?.Stop();
                _animationTimer?.Dispose();
                _renderer?.Dispose();
            }
            base.Dispose(disposing);
        }
    }
} 

6. 主页面设计

1. 标准医学布局

12导联按照医学标准排列:

|   I   |  aVR  |  V1   |  V4   |
|  II   |  aVL  |  V2   |  V5   |
|  III  |  aVF  |  V3   |  V6   |
2. 直观的控制界面
  • 播放控制: 播放、停止、重置按钮
  • 动画开关: 可以禁用动画查看静态图像
  • 速度调节: 实时调整播放速度
  • 数据加载: 支持外部数据文件加载
3. 响应式布局

界面能够适应不同的窗口大小,保持最佳的显示效果。

4. 控件布局设计和事件实现
using System;
using System.Drawing;
using System.Windows.Forms;

namespace ECG12LeadViewer
{
    /// <summary>
    /// 主窗体
    /// </summary>
    public partial class MainForm : Form
    {
        private ECGViewerControl? _ecgViewer;
        private Button? _loadDataButton;
        private Button? _playButton;
        private Button? _stopButton;
        private Button? _resetButton;
        private CheckBox? _animationCheckBox;
        private TrackBar? _speedTrackBar;
        private Label? _speedLabel;
        private Panel? _controlPanel;

        public MainForm()
        {
            InitializeComponent();
            LoadSimulatedData();
        }

        private void InitializeComponent()
        {
            SuspendLayout();

            // 设置窗体属性
            Text = "12导联心电图查看器 - SkiaSharp";
            Size = new Size(1200, 800);
            StartPosition = FormStartPosition.CenterScreen;
            MinimumSize = new Size(800, 600);

            // 创建控制面板
            _controlPanel = new Panel
            {
                Dock = DockStyle.Top,
                Height = 80,
                BackColor = SystemColors.Control
            };

            // 创建按钮
            _loadDataButton = new Button
            {
                Text = "加载数据",
                Location = new Point(10, 10),
                Size = new Size(80, 30)
            };
            _loadDataButton.Click += OnLoadDataClick;

            _playButton = new Button
            {
                Text = "播放",
                Location = new Point(100, 10),
                Size = new Size(60, 30)
            };
            _playButton.Click += OnPlayClick;

            _stopButton = new Button
            {
                Text = "停止",
                Location = new Point(170, 10),
                Size = new Size(60, 30)
            };
            _stopButton.Click += OnStopClick;

            _resetButton = new Button
            {
                Text = "重置",
                Location = new Point(240, 10),
                Size = new Size(60, 30)
            };
            _resetButton.Click += OnResetClick;

            // 创建动画复选框
            _animationCheckBox = new CheckBox
            {
                Text = "启用动画",
                Location = new Point(320, 15),
                Size = new Size(80, 20),
                Checked = true
            };
            _animationCheckBox.CheckedChanged += OnAnimationCheckChanged;

            // 创建速度控制
            _speedLabel = new Label
            {
                Text = "播放速度:",
                Location = new Point(420, 15),
                Size = new Size(70, 20)
            };

            _speedTrackBar = new TrackBar
            {
                Location = new Point(500, 10),
                Size = new Size(150, 30),
                Minimum = 1,
                Maximum = 10,
                Value = 5,
                TickStyle = TickStyle.BottomRight,
                TickFrequency = 1
            };
            _speedTrackBar.ValueChanged += OnSpeedChanged;

            // 添加控件到控制面板
            _controlPanel.Controls.AddRange(new Control[] 
            { 
                _loadDataButton, _playButton, _stopButton, _resetButton, 
                _animationCheckBox, _speedLabel, _speedTrackBar 
            });

            // 创建ECG查看器
            _ecgViewer = new ECGViewerControl
            {
                Dock = DockStyle.Fill,
                EnableAnimation = true,
                AnimationInterval = 100
            };

            // 添加控件到窗体
            Controls.Add(_ecgViewer);
            Controls.Add(_controlPanel);

            ResumeLayout(false);
        }

        private void LoadSimulatedData()
        {
            try
            {
                var ecgData = ECGData.GenerateSimulatedData();
                if (_ecgViewer != null)
                {
                    _ecgViewer.ECGData = ecgData;
                }
            }
            catch (Exception ex)
            {
                MessageBox.Show($"加载模拟数据时出错: {ex.Message}", "错误", 
                               MessageBoxButtons.OK, MessageBoxIcon.Error);
            }
        }

        private void OnLoadDataClick(object? sender, EventArgs e)
        {
            using var openFileDialog = new OpenFileDialog
            {
                Filter = "所有文件 (*.*)|*.*",
                Title = "选择ECG数据文件"
            };

            if (openFileDialog.ShowDialog() == DialogResult.OK)
            {
                try
                {
                    // 这里可以添加实际的文件加载逻辑
                    // 目前使用模拟数据
                    LoadSimulatedData();
                    MessageBox.Show("数据加载成功!\n(当前显示模拟数据)", "信息", 
                                   MessageBoxButtons.OK, MessageBoxIcon.Information);
                }
                catch (Exception ex)
                {
                    MessageBox.Show($"加载文件时出错: {ex.Message}", "错误", 
                                   MessageBoxButtons.OK, MessageBoxIcon.Error);
                }
            }
        }

        private void OnPlayClick(object? sender, EventArgs e)
        {
            _ecgViewer?.StartAnimation();
            UpdateButtonStates(true);
        }

        private void OnStopClick(object? sender, EventArgs e)
        {
            _ecgViewer?.StopAnimation();
            UpdateButtonStates(false);
        }

        private void OnResetClick(object? sender, EventArgs e)
        {
            _ecgViewer?.Reset();
            UpdateButtonStates(false);
        }

        private void OnAnimationCheckChanged(object? sender, EventArgs e)
        {
            if (_ecgViewer != null && _animationCheckBox != null)
            {
                _ecgViewer.EnableAnimation = _animationCheckBox.Checked;
                if (!_animationCheckBox.Checked)
                {
                    _ecgViewer.StopAnimation();
                    UpdateButtonStates(false);
                }
            }
        }

        private void OnSpeedChanged(object? sender, EventArgs e)
        {
            if (_ecgViewer != null && _speedTrackBar != null)
            {
                // 将滑块值转换为时间间隔(值越大,间隔越小,速度越快)
                int interval = 200 - (_speedTrackBar.Value * 15);
                _ecgViewer.AnimationInterval = Math.Max(20, interval);
            }
        }

        private void UpdateButtonStates(bool isPlaying)
        {
            if (_playButton != null) _playButton.Enabled = !isPlaying;
            if (_stopButton != null) _stopButton.Enabled = isPlaying;
        }

        protected override void OnFormClosing(FormClosingEventArgs e)
        {
            _ecgViewer?.StopAnimation();
            base.OnFormClosing(e);
        }
    }
} 

性能优化策略

1. 内存管理

在图形密集型应用中,内存管理至关重要:

public void Dispose()
{
    _gridPaint?.Dispose();
    _waveformPaint?.Dispose();
    _textPaint?.Dispose();
    _backgroundPaint?.Dispose();
}

2. 按需渲染

只在数据更新时触发重绘,避免不必要的CPU消耗:

public ECGData? ECGData
{
    get => _ecgData;
    set
    {
        _ecgData = value;
        _currentSample = 0;
        Invalidate(); // 只在数据变化时重绘
    }
}

3. 硬件加速

SkiaSharp的一个重要优势是支持GPU硬件加速,特别是在绘制大量数据点时能显著提升性能。

扩展性考虑

1. 文件格式支持

项目架构支持轻松添加各种医学数据格式:

  • EDF (European Data Format)
  • MIT-BIH数据库格式
  • WFDB (Waveform Database)
  • HL7 FHIR标准

2. 信号处理集成

可以集成各种信号处理算法:

public interface IECGProcessor
{
    float[] ApplyFilter(float[] rawData, FilterType type);
    QRSComplex[] DetectQRS(float[] data);
    DiagnosticResult AnalyzeRhythm(ECGData data);
}

3. 云端集成

支持与云端医疗系统集成,实现远程监护和数据同步。

部署与测试

运行命令

dotnet restore
dotnet build
dotnet run

在这里插入图片描述
生成的动画
在这里插入图片描述

总结

通过这个项目,我们可以深刻体会到SkiaSharp在医学软件开发中的巨大潜力。它不仅提供了出色的性能和灵活性,还让我们能够创建真正专业级的医学数据可视化应用。SkiaSharp的强大功能: 从基础绘图到复杂动画,一站式解决方案。希望这个项目和分享的经验能够帮助更多开发者在医疗软件领域探索和创新。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

dotnet研习社

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值