songvan
songvan

Reputation: 379

How to arrange items according to groups using QSortFilterProxyModel?

I have a table like this

enter image description here

In this table, Abbreviation and Meaning are headers. When we click onto that, the items should be arranged. But the order I need when we click onto that is:

|Last Used               or        |Last Used 
|--FBI                             |--PIG
|--PIG                             |--FBI
|Something                         |Something
|--ADIDAS                          |--TEAM
|--DIET                            |--DIET
|--TEAM                            |--ADIDAS
|Lorem Ipsum                       |Lorem Ipsum
|--CLASS                           |--PwC
|--PMS                             |--PMS
|--PwC                             |--CLASS

which means, I arrange the items only inside each group (Last Used, Something, and Lorem Ipsum), the order of the groups should be remained.

These are my daten:

CompleterSourceModel.h

#include <QStandardItemModel>
#include <CompleterData.h>

class CompleterSourceModel : public QStandardItemModel
{
public:
  CompleterSourceModel( QObject *p_parent = nullptr );
  Qt::ItemFlags flags( const QModelIndex &index ) const override;
  void setCompleterData( const CompleterData &p_completerData );

private:
  CompleterData m_completerData;
};

CompleterSourceModel.cpp

#include "CompleterSourceModel.h"

CompleterSourceModel::CompleterSourceModel( QObject *p_parent ) : QStandardItemModel( p_parent )
{
}

Qt::ItemFlags CompleterSourceModel::flags( const QModelIndex &p_index ) const
{
  if ( !p_index.isValid() ) {
     return Qt::NoItemFlags;
  }

  CompleterDataRow::Type type = m_completerData.data().at( p_index.row() ).type();
  if ( type == CompleterDataRow::Type::Data || type == CompleterDataRow::Type::LastUsed ) {
     return Qt::ItemIsSelectable | Qt::ItemIsEnabled;
  }
  return Qt::NoItemFlags;
}

void CompleterSourceModel::setCompleterData( const CompleterData &p_completerData )
{
  m_completerData = p_completerData;
  setColumnCount( m_completerData.headers().size() + 1 );
  setRowCount( m_completerData.data().size() );

  for ( int col = 0; col <= m_completerData.headers().size(); col++ ) {
     col < m_completerData.headers().size() ? setHeaderData( col, Qt::Horizontal, m_completerData.headers().at( col ) ) : setHeaderData( col, Qt::Horizontal, {} );
  }

  for ( int row = 0; row < m_completerData.data().size(); row++ ) {
     for ( int col = 0; col <= m_completerData.headers().size(); col++ ) {
        if ( m_completerData.data().at( row ).type() == CompleterDataRow::Type::Header || m_completerData.data().at( row ).type() == CompleterDataRow::Type::SecondHeader ) {
           col == 0 ? setData( index( row, col ), m_completerData.data().at( row ).rowData().at( col ).first, Qt::EditRole ) : setData( index( row, col ), {}, Qt::EditRole );
        }
        else {
           col == m_completerData.headers().size() ? setData( index( row, col ), {}, Qt::EditRole ) : setData( index( row, col ), m_completerData.data().at( row ).rowData().at( col ).first, Qt::EditRole );
        }
        setData( index( row, col ), QVariant( static_cast<int>( m_completerData.data().at( row ).type() ) ), Qt::UserRole );
     }
  }
}

CompleterData.h

#include <QList>
#include <QPair>
#include <QVariant>
#include <QVector>

class CompleterDataRow
{
public:
  enum class Type
  {
     Header,
     SecondHeader,
     Data,
     LastUsed         
  };
  CompleterDataRow() = default;
  CompleterDataRow( const CompleterDataRow::Type p_rowType, const 
  QList<QPair<QString, QVariant>> &p_rowData );
  void setType( const CompleterDataRow::Type p_type );
  CompleterDataRow::Type type() const;
  QList<QPair<QString, QVariant>> rowData() const;
  void setRowData( const QList<QPair<QString, QVariant>> &p_rowData );

private:
  QList<QPair<QString, QVariant>> m_rowData;
  Type m_type;
};

class CompleterData
{
public:
  CompleterData() = default;
  QVector<CompleterDataRow> data() const;
  void setData( const QVector<CompleterDataRow> &p_data );
  void addData( const CompleterDataRow &p_rowData );
  void removeData( int p_row );
  void setHeaders( const QStringList &p_headers );
  void setTitle( const QString &p_label );
  const QStringList &headers() const;
  const QString &title() const;

private:
  QVector<CompleterDataRow> m_completerData;
  QString m_title;
  QStringList m_headers;
};

CompleterData.cpp

#include "CompleterData.h"

CompleterDataRow::CompleterDataRow( const CompleterDataRow::Type p_rowType, const QList<QPair<QString, QVariant>> &p_rowData )
{
  m_type = p_rowType;
  m_rowData = p_rowData;
}

QList<QPair<QString, QVariant>> CompleterDataRow::rowData() const
{
  return m_rowData;
}

void CompleterDataRow::setRowData( const QList<QPair<QString, QVariant>> &p_rowData )
{
  m_rowData = p_rowData;
}

CompleterDataRow::Type CompleterDataRow::type() const
{
  return m_type;
}

void CompleterDataRow::setType( const Type p_type )
{
  m_type = p_type;
}

QVector<CompleterDataRow> CompleterData::data() const
{
  return m_completerData;
}

void CompleterData::addData( const CompleterDataRow &p_rowData )
{
  m_completerData.append( p_rowData );
}

void CompleterData::removeData( int p_row )
{
  m_completerData.remove( p_row );
}

void CompleterData::setData( const QVector<CompleterDataRow> &p_data )
{
  m_completerData = p_data;
}

void CompleterData::setTitle( const QString &p_title )
{
  m_title = p_title;
}

const QString &CompleterData::title() const
{
  return m_title;
}

void CompleterData::setHeaders( const QStringList &p_headers )
{
  m_headers = p_headers;
}

const QStringList &CompleterData::headers() const
{
  return m_headers;
}

MyComboBox.h

#include <QComboBox>
#include <QTreeView>
#include "CompleterData.h"
#include "CompleterSourceModel.h"
#include "CompleterProxyModel.h"

class MyComboBox : public QComboBox
{      
public:
  MyComboBox( QWidget *p_parent = nullptr );
  CompleterData createTestData();
  void setDataForCompleter(const CompleterData &p_data);  // this function should be set in main.cpp in Qt Project so that problem can be reproduced

private:
  QTreeView *m_view = nullptr;
  CompleterSourceModel *m_sourceModel = nullptr;
  CompleterProxyModel *m_proxyModel =nullptr;
};

MyComboBox.cpp

#include "MyComboBox.h"
MyComboBox::MyComboBox( QWidget *p_parent ) : QComboBox( p_parent )
{
  setEditable( true );
  m_view = new QTreeView();
  m_sourceModel = new CompleterSourceModel( this );
  m_proxyModel = new CompleterProxyModel( this );
  m_proxyModel->setSourceModel(m_sourceModel);      
  setModel( m_proxyModel );
  setView( m_view );
}

void MyComboBox::setDataForCompleter(const CompleterData &p_data)
{
  m_sourceModel->setCompleterData( p_data );
} 

CompleterData MyComboBox::createTestData()  
{
  CompleterData data;
  data.addData( CompleterDataRow( CompleterDataRow::Type::Header, { { "Last Used", {} } } ) );
  data.addData( CompleterDataRow( CompleterDataRow::Type::Data, { { "FBI", {} }, { "Female Body Inspector", {} } } ) );
  data.addData( CompleterDataRow( CompleterDataRow::Type::Data, { { "PIG", {} }, { "Pretty Insensitive Guy", {} } } ) );
  data.addData( CompleterDataRow( CompleterDataRow::Type::Header, { { "Something", {}} } ) );
  data.addData( CompleterDataRow( CompleterDataRow::Type::LastUsed, { { "ADIDAS", {} }, {"All Day I Dream About Soccer", {} } } ) );
  data.addData( CompleterDataRow( CompleterDataRow::Type::LastUsed, { { "DIET", {}}, {"Do I eat today?", {}} } ) );
  data.addData( CompleterDataRow( CompleterDataRow::Type::Data, { { "TEAM", {} }, { "Together Everyone Achieves More", {} } } ) );
  data.addData( CompleterDataRow( CompleterDataRow::Type::SecondHeader, { { "Lorem Ipsum", {} } } ) );
  data.addData( CompleterDataRow( CompleterDataRow::Type::LastUsed, { { "CLASS", {}}, {"Come late and start sleeping", {}} } ) );
  data.addData( CompleterDataRow( CompleterDataRow::Type::LastUsed, { { "PMS", {}}, {"Purchase More Shoes", {}} } ) );
  data.addData( CompleterDataRow( CompleterDataRow::Type::LastUsed, { { "PwC", {}}, {"Partner want Cash", {}} } ) );
  data.addData( CompleterDataRow( CompleterDataRow::Type::Header, { { "Some Countries", {} } } ) );
  data.addData( CompleterDataRow( CompleterDataRow::Type::SecondHeader, { { "Some Cities", {} } } ) );
  data.setTitle( "Proposal List" );
  data.setHeaders( { "Abbreviation", "Meaning" } );
  return data;
}

main.cpp

#include "mycombobox.h"
#include <QApplication>

int main(int argc, char *argv[])
{
  QApplication a(argc, argv);
  MyComboBox combo;
  combo.setDataForCompleter(combo.createTestData());
  combo.show();
  return a.exec();
}

In order to satisfy the requirement I think I should use Proxymodel. But I have something wrong with my proxy model, so the result I become is like this, when I load the model (I did not click onto headers to sort items yet)

enter image description here

As you can see, the Lorem Ipsum is moved to the end of the list, the order right at the beginning is wrong. So I think I have mistakes in my proxy model. Could you show me where exactly in my proxy model? Or any other solutions are also welcomed. This is my proxy model:

CompleterProxyModel.h

#include <QSortFilterProxyModel>
#include <CompleterData.h>

