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 返回后处理)
}
布局设计方法
// ① 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) (画背景)