[SOLVED] Drag and dropping widgets in a grid at runtime
Here's what I'm trying to do: I have certain data that is displayed through a custom widget. There is a lot of such data. At runtime, the user will be presented with a list of all data and they will choose which ones they want to monitor, which will dynamically create the custom widget in a grid. I want to let the users reorganize the widgets in the grid as they please, preferably though drag and dropping.
The closest thing I could find to achieve this is using a QTableWidget with the following properties: setDragEnabled(True), setDragDropOverwrite(False), setDragDropMode(QtCore.Qt.InternalMove). With these properties, it's possible to drag cells and move them to other cells.
However, this seems to only work with the QTableWidgetItem attached to each cell, not the widgets set though setCellWidget().
Here is a small project illustrating what I'm trying to do: https://gist.github.com/anonymous/02bbcc201316fa8f43e9
You can see that the widgets created (by clicking on the button) are not drag'n'droppable, however if you edit a cell (which sets the QTableWidgetItem), those are drag'n'droppable. It seems the cellWidget part and the item part of a cell are completely independent, and the drag and drop logic only applies to the items, not the cell widgets.
I'm afraid I already know the answer, but... is there any way to do what I want, short of replacing the QTableWidget by a QTableView, using a model, and re-implementing all the drag and drop logic myself?
Or maybe my approach is completely wrong. Maybe there's some other widget or Qt facility that I can use? I'm open to any and all suggestions.
Thanks in advance!
What about creating a QStyledItemDelegate that will show the information the way you want them ? Doing so you'll have the drag & drop feature for free without reimplementing QTableView/Widget.
Hope it helps
Thanks for the suggestion, it seems very promising! I've tried a few things and I think I'm much closer to my goal.
I'm wondering how best to "link" my custom widget to the delegate. I found this Stack Overflow thread, but creating the widget inside the paint() method is not a usable solution in practice. I need to create each widget only once and have the delegate somehow be aware of them so it can render them.
In this other Stack Overflow thread, someone commented "AFAIK, you can't use custom widgets for viewing items in a List/Table/Tree-View-delegates. You can use them for editing, but if the view-mode requires anything other than just a redraw, you're pretty stumped."
Any further ideas?
Do you really need to render a widget to show your data ?
Sadly, yes. Data is pulled from a backend, the widget needs to be registered in another class so the data gets refreshed. This is an existing (badly designed) system, and the widget is just too tightly coupled with so many other things.
I guess I'm SOL... Obviously the proper way to use widgets in a QTableWidget is through the setCellWidget() method, which doesn't support my "reorganizing at runtime" usecase. I'll take a look at the QTableWidget's source code and see if it's feasible to adapt the drag and drop logic to widgets.
Don't be sad so early. What would you like to have ? Something like a vertical list of your widgets ? e.g. like a QScrollArea containing a widget with a QVBoxLayout filled with your special widget ?
Something like that, yes. Check out the mini-project I included in the first post if you can, it should illustrate what I want.
My first prototype was actually a QScrollArea with a QVBoxLayout. I figured if I could make the drag and drop work, then turning the vertical list into a 2D grid wouldn't be too difficult, but I couldn't make the drag and drop work in the first place, and thus was hoping for some other Qt goodness that would do what I want out of the box...
Since you depend on that widget (are you sure you can't reimplement it to be cleaner ?) Then you should add the drag handling to it (putting e.g. a pointer to the widget in the QDrag mime data) and have the widget in the QScrollArea handle the drop.
Actually, I think I managed to do what I want by deriving QTableWidget and reimplementing the dropEvent() method.
Due to the way QAbstractListView manages the list of widgets attached to indexes, I can't just move the existing widget around. Doing the following:
widget = self.cellWidget(orig_row, orig_column) self.setCellWidget(dest_row, dest_column)
won't work because internally, QAbstractListView::setIndexWidget() will delete the "old" widget first. Except in this case, the "old" and "new" widget are one and the same, so I'll have to add a clone() method to my widget. I'll have to see how it works in practice, but I think this will work.
In case anyone is interested, here is an updated version of the project in the first post, with my solution: https://gist.github.com/anonymous/6bd244c8ca30fee10918
Thanks a lot for your help and suggestions, SGaist!
One thing you have to also take into account: how many of these widgets will you have at the same time ?
Inside the QTableWidget, I'm guessing I'll have ~100 of my custom widgets at most.
In other use cases (i.e. not in the QTableWidget, but in static layouts), I've had up to 4000 of these widgets, with a few hundred being active (i.e. actively refreshing their data from the backend) at the same time, without any serious performance issues, so I know it scales relatively well.
Also, to elaborate on the solution I offered above: cloning the custom widget proved difficult, because it has a lot of properties (I simplified the problem, the "custom widget" is actually a family of different widgets to fit different data), and because copying Qt objects is not trivial (see http://doc.qt.io/qt-5/object.html#identity-vs-value).
What I ended up doing was write a ContainerWidget to go along the GridWidget. The ContainerWidget has a layout with no margins and is used to display another widget through a setContainedWidget() method. I also added a cloneAndPassContainedWidget() method that does what you'd expect: it returns a new ContainerWidget and gives it ownership of the contained widget. Obviously, after calling the method the original ContainerWidget becomes useless, but the method is called precisely because the original ContainerWidget is set to be destroyed, so that's not a problem. I can now use my GridWidget to display any kind of widgets, it's pretty neat! (I wrote "widget" too many times!)
I realize the "proper" way would have been to write a model, views, and delegates, but that would have required a lot more work.
EDIT: Here's my updated solution: https://gist.github.com/anonymous/a3b2d7e61c6b3e11742c