0.前言
Qt提供了一个QComboBox下拉框组件,但是对于一些自定义样式的需求实现起来并不方便,很多东西还得去倒腾源码,还不如直接用基础的组件自己来实现一个下拉框。不过,自己组合的组件要做的细节太多了,所以我只在一些定制化程度高的需求才使用这种方式。
1.实现思路与问题
首先是下拉框的文本框和按钮,我使用QLineEdit+QPushButton;然后弹出框我分为两部分,一是弹出框Widget容器,二是弹出框中的内容为了后续的自定义抽象了一个简单的基类,使用的时候继承基类实现自己的弹出框样式即可。剩下的就是模拟一些QComboBox的接口和效果了。
遇到的问题一,弹出框QWidget子类设置为Popup后,没法设置背景半透明。后来发现可以给要展示的Widget的parent或者更上层的parent设置透明属性,这样展示的Widget就可以半透明了。
//背景透明FramelessWindowHint+WA_TranslucentBackground
//这样才能给上面的组件设置透明色
setWindowFlags(Qt::Popup|Qt::FramelessWindowHint);
setAttribute(Qt::WA_TranslucentBackground);
遇到的问题二, 弹出后点击标题栏拖动弹框不会消失。开始我以为是焦点的问题,但是设置后也没效果。后来发现因为我弹出动画高度是从0开始,设置成从1开始就没问题了。
resize(width,1);
animation->setStartValue(QSize(width,1));
待实现:弹出框的方向目前直接往下的,应该判断下位置来决定向上还是向下。
2.实现代码
完整代码链接(CuteBasicComboBox类):https://github.com/gongjianbo/QtWidgetsComponent
部分实现(因为有好几个组成部分,所以只贴了主体类):
#pragma once
#include <QWidget>
#include "CuteComponentExport.h"
#include "CuteBasicComboContainer.h"
#include "CuteBasicComboModel.h"
#include "CuteBasicComboPopup.h"
#include "CuteBasicComboView.h"
class QLineEdit;
class QPushButton;
class QHBoxLayout;
class QTimer;
/**
* @brief 使用 QLineEdit + QPushButton + 弹出框组合的下拉框
* @author 龚建波
* @date 2020-7-6
* @history
* [2020-7-7]
* 重构弹出框部分,增加可扩展性
* 基础组件:Box+Popup,继承 Container 实现接口后设置给 Popup
*/
class Cute_API CuteBasicComboBox : public QWidget
{
Q_OBJECT
Q_PROPERTY(int borderWidth READ getBorderWidth WRITE setBorderWidth NOTIFY borderWidthChanged)
Q_PROPERTY(bool popupVisible READ getPopupVisible WRITE setPopupVisible NOTIFY popupVisibleChanged)
public:
explicit CuteBasicComboBox(QWidget *parent = nullptr);
~CuteBasicComboBox();
// 边框 size,自定义属性方便 qss 设置
int getBorderWidth() const;
void setBorderWidth(int px);
// 弹框可见
bool getPopupVisible() const;
void setPopupVisible(bool visible);
// 当前行
int getCurrentIndex() const;
void setCurrentIndex(int index);
// 当前文本
QString getCurrentText() const;
void setCurrentText(const QString &text);
// 数据项
QList<QString> getItems() const;
void setItems(const QList<QString> &items);
// 弹出框
CuteBasicComboPopup *getPopup() const;
protected:
// 过滤组件事件
bool eventFilter(QObject *watched, QEvent *event) override;
private:
// 初始化组件设置
void initComponent();
// popup-container
void initContainer();
// 编辑后查询对应行并设置 model
void checkTextRow();
signals:
void borderWidthChanged(int px);
void popupVisibleChanged(bool visible);
void currentIndexChanged(int index);
void currentTextChanged(const QString text);
private:
// 边框
int borderWidth{ 3 };
// 弹框当前可见
bool popupVisible{ false };
// 文本框
QLineEdit *boxEdit{ nullptr };
// 下拉按钮
QPushButton *boxDown{ nullptr };
// 布局
QHBoxLayout *boxLayout{ nullptr };
// 弹框
CuteBasicComboPopup *boxPop{ nullptr };
// 定时器
QTimer *editTimer{ nullptr };
};
#include "CuteBasicComboBox.h"
#include <QLineEdit>
#include <QPushButton>
#include <QHBoxLayout>
#include <QTimer>
#include <QDesktopWidget>
#include <QKeyEvent>
#include <QFocusEvent>
#include <QMouseEvent>
#include <QDebug>
CuteBasicComboBox::CuteBasicComboBox(QWidget *parent)
: QWidget{parent}
, boxEdit{new QLineEdit(this)}
, boxDown{new QPushButton(this)}
, boxLayout{new QHBoxLayout(this)}
, boxPop{new CuteBasicComboPopup(this)}
, editTimer{new QTimer(this)}
{
// 支持样式表
setAttribute(Qt::WA_StyledBackground);
setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Fixed);
initComponent();
}
CuteBasicComboBox::~CuteBasicComboBox()
{
boxPop->hidePopup();
}
int CuteBasicComboBox::getBorderWidth() const
{
return borderWidth;
}
void CuteBasicComboBox::setBorderWidth(int px)
{
if (borderWidth == px)
return;
borderWidth = px;
boxLayout->setContentsMargins(borderWidth, borderWidth, borderWidth, borderWidth);
emit borderWidthChanged(borderWidth);
}
bool CuteBasicComboBox::getPopupVisible() const
{
return popupVisible;
}
void CuteBasicComboBox::setPopupVisible(bool visible)
{
// qDebug() << __FUNCTION__ << popupVisible << visible;
if (popupVisible == visible)
return;
popupVisible = visible;
emit popupVisibleChanged(popupVisible);
}
int CuteBasicComboBox::getCurrentIndex() const
{
if (boxPop->getContainer()) {
return boxPop->getContainer()->getCurrentIndex();
}
return -1;
}
void CuteBasicComboBox::setCurrentIndex(int index)
{
if (boxPop->getContainer()) {
boxPop->getContainer()->setCurrentIndex(index);
}
}
QString CuteBasicComboBox::getCurrentText() const
{
return boxEdit->text();
}
void CuteBasicComboBox::setCurrentText(const QString &text)
{
editTimer->stop();
boxEdit->setText(text);
}
QList<QString> CuteBasicComboBox::getItems() const
{
if (boxPop->getContainer()) {
return boxPop->getContainer()->getItems();
}
return QList<QString>();
}
void CuteBasicComboBox::setItems(const QList<QString> &items)
{
if (boxPop->getContainer()) {
boxPop->getContainer()->setItems(items);
}
}
CuteBasicComboPopup *CuteBasicComboBox::getPopup() const
{
return boxPop;
}
bool CuteBasicComboBox::eventFilter(QObject *watched, QEvent *event)
{
if (watched == boxEdit) {
// 过滤编辑框事件
switch (event->type()) {
case QEvent::KeyRelease:
{
if (boxPop->getContainer()) {
// 这里只考虑了edit可编辑的情况
QKeyEvent *key_event = static_cast<QKeyEvent*>(event);
if (key_event->key() == Qt::Key_Up) {
setCurrentText(boxPop->getContainer()->getPrevText());
} else if(key_event->key() == Qt::Key_Down) {
setCurrentText(boxPop->getContainer()->getNextText());
}
}
}
break;
case QEvent::FocusAboutToChange:
case QEvent::FocusIn:
{
// 获得焦点时全选
QFocusEvent *focus_event = static_cast<QFocusEvent*>(event);
if (focus_event->gotFocus()) {
QTimer::singleShot(20, boxEdit, &QLineEdit::selectAll);
}
}
break;
default:
break;
}
} else if(watched == boxDown) {
// 过滤按钮事件
} else if(watched == boxPop) {
// 过滤弹框事件
}
return false;
}
void CuteBasicComboBox::initComponent()
{
// 按钮设置
boxDown->setObjectName("down");
boxDown->setFocusPolicy(Qt::NoFocus);
boxDown->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Preferred);
boxDown->installEventFilter(this);
// 编辑框设置
boxEdit->setObjectName("edit");
boxEdit->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
boxEdit->installEventFilter(this);
// index 和 text 两个属性可以看成独立的
connect(boxEdit, &QLineEdit::textChanged, this, &CuteBasicComboBox::currentTextChanged);
// 编辑时,延迟一会儿进行查询-currentindex
connect(boxEdit, &QLineEdit::textEdited, [this] {
editTimer->start(300);
});
// 编辑结束,进行查询-currentindex
connect(boxEdit, &QLineEdit::editingFinished, this, &CuteBasicComboBox::checkTextRow);
editTimer->setSingleShot(true);
connect(editTimer, &QTimer::timeout, this, &CuteBasicComboBox::checkTextRow);
// 弹框容器设置
boxPop->attachTarget(this);
boxPop->installEventFilter(this);
initContainer();
connect(boxPop, &CuteBasicComboPopup::containerChanged, this, &CuteBasicComboBox::initContainer);
connect(boxPop, &CuteBasicComboPopup::visibleChanged, this, &CuteBasicComboBox::setPopupVisible);
// 布局
boxLayout->setContentsMargins(borderWidth, borderWidth, borderWidth, borderWidth);
boxLayout->setSpacing(0);
boxLayout->addWidget(boxEdit);
boxLayout->addWidget(boxDown);
// 点击按钮,弹出
// 这里有个问题,切换焦点导致弹框自动关闭,再进入该逻辑则visible已经是false,于是又弹出了
connect(boxDown, &QPushButton::pressed, [this]() {
// qDebug() << "toggled" << getPopupVisible();
if (getPopupVisible()) {
boxPop->hidePopup();
} else {
boxPop->showPopup();
}
});
}
void CuteBasicComboBox::initContainer()
{
if(!boxPop->getContainer())
return;
connect(boxPop->getContainer(), &CuteBasicComboContainer::currentIndexChanged, this, &CuteBasicComboBox::currentIndexChanged);
connect(boxPop->getContainer(), &CuteBasicComboContainer::updateData, [this] {
setCurrentText(boxPop->getContainer()->getCurrentText());
});
}
void CuteBasicComboBox::checkTextRow()
{
// 如果 model 中有匹配的文本,就修改 view 的 currentIndex
if (boxPop->getContainer() && boxPop->getContainer()->checkTextRow(boxEdit->text()) >= 0){
setCurrentText(boxPop->getContainer()->getCurrentText());
}
}