Понадобилось реализовать выпадающий список с множественным выбором для программы на Qt5. Для этого реализовал модель, у элементов которой выставлен флаг Qt::ItemIsUserCheckable
class ItemModel : public QStandardItemModel
{
Q_OBJECT
public:
ItemModel(QObject *parent = nullptr);
Qt::ItemFlags flags(const QModelIndex &index) const override;
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
bool setData(const QModelIndex &index, const QVariant &value, int role = Qt::EditRole) override;
signals:
void itemCheckStateChanged();
};
ItemModel::ItemModel(QObject *parent)
: QStandardItemModel(0, 1, parent)
{
}
Qt::ItemFlags ItemModel::flags(const QModelIndex &index) const
{
return QStandardItemModel::flags(index) | Qt::ItemIsUserCheckable;
}
QVariant ItemModel::data(const QModelIndex &index, int role) const
{
QVariant value = QStandardItemModel::data(index, role);
if (index.isValid() && role == Qt::CheckStateRole && !value.isValid())
{
value = Qt::Unchecked;
}
return value;
}
bool ItemModel::setData(const QModelIndex &index, const QVariant &value, int role)
{
bool ok = QStandardItemModel::setData(index, value, role);
if (ok && role == Qt::CheckStateRole)
{
emit dataChanged(index, index);
emit itemCheckStateChanged();
}
return ok;
}
и собственно сам combobox с отображением флажков
class CComboBox : public QComboBox
{
Q_OBJECT
public:
CComboBox(QWidget *parent = nullptr);
QString separator() const;
void setSeparator(const QString &separator);
QString defaultText() const;
void setDefaultText(const QString &text);
QStringList checkedItems() const;
Qt::CheckState itemCheckState(int index) const;
void setItemCheckState(int index, Qt::CheckState state);
void toggleItemCheckState(int index);
void hidePopup() override;
bool eventFilter(QObject *object, QEvent *event) override;
signals:
void checkedItemsChanged(const QStringList &items);
public slots:
void setCheckedItems(const QStringList &items);
protected:
void resizeEvent(QResizeEvent *event) override;
protected slots:
void showContextMenu(QPoint pos);
void selectAllOptions();
void deselectAllOptions();
private:
void updateCheckedItems();
void updateDisplayText();
QString mSeparator;
QString mDefaultText;
bool mSkipHide = false;
QMenu *mContextMenu = nullptr;
QAction *mSelectAllAction = nullptr;
QAction *mDeselectAllAction = nullptr;
};
CComboBox::CComboBox(QWidget *parent) : QComboBox(parent), mSeparator(QStringLiteral(", "))
{
setModel(new ItemModel(this));
QLineEdit *lineEdit = new QLineEdit(this);
lineEdit->setReadOnly(true);
setLineEdit(lineEdit);
mContextMenu = new QMenu(this);
mSelectAllAction = mContextMenu->addAction(tr("Select All"));
mDeselectAllAction = mContextMenu->addAction(tr("Deselect All"));
connect(mSelectAllAction, &QAction::triggered, this, &CComboBox::selectAllOptions);
connect(mDeselectAllAction, &QAction::triggered, this, &CComboBox::deselectAllOptions);
view()->viewport()->installEventFilter(this);
view()->setContextMenuPolicy(Qt::CustomContextMenu);
connect(view(), &QAbstractItemView::customContextMenuRequested, this, &CComboBox::showContextMenu);
ItemModel *myModel = qobject_cast<QgsCheckableItemModel *>(model());
connect(myModel, &QgsCheckableItemModel::itemCheckStateChanged, this, &CComboBox::updateCheckedItems);
connect(model(), &QStandardItemModel::rowsInserted, this, [ = ](const QModelIndex &, int, int) { updateCheckedItems(); });
connect(model(), &QStandardItemModel::rowsRemoved, this, [ = ](const QModelIndex &, int, int) { updateCheckedItems(); });
connect(this, static_cast< void (QComboBox::*)(int) >(&QComboBox::activated), this, &CComboBox::toggleItemCheckState);
}
QString CComboBox::separator() const
{
return mSeparator;
}
void CComboBox::setSeparator(const QString &separator)
{
if (mSeparator != separator)
{
mSeparator = separator;
updateDisplayText();
}
}
QString CComboBox::defaultText() const
{
return mDefaultText;
}
void CComboBox::setDefaultText(const QString &text)
{
if (mDefaultText != text)
{
mDefaultText = text;
updateDisplayText();
}
}
QStringList CComboBox::checkedItems() const
{
QStringList items;
if (model())
{
QModelIndex index = model()->index(0, modelColumn(), rootModelIndex());
QModelIndexList indexes = model()->match(index, Qt::CheckStateRole, Qt::Checked, -1, Qt::MatchExactly);
const auto constIndexes = indexes;
for (const QModelIndex &index : constIndexes)
{
items += index.data().toString();
}
}
return items;
}
Qt::CheckState CComboBox::itemCheckState(int index) const
{
return static_cast<Qt::CheckState>(itemData(index, Qt::CheckStateRole).toInt());
}
void CComboBox::setItemCheckState(int index, Qt::CheckState state)
{
setItemData(index, state, Qt::CheckStateRole);
}
void CComboBox::toggleItemCheckState(int index)
{
QVariant value = itemData(index, Qt::CheckStateRole);
if (value.isValid())
{
Qt::CheckState state = static_cast<Qt::CheckState>(value.toInt());
setItemData(index, (state == Qt::Unchecked ? Qt::Checked : Qt::Unchecked), Qt::CheckStateRole);
}
}
void CComboBox::hidePopup()
{
if (!mSkipHide)
{
QComboBox::hidePopup();
}
mSkipHide = false;
}
void CComboBox::showContextMenu(QPoint pos)
{
Q_UNUSED(pos)
mContextMenu->exec(QCursor::pos());
}
void CComboBox::selectAllOptions()
{
blockSignals(true);
for (int i = 0; i < count(); i++)
{
setItemData(i, Qt::Checked, Qt::CheckStateRole);
}
blockSignals(false);
updateCheckedItems();
}
void CComboBox::deselectAllOptions()
{
blockSignals(true);
for (int i = 0; i < count(); i++)
{
setItemData(i, Qt::Unchecked, Qt::CheckStateRole);
}
blockSignals(false);
updateCheckedItems();
}
bool CComboBox::eventFilter(QObject *object, QEvent *event)
{
if ((event->type() == QEvent::MouseButtonPress || event->type() == QEvent::MouseButtonRelease) && object == view()->viewport())
{
mSkipHide = true;
}
if (event->type() == QEvent::MouseButtonRelease)
{
if (static_cast<QMouseEvent *>(event)->button() == Qt::RightButton)
{
return true;
}
}
return QComboBox::eventFilter(object, event);
}
void CComboBox::setCheckedItems(const QStringList &items)
{
const auto constItems = items;
for (const QString &text : constItems)
{
const int index = findText(text);
setItemCheckState(index, index != -1 ? Qt::Checked : Qt::Unchecked);
}
}
void CComboBox::resizeEvent(QResizeEvent *event)
{
QComboBox::resizeEvent(event);
updateDisplayText();
}
void CComboBox::updateCheckedItems()
{
QStringList items = checkedItems();
updateDisplayText();
emit checkedItemsChanged(items);
}
void CComboBox::updateDisplayText()
{
QString text;
QStringList items = checkedItems();
if (items.isEmpty())
{
text = mDefaultText;
}
else
{
text = items.join(mSeparator);
}
QRect rect = lineEdit()->rect();
QFontMetrics fontMetrics(font());
text = fontMetrics.elidedText(text, Qt::ElideRight, rect.width());
setEditText(text);
}
В целом все работает, но есть один непонятный баг. Если отметить флажками два элемента (или больше), а затем, не закрывая выпадающий список, снять флажки у всех элементов, кроме одного и щелкнуть за пределами списка чтобы закрыть его, то у единственного отмеченого элемента практически всегда сбрасывается состояние на неотмеченное. Подозреваю, что это каким-то образом связано з сигналом activated
, но решить проблему не могу. Кто-нибудь может подсказать как поправить?