2

I need to add a header to a TableView that uses a custom model defined in Python.

I've tried overriding the headerData function in QAbstractTableModel with my custom headers. I'm following the same series of steps described in this C++ example of the same type of implementation: Header to a TableView

Unfortunately, the headers still don't show up at the top of the table. The table does however contain the data from overriding the data function in QAbstractTableModel.

Python:

class RouteTableModel(QAbstractTableModel):

    def __init__(self, parent=None, *args): 
        super().__init__()
        self._datatable = None
        self._header = {
            0 : 'X',
            1 : 'Y',
            2 : 'Z'
        }

    def data(self, index, role=Value):
        i = index.row()
        j = index.column()
        if role == self.Value:
            return '{0}'.format(self._datatable[i][j]['value'])
        elif role == self.Selected:
            return self._datatable[i][j]['selected']
        else:
            return None

    def headerData(self, section, orientation, role=Qt.DisplayRole):
        if role == Qt.DisplayRole:
            if orientation == Qt.Horizontal:
                return self._header[section]
            else:
                return None
        else:
            return None

# create the application instance
app = QApplication(sys.argv)

# create a QML engine
engine = QQmlApplicationEngine()

# instantiate the TableModel class
xyztablemodel = RouteTableModel()

engine.rootContext().setContextProperty('XYZTableModel', xyztablemodel)

# load main QML file and start app engine
engine.load('view.qml')

if not engine.rootObjects():
    sys.exit(-1)

sys.exit(app.exec_())

QML:

import QtQuick 2.12
import QtQuick.Controls 2.12
GridLayout {
    id: gridfpc2
    flow: GridLayout.TopToBottom
    rows: 4
    columns: 2
    rowSpacing: 20
    columnSpacing: 35

    TableView {
        id: xyztable
        Layout.rowSpan: 4
        // Layout.alignment: Qt.AlignCenter
        Layout.fillHeight: true

        model: XYZTableModel
        width: 350

        delegate: CustomComp.XYZTableDelegate {
            implicitWidth: parent.width / 3
            implicitHeight: 20
        }
    }
}

No error messages occur in the Python or qml code. I'd expect the header to populate above the columns in the TableView, but they do not show up.

