Множественный выбор в 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 }"); 

Изображения:

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

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

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

ссылка на оригинал статьи http://habrahabr.ru/post/215289/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *