重写QWidget,实现弹出下拉菜单的功能

发布时间:2026/5/16 2:23:11

重写QWidget,实现弹出下拉菜单的功能 一、基本需求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_castQMouseEvent *(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_castQMouseEvent *(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 pressedaboutToShowmenu is showevent filter, menu is visible and mouse pressedaboutToHideselfRect contains global pos, menu is hidemouse pressedaboutToShowmenu is showevent filter, menu is visible and mouse pressedaboutToHidemenu 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上的eventFilterselfRect().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。

相关新闻