TableModel
TableModel provides a QAbstractTableModel that can be used in QML with TableView.
Requirements
#1
Allow different data format/structure for rows; need to be flexible. For example, it should be possible to specify several roles per column, as QAbstractTableModel supports this:
model: TableModel { // Each row is one type of fruit that can be ordered rows: [ [ // Each object (line) is one cell/column, // and each property in that object is a role. { checked: false, checkable: true }, { amount: 1 }, { fruitType: "Apple" }, { fruitName: "Granny Smith" }, { fruitPrice: 1.50 } ], [ { checked: true, checkable: true }, { amount: 4 }, { fruitType: "Orange" }, { fruitName: "Navel" }, { fruitPrice: 2.50 } ] ] }
In addition, it should be possible to use a simple object for a row, as this seems to be common with web APIs:
[ { "driverId":"fisichella", "code":"FIS", "url":"http://en.wikipedia.org/wiki/Giancarlo_Fisichella", "givenName":"Giancarlo", "familyName":"Fisichella", "dateOfBirth":"1973-01-14", "nationality":"Italian" }, { "driverId":"barrichello", "code":"BAR", "url":"http://en.wikipedia.org/wiki/Rubens_Barrichello", "givenName":"Rubens", "familyName":"Barrichello", "dateOfBirth":"1972-05-23", "nationality":"Brazilian" } ]
#2
We know we want settable rowCount + columnCount for prototyping and allowing use cases such as e.g. spreadsheets.
Solutions
Below are alternative solutions that aim to solve all of the requirements.
#1 "rows"
Current (unfinished) state as of writing can be seen here: http://code.qt.io/cgit/qt/qtdeclarative.git/tree/src/qml/types/qqmltablemodel.cpp?h=dev&id=b4ee855eb10cf9bfa44ce8e5de8f9ee6c5917764
In addition, there are some follow-up patches:
- https://codereview.qt-project.org/#/c/253255/
- https://codereview.qt-project.org/#/c/253234/
- https://codereview.qt-project.org/#/c/253509/
#2 No "rows"
- We know we can't expect one data format/structure for rows; need to be flexible.
- We know we want settable rowCount + columnCount.
- roleDataLookPolicy (https://codereview.qt-project.org/#/c/253509/) is making things quite complex.
So:
- Remove "rows" altogether. How the actual source JS data is stored and formatted is up to the user,
- rowCount and columnCount would then have to always be specified so that we know how many there are.
- "roles" allows the user to use their custom roles without us having parsed the rows data (currently we parse it up front using an expected format so that we know which roles are available):
roles: [ ["driverId"], ["code"], ["url"], ["givenName"], ["familyName"], ["dateOfBirth"], ["nationality"] ]
- Then we call roleDataProvider() unconditionally each time data() is called for a "new" index, and store that in our own well-formatted map/hash, so we don't need to call roleDataProvider() again for that index.
- As appendRow(), moveRow(), etc. would no longer take the row as an argument, they would need to be replaced with beginAppendRow()/endAppendRow(), etc.
An example:
import QtQuick 2.12
import QtQuick.Window 2.12
import Qt.labs.qmlmodels 1.0
Window {
width: 400
height: 400
visible: true
// pretend we got data from some web API...
property var tableData: [
[
// Each object (line) is one cell/column,
// and each property in that object is a role.
{ checked: false, checkable: true },
{ amount: 1 },
{ fruitType: "Apple" },
{ fruitName: "Granny Smith" },
{ fruitPrice: 1.50 }
],
[
{ checked: true, checkable: true },
{ amount: 4 },
{ fruitType: "Orange" },
{ fruitName: "Navel" },
{ fruitPrice: 2.50 }
],
[
{ checked: false, checkable: true },
{ amount: 1 },
{ fruitType: "Banana" },
{ fruitName: "Cavendish" },
{ fruitPrice: 3.50 }
]
]
TableView {
anchors.fill: parent
columnSpacing: 1
rowSpacing: 1
boundsBehavior: Flickable.StopAtBounds
model: TableModel {
rowCount: tableData.length
columnCount: roles.length
// If there is only one role per column:
// roleDataProvider: function(index, role) {
// return tableData[index.row][role]
// }
// Otherwise, handle those with more than one:
roleDataProvider: function(index, role) {
var rowData = tableData[index.row]
switch (index.column) {
case 0:
return role === "checked" ? rowData.checked : rowData.checkable
default:
return tableData[index.row][role]
}
}
roles: [
["checked", "checkable"],
["fruitType"],
["fruitName"],
["fruitPrice"]
]
}
// delegate: ...
}
/*
To append a row:
model.beginAppendRow()
// user code to append row to tableData
model.endAppendRow()
*/
}
#3 Discrete ModelColumn objects
ModelColumn is implemented similar to ListElement: it declares additional roles that the delegate can use, and what they map to.
- A role can map to a JSON role directly
- A role can map to a function: this idea replaces roleDataProvider, but now the function has a narrower scope, so it's less likely to need a big switch statement. But if you want to write one big function and reuse it, you just have to declare it separatly.
- index is special: it can be a number telling which column this declaration defines. If omitted, the declaration applies to all columns except those where explicit declarations override it.
- header is special: it can be a string to display in the column heading, or a function taking a column number and returning a string. This replaces horizontalHeaderData and verticalHeaderData.
- If any column or role that the delegate uses is left undefined, or if there are no ModelColumn declarations at all, fall back to looking up the role explicitly in the JSON data, or taking the unlabeled data from that cell (e.g. the string "Apple" in the example is an unlabeled cell datum), or taking the first labeled role (e.g. the "Granny Smith" variety is labeled data, but there is no role mapping in column 1).
TableModel {
ModelColumn {
// index is omitted: this is default for all columns
// all roles are omitted: so just look for explicit "display" or unlabeled data as necessary
header: function(index) { return index } // by default just display the column index (0..3)
}
ModelColumn {
index: 2 // override behavior in column 2
display: "price" // when the delegate asks for "display" role, look for "price" in the JSON
edit: "price"
header: "Price" // a plain string to display (not a function, not a role)
}
ModelColumn {
index: 4
display: function(modelIndex, role, cellData) {
function v(c) { return ... }
eval(cellData["formula"]) // or whatever else is necessary
} // this replaces the roleDataProvider declaration
edit: "formula" // this is a role mapping: when the delegate asks for "edit" role, look for "formula" in the JSON
}
rows: [
[
"Apple",
{ "variety": "Granny Smith" },
{ "price": 1.50 },
1,
{ "formula": "v(3) * v(2)" }
],
[
"Banana",
{ "variety": "Cavendish" },
{ "price": 3.50 },
1,
// formula is a mini-language: only sensible to the calculation function
{ "formula": "v(3) * v(2)" }
]
]
}
Another example with row objects rather than arrays: here maybe we use ModelColumn instances to define the columns.
TableModel {
ModelColumn {
index: 0
// let's say the delegate needs 3 user-defined roles to render
property string givenName: "givenName"
property string familyName: "familyName"
property string link: "url"
}
ModelColumn {
index: 1
display: "dateOfBirth"
}
ModelColumn {
index: 2
display: "nationality"
}
ModelColumn {
index: 3
display: "driverId"
property string code: "code"
}
rows: [
{
"driverId":"fisichella",
"code":"FIS",
"url":"http://en.wikipedia.org/wiki/Giancarlo_Fisichella",
"givenName":"Giancarlo",
"familyName":"Fisichella",
"dateOfBirth":"1973-01-14",
"nationality":"Italian"
},
{
"driverId":"barrichello",
"code":"BAR",
"url":"http://en.wikipedia.org/wiki/Rubens_Barrichello",
"givenName":"Rubens",
"familyName":"Barrichello",
"dateOfBirth":"1972-05-23",
"nationality":"Brazilian"
}
]
}
It would be nice to pretend that dynamic properties (like givenName and link) are the same as explicit Q_PROPERTYs in the ModelColumn class, but we don't want to have to write and maintain more code like http://code.qt.io/cgit/qt/qtdeclarative.git/tree/src/qml/types/qqmllistmodel.cpp so it seems it might be better to have explicit C++ properties for all Qt-supported roles, and something special for the case when a delegate wants user-defined roles. Perhaps explicit dynamic properties (as in property string givenName) or perhaps the column is required to have a roleDataProvider.
The ModelColumn idea is faintly reminiscent of QQC1 TableViewColumn
#4
TableModel {
rowIndexSpecifier: "myRowIndex" // not necessary if it's named "row"
ModelColumn {
index: 0
// let's say the delegate needs 3 user-defined roles to render
// property string givenName: "givenName"
// property string familyName: "familyName"
// property string link: "url"
// Inject cellData as delegate context property?
}
ModelColumn {
index: 1
display: "dateOfBirth"
}
ModelColumn {
index: 2
display: "nationality"
}
// Only provides built-in roles as properties, use roleDataProvider for more complex role data
ModelColumn {
index: 3
display: "driverId"
roleDataProvider: function(modelIndex, role, cellData) {
return cellData.name
}
}
// Will handle all columns which haven't been specified elsewhere
// This one only makes sense when each row is an array (i.e. each column possibly has multiple roles)
ModelColumn {
roleDataProvider: function(modelIndex, role, cellData) {
return ... ;
}
}
ModelColumn {
index: 2000
display: "code"
}
rows: [
{
// "row": 34, // For the case of rowCount and columnCount being set
"driverId": {
"foo": 1,
"bar": false,
"name": "fisichella"
},
"code":"FIS",
"url":"http://en.wikipedia.org/wiki/Giancarlo_Fisichella",
"givenName":"Giancarlo",
"familyName":"Fisichella",
"dateOfBirth":"1973-01-14",
"nationality":"Italian"
},
{
// "row": 48,
"driverId": {
"foo": 1,
"bar": false,
"name": "barrichello"
},
"code":"BAR",
"url":"http://en.wikipedia.org/wiki/Rubens_Barrichello",
"givenName":"Rubens",
"familyName":"Barrichello",
"dateOfBirth":"1972-05-23",
"nationality":"Brazilian"
}
]
}
delegate: DelegateChooser {
DelegateChoice {
column: 3
Row {
Label {
text: display
}
Label {
text: cellData.foo
}
}
}
}
#5
- Primary focus is on supporting simple "web API" object structures: set a string on the each TableModelColumn for the roles you want available to delegates.
- Complex structures are supported (with limited functionality) by setting a getter and setter function on each role property. Sacrifice speed for convenience of not having to transform the data up front: just need to handle the mapping in TableModelColumn. The lost functionality is row manipulation: appendRow(), etc. Since we don't know the structure of the user's data, our copy of their data becomes out-dated very quickly (see https://codereview.qt-project.org/gitweb?p=qt/qtdeclarative.git;a=blob;f=src/qml/types/qqmltablemodel.cpp;h=7353556518647a58d6327680f42db408201ec2aa;hb=23a8b58d959665372685a652643eb35bc6b76fe7#l884 for more information).
- Only the roles specified by each TableModelColumn will be available to the delegate; the rest will be ignored.
- Can have writable rowCount/columnCount: it will be documented that each row is stored internally, even if it has no data. So, it's up to the user to ensure that the table isn't too big. If they need larger tables, they should write one in C++.
- Only supports built-in roles. For custom roles, use C++.
Simple case:
TableModel {
// Each column needs a TableModelColumn
TableModelColumn {
index: 0
display: "givenName"
}
TableModelColumn {
index: 1
display: "dateOfBirth"
}
TableModelColumn {
index: 2
display: "nationality"
}
TableModelColumn {
index: 3
display: "url"
}
rows: [
{
"driverId": "fisichella",
"code":"FIS",
"url":"http://en.wikipedia.org/wiki/Giancarlo_Fisichella",
"givenName":"Giancarlo",
"familyName":"Fisichella",
"dateOfBirth":"1973-01-14",
"nationality":"Italian"
},
{
"driverId":"barrichello",
"code":"BAR",
"url":"http://en.wikipedia.org/wiki/Rubens_Barrichello",
"givenName":"Rubens",
"familyName":"Barrichello",
"dateOfBirth":"1972-05-23",
"nationality":"Brazilian"
}
]
}
Complex (each row is an array) case:
TableModel {
// Each column needs a TableModelColumn
TableModelColumn {
index: 0
display: function(rowIndex, rowData) { return rows[rowIndex][0].checked }
setDisplay: function(rowIndex, cellData) { rows[rowIndex][0].checked = cellData }
}
TableModelColumn {
index: 1
display: function(rowIndex, rowData) { return rows[rowIndex][1].amount}
setDisplay: function(rowIndex, cellData) { rows[rowIndex][1].amount = cellData }
}
// etc.
rows: [
[
// Each object (line) is one cell/column,
// and each property in that object is a role.
{ checked: false, checkable: true },
{ amount: 1 },
{ fruitType: "Apple" },
{ fruitName: "Granny Smith" },
{ fruitPrice: 1.50 }
],
[
{ checked: true, checkable: true },
{ amount: 4 },
{ fruitType: "Orange" },
{ fruitName: "Navel" },
{ fruitPrice: 2.50 }
]
]
}
Complex (some rows are weird objects) case:
TableModel {
// Each column needs a TableModelColumn
TableModelColumn {
index: 0
display: function(rowIndex, rowData) { return rows[rowIndex].driverId.nickname }
setDisplay: function(rowIndex, cellData) { rows[rowIndex].driverId.nickname = cellData }
}
// Can mix-and-match with simple API where possible (assuming the row is an object and the column is a simple key-value pair).
TableModelColumn {
index: 1
display: "url"
}
// etc.
rows: [
{
"driverId": {
"foo": 1,
"bar": false,
"nickname": "The Chella"
},
"code":"FIS",
"url":"http://en.wikipedia.org/wiki/Giancarlo_Fisichella",
"givenName":"Giancarlo",
"familyName":"Fisichella",
"dateOfBirth":"1973-01-14",
"nationality":"Italian"
},
{
"driverId": {
"foo": 1,
"bar": false,
"nickname": "Barracuda"
},
"code":"BAR",
"url":"http://en.wikipedia.org/wiki/Rubens_Barrichello",
"givenName":"Rubens",
"familyName":"Barrichello",
"dateOfBirth":"1972-05-23",
"nationality":"Brazilian"
}
]
}