在 C++ 的 Qt 图形界面开发中,窗口尺寸变化(resize) 是一个高频事件。每当用户拖动窗口边框、最大化/还原窗口,或程序调用 resize() 时,Qt 默认会触发整个窗口的 paintEvent() 重绘。
对于大多数普通控件(如按钮、文本框),这没有问题。但对于自定义绘图密集型窗口(如绘图板、波形显示器、实时视频预览、大型图表),每一次 resize 都重绘整个画面,会造成:
- CPU/GPU 资源浪费
- 界面卡顿、掉帧
- 用户体验下降
幸运的是,Qt 提供了一个精巧的优化机制:Qt::WA_StaticContents 属性。
✅ 设置 this->setAttribute(Qt::WA_StaticContents, true); 后,窗口内容将被视为“静态”,仅在窗口首次显示或内容真正改变时重绘,而尺寸变化时不再自动触发全屏重绘。
本文将深入解析该属性的工作原理、适用场景、限制条件,并提供多个可运行的代码示例,助你构建高性能自定义绘图控件。
一、默认行为:为什么 resize 会触发重绘?
1.1 Qt 的重绘机制简述
- 当窗口尺寸改变,Qt 认为 “可视区域已变”,原有像素可能无效;
- 因此标记整个窗口为 “脏区域(dirty region)”;
- 下次事件循环中,调用
paintEvent() 重绘全部内容。
1.2 问题复现:无优化的绘图窗口
// BadPaintWidget.h
#ifndef BADPAINTWIDGET_H
#define BADPAINTWIDGET_H
#include <QWidget>
#include <QDateTime>
class BadPaintWidget : public QWidget
{
Q_OBJECT
public:
explicit BadPaintWidget(QWidget *parent = nullptr);
protected:
void paintEvent(QPaintEvent *event) override;
void resizeEvent(QResizeEvent *event) override;
private:
int m_paintCount = 0;
};
#endif // BADPAINTWIDGET_H
// BadPaintWidget.cpp
#include "BadPaintWidget.h"
#include <QPainter>
#include <QDebug>
BadPaintWidget::BadPaintWidget(QWidget *parent)
: QWidget(parent)
{
setAttribute(Qt::WA_OpaquePaintEvent); // 不绘制背景,提升性能
}
void BadPaintWidget::paintEvent(QPaintEvent *event)
{
Q_UNUSED(event);
m_paintCount++;
qDebug() << "paintEvent called! Count:" << m_paintCount
<< "| Time:" << QDateTime::currentMSecsSinceEpoch();
QPainter painter(this);
painter.fillRect(rect(), Qt::black);
// 模拟复杂绘图:绘制1000个随机圆(耗时操作)
for(int i = 0; i < 1000; ++i){
painter.setPen(QColor::fromHsv(qrand()%360,255,255));
painter.drawEllipse(
qrand()%width(),
qrand()%height(),
20+(qrand()%30),
20+(qrand()%30)
);
}
}
void BadPaintWidget::resizeEvent(QResizeEvent *event)
{
qDebug() << "resizeEvent: new size" << event->size();
QWidget::resizeEvent(event);
}
1.3 运行效果
- 拖动窗口大小 → 控制台疯狂输出
paintEvent called!
- 界面明显卡顿,尤其在高分辨率下
- 即使内容未变,每次 resize 都重绘全部 1000 个圆!
❌ 这就是典型的“过度重绘”问题。
二、解决方案:启用 Qt::WA_StaticContents
2.1 属性作用详解
| 属性 |
行为 |
| 未设置(默认) |
尺寸变化 → 标记整个窗口为 dirty → 触发 paintEvent() |
WA_StaticContents = true |
尺寸变化 → 不标记 dirty → 仅当调用 update() 或内容改变时才重绘 |
📌 关键前提:你的绘图内容与窗口尺寸无关,或你自行管理缩放逻辑。
2.2 优化后的代码
// GoodPaintWidget.h
#ifndef GOODPAINTWIDGET_H
#define GOODPAINTWIDGET_H
#include <QWidget>
class GoodPaintWidget : public QWidget
{
Q_OBJECT
public:
explicit GoodPaintWidget(QWidget *parent = nullptr);
protected:
void paintEvent(QPaintEvent *event) override;
void resizeEvent(QResizeEvent *event) override;
private:
QPixmap m_buffer; // 双缓冲 pixmap
bool m_needsRepaint = true;
int m_paintCount = 0;
};
#endif // GOODPAINTWIDGET_H
// GoodPaintWidget.cpp
#include "GoodPaintWidget.h"
#include <QPainter>
#include <QDebug>
GoodPaintWidget::GoodPaintWidget(QWidget *parent)
: QWidget(parent)
{
setAttribute(Qt::WA_OpaquePaintEvent);
setAttribute(Qt::WA_StaticContents, true); // ✅ 关键优化!
// 初始化缓冲区
connect(this, &GoodPaintWidget::resized, this, [this](){
m_needsRepaint = true;
update(); // 仅当需要时触发重绘
});
}
void GoodPaintWidget::paintEvent(QPaintEvent *event)
{
Q_UNUSED(event);
m_paintCount++;
qDebug() << "paintEvent called! Count:" << m_paintCount;
// 重建缓冲区(仅当尺寸变化或首次绘制)
if(m_needsRepaint || m_buffer.size() != size()){
m_buffer = QPixmap(size());
QPainter p(&m_buffer);
p.fillRect(rect(), Qt::black);
// 复杂绘图只在此处执行一次
for(int i = 0; i < 1000; ++i){
p.setPen(QColor::fromHsv(qrand()%360,255,255));
p.drawEllipse(
qrand()%width(),
qrand()%height(),
20+(qrand()%30),
20+(qrand()%30)
);
}
m_needsRepaint = false;
}
// 快速 blit 缓冲区到屏幕
QPainter painter(this);
painter.drawPixmap(0,0, m_buffer);
}
void GoodPaintWidget::resizeEvent(QResizeEvent *event)
{
qDebug() << "resizeEvent: new size" << event->size();
// 注意:此处不会自动触发 paintEvent!
QWidget::resizeEvent(event);
}
2.3 运行效果对比
| 操作 |
BadPaintWidget |
GoodPaintWidget |
| 初始显示 |
重绘 1 次 |
重绘 1 次 |
| 拖动窗口 5 次 |
重绘 5+ 次 |
仅重绘 1 次(缓冲区重建) |
| CPU 占用 |
高(持续绘图) |
低(仅 resize 后绘图一次) |
| 流畅度 |
卡顿 |
流畅 |
✅ WA_StaticContents + 双缓冲 = 性能飞跃
三、高级用法:动态内容 + 静态布局
有时,你的内容部分是静态的(如背景网格),部分是动态的(如移动的光标)。此时可结合使用:
void MyWidget::paintEvent(QPaintEvent *event)
{
// 静态背景:仅当尺寸变化时重绘
if(m_backgroundDirty){
redrawBackground();
m_backgroundDirty = false;
}
// 动态前景:每次都需要重绘
QPainter painter(this);
painter.drawPixmap(0,0, m_backgroundBuffer);
drawMovingCursor(painter); // 如鼠标位置、实时数据点
}
并在 resizeEvent 中标记:
void MyWidget::resizeEvent(QResizeEvent *event)
{
m_backgroundDirty = true;
QWidget::resizeEvent(event);
}
同时仍保留 WA_StaticContents = true,因为背景重绘由你控制,而非 Qt 自动触发。
四、重要限制与注意事项
4.1 适用场景
✅ 适合:
- 内容与窗口尺寸无关(如固定分辨率图像)
- 内容可缓存为 pixmap(双缓冲)
- 自行处理缩放/裁剪逻辑
❌ 不适合:
- 内容必须随窗口尺寸动态调整(如响应式布局)
- 使用
QGraphicsView(它有自己的优化机制)
- 需要精确像素对齐的矢量图形(可能需重新计算)
4.2 与其他属性的关系
| 属性 |
说明 |
Qt::WA_OpaquePaintEvent |
建议同时设置,避免绘制透明背景 |
Qt::WA_NoSystemBackground |
可进一步提升性能 |
Qt::WA_PaintOnScreen |
与 WA_StaticContents 冲突,勿混用 |
4.3 移动端与高 DPI
- 在高 DPI 屏幕上,
size() 返回的是设备无关像素;
- 确保
m_buffer 使用 devicePixelRatioF() 创建正确分辨率的 pixmap:
m_buffer = QPixmap(size()*devicePixelRatioF());
m_buffer.setDevicePixelRatio(devicePixelRatioF());
五、完整可运行示例:性能对比演示
// main.cpp
#include <QApplication>
#include "BadPaintWidget.h"
#include "GoodPaintWidget.h"
int main(int argc, char* argv[])
{
QApplication app(argc, argv);
// 对比两个窗口
BadPaintWidget bad;
bad.setWindowTitle("Without WA_StaticContents");
bad.resize(800,600);
bad.show();
GoodPaintWidget good;
good.setWindowTitle("With WA_StaticContents");
good.resize(800,600);
good.move(850,0);
good.show();
return app.exec();
}
运行后,同时拖动两个窗口,观察控制台输出和流畅度差异。
结语
Qt::WA_StaticContents 是 Qt 提供的一个轻量级但极其有效的性能优化开关。它通过将重绘控制权交还给开发者,避免了不必要的全屏刷新。
然而,它不是“银弹”。正确使用它需要你:
- 理解内容是否“静态”
- 配合双缓冲技术
- 手动管理 resize 后的重绘逻辑
当你在开发绘图软件、监控面板、科学可视化工具时,请记住:
“不要让 Qt 为你重绘,除非你真的需要。”
善用 WA_StaticContents,配合合理的缓冲区管理,能让你的自定义界面如丝般顺滑。如果你在 Qt 图形性能优化方面有更多心得,欢迎到 云栈社区 与更多开发者交流探讨。