Important: Please read the Qt Code of Conduct - https://forum.qt.io/topic/113070/qt-code-of-conduct

QTableview confusion



  • Hi guys

    i am starting to use QTableView with QAbstractItemModel and the Delegates as it should be used. Or at least i try.
    In the past i derived my own tables from QTableView and basically did everything myself (updating, sorting, filtering ). I didnt use any QStandardItemModel-Feature expect creating QStandardItems or deleting them. Most things worked ( Celltext, colors, etc.) but i think i made things way to complicated. Never got DragDrop to work with my first approach.

    So now i want to do it the "right" way, and that is where things appear strange to me.

    I have my TableView and my derived model, and have some questions:
    What i want:

    • Row-based table, ( constant number of columns, dynamic number of rows )
    • Sortable, Filterable ( hide some rows, show some )
    • Drag/Move/Drop between two tables of the same type(model).

    So the first questions:

    Where do i store my "user-data" ?
    As e.g. std::map in my derived model?

    How do i assign one user-data-item to a specific row?
    As the models arent row-based, but cell-based i have to assign the same set of user-data to all cells of a row, correct?

    When implementing the data()-method of the model:

    QVariant MyTestModel::data(const QModelIndex &index, int role ) const
    {
        switch ( index.column() )
        {
            case 0 : todo
            case 1 : todo
        }
        return QVariant();
    }
    

    How can i find the correct user-data inside the "data()"-method when i only have a modelindex? When sorting the table, the row-numbers may change. So i cant use the row-number as an index to my map of userdata-items.

    What do i exactly return there? A single QVariant cannot represent the complete "design" of a cell? ( e.g. TextColor, BackgroundColor, Text ).
    I can just return a number or a string, nothing more, right?

    What worked so far is that one:

    QVariant MyTestModel::headerData( int column, Qt::Orientation orientation, int role ) const
    {
        if ( role != Qt::DisplayRole || orientation != Qt::Horizontal )
            return QVariant();
        switch ( column )
        {
            case 0 : return "Column1";
            case 1 : return "Column2";
        }
    
        return QVariant();
    }
    

    which also make sense to me, as the columns dont move and are always visible so using the "int column" to obtain the name of the column is valid.

    Next one:
    I return the count of my userdata-items. But what if some rows are hidden? Does rowcount refer to the visible rows, or to all rows?

    int	MyTestModel::rowCount( const QModelIndex &index ) const
    {
        return UserData.size();
    }
    

    greetings & thanks so far
    toby



  • @Toby
    There are so many questions here it's impossible to keep up!

    Suffice to say, yes, you do everything at the model side.

    You will want to use QSortFilterProxyModel to do both your sorting & filtering. You interpose that between model & view, so QAbstractItemModel -> QSortFilterProxyModel -> QTableView. Your indexes in QAbstractItemModel will retain the correct row & column for your model, unaffected by the QSortFilterProxyModel you have imposed between it & the view.

    You can store your underlying data in whatever data structure you please. You must be able to map row/column to & from your data and the model's indexes/data accesses. Yes, your data() override will have a big switch statement mapping between column numbers and the types/fields of your underlying data.

    Look carefully at the role parameter to data (and setData() and headerData()). Only DisplayRole/EditRole map to/from your data. Qt will call your code with other values for the role, for text color etc. data() and headerData() are different methods for different purposes.

    As I say, start out by inserting the QSortFilterProxyModel and get it working. Most of your questions about, say, hidden will go away/be resolved.



  • Thank u for your valuable response! QSortFilterProxyModel is new to me, and i will focus on that. do i have to derive from QSortFilterProxyModel or can i use it as it is?

    There are so many questions because i don't keep up with the complex model-related stuff :D
    So i do basically everything with the model, and the view is only "the frame" ?



  • @Toby
    Start off with QSortFilterProxyModel as-is. You can derive from it later if you need to, e.g. to do your own custom sorting, or filtering. But get it working as-is to start with.

    The view is supposed to be only for the actual visibles of items which are shown. Everything else/most things should be done in the model. Note how Qt uses the role parameter to data() to allow the model to specify what color/font/alignment etc, it wants, then the view respects just that. And we are using QSortFilterProxyModel to so both sorting & filtering, we don't do those in the view (nor in the base model).


  • Lifetime Qt Champion

    Hi
    Just as a note.
    This example is showing off a custom model with sorting
    https://doc.qt.io/qt-5/qtwidgets-itemviews-addressbook-example.html



  • Thank u so far.

    I have at least understood the basics of Models.

    
    QVariant MyItemModel::data(const QModelIndex &index, int role ) const
    {
        if ( !index.isValid() || index.column() >= columnCount() || index.row() < 0 || index.row() >= (int)UserData.size() )
            return QVariant();
        if ( role == Qt::DisplayRole )
        {
            auto item = UserData[ index.row() ].second;
            switch ( index.column() )
            {
                case 0 : return item->text1();
                case 1 : return item->text2();
                default: break;
            }
        }
        else if ( role == Qt::TextAlignmentRole )
        {
            switch ( index.column() )
            {
                case 0 : return QVariant( Qt::AlignCenter );
                case 1 : return QVariant( Qt::AlignLeft | Qt::AlignVCenter );
                default: break;
            }
        }
        else if ( role == Qt::BackgroundRole )
        {
            return QVariant( QColor( 255, 0, 0 ) );
        }
        else if ( role == Qt::ForegroundRole )
        {
            return QVariant( QColor( 255, 255, 0 ) );
        }
        return QVariant();
    }
    

    The data()-Method seems to be more or less the root of all evil. I didnt insert anything into the view, i just specified the number of rows (and columns), and by calling the data()-Method the model seems to request all needed information from me for every cell.
    So far so good. Not as complicated as expected.

    Now i am trying to attach the proxy model....



  • @Toby
    Good start! You can omit all those explicit QVariant( ... ) conversions, C++ will do that for you from function signature QVariant MyItemModel::data(), e.g just return Qt::AlignCenter; or return QColor( 255, 0, 0 );.



  • I explicitely used that QVariant, everywhere because it doesnt work for combined Alignment-Flags like "Qt::AlignLeft | Qt::AlignVCenter". But it works at least for single alignments, colors and strings.

    Working on filtering at the moment, but "filterAcceptsRow" never gets called.
    Still a lot to learn ;-)



  • @Toby
    filterAcceptsRow does get called. You have to subclass QSortFilterProxyModel to re-implement/place a breakpoint etc.



  • My subclassed QSortFilterProxyModel still doesnt work.
    At the moment it's basically a fulltextsearch, where the SearchText gets specified via slot and calls invalidateFilter().

    Header-File:

    class ProxyModel : public QSortFilterProxyModel
    {
        Q_OBJECT
    public:
        ProxyModel( QObject *parent_ ) : QSortFilterProxyModel( parent_ ) {}
        virtual ~ProxyModel() = default;
    
        bool filterAcceptsRow( int, const QModelIndex & ) const override;
        QVariant headerData( int, Qt::Orientation, int ) const override;
        bool lessThan( const QModelIndex &, const QModelIndex & ) const override;
    
    public slots:
        void setSearchText( const QString & );
    
    private:
        QString SearchText = "";
    };
    

    Source-File:

    bool ProxyModel::lessThan( const QModelIndex &left, const QModelIndex &right ) const
    {
        qDebug() << "lessThan";
        return  true;
    }
    
    
    bool ProxyModel::filterAcceptsRow( int source_row, const QModelIndex & ) const
    {
        qDebug() << "filterAcceptsRow";
        return true;
    }
    
    
    QVariant ProxyModel::headerData(int section, Qt::Orientation orientation, int role ) const
    {
        return sourceModel()->headerData( section, orientation, role );
    }
    
    void ProxyModel::setSearchText( const QString &text_ )
    {
        SearchText = text_;
        qDebug() << "New Searchtext: " << SearchText;
        invalidateFilter();
    }
    
    

    MainWindow:

        auto tbl = new QTableView( h.parentWidget() );
        h.addWidget( tbl );
        tbl->setEditTriggers( QAbstractItemView::NoEditTriggers );
    
        model = new MyTableModel( this );
        proxy = new ProxyModel( this );
        proxy->setSourceModel( model );
        proxy->setFilterRole( MyTableModel::Name );
    
        tbl->setModel( model );
        tbl->verticalHeader()->hide();
        tbl->setSelectionBehavior( QAbstractItemView::SelectionBehavior::SelectRows );
        tbl->setColumnWidth( 0, 100 );
        tbl->horizontalHeader()->setSectionResizeMode( 1, QHeaderView::Stretch );
        tbl->verticalHeader()->setDefaultSectionSize( 20 );
        tbl->setSortingEnabled( true );
        tbl->sortByColumn( 0, Qt::DescendingOrder );
        connect( edTextSearch, SIGNAL(textChanged(QString)), proxy, SLOT(setSearchText(QString)));
    

  • Lifetime Qt Champion

    Hi,

    @Toby said in QTableview confusion:

    tbl->setModel( model );

    That's your issue, you are setting your model rather than the proxy, therefore it's not used.



  • Now i dont see anything. My data is not visualized anymore. And filterAcceptsRow() isn't called either.

    But it is correct to fill the SourceModel with all the user-data ( data()-Method ) and to only override the methods "lessThan()" and "filterAcceptsRow()" in the ProxyModel?



  • Just as a test i also tried overriding some other methods:

    int ProxyModel::columnCount(const QModelIndex &index ) const
    {
        return sourceModel()->columnCount( index );
    }
    
    int	ProxyModel::rowCount( const QModelIndex &index ) const
    {
        return sourceModel()->rowCount( index );
    }
    
    QVariant ProxyModel::data(const QModelIndex &index, int role ) const
    {
        return sourceModel()->data( index, role );
    }
    
    

    but the data()-method of the ProxyModel doesnt get called. Defining rowCount() and columnCount() at least made the view draw the Grid-Lines. But no visible content so far in the view...

    Ok when i use the default QSortFilterProxyModel it works ( of course my fulltextsearch doesnt work with the default proxymodel ). So it's an issue with the subclassing....

    It works, using QStandardItemModel and QSortFilterProxyModel ... but it doesnt work using my own model and QSortFilterProxyModel

    What i found out:
    Using my own Data-Model and the default QSortFilterProxyModel , only the following methods of my model get called:

    • rowCount()
    • columnCount()
    • headerData()

    Overridden methods that don't get called:

    • data()
    • index()
    • parent()


  • @Toby
    There are a couple of things wrong in your implementation. Whether they cause the behaviour you see I cannot say, but you should get the code right before you do any investigations/analysis.

    • ProxyModel::lessThan(): Yours always goes return true. You must not do this, it will confuse any sorting. You will find the Qt infrastructure calls lessThan() with the same model indexes but reversing left and right. By returning true in both cases you can make it impossible for it to sort correctly.

    • When writing a QSortFilterProxyModel overridden function which deals with QIndexes you must be aware of whether indexes point into the proxy model or the source model. You may need to map correctly between these. See the QSortFilterProxyModel::mapFrom/To...() methods, and use as appropriate.

    There is a Custom Sort/Filter Model Example. You should look carefully at that, test it works, then compare against your code.



  • @JonB i downloaded that example here:
    https://www.walletfox.com/course/qsortfilterproxymodelexample.php
    which works fine for me. He subclasses QAbstractTableModel, while i subclass QAbstractItemModel. I did the same but it did not improve anything.
    atm i am using the default QSortFilterProxyModel to reduce complexity. Seems like there is still an issue with my TableModel (sourcemodel).



  • I've found out about my issue.

    The problem was, that i created view and models first, and initiated the userdata a little later via setter-Function. Now that i deliver the data already within the models constructor, it works perfectly, also with my subclassed QSortFilterProxyModel.

    So the real issue was the update/invalidate-functionality. I probably did something wrong.

    This is how i updated my models userdata:

        UserData.clear();
        UserData.reserve( data_.size() );
        for ( auto &it : data_ )
            UserData.push_back( it );
        QModelIndex topleft = index( 0, 0 );
        QModelIndex bottomright = index( rowCount() - 1, columnCount() - 1 );
        emit dataChanged( topleft , bottomright );
    

    And that dataChanged-signal doesn't seem to do what i expected it to do...

    Edit:

    My solution is now to use beginModelReset() and endModelReset around the updating-process.

    beginModelReset();
    UserData.clear();
    UserData.reserve( data_.size() );
    for ( auto &it : data_ )
        UserData.push_back( it );
    endModelReset();
    

    which works perfectly, although it is probably overkill, but an easy way if one doesn't know where the changes exactly occur.

    Thanks to all of you for your help! :-)


  • Lifetime Qt Champion

    @Toby said in QTableview confusion:

    which works perfectly, although it is probably overkill, but an easy way if one doesn't know where the changes exactly occur.

    It this case it's not overkill but the right thing to do. You complexly nuke your model content and repopulate it so it is, in fact, a reset.


Log in to reply