Search code examples
c++qtarchitecturesignals-slots

Too deep hierarchy of signals-slots in Qt


I am writing GUI applicaton in Qt. Currently I waste too much time on routine. It seems something wrong with my architecture. Please tell me how can I change my approach to improve code.

What I am doing:

My program can be decomposed as hierarchy of classes (not inheritance but composition). In example:

class D { /* something */ };
class C { /* something */ };
class B { C c1; C c2; D d1; };
class A { D d1; C c1; };

So, actually it is a tree hierarchy where leaf nodes (class C, class D) are "models" in Qt terminology which hold data. At the top of hierarchy MainWindow (class A) is placed, which holds first level of "views" (class D, i.e. subwidget) and leaf node with data (class C, i.e. text field).

To pass information down from main window to data I use function calls from mainwindow (pushbuttons) to leaf nodes. After that data changes and tells parents about it with signal slot mechanism. Parents continue to pass message up with signaling.

I am really bored by establishing all these communication. Now I have up to 5 levels, however it is not that much in usual code when using composition. Please tell me how can I change my approach. Due to complexity of these connections, development of the code extremely slow, almost stopped.


It is hard to give a concrete example, because there are a lot of code, but the idea of problem which is very difficult to solve is following:

There are two QTreeView, which differently shows data from own model inherited from QAbstractItemModel (tree inside model is not that tree in previous discussion, this tree is only single level of hierarchy). I want to select objects in one QTreeView and change by that selection in second QTreeView. There are total 2 QTreeView, 2 different QAbstractItemModel instances, 2 trees of own objects (for each QAbstractItemModel), and single data.


Solution

  • Since there is only a single data source, you could do the following:

    1. Create a general model for that data source. The model should represent the data source generally, without consideration of what the views need.

    2. Create two proxy viewmodels that adapt the general model to the needs of the views.

    3. Couple the selection models of the views that display the viewmodels.

    Given the selection models on top of the two proxy models that map to the same source, we can propagate the selection change between them. We leverage the selection mapping provided by the proxy. The QAbstractProxyModel has a functional implementation of mapSelectionxxxx.

    void applySel(const QItemSelectionModel *src, const QItemSelection &sel,
                  const QItemSelection &desel, const QItemSelectionModel *dst) {
      // Disallow reentrancy on the selection models
      static QHash<QObject*> busySelectionModels;
      if (busySelectionModels.contains(src) || busySelectionModels.contains(dst))
        return;
      busySelectionModels.insert(src);
      busySelectionModels.insert(dst);
      // The models must be proxies
      auto *srcModel = qobject_cast<QAbstractProxyItemModel*>(src->model());
      auto *dstModel = qobject_cast<QAbstractProxyItemModel*>(dst->model());
      Q_ASSERT(srcModel && dstModel);
      // The proxies must refer to the same source model
      auto *srcSourceModel = srcModel->sourceModel();
      auto *dstSourceModel = dstModel->sourceModel();
      Q_ASSERT(srcSourceModel && (srcSourceModel == dstSourceModel));
      // Convey the selection
      auto const srcSel = srcModel->mapSelectionToSource(sel);
      auto const srcDesel = srcModel->mapSelectionToSource(desel);
      auto const dstSel = dstModel->mapSelectionFromSource(srcSel);
      auto const dstDesel = dstModel->mapSelectionFromSource(srcDesel);
         // we would re-enter in the select calls
      dst->select(dstSel, QItemSelectionModel::Select);
      dst->select(dstDesel, QItemSelectionModel::Deselect);
      // Allow re-entrancy
      busySelectionModels.remove(src);
      busySelectionModels.remove(dst);
    }
    

    The above could be easily adapted for a list of destination item selection models, in case you had more than two views.

    We can use this translation to couple the selection models of the views:

    void coupleSelections(QAbstractItemView *view1, QAbstractItemView *view2) {
      auto *sel1 = view1->selectionModel();
      auto *sel2 = view2->selectionModel();
      Q_ASSERT(sel1 && sel2);
      connect(sel1, &QItemSelectionModel::selectionChanged, 
              [=](const QItemSelection &sel, const QItemSelection &desel){
                applySel(sel1, sel, desel, sel2);
              });
      connect(sel2, &QItemSelectionModel::selectionChanged, 
              [=](const QItemSelection &sel, const QItemSelection &desel){
                applySel(sel2, sel, desel, sel1);
              });
    }
    

    The above is untested and written from memory, but hopefully will work without much ado.