RobertW
RobertW

Reputation: 103

Beginning Qt Model / View with Nested Object Collections

I'm primarily a .Net developer and have been investigating Qt for a while now. I'm now at the stage of trying to implement the model / view framework in Qt. I think I have a grasp of the basic principles but am unclear of how to hang things together in a more complex UI where widgets need to communicate with each other. Given the following:

// 'domain' model classes
class NestedDomainModel1
{
public:
  NestedDomainModel1();

  QString name() const;
  void setName(const QString& newName);

  // other properties

private:
  QString m_name;
};

class NestedDomainModel2
{
public:
  NestedDomainModel2();

  QString name() const;
  void setName(const QString& newName);

  // other properties
};

class MyDomainModel
{
public:
  MyDomainModel();

  void addNestedModel1(const NestedDomainModel1& modelToAdd);
  NestedDomainModel& nestedObjectModel1At(int index);
  int nestedObjectModel1Count() const;

  // repeat for model 2


private:

  QList<NestedDomainModel1> m_nestedModels1;
  QList<NestedDomainModel2> m_nestedModels2;
};


// 'GUI' classes
class MainWindow : public QMainWindow
{

private:
  MyDomainModel* m_model;
  MyTreeViewWidget* m_treeWidget;  // -> this sits in a left dock window
  MyInfoDisplayWidget* m_infoWidget; // -> this sits in a right dock window and display details about the item selected in the tree
};

class MyDomainModelTreeModel : public QAbstractItemModel
{
public:
  explicit MyDomainModelTreeModel(MyDomainModel* model);

  // required overrides for QAbstractItemModel
private:
  MyDomainModel* m_model;
};

class MyTreeViewWidget : public QWidget
{
public:
  // Take a pointer to the domain model and create a model for the 'view'.
  // Will create a tree like:
  // Nested Objects 1
  //   |- object 001
  //   |- object 002
  //   |- you get the idea
  // Nested Objects 2
  //   |- other object 001
  //   |- more of the same
  explicit MyTreeViewWidget(MyDomainModel* model);

public slots:  
  // Used to notify widget when an item is added to the underlying model.
  void nestedModel1Added(); 
  void nestedModel2Added(); 

signals:
  void nestedModel1Selected(NestedDomainModel1& selectedModel);  
  void nestedModel2Selected(NestedDomainModel2& selectedModel);

private slots:
  // connect to tree view event when an item is selected and if all ok, emit one of the selected events
  void onTreeItemSelectionChanged(const QItemSelection &selected, const QItemSelection &deselected);  

private:
  QTreeView* m_treeView;
  MyDomainModelTreeModel* m_treeModel;
};

class MyNestedClass1ViewModel : QAbstractItemModel
{
public:
  explicit MyNestedClass1ViewModel(NestedDomainModel1* model);

  setModel(NestedDomainModel1* model);

  // required overrides for QAbstractItemModel

private:
  NestedDomainModel1* m_model
};

class MyInfoDisplayWidget : public QWidget
{
public:
  explicit MyInfoDisplayWidget(QWidget* parent = 0);

public slots:
  // this is connected to the 'tree' widget signal in MainWindow
  void setModel(NestedDomainModel1& selectedModel);
};

The basic premise of the UI is something similar in feel to Visual Studio. The tree is similar to the Solution Explorer and the 'info display' is similar to the properties window.

  1. Is this how you use the model / view framework? For those familar with WPF / Silverlight development, is the model / view framework similar to MVVM (at a high level) in that it is the 'model of the view' and wraps / contains the domain model?

  2. Is this how you connect the widgets using the model / view framework (ie. one widget passes a pointer or reference of the model to another)? Or should I be using the SelectionModel? Does that work since the tree model contains different types of objects?

  3. How do you identify the root nodes? For instance, when a MyNestedObject1 is created and needs to be added to tree do I rely on the knowledge that root node is at a model index QModelIndex(0, 0) (ie. row 0 with an invalid parent index)?

Upvotes: 0

Views: 1190

Answers (1)

Simon Hibbs
Simon Hibbs

Reputation: 6211

I'm finding the terminology you're using a bit awkward, for example MyNestedClass1ViewModel is just a model. I'm not sure what a ViewModel would be.

What you're missing in this example is an actual view. MyTreeViewWidget is just a dumb widget that isn't actually a view in Qt terms at all, it's essdentialy just a dumb 'canvas' that you want to display data in. So the way to do this is:

  1. You have underlying data in ordinary objects such as NestedDomainModel2. These are not Models in the Qt sense though and I wouldn't name them as such. They're just ordinary objects and don't implement any of the MVC interfaces.

  2. Your MyNestedClass1ViewModel, which is a Qt model class. It accesses the underlying data objects above (1) in the implementation of it's data() and setData() methods.

  3. A view class subclassed from QAbstractItemView. This is what you're missing. It has all the magic hooks to plug into the API of the model class from (2) above. It gets signals from the model telling it when there have been changed, which invoke methods such as dataChanged(), rowsInserted(). You implement these methods to make appropriate changes in your display widget below in point (4).

  4. Your display widget. It doesn't implement any of the model/view API itself and is updated by your view. If it's interactive and can be used to change model data, you do that by calling setData(), insertRows(), removeRows(), etc on the model. The display changes will automatically propagate back to the widget via the view. Be careful not to generate infinite loops of changes propagating from widget->model->view->widget->model->view etc.

I have done a similar thing to use a QGraphicsScene/QGraphicsView to display items in a model. Despite it's name QGraphicsView isn't part of the model/view framework, so I implemented a custom view class which drew the model data on the QGraphicsScene.

Here's my code, in Python. It draws worlds on a map for an SF wargame:

class WorldItemView(QtGui.QAbstractItemView):
""" Hidden view which interfaces between the model and the scene.
"""
def __init__(self, model, parent=None):
    QtGui.QAbstractItemView.__init__(self, parent)
    self.hide()
    self.setModel(model)
    self.my_model = model
    self.scene = MapScene(self.my_model)
    self.resetWorlds()

def dataChanged(self, topLeft, bottomRight):
    top_row = topLeft.row()
    bottom_row = bottomRight.row()
    #debug_log("Top row " + str(top_row) + " Bottom row " + str(bottom_row))
    for row in range(top_row, (bottom_row + 1)):
        self.scene.worldChanged(row)

def rowsInserted(self, parent, start, end):
    for row in range(start, (end + 1) ):
        pmi = self.my_model.getPMI(row)
        self.scene.insertWorld(pmi)

def rowsAboutToBeRemoved(self, parent, start, end):
    for row in range(start, (end + 1)):
        self.scene.removeWorld(row)

def resetWorlds(self):
    self.scene.clearWorlds()
    # Add worlds to scene
    last_row = self.my_model.rowCount() - 1
    self.rowsInserted(None, 0, last_row)

I hope that helped.

Upvotes: 1

Related Questions