引言
前两课我们搭建了 QCommonStyle 子类骨架和 Theme 系统。现在要回答一个核心问题:Style 对象创建后,如何把自己的"意志"注入到所有 Widget 身上?
答案藏在两个武器里:
polish()——Style 生命周期钩子,对每个 Widget 逐个"调校”- EventFilter——Qt 的事件拦截器,在事件抵达 Widget 之前截获并改写行为
这两者组合在一起,就是你控制 UI 所有细节的神经系统。本课以 qlementine 源码为蓝本,拆解完整链路。
1. QApplication::setStyle() 的完整流程
我们先从源头看起——当用户调用 QApplication::setStyle(new MyStyle) 时,Qt 内部到底做了什么。
1.1 调用链
// 用户代码
auto* style = new QlementineStyle;
QApplication::setStyle(style);
// Qt 内部 (qapplication.cpp) 简化的伪代码:
void QApplication::setStyle(QStyle* style) {
if (QStyle* old = this->style()) {
old->unpolish(this); // ① 撤销旧 style
}
// ... 换上新的 QStyle 指针 ...
style->polish(this); // ② 初始化新 style
// ...
}
当 polish(QApplication*) 被调用时,Qt 会自动递归对所有顶级窗口及其子控件调用 polish(QWidget*)。
1.2 qlementine 的实际实现
// QlementineStyle.cpp
void QlementineStyle::polish(QApplication* app) {
QCommonStyle::polish(app);
app->setFont(_impl->theme.fontRegular);
// 全局属性设置:菜单显示图标、显示快捷键
QCoreApplication::setAttribute(Qt::AA_DontShowIconsInMenus, false);
QCoreApplication::setAttribute(Qt::AA_DontShowShortcutsInContextMenus, false);
// 全局动效开关
QApplication::setEffectEnabled(Qt::UIEffect::UI_AnimateMenu, true);
QApplication::setEffectEnabled(Qt::UIEffect::UI_FadeMenu, true);
QApplication::setEffectEnabled(Qt::UIEffect::UI_AnimateCombo, true);
QApplication::setEffectEnabled(Qt::UIEffect::UI_AnimateTooltip, true);
QApplication::setEffectEnabled(Qt::UIEffect::UI_FadeTooltip, true);
}
void QlementineStyle::unpolish(QApplication* app) {
QCommonStyle::unpolish(app);
}
polish(QApplication*) 适合做全局级配置:应用字体、全局属性、动效开关。这是"对整个应用下指令"的地方。
1.3 polish(QWidget*) 的递归机制
Qt 在 setStyle() 结束后,会自动调用 polish(QWidget*) 遍历所有 Widget:
setStyle(newStyle)
├── oldStyle->unpolish(app) // 撤销旧样式
├── newStyle->polish(app) // 初始化新样式(全局配置)
└── Qt 内部遍历所有顶级窗口
├── newStyle->polish(topLevelWindow1)
│ ├── newStyle->polish(child1)
│ │ └── ... 递归所有子孙
│ └── ...
└── newStyle->polish(topLevelWindow2)
└── ...
关键要理解:polish(QWidget*) 对每个 Widget 都会被调用一次。包括:
- 顶级窗口(QMainWindow、QDialog 等)
- 布局中的每个控件
- Qt 内部创建的私有子控件(如 QLineEdit 内的清除按钮
QLineEditIconButton、QTabBar 的左右滚动按钮 QToolButton)
这也是为什么 polish(QWidget*) 内充满了 qobject_cast 类型判断——你对不同 Widget 做不同的事。
2. polish(QWidget*) 作为行为注入点
polish(QWidget*) 是 Style 对 Widget 的"出厂调校"时刻。qlementine 在这里做了大量精细控制。
2.1 设置 WA_Hover —— 启用鼠标悬停状态
Qt 默认不追踪鼠标悬停(WA_Hover),你需要在 polish() 中手动开启:
// qlementine 通过工具函数判断哪些 Widget 需要 hover
if (shouldHaveHoverEvents(w)) {
w->setAttribute(Qt::WA_Hover, true);
w->setAttribute(Qt::WA_OpaquePaintEvent, false); // 允许透明绘制(hover 渐变动画需要)
}
// 哪些需要 hover?按钮、Tab、菜单项、列表项……
bool shouldHaveHoverEvents(const QWidget* w) {
return qobject_cast<const QAbstractButton*>(w)
|| qobject_cast<const QComboBox*>(w)
|| qobject_cast<const QTabBar*>(w)
|| qobject_cast<const QAbstractSpinBox*>(w)
|| qobject_cast<const QAbstractSlider*>(w)
|| qobject_cast<const QHeaderView*>(w);
}
为什么重要:没有 WA_Hover,Qt 不会生成 State_MouseOver 标记,你的 drawControl() 就永远拿不到"鼠标悬停"状态——hover 效果无从谈起。
2.2 设置 WA_TranslucentBackground —— 透明菜单
QMenu 默认有系统背景,无法绘制圆角和阴影。qlementine 的处理:
if (auto* menu = qobject_cast<QMenu*>(w)) {
menu->setBackgroundRole(QPalette::NoRole);
menu->setAutoFillBackground(false);
menu->setAttribute(Qt::WA_TranslucentBackground, true);
menu->setAttribute(Qt::WA_OpaquePaintEvent, false);
menu->setAttribute(Qt::WA_NoSystemBackground, true);
menu->setWindowFlag(Qt::FramelessWindowHint, true);
menu->setWindowFlag(Qt::NoDropShadowWindowHint, true);
menu->setProperty("_q_windowsDropShadow", false);
// 安装 MenuEventFilter 处理阴影偏移
menu->installEventFilter(new MenuEventFilter(menu));
}
这段代码做了四件事:
- 关闭系统背景:
WA_TranslucentBackground+WA_NoSystemBackground让 QMenu 窗口变透明 - 去掉标题栏:
FramelessWindowHint去掉窗口装饰 - 禁止系统投影:
NoDropShadowWindowHint让你自己画阴影 - 注入 EventFilter:处理阴影偏移导致的菜单位置校正
QComboBox 的弹出框同理:
if (auto* itemView = qobject_cast<QAbstractItemView*>(w)) {
auto* popup = itemView->parentWidget();
if (popup && popup->inherits("QComboBoxPrivateContainer")) {
popup->setAttribute(Qt::WA_TranslucentBackground, true);
// ... 同上设置透明属性 ...
// 注入 ComboboxItemViewFilter 控制弹出框尺寸
new ComboboxItemViewFilter(comboBox, itemView);
}
}
2.3 调整字体 —— shouldHaveBoldFont 模式
某些控件需要粗体,qlementine 用类型判断:
if (shouldHaveBoldFont(w)) {
auto font = QFont{ w->font() };
font.setBold(true);
w->setFont(font);
}
// QGroupBox 标题、QCommandLinkButton 需要粗体
bool shouldHaveBoldFont(const QWidget* w) {
return qobject_cast<const QGroupBox*>(w)
|| qobject_cast<const QCommandLinkButton*>(w);
}
2.4 设置尺寸策略
防止某些控件被布局压缩:
// 确保 CheckBox、RadioButton 不会被 QFormLayout 压缩到最小
if (shouldNotBeVerticallyCompressed(w)) {
const auto minHeight = w->minimumHeight();
if (minHeight == 0 || minHeight == 1) {
const auto heightHint = w->sizeHint().height();
if (heightHint > 0) {
w->setMinimumHeight(w->sizeHint().height());
}
}
}
2.5 安装 EventFilters —— 核心控制手段
polish() 的最终武器:给特定控件安装 EventFilter。qlementine 安装了 7+ 种:
// LineEdit 的清除按钮
w->installEventFilter(new LineEditButtonEventFilter(this, anims, button));
// 外部焦点框
w->installEventFilter(new WidgetWithFocusFrameEventFilter(w));
// 菜单
menu->installEventFilter(new MenuEventFilter(menu));
// 滚轮拦截
w->installEventFilter(new MouseWheelBlockerEventFilter(w));
// TabBar 拖拽/右键
tabBar->installEventFilter(new TabBarEventFilter(tabBar));
// 文本编辑器事件
plainTextEdit->installEventFilter(new TextEditEventFilter(plainTextEdit));
textEdit->installEventFilter(new TextEditEventFilter(textEdit));
// LineEdit/SpinBox 右键菜单
lineEdit->installEventFilter(new LineEditMenuEventFilter(lineEdit));
3. EventFilter 机制深度解析
3.1 QObject::installEventFilter 的工作原理
// 安装
widget->installEventFilter(filter);
// filter 必须重写
bool eventFilter(QObject* watched, QEvent* event) override;
事件流:
系统事件 → QApplication::notify()
→ 检查 watched 的 eventFilter 列表
→ filter1->eventFilter(watched, event)
→ return true // 吃掉事件,不再传递给 widget
→ return false // 放行,继续检查下一个 filter
→ 如果没有 filter 拦截,调用 widget->event()
关键模式:Filter 通常在构造函数中持有对目标 Widget 的裸指针(QPointer),在 eventFilter() 中根据事件类型做判断,拦截特定事件并改写行为。
3.2 qlementine 的 6 种 Filter 类型详解
① LineEditButtonEventFilter —— 接管清除按钮的绘制和布局
Qt 的 QLineEdit 内部有一个私有类 QLineEditIconButton(清除按钮),它有自己的 paintEvent() 和 resizeEvent()。qlementine 通过 EventFilter 完全接管其绘制:
// 来自 eventFilters/LineEditButtonEventFilter.hpp
class LineEditButtonEventFilter : public QObject {
public:
LineEditButtonEventFilter(QlementineStyle* style, WidgetAnimationManager& anim, QToolButton* button)
: QObject(button), _style(style), _animManager(anim), _button(button) {}
bool eventFilter(QObject*, QEvent* evt) override {
switch (evt->type()) {
case QEvent::Resize:
evt->ignore(); // 阻止 Qt 内部的 resize 逻辑
return true;
case QEvent::Move: {
evt->ignore(); // 阻止 Qt 内部的 move 逻辑
// 自己计算按钮位置
const auto* moveEvent = static_cast<QMoveEvent*>(evt);
// ... 计算 buttonX, buttonY 根据 LTR/RTL ...
_button->setGeometry(buttonX, buttonY, buttonW, buttonH);
return true;
}
case QEvent::Paint: {
// 自己绘制按钮(圆形背景 + 图标)
QPainter p(_button);
p.setOpacity(opacity);
// 绘制圆形背景
p.setBrush(currentBgColor);
p.drawEllipse(circleRect);
// 绘制图标
p.drawPixmap(pixmapRect, colorizedPixmap);
evt->accept();
return true; // 已处理,Qt 不再绘制
}
}
return false;
}
};
要点:当 eventFilter 返回 true 时,Qt 不会再调用 widget 自己的事件处理器。这让你可以完全替换 Qt 的内部行为。
② MenuEventFilter —— 校正菜单弹出位置(阴影补偿)
因为 qlementine 的菜单有自己绘制的阴影(padding),菜单的实际显示区域比 QMenu 的几何尺寸要小。需要用 EventFilter 在 Show 事件时调整位置:
// 来自 eventFilters/MenuEventFilter.hpp
class MenuEventFilter : public QObject {
public:
explicit MenuEventFilter(QMenu* menu) : QObject(menu), _menu(menu) {}
bool eventFilter(QObject*, QEvent* evt) override {
switch (evt->type()) {
case QEvent::Type::Show: {
// 计算阴影偏移
const auto menuDropShadowWidth = qlementineStyle->theme().spacing;
const auto shadowTranslation = QPoint(-menuDropShadowWidth, -menuDropShadowWidth);
const auto menuNewPos = _menu->pos() + shadowTranslation;
// 延迟到事件循环完成后再设置位置(避免 Qt 内部的 sizing bug)
QTimer::singleShot(0, _menu, [this, menuNewPos]() {
_menu->move(menuNewPos);
});
break;
}
case QEvent::Type::MouseButtonPress: {
// 禁止点击分隔线、禁用项
const auto* mouseEvt = static_cast<QMouseEvent*>(evt);
if (const auto* action = _menu->actionAt(mouseEvt->pos())) {
if (action->isSeparator() || !action->isEnabled())
return true; // 吃掉事件
}
break;
}
case QEvent::Type::MouseButtonRelease: {
// 处理点击动画(flash 效果)后发送合成事件
flashAction(action, _menu, [this, action]() {
auto evt = new QMouseEvent(...);
QCoreApplication::sendEvent(_menu, evt);
});
return true;
}
}
return false;
}
};
Flash 动画技巧:点击菜单项时,先拦截 MouseButtonRelease 事件自己处理完动画,然后手动构造一个新的 QMouseEvent 通过 QCoreApplication::sendEvent() 发给 QMenu,让 Qt 理解为"用户又点击了一次”。这种「合成事件」是 EventFilter 的进阶用法。
③ TabBarEventFilter —— 自定义拖拽行为和滚轮切换
class TabBarEventFilter : public QObject {
public:
TabBarEventFilter(QTabBar* tabBar) : QObject(tabBar), _tabBar(tabBar) {
// 找到左右滚动按钮并配置
const auto toolButtons = tabBar->findChildren<QToolButton*>();
if (toolButtons.size() == 2) {
_leftButton = toolButtons.at(0);
_rightButton = toolButtons.at(1);
// 固定尺寸、禁用焦点、禁用自动图标着色
_leftButton->setFixedSize(_leftButton->sizeHint());
_rightButton->setFixedSize(_rightButton->sizeHint());
}
}
bool eventFilter(QObject*, QEvent* evt) override {
if (evt->type() == QEvent::MouseButtonRelease) {
const auto* me = static_cast<QMouseEvent*>(evt);
if (me->button() == Qt::MiddleButton) {
// 中键关闭 Tab
const auto idx = _tabBar->tabAt(me->pos());
if (idx != -1) Q_EMIT _tabBar->tabCloseRequested(idx);
return true;
} else if (me->button() == Qt::RightButton) {
// 右键弹出上下文菜单
const auto idx = _tabBar->tabAt(me->pos());
if (idx != -1) Q_EMIT _tabBar->customContextMenuRequested(me->pos());
return true;
}
} else if (evt->type() == QEvent::Wheel) {
const auto* we = static_cast<QWheelEvent*>(evt);
// 只响应水平滚轮
if (qAbs(we->angleDelta().y()) > qAbs(we->angleDelta().x())) {
evt->ignore();
return true;
}
// 根据滚轮方向点击左右按钮
if (delta > 0) _rightButton->click();
else _leftButton->click();
return true;
}
return false;
}
};
技巧:用 findChildren<QToolButton*>() 拿到 Qt 内部的私有按钮(只有 2 个),然后通过手动 click() 来驱动滚动。
④ ComboboxItemViewFilter —— 下拉列表尺寸自适应
QComboBox 的弹出框尺寸由 Qt 内部控制,没有公开 API。qlementine 用 EventFilter 接管 Show 事件:
class ComboboxItemViewFilter : public QObject {
void fixViewGeometry() const {
if (auto* view = _comboBox->view()) {
// 计算宽度:取 comboBox 宽度、内容宽度、最小宽度的最大值
const auto width = std::min(absoluteMaxWidth, std::max({
_comboBox->width(),
view->sizeHintForColumn(0),
absoluteMinWidth,
})) + shadowWidth * 2 + hMargin * 2 + borderWidth * 2;
// 计算高度:不超出屏幕
const auto screenHeight = screen->geometry().height();
const auto height = std::min(absoluteMaxHeight,
std::max(absoluteMinHeight, viewMinimumSizeHint().height()));
view->setFixedWidth(width);
view->setFixedHeight(height);
}
}
bool eventFilter(QObject* watched, QEvent* evt) override {
if (evt->type() == QEvent::Type::Show) fixViewGeometry();
if (evt->type() == QEvent::Type::Resize && watched == _comboBox) fixViewGeometry();
return false;
}
};
⑤ MouseWheelBlockerEventFilter —— 阻止无焦点控件的滚轮
一个常见的用户体验问题:鼠标悬停在 QSpinBox 上但焦点在别处,滚动滚轮时 SpinBox 值会变。这会令人困惑。解决:
class MouseWheelBlockerEventFilter : public QObject {
public:
explicit MouseWheelBlockerEventFilter(QWidget* widget) : QObject(widget), _widget(widget) {}
bool eventFilter(QObject*, QEvent* evt) override {
if (evt->type() == QEvent::Wheel && !_widget->hasFocus()) {
evt->ignore(); // 标记为忽略(Qt 仍可能处理,但至少不传播)
return true; // 拦截事件
}
return false;
}
};
Quirk:evt->ignore() + return true 的组合。ignore() 标记事件未被接受,return true 阻止事件继续分发给 Widget。两者配合才能彻底阻止。
⑥ WidgetWithFocusFrameEventFilter —— 绘制外部焦点框
某些控件需要在控件边界之外绘制焦点指示(如圆角按钮的焦点环)。Qt 的 QFocusFrame 可以实现,但需要精确控制创建时机:
class WidgetWithFocusFrameEventFilter : public QObject {
public:
explicit WidgetWithFocusFrameEventFilter(QWidget* widget) : QObject(widget), _widget(widget) {
_focusFrame = new QFocusFrame(_widget); // 提前创建
}
bool eventFilter(QObject* watched, QEvent* evt) override {
if (evt->type() == QEvent::Paint && !_added) {
// 延迟到第一次 Paint 之后才关联(确保父控件已就绪)
QTimer::singleShot(0, this, [this]() {
if (!_added) {
_added = true;
_focusFrame->setWidget(_widget);
}
});
} else if (evt->type() == QEvent::Show && _added) {
// Show 时重新关联(处理 reparent)
_focusFrame->setWidget(nullptr);
_focusFrame->setWidget(_widget);
}
return false;
}
};
为什么延迟:如果在构造函数中立即 setWidget(),父控件的布局可能还没完成。等到第一次 QEvent::Paint 再用 QTimer::singleShot(0) 延迟到下一个事件循环,是最安全的时机。
4. Delegate 系统 —— 接管 Item 绘制
EventFilter 处理事件,但 Item 的绘制(列表项、表格单元格、下拉菜单项)由 Delegate 负责。
4.1 QItemDelegate vs QStyledItemDelegate
| QItemDelegate | QStyledItemDelegate | |
|---|---|---|
| 绘制方式 | 直接调用 QStyle 的绘制方法 | 自己绘制,再让 Style 绘制 |
| 与 Style 耦合 | 紧耦合(使用当前 style) | 松耦合(可独立) |
| Qt 默认 | Qt4 默认 | Qt5+ 默认 |
qlementine 的 ComboBoxDelegate 继承自 QItemDelegate,这意味着它可以调用当前 Style 的方法来保持视觉一致。
4.2 ComboBoxDelegate 的完整实现
// 来自 Delegates.cpp
class ComboBoxDelegate : public QItemDelegate {
public:
ComboBoxDelegate(QWidget* widget, QlementineStyle& style)
: QItemDelegate(widget), _widget(widget), _qlementineStyle(&style) {}
void paint(QPainter* p, const QStyleOptionViewItem& opt, const QModelIndex& idx) const override {
const auto& theme = _qlementineStyle->theme();
// 分隔线处理
const auto isSeparator = idx.data(Qt::AccessibleDescriptionRole).toString() == "separator";
if (isSeparator) {
// 画水平分隔线
p->setPen(QPen(color, theme.borderWidth));
p->drawLine(QPointF(x, y1), QPointF(x, y2));
return;
}
// 计算鼠标状态
const auto mouse = getComboBoxItemMouseState(opt.state);
// ① 绘制背景(圆角矩形,hover 高亮)
const auto& bgColor = _qlementineStyle->menuItemBackgroundColor(mouse);
p->setPen(Qt::NoPen);
p->setBrush(bgColor);
p->drawRoundedRect(bgRect, radius, radius);
// ② 绘制图标(支持自动着色)
if (!icon.isNull()) {
const auto pixmap = getPixmap(icon, iconSize, mouse, ...);
const auto& colorizedPixmap = _qlementineStyle->getColorizedPixmap(pixmap, ...);
p->drawPixmap(pixmapRect, colorizedPixmap);
}
// ③ 绘制文本(elide 截断)
const auto elidedText = fm.elidedText(text, Qt::ElideRight, availableW);
p->setPen(fgColor);
p->drawText(textRect, Qt::AlignVCenter | Qt::AlignLeft, elidedText);
// ④ TreeView 模式下绘制展开/折叠箭头
if (viewIsTreeView && opt.state.testFlag(QStyle::State_Selected)) {
// drawArrowDown 或 drawArrowRight
}
}
QSize sizeHint(const QStyleOptionViewItem& opt, const QModelIndex& idx) const override {
// 计算项的高度和宽度
const auto w = contentMargin * 2 + hPadding + iconSize + textW + hPadding;
const auto h = std::max(theme.controlHeightMedium, iconSize + vPadding);
return QSize(w, h);
}
};
4.3 安装 Delegate 的时机
Delegate 在 polish() 中安装:
if (auto* comboBox = qobject_cast<QComboBox*>(w)) {
// 只替换默认 Delegate,保留用户自定义的
if (isDefaultItemDelegate(comboBox->itemDelegate())) {
comboBox->setItemDelegate(new ComboBoxDelegate(comboBox, *this));
// 监听 View 变化(QComboBox 可能在运行时更换 view)
new ComboboxFilter(comboBox);
}
}
ComboboxFilter 监听 QEvent::ChildAdded,当 QComboBox 更换其内部 View 时,重新安装 Delegate。
5. 代码实战:透明菜单 + Hover 按钮
现在我们把知识串起来,写一个可运行的 demo。
5.1 MenuEventFilter —— 让 QMenu 背景透明
// menu_event_filter.h
#pragma once
#include <QObject>
#include <QEvent>
#include <QMenu>
#include <QTimer>
class MenuEventFilter : public QObject {
Q_OBJECT
public:
explicit MenuEventFilter(QMenu* menu)
: QObject(menu), _menu(menu) {}
protected:
bool eventFilter(QObject*, QEvent* evt) override {
if (evt->type() == QEvent::Show) {
// 菜单弹出时,补偿阴影偏移
constexpr int shadowWidth = 12; // 阴影宽度
const auto originalPos = _menu->pos();
const auto adjustedPos = originalPos + QPoint(-shadowWidth, -shadowWidth);
// 延迟设置位置(避免 Qt 内部 sizing bug)
_menu->move(adjustedPos);
}
return false;
}
private:
QMenu* _menu{nullptr};
};
5.2 自定义 Style —— polish() 中安装 Filter
// my_style.h
#pragma once
#include <QCommonStyle>
#include <QMenu>
#include <QPushButton>
class MyStyle : public QCommonStyle {
Q_OBJECT
public:
using QCommonStyle::QCommonStyle;
void polish(QWidget* w) override {
if (!w) return;
QCommonStyle::polish(w);
// ① 为 QPushButton 启用 hover 追踪
if (qobject_cast<QPushButton*>(w)) {
w->setAttribute(Qt::WA_Hover, true);
}
// ② 为 QMenu 设置透明背景 + 安装位置校正 Filter
if (auto* menu = qobject_cast<QMenu*>(w)) {
menu->setAttribute(Qt::WA_TranslucentBackground, true);
menu->setAutoFillBackground(false);
menu->setWindowFlag(Qt::FramelessWindowHint, true);
menu->setWindowFlag(Qt::NoDropShadowWindowHint, true);
menu->installEventFilter(new MenuEventFilter(menu));
}
}
void unpolish(QWidget* w) override {
if (!w) return;
if (qobject_cast<QPushButton*>(w)) {
w->setAttribute(Qt::WA_Hover, false);
}
QCommonStyle::unpolish(w);
}
// 绘制菜单背景(带圆角和阴影)
void drawPrimitive(PrimitiveElement pe, const QStyleOption* opt,
QPainter* p, const QWidget* w) const override {
if (pe == PE_PanelMenu) {
constexpr int radius = 8;
constexpr int shadowPadding = 12;
const auto totalRect = opt->rect;
const auto frameRect = totalRect.adjusted(
shadowPadding, shadowPadding, -shadowPadding, -shadowPadding);
p->save();
p->setRenderHint(QPainter::Antialiasing, true);
// 绘制阴影(简单实现:多层半透明矩形)
// 生产代码中应使用 QGraphicsDropShadowEffect 或 Pixmap 模糊
for (int i = shadowPadding; i > 0; --i) {
const auto alpha = static_cast<int>(15.0 * (1.0 - static_cast<double>(i) / shadowPadding));
p->setPen(Qt::NoPen);
p->setBrush(QColor(0, 0, 0, alpha));
const auto shadowRect = frameRect.adjusted(-i, -i, i, i);
p->drawRoundedRect(shadowRect, radius + i / 2.0, radius + i / 2.0);
}
// 绘制白色背景
p->setPen(QPen(QColor(200, 200, 200), 1));
p->setBrush(QColor(255, 255, 255));
p->drawRoundedRect(frameRect, radius, radius);
p->restore();
return;
}
QCommonStyle::drawPrimitive(pe, opt, p, w);
}
// 绘制按钮 hover 状态
void drawControl(ControlElement ce, const QStyleOption* opt,
QPainter* p, const QWidget* w) const override {
if (ce == CE_PushButtonBevel) {
if (const auto* btn = qstyleoption_cast<const QStyleOptionButton*>(opt)) {
const bool hovered = btn->state & State_MouseOver;
const bool pressed = btn->state & State_Sunken;
QColor bgColor = QColor(225, 225, 225); // 默认
if (pressed) bgColor = QColor(180, 180, 180);
else if (hovered) bgColor = QColor(210, 210, 210);
p->save();
p->setRenderHint(QPainter::Antialiasing, true);
p->setPen(QPen(QColor(170, 170, 170), 1));
p->setBrush(bgColor);
p->drawRoundedRect(btn->rect.adjusted(0, 0, -1, -1), 4, 4);
p->restore();
return;
}
}
QCommonStyle::drawControl(ce, opt, p, w);
}
};
5.3 完整可运行的 Demo
// main.cpp
#include <QApplication>
#include <QMainWindow>
#include <QMenuBar>
#include <QMenu>
#include <QPushButton>
#include <QVBoxLayout>
#include <QLabel>
#include "my_style.h"
int main(int argc, char* argv[]) {
QApplication app(argc, argv);
// 安装自定义 Style
app.setStyle(new MyStyle);
QMainWindow window;
window.setWindowTitle("polish & EventFilter Demo");
// 创建菜单栏
auto* menuBar = window.menuBar();
auto* fileMenu = menuBar->addMenu("文件");
fileMenu->addAction("新建");
fileMenu->addAction("打开");
fileMenu->addSeparator();
fileMenu->addAction("退出");
auto* editMenu = menuBar->addMenu("编辑");
editMenu->addAction("撤销");
editMenu->addAction("重做");
// 中心控件
auto* central = new QWidget;
auto* layout = new QVBoxLayout(central);
auto* label = new QLabel("将鼠标悬停在按钮上查看 hover 效果");
label->setAlignment(Qt::AlignCenter);
layout->addWidget(label);
auto* button = new QPushButton("点击我");
layout->addWidget(button);
auto* statusLabel = new QLabel;
QObject::connect(button, &QPushButton::clicked, [&]() {
statusLabel->setText("按钮被点击了!✓");
});
layout->addWidget(statusLabel);
window.setCentralWidget(central);
window.resize(400, 250);
window.show();
return app.exec();
}
编译运行:
# CMakeLists.txt
cmake_minimum_required(VERSION 3.16)
project(MenuDemo)
find_package(Qt6 REQUIRED COMPONENTS Widgets)
qt_add_executable(menu-demo
main.cpp
)
target_link_libraries(menu-demo PRIVATE Qt6::Widgets)
cmake -B build && cmake --build build
./build/menu-demo
效果:
- 按钮有 hover 和 pressed 颜色变化
- 菜单有圆角、阴影,背景透明
- 菜单位置正确(补偿阴影偏移)
5.4 进阶:用 QTimer::singleShot 处理复杂时序
在 MenuEventFilter 的真实实现中,有一行关键的延迟:
QTimer::singleShot(0, _menu, [this, menuNewPos, menuSize]() {
_menu->move(menuNewPos);
_menu->resize(menuSize);
});
为什么需要延迟:在 QEvent::Show 阶段,QMenu 的内部布局还没完成。直接调用 move() 可能导致 Qt 在后续布局计算中把你的位置覆盖掉。QTimer::singleShot(0, ...) 相当于 schedule for next event loop iteration,此时布局已经稳定。
同理,在 WidgetWithFocusFrameEventFilter 中,焦点框的 setWidget 也延迟到第一次 Paint 之后。
6. 总结
| 机制 | 作用 | 时机 |
|---|---|---|
polish(QApplication*) |
全局配置(字体、属性、动效开关) | setStyle 时一次 |
polish(QWidget*) |
逐控件"调校”(WA_属性、字体、Filter 安装) | setStyle 时递归 |
unpolish(QWidget*) |
撤销 polish 的修改 | 换 Style 时 |
EventFilter |
拦截事件,改写行为 | 安装在 polish 中,持续生效 |
QItemDelegate |
接管 Item 绘制逻辑 | 在 polish 中 setItemDelegate |
QTimer::singleShot(0,...) |
延迟到下个事件循环 | 需要等 Qt 内部布局完成时 |
一条完整的控制链:
QApplication::setStyle(MyStyle)
└─ MyStyle::polish(app)
└─ Qt 递归调用 MyStyle::polish(widget) 对每个 Widget
├─ setAttribute(WA_Hover) → 启用 hover 状态
├─ setAttribute(WA_Translucent) → 透明背景
├─ setFont(bold) → 调整字体
├─ installEventFilter(menuFilter) → 注入菜单位置补偿
├─ installEventFilter(hoverFilter)→ 注入 hover 行为
├─ setItemDelegate(myDelegate) → 接管 Item 绘制
└─ ...
下节预告:有了 polish 注入的控制能力,下一课我们深入 drawPrimitive() 和 drawControl() 的绘制引擎,用几何工具函数画出完整的按钮、菜单、滚动条。
参考文件(对应 qlementine 源码):
| 文件 | 内容 |
|---|---|
QlementineStyle.cpp:4735-4940 |
polish()/unpolish() 全部实现 |
eventFilters/LineEditButtonEventFilter.hpp |
清除按钮接管 Filter |
eventFilters/MenuEventFilter.hpp |
菜单位置校正 Filter |
eventFilters/TabBarEventFilter.hpp |
TabBar 中键/右键/滚轮 Filter |
eventFilters/ComboboxItemViewFilter.hpp |
ComboBox 弹出框尺寸 Filter |
eventFilters/MouseWheelBlockerEventFilter.hpp |
滚轮拦截 Filter |
eventFilters/WidgetWithFocusFrameEventFilter.hpp |
外部焦点框 Filter |
Delegates.cpp |
ComboBoxDelegate 完整绘制 |
EventFilters.hpp |
所有 Filter 的统一 include |