QComboBox下拉框给选项增加删除按钮

0.前言

给下拉框增加按钮是常见的功能,如 QQ 账号输入框的下拉:

网上有不少 Qt 实现的例子,实现方式也很多,在参照了别人的思路后我也实现了选项带按钮的下拉框。中间遇到不少坑,最后效果也不完美,后来没用到就不了了之了,仅供参考。

1.实现过程

本文选的是 QListWidget + QListWidgetItem 的方式,因为感觉相对简单点,而且和样式表更好搭配。这种方式主要是把 QListWidget 作为 QComboBox 的 View ,List 的 model 也设置为 QComboBox 的 model,这样展现出来的选项就是一个 QListWidgetItem,我们只需要把按钮设置到 QListWidgetItem 的 widget 中就行了。后来感觉 QComboBox 和 QListWidget 太多内置的规则,细节完善任重道远。

首先,要让 QComboBox 读取到选项的文本,需要给 QListWidgetItem 设置 DisplayRole:

QListWidgetItem* item_widget = new QListWidgetItem();
//设置显示的data,这样combox才有文字
item_widget->setData(Qt::DisplayRole,"text");

然后来看一下比较简单的实现:

    QListWidget *item_list=new QListWidget(this);
    ui->comboBoxA->setModel(item_list->model());
    ui->comboBoxA->setView(item_list);

    //添加选项
    for(int i=0;i<5;i++)
    {
        //组合一个带按钮的widget
        QWidget *item_widget=new QWidget();
        QHBoxLayout *layout=new QHBoxLayout(item_widget);
        layout->addStretch(); //弹簧
        QPushButton *btn=new QPushButton(item_widget);
        layout->addWidget(btn);
        layout->setMargin(0);
        layout->setSpacing(0);

        QListWidgetItem* item_wrap = new QListWidgetItem(item_list);
        //测试长文字
        QString text=(i==0)?"text long long long":"text";
        //设置显示的data,这样combox才有文字
        item_wrap->setData(Qt::DisplayRole,text);
        item_list->setItemWidget(item_wrap,item_widget);

        connect(btn,&QPushButton::clicked,this,[=](){
            ui->comboBoxA->hidePopup(); //没有刷新弹框大小
            item_list->takeItem(item_list->row(item_wrap));
            delete item_wrap;
        });
    }

很快就发现了问题,文字足够长时,选项整体宽度被拉长了,导致按钮位置超出了显示范围(下图蓝色框部分):

 

(按钮位置我是配合样式表的 margin 调整的,可见源代码) 

索性我就继承了 QListWidget,重写了他的 visualRect 接口,在有滚动条时,rect 就去掉滚动条的宽度:

QRect ComboView::visualRect(const QModelIndex &index) const
{
    QRect rect=QListWidget::visualRect(index);
    int width=this->width();
    if(verticalScrollBar()->isVisible()){
        width-=verticalScrollBar()->width();
    }
    rect.setWidth(width);
    return rect;
}

这下就能把选项挤过来了:

(2022-09-04 补充)之前有评论指出下拉滚动条滚动后,再次弹出时按钮和选项没对齐,拖动窗口大小后弹出又能恢复对齐,应该是WidgetItem的坐标同步问题。

目前想的简单的解决方法有两种,一是弹出时执行一遍resize的逻辑,二是隐藏时恢复弹出时的位置,以思路二为例:

void MyComboBox::hidePopup()
{
    QStyle * const style = this->style();
    QStyleOptionComboBox opt;
    initStyleOption(&opt);
    view()->scrollTo(view()->currentIndex(),
                     style->styleHint(QStyle::SH_ComboBox_Popup, &opt, this)
                     ? QAbstractItemView::PositionAtCenter
                     : QAbstractItemView::EnsureVisible);
    QComboBox::hidePopup();
}

其他问题:

1.滚动条占位宽度计算的问题,我的想法是显示或增删选项时去判断是否有滚动条,然后调整按钮的 margin。 

2.点击按钮删除选项后,下次弹出可能某个选项处于hover状态。

2.完整代码

github 链接:https://github.com/gongjianbo/MyTestCode/tree/master/Qt/MyComboBox

(demo 没怎么写注释,而且没有应用到自己的项目):

#pragma once
#include <QComboBox>
#include <QListView>
#include <QListWidget>
#include <QPushButton>
#include <QLayout>
#include <QStyledItemDelegate>

class ComboView : public QListWidget
{
    Q_OBJECT
public:
    explicit ComboView(QWidget * parent=nullptr);

    QRect visualRect(const QModelIndex &index) const override;
};

class ComboItem : public QWidget
{
    Q_OBJECT
public:
    explicit ComboItem(const QString &text,QWidget *parent = nullptr);
    ~ComboItem();
    QString text() const;

signals:
    void itemClicked(const QString &text);

private:
    QString textValue;
    QPushButton *btn;
};

