一、基本需求
QT 重写QWidget,实现以下功能:1、点击弹出下拉菜单,再次点击隐藏下拉框;2、弹出下拉框后,点击QWidget外,隐藏下拉框。
二、实现代码
方法一:下拉菜单用QListWidget实现
DropDownWidget.h
#ifndef DROPDOWNWIDGET_H #define DROPDOWNWIDGET_H #include <qlistwidget.h> class DropDownWidget : public QWidget { Q_OBJECT public: explicit DropDownWidget(QWidget *parent = nullptr); ~DropDownWidget() override; protected: void mousePressEvent(QMouseEvent *event) override; bool eventFilter(QObject *watched, QEvent *event) override; void paintEvent(QPaintEvent *) override; private: void showPopup(); void hidePopup(); bool isInSelfOrPopup(const QPoint &globalPos) const; private: QListWidget *m_popupList = nullptr; }; #endif // DROPDOWNWIDGET_HDropDownWidget.cpp
#include "DropDownWidget.h" #include <qapplication.h> #include <qevent.h> #include <qpainter.h> DropDownWidget::DropDownWidget(QWidget *parent) : QWidget(parent), m_popupList(new QListWidget(nullptr)) { setFixedSize(180, 36); m_popupList->addItems({QStringLiteral("选项1"), QStringLiteral("选项2"), QStringLiteral("选项3")}); m_popupList->setWindowFlags(Qt::FramelessWindowHint | Qt::Tool); m_popupList->setFocusPolicy(Qt::NoFocus); m_popupList->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); m_popupList->setFixedWidth(width()); m_popupList->hide(); qApp->installEventFilter(this); connect(m_popupList, &QListWidget::itemClicked, this, [this](QListWidgetItem *) { hidePopup(); update(); }); } DropDownWidget::~DropDownWidget() { qApp->removeEventFilter(this); delete m_popupList; } void DropDownWidget::mousePressEvent(QMouseEvent *event) { if (event->button() == Qt::LeftButton) { if (m_popupList->isVisible()) { hidePopup(); } else { showPopup(); } update(); } QWidget::mousePressEvent(event); } bool DropDownWidget::eventFilter(QObject *watched, QEvent *event) { Q_UNUSED(watched); if (m_popupList->isVisible() && event->type() == QEvent::MouseButtonPress) { auto *mouseEvent = static_cast<QMouseEvent *>(event); #if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) const QPoint globalPos = mouseEvent->globalPosition().toPoint(); #else const QPoint globalPos = mouseEvent->globalPos(); #endif if (!isInSelfOrPopup(globalPos)) { hidePopup(); update(); } } return QWidget::eventFilter(watched, event); } void DropDownWidget::paintEvent(QPaintEvent *) { QPainter painter(this); painter.setRenderHint(QPainter::Antialiasing, true); painter.setPen(QColor("#C9CDD4")); painter.setBrush(Qt::white); painter.drawRoundedRect(rect().adjusted(0, 0, -1, -1), 6, 6); painter.setPen(QColor("#222222")); painter.drawText(rect().adjusted(12, 0, -30, 0), Qt::AlignVCenter | Qt::AlignLeft, QStringLiteral("点击展开下拉框")); QPolygon arrow; if (m_popupList->isVisible()) { arrow << QPoint(width() - 20, 22) << QPoint(width() - 12, 14) << QPoint(width() - 4, 22); } else { arrow << QPoint(width() - 20, 14) << QPoint(width() - 12, 22) << QPoint(width() - 4, 14); } painter.setPen(Qt::NoPen); painter.setBrush(QColor("#222222")); painter.drawPolygon(arrow); } void DropDownWidget::showPopup() { m_popupList->setFixedWidth(width()); m_popupList->move(mapToGlobal(QPoint(0, height()))); m_popupList->show(); m_popupList->raise(); } void DropDownWidget::hidePopup() { m_popupList->hide(); } bool DropDownWidget::isInSelfOrPopup(const QPoint &globalPos) const { const QRect selfRect(mapToGlobal(QPoint(0, 0)), size()); const QRect popupRect(m_popupList->pos(), m_popupList->size()); return selfRect.contains(globalPos) || popupRect.contains(globalPos); }main.cpp
#include "dialog.h" #include "DropDownWidget.h" #include <qboxlayout.h> #include <QApplication> int main(int argc, char *argv[]) { QApplication a(argc, argv); Dialog w; w.resize(400, 300); auto *layout = new QVBoxLayout(&w); layout->addSpacing(20); layout->addWidget(new DropDownWidget); layout->addStretch(); w.show(); return a.exec(); }运行界面:
核心逻辑就三点:
- 在 mousePressEvent 里判断 popup 是否可见,可见就隐藏,不可见就显示
- 用 qApp->installEventFilter(this) 监听全局鼠标点击
- 在 eventFilter 里判断点击位置是否在当前 QWidget 或下拉框区域外,如果在外面就隐藏
方法二:下拉菜单用QMenu实现
MenuDropDownWidget.h
#include <QApplication> #include <QMenu> class MenuDropDownWidget : public QWidget { public: explicit MenuDropDownWidget(QWidget *parent = nullptr); ~MenuDropDownWidget() override; protected: void mousePressEvent(QMouseEvent *event) override; bool eventFilter(QObject *watched, QEvent *event) override; void paintEvent(QPaintEvent *event) override; private: void showMenu(); QRect selfRect() const; private: QMenu *m_menu; QString m_currentText; };MenuDropDownWidget.cpp
#include "MenuDropDownWidget.h" #include <QAction> #include <QMouseEvent> #include <QPainter> #include <qdebug.h> MenuDropDownWidget::MenuDropDownWidget(QWidget *parent) : QWidget(parent), m_menu(new QMenu(this)), m_currentText(QStringLiteral("请选择")) { setFixedSize(180, 36); m_menu->addAction(QStringLiteral("选项1")); m_menu->addAction(QStringLiteral("选项2")); m_menu->addAction(QStringLiteral("选项3")); m_menu->setMinimumWidth(width()); connect(m_menu, &QMenu::triggered, this, [this](QAction *action) { if (action == nullptr) { return; } m_currentText = action->text(); update(); }); connect(m_menu, &QMenu::aboutToShow, this, [this]() { qDebug() << "aboutToShow"; update(); }); connect(m_menu, &QMenu::aboutToHide, this, [this]() { qDebug() << "aboutToHide"; update(); }); qApp->installEventFilter(this); } MenuDropDownWidget::~MenuDropDownWidget() { qApp->removeEventFilter(this); } void MenuDropDownWidget::mousePressEvent(QMouseEvent *event) { if (event->button() != Qt::LeftButton) { QWidget::mousePressEvent(event); return; } qDebug() << "mouse pressed"; if (m_menu->isVisible()) { m_menu->hide(); qDebug() << "menu is hide"; } else { showMenu(); qDebug() << "menu is show"; } event->accept(); } bool MenuDropDownWidget::eventFilter(QObject *watched, QEvent *event) { if (!m_menu->isVisible() || event->type() != QEvent::MouseButtonPress) { return QWidget::eventFilter(watched, event); } qDebug() << "event filter, menu is visible and mouse pressed"; Q_UNUSED(watched); auto *mouseEvent = static_cast<QMouseEvent *>(event); #if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) const QPoint globalPos = mouseEvent->globalPosition().toPoint(); #else const QPoint globalPos = mouseEvent->globalPos(); #endif if (selfRect().contains(globalPos)) { m_menu->hide(); qDebug() << "selfRect contains global pos, menu is hide"; return true; } if (!m_menu->geometry().contains(globalPos)) { m_menu->hide(); qDebug() << "menu geometry is not contains global pos, menu is hide"; } return QWidget::eventFilter(watched, event); } void MenuDropDownWidget::paintEvent(QPaintEvent *event) { Q_UNUSED(event); QPainter painter(this); painter.setRenderHint(QPainter::Antialiasing, true); painter.setPen(QColor("#C9CDD4")); painter.setBrush(Qt::white); painter.drawRoundedRect(rect().adjusted(0, 0, -1, -1), 6, 6); painter.setPen(QColor("#222222")); painter.drawText(rect().adjusted(12, 0, -30, 0), Qt::AlignVCenter | Qt::AlignLeft, m_currentText); QPolygon arrow; if (m_menu->isVisible()) { arrow << QPoint(width() - 20, 22) << QPoint(width() - 12, 14) << QPoint(width() - 4, 22); } else { arrow << QPoint(width() - 20, 14) << QPoint(width() - 12, 22) << QPoint(width() - 4, 14); } painter.setPen(Qt::NoPen); painter.setBrush(QColor("#222222")); painter.drawPolygon(arrow); } void MenuDropDownWidget::showMenu() { m_menu->setMinimumWidth(width()); m_menu->popup(mapToGlobal(QPoint(0, height()))); update(); } QRect MenuDropDownWidget::selfRect() const { return QRect(mapToGlobal(QPoint(0, 0)), size()); }main.cpp
#include "dialog.h" #include "MenuDropDownWidget.h" #include <qboxlayout.h> #include <QApplication> int main(int argc, char *argv[]) { QApplication app(argc, argv); QWidget window; window.resize(400, 300); auto *layout = new QVBoxLayout(&window); layout->addSpacing(40); layout->addWidget(new MenuDropDownWidget); layout->addStretch(); window.show(); return app.exec(); }运行界面:
点击3次MenuDropDownWidget,再在MenuDropDownWidget外点击一次,控制台打印的消息为:
mouse pressed
aboutToShow
menu is show
event filter, menu is visible and mouse pressed
aboutToHide
selfRect contains global pos, menu is hide
mouse pressed
aboutToShow
menu is show
event filter, menu is visible and mouse pressed
aboutToHide
menu geometry is not contains global pos, menu is hide
关键点就两处:
- mousePressEvent 负责第一次点击弹出,再次点击隐藏。
- eventFilter 负责在菜单已经弹出时,拦截全局鼠标点击。
如果点击的是当前 QWidget,自定义隐藏并直接消费这次点击,避免 QMenu 先关掉后又被重新弹出。
如果点击的是 QWidget 和 QMenu 外部,也隐藏菜单。
第二次点击进入eventFilter后,为何没进入MenuDropDownWidget::mousePressEvent?
原因分两层,第一层是你这段代码自己把事件截断了,第二层是 QMenu 的弹出机制本身就会吞掉这次点击。
先看代码里的这一段:
if (selfRect().contains(globalPos)) { m_menu->hide(); return true; }这里的return true是关键。
在 Qt 里事件分发顺序大致是:
- 原生鼠标事件到 QApplication
- 先经过安装在对象上的 eventFilter
- 如果没有被过滤,再分发给目标对象
- 目标 QWidget 才会进入
mousePressEvent
所以第二次点击时,流程其实是:
- 你点击了 MenuDropDownWidget 所在区域
- QApplication 先调用你装在
qApp上的eventFilter selfRect().contains(globalPos)为 true- 你执行了
m_menu->hide(); return true; - 事件被认定为“已经处理完”,不会再继续发给 MenuDropDownWidget
- 所以自然不会进入
MenuDropDownWidget::mousePressEvent
也就是说,这不是“没进到 mousePressEvent 的奇怪现象”,而是你主动在eventFilter里把它拦下来了。
再说第二层:即使你把这里改成return false,也不一定就能稳定进入mousePressEvent。
因为你用的是QMenu::popup(),QMenu 本质上是Qt::Popup类型的弹出窗口。它显示期间有两个典型行为:
- 它会成为当前活动弹层
- 点击菜单外部时,QMenu 往往会先消费这次点击,用来关闭自己
所以第二次点击触发控件时,Qt 很可能先把这次点击当成“关闭 popup 的外部点击”处理掉,而不是老老实实再投递给下面那个 QWidget。这个是 QMenu 的默认交互模型,不是普通 QWidget 弹层那种行为。
所以你现在看到的现象,本质上是这两个原因叠加:
- 你的
eventFilter里return true,直接阻止了后续投递 - QMenu 作为 popup,本来也可能吞掉这次外部点击
如果你问“上述代码里为什么第二次点击进了 eventFilter,却没进 mousePressEvent”,最直接的答案就是:
因为eventFilter比mousePressEvent更早执行,而你在eventFilter里返回了true,事件已经被消费,不会再传给MenuDropDownWidget::mousePressEvent。