Checkboxes in Table Views with custom model

Show check boxes for boolean values
Heads up! You've already completed this tutorial.

T Carson wrote

I found some code with an example of an abstract table model. I got it running with Pyside2 and now I want to add a column of checkboxes. I see where the combo box is made in the AssetDelegate class. Should I make a similar class for a checkbox or somehow add to this class?

python
import sys
from PySide2 import QtGui, QtCore, QtWidgets


class AssetDelegate(QtWidgets.QStyledItemDelegate):
    def paint(self, painter, option, index):
        if isinstance(self.parent(), QtWidgets.QAbstractItemView):
            self.parent().openPersistentEditor(index)
        QtWidgets.QStyledItemDelegate.paint(self, painter, option, index)

    def createEditor(self, parent, option, index):
        combobox = QtWidgets.QComboBox(parent)
        combobox.addItems(index.data(AssetModel.ItemsRole))
        combobox.currentIndexChanged.connect(self.onCurrentIndexChanged)
        return combobox

    def onCurrentIndexChanged(self, ix):
        editor = self.sender()
        self.commitData.emit(editor)
        self.closeEditor.emit(editor, QtWidgets.QAbstractItemDelegate.NoHint)

    def setEditorData(self, editor, index):
        ix = index.data(AssetModel.ActiveRole)
        editor.setCurrentIndex(ix)

    def setModelData(self, editor, model, index):
        ix = editor.currentIndex()
        model.setData(index, ix, AssetModel.ActiveRole)


class Asset(object):
    def __init__(self, name, items=[], active=0):
        self.active = active
        self.name = name
        self.items = items

    @property
    def status(self):
        return self.active == len(self.items) - 1


class AssetModel(QtCore.QAbstractTableModel):
    attr = ["Name", "Options", "Extra"]
    ItemsRole = QtCore.Qt.UserRole + 1
    ActiveRole = QtCore.Qt.UserRole + 2

    def __init__(self, *args, **kwargs):
        QtCore.QAbstractTableModel.__init__(self, *args, **kwargs)
        self._items = []

    def flags(self, index):
        fl = QtCore.QAbstractTableModel.flags(self, index)
        if index.column() == 1:
            fl |= QtCore.Qt.ItemIsEditable
        return fl

    def clear(self):
        self.beginResetModel()
        self._items = []
        self.endResetModel()

    def rowCount(self, index=QtCore.QModelIndex()):
        return len(self._items)

    def columnCount(self, index=QtCore.QModelIndex()):
        return len(self.attr)

    def addItem(self, sbsFileObject):
        self.beginInsertRows(QtCore.QModelIndex(),
                             self.rowCount(), self.rowCount())
        self._items.append(sbsFileObject)
        self.endInsertRows()

    def headerData(self, section, orientation, role=QtCore.Qt.DisplayRole):
        if orientation == QtCore.Qt.Horizontal and role == QtCore.Qt.DisplayRole:
            return AssetModel.attr[section]
        return QtCore.QAbstractTableModel.headerData(self, section, orientation, role)

    def data(self, index, role=QtCore.Qt.DisplayRole):
        if not index.isValid():
            return None
        if 0 <= index.row() < self.rowCount():
            item = self._items[index.row()]
            col = index.column()
            if role == AssetModel.ItemsRole:
                return getattr(item, 'items')

            if role == AssetModel.ActiveRole:
                return getattr(item, 'active')

            if 0 <= col < self.columnCount():
                if role == QtCore.Qt.DisplayRole:
                    if col == 0:
                        return getattr(item, 'name', '')
                    if col == 1:
                        return getattr(item, 'items')[getattr(item, 'active')]
                elif role == QtCore.Qt.DecorationRole:
                    if col == 0:
                        status = getattr(item, 'status')
                        col = QtGui.QColor(QtCore.Qt.red) if status else QtGui.QColor(
                            QtCore.Qt.green)
                        px = QtGui.QPixmap(120, 120)
                        px.fill(QtCore.Qt.transparent)
                        painter = QtGui.QPainter(px)
                        painter.setRenderHint(QtGui.QPainter.Antialiasing)
                        px_size = px.rect().adjusted(12, 12, -12, -12)
                        painter.setBrush(col)
                        painter.setPen(QtGui.QPen(QtCore.Qt.black, 4,
                                                  QtCore.Qt.SolidLine,
                                                  QtCore.Qt.RoundCap,
                                                  QtCore.Qt.RoundJoin))
                        painter.drawEllipse(px_size)
                        painter.end()

                        return QtGui.QIcon(px)

    def setData(self, index, value, role=QtCore.Qt.EditRole):
        if 0 <= index.row() < self.rowCount():
            item = self._items[index.row()]
            if role == AssetModel.ActiveRole:
                setattr(item, 'active', value)
                return True
        return QtCore.QAbstractTableModel.setData(self, index, value, role)


