news 2026/5/16 2:22:52

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
重写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_H

DropDownWidget.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(); }

运行界面:

核心逻辑就三点:

  1. 在 mousePressEvent 里判断 popup 是否可见,可见就隐藏,不可见就显示
  2. 用 qApp->installEventFilter(this) 监听全局鼠标点击
  3. 在 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

关键点就两处:

  1. mousePressEvent 负责第一次点击弹出,再次点击隐藏。
  2. eventFilter 负责在菜单已经弹出时,拦截全局鼠标点击。
    如果点击的是当前 QWidget,自定义隐藏并直接消费这次点击,避免 QMenu 先关掉后又被重新弹出。
    如果点击的是 QWidget 和 QMenu 外部,也隐藏菜单。

第二次点击进入eventFilter后,为何没进入MenuDropDownWidget::mousePressEvent?

原因分两层,第一层是你这段代码自己把事件截断了,第二层是 QMenu 的弹出机制本身就会吞掉这次点击。

先看代码里的这一段:

if (selfRect().contains(globalPos)) { m_menu->hide(); return true; }

这里的return true是关键。

在 Qt 里事件分发顺序大致是:

  1. 原生鼠标事件到 QApplication
  2. 先经过安装在对象上的 eventFilter
  3. 如果没有被过滤,再分发给目标对象
  4. 目标 QWidget 才会进入mousePressEvent

所以第二次点击时,流程其实是:

  1. 你点击了 MenuDropDownWidget 所在区域
  2. QApplication 先调用你装在qApp上的eventFilter
  3. selfRect().contains(globalPos)为 true
  4. 你执行了m_menu->hide(); return true;
  5. 事件被认定为“已经处理完”,不会再继续发给 MenuDropDownWidget
  6. 所以自然不会进入MenuDropDownWidget::mousePressEvent

也就是说,这不是“没进到 mousePressEvent 的奇怪现象”,而是你主动在eventFilter里把它拦下来了。

再说第二层:即使你把这里改成return false,也不一定就能稳定进入mousePressEvent

因为你用的是QMenu::popup(),QMenu 本质上是Qt::Popup类型的弹出窗口。它显示期间有两个典型行为:

  1. 它会成为当前活动弹层
  2. 点击菜单外部时,QMenu 往往会先消费这次点击,用来关闭自己

所以第二次点击触发控件时,Qt 很可能先把这次点击当成“关闭 popup 的外部点击”处理掉,而不是老老实实再投递给下面那个 QWidget。这个是 QMenu 的默认交互模型,不是普通 QWidget 弹层那种行为。

所以你现在看到的现象,本质上是这两个原因叠加:

  1. 你的eventFilterreturn true,直接阻止了后续投递
  2. QMenu 作为 popup,本来也可能吞掉这次外部点击

如果你问“上述代码里为什么第二次点击进了 eventFilter,却没进 mousePressEvent”,最直接的答案就是:

因为eventFiltermousePressEvent更早执行,而你在eventFilter里返回了true,事件已经被消费,不会再传给MenuDropDownWidget::mousePressEvent

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/5/16 2:22:44

从零构建RAG系统:Embedder——让机器读懂文字的第一步

这个RAG系统是用来干什么的&#xff1f; 在正式拆解代码之前&#xff0c;先说清楚这个项目的背景。 这套 RAG 系统&#xff0c;是为内容创作场景而生的。它的核心任务只有一件事&#xff1a;检索爆款文章&#xff0c;辅助创作。 具体来说&#xff0c;系统会提前将大量爆款文章…

作者头像 李华
网站建设 2026/5/16 2:20:06

GitHub开源项目法律合规自动化:exoclaw-github的设计与实现

1. 项目概述&#xff1a;一个为GitHub仓库定制的“法律条款”守护者最近在开源社区里折腾&#xff0c;发现一个挺有意思的现象&#xff1a;很多开发者辛辛苦苦维护的项目&#xff0c;因为缺少清晰、合规的贡献者协议或开源许可证&#xff0c;导致后续在代码合并、版权归属甚至商…

作者头像 李华
网站建设 2026/5/16 2:09:04

Go语言实现HTTP代理核心原理与工程实践详解

1. 项目概述&#xff1a;一个Go语言实现的轻量级HTTP代理工具 最近在整理自己的工具箱时&#xff0c;翻到了一个挺有意思的旧项目——GoPaw。这是一个用Go语言编写的、结构非常清晰的HTTP代理服务器。它不像那些功能庞杂的“全家桶”&#xff0c;GoPaw的定位很明确&#xff1a;…

作者头像 李华
网站建设 2026/5/16 2:06:04

怎么给照片更换背景?2026年最实用的免费工具推荐

前几天&#xff0c;一个朋友问我怎么快速给证件照换底色&#xff0c;她说用了好几个app都不太满意&#xff0c;不是效果差就是操作复杂。我才意识到&#xff0c;虽然现在给照片更换背景的工具这么多&#xff0c;但真正好用的却没几个。今天就来分享一下我用过的、靠谱的解决方案…

作者头像 李华
网站建设 2026/5/16 2:05:05

DataChad:基于大语言模型的私有数据库智能查询助手部署指南

1. 项目概述&#xff1a;当你的数据对话伙伴DataChad如果你经常和数据打交道&#xff0c;无论是分析销售报表、研究用户行为&#xff0c;还是处理一堆杂乱无章的日志文件&#xff0c;你肯定幻想过有一个“懂行”的伙伴&#xff0c;能直接用大白话回答你的问题&#xff0c;而不是…

作者头像 李华