Qt Forum

    • Login
    • Search
    • Categories
    • Recent
    • Tags
    • Popular
    • Users
    • Groups
    • Search
    • Unsolved

    Unsolved QComboBox with checkable items: issue with single checked item

    General and Desktop
    qcombobox qcheckbox
    2
    3
    654
    Loading More Posts
    • Oldest to Newest
    • Newest to Oldest
    • Most Votes
    Reply
    • Reply as topic
    Log in to reply
    This topic has been deleted. Only users with topic management privileges can see it.
    • V
      voltron last edited by

      I'm working on a multi-select combobox. It is implemented using custom model

      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;
      }
      

      and QComboBox subclass

      class GUI_EXPORT 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 );
      }
      

      This implementation works almost fine. There is only one problem: if I check two or more items in the combobox, then uncheck all items except one and click outside combobox to close its popup, the item that was still checked is also become unchecked most of the times.
      I suspect that this somehow related to use of activated() signal to toggle item checkstate, but stuck with finding a solution. Any ideas how to fix this issue?

      1 Reply Last reply Reply Quote 0
      • V
        voltron last edited by

        Apparently the problem is activated() signal which is sent even when the choice is not changed. So if I have a single item selected then in the slot item state it toggled to the unchecked state.

        I have tried to use clicked() signal of the combobox's view but it does not work at all, slot never called.

        I'm on Linux with Qt 5.9.5.

        1 Reply Last reply Reply Quote 0
        • ptstream
          ptstream last edited by

          I think I have found a solution that is not necessarily complete (e.g. drive the combbox with keyboard).

          1. I add a boolean (e.g. mSkipToogle=false) at the same place than mSkipHide.

          2. I rewrite eventFilter (QObject* object, QEvent* event).

          bool CFilterCountryComboBox::eventFilter (QObject* object, QEvent* event)
          {
            QEvent::Type type = event->type ();
            if (object == view ()->viewport ())
            {
              switch (type)
              {
                case QEvent::MouseButtonRelease :
                case QEvent::MouseButtonPress :
                  mSkipHide   = true;
                  mSkipToogle = false;
                  break;
          
                case QEvent::Hide :
                case QEvent::Show :
                  mSkipToogle = true;
                 break;
          
                default :
                  break;
              }
            }
          
            if (type == QEvent::MouseButtonRelease && static_cast<QMouseEvent*>(event)->button () == Qt::RightButton)
            {
              return true;
            }
          
            return QComboBox::eventFilter (object, event);
          }
          
          1. Now rewrite toggleItemCheckState (int index)
          void CFilterCountryComboBox::toggleItemCheckState (int index)
          {
            if (!mSkipToogle)
            {
              QVariant value = itemData (index, Qt::CheckStateRole);
              Qt::CheckState state = static_cast<Qt::CheckState>(value.toInt ());
              setItemData (index, (state == Qt::Unchecked ? Qt::Checked : Qt::Unchecked), Qt::CheckStateRole);
            }
            else
            {
              mSkipToogle = false;
            }
          }
          

          It seems to work on Windows with Qt 5.12.7.
          Good luck

          1 Reply Last reply Reply Quote 0
          • First post
            Last post