⬆️ ⬇️

Multiple selection in QComboBox



Picture to attract attention

(possibly related to the post)



Sometimes, quite convenient is the possibility of multiple selection in the QComboBox widget. This tutorial will show you how to do it.



The basic idea is that the elements of the model used in QComboBox need to raise the Qt :: ItemIsUserCheckable flag , thus making them checked. It will also take care of displaying the list of selected items on the widget.



Let's declare the MultiListWidget class (the property and the corresponding checkedItems methods give access to the list of elements that we have previously installed or that the user has noted, and the collectCheckedItems method stores the marked model elements in mCheckedItems):

class MultiListWidget : public QComboBox { Q_OBJECT Q_PROPERTY(QStringList checkedItems READ checkedItems WRITE setCheckedItems) public: MultiListWidget(); virtual ~MultiListWidget(); QStringList checkedItems() const; void setCheckedItems(const QStringList &items); private: QStringList mCheckedItems; void collectCheckedItems(); }; 


The QComboBox model has several signals we need:



Also useful is the itemChanged (QStandardItem * item), which is emitted when you check and uncheck the box (by the user or programmatically).

')

Let's declare slots for these signals:

 private slots: void slotModelRowsInserted(const QModelIndex &parent, int start, int end); void slotModelRowsRemoved(const QModelIndex &parent, int start, int end); void slotModelItemChanged(QStandardItem *item); 


And link the signals to the slots in the constructor (note that model () returns a pointer to QAbstractItemModel, and the itemChanged signal is emitted in QStandardItemModel, so the cast is needed here):

 MultiListWidget::MultiListWidget() { connect(model(), SIGNAL(rowsInserted(QModelIndex, int, int)), this, SLOT(slotModelRowsInserted(QModelIndex,int,int))); connect(model(), SIGNAL(rowsRemoved(QModelIndex, int, int)), this, SLOT(slotModelRowsRemoved(QModelIndex,int,int))); QStandardItemModel *standartModel = qobject_cast<QStandardItemModel*>(model()); connect(standartModel, SIGNAL(itemChanged(QStandardItem*)), this, SLOT(slotModelItemChanged(QStandardItem*))); } MultiListWidget::~MultiListWidget() { } 


