PDA

View Full Version : MVC + QGraphicsView + Commands



panicq
23rd October 2018, 20:03
Hi,

I'm currently developing a timeline widget (in the spirit of flash). For now my data is stored directly in the widget and I'm using 2 views (one for the layers and one for the corresponding frames) in a QSpliter but I would like to use the Model View framework as it sounds more suited for my needs. Though I would like advice to get started with this new approach.

- What is the best way to create a custom view that uses a model ? I've already used the premade ones (QListView for instance) but never created custom o.
- How dose it works with QGraphicsView ? Should I use multi-inheritance with QAbstractItemView ?
- How dose my modifications in the QGraphicsScene updates the model accordingly ?
- What is the best way to integrate commands (for undo) in order to add/remove layers or frames with this approach ?

- Do you think this approach of using ModelView framework is good in my case ?

Thank you in advance.

Have a nice day,

Panicq.

lni
24th October 2018, 14:14
There is no model/view for QGraphicView framework. You can use Qwt, or inherit from QGraphicItem and add the items to QGraphicScene.

d_stranz
25th October 2018, 23:14
Should I use multi-inheritance with QAbstractItemView ?

Multiple inheritance is not allowed in Qt for any classes where both classes inherit from QObject, so this is not a possibility.


What is the best way to create a custom view that uses a model?

Normally, you would derive a custom model from QAbstractItemModel and then also derive a custom view from QAbstractItemView or one of the classes derived from that. You can't use a QAbstractItemView with QGraphicsScene / QGraphicsItem, but this doesn't prevent you from using a QAbstractItemModel to hold the information about the items in your scene.

If you can represent the items in your scene as a table or tree (with columns for item name, x and y positions, width and eight, item type, ancestor / descendant relationships, etc.) then you can derive from QAbstractTableModel or QAbstractItemModel and use roles greater than Qt::UserRole to extract these columns and use them to create items in your scene.

For example, for the x column, Qt::DisplayRole might return the QString "1234.5", but Qt::UserRole + 1 would return a QVariant containing a float with value 1234.5. So if you are displaying the model is a table or tree, that view is going to call the model's data() method with Qt::DisplayRole, but your scene will call it using the UserRole you have defined (you'll write that code... see below).

You will also have to derive from QGraphicsScene to create a custom scene class that holds a pointer to your model (just like QAbstractItemView does). You should create slots in this class to respond to signals sent by your model, for example dataChanged(), modelReset(), rowsInserted(), etc. When your scene receives one of these signals, it should update / add / remove the items that have changed.

Your scene updates itself by calling the model's data() method with the roles corresponding to the custom UserRole(s) you have defined. The reply from the model will tell you the new values for each of columns in the row(s) of the items that have changed.


How dose my modifications in the QGraphicsScene updates the model accordingly?

You will have to implement setData(), insertRows(), and removeRows() (and maybe more) for your custom model. This means that you also have to associate a "row number" from your model with each item in your graphics scene so you can create a QModelIndex for it that will be used when you update the model.

Whenever you change the model, setData() / insertRows() / removeRows() have to emit the right signals, by calling the QAbstractItemModel protected methods beginInsertRows() / endInsertRows() (when a new item is added) or beginRemoveRows() /endRemoveRows() (when an item is deleted) or emitting dataChanged() when existing data is modified through setData().


What is the best way to integrate commands (for undo) in order to add/remove layers or frames with this approach?