class ComboDelegate : public QStyledItemDelegate
{
    Q_OBJECT
public:
    explicit ComboDelegate(QObject *parent=nullptr);
    void paint(QPainter *painter, const QStyleOptionViewItem &option,
               const QModelIndex &index) const override;
};

class MyComboBox : public QComboBox
{
    Q_OBJECT
public:
    explicit MyComboBox(QWidget *parent = nullptr);

    //设置下拉选项
    void setRemovableItems(const QStringList &items);
    //显示弹框
    void showPopup() override;
    void hidePopup() override;

signals:
    void itemRemoved(const QString &text);

private:
    QListWidget *itemList;
};
#include "MyComboBox.h"

#include <QMouseEvent>
#include <QLineEdit>
#include <QStyleOptionViewItem>
#include <QStyle>
#include <QScrollBar>

#include <QDebug>

ComboView::ComboView(QWidget *parent)
    : QListWidget(parent)
{

}

QRect ComboView::visualRect(const QModelIndex &index) const
{
    QRect rect=QListWidget::visualRect(index);
    int width=this->width();
    if(verticalScrollBar()->isVisible()){
        width-=verticalScrollBar()->width();
    }
    rect.setWidth(width);
    return rect;
}

ComboItem::ComboItem(const QString &text, QWidget *parent)
    : QWidget(parent),
      textValue(text),
      btn(new QPushButton(text,this))
{
    QHBoxLayout *layout=new QHBoxLayout(this);
    layout->addStretch();
    layout->addWidget(btn);
    layout->setMargin(0);
    layout->setSpacing(0);

    connect(btn,&QPushButton::clicked,[this]{
        emit itemClicked(textValue);
    });
}

ComboItem::~ComboItem()
{
    qDebug()<<"~delete"<<textValue;
}

QString ComboItem::text() const
{
    return textValue;
}

ComboDelegate::ComboDelegate(QObject *parent)
    : QStyledItemDelegate(parent)
{

}

void ComboDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const
{
    QStyleOptionViewItem opt(option);
    //去掉焦点带来的虚线框
    opt.state &= ~QStyle::State_HasFocus;
    
    initStyleOption(&opt, index);
    QStyledItemDelegate::paint(painter, opt, index);
}

MyComboBox::MyComboBox(QWidget *parent)
    : QComboBox(parent),
      itemList(new ComboView(this))
{
    //itemList->setTextElideMode(Qt::ElideNone);
    setModel(itemList->model());
    setView(itemList);
    setItemDelegate(new ComboDelegate(this));
}

void MyComboBox::setRemovableItems(const QStringList &items)
{
    //combox的additem insertitem不是虚函数
    //实现里时调用的model->insertRow,但是懒得再去重写listmodel-view,就新增一个接口
    itemList->clear();
    if(items.isEmpty())
        return;

    for(int i=0;i<items.count();i++)
    {
        QListWidgetItem* widget_item = new QListWidgetItem(itemList);
        ComboItem *item=new ComboItem(items.at(i),itemList);
        widget_item->setData(Qt::DisplayRole,items.at(i));
        //widget_item->setData(Qt::TextAlignmentRole,int(Qt::AlignRight|Qt::AlignVCenter));
        //itemList->addItem(widget_item);
        itemList->setItemWidget(widget_item,item);

        connect(item,&ComboItem::itemClicked,this,[this,item,widget_item](){
            //take移除item后没有刷新弹框大小,干脆隐藏掉先
            hidePopup();

            itemList->takeItem(itemList->row(widget_item));
            delete widget_item;
            emit itemRemoved(item->text());
        });
    }
}

void MyComboBox::showPopup()
{
    QComboBox::showPopup();
}

void MyComboBox::hidePopup()
{
    QStyle * const style = this->style();
    QStyleOptionComboBox opt;
    initStyleOption(&opt);
    view()->scrollTo(view()->currentIndex(),
                     style->styleHint(QStyle::SH_ComboBox_Popup, &opt, this)
                     ? QAbstractItemView::PositionAtCenter
                     : QAbstractItemView::EnsureVisible);
    QComboBox::hidePopup();
}

#include "mainwindow.h"
#include "ui_mainwindow.h"

#include "MyComboBox.h"

#include <QListWidget>
#include <QScrollBar>
#include <QDebug>

