Implementing a QTableModel for displaying fusion adapted structs
published at 22.03.2018 22:38
Save to Instapaper Pocket
Before I go into how this all works, I need to talk a bit about Qt, QObject, the Moc and templates/generic programming. See, when the Moc was implemented, and this basic system of Qt was designed more then 20 years ago, templates were an idea and a fairly obscure implementation. Qt is modeled after Java like OOP, at least when it got started, and most of that has stuck around in bits until today. Like that QObject is the base of everything, and that the Moc enables QObject todo some, um, cool things. Like limited reflection, the Qt metaobject model is what also QML and previous Qt Scripting things were build upon.
So to keep this short, when you derive from QObject, you can't have that class be a template, because the Moc never could handle that, and nobody has fixed that. So vanilla out of the bot Qt is mostly runtime, doing things at compile time and TMP is part of Qt, but not of the user interface. The internal APIs do use some level of template trickery, but its never exposed to the user. Qt has no traits, or interfaces that would enable the user to write their own Qt-templates. Qt uses templates for containers and such, but thats about it. Except template methods, a few classes use this to support user defined types, such as QVariants T value<T>() conversion method.
And as the last blog post was about how to count things with boost mp11, a library like mp11 is of limited use in the Qt world. Of course for writing template methods its surely a good tool to have, and a few other tricks exist to enhance template usage in Qt. CRTP can be used to plug a template layer between the base class and the implemented, concrete leaf class. But you usually always have to implement such a boilerplate class. Which in modern C++ could be a template, is in Qt often a set of implemented leaf classes, just because you need to use features which require the moc, like signal/slot.
So what follows, is for most of Qt a what if, as its not possible to use such code. I've always wondered how to connect the abilities of boost::fusion with adapting structs with Qts Model/View system, but the above mentioned limitations always kept me from doing so. There is CopperSpice, a fork of Qt4 which does not have above limitations, but I'd like to stay with mainline, well supported Qt if possible. Verdigris, a CopperSpice inspired C++14 library by Woboq enables Qt users to do that. Woboq has implemented a set of constexpr functions and Macros, to generate the code, which normally is generated by the moc. This makes the code you write with Verdigris compatible with vanilla Qt, but removes the limitations of the moc. Verdigris is header only, its more or less just two includes and two macros for each template which is derived from QObject or its children.
The QTableModel template
The first thing - to test this overall idea of combining boost fusion adapted types, TMP and Qt - was to implement a simple model for displaying each struct member in a column, and using a vector to hold n structs. I've implemented models previously, and this blog post covers the basics of writing a QTableModel. Because all this is build with Qts limitations, the method of choice to hold a generic value is QVariant for Qt. QVariant is more like std::any, but with a wide interface, and lots of build in things. So Qt Models use QVariants to exchange values between the data layer and the UI. I've chose to first cover the Model implementation, and then go into the conversion layer, so some details here have to wait. For now its just important, that even with using a template, we still have to implement the interfaces which are demanded by Qts Model View interface.
But lets start with the template class it self first:
#include <wobjectdefs.h>//verdigris header for definitions
template< class Container, typename ValueType = typename Container::value_type, typename ...Tags> class FusionTableModel : public QAbstractTableModel { static_assert(boost::fusion::traits::is_sequence< ValueType>::value,"ValueType must be fusion sequence"); W_OBJECT(FusionTableModel) //Q_OBJECT for templates from verdigris Container& con; std::array< const char*,boost::fusion::extension::struct_size< ValueType>::value> member_names = tagtype::get_member_names< ValueType>(); using taglist = boost::mp11::mp_list< Tags...>; const size_t colnumber = uitags::count_editable_tags< Tags...>(); std::array< size_t, uitags::count_editable_tags< Tags...>()> index_array = uitags::make_edit_index_array< Tags...>();
Wonder how much I need to explain here... Basic template stuff, then I use the verdigris macro to enable this to actually work with Qt. I decided to take the container it self by reference. One of the things I noticed is that suddenly I use std::array everywhere. Never used it previously, now its everywhere to store some values which come in contact with compile time calculations. Which happens here for the member names, which are used as column titles, and for index_array. The two functions from uitags:: are the ones from the last blog post. And I store the variadic template parameters in a typelist from mp11, right now just because I can. Because, I'm not sure, if variadic templates everywhere are the right interface, should I also write templates taking a typelist instead of a list of types? Would that compile faster? Still a TMP newbie I am...
And index_array is used to translate the model index to the type index, as I introduced a tag for not displaying a member in the last blog post also. I also need a tag for NoEdit it self, as some struct members maybe should be visible in the UI, but not editable. The colnumber member is returned in the columnCount method, while con.size() is what returns rowCount(). Qt uses int for sizes, but I'd like to keep size_t in my templates around...
A model has to implement various virtual functions from its parent, which are the interfaces used by views and delegates to access the data from the model. Hence the data method:
QVariant data(const QModelIndex &index, int role) const override { QVariant x; if(!index.isValid() || (index.row() >= con.size() || index.row() < 0)) return x; if(role == Qt::DisplayRole || role == Qt::EditRole) visit_fusion_sequence_at(con[index.row()],index_array[index.column()],[&x](auto& v){assign(x,v);}); return x; }
So far I haven't mentioned QModelndex, its the class that serves as the index interface, the model class accesses row() and column() mostly. Apart from asking first if the index is valid. This function returns a QVariant, but needs to have access to the nth member of whatever type we use to be exposed to Qt here. Afaik fusion does not offer a way to do so at runtime. So a layer for converting from fusion to a type at runtime needs to be implemented. This will be shown in the next blog post, for now all that matters is that a visitor like method is used to get access to the member via fusion, and then its assigned via an assign function.
Other virtual methods needed to be implemented are headerData for returning the column titles, which is very similar to aboves method, just returning the correct index from member_names. Then there is setData, which uses aboves fusion visit function to assign the QVariant to the struct member:
bool setData(const QModelIndex &index, const QVariant &value, int role)override { if (index.isValid() && role == Qt::EditRole && !(index.row() >= con.size() || index.row() < 0)) { visit_fusion_sequence_at(con[index.row()],index_array[index.column()],[&value](auto& v){qt_assign(v,value);}); emit dataChanged(index, index); return true; } return false; }
Except, this time the function called is qt_assign. As you can assign into a QVariant most types, but the other way around is Qt specific, and can't be covered by generic code which is part of the non-qt-specific layers for this. The assign function is part of this non Qt layer, and uses enable_if with is_convertible and is_constructible to ensure one actually can assign a type from another. But some types need to be overloaded/specialized to do the proper conversion, as the enable_if and is_con(vertible|strucible) isn't perfect on this. But into the g(l)ory of type conversions for Qts Model View system I'll get the next time.
To make the model actually editable, one needs to override the flags method:
Qt::ItemFlags flags(const QModelIndex &index) const override { if (!index.isValid()) return Qt::ItemIsEnabled; return QAbstractTableModel::flags(index) | Qt::ItemIsEditable; }
This will need a refactor, once the NoEdit tag exists, to return Qt::ItemIsEdible only for columns which should be user editable.
An then is only one thing left to do, verdigris needs a bit of more magic macros to work, there is an implemetation header and an IMPL macro:
#include <wobjectimpl.h> W_OBJECT_IMPL((FusionTableModel< Container,ValueType,Tags...>), template< class Container, typename ValueType, typename ...Tags>)
And then all one needs to do is instantiating this template with a class, giving it the proper tags, and the rest just works... As a long time Qt dev, this feels a bit weird, to suddenly be able to use this. But it opens a whole new world of possibilities...
Join the Meeting C++ patreon community!
This and other posts on Meeting C++ are enabled by my supporters on patreon!