Take a look at the Qt Undo Framework (https://doc.qt.io/qt-5/qundo.html) for ideas.


Do you think this approach of using ModelView framework is good in my case?

Sure. It will take some work, and you should spend some time studying how the Model / View framework works with custom models and views. There are some huge advantages, though:

1 - When you use a Model, your model data is now independent of how you display it. Maybe your primary display is a graphics scene, but maybe you might also want to display the data in a table or a tree. If the data is in a model, you do not have to make any changes to do this except to ensure that the parent() / rowCount(), columnCount(), etc. methods are all correctly implemented.

2 - You change the model item when editing, not the QGraphicsItem. This means any changes made to the model from anywhere will automatically update any view that shows the data.

3 - You can use the model as the source for persistence to a file or DB. It is very easy to write serialization to / from XML for example using a model as the source.

4 - Because each graphics item is associated with a row inthe model, it is also very easy to write custom tool tips, editing dialogs, etc. which use the model and row number as the data source.

Remember that your custom model is just a wrapper around some data structure that works best to hold your timeline data. You might use a QVector of items, each of which represents one row in the model, or you might have to use a tree-like data structure if there are parent / child relations among the items. In any case, the model will map this private data structure into QModelIndex indexes that the scene and all of the views will use.
So go for it.

panicq
26th October 2018, 17:33
Wow thanks for that exhaustive answer !

So while I was waiting for answers I started to implement my own system and I'm pretty happy with it as it is for now. It follows the same principle but doesn't use the official framework as I wasn't sure about the best practices of the framework even after skimming through the doc.
12978
So I went with this approach. The timeline widget makes the link between the model and the actual views (layer and frame). And I'm using commands for updating the model using a set dirty approach for destroying my memory when the undostack is cleared.

But for sure I'll follow what you've said in case I need to refactor.

Thank you again !

d_stranz
27th October 2018, 02:44
So I went with this approach.

As they say, there is more than one way to skin a cat.

An example of how I use an abstract model:

I have a program that analyzes scientific data which produces results that are represented in a tree-like hierarchy. I can use taxonomic rank as an example - domain -> kingdom -> phylum -> class -> order -> family -> genus -> species -> individual just like you learned in biology class. In my case, I am using only the last three layers - genus -> species -> individual.

The QAbstractItemModel is therefore a tree model, and I can directly display it in a QTreeView. However, I also want to flatten it into a table with one row per each individual. For this, I use the KDE FlatProxyModel (https://api.kde.org/bundled-apps-api/calligra-apidocs/calligra/plan/html/kptflatproxymodel_8cpp.html) and a QTableView. So far no big deal.

It gets interesting, though: At the "individual" level, there are multiple numerical data values in the columns of each row. I might want to produce a histogram bar chart of the distribution of a particular column by species, say height or weight. So I create a QAbstractProxyModel which selects out only the species and height columns. I use QCustomPlot (https://www.qcustomplot.com/) to create these histograms. The QCustomPlot instance is contained in a custom QAbstractItemView. When any of the models or proxies change, my abstract view responds to the model's signals by pulling data from the model (through the various proxy layers), binning it (height < 4', 4' - 5', 5' - 6', height > 6' say) and rebuilds the histogram.

Likewise, if I want to create a scatter plot of height vs. weight by species, I use a different QAbstractProxyModel which selects out the columns for species, height, and weight. Now for each species, I can create a set of (height, weight) dots (one dot per individual) and plot them as a scatter plot. If I want to add more information (say the age of the individual), I can add another column to the proxy to pull out "age" and use that value to change the size, shape, or color of the dot. So now I have a scatter plot of height vs weight, with a different marker shape of each species and a different color for each age group.

My app has a dozen or more different plots the user can produce. Every one of them uses the same base model, flattened and filtered through proxies, to create the items in the plot. Any time the user changes a data analysis parameter that causes a recomputation of results in the base model, everything in the UI that uses that model as a source gets updated automatically.

Adding a new type of plot simply means creating a new QAbstractItemView, a new proxy model, and hooking up the right signals and slots. The proxy models have been abstracted such that all scatter plots use the same proxy model and view classes. The proxy class has slots to set the x column number, y column number, color / size / marker column number, etc. from the base model. The view class doesn't need to know these column numbers - it simply asks for column 0 for x, column 1 for y, column 2 for color, etc., and the proxy maps those to the real column numbers (mapToSource(), mapFromSource()).

It is all very slick, but it took a bit of thought and attention to get the right signals and slots talking.