3、减少绘制开销:避免不必要的重绘、使用update()与repaint()的正确姿势、setAttribute(Qt::WA_OpaquePaintEvent)

绘制开销,说白了就是你的界面在「画」的时候有多费劲。我见过太多项目,功能没毛病,一跑起来界面就卡成PPT。查到最后,十有八九是绘制这块出了问题。

你想想看,Qt的绘制机制其实挺聪明的。它不会没事就重绘整个窗口,而是只画那些需要更新的区域。但问题是,很多开发者一不小心就触发了全量重绘。嗯,这里要好好聊聊。

3.1 避免不必要的重绘

先问个问题:你的窗口真的需要每帧都重绘吗?

我在项目中遇到过这样一个案例——一个实时数据显示面板,每秒刷新50次。结果CPU直接飙到80%。排查后发现,每次数据更新都调用了repaint(),导致整个面板全部重绘。其实只需要更新数值变化的那一小块区域。

常见的触发不必要重绘的场景:

  • 频繁调用repaint()或update(),但只有局部变化
  • 父窗口重绘导致所有子控件跟着重绘
  • QWidget的setVisible()、resize()等操作触发隐式重绘
  • 样式表频繁切换,导致整个控件树重新计算

核心原则:只重绘真正需要变化的部分。能用局部更新,就别动全局。

我个人习惯的做法是:先分析哪些区域是静态的,哪些是动态的。静态部分用QPixmap缓存起来,动态部分单独处理。这样CPU占用能降一大截。

3.2 update()与repaint()的正确姿势

这两个函数看起来差不多,但差别大了去了。我刚开始用Qt时也搞混过,后来踩了坑才明白。

特性 update() repaint()
触发方式 异步,合并到事件队列 同步,立即执行
性能 高效,支持合并 低效,频繁调用会卡顿
适用场景 绝大多数情况 极少数需要立即响应的场景
是否阻塞 不阻塞 阻塞,直到绘制完成

为什么update()更推荐?

因为它是异步的。你连续调用三次update(),Qt会把这些请求合并成一次重绘。而repaint()是同步的,调用一次就画一次,毫不留情。

我的建议:99%的情况下用update()就够了。只有在你需要立即看到绘制结果,比如动画的某一帧必须立刻显示时,才考虑repaint()。

我曾经在一个波形绘制工具里,用了repaint()来刷新每一条数据点。结果波形一多,界面直接卡死。改成update()后,流畅度提升了不止一个档次。

代码示例:

// ❌ 错误示范:频繁调用 repaint()
void DataWidget::onDataReceived(const QByteArray &data) {
    m_data.append(data);
    repaint();  // 每次数据到达都立即重绘
}

// ✅ 正确示范:使用 update() 合并重绘请求
void DataWidget::onDataReceived(const QByteArray &data) {
    m_data.append(data);
    update();   // 异步请求,Qt 会自动合并
}

// 如果需要局部更新,指定区域
void DataWidget::onPartialUpdate(int x, int y, int w, int h) {
    update(x, y, w, h);  // 只更新指定矩形区域
}

3.3 setAttribute(Qt::WA_OpaquePaintEvent)

这个属性,说实话,很多人不知道。但它是个宝贝。

Qt::WA_OpaquePaintEvent 的意思是:告诉Qt,你的控件完全不透明,不需要先擦除背景再绘制。说白了,就是跳过背景填充这一步。

为什么有用?

默认情况下,Qt在绘制控件前会先填充背景色。如果你的控件完全覆盖了背景(比如你画了一个矩形填满了整个区域),那这步擦除就是多余的。开启这个属性后,Qt会直接调用你的paintEvent(),省掉一次不必要的绘制操作。

注意:开启这个属性后,你的paintEvent()必须绘制控件的每一个像素。如果有遗漏,会出现奇怪的残留或闪烁。我曾经在一个自定义按钮上忘了画边框,结果按钮边缘一直留着上一帧的残影,排查了半天才发现。

使用场景:

  • 自定义绘制控件,且完全覆盖背景
  • 视频播放器、游戏画面等需要高性能绘制的场景
  • 使用OpenGL或QPainter绘制全屏内容的控件

代码示例:

// 在构造函数中启用
CustomWidget::CustomWidget(QWidget *parent) : QWidget(parent) {
    setAttribute(Qt::WA_OpaquePaintEvent);
}

// paintEvent 中必须绘制所有像素
void CustomWidget::paintEvent(QPaintEvent *event) {
    QPainter painter(this);
    
    // 先绘制背景(必须!)
    painter.fillRect(rect(), QColor(240, 240, 240));
    
    // 再绘制前景内容
    painter.setPen(Qt::blue);
    painter.drawText(rect(), Qt::AlignCenter, "Hello");
    
    // 注意:不能留任何未绘制的区域
}

性能提升效果:在我的一个项目中,开启WA_OpaquePaintEvent后,绘制时间从平均8ms降到了5ms。对于需要60fps刷新的界面来说,这3ms的差距就是流畅与卡顿的分界线。

3.4 知识体系总结

下面这张图,是我自己整理的核心逻辑。你看一遍就能明白这三者的关系。

减少绘制开销核心逻辑 绘制请求 update() 异步合并 repaint() 同步立即 合并到事件队列 立即执行绘制 paintEvent() 执行 WA_OpaquePaintEvent 跳过背景擦除 核心策略:优先使用 update(),配合 WA_OpaquePaintEvent 减少不必要绘制

这张图想表达的核心就三点:

  • 入口选择:能用update()就别用repaint(),异步合并是性能的关键
  • 绘制过程:开启WA_OpaquePaintEvent可以跳过背景擦除,但前提是你的paintEvent()必须画满所有像素
  • 最终目标:减少paintEvent()的调用次数和每次的执行时间

避坑指南:我曾经在一个项目里同时用了repaint()和WA_OpaquePaintEvent,结果因为repaint()太频繁,虽然跳过了背景擦除,但绘制次数太多,CPU照样扛不住。后来改成update() + 局部更新,才真正解决问题。

记住一句话:绘制优化的本质,不是让每次绘制更快,而是让不必要的绘制根本不发生。

可可精品资料库