Search code examples
qtpyqtqt5pyqt5qlistwidget

Qt: Multiple QListWidgets and only a single entry selected


I have several QListWidgets and would like to only allow a single row to be selected for all of these lists. So for example if I select a row in one of the lists any other selection in the other lists would be cleared. How can I achieve this?

Is there a built-in way to do this (similar to QButtonGroup for buttons)? If not, what approach would you recommend that I take when trying to implement this myself?

Grateful for help and with kind regards, Tord


Solution

  • AFAIK, there is no ready built-in feature to provide a single selection in multiple list views.

    Instead, it can be done by a respective signal handler for the QSelectionModel::selectionChanged signal which does this whenever the selection of one of the involved list views changes.

    Thereby, you have to consider that clearing the selection will emit a selectionChanged signal as well. (Otherwise, you may end up in a recursive call of your signal handler until a stack overflow occurs.)

    Unfortunately, I use Qt in C++. (My Python knowledge is rather limited.)

    Thus, all I can provide for now is my "proof of concept" in C++:

    #include <QtWidgets>
    
    void singleSel(QListView *pLstView, const QList<QListView*> &pLstViews)
    {
      for (QListView *pLstViewI : pLstViews) {
        if (pLstViewI == pLstView) continue; // skip sender
        // the check is necessary to prevent recursions...
        if (pLstView->selectionModel()->hasSelection()) {
          // ...as this causes emission of selectionChanged() signal as well:
          pLstViewI->selectionModel()->clearSelection();
        }
      }
    }
    
    int main(int argc, char **argv)
    {
      qDebug() << "Qt Version: " << QT_VERSION_STR;
      QApplication app(argc, argv);
      // build contents
      QStandardItemModel tblModel(0, 1);
      for (int i = 0; i < 10; ++i) {
        tblModel.appendRow(
          new QStandardItem(QString::fromUtf8("Entry %0").arg(i + 1)));
      }
      // build some GUI
      QWidget win;
      QHBoxLayout qHBox;
      QListView lstView1;
      lstView1.setModel(&tblModel);
      qHBox.addWidget(&lstView1);
      QListView lstView2;
      lstView2.setModel(&tblModel);
      qHBox.addWidget(&lstView2);
      QListView lstView3;
      lstView3.setModel(&tblModel);
      qHBox.addWidget(&lstView3);
      win.setLayout(&qHBox);
      win.show();
      // install signal handlers
      QList<QListView*> pLstViews = { &lstView1, &lstView2, &lstView3 };
      QObject::connect(lstView1.selectionModel(),
        &QItemSelectionModel::selectionChanged,
        [&lstView1, &pLstViews](const QItemSelection&, const QItemSelection &)
        {
          singleSel(&lstView1, pLstViews);
        });
      QObject::connect(lstView2.selectionModel(),
        &QItemSelectionModel::selectionChanged,
        [&lstView2, &pLstViews](const QItemSelection&, const QItemSelection &)
        {
          singleSel(&lstView2, pLstViews);
        });
      QObject::connect(lstView3.selectionModel(),
        &QItemSelectionModel::selectionChanged,
        [&lstView3, &pLstViews](const QItemSelection&, const QItemSelection &)
        {
          singleSel(&lstView3, pLstViews);
        });
      // exec. application
      return app.exec();
    }
    

    I compiled and tested on Windows 10 (64 bit). This is how it looks:

    Snapshot of testQSingleSelectInMultipleListViews


    Update:

    I tried to port the C++ application to Python/PyQt5:

    #!/usr/bin/python3
    
    import sys
    from PyQt5.QtWidgets import QApplication, QWidget, QHBoxLayout, QListView
    from PyQt5.QtGui import QStandardItemModel, QStandardItem
    
    def singleSelect(lstView, lstViews):
      for lstViewI in lstViews:
        if lstViewI == lstView:
          continue
        # the check is necessary to prevent recursions...
        if lstViewI.selectionModel().hasSelection():
          # ...as this causes emission of selectionChanged() signal as well:
          lstViewI.selectionModel().clearSelection()
    
    if __name__ == '__main__':
      app = QApplication(sys.argv)
      # build contents
      tblModel = QStandardItemModel(0, 1)
      for i in range(0, 10):
        tblModel.appendRow(QStandardItem("Entry %d" % (i + 1)))
      # build GUI
      win = QWidget()
      hBox = QHBoxLayout()
      lstView1 = QListView()
      lstView1.setSelectionMode(QListView.SingleSelection)
      lstView1.setModel(tblModel)
      hBox.addWidget(lstView1)
      lstView2 = QListView()
      lstView2.setSelectionMode(QListView.SingleSelection)
      lstView2.setModel(tblModel)
      hBox.addWidget(lstView2)
      lstView3 = QListView()
      lstView3.setSelectionMode(QListView.SingleSelection)
      lstView3.setModel(tblModel)
      hBox.addWidget(lstView3)
      win.setLayout(hBox)
      win.show()
      # install signal handlers
      lstViews = [lstView1, lstView2, lstView3]
      lstView1.selectionModel().selectionChanged.connect(lambda sel, unsel: singleSelect(lstView1, lstViews))
      lstView2.selectionModel().selectionChanged.connect(lambda sel, unsel: singleSelect(lstView2, lstViews))
      lstView3.selectionModel().selectionChanged.connect(lambda sel, unsel: singleSelect(lstView3, lstViews))
      # exec. application
      sys.exit(app.exec_())
    

    From what I saw in the test, it behaves similar like the one written in C++. The only exception is that I have to click twice on an entry when I change the list view.

    Snapshot of testQSingleSelectInMultipleListViews.py

    As I already said: my experiences in PyQt are very limited. (Actually, I started today by making this sample.) Thus, I may have overseen something. Please, take it with a grain of salt...