Множественный выбор в QComboBox

в 23:10, , рубрики: c++, qt, Qt Software, весна, медведи, природа, Программирование, С++, метки: , , , ,

Множественный выбор в QComboBox
Картинка для привлечения внимания
(возможно имеющая отношение к посту)

Иногда, довольно удобным бывает возможность множественного выбора в виджете QComboBox. В этом небольшом туториале будет показано, как это cделать.

Основная идея состоит в том, что элементам модели, используемой в QComboBox, необходимо поднять флажок Qt::ItemIsUserCheckable, таким образом сделав их отмечаемыми. А также позаботится о выводе списка отмеченых элементов на виджете.

Объявим класс MultiListWidget (свойство и соответствующие методы checkedItems дают доступ к списку элементов, которые мы предварительно установили или которые отметил пользователь, а метод collectCheckedItems сохраняет отмеченные элементы модели в 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();

};

В модели QComboBox есть несколько нужных нам сигналов:

  • rowsInserted(const QModelIndex &parent, int start, int end) — при добавлении элементов в модель (вызов методов addItem, insertItem и т.д.)
  • rowsRemoved(const QModelIndex &parent, int start, int end) — при удалении элементов из модели (вызов метода removeItem)

Также пригодится itemChanged(QStandardItem *item), который испускается при установке и снятии флажка (пользователем или программно).

Объявим слоты для этих сигналов:

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

И свяжем сигналы со слотами в конструкторе (обратите внимание, что model() возвращает указатель на QAbstractItemModel, а сигнал itemChanged испускается в QStandardItemModel, поэтому тут необходимо приведение):

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()
{
}

Теперь, реализуем методы checkedItems() и setCheckedItems(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);
		}
	}

	// присоединяемся к сигналу
	disconnect(standartModel, SIGNAL(itemChanged(QStandardItem*)), this, SLOT(slotModelItemChanged(QStandardItem*)));

	// обновляем список отмеченных элементов
	collectCheckedItems();
}

Внутри метода collectCheckedItems() всё просто — пробегаемся по элементам модели, если он отмечен, добавляем в список:

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());
		}
	}
}

При вставке нового элемента в модель нам необходимо указать, что он будет отмечаемым пользователем и изначально со снятым флажком:

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*)));
}

При удалении элементов из модели, также нужно удалить их из mCheckedItems. Воспользуемся collectCheckedItems():

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

	collectCheckedItems();
}

В слоте slotModelItemChanged(QStandardItem *item) собираем отмеченные элементы:

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

	collectCheckedItems();
}

Поместим объявление класса и его реализацию в, соответственно, multilist.h и multilist.cpp, и попробуем MultiListWidget в деле (файл 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();
}

Неплохо, но осталось еще вывести на виджете список отмеченных элементов. Для этого объявим (в закрытой секции) в классе переменную для хранения текста для вывода, дельту для прямоугольника (объяснение будет ниже), в пределах которого будет рисовать этот текст, и метод, обновляющий текст для вывода при изменении списка отмеченных элементов:

QString mDisplayText;
const QRect mDisplayRectDelta;

void updateDisplayText();

Добавим в конструктор инициализацию mDisplayRectDelta:

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

Теперь, рассмотрим подробнее 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 += "...";
	}
}

Для отрисовки текста необходимо переопределить виртуальный метод paintEvent(QPaintEvent *event). Также нужно переопределить метод resizeEvent(QResizeEvent *event), так как границы текста при изменении размера виджета изменятся. Вот объявление этих методов:

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

И их реализация:

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();
}

Осталось только после обновлять выводимый текст после изменения списка элементов модели. Добавим в конец collectCheckedItems() вызов updateDisplayText() и перерисуем виджет:

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

В стилях GTK и Mac есть баг, при котором не отображаются флажки в развёрнутом списке. Для временного решения этого бага нужно установить значения combobox-popup в styleSheet виджета (поместите этот код в конструктор):

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

Изображения:
Множественный выбор в QComboBox

Множественный выбор в QComboBox

Исходный код:

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();
}

Спасибо за внимание!

Автор: blackmaster

Источник


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js