Qt 中,QComboBox 默認只支持單選,但實際使用過程中,我們經常會碰到需要多選的情況,但是通過一些直接或者曲折的方法還是可以實現的。
1、通過 QListWidget 間接實現
這種方式是網上搜索最多的一種方式,也是相對來說比較簡單的一種方法。
首先,自定義ComboBox類并繼承自 QComboBox,在類內并定義一個QListWidget對象。
class MultiWdgComboBox : public QComboBox
{Q_OBJECT
public:using QComboBox::QComboBox;MultiComboBox(QWidget* parent = nullptr);void addItem(const QString &text, const QVariant &userData /* = QVariant() */);private:QListWidget* m_view_ptr{ nullptr };QString m_text{ QString() };
};
如上所示,定義好自定義之后,在其構造函數中對ComboBox的 view 和 model 重新設置。因為多選情況下控件顯示的內容可能需要自定義,QComboBox默認是不支持的,所以我們可以使用QComboBox自帶的QLineEdit 來實現自定義格式的數據顯示。
MultiWdgComboBox::MultiWdgComboBox(QWidget* parent) : QComboBox(parent)
{m_view_ptr = new QListWidget;m_view_ptr->setContentsMargins(QMargins(15, 0, 0, 0));setEditable(true);lineEdit()->setReadOnly(true);setModel(m_view_ptr->model());setView(m_view_ptr); }
緊接著,我們需要重寫基類的 addItem 函數,讓其能滿足我們自定義的類。下面的這種方式也就是在我們定義的 QListWdiget 對象中 insert 一個item,并對 item 設置對應的QWidget。
void MultiWdgComboBox::addItem(const QString &text, const QVariant &userData)
{QListWidgetItem *pItem = new QListWidgetItem;QCheckBox* checkBox = new QCheckBox(this);checkBox->setText(text);pItem->setData(Qt::UserRole, userData);m_view_ptr->addItem(pItem);m_view_ptr->setItemWidget(pItem, checkBox);...
}
為了方便我們在點擊 QCheckBox 時能實時改變顯示的數據,我們需要實現一個信號槽的連接。
void MultiWdgComboBox::addItem(const QString &text, const QVariant &userData)
{...QCheckBox* checkBox = new QCheckBox(this);...connect(checkBox, &QCheckBox::clicked, this, [this](bool checked){auto box = static_cast<QCheckBox*>(sender());QStringList texts = m_text.isEmpty() ? QStringList() : m_text.split(";");if (checked){texts.append(box->text());}else{texts.removeOne(box->text());}m_text = texts.join(";");this->setEditText(m_text);});
}
有了上面的步驟,基本上一個簡單的多選 ComboBox 控件就初具雛形了。
如果初始狀態下,已經有部分的item被選中了,那我們該如何設置對應的item狀態呢?
void MultiWdgComboBox::setData(const QVariant& data)
{QStringList text;QVariantList datas = data.value<QVariantList>();for (int index = 0; index < m_view_ptr->count(); ++index){auto item = m_view_ptr->item(index);auto ptr = static_cast<QCheckBox*>(m_view_ptr->itemWidget(item));if (datas.contains(item->data(Qt::UserRole))){if (ptr != nullptr && ptr->isEnabled()){ptr->setChecked(true);text.push_back(ptr->text());}}}m_text = text.join(";");
}
上面的這個函數是根據item的userData來進行設置的,當然也可以通過item的text來進行設置。
同理,獲取已經選中的item數據是一樣的道理。
QVariant MultiWdgComboBox::data() const
{QVariantList datas;for (int index = 0; index < m_view_ptr->count(); ++index){auto item = m_view_ptr->item(index);auto ptr = static_cast<QCheckBox*>(m_view_ptr->itemWidget(item));if (ptr != nullptr && ptr->isChecked()){datas.push_back(item->data(Qt::UserRole));}}return datas;}
當然如果 item 的 userData 沒有實際的意義,只是想標識一下被勾選的item,也可以用 二進制的方式來實現,最后可通過循環右移一位的方式遍歷獲得勾選的全部item項。也可以使用 Qt 的 QFlags 屬性來實現。
根據上面的內容,我們基本上已經實現了一個ComboBox 的基本功能,但是由于使用了 QComboBox的edit屬性,所以存在一個不好的體驗,就是在點擊下拉的時候,響應區域只有下拉箭頭表示的那部分范圍,而在 其 QLineEdit 所在的范圍并不響應。
所以,根據以前 《QComboBox文字居中的幾種實現方式》這篇文章的內容,對自定義ComboBox的顯示部分做了重新繪制。
重寫 paintEvent 事件函數即可。
void MultiWdgComboBox::paintEvent(QPaintEvent* e)
{QStylePainter painter(this);painter.setPen(palette().color(QPalette::Text));QStyleOptionComboBox opt;initStyleOption(&opt);painter.drawComplexControl(QStyle::CC_ComboBox, opt);if (opt.editable){painter.drawControl(QStyle::CE_ComboBoxLabel, opt);return;}QRect editRect = this->style()->subControlRect(QStyle::CC_ComboBox, &opt, QStyle::SC_ComboBoxEditField, this);QStyleOptionButton buttonOpt;buttonOpt.initFrom(this);buttonOpt.direction = Qt::LeftToRight;buttonOpt.rect = editRect;buttonOpt.text = m_text;buttonOpt.icon = opt.currentIcon;buttonOpt.iconSize = opt.iconSize;painter.drawControl(QStyle::CE_CheckBoxLabel, buttonOpt);}
2、另辟蹊徑,使用 QPushBututon 的 setMenu 方法,set一個 自定義Action的Menu。
這種方法呢,是我在做其他需求的時候偶然發現的,原來一些比較復雜的控件都可以用原生的控件可以實現。
首先,自定義一個繼承自 QPushButton的子類 MultiButtonComboBox。
class MultiButtonComboBox : public QPushButton
{Q_OBJECT
public:MultiButtonComboBox(QWidget* parent = nullptr);void addItem(const QString& text, const QVariant& data = QVariant());void setData(const QVariant& data);QVariant data() const;private:MultiListWidget* m_ptr{ nullptr };
};
如上所示,定義好自定義之后,在其構造函數中設置一個自定義界面的menu。
MultiButtonComboBox::MultiButtonComboBox(QWidget* parent) : QPushButton(parent)
{auto menu = new QMenu(this);m_ptr = new MultiListWidget(this);QWidgetAction *action = new QWidgetAction(this);action->setDefaultWidget(m_ptr);menu->addAction(action);setMenu(menu);connect(m_ptr, &MultiListWidget::signal_text, this, [this](const QString& text){setText(text);});}
設置好自定義的菜單界面之后,只需要根據需要實現自定義的菜單界面就行了。可以用 QListWidget, 也可以用 QListView,我這邊用的是 QListView 和 自定義listview 的 item 代理實現的。
class MultiListWidget : public QWidget
{Q_OBJECTpublic:MultiListWidget(QWidget *parent = nullptr);~MultiListWidget();void addItem(const QString& text, const QVariant& userData);void setData(const QVariant& data);QVariant data() const;private:void initPage();signals:void signal_text(const QString& text);private:Ui::MultiListWidget *ui;QStandardItemModel* m_pModel{ nullptr };QString m_text{ QString() };};
這種方式的實現是比較簡單的,對 QListView 設置 item 代理及 model 之后,后續的操作只是對 model 的數據操作。為了圖方便,使用了標準的 QStandardItemModel 類,當然可以自定義 model 類,通過重寫 自定義類的 data 函數,能更好的滿足自定義功能的需求。
void MultiListWidget::initPage()
{m_pModel = new QStandardItemModel();auto *delegate = new CmbBoxItemDelegate(this);ui->listView->setItemDelegate(delegate);ui->listView->setModel(m_pModel);...
}
自定義 QListView 的 item 代理后,可以通過重寫它的 editorEvent 函數來控制item的編輯事件。那我們就可以通過實現 代理與 當前類的信號槽來實現我們已經選擇、顯示的item 數據。
void MultiListWidget::initPage()
{...auto *delegate = new CmbBoxItemDelegate(this);...connect(delegate, &CmbBoxItemDelegate::signal_btn_clicked, this, [this](const QModelIndex& index){QStringList texts = m_text.isEmpty() ? QStringList() : m_text.split(";");bool checked = !index.data(Qt::UserRole + 1).toBool();m_pModel->setData(index, checked, Qt::UserRole + 1);if (checked){texts.append(index.data(Qt::DisplayRole).toString());}else{texts.removeOne(index.data(Qt::DisplayRole).toString());}m_text = texts.join(";");emit signal_text(m_text);});
}
緊接著,實現 addItem 函數。
void MultiListWidget::addItem(const QString& text, const QVariant& userData)
{QStandardItem *pItem = new QStandardItem;pItem->setData(false, Qt::UserRole + 1);pItem->setData(text, Qt::DisplayRole);pItem->setData(userData, Qt::UserRole);m_pModel->appendRow(pItem);
}
setData函數。
void MultiListWidget::setData(const QVariant& data)
{QStringList texts;auto datas = data.toList();for (int index = 0; index < m_pModel->rowCount(); ++index){auto mIndex = m_pModel->index(index, 0);if (datas.contains(mIndex.data(Qt::UserRole))){m_pModel->setData(mIndex, true, Qt::UserRole + 1);texts.append(mIndex.data().toString());}}m_text = texts.join(";");emit signal_text(m_text);
}
data 函數。
QVariant MultiListWidget::data() const
{QVariantList datas;for (int index = 0; index < m_pModel->rowCount(); ++index){auto mIndex = m_pModel->index(index, 0);if (mIndex.data(Qt::UserRole + 1).toBool()){datas.append(mIndex.data(Qt::UserRole));}}return datas;
}
3、自定義QComboBox 的 item delegate
這種方式跟上面第二種來說是差不多的,只不過上面的第二種方法實現的比較曲折,而QComboBox的view也可以是個ListView。所以我們取了第一種方法的繪制顯示text的方法和第二種的item代理,結合就有了第三種方法。相對來說,這種方法更簡單直接些。
首先自定義繼承自QComboBox的類并設置代理,實現代理的信號槽函數。
MultiWdgComboBox::MultiViewComboBox(QWidget* parent) : QComboBox(parent)
{auto delegate = new CmboBoxItemDelegate(this);setItemDelegate(delegate);connect(delegate, &CmboBoxItemDelegate::signal_btn_clicked, this, [this](const QModelIndex& index){auto checked = !index.data(Qt::UserRole + 1).toBool();this->model()->setData(index, checked, Qt::UserRole + 1);QStringList texts = m_text.isEmpty() ? QStringList() : m_text.split(";");if (checked){texts.append(index.data().toString());}else{texts.removeOne(index.data().toString());}m_text = texts.join(";");});
}
這種方式也是唯一一種不需要重寫 addItem 方法的實現形式,后面的 setData 和 data 與上面第二種方式的完全一樣。
這種方式要避免多次設置item的代理,否則代理可能不生效。
如果我們使用的是第一種方法,鼠標點擊的范圍如果超過了每行QCheckBox的實際范圍,實際效果下來是,既沒有選中某行,comboBox的下拉框也會收起。
同樣的,如果使用的是第三種,自定義了QComboBox的 item 代理,點擊起哄一行也會將下拉框收起,這樣我們要完成多選的話就得點擊好幾次。
所以,我們可以通過重寫hidePopup函數來避免這個問題。
void MultiViewComboBox::hidePopup()
{QWidget *popup = this->findChild<QFrame*>();if(!popup->geometry().contains(QCursor::pos())){QComboBox::hidePopup();}
}
4、注意事項
在第一種使用 QListWidget 的時候,測試時出現了下面這種情況。

這個問題的原因是我們給QListWidget設置了model,為什么呢?因為根據Qt已經有的那種 model/view的結構,QListWidget已經是對應的結構了,它有自己的model,如果想要通過重寫自己的model來實現數據的話,建議使用QlistView。
但是在這個里面,我們已經選擇了QListWidget,那有沒有什么解決辦法。
其實我們只需要在給ComboBox設置view之前設置model就可以了,也就是說,下面的這兩行代碼順序不能互換。
MultiComboBox::MultiComboBox(QWidget* parent) : QComboBox(parent)
{...setModel(m_view_ptr->model());setView(m_view_ptr); }
測試代碼