一直以为做一个按钮弹窗控件很简单,可做起来发现并不是那么顺利,折腾了挺长时间的,先看下效果:
前言
尝试过两种方案,方案一:使用QToolButton控件,我们可以自定义一个widget,然后setMenu设置为该widget;方案二:点击一个QPushButton然后show一个自定义widget。使用方案一的话各种鼠标事件不用我们管了,但是如果我们想要做的漂亮一些就会有很多局限性,不容易实现。上面的效果图采用的方案二实现的。
需要注意的几点
1.点击按钮弹出一个窗口。再次点击按钮窗口消失。
2.同时只能显示一个窗口。
3.窗口弹出时,点击主窗口其它位置,窗口消失(因为阴影效果的原因会有些小瑕疵,下面会介绍到)。
4.弹出窗口圆角并具有阴影效果。
源码分析
下面分析下源码中比较重要的几个点。
1.弹出的窗口需要设置下面这些属性:
setWindowFlags((Qt::Dialog | Qt::FramelessWindowHint | this->windowFlags()));
setAttribute(Qt::WA_TranslucentBackground);
setMouseTracking(true);
2.阴影效果:
m_pShadow = new QGraphicsDropShadowEffect(this);
m_pShadow->setOffset(0, 0);
m_pShadow->setColor(QColor("#cccccc"));
m_pShadow->setBlurRadius(10);
m_pMainWidget = new QWidget(this);
m_pCentralWidget = new QWidget(m_pMainWidget);
m_pCentralWidget->setStyleSheet("QWidget { border: none; }");
m_pCentralLayout = new QHBoxLayout(m_pMainWidget);
m_pCentralLayout->addWidget(m_pCentralWidget);
m_pMainWidget->setStyleSheet(" border: 1px solid lightgray; background-color: #fafafa; border-radius: 10px;");
m_pMainWidget->setGraphicsEffect(m_pShadow);
小瑕疵:这里实现阴影效果采用了一个QWidget嵌入到另一个QWidget(该widget是透明的),所以如果点击弹窗阴影附近(范围和被嵌入的widget的布局的margin大小有关)的时候弹窗是不会消失的。
3.按钮点击时需要将弹出widget的坐标映射到屏幕的坐标:
connect(this, &PopupWidgetButton::ButtonClicked, [=]() {
if (m_pMainWidget->isHidden()) {
QPoint pos;
if (m_orien == PWB::Horizontal) {
pos.setX(m_pButton->mapToGlobal(QPoint(0, 0)).x() + m_pButton->width());
pos.setY(m_pButton->mapToGlobal(QPoint(0, 0)).y() - 40);
} else if (m_orien == PWB::Vertical) {
pos.setX(m_pButton->mapToGlobal(QPoint(0, 0)).x() + m_pButton->width()/2 - m_pMainWidget->width()/2);
pos.setY(m_pButton->mapToGlobal(QPoint(0, 0)).y() + m_pButton->height());
}
// 同时只能显示一个 popupwidget
foreach (auto widget, m_pWidgets) {
if (widget != m_pMainWidget) {
widget->hide();
}
}
m_pMainWidget->move(pos);
m_pMainWidget->show();
} else {
m_pMainWidget->hide();
}
});
connect(this, &PopupWidgetButton::OthersClicked, m_pMainWidget, &PopupWidget::hide);
4.事件处理:
bool PopupWidgetButton::eventFilter(QObject *watched, QEvent *event)
{
if (event->type() == QEvent::MouseButtonRelease) {
QMouseEvent *e = static_cast<QMouseEvent *>(event);
QPoint buttonPoint = m_pButton->mapToGlobal(QPoint(0, 0));
// 需要获取主窗口的坐标
QPoint ePoint = e->pos() + MainWidget::getMainWidgetPos();
/**
* \warning
* 主窗口的事件先传递给它一个私有实现类QWidgetWindow,该类的objectName会自动加上“Window”
*/
if (watched->objectName() == "MainWidgetWindow") {
m_mainWidgetClicked = false;
// 点击按钮
if (ePoint.x() >= buttonPoint.x() && ePoint.x() <= buttonPoint.x() + m_pButton->width()
&& ePoint.y() >= buttonPoint.y() && ePoint.y() <= buttonPoint.y() + m_pButton->height()) {
emit ButtonClicked();
return true;
}
}
// 点击弹出的widget
else if (watched->objectName() == "PopupMainWidgetWindow") {
m_mainWidgetClicked = true;
return QWidget::eventFilter(watched, event);
}
if (!m_pMainWidget->isHidden() && !m_mainWidgetClicked) {
emit OthersClicked();
}
}
// 点击标题栏
else if (event->type() == QEvent::NonClientAreaMouseButtonRelease ||
event->type() == QEvent::NonClientAreaMouseButtonPress ||
event->type() == QEvent::NonClientAreaMouseButtonDblClick) {
emit OthersClicked();
}
return QObject::eventFilter(watched, event);
}
关于事件的处理多说两句。QWidget有一个私有实现类QWidgetWindow类先捕获到事件,然后再传递给QWidget。QWidgetWindow会有一个objectname,就是在QWidget的objectname的基础上加上window字符串。这些信息在我们写事件过滤器的时候会很有用。
代码下载
CSDN:
https://download.csdn.net/download/a844651990/10735129
github:
https://github.com/FlyWM/PopupWidgetButton
更新
2018.12.28更新说明:
- 去掉了主窗体initPos()和getMainWidgetPos方法,降低耦合性。
-
构造函数添加了主窗体参数指针参数。
github:
https://github.com/FlyWM/PopupWidgetButton