[PySide] How to display a custom widget in an item view

I have a list of components that are currently displayed in a custom widget that has an enabled checkbox, some text and buttons, and is also expandable to allow editing the internal data of the component. Right now they are in a QVBoxLayout inside of a QScrollArea. However I want to add two things:

  1. Drag-and-drop reordering of the widgets
  2. A way to range-select the enabled checkboxes (click a checkbox then shift-click another checkbox and everything in-between gets toggled as well)

Is MVC the right approach here? I don’t see a lot of examples/documentation on displaying a model data as a single custom widget with buttons and dropdowns and stuff.
My question is whether I should switch to using a model and list view to display the data, or if I should manage my own drag-and-drop and checkbox range-select using the QVBoxLayout.

Thanks!

I admire your bravery in asking this question.

Most turn straight for MVC and either ask how the heck it works, or over-engineer something they later regret. I think this is a perfectly valid approach, but I wouldn’t be able to tell you whether it’s the “right” approach. That I think depends too much on exactly what you are looking to accomplish.

If, for example, you are interested in learning about the “why” of MVC, then you’re on the right track. You’ll discover things in the MVC boilerplate of spaghetti code actually does makes life easier (e.g. separated selection model), while at the same time start to question why it works the way it does now that you’ve peeked behind the curtains (e.g. why “roles” aren’t string).

If, for example, you are still exploring whether the final product and user experience is going to work, then this approach might shave a few hours/days off of the time it takes to get it into their hands and find out. Who knows whether next week you discover that you didn’t actually need this list of custom widgets at all? There’s a saying that writing code that is easy to rewrite is sometimes preferable to code which is easy to extend. This may be one of those cases.

If, on the other hand, you are already familiar with MVC and its quirks then your time-to-market may very well be the same, or faster. And each time you jump through those hoops, you’ll get better at it and iterate faster than before. Which raises the question whether it is worth taking the long road this time, even if you end up throwing it away, and invest in future.

For examples on custom views, there is a few examples in the /examples directory of the PyQt installation.

Hey capper, I went through Yasin’s tutorials a couple of years ago which initiated me to MVC with PySide/PyQt.

For more info on drag and drop, there is some info here.

I put this on the backburner after I made this post but it’s coming back into focus and I have a little more clarity about where my roadblock is.

I have a UI that displays a list of widgets as a queue. Each widget (or component) performs a specific task and takes different inputs. So while each widget has a common header (name, description, enabled checkbox), it can be expanded to reveal fields that allow the user to edit the input data it uses to run. Currently these components are displayed using a QVBoxWidget inside of a QScrollArea.

I want to:

  • Add the ability to drag-and-drop each widget/component to re-order it in the list
  • Be able to select multiple components/widgets at once to enable/disable them

When I think of that functionality I think of MVC, however MVC is not really built to display QWidgets. I can use a QListWidget and its setItemWidget method to add widgets to the list, however I see warnings both in the docs and on forums that this approach is not ideal and is heavy and may cause performance issues.

Continuing towards an MVC approach, it seems that if I wanted to use a QListView and custom model, I would also need a custom delegate and would need a completely custom paint method that drew every type of input widget I wanted to display. That also means any time we wanted to add a different input widget, it would need to be added to the delegate’s paint method. This sounds complicated.

I guess the other alternative is to keep the QVBox and scroll area and write my own drag-and-drop support and multi-selection behavior. I’m tempted to try that out because it sounds easier than creating a complex ItemDelegate.

Anyone explored something like this before? Inputs? Ideas?

Have you tried a simple version using the QListWidget? While the performance might not be the best, it could easily still be acceptable.
Its been a few years since I’ve seriously used Qt, but I remember seeing those same warnings, and not having to much of an issue with the final performance.

By the sounds of it, what you want is a TableView, with at least one custom delegate. I mention this because a ListView will only display a single ‘column’ from your data.

The custom delegate route may seem complicated, but it becomes clearer after going through the motions at least once. I see it as a long term investment; I’ve solved many UI issues using delegates.

