Canonical way to make custom TableView from ListView in Qt Quick
Asked Answered
S

1

8

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.

Sexagenary answered 18/7, 2017 at 13:47 Comment(28)
1. The canonical way would not be, to use a 2d array of strings. It would be a QAbstractItemModel-descendent or a ListModel.Karalynn
2. You can't determine the ideal size for the longest delegate unless you created it. Usually you will want to create the delegates lazily, so you won't determine the length. From a design point of view it is better to set the sizes in dependence of the window size, anyway. Otherwise, the next time you use the view, the texts will be longer, and break your precious design. Use elide for cases where the content creator is unable to restrict himself. If you don't want to be lazy, use a GridLayout and some Repeaters to fill it.Karalynn
3. Do you want to have TableView or a TreeView now?Karalynn
@derM Surely I want. Can you say how and where to determine maximum width of ListView delegate. I need to resize highlight for full width of delegate. Otherwise it looks bad.Sexagenary
If you only want the maximum width of a delegate created so far, use the delegates Component.onCompleted-handler and a property maxWidth in the ListView that you update.Karalynn
@derM But what if content of a delegate will be changed?Sexagenary
handle the delegates onWidthChanged and update the maxWidth. It won't go down with this though, but as hopefully the view's widht should not change all the time something tiny will change, it should be ok?Karalynn
there is no nice way to do this, but there is a way. I don't have time to provide an answer right now, but see what I answered to this question: 45029968/how-do-i-set-the-combobox-width-to-fit-the-largest-item. The principle is to connect to onModelChanged, and use a javascript loop and a TextMetrics component to calculate the width for each text string in the model. onModelChanged will only get called once. other signal handlers like onWidthChanged can cause recursion.Peaceful
@MarkCh But what do you think about GreedView?Sexagenary
@Orient I don't think GridView is the right tool for the job. Anyone else have an opinion on it?Peaceful
@MarkCh I don't know. I just try to evaluate an approaches proposed.Sexagenary
@Orient GridView uses a 1-dimensional list just like ListView, with every delegate showing the same data role (or roles), so it can't easily be used for making a table.Peaceful
@MarkCh Oh, my mistake. I mean GridLayout. Layout.row: 123 may help to make them non-1-dimensional.Sexagenary
@Orient Gridlayout cannot display data from a model, so it is not a View in the sense of Qt model-view framework. You have three options, ListView, Repeater or QC1 TableView. I recommend ListView.Peaceful
You can easily fill a GridLayout with a Repeater, but this won't be a canonical way for a TableView - neither will @MarkCh's solution with a JS-loop be, for that would work only for finite models, and there only for reasonably small ones. onModelChanged is useless, as the model will (usually) not change, if you use a proper QAbstractItemModel descendent, and not just a JSArray, for which you invoke the onModelChanged-signal every time some data changes. You might connect to the (proper) models onDataChanged though, but this might be invoked when something else changes, too.Karalynn
If you use @MarkCh's solution with the JS-loop, this means, you need to turn the whole loop again, whenever anything in the model changes, because the Items won't broadcast their new width themselves. If you write onImplicitWidthChanged: maxImplicitWidth = Math.max(maxImplicitWidth, implicitWidth), this will automatically called for the right element. He is right that the onWidthChanged-handler is not the best, as it will be invoked for every instantiated element after the width of one changed. Use the implicitWidthChanged()-signal instead.Karalynn
If you need to shrink the width of the column when the text of the longest entry will be reduced, you need to know when the longest entry will not be the longest anymore. For this it might be beneficial to keep a sorted list of all widths of your item. When the width of one item changes, it can find it's own width with a binary search comperably fast, remove it and add its new width to the appropriate position. You then use the largest (first/last?) element of this ordered list as the width of your column. This will take quite some JS work, if you don't want to use C++.Karalynn
@derM Instead of list of sorted widths I can store a counter of the longest items for each column in the view along with longest width per column. Counter became one when some item exceeds the current longest width. Counter incremented, when some another item became as long as longest width. If width of item remains the same after some content changed, then no handling of maximum width is performed.Sexagenary
@Orient: As long as the width is only increasing, yes. That is why I started with "If you need to shrink the width of the column, when the longest entry will be reduced [...] But if you are only interested in the increase, you don't need to have a counter. You will just need the longest width. Why would you want to know how many items have the same width?Karalynn
@derM OK, I'll continue. If the width of an item shrinks, then if the width is equal to maximum width property, stored in the view, then need to decrement counter for proper column. If counter is 1, then there is a need to make a loop over the whole column to find maximum size and then shrink the column maximum size. Now I see there is big tradeoff between memory (sorted list) and time (loop on widest element removal).Sexagenary
In this case, I would go for the memory, as the measurement of the text width is not too trivial. In theory you need to layout the text with the font each time to measure it, as fonts might come with various rules on the spacing between letters and so on. Further, as long as no monospace font is used, I'd expect quite a variety in widths, so the counter should never be too large and recalculations would be needed quite often. Here you could consider another tradeoff with the accuracy, by not laying the text out, but just multiplying the word length by the maximum letter width and spacing.Karalynn
@derM I just forgot, that there is no integer widths in QML.Sexagenary
okay, first of all, i overlooked in your question that the text is mutable, so i'm sorry about that. 2nd of all, something you said about GridLayout made me question my understanding. I will now provide an answer.Peaceful
okay my answer is more or less finished. any feedback welcomePeaceful
TableView in qtquickcontrols1 is made of listview you can read its source code master.qt.io/ministro/android/qt5/objects/5.32-x86/qml/QtQuick/…Xenophobe
@MahdiKhalili There is TableView in Qt 5.12. No more need to invent it by oneself.Sexagenary
@Orient it does not have header delegate so its not useful yetXenophobe
@Orient if you know any way to add header for it then answer this #54193679Xenophobe
P
15

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://mcmap.net/q/1324914/-tableview-columns-autofit

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
        }
    }
}
Peaceful answered 19/7, 2017 at 10:59 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.