PDA

View Full Version : Merging Rows of QStandardItemModels to Show in One QTreeView



ThinkingShip
5th September 2016, 20:25
I have several QStandardItemModels (each wrapped in a QSortedFilterProxyModel) that are populated in a tree-like way.
Conceptually what I'm modelling is variable values.
These variables exist under namespaces, which are potentially nested.
Each namespace item has 1 column (name), and is the parent item of all namespace and value items underneath it.
Each value item has 3 columns (name, modifiable status, value), and are the leaf nodes.

I have several of these models, one for each data type (int8_t, int16_t, float, ...), and currently I have a QTreeView and a QTab for each of them.
The views exist in their own tabs.

Now I want to be able to display a merged version of all the variables in a single window - assume I don't have access to the original data source, and that no fully qualified variable name conflicts.
Since namespaces are shared between data types, ideally the children of that namespace should all belong to the same parent - rather than copies of the namespace.

e.g. if I have foo.bar (float) and foo.baz (int), I would prefer the combined tree view to show

foo
├── bar
└── baz

rather than

foo
└── bar
foo
└── baz

I have already implemented this when serializing into YAML format by keeping track of the existence of namespace nodes; if they exist, attach to it; if not, create it, but I don't know if this would translate into the same solution when merging models.

It's also important to maintain the itemChanged signals of the original models, since I have different slots depending on the type I'm changing.

