Multiple Choice in QComboBox

  • Tutorial

A picture to attract attention
(possibly related to the post)


Sometimes, it is quite convenient to have multiple choices in the QComboBox widget. This short tutorial will show you how to do it.

The basic idea is that the elements of the model used in the QComboBox need to raise the Qt :: ItemIsUserCheckable check box , thereby making them flagged . It will also take care of listing the marked items on the widget.

We declare the MultiListWidget class (the property and the corresponding checkedItems methods give access to the list of elements that we previously set or that the user marked, 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();
};

There are several signals we need in the QComboBox model:
  • rowsInserted (const QModelIndex & parent, int start, int end) - when adding elements to the model (calling addItem, insertItem, etc. methods)
  • rowsRemoved (const QModelIndex & parent, int start, int end) - when removing elements from the model (call the removeItem method)

Also useful is itemChanged (QStandardItem * item), which is emitted when the flag is set or unchecked (by user or programmatically).

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 we will associate the signals with the slots in the constructor (note that model () returns a pointer to QAbstractItemModel, and the itemChanged signal is emitted in QStandardItemModel, so a 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(model());
	connect(standartModel, SIGNAL(itemChanged(QStandardItem*)), this, SLOT(slotModelItemChanged(QStandardItem*)));
}
MultiListWidget::~MultiListWidget()
{
}

Now, we implement the checkedItems () and setCheckedItems (const QStringList & items) methods:
QStringList MultiListWidget::checkedItems() const
{
	return mCheckedItems;
}
void MultiListWidget::setCheckedItems(const QStringList &items)
{
	// необходимо приведение
	QStandardItemModel *standartModel = qobject_cast(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 - we go over the model elements, if it is checked, add it to the list:
void MultiListWidget::collectCheckedItems()
{
	QStandardItemModel *standartModel = qobject_cast(model());
	mCheckedItems.clear();
	for (int i = 0; i < count(); ++i)
	{
		QStandardItem *currentItem = standartModel->item(i);
		Qt::CheckState checkState = static_cast(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 indicate that it will be a marked user and initially unchecked:
void MultiListWidget::slotModelRowsInserted(const QModelIndex &parent, int start, int end)
{
	// чтобы компилятор не ругался
	(void)parent;
	QStandardItemModel *standartModel = qobject_cast(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 the slot slotModelItemChanged (QStandardItem * item), we collect the marked elements:
void MultiListWidget::slotModelItemChanged(QStandardItem *item)
{
	(void)item;
	collectCheckedItems();
}

We place the class declaration and its implementation in, respectively, multilist.h and multilist.cpp, and try MultiListWidget in action (file 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();
}

Not bad, but it remains to display a list of marked elements on the widget. To do this, we declare (in the closed section) in the class a variable for storing text for output, a delta for a rectangle (an explanation will be given below) within which this text will be drawn, and a method that updates the text for output when changing the list of marked elements:
QString mDisplayText;
const QRect mDisplayRectDelta;
void updateDisplayText();

Add mDisplayRectDelta initialization 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 virtual method paintEvent (QPaintEvent * event). You also need to override the resizeEvent (QResizeEvent * event) method, since the borders of the text 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 after updating the displayed text after changing the list of model elements. Add the call to updateDisplayText () to the end of collectCheckedItems () and redraw the widget:
void MultiListWidget::setCheckedItems(const QStringList &items)
{
	...
	updateDisplayText();
	repaint();
}

In the styles of GTK and Mac, there is a bug in which the flags in the expanded list are not displayed. To work around this bug, you need to set the combobox-popup values ​​in the styleSheet of the widget (put this code in the constructor):
setStyleSheet("QComboBox { combobox-popup: 1px }");

Images:




Source Code:

multilist.h
#ifndef MULTILIST_H
#define MULTILIST_H
#include 
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(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(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(model());
	mCheckedItems.clear();
	for (int i = 0; i < count(); ++i)
	{
		QStandardItem *currentItem = standartModel->item(i);
		Qt::CheckState checkState = static_cast(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(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!

Also popular now: