PyQt5 get data from model to show custom widget in QListView

Viewed 1234

I'm using PyQt5 and trying to create a GUI with a list of some custom data structure (Recipe), and I defined a custom widget as a subclass of QWidget that describes how it should be displayed. I am trying to use MVC so I have a QListView and I subclassed QAbstractListModel. I want the list to contain my custom widget instead of just text, so I also defined an item delegate for it that returns that widget. Perhaps my understanding of exactly how these three components (model, item delegate, view) work together is flawed, because I'm not sure how to access whatever is returned from the data() method of the model and turn that into the particular widget I want to display in that list cell.

Example code:

class Recipe:
    pass # custom data structure

class RecipeWidget(QWidget, RecipeWidgetUI):
    def __init__(self, recipe, *args, **kwargs):
        super(RecipeWidget, self).__init__(*args, **kwargs)
        self.recipe = recipe
        self.setupUi(self)
        # some code here to populate the UI elements with data from self.recipe

class RecipeListModel(QAbstractListModel):
    def __init__(self, recipes, *args, **kwargs):
        super(RecipeListModel, self).__init__(*args, **kwargs)
        self.recipes = recipes
    def rowCount(self, index):
        return len(self.recipes)
    def data(self, index, role):
        if role == Qt.DisplayRole:
            return str(self.recipes[index.row()]) # this is the part I'm confused about

class RecipeItemDelegate(QItemDelegate):
    def __init__(self, parent):
        QItemDelegate.__init__(self, parent)

    def createEditor(self, parent, option, index):
        return RecipeWidget(recipe, parent=parent) # where do I get recipe from??

class MainWindow(...):
    def __init__(self, ...):
        ...
        self.model = RecipeListModel(recipes)
        self.listView.setModel(self.model)
        self.listView.setItemDelegate(RecipeItemDelegate(self.listView))

I know the data(...) function in the model is supposed to return a string; what I don't understand is:

  • How do I instead return my Recipe data structure? Do I have to serialize it somehow and then deserialize later?
  • Where does RecipeItemDelegate see what was returned by the data(...) function in order to properly construct the recipe object?
2 Answers

Each item can hold several data properties based on roles. These data properties are QVariants. You can embed a lot of different things in a QVariant, even custom objects.

I would say it is best practice to use a string for the Display role, and use Qt.UserRole (or Qt.UserRole + n) for other things. Your delegate can still access those by providing that role when querying the item's data.

I am not versed in how to use custom types for QVariant with PyQt5 though as I develop with C++. Note that if you were to use Qt for Python instead of PyQt5, you would not need to work with QVariant and could just set and get any custom python variable.

  1. model.data() needs to provide data for desirable role. in you case, you may decide to use Qt.DisplayRole to represent data for display. You can just return self.recipes[index.row()]. but if you want to support drag and drop, QT needs to be able to serialize the data. for now let's just provide python index in the self.recipes, which can be serialized. return index.row() # which happens to be python index in the self.recipes
  2. RecipeItemDelegate needs to be able to reference to the Recipe object with data from model.data(). So, let's init RecipeItemDelegate(parent, parent.model().recipes)
  3. RecipeItemDelegate.paint(self, painter, option, index) shall be implemented to paint the "display". Inside paint(), index.data(Qt.DisplayRole) gets the data - row in the recipes(happens to be index.row()). self.reference_to_recipes[index.data(Qt.DisplayRole)] gets the Recipe object. then you pick up "painter", paint information from data you get, in the "option" specified area and style. see also QItemDelegate::paint in QT documenation.

if you want the delegate to support editing,:

  1. createEditor() should create the widget used to change data from the model
  2. setEditorData() should populate the widget with data to manipulate. like editor.set_recipe(self.reference_to_recipes[index.data(Qt.DisplayRole)])
  3. updateEditorGeometry() ensures that the editor is displayed correctly with respect to the item view.
  4. setModelData() returns updated data to the model. like model.setData(index, editor.get_modified_recipe(), Qt.EditRole) of course, model.setData() needs to be able to handle Recipe object upon Qt.EditRole
Related