![](http://habrastorage.org/getpro/habr/post_images/9a6/ae0/28e/9a6ae028e8c259b38c7229130dcb7271.png)
Картинка для привлечения внимания
(возможно имеющая отношение к посту)
Иногда, довольно удобным бывает возможность множественного выбора в виджете 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 }");
Изображения:
Исходный код:
#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
#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(); }
#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/
Добавить комментарий