A List Widget of Widgets in Qt
Qt is a very flexible and complete tool. That flexibility has its limits, though, and sometimes the Qt documentation can lead one down dead-ends. That is what happened to me recently. All I wanted to do was to put some buttons and text fields in a list widget. It seems like a reasonable thing to do, right? After all, the built-in list widget allows adding a check-mark to each item very easily. How hard could it be to add a few more buttons?
At the same time, I wanted to be able to drag these items between multiple list widgets. This is easily supported in the standard list widget. Drag-and-drop allows you to design a user interface that is easy to use and intuitive.
It turns out that fulfilling these two requirements proved to be more difficult than I anticipated. In this article, I’ll outline all the problems I encountered, and tell you the final, surprisingly simple answer to all those problems.
The first, naïve, approach is to assume that it is possible to simply add a QWidget containing multiple sub-widgets in a QListWidget. After all, there are many examples of list views containing check-boxes. Therefore, adding other types of buttons must simply require finding the right function to call. Something about it must be buried somewhere in the Qt documentation, I thought.
I carefully sifted through the documentation, but always came up empty. Turns out, this is just plain impossible.
When you think about it, there is a very sensible reason why Qt does not support this. A Qt list view can contain thousands of items in a single view. If each of those items contained multiple live widgets, it could add up to thousands of live widgets. Given that each widget provides a rich interface, and thus a lot of data, you’d end up with too heavy a load. Instead, Qt only keeps the barest minimum amount of data per item. This design choice means that widgets cannot be put directly in a list view.
Digging in the Qt documentation, I discovered the concept of the item delegate. This is how Qt allows the editing of data in a list view item. Unfortunately, this solution is far from ideal, because the buttons are not visible in the list view. To make the buttons visible, the user must invoke the item editor through what Qt calls an edit action: either pressing F2 or double-clicking. But these are not actions a user would know. The resulting user interface is neither intuitive nor easy to discover, making it inelegant.
Besides, this requires the user to know the item is editable in the first place.
If only there was a way to show the editable form at all times…
… but there is! The item delegate provides a paint function that can be modified to paint anything. But how does one paint an arbitrary widget? There is actually a way. If you look at the source code for how the standard list widget paints the check-box, you can see that Qt provides “style-option” classes to draw widgets. Unfortunately, you’ll quickly notice that this feature is seriously under-documented and, worse, that you’ll need a lot of code to support all the normal functionalities of the widget, like hover and selection.
Then, even if you do paint the widgets, they’re still not interactive! Users still have to double-click to make them editable, which makes things even more confusing for them.
And there is yet another catch! Item delegates can only be used by custom item models, i.e. you cannot use a delegate with a standard item model. Think this is not so bad? Unfortunately, implementing your own custom model is anything but easy.
The problem is that Qt has strict rules on how an item model should behave. Getting all the rules straight is difficult, and the slightest deviation leads to strange behavior and glitches that are very hard to diagnose. And this is just for the non-editable models! If you want to support drag-and-drop, then the rules are even more complex and unforgiving.
So, choosing the route of using a delegate results in writing a lot of code that is highly complicated to get right and maintain. There must be a better way!
So what is the correct way to put complex widgets in a list? The answer is disappointingly simple: don’t use a list widget. Use a simple widget. Yes, a straight QWidget.
After all, a QWidget can contain an arbitrary list of sub-widgets. Of course, you then have to add the standard functionality of a list view: things like selection, drag-and-drop and scrolling. But it turns out that this is the simplest of all the approaches. And, as a bonus, you are assured that the behavior of all user interfaces you put in your items will be exactly the same as the normal behavior the user is expecting.
My solution to the problem of making a Qt list widget that can contain complex widgets is as follows. Four classes interact with each other to provide the functionality:
|QWidgetListWidget||The widget that contains the items.|
|QWidgetListItem||The items that can be put in the list. Can contain any number of sub-widgets, of any type.|
|QWidgetScrollListWidget||A wrapper around QWidgetListWidget to provide scrolling.|
|QWidgetListMimeData||The MIME data used to support drag-and-drop between multiple QWidgetListWidget.|
This is one of the two main points of interest in the design: the list widget. This list view supports item selection and drag-and-drop. It provides a simple interface, made of a few functions. Here is the C++ declaration of these functions.
// Create a widget list widget. QWidgetListWidget(ListModifiedCallbackFunction modifCallback, bool stretch, QBoxLayout::Direction dir, QWidget * parent); // Check if the list is vertical or horizontal. bool isVertical() const; // Clears the list panel of all items. void clear(); // Add a widget item. QWidgetListItem* addItem(QWidgetListItem* item, int index = -1); // Remove a widget item. void removeItem(QWidgetListItem* item); // Retrieve all widget items kept directly in this list widget. std::vector<QWidgetListItem*> getItems(bool onlySelected = false) const; // Retrieve all selected widget items kept directly in this list widget. std::vector<QWidgetListItem*> getSelectedItems() const;
This is the second main point of interest. It is a simple item that can be selected and cloned. The cloning is used during drag-and-drop to copy an item from one list to another. Here is the full C++ interface, which is quite short.
// Create an item. QWidgetListItem(QWidget* parent); // Selection. bool isSelected() const; void select(bool sel); // Item cloning for drag-and-drop. virtual QWidgetListItem* clone() const;
This class only exists to make scrolling in the list widget optional. It might seem strange to make scrolling optional, but it is quite handy when you want to embed a list view within items in another list view. The whole C++ interface is simply this:
// Create a scrollable widget around another widget. QWidgetScrollListWidget(QWidget * widget, QWidget* parent);
The final class exists to support drag-and-drop. You should never have to use it directly. The entirety of its implementation in C++ is as follows:
static constexpr char MimeType = "application/x-qwidget-list-item"; QWidgetListItem* Widget = nullptr; QPoint HotSpot;
The entirety of the implementation is available on GitHub.
The code comes with an example application showing how to use the classes. See the description of how to build the project on GitHub.
More complex example can be found in the TreeFilterApp project. It even shows list widgets within another list widget. The code is also on GitHub.