class CompleterProxyModel : public QSortFilterProxyModel
{
public:
  CompleterProxyModel( QObject *p_parent = nullptr );

protected:    
  bool lessThan( const QModelIndex &p_left, const QModelIndex &p_right ) const override;
};

CompleterProxyModel.cpp

#include "CompleterProxyModel.h"

CompleterProxyModel::CompleterProxyModel( QObject *p_parent ) : QSortFilterProxyModel( p_parent )
{
}

bool CompleterProxyModel::lessThan( const QModelIndex &p_left, const QModelIndex &p_right ) const
{
  CompleterDataRow::Type leftType = static_cast<CompleterDataRow::Type>( p_left.data( Qt::UserRole ).toInt() );
  CompleterDataRow::Type rightType = static_cast<CompleterDataRow::Type>( p_right.data( Qt::UserRole ).toInt() );

  if ( ( leftType == CompleterDataRow::Type::Data && rightType == CompleterDataRow::Type::Data ) ||
        ( leftType == CompleterDataRow::Type::LastUsed && rightType == CompleterDataRow::Type::LastUsed ) )
  {

     QString leftString = p_left.data( Qt::EditRole ).toString();
     QString rightString = p_right.data( Qt::EditRole ).toString();
     qDebug() << leftString << rightString << QString::localeAwareCompare( leftString, rightString );

     return QString::localeAwareCompare( leftString, rightString ) < 0;
  }
  return false;
}

Upvotes: 0

Views: 514

Answers (2)

songvan
songvan

Reputation: 379

In this case I have just found a solution. I think the model should be updated as I import data into it. So I put beginResetModel() and endResetModel() into function setCompleterData and it works now.

void CompleterSourceModel::setCompleterData( const CompleterData &p_completerData )
{
  beginResetModel();

  m_completerData = p_completerData;
  setColumnCount( m_completerData.headers().size() + 1 );
  setRowCount( m_completerData.data().size() );

  for ( int col = 0; col <= m_completerData.headers().size(); col++ ) {
     col < m_completerData.headers().size() ? setHeaderData( col, Qt::Horizontal, m_completerData.headers().at( col ) ) : setHeaderData( col, Qt::Horizontal, {} );
  }

  for ( int row = 0; row < m_completerData.data().size(); row++ ) {
     for ( int col = 0; col <= m_completerData.headers().size(); col++ ) {
        if ( m_completerData.data().at( row ).type() == CompleterDataRow::Type::Header || m_completerData.data().at( row ).type() == CompleterDataRow::Type::SecondHeader ) {
       col == 0 ? setData( index( row, col ), m_completerData.data().at( row ).rowData().at( col ).first, Qt::EditRole ) : setData( index( row, col ), {}, Qt::EditRole );
     }
     else {
       col == m_completerData.headers().size() ? setData( index( row, col ), {}, Qt::EditRole ) : setData( index( row, col ), m_completerData.data().at( row ).rowData().at( col ).first, Qt::EditRole );
     }
     setData( index( row, col ), QVariant( static_cast<int>( m_completerData.data().at( row ).type() ) ), Qt::UserRole );
  }

  endResetModel();
}

Upvotes: 0

Dimitry Ernot
Dimitry Ernot

Reputation: 6584

You have two rules for your sorting proxy:

An item is a category. So it's a root item with no parent and you have to sort on the row number to keep the original order.

Otherwise, it's a child item and you can sort it by its value.

Your proxy is quite simple: if an item has a parent, sort it regarding to the row. Otherwise use the original sorting rules (or another one).

The only tricky part is the order (ascending or descending). So, you have to deal with it (or your category will be inverted).

For example:

class SortProxyModel: public QSortFilterProxyModel
{
public:
    SortProxyModel(): QSortFilterProxyModel() {}

    bool lessThan(const QModelIndex &left, const QModelIndex &right) const override
    {

        if (!left.parent().isValid())
        {
            if (sortOrder() == Qt::DescendingOrder) // Don't care about the order
                return left.row() > right.row();
            return left.row() < right.row();
        }
        return QSortFilterProxyModel::lessThan(left, right);
    }
};

The tests:

    QTreeView* view = new QTreeView();
    QStandardItemModel* model = new QStandardItemModel();
    QSortFilterProxyModel* proxy = new SortProxyModel();
    proxy->setSourceModel(model);

    model->setHorizontalHeaderLabels(QStringList() << "Col 1");

    QStandardItem* item1 = new QStandardItem("Last Used");
    item1->appendRows(QList<QStandardItem*>() << new QStandardItem("A") << new QStandardItem("C") << new QStandardItem("B"));
    QStandardItem* item2 = new QStandardItem("Something");
    item2->appendRows(QList<QStandardItem*>() << new QStandardItem("F") << new QStandardItem("E") << new QStandardItem("D"));
    QStandardItem* item3 = new QStandardItem("Lorem");
    item3->appendRows(QList<QStandardItem*>() << new QStandardItem("I") << new QStandardItem("G") << new QStandardItem("H"));
    model->appendRow(item1);
    model->appendRow(item2);
    model->appendRow(item3);
    view->setModel(proxy);

    view->setSortingEnabled(true);
    view->show();

Upvotes: 1

Related Questions