输入关键词开始搜索

Qt 界面绘制 — 顺序逻辑与设计方法

Qt 渲染管线

事件循环

  ├─→ update() 调用(标记脏区域)
  │     │
  │     └─→ QWidgetBackingStore::sync()
  │           │
  │           ├─→ paintEvent(QPaintEvent *) ← 你写绘制代码的地方
  │           │     └─ QPainter 绘制到 QPixmap(离屏)
  │           │
  │           └─→ 贴到屏幕(平台相关: D3D / OpenGL / X11)

  └─→ 下一帧(16ms 后,如果 60FPS)

paintEvent 调用时机

// 以下操作触发 paintEvent:
widget->update();        // 异步,合并多次调用(推荐)
widget->repaint();       // 同步,立即绘制(慎用)
widget->show();          // 首次显示
widget->resize(w, h);    // 尺寸变化
widget->setVisible(true);// 从隐藏到可见
// 被遮挡部分重新露出     // 系统触发

update() vs repaint()

// update() — 标记需要重绘,等事件循环统一处理
void onDataReceived() {
    m_buffer.push(data);
    update();  // 不阻塞,数据继续进来
    update();  // 连续两次 → 合并为一次 paintEvent
}

// repaint() — 当场画,不等待
void onScreenshot() {
    repaint();  // 立即画,保证截图是当前状态
    grab().save("screenshot.png");
}
// ⚠️ repaint 在 60FPS 下每帧只能用一次,频繁调用卡主事件循环

控件层级与绘制顺序

父控件::paintEvent 先执行
  ├─ 绘制自身背景
  ├─ 绘制自身内容
  ├─ 子控件 1::paintEvent
  ├─ 子控件 2::paintEvent
  └─ 子控件 N::paintEvent
后绘制的控件覆盖在前面(Z 轴叠加)
// 控件层级控制绘制顺序
void CustomWidget::paintEvent(QPaintEvent *) {
    QPainter p(this);

    // 1. 背景层 — 先画
    p.fillRect(rect(), QColor("#f5f5f5"));

    // 2. 网格层
    drawGrid(p);

    // 3. 数据层 — 后画(覆盖在上面)
    drawCurves(p);

    // 4. 标注层 — 最上面
    drawLegend(p);
    drawCrosshair(p, m_mousePos);

    // 5. 子控件自动绘制(Qt 在 paintEvent 返回后处理)
}

布局设计方法

QWidget 布局三要素

// ① sizeHint — 告诉布局管理器"我想要多大"
QSize MyWidget::sizeHint() const override {
    return QSize(200, 150);  // 理想大小
}

// ② minimumSizeHint — "不能再小了"
QSize MyWidget::minimumSizeHint() const override {
    return QSize(100, 80);
}

// ③ sizePolicy — "空间多了/少了怎么分配"
setSizePolicy(QSizePolicy::Expanding,  // 水平:尽量占满
              QSizePolicy::Fixed);     // 垂直:固定

布局策略选择

场景布局策略原因
固定尺寸控件Fixed按钮、标签
表单输入框Expanding 水平 + Fixed 垂直横向随窗口伸缩
日志/终端输出Expanding x 2占满所有剩余空间
侧边栏Preferred + 限制 maximumWidth可伸缩但有上限
占位块Ignored布局 spacer 替代

自定义控件在布局中的行为

// 自绘控件必须重写 sizeHint,否则默认 640x480
class RealTimeChart : public QWidget {
public:
    QSize sizeHint() const override {
        return QSize(400, 300);  // 不给布局一个期望值 = 布局不工作
    }

    // ⚠️ 绝对不能用 setGeometry 替代布局
    // RealTimeChart(0,0, 400,300) ← 窗口放大控件不变
};

双缓冲的正确实现

class SmoothWidget : public QWidget {
    QPixmap m_offscreen;

    void paintEvent(QPaintEvent *) override {
        // ① 尺寸变了 → 重建
        if (m_offscreen.size() != size())
            m_offscreen = QPixmap(size());

        // ② 清空
        m_offscreen.fill(palette().window().color());

        // ③ 在内存中画完
        {
            QPainter p(&m_offscreen);
            p.setRenderHint(QPainter::Antialiasing, false);
            drawAll(p);
        }  // p 析构 → flush 到 QPixmap

        // ④ 一次性贴到屏幕
        QPainter screen(this);
        screen.drawPixmap(0, 0, m_offscreen);
    }
};

抗锯齿按区域控制

void drawContent(QPainter &p) {
    // 文字和轴 → 开抗锯齿(没几笔画,开销可忽略)
    p.setRenderHint(QPainter::Antialiasing, true);
    drawAxis(p);
    drawTextLabels(p);

    // 数据曲线 → 关抗锯齿(数千条线段,开抗锯齿慢 20 倍)
    p.setRenderHint(QPainter::Antialiasing, false);
    drawDataCurves(p);
}

常见错误

❌ 在 paintEvent 外创建 QPainter(this)  → 未定义行为
   QPainter 生命周期 = paintEvent 内

❌ 在 paintEvent 里 new 对象           → 每帧分配 → 堆碎片
   ✅ 成员变量复用(QPixmap、QPen、QBrush、QFont)

❌ 忽略 update() 的脏区域优化
   QPaintEvent::rect() 告诉你要画多大,可以只画这条
   ✅ 裁剪到 damage rect: p.setClipRect(event->rect())

❌ 在子控件 paintEvent 前忘了调父类
   ✅ 第一行: QWidget::paintEvent(event) (画背景)