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

Row Selection and Image Cells with QML TableView & Qt 5.12.11 & Qt Quick Controls 2



  • I am having some trouble figuring how to to use the TableView for my situation.

    I have a simple sample project here which demonstrate the problem.

    I've included below what I believe is the relevant source, but the remainder is available in the project link above or I can edit and include more if useful.

    Row Selection

    I am looking at the TableView documentation here. I do not see any mention of how to support row selection. If I look here, I see documentation for 5.15, where row selection is described. And, if I look here, I see some documentation for row selection for Qt Quick Controls 1, but that also does not apply to my situation.

    For Qt 5.12 and Qt Quick Controls 2, I am having trouble locating the appropriate documentation.
    How do I support row selection in a TableView for my case? How can I find the correct documentation for my situation?

    Image Cells

    Based on some research, it appears that I need to use the Qt::DecorationRole in my data function and return an image when the column is 1. However, that part of the code is never executed. I am missing some important and obvious about how the role concept works with Qt QML TableView's.

    What do I need to change so I can draw a circle in Column 1 (average age)? I'd like this circle to be red if the age < 13, yellow if < 35, and green otherwise.

    main.qml

    import QtQuick 2.12
    import QtQuick.Controls 2.12
    import QtQuick.Layouts 1.12
    
    import Backend 1.0
    
    ApplicationWindow
    {
      id:      root
      visible: true
    
      width:  768
      height: 450
    
      minimumWidth:  768
      minimumHeight: 450
    
      property string backendReference: Backend.objectName
    
      TableView
      {
        id: tableView
    
        columnWidthProvider: function( column )
        {
          return 100;
        }
    
        rowHeightProvider: function( column )
        {
          return 23;
        }
    
        anchors.fill: parent
        topMargin:    columnsHeader.implicitHeight
    
        model: Backend.modelResults.list
    
        ScrollBar.horizontal: ScrollBar {}
        ScrollBar.vertical:   ScrollBar {}
    
        clip: true
    
        delegate: Rectangle
        {
          Text
          {
            text: display
            anchors.fill: parent
            anchors.margins: 10
            color: 'black'
            font.pixelSize: 15
            verticalAlignment: Text.AlignVCenter
          }
        }
    
        Rectangle // mask the headers
        {
          z: 3
    
          color: "#222222"
    
          y: tableView.contentY
          x: tableView.contentX
    
          width:  tableView.leftMargin
          height: tableView.topMargin
        }
    
        Row
        {
          id: columnsHeader
          y:  tableView.contentY
    
          z: 2
    
          Repeater
          {
            model: tableView.columns > 0 ? tableView.columns : 1
    
            Label
            {
              width:  tableView.columnWidthProvider(modelData)
              height: 35
    
              text: Backend.modelResults.list.headerData( modelData, Qt.Horizontal )
    
              font.pixelSize:    15
              padding:           10
              verticalAlignment: Text.AlignVCenter
    
              background: Rectangle
              {
                color: "#eeeeee"
              }
            }
          }
        }
    
        ScrollIndicator.horizontal: ScrollIndicator { }
        ScrollIndicator.vertical: ScrollIndicator { }
      }
    }
    

    modeldata.cpp

    #include "modeldata.h"
    
    //
    // ModelList
    //
    ModelList::
    ModelList( QObject* parent )
        : QAbstractTableModel (parent )
    {
    }
    
    
    
    int
    ModelList::
    rowCount(const QModelIndex &) const
    {
       int size = mList.size();
    
       return size;
    }
    
    
    
    int
    ModelList::
    columnCount( const QModelIndex & ) const
    {
       return 2;
    }
    
    
    
    
    QVariant
    ModelList::
    data( const QModelIndex& index, int role ) const
    {
        const ModelItem modelItem = mList.at( index.row() );
    
        QVariant result = QVariant();
    
        if ( role == Qt::DisplayRole )
        {
            if ( index.column() == 0 )
            {
              result = QVariant( QString( modelItem.population ) );
            }
            else
            {
              result = QVariant( QString::number( modelItem.averageAge ) );
            }
        }
        else if ( role == Qt::DecorationRole )
        {
            qDebug() << "decorate 1";
        }
    
        return result;
    }
    
    
    
    QVariant
    ModelList::
    headerData( int section, Qt::Orientation orientation, int role ) const
    {
        if ( section == 0 )
            return QVariant( QString( "Population" ) );
        else
            return QVariant( QString( "Average Age" ) );
    }
    
    
    
    int
    ModelList::
    size() const
    {
        return mList.size();
    }
    
    
    
    const QList<ModelItem>&
    ModelList::
    list() const
    {
        return mList;
    }
    
    
    
    void
    ModelList::
    removeAt( int index )
    {
        if ( index < 0 || index >= mList.size() )
            return;
    
        beginRemoveRows( QModelIndex(), index, index );
        mList.removeAt( index );
        endRemoveRows();
    
        emit sizeChanged();
    }
    
    
    
    void
    ModelList::
    add( const QString& population, const int averageAge )
    {
        ModelItem item;
    
        item.population = population;
        item.averageAge = averageAge;
    
        add( item );
    }
    
    
    
    void
    ModelList::
    add(const ModelItem& item)
    {
        const int index = mList.size();
    
        beginInsertRows( QModelIndex(), index, index );
        mList.append( item );
        endInsertRows();
    
        emit sizeChanged();
    }
    
    
    
    void
    ModelList::
    reset()
    {
        if ( mList.isEmpty() )
            return;
    
        beginRemoveRows( QModelIndex(), 0, mList.size() - 1 );
        mList.clear();
        endRemoveRows();
    
        emit sizeChanged();
    }
    
    
    
    //
    // ModelResults
    //
    ModelResults::ModelResults(QObject* parent)
        : QObject(parent)
    {
        mList = new ModelList( this );
    
        qRegisterMetaType<ModelItem>("ModelItem");
    }
    
    ModelList* ModelResults::list() const
    {
        return mList;
    }
    
    void ModelResults::reset()
    {
        mList->reset();
    }


  • I have been able to get the correct circle drawn in the averageAge field.

    My ModelItem looks like:

    struct ModelItem
    {
        Q_GADGET
    
        Q_PROPERTY( QString population MEMBER population )
        Q_PROPERTY( int averageAge MEMBER averageAge )
        Q_PROPERTY( bool selected MEMBER selected )
    
    public:
    
        enum class Role {
          Selection = Qt::UserRole,
          ColumnType,
          ColorValue
        };
        Q_ENUM(Role)
    
        QString population;
        int     averageAge;
        bool    selected    { false };
    
        bool operator!=( const ModelItem& other )
        {
            return other.population != this->population
                || other.averageAge != this->averageAge;
        }
    
    };
    

    The key point here is the definition of the ColumnType and ColorValue Role.

    I needed a roleNames function for my custom role

    QHash<int, QByteArray>
    ModelList::
    roleNames() const
    {
      return {
        { Qt::DisplayRole, "display" },
        { int( ModelItem::Role::Selection ), "selected" },
        { int( ModelItem::Role::ColumnType ), "type" },
        { int( ModelItem::Role::ColorValue ), "colorValue" }
      };
    }
    

    The custom roles needed to be supplied by roleNames and have the strings "type" and "colorValue" specified.

    My data function looks like:

    QVariant
    ModelList::
    data( const QModelIndex& index, int role ) const
    {
        const ModelItem modelItem = mList.at( index.row() );
    
        QVariant result = QVariant();
    
        if ( role == Qt::DisplayRole )
        {
            if ( index.column() == 0 )
            {
              result = QVariant( QString( modelItem.population ) );
            }
            else
            {
              result = QVariant( QString::number( modelItem.averageAge ) );
            }
        }
    
        if ( role == int( ModelItem::Role::Selection ) )
        {
            result = QVariant( QString( modelItem.selected ? "#eeeeee" : "white" ) );
        }
    
        if ( role == int( ModelItem::Role::ColumnType ) )
        {
          if ( index.column() == 0 )
            result = QVariant( QString( "stringValue" ) );
          else
            result = QVariant( QString( "colorValue" ) );
        }
    
        if ( role == int( ModelItem::Role::ColorValue ) )
        {
          QString color;
    
          if ( modelItem.averageAge < 13 )
            color = "red";
          else if ( modelItem.averageAge < 35 )
            color = "yellow";
          else
            color = "green";
    
          result = QVariant( color );
        }
    
        qDebug() << role << " " << result;
    
        return result;
    }
    

    A key point here is that when the role ColumnType is used, I return whether or not the column is a stringValue or a colorValue.

    Additionally, when the role ColorValue is used, I look at the averageAge of the modelItem and return a string containing the color to be used.

    The final piece is to have the QML the custom roles.

    delegate: DelegateChooser
    {
      role: "type"
    
      DelegateChoice
      {
        roleValue: "colorValue"
    
        delegate: Rectangle
        {
          color: selected
    
          Rectangle
          {
            color: colorValue
    
            width: parent.height
            height: parent.height
    
            radius: width * 0.5;
    
            anchors.horizontalCenter: parent.horizontalCenter;
          }
    
          MouseArea
          {
            anchors.fill: parent
    
            onClicked:
            {
              var idx = Backend.modelResults.list.index( row, column )
    
              console.log( "Clicked cell: ", idx.row, " ", Backend.modelResults.list.data( idx ) )
    
              Backend.modelResults.list.select( idx.row );
            }
          }
        }
      }
    
      DelegateChoice
      {
        delegate: Rectangle
        {
          color: selected
    
          Text
          {
            text: display
            anchors.fill: parent
            anchors.margins: 10
            color: 'black'
            font.pixelSize: 15
            verticalAlignment: Text.AlignVCenter
          }
    
          MouseArea
          {
            anchors.fill: parent
    
            onClicked:
            {
              var idx = Backend.modelResults.list.index( row, column )
    
              console.log( "Clicked cell: ", idx.row, " ", Backend.modelResults.list.data( idx ) )
    
              Backend.modelResults.list.select( idx.row );
            }
          }
        }
      }
    }
    

    First, for the DelegateChooser, the role is specified by our custom "type" role. The system knows to call our data function with this role. When the data function returns "colorValue", the first DelegateChoice is selected based because the roleValue is "colorValue". The second DelegateChoice does not have a roleValue because it appears there needs to be a default DelegateChoice and "stringValue" is the default.

    Second, the "colorValue" delegate choice has defined color: colorValue. This causes the system to again call the data function with the ColorValue role and it then returns the correct color for the cell.

    The example project has been updated.

    Suggested improvement to this solution are welcome.



  • I was able to roll my own selection. The logic needed was simple since I only needed to support the selection of a single row.

    My ModelItem looks like:

    struct ModelItem
    {
        Q_GADGET
    
        Q_PROPERTY( QString population MEMBER population )
        Q_PROPERTY( int averageAge MEMBER averageAge )
        Q_PROPERTY( bool selected MEMBER selected )
    
    public:
    
        enum class Role {
          Selection = Qt::UserRole,
          ColumnType,
          ColorValue
        };
        Q_ENUM(Role)
    
        QString population;
        int     averageAge;
        bool    selected    { false };
    
        bool operator!=( const ModelItem& other )
        {
            return other.population != this->population
                || other.averageAge != this->averageAge;
        }
    
    };
    

    The key points here are the selected property to hold whether or not an item is selected and the definition of the custom Selection role.

    I needed a roleNames function for my custom role

    QHash<int, QByteArray>
    ModelList::
    roleNames() const
    {
      return {
        { Qt::DisplayRole, "display" },
        { int( ModelItem::Role::Selection ), "selected" },
        { int( ModelItem::Role::ColumnType ), "type" },
        { int( ModelItem::Role::ColorValue ), "colorValue" }
      };
    }
    

    The key here is that I use the string "selected" to refer to my custom Selection role.

    My data function looks like:

    QVariant
    ModelList::
    data( const QModelIndex& index, int role ) const
    {
        const ModelItem modelItem = mList.at( index.row() );
    
        QVariant result = QVariant();
    
        if ( role == Qt::DisplayRole )
        {
            if ( index.column() == 0 )
            {
              result = QVariant( QString( modelItem.population ) );
            }
            else
            {
              result = QVariant( QString::number( modelItem.averageAge ) );
            }
        }
    
        if ( role == int( ModelItem::Role::Selection ) )
        {
            result = QVariant( QString( modelItem.selected ? "#eeeeee" : "white" ) );
        }
    
        if ( role == int( ModelItem::Role::ColumnType ) )
        {
          if ( index.column() == 0 )
            result = QVariant( QString( "stringValue" ) );
          else
            result = QVariant( QString( "colorValue" ) );
        }
    
        if ( role == int( ModelItem::Role::ColorValue ) )
        {
          QString color;
    
          if ( modelItem.averageAge < 13 )
            color = "red";
          else if ( modelItem.averageAge < 35 )
            color = "yellow";
          else
            color = "green";
    
          result = QVariant( color );
        }
    
        qDebug() << role << " " << result;
    
        return result;
    }
    

    The key here is to check to see if the role is the custom Selection role and then return a color based on the value of selected in the modelItem.

    The final piece is to have the QML use this custom role:

    delegate: DelegateChooser
    {
      role: "type"
    
      DelegateChoice
      {
        roleValue: "colorValue"
    
        delegate: Rectangle
        {
          color: selected
    
          Rectangle
          {
            color: colorValue
    
            width: parent.height
            height: parent.height
    
            radius: width * 0.5;
    
            anchors.horizontalCenter: parent.horizontalCenter;
          }
    
          MouseArea
          {
            anchors.fill: parent
    
            onClicked:
            {
              var idx = Backend.modelResults.list.index( row, column )
    
              console.log( "Clicked cell: ", idx.row, " ", Backend.modelResults.list.data( idx ) )
    
              Backend.modelResults.list.select( idx.row );
            }
          }
        }
      }
    
      DelegateChoice
      {
        delegate: Rectangle
        {
          color: selected
    
          Text
          {
            text: display
            anchors.fill: parent
            anchors.margins: 10
            color: 'black'
            font.pixelSize: 15
            verticalAlignment: Text.AlignVCenter
          }
    
          MouseArea
          {
            anchors.fill: parent
    
            onClicked:
            {
              var idx = Backend.modelResults.list.index( row, column )
    
              console.log( "Clicked cell: ", idx.row, " ", Backend.modelResults.list.data( idx ) )
    
              Backend.modelResults.list.select( idx.row );
            }
          }
        }
      }
    }
    

    The key here is the color: selected part of the Rectangle for the delegate of each DelegateChoice. selected refers to the string selected I setup in the roleNames function above. The system knows to call the data function with the correct ModelItem::Role so if ( role == int( ModelItem::Role::Selection ) ) resolves to true.

    For each DelegateChoice, I defined a MouseArea for the cell which calls the select function in the model. The select function is:

    void
    ModelList::
    select( int index )
    {
      beginResetModel();
    
      for ( int x = 0; x < this->mList.length(); x++ )
      {
        this->mList[x].selected = ( x == index );
      }
    
      endResetModel();
    }
    

    The begin/endResetModel causes the table to be redrawn when the selection changes.

    The example project has been updated.

    Suggested improvement to this solution are welcome.


Log in to reply