LINUX.ORG.RU

QComboBox c множественным выбором

 , ,


0

1

Понадобилось реализовать выпадающий список с множественным выбором для программы на 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, но решить проблему не могу. Кто-нибудь может подсказать как поправить?


Для Qt, в те времена, когда он был популярный и актуальный, была специальная либа-расширение, которая называлась Qxt. Там вроде как был подобный Combobox.

https://libqxt.bitbucket.io/doc/tip/qxtcheckcombobox.html

Посмотри исходники Qxt, возможно они направят тебя на верное решение твоей проблемы.

https://github.com/strixcode/libqxt/blob/master/src/widgets/qxtcheckcombobox.cpp

Удачи.

EXL ★★★★★ ()
Ответ на: комментарий от next_time

Нет, в случае двух и более элементов все ок.

Как понимаю, сигнал activated() посылается также при разворачивании выпадающего списка и если отмечен только один элемент, то его состояние сбрасывается, т.к. именно его индекс и передается.

sergs ()
Ответ на: комментарий от EXL

Спасибо. Про Qxt знаю, туда и полез первым делом. К сожалению, насколько вижу у них используется такой же подход: меняется состояение по сигналу activated.

Пробую переделать на использование сигналов clicked или pressed от view(), но там вылезли сложности с обновлением текста отмеченных элементов.

sergs ()
Ответ на: комментарий от sergs

и если отмечен только один элемент, то его состояние сбрасывается, т.к. именно его индекс и передается

а если несколько отмечено, чей индекс передаётся? по идее, да, activate банально говорит о том, что итем выделен, соотв. итем он выделяется и следовательно сигнал срабатывает при раскрытии комбика, но тогда вообще всегда выбранный последним элемент должен отключаться

next_time ★★★★★ ()
Последнее исправление: next_time (всего исправлений: 1)
Ответ на: комментарий от sergs

Я бы сделал костылём: при открытии popup комбобокса, выставлять true в булевной переменной, что он только что открыт, проверять её в toggleItemCheckState,и тут же сбрасывать

Некостылём: править код QComboBox, чтобы добавить сигнал о смене подсвеченой позиции в выпадающем списке. но это не всегда приемлимо.

Ещё я бы попробовал сигнал activated из QAbstractItemView (а не самого комбика), но полагаю, вы такой вариант пробовали и не помогло

next_time ★★★★★ ()