4、心电图显示组件:QPainter绘图基础、实时波形绘制、网格背景绘制、波形缩放与平移

好,咱们进入第四章。心电图显示组件,说白了就是整个监护仪的脸面。你想想看,医生盯着屏幕看什么?看的就是那条跳动的波形。波形画得漂不漂亮、流不流畅,直接决定了这套系统的专业度。

我个人习惯把心电图显示拆成四个层次:网格背景波形数据实时刷新交互操作。咱们一层一层剥开来讲。

4.1 QPainter绘图基础——画布上的基本功

Qt里的QPainter,就像你手里的画笔。你得先铺好画布(QWidget或QImage),然后才能往上画东西。我在项目中遇到过不少新手,上来就画,结果画布都没设置好,画出来的东西要么不显示,要么闪烁得厉害。

先看一个最基础的例子:

void ECGWidget::paintEvent(QPaintEvent *)
{
    QPainter painter(this);
    painter.setRenderHint(QPainter::Antialiasing);  // 抗锯齿,让线条更平滑

    // 设置画笔
    QPen pen(Qt::green, 2);
    painter.setPen(pen);

    // 画一条简单的直线
    painter.drawLine(0, height()/2, width(), height()/2);
}

嗯,这里要注意:paintEvent是自动调用的,你不能手动去调它。想刷新画面?用update()。我曾经犯过这个错,直接在循环里调repaint(),结果CPU飙到100%,画面还卡成PPT。

小技巧:QPainter的坐标系原点在左上角,X向右为正,Y向下为正。画心电图时,Y轴方向跟医学波形是反的,记得取反。

4.2 网格背景绘制——给波形一个坐标系

心电图的网格不是随便画的。标准监护仪的网格,大格是5mm×5mm,小格是1mm×1mm。每个大格代表0.2秒(走纸速度25mm/s时)。

我建议把网格绘制单独封装成一个函数:

void ECGWidget::drawGrid(QPainter &painter)
{
    int w = width();
    int h = height();

    // 小格间距:10像素
    int smallGrid = 10;
    // 大格间距:50像素(5个小格)
    int bigGrid = 50;

    QPen lightPen(QColor(0, 80, 0), 1);   // 暗绿色小格
    QPen darkPen(QColor(0, 150, 0), 2);   // 亮绿色大格

    // 画竖线
    for (int x = 0; x <= w; x += smallGrid) {
        painter.setPen((x % bigGrid == 0) ? darkPen : lightPen);
        painter.drawLine(x, 0, x, h);
    }

    // 画横线
    for (int y = 0; y <= h; y += smallGrid) {
        painter.setPen((y % bigGrid == 0) ? darkPen : lightPen);
        painter.drawLine(0, y, w, y);
    }
}
关键点:网格颜色不要用纯白或纯黑。医用监护仪通常用暗绿色或深灰色,这样波形(亮绿色)才能突出。我见过有人用白色网格配绿色波形,眼睛看十分钟就花了。

4.3 实时波形绘制——让心跳动起来

实时波形,说白了就是不断往右推数据。新数据从右边进来,旧数据从左边消失。这跟示波器的原理一模一样。

我常用的数据结构是环形缓冲区

class ECGWidget : public QWidget
{
private:
    static const int BUFFER_SIZE = 1000;
    double buffer[BUFFER_SIZE];   // 存储采样点
    int head;                     // 当前写入位置
    double scaleX;                // 水平缩放
    double scaleY;                // 垂直缩放
    int offsetX;                  // 水平偏移(平移)
};

绘制的时候,从缓冲区里取数据,逐点连线:

void ECGWidget::drawWaveform(QPainter &painter)
{
    QPen wavePen(Qt::green, 2);
    painter.setPen(wavePen);

    int midY = height() / 2;
    int startX = offsetX;

    for (int i = 1; i < BUFFER_SIZE; i++) {
        int idx1 = (head - i + 1 + BUFFER_SIZE) % BUFFER_SIZE;
        int idx2 = (head - i + BUFFER_SIZE) % BUFFER_SIZE;

        int x1 = width() - (i - 1) * scaleX + startX;
        int x2 = width() - i * scaleX + startX;
        int y1 = midY - buffer[idx1] * scaleY;
        int y2 = midY - buffer[idx2] * scaleY;

        painter.drawLine(x1, y1, x2, y2);
    }
}
避坑指南:我曾经在画实时波形时,直接用QPainterPath来画,结果数据量一大(比如5000个点),CPU直接拉满。后来改用drawLine逐段画,性能提升了好几倍。记住:实时系统里,简单就是快

4.4 波形缩放与平移——让医生看得更清楚

医生有时候想放大看某个波形细节,有时候想左右拖动看历史数据。这就是缩放和平移。

缩放我一般用鼠标滚轮事件:

void ECGWidget::wheelEvent(QWheelEvent *event)
{
    double delta = event->angleDelta().y() / 120.0;
    double factor = 1.0 + delta * 0.1;  // 每次缩放10%

    // 限制缩放范围
    if (scaleX * factor > 0.5 && scaleX * factor < 10.0) {
        scaleX *= factor;
        scaleY *= factor;
        update();
    }
}

平移我用鼠标拖拽:

void ECGWidget::mousePressEvent(QMouseEvent *event)
{
    lastMouseX = event->pos().x();
}

void ECGWidget::mouseMoveEvent(QMouseEvent *event)
{
    int dx = event->pos().x() - lastMouseX;
    offsetX += dx;
    lastMouseX = event->pos().x();
    update();
}
经验之谈:缩放时最好以鼠标位置为中心,而不是以屏幕中心。这样医生想放大看某个波形,直接把鼠标移过去滚轮就行,不用再拖动。这个细节,很多初版产品都忽略了。

4.5 性能优化——别让波形卡顿

实时波形最怕什么?卡顿。一卡顿,医生就怀疑病人出问题了。其实只是程序没写好。

我总结了几条优化原则:

  • 减少绘制区域:只绘制可见区域,不可见的点直接跳过
  • 双缓冲:Qt默认已经做了,但如果你自己操作QPixmap,记得用双缓冲
  • 降低刷新频率:不是每来一个数据点就刷新一次,而是攒够一批(比如20ms)再刷新
  • 避免在paintEvent里做计算:数据预处理放到单独的线程里

嗯,说到线程,我建议把数据采集和波形绘制分开。采集线程只管往缓冲区写数据,UI线程只管从缓冲区读数据画图。这样哪怕数据量再大,界面也不会卡。

4.6 完整示例——组装起来

最后,咱们把上面这些零件组装成一个完整的心电图显示组件:

void ECGWidget::paintEvent(QPaintEvent *)
{
    QPainter painter(this);
    painter.setRenderHint(QPainter::Antialiasing);

    // 1. 画网格背景
    drawGrid(painter);

    // 2. 画波形
    drawWaveform(painter);

    // 3. 画基线(中间那条参考线)
    QPen basePen(QColor(0, 100, 0), 1, Qt::DashLine);
    painter.setPen(basePen);
    painter.drawLine(0, height()/2, width(), height()/2);
}

你看,结构很清晰。先画网格,再画波形,最后画辅助线。顺序不能乱,否则波形会被网格盖住。

这一章的内容就到这里。心电图显示组件,说白了就是网格 + 波形 + 交互。网格给参考,波形给信息,交互给便利。三者缺一不可。

下一章咱们聊聊触摸交互——怎么让医生用手指就能完成所有操作。嗯,那才是真正考验UI设计功底的地方。