
简介这是一套基于VC开发的心电图机监控与诊断系统完整源码面向医疗电子方向的C开发者、生物医学工程专业学生及嵌入式医疗设备二次开发人员用于实现心电信号实时采集、波形动态显示、临床诊断辅助与电子病历管理。资源包共312个文件涵盖58个头文件h与50个实现文件cpp构成核心业务逻辑47个图标ico、9个位图bmp及8个资源脚本rc支撑界面交互另有多个工程配置文件dsp/dsw/sln、本地化资源APS、数据库mdb/biocaredb及DICOM/BMP格式转换模块ECGDICOMConv/ECGBMPConv体现完整医疗软件架构。压缩包大小为4.82MB结构清晰、模块解耦度高便于学习MFC框架下串口通信、实时绘图、多语言支持与医疗数据标准化处理。目前已有196人学习下载适合希望深入理解心电设备上位机开发全流程的中高级C实践者。1. 这不是通用绘图软件而是一套面向心电图机设备的 VC 实时数据采集与临床可视化系统当你在医院信息科或医疗设备厂商现场调试一台新接入的心电图机时最常遇到的不是“怎么画曲线”而是“怎么从这台机器的串口/USB/网络端口里把原始毫伏级电压信号一帧不丢地抓出来再按标准导联顺序实时渲染成可诊断的波形并关联到患者病历”。这个标题里的“心电图机监控软件”不是演示程序它必须满足采样率稳定在 500Hz 以上满足 AHA/IEC60601-2-27 要求、支持多导联同步采集I、II、III、aVR、aVL、aVF、V1–V6、能处理设备特有的帧头校验与协议解析如 GE Marquette、Philips Page、Siemens SC3000 的私有协议还要在 MFC 界面中实现低延迟滚动显示100ms 端到端延迟和病历字段绑定。它面向的是医疗器械软件开发工程师、医院信息科二次开发人员、以及需要对接国产心电设备的 OEM 厂商——这些人不关心“VC 怎么入门”只关心“第 3 个字节是不是导联标识符”“滚动缓冲区怎么设才不卡顿”“如何让医生双击波形直接跳转到对应病历页”。源码.zip 里真正值钱的从来不是CWnd::OnPaint()而是CSerialPort::ReadRawData()后那一段按设备手册逐位解包的ParseMarquetteFrame()函数。2. 用 VC MFC 构建心电数据采集层从物理接口到结构化波形帧心电图机的数据输出方式直接决定采集层架构。当前主流设备仍以 RS-232 串口为主尤其基层医院存量设备部分新型号支持 USB CDC 或 TCP/IP如 Philips IntelliSpace Cardiovascular 的 DICOM-WS 接口。MFC 工程中不能简单调用CreateFile(COM3)就完事——必须区分接口类型、配置硬件流控、设置精确超时并预处理设备特有的握手协议。2.1 串口采集的核心参数与容错设计实际工程中9600bps 是老旧设备底线但现代设备普遍支持 115200bps。关键参数不是波特率本身而是DCB结构体中三个易被忽略的字段DCB dcb {0}; dcb.DCBlength sizeof(DCB); GetCommState(hCom, dcb); dcb.BaudRate CBR_115200; dcb.ByteSize 8; dcb.Parity NOPARITY; dcb.StopBits ONESTOPBIT; // 必须关闭 DTR/RTS 自动控制否则设备可能拒绝发送 dcb.fDtrControl DTR_CONTROL_DISABLE; dcb.fRtsControl RTS_CONTROL_DISABLE; // 关键接收超时设为 0启用 ReadIntervalTimeout 控制单帧完整性 dcb.ReadIntervalTimeout 10; // 字节间最大间隔 ms dcb.ReadTotalTimeoutConstant 50; // 整帧最大等待 ms dcb.ReadTotalTimeoutMultiplier 0; SetCommState(hCom, dcb);提示ReadIntervalTimeout是救命参数。心电设备帧结构通常为“帧头 长度 数据 校验”若设备在帧内因干扰导致字节间隔超过默认值Windows 默认 1000msReadFile()会提前返回部分数据造成后续所有解析错位。设为 10ms 后只有连续字节流才会被当作一帧读取。2.2 解析 Marquette 协议的典型帧结构以 GE Marquette 系列为例其串口输出采用二进制帧格式非 ASCII典型结构如下字段长度说明Sync Byte1 byte固定值 0x02Frame Type1 byte0x01ECG Data, 0x02Patient InfoLength2 bytes小端序后续数据总长度Lead DataN bytes每导联 16-bit 有符号整数按 I, II, III, aVR... 顺序排列Checksum1 byte所有前 N 字节异或和解析函数需严格按字节偏移提取不可依赖sprintf或字符串分割bool CECGParser::ParseMarquetteFrame(const BYTE* pBuf, int nLen, ECG_FRAME frame) { if (nLen 6) return false; // 最小帧长SyncTypeLen(2)至少1字节数据Checksum if (pBuf[0] ! 0x02) return false; WORD wLength MAKEWORD(pBuf[3], pBuf[4]); // 小端序 if (nLen 5 wLength 1) return false; // 不够长度 BYTE checksum 0; for (int i 0; i 5 wLength; i) { checksum ^ pBuf[i]; } if (checksum ! pBuf[5 wLength]) return false; // 校验失败 // 提取导联数据每导联2字节共12导联 → 24字节 if (wLength 24) { for (int i 0; i 12; i) { frame.leads[i] MAKEWORD(pBuf[5 i*2], pBuf[5 i*2 1]); } frame.timestamp GetTickCount64(); return true; } return false; }2.2.1 导联映射与单位换算原始 ADC 值需转换为毫伏mVmV raw_value × (2.5V / 65536) × gain_factor其中gain_factor由设备当前增益档位决定如 10mm/mV 对应 gain1.05mm/mV 对应 gain2.0。该系数必须从设备协议中读取Marquette 在 Patient Info 帧中携带不能硬编码。3. 在 MFC CScrollView 中实现亚像素级心电曲线实时渲染心电波形显示不是“把数组画成折线”而是要解决三个临床刚需① 横轴时间精度达 2ms500Hz 采样下每点 2ms② 纵轴电压分辨率支持 ±5mV 全量程且无锯齿③ 滚动时波形边缘不撕裂。CScrollView是 MFC 中唯一能天然支持平滑滚动和坐标映射的视图类但默认 GDI 绘图在高刷新率下极易卡顿。3.1 双缓冲机制与滚动缓冲区设计直接在OnDraw()中调用MoveTo/LineTo会导致严重闪烁。必须使用内存 DC 位图双缓冲void CECGView::OnDraw(CDC* pDC) { CRect rectClient; GetClientRect(rectClient); // 创建兼容DC和位图 CDC memDC; CBitmap memBitmap; memDC.CreateCompatibleDC(pDC); memBitmap.CreateCompatibleBitmap(pDC, rectClient.Width(), rectClient.Height()); CBitmap* pOldBitmap memDC.SelectObject(memBitmap); // 清空背景深灰底色符合医疗显示规范 memDC.FillSolidRect(rectClient, RGB(30, 30, 30)); // 绘制网格线5mm×5mm 标准心电网格 DrawGrid(memDC, rectClient); // 关键只重绘新增波形段利用滚动缓冲区 DrawNewWaveform(memDC, rectClient); // 一次性 BitBlt 到屏幕 pDC-BitBlt(0, 0, rectClient.Width(), rectClient.Height(), memDC, 0, 0, SRCCOPY); memDC.SelectObject(pOldBitmap); }3.2 时间轴压缩算法解决 500Hz 下的内存爆炸问题若每秒采集 500 点10 秒即 5000 点。直接存储所有原始点会导致内存占用飙升且绘图变慢。实际工程采用“分段压缩”策略时间段存储方式说明最近 2 秒原始点500Hz支持放大查看 P 波细节2–10 秒每 5 点取 max/min100Hz保留 QRS 波群形态10 秒每 20 点取均值25Hz仅显示趋势内存降低 20 倍struct WAVE_COMPRESSED { float fMax; // 该段最大值 float fMin; // 该段最小值 float fAvg; // 该段均值 int nCount; // 原始点数 }; // 压缩逻辑在数据接收线程中完成避免阻塞UI void CECGDataBuffer::CompressBuffer() { const int RAW_POINTS 1000; // 最近2秒原始点 const int COMPRESSED_POINTS 4000; // 10秒压缩点 // 2秒后开始压缩 if (m_rawBuffer.size() RAW_POINTS) { std::vectorWAVE_COMPRESSED compressed; for (int i RAW_POINTS; i m_rawBuffer.size(); i 5) { WAVE_COMPRESSED wc {0}; for (int j 0; j 5 (ij) m_rawBuffer.size(); j) { float v m_rawBuffer[ij]; wc.fMax max(wc.fMax, v); wc.fMin min(wc.fMin, v); wc.fAvg v; wc.nCount; } wc.fAvg / wc.nCount; compressed.push_back(wc); } m_compressedBuffer compressed; // 清理原始缓冲区只保留最近2秒 m_rawBuffer.erase(m_rawBuffer.begin(), m_rawBuffer.begin() RAW_POINTS - 200); } }3.2.1 亚像素抗锯齿绘制心电波形要求线条平滑GDI 默认开启SetGraphicsMode(GM_ADVANCED)并使用GraphicsPathvoid CECGView::DrawWaveform(CDC* pDC, const std::vectorfloat points, int startX, COLORREF color) { Graphics graphics(pDC-GetSafeHdc()); graphics.SetSmoothingMode(SmoothingModeAntiAlias); GraphicsPath path; for (size_t i 0; i points.size(); i) { float x startX i * m_fXScale; // m_fXScale 2.0f (2像素/ms) float y GetYFromVoltage(points[i]); // 电压→像素Y if (i 0) { path.StartFigure(); path.AddLine((REAL)x, (REAL)y, (REAL)x, (REAL)y); } else { path.AddLine((REAL)x, (REAL)y, (REAL)x, (REAL)y); } } Pen pen(color, 1.2f); // 1.2像素线宽增强可读性 graphics.DrawPath(pen, path); }4. 病历数据绑定与诊断辅助功能的工程落地心电图监控软件的价值不在“能画线”而在“画的线能驱动临床决策”。源码中CRecordManager类负责将波形数据与结构化病历字段双向绑定这是通过 Windows 标准 COM 接口实现的而非简单数据库写入。4.1 使用 OLE DB 直连医院 HIS 数据库多数三甲医院 HIS 采用 SQL Server要求软件通过 Windows 身份验证连接避免明文密码// 连接字符串示例无需用户名密码 CString strConn _T(ProviderSQLOLEDB.1;Integrated SecuritySSPI;); strConn _T(Persist Security InfoFalse;); strConn _T(Initial CatalogHIS_ECG;); strConn _T(Data Source10.1.1.100;); HRESULT hr m_pConnection-Open(_bstr_t(strConn), L, L, adConnectUnspecified); if (FAILED(hr)) { // 检查 Windows 身份验证权限运行软件的账户必须是 HIS 数据库的 db_datareader/db_datawriter 角色 AfxMessageBox(_T(HIS数据库连接失败请检查域账户权限)); return FALSE; }4.2 双击波形跳转病历的底层机制MFC 中OnLButtonDblClk()获取点击位置后需反向计算时间戳并查询对应病历void CECGView::OnLButtonDblClk(UINT nFlags, CPoint point) { // 将屏幕坐标转为时间戳毫秒 CRect rectClient; GetClientRect(rectClient); float fMsPerPixel 1000.0f / (rectClient.Width() * m_fXScale); // 例如 2px/ms → 0.5ms/px float fElapsedMs (point.x - m_nLeftMargin) * fMsPerPixel; // 查询最近一次采集时间点 SYSTEMTIME st; GetLocalTime(st); FILETIME ft; SystemTimeToFileTime(st, ft); ULARGE_INTEGER uli; uli.LowPart ft.dwLowDateTime; uli.HighPart ft.dwHighDateTime; __int64 llNow uli.QuadPart; // 构造SQL查找该时间点前后5秒内的病历 CString strSQL; strSQL.Format(_T(SELECT TOP 1 patient_id, ecg_id, report_time FROM ecg_records ) _T(WHERE ABS(DATEDIFF(ms, report_time, GETDATE())) 5000 ) _T(ORDER BY ABS(DATEDIFF(ms, report_time, GETDATE())))); _RecordsetPtr pRS; pRS m_pConnection-Execute(_bstr_t(strSQL), NULL, adCmdText); if (!pRS-EndOfFile) { CString strPatientID (LPCTSTR)(_bstr_t)pRS-Fields-GetItem(patient_id)-Value; // 启动病历查看器调用医院标准病历系统exe ShellExecute(NULL, _T(open), _T(HISChartViewer.exe), strPatientID, NULL, SW_SHOW); } }4.2.1 诊断标记的持久化存储医生在波形上标注的“ST段抬高”“房颤”等标签需存入独立表ecg_annotations字段类型说明annotation_idint PK主键ecg_idint FK关联心电记录start_time_msint标记起始时间相对本条记录开头的毫秒end_time_msint标记结束时间diagnosis_codevarchar(10)ICD-10 编码如 I48.9annotator_idint医生工号插入语句必须带事务确保原子性m_pConnection-BeginTrans(); try { _CommandPtr pCmd; pCmd-ActiveConnection m_pConnection; pCmd-CommandText _T(INSERT INTO ecg_annotations (...) VALUES (?, ?, ?, ?, ?)); pCmd-Parameters-Append(pCmd-CreateParameter(...)); pCmd-Execute(NULL, NULL, adCmdText); m_pConnection-CommitTrans(); } catch (...) { m_pConnection-RollbackTrans(); throw; }5. 调试与部署关键技巧绕过 VC Runtime 依赖和串口权限陷阱交付给医院信息科的软件最大的上线障碍不是功能缺陷而是环境兼容性。以下三点是现场工程师反复踩坑后总结的硬性操作清单。5.1 静态链接 CRT 以消除 vc runtime repair tool 依赖MFC 工程默认动态链接 CRTMSVCR120.dll 等而医院电脑往往缺失这些运行库。必须改为静态链接右键项目 → 属性 → 配置属性 → C/C → 代码生成 → 运行库将/MD动态改为/MT静态关键MFC 库也必须同步改为静态属性 → 常规 → 使用 MFC → 在静态库中使用 MFC重新编译后用dumpbin /dependents YourApp.exe验证输出中不再出现MSVCR*.dll注意静态链接后 EXE 体积增大约 2MB但彻底规避了医院电脑缺少运行库的问题。这是医疗软件部署的强制要求。5.2 Windows 10/11 下串口权限的注册表修复Windows 10 1809 后默认禁止普通用户访问 COM 端口。即使以管理员运行仍需手动授权Windows Registry Editor Version 5.00 [HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\USBSTOR] Startdword:00000003 [HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\serenum] Startdword:00000003 ; 关键授予 Users 组对 COM 端口的读写权限 [HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\USBSerial\Enum] Securityhex:01,00,14,00,02,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,0......实际部署时应将此注册表项导出为.reg文件与安装包一同交付并在安装脚本中静默导入echo off reg import com_permission_fix.reg nul 21 echo COM端口权限已修复5.3 心电数据采集的实时性验证方法不能仅靠“看起来流畅”判断性能。必须用硬件级工具验证验证项工具合格标准端到端延迟示波器 信号发生器输入 1Hz 方波测量软件显示上升沿与输入相差 ≤120ms丢帧率串口监视工具如 SerialPortMonitor连续采集 1 小时校验失败帧数 0CPU 占用Windows 性能监视器Process\% Processor Time\YourApp.exe 15%i5-8250U若延迟超标优先检查①ReadIntervalTimeout是否设为 0②OnDraw()中是否调用了Sleep()③ 是否在 UI 线程中执行了数据库查询必须移至工作线程。本文还有配套的精品资源点击获取