Qt 自定义绘制 — QPainter 核心语法与实战
Qt 自定义绘制 — 核心语法与实战思路
基于 PulseQt T010(RealTimeChart)实战总结。涵盖 QPainter、双缓冲、坐标变换、交互事件、性能优化。
一、QPainter 基础
最小绘制骨架
void MyWidget::paintEvent(QPaintEvent *)
{
QPainter painter(this); // ① 绑定到当前 widget
painter.setRenderHint(QPainter::Antialiasing, true); // ② 抗锯齿
painter.setPen(Qt::black); // ③ 设置画笔
painter.setBrush(Qt::white); // ④ 设置画刷(填充)
painter.drawLine(0, 0, 100, 100); // ⑤ 画
}
| 步骤 | API | 说明 |
|---|---|---|
| ① | QPainter(widget) | 绑到 widget,画的内容直接显示在控件上 |
| ② | setRenderHint(Antialiasing) | 抗锯齿(⚠️ 软件渲染下极慢,见第七节) |
| ③ | setPen(QPen) | 线条颜色、粗细、样式 |
| ④ | setBrush(QBrush) | 填充颜色、图案 |
| ⑤ | drawLine/drawRect/... | 具体绘制 |
坐标系
(0,0) ────→ x 增(右)
│
↓
y 增(下)
和数学坐标系 Y 轴相反!绘图时时刻记住:Y 越大越靠下。
二、常用绘制 API
线条与形状
painter.drawLine(x1, y1, x2, y2); // 直线
painter.drawRect(x, y, w, h); // 矩形
painter.drawEllipse(cx, cy, rx, ry); // 椭圆
painter.drawText(x, y, "text"); // 文字
// 网格线:浅灰 + 细线
painter.setPen(QPen(QColor(0xE0, 0xE0, 0xE0), 0.5));
for (int i = 0; i <= 4; ++i) {
double y = top + (bottom - top) * i / 4.0;
painter.drawLine(left, y, right, y);
}
折线(推荐用于实时数据)
QPolygonF polyline;
polyline << QPointF(10, 20) << QPointF(30, 50) << QPointF(80, 30);
painter.setPen(QPen(Qt::red, 1.5));
painter.drawPolyline(polyline); // 不闭合(折线)
// painter.drawPolygon(polyline); // 闭合(多边形)
QPainterPath(贝塞尔路径,不适合大量折线)
QPainterPath path;
path.moveTo(startX, startY); // 移到起点
path.lineTo(x1, y1); // 画直线
path.cubicTo(...); // 贝塞尔曲线
painter.drawPath(path);
// ⚠️ drawPath 比 drawPolyline 慢 3-5 倍,适合曲线,不适合大量折线
三、坐标变换(数据 → 像素)
这是自绘曲线最核心的数学。数据有自己的坐标系(时间 + 数值),需要映射到屏幕像素。
// 时间戳(毫秒)→ 像素 X
double timeToPixelX(uint64_t ts, uint64_t latestTs, double windowMs) const
{
// ts 在 [latestTs-windowMs, latestTs] 之间
double ratio = (latestTs - ts) / windowMs; // 0.0(最新)→ 1.0(最旧)
double rightEdge = width() - 20.0; // 右侧留 20px
double leftEdge = 50.0; // 左侧留 50px(Y 轴宽度)
return rightEdge - ratio * (rightEdge - leftEdge);
}
// 数值 → 像素 Y
double valueToPixelY(double val, double yMin, double yMax) const
{
// val 在 [yMin, yMax] 之间
double ratio = (val - yMin) / (yMax - yMin); // 0.0(最小)→ 1.0(最大)
double bottom = height() - 40.0; // 底部留 40px
double top = 20.0; // 顶部留 20px
return bottom - ratio * (bottom - top); // Y 轴翻转!
}
关键:Y 像素 = 底部 - ratio × 高度。因为屏幕 Y 往下增长,而数据通常 Y 往上增长。
四、双缓冲
为什么需要
// ❌ 直接绘制 — 用户看到绘制过程(空白→网格→曲线→图例),闪烁
void paintEvent(QPaintEvent *) {
QPainter p(this);
drawBackground(p); // 屏幕先白后黑
drawCurves(p); // 屏幕再画曲线
}
// ✅ 双缓冲 — 在内存中画完整张图,一次性贴到屏幕
void paintEvent(QPaintEvent *) {
QPixmap offscreen(size()); // 离屏画布
offscreen.fill(Qt::white);
QPainter p(&offscreen); // 在内存中画
drawBackground(p);
drawCurves(p);
p.end();
QPainter screen(this); // 一次性贴到屏幕
screen.drawPixmap(0, 0, offscreen);
}
性能优化:复用 QPixmap
// 成员变量
QPixmap m_offscreen;
void paintEvent(QPaintEvent *) {
if (m_offscreen.size() != size()) // 只在尺寸变化时重建
m_offscreen = QPixmap(size());
m_offscreen.fill(Qt::white); // 填充(memset 3.8MB,很快)
QPainter p(&m_offscreen);
// ... 绘制 ...
p.end();
QPainter screen(this);
screen.drawPixmap(0, 0, m_offscreen); // RAM→VRAM 拷贝
}
收益:避免每帧 new QPixmap 导致 3.8MB 堆碎片累积。
五、大量数据点的处理
裁剪(只画可见区域)
auto snap = m_buffer->snapshot(); // 可能 10000 条
// 二分查找跳到第一个可见点,跳过屏外 7000 条
uint64_t minTs = latestTs - windowMs;
auto it = std::lower_bound(snap.begin(), snap.end(), minTs,
[](const DataPoint &dp, uint64_t ts) { return dp.timestamp < ts; });
for (; it != snap.end(); ++it) { /* 只画可见的 3000 条 */ }
抽稀(相邻点太近就跳过)
double threshold = 1.5; // 像素间距阈值
double lastPx = -9999;
for (auto &dp : snap) {
double px = timeToPixelX(dp.timestamp, ...);
if (qAbs(px - lastPx) < threshold) continue; // 太近,跳过
lastPx = px;
polyline.append(QPointF(px, py));
}
效果:3000 个数据点 → 900px 画面 → 约 600 个有效像素(阈值 1.5)。画面流畅,CPU 省 5 倍。
六、交互事件
滚轮缩放
void wheelEvent(QWheelEvent *event) override {
if (event->angleDelta().y() > 0)
m_timeWindow /= 1.2; // 放大
else
m_timeWindow *= 1.2; // 缩小
m_timeWindow = qBound(5.0, m_timeWindow, 120.0); // 限制范围
update(); // 触发重绘
}
鼠标拖拽
void mousePressEvent(QMouseEvent *e) override { m_dragging = true; m_lastPos = e->pos(); }
void mouseMoveEvent(QMouseEvent *e) override {
if (!m_dragging) return;
double dx = e->pos().x() - m_lastPos.x();
double msPerPixel = (m_timeWindow * 1000.0) / (width() - 70.0);
m_xOffset -= dx * msPerPixel; // 像素位移 → 时间偏移
m_lastPos = e->pos();
update();
}
void mouseReleaseEvent(QMouseEvent *) override { m_dragging = false; }
右键重置
void contextMenuEvent(QContextMenuEvent *) override {
m_timeWindow = 30.0; m_xOffset = 0.0; update();
}
七、性能陷阱与解决方案
🔴 陷阱 1:抗锯齿(最致命)
| 开启 | 关闭 | |
|---|---|---|
| 软件渲染 | 400ms/帧 | 5ms/帧 |
| GPU 加速 | 10ms/帧 | 3ms/帧 |
规则:大量折线(100+ 线段)→ 关抗锯齿。文字、坐标轴(少量线条)→ 可保留。
// 分区域控制
painter.setRenderHint(QPainter::Antialiasing, true);
drawBackground(p); // 坐标轴平滑
painter.setRenderHint(QPainter::Antialiasing, false);
drawCurves(p); // 曲线用 QPolygonF,本身就直,不需要 AA
🔴 陷阱 2:QPainterPath vs QPolygonF
// ❌ 慢 — 即使全直线,drawPath 也走贝塞尔管线
path.moveTo(x, y); path.lineTo(x2, y2); painter.drawPath(path);
// ✅ 快 3-5 倍 — 纯折线,不涉及曲线计算
QPolygonF poly; poly.append(QPointF(x, y)); painter.drawPolyline(poly);
🟡 陷阱 3:每帧 new QPixmap
// ❌ 每帧分配 3.8MB → 堆碎片 → 越来越慢
QPixmap offscreen(size());
// ✅ 成员变量复用
if (m_offscreen.size() != size()) m_offscreen = QPixmap(size());
🟡 陷阱 4:repaint() vs update()
repaint() | update() | |
|---|---|---|
| 执行方式 | 同步,立即画 | 异步,事件队列 |
| 阻塞事件循环 | ✅ 是 | ❌ 否 |
| 适用场景 | 罕见(截图) | 99% 情况用这个 |
void onTimer() { update(); } // ✅ 不阻塞,数据照常进来
🟡 陷阱 5:snapshot 每点调一次
// ❌ 循环内取 snapshot → 锁竞争
for (auto &dp : snap) { auto ts = m_buffer->snapshot().last().timestamp; }
// ✅ 循环外取一次,参数传入
auto snap = m_buffer->snapshot();
uint64_t latestTs = snap.last().timestamp;
for (auto &dp : snap) { double px = timeToPixelX(dp.ts, latestTs, windowMs); }
八、完整绘制流程(以 PulseQt RealTimeChart 为例)
定时器 40ms →
update() → Qt 事件队列 →
paintEvent():
① QPixmap 复用(尺寸不变不重建)
② fill(Qt::white)(memset 3.8MB)
③ QPainter 绑到 QPixmap
④ setRenderHint(Antialiasing, true) → drawBackground(网格+轴)
⑤ setRenderHint(Antialiasing, false) → drawCurves():
snapshot() ×1
lower_bound 跳到可见区域
3 通道 × for loop:
timeToPixelX + valueToPixelY × N 点
间距 < 1.5px → 跳过(抽稀)
QPolygonF::append
drawPolyline ×3
⑥ drawLegend(小色块)
⑦ p.end()
⑧ screenPainter.drawPixmap(贴到屏幕)
每帧耗时:~5ms(无抗锯齿 + 抽稀 + 裁剪),25FPS 下 5/40ms = 12.5% CPU。