...

вторник, 11 марта 2014 г.

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







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

This entry passed through the Full-Text RSS service — if this is your content and you're reading it on someone else's site, please read the FAQ at http://ift.tt/jcXqJW.


Комментариев нет:

Отправить комментарий