MainWindow::MainWindow(QWidget *parent) :
    QMainWindow(parent),
    ui(new Ui::MainWindow)
{
    ui->setupUi(this);

    //准备下拉选项串
    QStringList str_list;
    for(int i=0;i<20;i++)
    {
        str_list.push_back(QString("%1 item").arg(i));
    }
    //测试长文本显示
    str_list[0]="0 item: text long long long long long long";

    //【1】
    QListWidget *item_list=new QListWidget(this);
    ui->comboBoxA->setModel(item_list->model());
    ui->comboBoxA->setView(item_list);

    //添加选项
    for(int i=0;i<str_list.size();i++)
    {
        //组合一个带按钮的widget
        QWidget *item_widget=new QWidget();
        QHBoxLayout *layout=new QHBoxLayout(item_widget);
        layout->addStretch(); //弹簧
        QPushButton *btn=new QPushButton(str_list.at(i),item_widget);
        layout->addWidget(btn);
        layout->setMargin(0);
        layout->setSpacing(0);

        QListWidgetItem* item_wrap = new QListWidgetItem(item_list);
        //设置显示的data,这样combox才有文字
        item_wrap->setData(Qt::DisplayRole,str_list.at(i));
        //item_list->addItem(item_wrap);
        item_list->setItemWidget(item_wrap,item_widget);

        connect(btn,&QPushButton::clicked,this,[=](){
            ui->comboBoxA->hidePopup(); //没有刷新弹框大小
            item_list->takeItem(item_list->row(item_wrap));
            delete item_wrap;
        });
    }

    //【2】
    //QStringList str_list;
    ui->comboBoxB->setRemovableItems(str_list);
    //ui->comboBoxB->setMaxVisibleItems(20);

    connect(ui->comboBoxB,&MyComboBox::itemRemoved,this,[=](const QString &text){
        qDebug()<<"item remove"<<text;
        //ui->comboBox_2->clear();
    });
}

MainWindow::~MainWindow()
{
    delete ui;
}
/*下拉框*/
QComboBox{
min-width:1px;
min-height:26px;
padding-left: 5px;
padding-right: 2px;
color: white;
border:1px solid rgb(128, 128, 128);
background-color:  rgb(100 ,100 ,100);
}
QComboBox:on{
/*弹出为on*/
}
QComboBox:hover{
border:1px solid rgb(255, 170, 0);
}
QComboBox:disabled{
color: rgb(230, 230, 230);
background-color:rgb(150, 150, 150);
}
QComboBox:editable{
background-color:rgb(100, 100, 100);
}
QComboBox:editable:disabled{
background-color:rgb(150, 150, 150);
}
/*下拉按钮-配合贴图*/
QComboBox::drop-down{
min-width:24px;
}
/*下拉框弹出项*/
QComboBox QAbstractItemView{
font: 15px "宋体";
background-color:rgb(110, 110, 110);
}
QComboBox QAbstractItemView::item{
height:24px;
color:white;
}
QComboBox QAbstractItemView::item:hover{
background-color: rgb(255, 170, 0);
}
QComboBox QAbstractItemView::item:selected{
background-color: rgb(255, 170, 0);
}
/*按钮样式*/
QComboBox QAbstractItemView QPushButton{
max-width:40px;
min-width:40px;
max-height:20px;
min-height:20px;
border-radius:2px;
border:0;
margin-right:20px;
background-color:white;
}
QComboBox QAbstractItemView QPushButton:hover{
background-color:cyan;
}

 3.参考

博客:QComboBox的代理(订制QComboBox组合框)_Always0nTheWay的博客-CSDN博客

博客:新浪博客

可以使用PyQt5的信号槽机制,将QComboBox的currentIndexChanged信号与按钮的clicked信号连接起来,实现这个绑定按钮事件的功能。具体的代码如下: ```python from PyQt5.QtWidgets import QApplication, QWidget, QComboBox, QPushButton, QVBoxLayout class MyWidget(QWidget): def __init__(self): super().__init__() # 创建组件 self.comboBox = QComboBox() self.comboBox.addItems(['选项1', '选项2', '选项3']) self.button = QPushButton('按钮') # 创建布局 layout = QVBoxLayout() layout.addWidget(self.comboBox) layout.addWidget(self.button) self.setLayout(layout) # 连接信号槽 self.comboBox.currentIndexChanged.connect(self.on_combobox_changed) self.button.clicked.connect(self.on_button_clicked) # 当下拉框选项改变时触发 def on_combobox_changed(self, index): print('选中了第{}个选项'.format(index)) # 当按钮被点击时触发 def on_button_clicked(self): print('按钮被点击了') if __name__ == '__main__': app = QApplication([]) widget = MyWidget() widget.show() app.exec_() ``` 在这个代码中,我们先创建了一个QComboBox和一个QPushButton,并将它们添加到了一个QVBoxLayout中。然后,我们将QComboBox的currentIndexChanged信号与on_combobox_changed方法连接起来,将按钮的clicked信号与on_button_clicked方法连接起来。当用户选中下拉框的某个选项时,就会触发on_combobox_changed方法,当用户点击按钮时,就会触发on_button_clicked方法。你可以根据自己的需求修改这些方法的实现。
评论 8
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

龚建波

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值