输入关键词开始搜索

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。