Картинка для привлечения внимания
(возможно имеющая отношение к посту)
Иногда, довольно удобным бывает возможность множественного выбора в виджете 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();
}
Спасибо за внимание!
Автор: blackmaster