Note that checkboxes can be handled directly in the model itself through the data/setData methods; it’s a PySide.QtCore.Qt.ItemDataRole in itself called QtCore.Qt.CheckStateRole.

I don’t think you would need to re-implement the paint method for a custom delegate. You’d want to re-implement paint if the existing widgets don’t cover your needs. Something like a star rating widget would require re-implementing paint (this is in fact the Qt example for re-implementing paint).

The process of adding a delegate to a data column is rather straight forward:

-define what existing widget will be used as an editor for the data you want to display/edit.
-implement the delegate with that editor; define the virtual methods createEditor, setEditorData, and setModelData.
-set what column in your view will use the delegate.

As an example, here’s a custom delegate that handles the python enum34 (enum34 · PyPI) which I used in certain columns of various tools:

class EnumComboDelegate(QtGui.QStyledItemDelegate):
    """
    Combobox delegate to represent an Enum.

    Assigning this delegate to a column will display enum as a combobox if the underlying model data is derived from
    the Enum class.
    """
    def __init__(self, parent=None):
        super(EnumComboDelegate, self).__init__(parent)

    @staticmethod
    def data(index):
        """
        Custom method to derive the model data from the model.
         
        Note: assumes there is an intermediate proxyModel.
        """
        # get the sourceModel, we know that the index.model() is a proxy model. 
        # If you don't know you can use the following:
        # proxy_model = index.model()
        # try:
        #     source_model = proxy_model.sourceModel()
        #     source_index = proxy_model.mapToSource(index)
        # except:
        #     source_model = proxy_model
        #     source_index = index
        # return source_model.getItemFromIndex(source_index).qt_data(index.column())
        
        proxy_model = index.model()  
        # obtain the source model from the proxy
        source_model = proxy_model.sourceModel() 
        # map the index back to the source model's index (true index)
        source_index = proxy_model.mapToSource(index) 
        # use custom method in source model to obtain data for column
        return source_model.getItemFromIndex(source_index).qt_data(index.column()) 

    def createEditor(self, parent, option, index):
        """
        Inits delegate; creates the widget to be used.
        """
        data = self.data(index)
        if isinstance(data, Enum):
            combo_box = QtGui.QComboBox(parent)
            names = type(data).__members__.keys()
            combo_box.addItems(names)
            # connect the signal so that when the index changes, the view can signal the model
            combo_box.currentIndexChanged.connect(self.currentIndexChanged)
            return combo_box
        else:
            return QtGui.QStyledItemDelegate.createEditor(self, parent, option, index)

    def setEditorData(self, editor, index):
        """
        Init editor with current data from the model when the user 'activates' the editor.
        """
        data = self.data(index)
        if isinstance(data, Enum):
            names = type(data).__members__.keys()
            editor.setCurrentIndex(names.index(data.name))
        else:
            return QtGui.QStyledItemDelegate.setEditorData(self, editor, index)

    def setModelData(self, editor, model, index):
        """
        This writes data to the model when the user has finished entering values.
        """
        data = self.data(index)
        if isinstance(data, Enum):
            enum_type = type(data)
            names = enum_type.__members__.keys()
            value = enum_type[names[editor.currentIndex()]]
            model.setData(index, value)
        else:
            QtGui.QStyledItemDelegate.setModelData(self, editor, model, index)

    def currentIndexChanged(self):
        """
        Sends signal to the sender (view) that the data can be committed. 
        """
        self.commitData.emit(self.sender())

In the snippet above, the data method is a custom method used to fetch column data from the model. While the getItemFromIndex is a custom method of the model, to fetch data based on its column.

To assign the delegate in your view, simply add this to your widget’s init after declaring your view/model/etc…:

self.myTableView.setItemDelegateForColumn(0, EnumComboDelegate(self))

Hopefully, this gets you started.

@R.White: I haven’t tried testing this using a QListWidget yet, no. I’ll probably give that a test run to see how it performs.

@Claudio A: Thanks, I’ll take a look at that.