Matt Brauer
  • 211
  • 4
  • 13
  • 1
    What TableView are you using: [QQC1](https://doc.qt.io/qt-5/qml-qtquick-controls-tableview.html) or [QQ](https://doc.qt.io/qt-5/qml-qtquick-tableview.html)? – eyllanesc Aug 01 '19 at 20:31
  • I didn't realize there were two types with the same name. I'm assuming its QQC1 since I import QtQuick and the QtQuick.Controls second. How would I know for sure if I am importing both? – Matt Brauer Aug 01 '19 at 20:56
  • Turns out its QQ: https://stackoverflow.com/questions/57316457/how-to-add-headers-to-qml-tableview-from-tablemodel I am importing 2.12 for both QtQuick and QtQuick.Controls. Only the QtQuick implementation exists in that version. – Matt Brauer Aug 01 '19 at 21:46

1 Answers1

4

In the example you indicate in your question, it is from a QTableView that is very different from the TableView offered by QML. In your case you are using the TableView of QtQuick. This TableView has no headers so you must implement it, in my example I will use Repeaters. On the other hand headerData is not accessible from QML, so I will implement the Q_INVOKABLE of C++ using @Slot(), passing as an argument the result of the type of variable that returns the function:

main.py

from PySide2 import QtCore, QtGui, QtQml


class RouteTableModel(QtCore.QAbstractTableModel):
    def __init__(self, parent=None):
        super().__init__(parent)
        self._header = {0: "X", 1: "Y", 2: "Z"}

    def columnCount(self, parent=QtCore.QModelIndex()):
        return 3

    def rowCount(self, parent=QtCore.QModelIndex()):
        return 100

    def data(self, index, role=QtCore.Qt.DisplayRole):
        i = index.row()
        j = index.column()
        if role == QtCore.Qt.DisplayRole:
            return "{}-{}".format(i, j)

    @QtCore.Slot(int, QtCore.Qt.Orientation, result="QVariant")
    def headerData(self, section, orientation, role=QtCore.Qt.DisplayRole):
        if role == QtCore.Qt.DisplayRole:
            if orientation == QtCore.Qt.Horizontal:
                return self._header[section]
            else:
                return str(section)


if __name__ == "__main__":
    import os
    import sys

    app = QtGui.QGuiApplication(sys.argv)

    engine = QtQml.QQmlApplicationEngine()

    xyztablemodel = RouteTableModel()
    engine.rootContext().setContextProperty("XYZTableModel", xyztablemodel)

    current_dir = os.path.dirname(os.path.realpath(__file__))
    filename = os.path.join(current_dir, "view.qml")

    engine.load(QtCore.QUrl.fromLocalFile(filename))

    if not engine.rootObjects():
        sys.exit(-1)

    sys.exit(app.exec_())

view.qml

import QtQuick 2.12
import QtQuick.Controls 2.4
import QtQuick.Window 2.11

Window {
    visible: true
    width: 640
    height: 480
    title: qsTr("Hello World")
    color: '#222222'

    TableView {
        id: tableView
        columnWidthProvider: function (column) { return 100; }
        rowHeightProvider: function (column) { return 60; }
        anchors.fill: parent
        leftMargin: rowsHeader.implicitWidth
        topMargin: columnsHeader.implicitHeight
        model: XYZTableModel
        width: 350
        delegate: Rectangle {
            implicitWidth: 100
            implicitHeight: 50
            Text {
                text: display
            }
        }

        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: XYZTableModel.headerData(modelData, Qt.Horizontal)
                    color: '#aaaaaa'
                    font.pixelSize: 15
                    padding: 10
                    verticalAlignment: Text.AlignVCenter

                    background: Rectangle { color: "#333333" }
                }
            }
        }
        Column {
            id: rowsHeader
            x: tableView.contentX
            z: 2
            Repeater {
                model: tableView.rows > 0 ? tableView.rows : 1
                Label {
                    width: 40
                    height: tableView.rowHeightProvider(modelData)
                    text: XYZTableModel.headerData(modelData, Qt.Vertical)
                    color: '#aaaaaa'
                    font.pixelSize: 15
                    padding: 10
                    verticalAlignment: Text.AlignVCenter
                    background: Rectangle { color: "#333333" }
                }
            }
        }

        ScrollIndicator.horizontal: ScrollIndicator { }
        ScrollIndicator.vertical: ScrollIndicator { }
    }
}
eyllanesc
  • 235,170
  • 19
  • 170
  • 241
  • Is there another TableView I can use where the headers are already implemented? I was able to use your solution to implement them explicitly, but if there is something already pre-built, it might make sense to use that. It looked like TableView implemented from QtQuick.Controls wasn't matching my model implementation and has been retired in more recent versions. – Matt Brauer Aug 02 '19 at 21:58
  • @MattBrauer The other TableView only supports QAbstractListModel or classes derived from them and the multicolumn idea is based on roles. If you check the docs you will see an example, and then you can try to implement it in python: https://doc.qt.io/qt-5/qml-qtquick-controls-tableview.html – eyllanesc Aug 02 '19 at 22:01
  • @eyllanesc is is possible to have the columns all be equal width based on the width of the `TableView` instead of hardcoding the width of each column as `100`? My attempt of defining the width in the delegate as `implicitWidth: tableView.width / tableView.model.columnCount()` only partially works. It will resize the columns after clicking and dragging the table. – roundtheworld Sep 13 '19 at 18:10
  • @roundtheworld try with: `columnWidthProvider: function (column) { tableView.width / tableView.model.columnCount(); }` – eyllanesc Sep 13 '19 at 18:17
  • That didn't work...maybe it's because my `TableiView` is in a layout...I'll open a new question. – roundtheworld Sep 13 '19 at 18:32
  • @roundtheworld I am based only on the code of my answer, other things are irrelevant to me. Please this section is just to ask about my answer, not about something else. – eyllanesc Sep 13 '19 at 18:36