Search code examples
qtqmlqtquick2qtquickcontrolsqt5.5

QtQuick - make TableView update its rowCount after its model has changed


I have a TableView that I want to fill dynamically after it is created - i.e. I read a list of strings from my c++ app that I want to set as the model of the TableView. I use a ListModel for that, I fill it with no problems from the Component.onCompleted of the TableView. However, after it is loaded, I also want to select certain item from the list as the default option. The problem is that even though the ListModel contains all the data, the rowCount property of the TableView is not updated, so when I call tableView.selection.select(indexOfTheDefaultItem), I get a warning "TableViewSelection: index out of range". I have tried emiting the ListModel.dataChanged() signal and handle the initial selection there, but that does not help, I also tried to do it on all other signals (onLayoutChanged, onModelChanged...) I could find but with no results. If I reload my data after the first failed attempt, it works, because there are already some rows from the first attempt, so the selection does not go out of range. So it seems the only problem is that the rowCount is not updated until it is too late for me (maybe it is after the component is rendered?).

So ultimately the question is - is there some signal that is emmitted AFTER the rowCount has been updated to which I could react? I have seen some solutions that involve creating the list on the c++ side of the app and using the beginInsertRows() and endInsertRows(), but these are apparently not a function of ListView and I would prefer to keep the code in the qml. Here is what my solution looks right now:

ListModel
{
    id:listModel
}
TableView
{
    id: tableView
    selectionMode:SelectionMode.SingleSelection 
    model: listModel
    TableViewColumn
    {
         role: "myRole"
         title: "myTitle"
    }
    onModelChanged // or Connections{target:listModel onDataChanged .... } or onAnythingThatWillWorkPlease:
    {
        if (tableView.rowCount > 0) // this prevents the "index out of range" warning, but does not really solve the problem
        {
            var defaultEntityIndex = 5;
            tableView.currentRow = defaultEntityIndex; // when I don't call this, the code in the onSelectionChanged below has "null pointer" exception, i.e. I presume the currentRow stays at the initial -1 - hence this "hack"
            tableView.selection.select(defaultEntityIndex);
        }
    }
    Connections
    {
        target: tableView.selection
        onSelectionChanged : 
        {
            if (tableView.currentRow >= 0)
                myC++Class.selectedThing = listModel.get(tableView.currentRow).role;
         }
    }
}
function initList()
{
    var vals = myC++Class.getTheListWithData();
    for(var i =0; i<vals.length; i++)
    {
        listModel.append({role: vals[i]});
    }
    tableView.model = listModel // I just tried to do this to trigger the modelChanged signal
    listModel.dataChanged(0, vals.length - 1);
}
Component.onCompleted:
{
    initList();
}
Connections // this is to re-fresh the tableView when my data changes
{
    target: myC++class
    onDataChanged: // my custom signal
    {
        initList();
    }
}

The current behavior is: I open the window with this code, the listView is filled with my strings read from the getTheListWithData method, but nothing is selected. When I then reload the data (have a button for that), the initList is called again from thanks to the connections in the end of the code example and this time it selects the desired row.


Solution

  • As Velkan pointed out in the comments, the solution is to simply use the onRowCountChanged signal, then it works as expected. Below is my complete code.

    ListModel
    {
        id:listModel
    }
    TableView
    {
        id: tableView
        selectionMode:SelectionMode.SingleSelection 
        model: listModel
        TableViewColumn
        {
            role: "myRole"
            title: "myTitle"
        }
        onRowCountChanged:
        {
            if (rowCount > 0) 
            {
                var defaultEntityIndex = 0;
                for (var i = 0; i < rowCount; i++)
                {
                    if (model.get(i).role == qsTr("NameOfTheDefaultItem"))
                        defaultEntityIndex = i;
                }
                // select the default item and also set it as current so that it is highlighted
                currentRow = defaultEntityIndex
                selection.select(defaultEntityIndex)
                // move the view to the item so that the user knows that something got selected without his input....though this function actually does not seem to be working
                positionViewAtRow(defaultEntityIndex, ListView.Beginning)
                focus = true     
            }
        }
        Connections
        {
            target: tableView.selection
            onSelectionChanged : 
            {
                if (tableView.currentRow >= 0)
                    myC++Class.selectedThing = listModel.get(tableView.currentRow).role;
             }
        }
    }
    function initList()
    {
        var vals = myC++Class.getTheListWithData();
        for(var i =0; i<vals.length; i++)
        {
            listModel.append({role: vals[i]});
        }
        // the lines that were here in the original post are (as expected) not needed
    }
    Component.onCompleted:
    {
        initList();
    }
    Connections // this is to re-fresh the tableView when my data changes
    {
        target: myC++class
        onDataChanged: initList();
    }
    

    Once again, thank you Velkan.

    The only, rather cosmetic, issue with it is that I would like the TableView to scroll down to the default element when it gets selected automatically, so that user is aware that this default selection happened. The positionViewAtRow function should do just that, but doesn't. I have found a few posts of other people having the same problem, but so far none of their solutions worked for me. But that is a problem for more research on my part or in the worst case another SO question :) I will update this answer if I find a solution.