Qt modelview: models

From wikinotes

Models are where you store your data. The vast majority of the time you'll likely want to use a QStandardItemModel, but there are also other options available:

  • QFilesystemModel
  • QStringListModel
  • ... and more ...

Basics

MVC provides a useful abstraction between your GUI and your program's logic.

  • data is written to the model
  • a view renders the information in the model.

It makes it easier to test your logic independently of your UI, and test your UI independently of your logic. Also, it makes it trivial to create different views of your data.

Example Table Model with Icons/Descriptions

from Qt import QtCore, QtGui

# Build the Model
model = QtGui.QStandardItemModel()
item_A = QtGui.QStandardItem()
item_A.setData('aaaaaaaaaaaaaaaaa', QtCore.Qt.DisplayRole)     # text
item_A.setData('/path/image_A.png', QtCore.Qt.DecorationRole)  # icon/pixmap/color
item_A.setData('The A item ......', QtCore.Qt.ToolTipRole)     # tooltip of the item
item_B = QtGui.QStandardItem('b')

model.setItem(0, 0, item_A)
model.setItem(1, 0, item_B)

# View the Model
tree = QtWidgets.QTreeView()
tree.setModel(model)

Example Tree Model

model = QtGui.QStandardItemModel()

item_A_0 = QtGui.QStandardItem('a-0')
item_A_1 = QtGui.QStandardItem('a-1')
item_A_2 = QtGui.QStandardItem('a-2')

model.setItem(0, 0, item_A_0)
item_A_0.setChild(0, 0, item_A_1)
item_A_1.setChild(0, 0, item_A_2)

Custom Models

You'll very quickly find that rather than directly using primitive objects it is much more useful to create subclasses of models specific to your needs.

Note that:

  • rows/columns can be hidden in views
  • headers cannot be changed in views (without creating a new model)
  • items can be nested, but column-headers remain the same for each nested row

Custom Model Example

class ContactsModel(QtGui.QAbstractItemModel):
    columns = ('firstname', 'lastname', 'cell', 'email', 'imagepath')

    def __init__(self):
        super(ContactsModel, self).__init__()

    def clear(self):
        super(ContactsModel, self).clear()
        self._setup_headers()

    def _setup_headers(self):
        self.setColumnCount(len(self.columns))
        self.setHorizontalHeaderLabels(self.columns)

    def load_from_contactslist(self, contacts):
        # ...

Moving Rows

moveRow example

I have not yet successfully performed this yet..

takeRow/insertRow example

row_items = model.takeRow(0)
model.insertRow(0, row_items)

Signals

Call Chart

The cases where QStandardItemView methods are/are-not called may surprise you.


method called not called
model.dataChanged.emit()
  • model.setItem(x, y, item)
  • model.item(x, y).setData(z)
  • model.insertRow(col, item)
  • model.appendRow(col, item)
  • model.removeRow()
model.endInsertRow()
model.beginInsertRow()
???
  • model.setRowCount() increases row count
  • model.insertRow() is called
  • model.setItem() adds an item to a new row
  • model.appendRow() is called

model.beginRemoveRows()
model.beginRemoveRows()
model.endRemoveRow()
model.endRemoveRows()

???
  • model.removeRow()
  • model.removeRows()
model.itemChanged() ???
  • When item is created, then added then added to model using model.setItem()
  • Modifying an item that already exists in model (ex: model.item(x, y).setData(z))

Best Practices

Keep away from Model Signals

Instead of relying on Qt's model signals (which seem unpredictable), I usually implement my own.

These signals can be silenced without interfering with model or view implementations.

Template Model BaseClass

class Model(QtGui.QStandardItemModel):
    model_items_changed = QtCore.Signal()  # emitted any time an item in the model changes (insertRow, removeRows, dataChanged, ...)
    row_inserted = QtCore.Signal(row)      # emitted insertRow/insertRows (new row has items when called)
    row_removed = QtCore.Signal(row)       # emitted when row removed (not during clear)

    def __init__(self):
        super(Model, self).__init__()

    def insertRow(self, row, item):
        parent = self.index(-1, -1)
        first = row
        last = row

        # insert row
        self.beginInsertRows(parent, first, last)
        super(Model, self).insertRow(row, item)
        self.endInsertRows()

        # emit signals
        self.row_inserted.emit(row)
        self.model_items_changed.emit()

    #
    # NOTE: QStandardItemModel does not override insertRows()
    #

    def removeRow(self, row, *args):
        # get args
        parent = self.index(-1, -1)
        if len(args) > 0:
            parent = args[0]

        first = row
        last = row

        # remove row
        self.beginRemoveRows(parent, first, last)
        super(Model, self).removeRow(row, *args)
        self.endRemoveRows()

        # emit signals
        self.row_removed.emit(row)
        self.model_items_changed.emit()

    def removeRows(self, row, count, *args)
        # get args
        parent = self.index(-1, -1)
        if len(args) > 0:
            parent = args[0]

        first = row
        last = row + count

        # remove row
        self.beginRemoveRows(parent, first, last)
        super(Model, self).removeRow(row, count, *args)
        self.endRemoveRows()

        # emit signals
        for row in (row, row + count):
            self.row_removed.emit(row)
        self.model_items_changed.emit()

    def _handle_dataChanged(self):
        self.model_items_changed.emit()

Use QModelIndexes instead of QStandardItems

Use model.setData(modelindex, 'data', QtCore.Qt.DisplayRole) instead of using the QStandardItem directly. I have had issues with QStandardItems getting garbage collected.

See https://stackoverflow.com/questions/59012535/pyside2-model-contents-disappearing