class Example(QtWidgets.QWidget):

    def __init__(self):
        super().__init__()
        self.resize(400, 300)

        # controls
        asset_model = QtCore.QSortFilterProxyModel()
        asset_model.setSortCaseSensitivity(QtCore.Qt.CaseInsensitive)
        asset_model.setSourceModel(AssetModel())

        self.ui_assets = QtWidgets.QTableView()
        self.ui_assets.setEditTriggers(
            QtWidgets.QAbstractItemView.NoEditTriggers)
        self.ui_assets.setModel(asset_model)
        self.ui_assets.verticalHeader().hide()
        self.ui_assets.setItemDelegateForColumn(1, AssetDelegate(self.ui_assets))

        main_layout = QtWidgets.QVBoxLayout()
        main_layout.addWidget(self.ui_assets)
        self.setLayout(main_layout)

        self.unit_test()

    def unit_test(self):
        assets = [
            Asset('Dev1', ['v01', 'v02', 'v03'], 0),
            Asset('Dev2', ['v10', 'v11', 'v13'], 1),
            Asset('Dev3', ['v11', 'v22', 'v53'], 2),
            Asset('Dev4', ['v13', 'v21', 'v23'], 0)
        ]

        self.ui_assets.model().sourceModel().clear()
        for i, obj in enumerate(assets):
            self.ui_assets.model().sourceModel().addItem(obj)


def main():
    app = QtWidgets.QApplication(sys.argv)
    ex = Example()
    ex.show()
    sys.exit(app.exec_())


if __name__ == '__main__':
    main()


Martin Fitzpatrick

You can use a custom delegate to draw the widget if you like, but you don't have to. There is Qt.CheckStateRole which you can use to display a checkbox depending on whether you return Qt.Checked or Qt.Unchecked in your .data method.

python
    def data(self, index, role):
        if role == Qt.DisplayRole:
            value = self._data[index.row()][index.column()]
            return str(value)

        if role == Qt.CheckStateRole:
            return Qt.Checked

This shows up like this

checked|690x277

If you want to allow the user to toggle the checkboxes you need a few things --

Packaging Python Applications with PyInstaller by Martin Fitzpatrick — This step-by-step guide walks you through packaging your own Python applications from simple examples to complete installers and signed executables.

More info Get the book

  1. a data store for the check state
  2. to return Qt.ItemIsUserCheckable from the .flags() method
  3. to implement the .setData method to accept and store the updated data.

The following example implements that. The check state data is stored in a separate table, just for clarity --

python
import sys
from PyQt5 import QtCore, QtGui, QtWidgets
from PyQt5.QtCore import Qt


class TableModel(QtCore.QAbstractTableModel):

    def __init__(self, data, checked):
        super().__init__()
        self._data = data
        self._checked = checked

    def data(self, index, role):
        if role == Qt.DisplayRole:
            value = self._data[index.row()][index.column()]
            return str(value)

        if role == Qt.CheckStateRole:
            checked = self._checked[index.row()][index.column()]
            return Qt.Checked if checked else Qt.Unchecked

    def setData(self, index, value, role):
        if role == Qt.CheckStateRole:
            checked = value == Qt.Checked
            self._checked[index.row()][index.column()] = checked
            return True

    def rowCount(self, index):
        return len(self._data)

    def columnCount(self, index):
        return len(self._data[0])

    def flags(self, index):
        return Qt.ItemIsSelectable|Qt.ItemIsEnabled|Qt.ItemIsUserCheckable

class MainWindow(QtWidgets.QMainWindow):

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


        self.table = QtWidgets.QTableView()

        data = [
          [1, 9, 2],
          [1, 0, -1],
          [3, 5, 2],
          [3, 3, 2],
          [5, 8, 9],
        ]

        checked = [
          [True, True, True],
          [False, False, False],
          [True, False, False],
          [True, False, True],
          [False, True, True],
        ]

        self.model = TableModel(data, checked)
        self.table.setModel(self.model)

        self.setCentralWidget(self.table)



app=QtWidgets.QApplication(sys.argv)
window=MainWindow()
window.show()
app.exec_()

If you run this, you'll see you are able to check and toggle the checkboxes next to any value.

Create GUI Applications with Python & Qt6 by Martin Fitzpatrick — (PyQt6 Edition) The hands-on guide to making apps with Python — Over 10,000 copies sold!

More info Get the book

Well done, you've finished this tutorial! Mark As Complete
[[ user.completed.length ]] completed [[ user.streak+1 ]] day streak

Checkboxes in Table Views with custom model was written by Martin Fitzpatrick .

Martin Fitzpatrick has been developing Python/Qt apps for 8 years. Building desktop applications to make data-analysis tools more user-friendly, Python was the obvious choice. Starting with Tk, later moving to wxWidgets and finally adopting PyQt.