0.前言

Qt提供了一个QComboBox下拉框组件,但是对于一些自定义样式的需求实现起来并不方便,很多东西还得去倒腾源码,还不如直接用基础的组件自己来实现一个下拉框。不过,自己组合的组件要做的细节太多了,所以我只在一些定制化程度高的需求才使用这种方式。

1.实现思路与问题

首先是下拉框的文本框和按钮,我使用QLineEdit+QPushButton;然后弹出框我分为两部分,一是弹出框Widget容器,二是弹出框中的内容为了后续的自定义抽象了一个简单的基类,使用的时候继承基类实现自己的弹出框样式即可。剩下的就是模拟一些QComboBox的接口和效果了。

python qt取下拉框的值 qt 下拉选择框_Qt

遇到的问题一,弹出框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());
    }
}