How do I achieve this sort of tree merge? I saw a post here about wrapping multiple QStandardItemModels (http://www.qtcentre.org/threads/24900-QAbstractItemModel-to-wrap-multiple-QStandardItemModels-throws-ASSERT-in-QTreeView), but no one replied to that post,
and the result we're looking for is slightly different (they want parallel trees, I want merged trees).

anda_skoa
6th September 2016, 11:33
My suggestion would be to model the data in a respective data structure and then use custom models on that.
This allows you to handle the combined case just like any of the type specific cases.

Alternatively one QStandardItemModel that contains everything and custom QSortFilterProxyModel that can additionally filter for a type.

Cheers,
_

ThinkingShip
6th September 2016, 12:27
I see, thank you for the quick response!
Option #1 isn't really doable due to the low level nature of the values (they have to stored in statically allocated arrays indexed by an enum),
but option #2 looks workable.

If I have a single underlying model, and a QSortFilterProxyModel for each type (which I already do, except the current mapping is 1 to 1),
how would my connection of type-specific callbacks to QStandardItemModel::itemChanged be changed?
Would I then have to store a type column into the leaf nodes (so the value items will be 4 columns long), then on any itemChanged
for the whole model, I dispatch based on the type stored?

anda_skoa
6th September 2016, 14:54
Option #1 isn't really doable due to the low level nature of the values (they have to stored in statically allocated arrays indexed by an enum),

I am afraid I don't understand.

Even if the source data structure is not capable of providing all information (which sounds doubtful), you could copy the data into a structure that does, just like you are doing now.



If I have a single underlying model, and a QSortFilterProxyModel for each type (which I already do, except the current mapping is 1 to 1),
how would my connection of type-specific callbacks to QStandardItemModel::itemChanged be changed?

Not sure what you mean, each type's filter model will be a separate signal source.



Would I then have to store a type column into the leaf nodes (so the value items will be 4 columns long), then on any itemChanged
for the whole model, I dispatch based on the type stored?
Don't you already know the type of an entry? How would you otherwise even filter for a type?

Cheers,
_

ThinkingShip
6th September 2016, 17:04
Currently the data representation looks like this (conceptual)
Flat array ⇋ Tree QStandardItemModel
Where the actual data structures are these (for each parameter type)
2 maps for QStandardItem* ⇋ parameter index
2 maps for const char* parameter name ⇋ parameter index
array of ParameterInfo<type> holding boundary value and write status, indexed by parameter index
array of type holding actual value, indexed by parameter index

If I could redesign it I'd have a single parameter data structure for all types, holding its own type information, variant value, variant value boundaries, write status, name, and parameter index, held in an array indexed by the parameter index. I still wouldn't make the base data structure a tree because there's the requirement of no dynamic memory allocation for embedded usage (data structure is unfortunately shared between PC and onboard...).

Your suggestion of having an intermediate tree data structure would make it (conceptually)
Flat array ⇋ Tree of a new struct holding a variant for a value ⇋ Tree QStandardItemModel
Except here it's a many-to-1 mapping from flat array to the tree holding the data structure I mentioned above (with the addition of a list of children).
It seems like a lot of work for not so much gain.
If this is the minimum amount of work to merge the trees, I would be fine with a solution that presented the trees in parallel like


foo
└── bar
foo
└── baz


Not sure what you mean, each type's filter model will be a separate signal source.
The QSortFilterProxyModel is a signal source, but does it forward its underlying model's signals?
What I mean is that from the documentations, there is no QSortFilterProxyModel::itemChanged signal.
To establish context (and to set your expectation of my knowledge level so you know the intent behind my questions), I'm relatively inexperienced with Qt programming and aren't sure if the following is allowed:

connect(myProxyModel[type], &QStandardItemModel::itemChanged, this, myCallback[type]);



Don't you already know the type of an entry? How would you otherwise even filter for a type?
I do know the type of each entry; it's just that it's implicitly rather than explicitly encoded right now.
Currently we know the type of an entry based on which model it's in, since each model holds only parameters of that type.
My question relates to my previous premise that I couldn't distinguish the type based on the signal source.
However, if the previous line of code I posted is valid (i.e. the type can still be implicitly encoded in which proxy model is the signal source),
then this is not needed.

I'm assuming that if the previous line is valid, the proxy model doesn't fire on items that were filtered out.

anda_skoa
6th September 2016, 18:48
array of ParameterInfo<type> holding boundary value and write status, indexed by parameter index
array of type holding actual value, indexed by parameter index

So the "foo" part in your graphic comes from the ParameterInfo?


I still wouldn't make the base data structure a tree because there's the requirement of no dynamic memory allocation for embedded usage (data structure is unfortunately shared between PC and onboard...).

Shared as in same code or shared as in shared-memory?

In any case, I am right when I assume that you treat the array of ParameterInfo for a type and the data array for a type like an array that contains a "pair" with those two entries?
I.e. they have the same length and the same index refers to the same "pair" of info+data?



Your suggestion of having an intermediate tree data structure would make it (conceptually)
Flat array ⇋ Tree of a new struct holding a variant for a value ⇋ Tree QStandardItemModel

No, in this scenario there is no QStandardItemModel, duplicating the data third time would be really wasteful.

And you don't necessarily need the tree structure if you can do the mapping for a "tree node" into the flat structure on-the-fly.
It is often just easier to have an actual tree when implementing a tree model.



The QSortFilterProxyModel is a signal source, but does it forward its underlying model's signals?

It would be useless if it didn't do that, a view working on the proxy model will need to get update notifications just like if it would work with the source model.
A filtering proxy will obviously apply its filtering before emitting its respective signals.



What I mean is that from the documentations, there is no QSortFilterProxyModel::itemChanged signal.

Yes, that convenience signal is specific to QStandardItemModel, all models emit dataChanged(), which is what the view classes are using.



To establish context (and to set your expectation of my knowledge level so you know the intent behind my questions), I'm relatively inexperienced with Qt programming and aren't sure if the following is allowed:

connect(myProxyModel[type], &QStandardItemModel::itemChanged, this, myCallback[type]);

That would fail as the class of myProxyModel[type] is not likely a QStandardItemModel or derived from it, see above.



I do know the type of each entry; it's just that it's implicitly rather than explicitly encoded right now.
Currently we know the type of an entry based on which model it's in, since each model holds only parameters of that type.

The combined model would need a type info per node for filtering to work.
But of course you could alternatively use separate models for the single type views.



I'm assuming that if the previous line is valid, the proxy model doesn't fire on items that were filtered out.
Yes, see above.

Cheers,
_

ThinkingShip
8th September 2016, 12:41
So the "foo" part in your graphic comes from the ParameterInfo?
The "foo" in the graphic is the namespace the variables bar and baz reside in.
This information is encoded in the variable name (a separate array using same index), so that their names would be "foo.baz", "foo.bar".



Shared as in same code or shared as in shared-memory?

Shared code (definitely not shared memory xD).



In any case, I am right when I assume that you treat the array of ParameterInfo for a type and the data array for a type like an array that contains a "pair" with those two entries?
I.e. they have the same length and the same index refers to the same "pair" of info+data?

Yes, it is informationally equivalent to a single array of structs with each struct holding info+data+name (info excludes name; it has just boundary information for some reason).


So let me summarize your proposals to verify that I'm interpreting them correctly:
1. The intermediate data structure is not necessary since we have the information to map flat array -> tree using the variable name, which is fully qualified and includes all namespaces the variable is under ("topnamespace.foo.bar"). The inverse mapping from Qt Item -> flat array could be created and cached during the creation of the items.

2. (this point I'm not clear about) I would have 1 combined model, with an additional 4th column encoding the type information.
You mentioned that "No, in this scenario there is no QStandardItemModel"; what would this combined model be?

The problem comes in here, as I would like for there to be an option to have just 1 merged view, and sometimes with type specific view.
For the 1 merged view, I can't just show the combined model in a TreeView because some variables are supposed to be hidden from view.

Actually, I think the better option would be to just not create items for those hidden variables - if they won't be shown, then they won't be modified.
However, I think I would still need an additional QSortProxyModel since it holds additional boundaries on the visible variables (2nd set of limits).

So in the end, what it might look like:


uint8_t ───┐
uint32_t ──┤
float ─────┴ Combined model (QStandardItemModel?)
│(single view mode)
├ single QSortProxyModel just to apply additional bounds checking ── QTreeView
│
│(or type-specific view mode)
├ uint8_t QSortProxyModel filter both type and bounds ── QTreeView
├ uint32_t QSortProxyModel filter both type and bounds ── QTreeView
└ float QSortProxyModel filter both type and bounds ── QTreeView


The main problem for me is structuring the infrastructure in such a way that there is little code replication between the two modes.

Thanks for looking into this.

anda_skoa
8th September 2016, 16:04
I would go for two type of models:
* a model that handles a single type
* a model that handles all types

If I were coding this I would create two custom model classes, potentially templating the single type model for the respective type.
But you can use QStandardItemModel if you feel that would be easier, writing a tree model is not that easy at the first time.

Cheers,
_

ThinkingShip
8th September 2016, 17:47
I would go for two type of models:
* a model that handles a single type
* a model that handles all types

If I were coding this I would create two custom model classes, potentially templating the single type model for the respective type.
But you can use QStandardItemModel if you feel that would be easier, writing a tree model is not that easy at the first time.

Cheers,
_

Ah I see, so similar to my last diagram except without the QStandardItemModel?
What would you subclass those models off of? QAbstractItemModel?

I think I have enough of an idea to start working on it; thanks for the help!

anda_skoa
8th September 2016, 19:00
Yes, you need to derive from QAbstractItemModel since you need a tree model.

The model for one data type needs a reference or pointer to the respective arrays, them build some lookup.
E.g. a map with the keys being the "namespace" parts and the value being the index into the array.

QModelIndex for a top level item is the the "n-th" key, etc.

Cheers,
_