解决 QGraphicsDropShadowEffect 导致的 UI 持续刷新
1. 问题描述
使用 QGraphicsDropShadowEffect 为无边框 QDialog 添加阴影,代码如下:
ShadowDialog::ShadowDialog(QWidget* parent): QDialog(parent), ui(new Ui::ShadowDialog){ui->setupUi(this);setWindowFlags(Qt::FramelessWindowHint | Qt::Dialog);setAttribute(Qt::WA_TranslucentBackground, true); //设置半透明背景// 设置阴影QGraphicsDropShadowEffect* shadow_effect = new QGraphicsDropShadowEffect(this);shadow_effect->setOffset(0, 0);shadow_effect->setColor(QColor(70, 70, 70, 184));shadow_effect->setBlurRadius(10);setGraphicsEffect(shadow_effect);……}
UI 设计及运行效果如下图所示:
那么当最上方的文本框获取焦点后,光标每闪烁一次,则对话框重绘一次。
2. 问题原因
文本框光标闪烁的定时事件触发文本框重绘,进而导致QGraphicsDropShadowEffect重绘,最终引起整个对话框的重绘。具体代码执行流程如下(部分关键流程):
2.1 鼠标光标闪烁定时事件
void QLineControl::timerEvent(QTimerEvent *event)
{if (event->timerId() == m_blinkTimer) {m_blinkStatus = !m_blinkStatus;emit updateNeeded(inputMask().isEmpty() ? cursorRect() : QRect());} ……
}
2.2 文本框更新
void QWidget::update(const QRect &rect)
{……if (hasBackingStoreSupport()) {QTLWExtra *tlwExtra = window()->d_func()->maybeTopData();if (tlwExtra && !tlwExtra->inTopLevelResize && tlwExtra->backingStore)tlwExtra->backingStore->markDirty(r, this);} else {d_func()->repaint_sys(r);}
}void QWidgetBackingStore::markDirty(const QRect &rect, QWidget *widget, bool updateImmediately,bool invalidateBuffer)
{
……
#ifndef QT_NO_GRAPHICSEFFECTwidget->d_func()->invalidateGraphicsEffectsRecursively();
#endif //QT_NO_GRAPHICSEFFECT
……
}
2.3 清理阴影设置
void QWidgetPrivate::invalidateGraphicsEffectsRecursively()
{Q_Q(QWidget);QWidget *w = q;do {if (w->graphicsEffect()) {QWidgetEffectSourcePrivate *sourced =static_cast<QWidgetEffectSourcePrivate *>(w->graphicsEffect()->source()->d_func());if (!sourced->updateDueToGraphicsEffect)w->graphicsEffect()->source()->d_func()->invalidateCache();}w = w->parentWidget();} while (w);
}void QGraphicsEffectSourcePrivate::invalidateCache(InvalidateReason reason) const
{……QPixmapCache::remove(m_cacheKey);
}
invalidateGraphicsEffectsRecursively()函数中循环查找父窗口的阴影设置并使阴影缓存无效,此处是导致重绘的关键!
2.4 发送重绘请求
// qbackingstore.cpp
static inline void sendUpdateRequest(QWidget *widget, bool updateImmediately)
{if (!widget)return;if (updateImmediately) {QEvent event(QEvent::UpdateRequest);QApplication::sendEvent(widget, &event);} else {QApplication::postEvent(widget, new QEvent(QEvent::UpdateRequest), Qt::LowEventPriority);}
}
2.5 QGraphicsDropShadowEffect重绘
void QGraphicsDropShadowEffect::draw(QPainter *painter)
{……// Draw pixmap in device coordinates to avoid pixmap scaling.QPoint offset;const QPixmap pixmap = sourcePixmap(Qt::DeviceCoordinates, &offset, mode);if (pixmap.isNull())return;……
}……QPixmap QWidgetEffectSourcePrivate::pixmap(Qt::CoordinateSystem system, QPoint *offset,QGraphicsEffect::PixmapPadMode mode) const
{……pixmapOffset -= effectRect.topLeft();QPixmap pixmap(effectRect.size());pixmap.fill(Qt::transparent);m_widget->render(&pixmap, pixmapOffset, QRegion(), QWidget::DrawChildren);return pixmap;
}
上述高亮代码里的 m_widget 为阴影所在窗口,即对话框窗口。
2.6 对话框重绘
void QWidget::render(QPaintDevice *target, const QPoint &targetOffset,const QRegion &sourceRegion, RenderFlags renderFlags)
{d_func()->render(target, targetOffset, sourceRegion, renderFlags, false);
}void QWidgetPrivate::render(QPaintDevice *target, const QPoint &targetOffset,const QRegion &sourceRegion, QWidget::RenderFlags renderFlags,bool readyToRender)
{……
#ifndef Q_WS_MAC// Render via backingstore.drawWidget(target, paintRegion, offset, flags, sharedPainter());// Restore shared painter.if (oldSharedPainter)setSharedPainter(oldSharedPainter);
#else// Render via backingstore (no shared painter).drawWidget(target, paintRegion, offset, flags, 0);
#endif
}
3. 解决方案
从代码执行流程可以发现问题的原因在于清理阴影设置引起的子窗口重绘,因此解决方案是让添加阴影的窗口没有子窗口。
创建一个对话框窗口的子窗口作为阴影窗口,阴影窗口大小与对话框窗口一致,位置与对话框窗口相同,即两个窗口重叠且把子窗口置于对话框窗口下层。代码如下:
class ShadowDialog : public QDialog
{
protected:void resizeEvent(QResizeEvent*);
private:QWidget* m_widgetShadow;
};ShadowDialog::ShadowDialog(QWidget* parent): QDialog(parent), ui(new Ui::ShadowDialog)
{ui->setupUi(this);//设置阴影QGraphicsDropShadowEffect* shadow_effect = new QGraphicsDropShadowEffect(this);shadow_effect->setOffset(0, 0);shadow_effect->setColor(QColor(70, 70, 70, 184));shadow_effect->setBlurRadius(10);m_widgetShadow = new QWidget(this);m_widgetShadow->setObjectName("widget_background");m_widgetShadow->lower();m_widgetShadow->setAttribute(Qt::WA_TransparentForMouseEvents);m_widgetShadow->setStyleSheet("background:white; border: 0px solid #ffffff");m_widgetShadow->setGraphicsEffect(shadow_effect);auto& margins = ui->verticalLayout->contentsMargins();m_widgetShadow->move(margins.left(), margins.top());……
}void ShadowDialog::resizeEvent(QResizeEvent* event)
{QDialog::resizeEvent(event);auto& margins = ui->verticalLayout->contentsMargins();auto size = event->size();size.setWidth(size.width() - margins.left() - margins.right());size.setHeight(size.height() - margins.top() - margins.bottom());m_widgetShadow->resize(size);
}