Now, we implement the checkedItems () and setCheckedItems methods (const QStringList & items):

 QStringList MultiListWidget::checkedItems() const { return mCheckedItems; } void MultiListWidget::setCheckedItems(const QStringList &items) { //   QStandardItemModel *standartModel = qobject_cast<QStandardItemModel*>(model()); //   ,      disconnect(standartModel, SIGNAL(itemChanged(QStandardItem*)), this, SLOT(slotModelItemChanged(QStandardItem*))); for (int i = 0; i < items.count(); ++i) { //    int index = findText(items.at(i)); if (index != -1) { //    standartModel->item(index)->setData(Qt::Checked, Qt::CheckStateRole); } } //    connect(standartModel, SIGNAL(itemChanged(QStandardItem*)), this, SLOT(slotModelItemChanged(QStandardItem*))); //     collectCheckedItems(); } 


Inside the collectCheckedItems () method, everything is simple - go over the elements of the model, if it is checked, add to the list:

 void MultiListWidget::collectCheckedItems() { QStandardItemModel *standartModel = qobject_cast<QStandardItemModel*>(model()); mCheckedItems.clear(); for (int i = 0; i < count(); ++i) { QStandardItem *currentItem = standartModel->item(i); Qt::CheckState checkState = static_cast<Qt::CheckState>(currentItem->data(Qt::CheckStateRole).toInt()); if (checkState == Qt::Checked) { mCheckedItems.push_back(currentItem->text()); } } } 


When inserting a new element into the model, we need to specify that it will be marked by the user and initially with the unchecked box:

 void MultiListWidget::slotModelRowsInserted(const QModelIndex &parent, int start, int end) { //     (void)parent; QStandardItemModel *standartModel = qobject_cast<QStandardItemModel*>(model()); disconnect(standartModel, SIGNAL(itemChanged(QStandardItem*)), this, SLOT(slotModelItemChanged(QStandardItem*))); for (int i = start; i <= end; ++i) { standartModel->item(i)->setFlags(Qt::ItemIsUserCheckable | Qt::ItemIsEnabled); standartModel->item(i)->setData(Qt::Unchecked, Qt::CheckStateRole); } connect(standartModel, SIGNAL(itemChanged(QStandardItem*)), this, SLOT(slotModelItemChanged(QStandardItem*))); } 


When removing items from the model, you also need to remove them from mCheckedItems. We use collectCheckedItems ():

 void MultiListWidget::slotModelRowsRemoved(const QModelIndex &parent, int start, int end) { (void)parent; (void)start; (void)end; collectCheckedItems(); } 


In slotModelItemChanged slot (QStandardItem * item) we collect the marked items:

 void MultiListWidget::slotModelItemChanged(QStandardItem *item) { (void)item; collectCheckedItems(); } 


Place the class declaration and its implementation in, respectively, multilist.h and multilist.cpp, and try MultiListWidget in the file (main.cpp file):

 #include "multilist.h" int main(int argc, char *argv[]) { QApplication app(argc, argv); MultiListWidget *multiList = new MultiListWidget(); multiList->addItems(QStringList() << "One" << "Two" << "Three" << "Four"); multiList->setCheckedItems(QStringList() << "One" << "Three"); QHBoxLayout *layout = new QHBoxLayout(); layout->addWidget(new QLabel("Select items:")); layout->addWidget(multiList, 1); QWidget widget; widget.setWindowTitle("MultiList example"); widget.setLayout(layout); widget.show(); return app.exec(); } 


Not bad, but it remains to display a list of selected items on the widget. To do this, we declare (in the closed section) in the class a variable to store the text for the output, a delta for the rectangle (the explanation will be lower), within which this text will be drawn, and a method that updates the text for the output when changing the list of marked elements:

 QString mDisplayText; const QRect mDisplayRectDelta; void updateDisplayText(); 


Add the initialization of mDisplayRectDelta to the constructor:

 MultiListWidget::MultiListWidget() : mDisplayRectDelta(4, 1, -25, 0) { ... } 


Now, let's take a closer look at updateDisplayText ():

 void MultiListWidget::updateDisplayText() { //    , mDisplayRectDelta   ""  //   ,    ,   QRect textRect = rect().adjusted(mDisplayRectDelta.left(), mDisplayRectDelta.top(), mDisplayRectDelta.right(), mDisplayRectDelta.bottom()); QFontMetrics fontMetrics(font()); //   mDisplayText = mCheckedItems.join(", "); //      if (fontMetrics.size(Qt::TextSingleLine, mDisplayText).width() > textRect.width()) { //   ,       while (mDisplayText != "" && fontMetrics.size(Qt::TextSingleLine, mDisplayText + "...").width() > textRect.width()) { mDisplayText.remove(mDisplayText.length() - 1, 1); } //   mDisplayText += "..."; } } 


To draw text, you must override the paintEvent virtual method (QPaintEvent * event). You also need to override the resizeEvent (QResizeEvent * event) method, since the text borders will change when the widget is resized. Here is the declaration of these methods:

 protected: virtual void paintEvent(QPaintEvent *event); virtual void resizeEvent(QResizeEvent *event); 


And their implementation:

 void MultiListWidget::paintEvent(QPaintEvent *event) { (void)event; QStylePainter painter(this); painter.setPen(palette().color(QPalette::Text)); QStyleOptionComboBox option; initStyleOption(&option); //     painter.drawComplexControl(QStyle::CC_ComboBox, option); //     QRect textRect = rect().adjusted(mDisplayRectDelta.left(), mDisplayRectDelta.top(), mDisplayRectDelta.right(), mDisplayRectDelta.bottom()); //   painter.drawText(textRect, Qt::AlignVCenter, mDisplayText); } void MultiListWidget::resizeEvent(QResizeEvent *event) { (void)event; updateDisplayText(); } 


It remains only to update the displayed text after changing the list of model elements. Add the updateDisplayText () call to the collectCheckedItems () end and redraw the widget:

 void MultiListWidget::setCheckedItems(const QStringList &items) { ... updateDisplayText(); repaint(); } 


There is a bug in GTK and Mac styles that do not display the checkboxes in the expanded list. To work around this bug, you need to set the combobox-popup values ​​in the widget's styleSheet (place this code in the constructor):

 setStyleSheet("QComboBox { combobox-popup: 1px }"); 


Images:









Source:



multilist.h
 #ifndef MULTILIST_H #define MULTILIST_H #include <QtGui> class MultiListWidget : public QComboBox { Q_OBJECT Q_PROPERTY(QStringList checkedItems READ checkedItems WRITE setCheckedItems) public: MultiListWidget(); virtual ~MultiListWidget(); QStringList checkedItems() const; void setCheckedItems(const QStringList &items); protected: virtual void paintEvent(QPaintEvent *event); virtual void resizeEvent(QResizeEvent *event); private: QStringList mCheckedItems; void collectCheckedItems(); QString mDisplayText; const QRect mDisplayRectDelta; void updateDisplayText(); private slots: void slotModelRowsInserted(const QModelIndex &parent, int start, int end); void slotModelRowsRemoved(const QModelIndex &parent, int start, int end); void slotModelItemChanged(QStandardItem *item); }; #endif // MULTILIST_H 




multilist.cpp
 #include "multilist.h" MultiListWidget::MultiListWidget() : mDisplayRectDelta(4, 1, -25, 0) { setStyleSheet("QComboBox { combobox-popup: 1px }"); connect(model(), SIGNAL(rowsInserted(QModelIndex, int, int)), this, SLOT(slotModelRowsInserted(QModelIndex,int,int))); connect(model(), SIGNAL(rowsRemoved(QModelIndex, int, int)), this, SLOT(slotModelRowsRemoved(QModelIndex,int,int))); QStandardItemModel *standartModel = qobject_cast<QStandardItemModel*>(model()); connect(standartModel, SIGNAL(itemChanged(QStandardItem*)), this, SLOT(slotModelItemChanged(QStandardItem*))); } MultiListWidget::~MultiListWidget() { } QStringList MultiListWidget::checkedItems() const { return mCheckedItems; } void MultiListWidget::setCheckedItems(const QStringList &items) { QStandardItemModel *standartModel = qobject_cast<QStandardItemModel*>(model()); disconnect(standartModel, SIGNAL(itemChanged(QStandardItem*)), this, SLOT(slotModelItemChanged(QStandardItem*))); for (int i = 0; i < items.count(); ++i) { int index = findText(items.at(i)); if (index != -1) { standartModel->item(index)->setData(Qt::Checked, Qt::CheckStateRole); } } connect(standartModel, SIGNAL(itemChanged(QStandardItem*)), this, SLOT(slotModelItemChanged(QStandardItem*))); collectCheckedItems(); } void MultiListWidget::paintEvent(QPaintEvent *event) { (void)event; QStylePainter painter(this); painter.setPen(palette().color(QPalette::Text)); QStyleOptionComboBox option; initStyleOption(&option); painter.drawComplexControl(QStyle::CC_ComboBox, option); QRect textRect = rect().adjusted(mDisplayRectDelta.left(), mDisplayRectDelta.top(), mDisplayRectDelta.right(), mDisplayRectDelta.bottom()); painter.drawText(textRect, Qt::AlignVCenter, mDisplayText); } void MultiListWidget::resizeEvent(QResizeEvent *event) { (void)event; updateDisplayText(); } void MultiListWidget::collectCheckedItems() { QStandardItemModel *standartModel = qobject_cast<QStandardItemModel*>(model()); mCheckedItems.clear(); for (int i = 0; i < count(); ++i) { QStandardItem *currentItem = standartModel->item(i); Qt::CheckState checkState = static_cast<Qt::CheckState>(currentItem->data(Qt::CheckStateRole).toInt()); if (checkState == Qt::Checked) { mCheckedItems.push_back(currentItem->text()); } } updateDisplayText(); repaint(); } void MultiListWidget::updateDisplayText() { QRect textRect = rect().adjusted(mDisplayRectDelta.left(), mDisplayRectDelta.top(), mDisplayRectDelta.right(), mDisplayRectDelta.bottom()); QFontMetrics fontMetrics(font()); mDisplayText = mCheckedItems.join(", "); if (fontMetrics.size(Qt::TextSingleLine, mDisplayText).width() > textRect.width()) { while (mDisplayText != "" && fontMetrics.size(Qt::TextSingleLine, mDisplayText + "...").width() > textRect.width()) { mDisplayText.remove(mDisplayText.length() - 1, 1); } mDisplayText += "..."; } } void MultiListWidget::slotModelRowsInserted(const QModelIndex &parent, int start, int end) { (void)parent; QStandardItemModel *standartModel = qobject_cast<QStandardItemModel*>(model()); disconnect(standartModel, SIGNAL(itemChanged(QStandardItem*)), this, SLOT(slotModelItemChanged(QStandardItem*))); for (int i = start; i <= end; ++i) { standartModel->item(i)->setFlags(Qt::ItemIsUserCheckable | Qt::ItemIsEnabled); standartModel->item(i)->setData(Qt::Unchecked, Qt::CheckStateRole); } connect(standartModel, SIGNAL(itemChanged(QStandardItem*)), this, SLOT(slotModelItemChanged(QStandardItem*))); } void MultiListWidget::slotModelRowsRemoved(const QModelIndex &parent, int start, int end) { (void)parent; (void)start; (void)end; collectCheckedItems(); } void MultiListWidget::slotModelItemChanged(QStandardItem *item) { (void)item; collectCheckedItems(); } 




main.cpp
 #include "multilist.h" int main(int argc, char *argv[]) { QApplication app(argc, argv); MultiListWidget *multiList = new MultiListWidget(); multiList->addItems(QStringList() << "One" << "Two" << "Three" << "Four"); multiList->setCheckedItems(QStringList() << "One" << "Three"); QHBoxLayout *layout = new QHBoxLayout(); layout->addWidget(new QLabel("Select items:")); layout->addWidget(multiList, 1); QWidget widget; widget.setWindowTitle("MultiList example"); widget.setLayout(layout); widget.show(); return app.exec(); } 




Thanks for attention!

Source: https://habr.com/ru/post/215289/



All Articles