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。
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设计功底的地方。