Search code examples
data-bindingdelegatesqml

QML bound variable differs from local copy in delegate - sometimes


I have a strange problem when clicking on the header in a tableview. I am trying to make columns that sort and reverse sort direction by clicking on the column header. After lots of chasing, I have discovered that sometimes when I click the ColumnHeader it seems to run the onClick code from a DIFFERENT ColumnHeader. As if the delegate were reusing/running a different instance. And this problem only occurs if I call the mysort function (which does nothing but call the ancestor's Qt sort function) in the onClick event.

To clarify, I print out the 'columnNum' which is bound to the colum 'index' in the delegate, and 'copyColumnNum' which is initialized to 'columnNum' when my header is created. I print these two value out whenever I click the column header, and they should ALWAYS be the same. Some runs they are, other times the values differ on every second click. This confirms sometimes the onClick code is running in a different instance that the one I expect.

Why? How can copyColumnNum ever be different than columnNum? I must be missing something basic about binding / delegates.

in main.qml

HorizontalHeaderView {
    anchors.top: parent.top
    anchors.left: parent.left
    anchors.right: parent.right
    id: myHorizontalHeaderView
    syncView: myTableView


    delegate: ColumnHeader {
        label: model.display
        columnNum: index
    }
}

in ColumnHeader.qml

import QtQuick 2.0
import QtQuick.Controls 2.15
import QtGraphicalEffects 1.15

Button {
    property string label // Bound to model's display role for this column
    property int columnNum  // Bound to column index
    property int sortOrder : 0
    property int copyColumnNum;

    Component.onCompleted: {
        copyColumnNum = columnNum;
    }

    Rectangle {
        anchors.fill: parent
        Item {
            Text {
                id: labelText
                text: label
                color: "white"
                anchors.verticalCenter: parent.verticalCenter
            }
    }

    onClicked: {
        console.log("Clicked on copyColumnNum "+copyColumnNum+" columnNum is "+columnNum);
        if (sortOrder === 1)
           sortOrder = 0;
        else
           sortOrder = 1;
        tableSortFilterProxyModel.mySort(copyColumnNum, sortOrder);
        sortingColumn = copyColumnNum;
    }
}

Solution

  • It's not a binding issue - it's a delegate issue. Look at the warning here on TableView (what HorizontalHeaderView inherits from) and its delegate property:

    https://doc.qt.io/qt-5/qml-qtquick-tableview.html#delegate-prop

    Note: Delegates are instantiated as needed and may be destroyed at any time. They are also reused if the reuseItems property is set to true. You should therefore avoid storing state information in the delegates.

    Most if not all *Views create a pool of delegates what can be seen in the current Item view but then get returned to and reissued from the pool as you scroll. In other words, when a column (delegate) goes out of view on one end of the view, it is moved and placed on the other end of the view as a new column comes into view. And there are other situations other than scrolling that will likely result in this too (inserts, deletes, sorts, etc.).

    That's where the issue is coming from. You should not store any state in the delegate itself since an existing one will get reused in a new column as you scroll without an intervening destroy/create.

    Normally, this limitation is managed by storing all state in the model. Or in this case, you can probably use the existing index, row and column properties exposed by the TableView (also mentioned in the delegate property documentation linked above).

    More discussion on how TableView reuses items here:

    https://doc.qt.io/qt-5/qml-qtquick-tableview.html#reusing-items

    As noted there, you could also manage this by catching the TableView::reused signal:

    Note: Avoid storing any state inside a delegate. If you do, reset it manually on receiving the TableView::reused signal.