Search code examples
c++qtqtablewidgetselectionchangedqitemselectionmodel

Behavior of selectionChanged() on removing first row


Please run the following code (I am using Qt 5.9):

QTableWidget* tableWidget = new QTableWidget(2, 2, nullptr);
tableWidget->setSelectionBehavior(QAbstractItemView::SelectRows);
tableWidget->setSelectionMode(QAbstractItemView::SingleSelection);
connect(tableWidget->selectionModel(), &QItemSelectionModel::selectionChanged,
   [&](const QItemSelection& selected, const QItemSelection& deselected) 
   { qDebug() << "selected =" << selected << endl << "deselected =" << deselected; });
tableWidget->show();
QTimer::singleShot(10000, [=](){ tableWidget->removeRow(0); });

Within 10 seconds, select the first of two rows. You will see debug ouput. It will show you that row 0 was selected by your click. Then, after 10s, row 0 is removed automatically. Debug output now shows that row 1 is selected and row 0 is deselected.

The latter doesn't make any sense to me. When removing row 0 I would expect the "new" row 0 being selected afterwards. Also the visually selected row still is row 0 and row 1 simply doesn't exist anymore.

This also happens with a custom model and generic view and makes my application crash by pointing to a row that does not exist.

Is this desired behavior? Where is my misunderstanding?


Solution

  • It makes perfectly sense to change the selected row before deleting it. Doing the opposite may lead to reading dangling data, for example, if the UI is refreshed when the model has changed but the view holds outdated indices.

    Think about removing row 0 twice: the second time is very obvious that the selection must be changed (deselected in this case) before removing the last row in the table to avoid having an invalid index as the selected row.

    You can use the following modified example to see when the model is actually updated.

    auto tableWidget = new QTableWidget(2, 2, nullptr);
    tableWidget->setSelectionBehavior(QAbstractItemView::SelectRows);
    tableWidget->setSelectionMode(QAbstractItemView::SingleSelection);
    
    connect(tableWidget->selectionModel(), &QItemSelectionModel::selectionChanged,
    [&](const QItemSelection& selected, const QItemSelection& deselected) 
    { qDebug() << "selected =" << selected << endl << "deselected =" << deselected; });
    
    connect(tableWidget->model(), &QAbstractItemModel::rowsRemoved, [&](const QModelIndex &, int first, int last)
    { qDebug() << "first row removed =" << first << endl << "last row removed =" << last; });
    
    tableWidget->show();
    
    QTimer::singleShot(10000, [=](){ tableWidget->removeRow(0); });
    QTimer::singleShot(15000, [=](){ tableWidget->removeRow(0); }); // remove twice
    

    Workaround

    The main problem is that, as you pointed out in the comments, you cannot rely on the signal information: those QModelIndex may or not be valid if a removal was performed. You can keep track of all changes but that would be exhausting.

    Instead, you can try deferring the selection signal, so when it is handled the model has been updated and you can trust the information from the selection model. The trick is to use a timer: the function handling the timeout event will be executed in the next iteration of the events loop (even if the timeout time is 0), while the model and the widget are updated in the current iteration:

    connect(tableWidget->selectionModel(), &QItemSelectionModel::selectionChanged,
    [&](const QItemSelection&, const QItemSelection&) {
      QTimer::singleShot(0, [&]() {
        qDebug() << "selected =" << tableWidget->selectionModel()->selectedIndexes() << endl;
      });
    });