引言

前两课我们搭建了 QCommonStyle 子类骨架和 Theme 系统。现在要回答一个核心问题:Style 对象创建后,如何把自己的"意志"注入到所有 Widget 身上?

答案藏在两个武器里:

  1. polish()——Style 生命周期钩子,对每个 Widget 逐个"调校”
  2. 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));
}

这段代码做了四件事:

  1. 关闭系统背景WA_TranslucentBackground + WA_NoSystemBackground 让 QMenu 窗口变透明
  2. 去掉标题栏FramelessWindowHint 去掉窗口装饰
  3. 禁止系统投影NoDropShadowWindowHint 让你自己画阴影
  4. 注入 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