Search code examples
qtlistviewqmltableviewqtquick2

Canonical way to make custom TableView from ListView in Qt Quick


What is the best way to make table from ListView?

Say, given a 2d array of strings and delegate for all the columns are Labels. How and when to calculate maximum item width for each column while using only QML? Content of each Label is not constant (i.e. implicitWidth is mutable during lifetime).

Practical reason to invent the TableView is the fact, that 1 step to TreeView will remain.


Solution

  • Questions about creating tables in QML seem to get posted fairly frequently, but I am yet to see an answer compiling all the different options. There are lots of ways to achieve what you are asking. I hope to provide in this answer a number of alternatives.

    TableView (5.12 and later)

    (Updated 16/07/2021)

    Qt 5.12 includes a new Qt Quick item called TableView, which has been redesigned from the ground up to have good performance for a data model with any number of rows or columns. It resolves the performance problems which were present in the previous TableView from`Quick Controls 1.

    At the time of creating this answer TableView did not exist, but I have provided a usage example for the new TableView in a more recent answer here: https://stackoverflow.com/a/68347396/5414907

    It provides good built-in support for sizing the column widths based on the delegate implicitWidth, but it does so only for the rows in the viewport, which means that scrolling could reveal data which does not fit in the column, unless you force a forceLayout().

    If you are using Qt 5.12, and you know that you will need both horizontal scrolling and vertical scrolling for your table (there are more rows AND columns than can fit in the view), then this would seem to be the first choice solution.

    Qt provided a performance comparison of the new TableView vs the old one here: http://blog.qt.io/blog/2018/12/20/tableview-performance/

    Below are a summary of alternative approaches for Qt 5.11 and earlier, or if for some reason you do not want to use the Qt 5.12 TableView (perhaps one of these alternative approaches better suits your data model?).

    GridLayout

    import QtQuick 2.7
    import QtQuick.Controls 2.0
    import QtQuick.Layouts 1.3
    
    ApplicationWindow {
        visible: true
        width: 640
        height: 480
    
        ListModel {
            id: listModel
            ListElement { name: 'item1'; code: "alpha"; language: "english" }
            ListElement { name: 'item2'; code: "beta"; language: "french" }
            ListElement { name: 'item3'; code: "long-code"; language: "long-language" }
        }
    
        GridLayout {
            flow: GridLayout.TopToBottom
            rows: listModel.count
            columnSpacing: 0
            rowSpacing: 0
    
            Repeater {
                model: listModel
    
                delegate: Label {
                    Layout.fillHeight: true
                    Layout.fillWidth: true
                    Layout.preferredHeight: implicitHeight
                    Layout.preferredWidth: implicitWidth
                    background: Rectangle { border.color: "red" }
                    text: name
                }
            }
            Repeater {
                model: listModel
    
                delegate: Label {
                    Layout.fillHeight: true
                    Layout.fillWidth: true
                    Layout.preferredHeight: implicitHeight
                    Layout.preferredWidth: implicitWidth
                    background: Rectangle { border.color: "green" }
                    text: code
                }
            }
            Repeater {
                model: listModel
    
                delegate: Label {
                    Layout.fillHeight: true
                    Layout.fillWidth: true
                    Layout.preferredHeight: implicitHeight
                    Layout.preferredWidth: implicitWidth
                    background: Rectangle { border.color: "blue" }
                    text: language
                }
            }
        }
    }
    

    Vertical ListView

    Creating a table with the Vertical ListView has its advantages and disadvantages. Pros:

    • Scrollable
    • Dynamic creation of delegates which are outside the viewable area, which should mean faster loading
    • Easy to create for fixed width columns, in which the text is elided or wrapped

    Cons:

    • For a vertical scrolling ListView (which is usually what people want), dynamic column width is difficult to achieve... i.e. column width is set to completely fit all values in the column

    Column widths must be calculated using a loop over all the model data inside that column, which could be slow and is not something you would want to perform often (for example if user can modify cell contents and you want the column to resize).

    A reasonable compromise can be achieved by only calculating the column widths once, when the model is assigned to the ListView, and having a mixture of fixed-width and calculated-width columns.

    Warning: Below is an example of calculating column widths to fit longest text. If you have a large model, you should consider scrapping the Javascript loop and resort to fixed width columns (or fixed proportions relative to the view size).

    import QtQuick 2.7
    import QtQuick.Controls 2.0
    import QtQuick.Layouts 1.3
    
    ApplicationWindow {
        visible: true
        width: 640
        height: 480
    
        ListModel {
            id: listModel
            ListElement { name: 'item1'; code: "alpha"; language: "english" }
            ListElement { name: 'item2'; code: "beta"; language: "french" }
            ListElement { name: 'item3'; code: "long-code"; language: "long-language" }
        }
    
        ListView {
            property var columnWidths: ({"name": 100, "code": 50}) // fixed sizes or minimum sizes
            property var calculatedColumns: ["code", "language"]   // list auto sized columns in here
    
            orientation: Qt.Vertical
            anchors.fill: parent
            model: listModel
    
            TextMetrics {
                id: textMetrics
            }
    
            onModelChanged: {
                for (var i = 0; i < calculatedColumns.length; i++) {
                    var role = calculatedColumns[i]
                    if (!columnWidths[role]) columnWidths[role] = 0
                    var modelWidth = columnWidths[role]
                    for(var j = 0; j < model.count; j++){
                        textMetrics.text = model.get(j)[role]
                        modelWidth = Math.max(textMetrics.width, modelWidth)
                    }
                    columnWidths[role] = modelWidth
                }
            }
    
            delegate: RowLayout {
    
                property var columnWidths: ListView.view.columnWidths
                spacing: 0
    
                Label {
                    Layout.fillHeight: true
                    Layout.fillWidth: true
                    Layout.preferredHeight: implicitHeight
                    Layout.preferredWidth: columnWidths.name
                    background: Rectangle { border.color: "red" }
                    text: name
                }
    
                Label {
                    Layout.fillHeight: true
                    Layout.fillWidth: true
                    Layout.preferredHeight: implicitHeight
                    Layout.preferredWidth: columnWidths.code
                    background: Rectangle { border.color: "green" }
                    text: code
                }
    
                Label {
                    Layout.fillHeight: true
                    Layout.fillWidth: true
                    Layout.preferredHeight: implicitHeight
                    Layout.preferredWidth: columnWidths.language
                    background: Rectangle { border.color: "blue" }
                    text: language
                }
            }
        }
    }
    

    TableView (5.11 and earlier)

    (from Quick Controls 1)

    QC1 has a TableView component. QC2 does not (in Qt 5.9). There is one in development, but with no guaranteed timescale.

    TableView has been unpopular due to performance issues, but it did receive improvements between Quick Controls 1.0 to 1.4, and it remains a useable component. QC1 and QC2 can be mixed in the same application.

    Pros

    • easy to achieve spreadsheet-style user-resizable columns
    • based on a ListView, so handles large numbers of rows well.
    • only built-in component resembling the QTableView from Widgets

    Cons

    • default styling is a sort of desktop-grey. You might spend more time trying to override the styling than if you started from scratch using a ListView.
    • auto resizing columns to fit longest contents not really practical / doesn't really work.

    Example:

    import QtQuick 2.7
    import QtQuick.Controls 1.4 as QC1
    import QtQuick.Controls 2.0
    import QtQuick.Layouts 1.3
    
    ApplicationWindow {
        visible: true
        width: 400
        height: 200
    
        ListModel {
            id: listModel
            ListElement { name: 'item1'; code: "alpha"; language: "english" }
            ListElement { name: 'item2'; code: "beta"; language: "french" }
            ListElement { name: 'item3'; code: "long-code"; language: "long-language" }
        }
    
        QC1.TableView {
            id: tableView
            width: parent.width
            model: listModel
    
            QC1.TableViewColumn {
                id: nameColumn
                role: "name"
                title: "name"
                width: 100
            }
            QC1.TableViewColumn {
                id: codeColumn
                role: "code"
                title: "code"
                width: 100
            }
            QC1.TableViewColumn {
                id: languageColumn
                role: "language"
                title: "language"
                width: tableView.viewport.width - nameColumn.width - codeColumn.width
            }
        }
    }