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 QMLTableView
'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 stringselected
I setup in the roleNames function above. The system knows to call the data function with the correct ModelItem::Role soif ( role == int( ModelItem::Role::Selection ) )
resolves totrue
.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.