Lazy Qt Models from QVariant
In Calamares there is a debug window; it shows some panes of information and one of them is a tree view of some internal data in the application. The data itself isn’t stored as a model though, it is stored in one big QVariantMap. So to display that map as a tree, the code needs to provide a Qt model so that then the regular Qt views can do their thing.
Each key in the map is a node in the tree to be shown; if the value is a map or a list, then sub-nodes are created for the items in the map or the list, and otherwise it’s a leaf that displays the string associated with the key. In the screenshot you can see the branding key which is a map, and that map contains a bunch of string values.
Historically, the way this map was presented as a model was as follows:
- A JSON document representing the contents of the map is made,
- The JSON document is rendered to text,
- A model is created from the JSON text using dridk’s QJsonModel,
- That model is displayed.
This struck me as a long-way-around. Even if there’s only a few dozen items overall in the tree, it looks like a lot of copying and buffer management going on. The code where all this happens, though, is only a few lines – it looks harmless enough.
I decided that I wanted to re-do this bit of code – dropping the third-party code in the process, and so simplifying Calamares a little – by using the data from the QVariant directly, with only a “light weight” amount of extra data. If I was smart, I would consult more closely with Marek Krajewski’s Hands-On High Performance Programming with Qt 5, but .. this was a case of “I feel this is more efficient” more than doing the smart thing.
I give you VariantModel.
This is strongly oriented towards the key-value display of a QVariantMap as a tree, but it could possibly be massaged into another form. It also is pushy in smashing everything into string form. It could probably use data from the map more directly (e.g. pixmaps) and be even more fancy that way.
Most of my software development is pretty “plain”. It is straightforward code. This was one of the rare occasions that I took out pencil and paper and sketched a data structure before coding (or more accurate: I did a bunch of hacking, got nowhere, and realised I’d have to do some thinking before I’d get anywhere – cue tea and chocolate).
What I ended up with was a QVector of quintptrs (since a QModelIndex can use that quintptr as intenal data). The length of the vector is equal to the number of nodes in the tree, each node is assigned an index in the tree (I used depth-first traversal along whatever arbitrary yet consistent order Qt gives me the keys, enumerating each node as it is encountered). In the vector, I store the parent index of each node, at the index of the node itself. The root is index 0, and has a special parent.
The image shows how a tree with nine nodes can be enumerated into a vector, and then how the vector is populated with parents. The root gets index 0, with a special parent. The first child of the root gets index 1, parent 0. The first child of that node gets index 2, parent 1; since it is a leaf node, its sibling gets index 3, parent 1 .. the whole list of nine parents looks like this:
-1, 0, 1, 1, 0, 0, 5, 5, 5
For QModelIndex purposes, this vector of numbers lets us do two things:
- the number of children of node n is the number of entries in this vector with n as parent (e.g. a simple QVector::count()).
- given a node n, we can find out its parent node (it’s at index n in the vector) but also which row it occupies (in QModelIndex terms), by counting how many other nodes have the same parent that occur before it.
In order to get the data from the QVariant, we have to walk the tree, which requires a bunch of parent lookups and recursively descending though the tree once the parents are all found.
Changing the underlying map isn’t immediately fatal, but changing the
number of nodes (expecially in intermediate levels) will do very strange things.
There is a reload()
method to re-build the list and parents indexes
if the underlying data changes – in that sense it’s not a very robust model.
It might make sense to memoize the data as well while walking the tree –
again, I need to read more of Marek’s work.
I’m kinda pleased with the way it turned out; the consumers of the model have become slightly simpler, even if the actual model code (from QJsonModel to VariantModel) isn’t much smaller. There’s a couple of places in Calamares that might benefit from this model besides the debug window, so it is likely to get some revisions as I use it more.