25. Model-View Programming - Delegates

In Qt’s model-view architecture, a delegate sits between the view and the model and is responsible for:

  • Rendering each item in the view,
  • Providing an editor widget when the user wants to edit that item.

By default, Qt uses QStyledItemDelegate, which handles common data types automatically. You can subclass to take full control over painting and editing behavior. Both the default QStyledItemDelegate and its custom subclasses rely on QItemEditorFactory to create editor widgets. The factory maps data types to the appropriate editor. You can register editors globally or on per-delegate bases, or you can bypass the factory entirely by overriding createEditor() in the delegate subclass.

You have several options for changing how model data is displayed and edited. The first three options work directly with the default QStyledItemDelegate:

  • Use item data roles with the default QStyledItemDelegate to adjust an item’s appearance. For example, change its background color with BackgroundRole, or add an image with DecorationRole.

  • Register a standard widget for a data type with QItemEditorFactory. For example, replace the default spin box for integers with a line edit.

  • Register a custom widget for a data type with QItemEditorFactory. For example, use a custom widget made of a pair of radio buttons to edit boolean values.

  • Subclass QStyledItemDelegate to control display.

  • Subclass QStyledItemDelegate to control editing.

  • Subclass QStyledItemDelegate to handle both display and editing.

Approach Subclassing Required Custom Display Custom Editing When to Use
Item Data Roles No Limited No Simple appearance tweaks (color, icon, alignment, font)
Register Standard Widget No No Yes Replace default editor for built-in types
Register Custom Widget No No Yes Need a small custom widget (e.g., Yes/No radio buttons)
Subclass for Display only Yes Full No Complex painting (LEDs, progress bars, custom shapes)
Subclass for Editing only Yes No Full Complex editor logic or validation
Subclass for Both Yes Full Full Complete control over look and behavior

25.1 Using Item Data Roles to Customize Display

Each item in a model can return different data depending on which role the view is asking about. The table below lists the available roles and the data types they accept:

Role Accepted Types
BackgroundRole QBrush
CheckStateRole CheckState
DecorationRole QIcon, QPixmap, QImage, QColor
DisplayRole QString and types with a string representation
FontRole QFont
SizeHintRole QSize
TextAlignmentRole Alignment
ForegroundRole QBrush

By returning the appropriate value for each role in your model’s data() method, you can control how items look.

An icon of a clipboard-list1

You are developing an application that displays a table of economic indicators. Use item data roles in your model to customize the display: apply green background to positive values and red to negative, right-align and italicize negative entries, render the Aggregate column boolean values as YES/NO text, and show the ‘Include in report’ column as a checkbox.

To use item data roles to customize how the model data is displayed:

  1 from PySide6.QtCore import QAbstractTableModel, QModelIndex, Qt
  2 from PySide6.QtGui import QFont, QBrush, QColor
  3 
  4 
  5 # 1. Create the model class.
  6 
  7 class CsvModel(QAbstractTableModel):
  8     
  9     def __init__(self, parent=None):
 10 
 11         super().__init__(parent)
 12         self.header = ['Indicator', 'Change (%)',
 13                        'Aggregate', 'Include in report']
 14         self.csv_data = [
 15             ['GDP', 3, True, True],
 16             ['CPI', 6, True, True],
 17             ['Jobs', 5, False, True],
 18             ['Confidence', 75, False, True],
 19             ['Industry', 92, False, True],
 20             ['Retail', 4, True, True],
 21         ]
 22         
 23         self.plus_brush = QBrush(QColor("#d9fdd3"))
 24         self.minus_brush = QBrush(QColor("#fce4e4"))
 25 
 26     def rowCount(self, parent=QModelIndex()):
 27         if parent.isValid():
 28             return 0
 29         return len(self.csv_data)
 30     
 31     def columnCount(self, parent=QModelIndex()):
 32         if parent.isValid():
 33             return 0
 34         return len(self.header)
 35     
 36     # 2. Customize the data() method.
 37     
 38     def data(self, index, role):
 39 
 40         if not index.isValid():
 41             return None
 42         
 43         row, col = index.row(), index.column()
 44         value = self.csv_data[row][col]
 45 
 46         if role == Qt.ItemDataRole.DisplayRole:
 47             if col in (2, 3):
 48                 return 'YES' if value else 'NO'
 49             return value
 50 
 51         if role == Qt.ItemDataRole.EditRole:
 52             return value
 53 
 54         if role == Qt.ItemDataRole.CheckStateRole:
 55             if col == 3:
 56                 if value:
 57                     return Qt.CheckState.Checked
 58                 else:
 59                     return Qt.CheckState.Unchecked
 60             return None
 61 
 62         if role == Qt.ItemDataRole.BackgroundRole:
 63             if col == 1:
 64                 if value >= 0:
 65                     return self.plus_brush
 66                 else:
 67                     return self.minus_brush
 68             if col in (2, 3):
 69                 if value:
 70                     return self.plus_brush
 71                 else:
 72                     return self.minus_brush
 73 
 74         if role == Qt.ItemDataRole.FontRole:
 75             font = QFont()
 76             if ((col == 1 and value <= 0)
 77                 or (col in (2, 3) and not value)):
 78                 font.setItalic(True)
 79             return font
 80 
 81         if role == Qt.ItemDataRole.TextAlignmentRole:
 82             if col in (1, 2, 3):
 83                 return Qt.AlignmentFlag.AlignCenter
 84             
 85         return None
 86     
 87     # 3. Customize setData().
 88     
 89     def setData(self, index, value, role):
 90 
 91         if not index.isValid():
 92             return False
 93 
 94         row, col = index.row(), index.column()
 95 
 96         if role == Qt.ItemDataRole.EditRole:
 97             if self.csv_data[row][col] != value:
 98                 self.csv_data[row][col] = value
 99                 self.dataChanged.emit(index, index)
100                 return True
101             return False
102 
103         if role == Qt.ItemDataRole.CheckStateRole:
104             if col == 3:
105                 checked = bool(value)
106                 if self.csv_data[row][col] != checked:
107                     self.csv_data[row][col] = checked
108                     self.dataChanged.emit(index, index)
109                     return True
110                 return False
111     
112     # 4. Customize flags().
113     
114     def flags(self, index) -> Qt.ItemFlags:
115         if not index.isValid():
116             return Qt.ItemFlags()
117         flags = super().flags(index)
118         if index.column() in (1, 2):
119             flags |= Qt.ItemFlags.ItemIsEditable
120         if index.column() == 3:      # checkbox lives here
121             flags |= Qt.ItemFlags.ItemIsUserCheckable
122         return flags
123 
124     def headerData(self, section, orientation, role):
125         if (orientation == Qt.Orientation.Horizontal
126             and role == Qt.ItemDataRole.DisplayRole):
127             return self.header[section]
128         return super().headerData(section, orientation, role)
  1. Create the model. Subclass QAbstractTableModel and name it CsvModel. Initialize the table data directly in __init__() for simplicity. Also declare the column headers and two QBrush fields (one green and one red) to be used for background colors.

  2. Customize data(). Handle each role as follows:

    • DisplayRole - for the boolean columns (2 and 3), return ‘YES’ for True and ‘NO’ for `False’ values.
    • CheckStateRole - for column 3 only, return Checked for True and Unchecked for False values.
    • BackgroundRole - return the green brush if the value is True or greater than zero; return the red brush otherwise.
    • FontRole - return an italic font for False values and for numeric values less than zero.
    • TextAlignmentRole - return AlignCenter for columns 1, 2, and 3.
  3. Customize setData(). The model needs to handle two kinds of edits:

    • EditRole - for regular editable columns (1 and 2), store the new value directly and emit dataChanged to notify the view.
    • CheckStateRole - for column 3, the view passes a CheckState enum value. convert it to a plain bool before storing, then emit dataChanged.
  4. Customize flags(). For column 3 to display a checkbox, its items must be user-checkable. Return the standard flags plus ItemIsUserCheckable for that column.

 1 import sys
 2 from PySide6.QtWidgets import (QApplication,
 3     QWidget, QTableView, QVBoxLayout)
 4 from PySide6.QtTest import QAbstractItemModelTester
 5 from models import CsvModel
 6 
 7 
 8 class Window(QWidget):
 9     
10     def __init__(self):
11 
12         super().__init__()
13 
14         layout = QVBoxLayout()
15         self.setLayout(layout)
16 
17         model = CsvModel()
18         QAbstractItemModelTester(model,
19             QAbstractItemModelTester.FailureReportingMode.Warning)
20         view = QTableView()
21         view.setModel(model)
22         view.resizeColumnsToContents()
23         layout.addWidget(view)
24         
25         model.dataChanged.connect(self.on_data_changed)
26         
27     def on_data_changed(self, topLeft, bottomRight, roles):
28         for row in topLeft.model().csv_data:
29             print(row)
30 
31 
32 if __name__ == '__main__':
33 
34     app = QApplication(sys.argv)
35     main_window = Window()
36     main_window.show()
37     sys.exit(app.exec())

Now, when you run the application you should see a window like this:

25.2 Registering Standard Widgets with QItemEditorFactory

When a user clicks to edit an item, the delegate is responsible for showing an editor widget. The delegate does not create those editors itself - it uses a QItemEditorFactory to create editors for it.

A factory maps data types to editor widgets. For each type, it holds an item editor creator, a small helper that produces the right editor widget for that type.

Qt provides a default factory that all delegates share. The table below shows the default editor widget for each supported type:

Type Editor Widget
bool QComboBox
double QDoubleSpinBox
int QSpinBox
unsigned int QSpinBox
QDate QDateEdit
QDateTime QDateTimeEdit
QPixmap QLabel
QString QLineEdit
QTime QTimeEdit
An icon of a clipboard-list1

You are building a banking application that displays a table of economic indicators. The Change (%) contains integer percentages that users need to edit. Use QItemEditorFactory to register a validated QLineEdit as the editor for integer values in that column.

To replace the default editor widget for a data type with another standard widget:

 1 from PySide6.QtCore import Qt
 2 from PySide6.QtGui import QIntValidator
 3 from PySide6.QtWidgets import QItemEditorCreatorBase, QLineEdit
 4 
 5 
 6 # 1. Create an item editor creator.
 7 
 8 class NumericLineEditCreator(QItemEditorCreatorBase):
 9     
10     def __init__(self, min_val=-100, max_val=100):
11         super().__init__()
12         self.min_val = min_val
13         self.max_val = max_val
14     
15     def createWidget(self, parent):
16         editor = QLineEdit(parent)
17         editor.setAutoFillBackground(True)
18         validator = QIntValidator(self.min_val, self.max_val, editor)
19         editor.setValidator(validator)
20         editor.setAlignment(Qt.AlignmentFlag.AlignCenter)
21         
22         # If we don't implement valuePropertyName()
23         # the widget's user property is used
24         # (in this case 'text')
25         print(editor.metaObject().userProperty().name())
26         
27         return editor
  1. Create an editor creator. Subclass QItemEditorCreatorBase and name it NumericLineEditCreator. Reimplement its createWidget()method to:
    • Create a QLineEdit object.
    • Call setAutoFillBackground(True) so the editor fully covers the table cell.
    • Create a QIntValideator and set it as the line edit’s validator. Note that the editor is constructed with the parent argument and that the creator does not keep a reference to the editor - deleting the editor is parents responsibility.

In the main window:

 1 import sys
 2 from PySide6.QtCore import QMetaType
 3 from PySide6.QtWidgets import (QApplication, QWidget,
 4     QTableView, QVBoxLayout, QItemEditorFactory,
 5     QStyledItemDelegate)
 6 from PySide6.QtTest import QAbstractItemModelTester
 7 from models import CsvModel
 8 from editorcreators import NumericLineEditCreator
 9 
10 
11 class Window(QWidget):
12     
13     def __init__(self):
14 
15         super().__init__()
16 
17         layout = QVBoxLayout()
18         self.setLayout(layout)
19 
20         model = CsvModel()
21         QAbstractItemModelTester(model,
22             QAbstractItemModelTester.FailureReportingMode.Warning)
23         
24         # 2. Create a QItemEditorFactory object
25         #    and register the custom editor with it.
26         
27         factory = QItemEditorFactory()
28         factory.registerEditor(
29             QMetaType.Type.Int, NumericLineEditCreator())
30         
31         # 3. Create a styled item delegate
32         #    and and set factory as its editor factory.
33         
34         delegate = QStyledItemDelegate()
35         delegate.setItemEditorFactory(factory)
36         
37         view = QTableView()
38         view.setModel(model)
39         
40         # 4. Set delegate as the item delegate
41         #    for the appropriate column.
42         
43         view.setItemDelegateForColumn(1, delegate)
44         
45         view.resizeColumnsToContents()
46         layout.addWidget(view)
47         model.dataChanged.connect(self.on_data_changed)
48         
49     def on_data_changed(self, topLeft, bottomRight, roles):
50         for row in topLeft.model().csv_data:
51             print(row)
52 
53 if __name__ == '__main__':
54 
55     app = QApplication(sys.argv)
56     main_window = Window()
57     main_window.show()
58     sys.exit(app.exec())
  1. Create a factory and register the custom creator. Instantiate a QItemEditorFactory and use registerEditor() to associate a NumericLineEditCreator with the int type. All other types keep their default editors.

  2. Create a delegate and assign factory to it. Instantiate a QStyledItemDelegate and use setItemEditorFactory() to give it the factory you just created.

  3. Assign the delegate to the target column. Call view.setItemDelegateForColumn(1, delegate) to apply the custom editor only to the ‘Change’ column.

    This is the most targeted option when using a custom view editor:

    • setItemDelegateForColumn() affects one column of one view.
    • setItemDelegate() affects the entire view
    • QItemEditorFactory.setsDefaultFactory() affects all delegates in the entire application.

The model class is the same CsvModel from the previous section, without any custom formatting:

 1 from PySide6.QtCore import QAbstractTableModel, QModelIndex, Qt
 2 
 3 
 4 class CsvModel(QAbstractTableModel):
 5     
 6     def __init__(self, parent=None):
 7         
 8         super().__init__(parent)
 9         
10         self.header = ['Indicator', 'Change (%)', 
11             'Aggregate', 'Include in report']
12         self.csv_data = [
13             ['GDP', 3, True, True],
14             ['CPI', 6, True, True],
15             ['Jobs', 5, False, True],
16             ['Confidence', 75, False, True],
17             ['Industry', 92, False, True],
18             ['Retail', 4, True, True],
19         ]
20 
21     def rowCount(self, parent=QModelIndex()):
22         if parent.isValid():
23             return 0
24         return len(self.csv_data)
25     
26     def columnCount(self, parent=QModelIndex()):
27         if parent.isValid():
28             return 0
29         return len(self.header)
30     
31     def data(self, index, role):
32         value = self.csv_data[index.row()][index.column()]
33         if role == Qt.ItemDataRole.DisplayRole:
34             return value
35         if role == Qt.ItemDataRole.EditRole:
36             return value
37     
38     def setData(self, index, value, role):
39         if role == Qt.ItemDataRole.EditRole:
40             row, col = index.row(), index.column()
41             if col == 1:
42                 try:
43                     value = int(value)
44                 except (ValueError, TypeError):
45                     return False
46             if self.csv_data[row][col] != value:
47                 self.csv_data[row][col] = value
48                 self.dataChanged.emit(index, index)
49                 return True
50             return False
51         return False
52     
53     def flags(self, index) -> Qt.ItemFlag:
54         if not index.isValid():
55             return Qt.ItemFlags()
56         return (super().flags(index) |
57                     Qt.ItemFlag.ItemIsEditable)
58 
59     def headerData(self, section, orientation, role):
60         if orientation == Qt.Orientation.Horizontal:
61             if role == Qt.ItemDataRole.DisplayRole:
62                 return self.header[section]

25.3 Registering Custom Widgets with QItemEditorFactory

You are not limited to Qt’s built-in widgets when providing editors - you can use your own custom widgets just as easily.

An icon of a clipboard-list1

You are extending the banking application’s indicator table. The ‘Aggregate’ and Include in report’ columns hold boolean values that users need to edit. Use QItemEditorFactory to register a custom ‘Yes/No’ radio button widget for boolean values in those columns.

To register a custom widget as an editor:

 1 from PySide6.QtCore import Property, Slot
 2 from PySide6.QtWidgets import (QWidget, QRadioButton,
 3     QHBoxLayout)
 4 
 5 
 6 # 1. Create the custom editor widget.
 7 
 8 class SwitchWidget(QWidget):
 9     
10     def __init__(self, parent=None):
11         
12         super().__init__(parent)
13         
14         self.setAutoFillBackground(True)
15         self._value = False
16         
17         self.true_radio = QRadioButton('Yes')
18         self.false_radio = QRadioButton('No')
19         self.true_radio.toggled.connect(self.on_toggled)
20         
21         layout = QHBoxLayout()
22         layout.addWidget(self.true_radio)
23         layout.addWidget(self.false_radio)
24         layout.setContentsMargins(0, 0, 0, 0)
25         self.setLayout(layout)
26         self.setMinimumHeight(self.sizeHint().height())
27     
28     @Slot(bool)
29     def on_toggled(self, checked):
30         self.setValue(checked)
31         
32     def getValue(self):
33         return self._value
34     
35     def setValue(self, value):
36         if value != self._value:
37             self._value = value
38         if value:
39             self.true_radio.setChecked(True)
40         else:
41             self.false_radio.setChecked(True)
42         
43     value = Property(bool, getValue, setValue, user=True)
  1. Create the custom editor widget. Subclass QWidget and lay out two radio buttons horizontally, one for True (‘Yes’) and one for False (‘No’). Add a value property, with a getter and setter, and mark it as the user property with user=True. This is the property that the delegate uses as the bridge to the model: it populates the editor by writing to value when the editor opens, and reads value back to store the result when the editor closes. If you want to bind a different property to the model, reimplement QItemEditorCreatorBase.valuePropertyName() in your creator class.
1 from PySide6.QtWidgets import QItemEditorCreatorBase
2 from switchwidget import SwitchWidget
3 
4 # 2. Create an editor creator.
5 
6 class SwitchCreator(QItemEditorCreatorBase):
7     
8     def createWidget(self, parent):
9         return SwitchWidget(parent)
  1. Create an editor creator. Subclass QItemEditorCreatorBase and reimplement createWidget(). Since all the widget logic lives in the widget class itself, the creator is minimal - it just returns a new instance of your widget.
 1 import sys
 2 from PySide6.QtCore import QMetaType
 3 from PySide6.QtWidgets import (QApplication, QWidget,
 4     QTableView, QVBoxLayout, QItemEditorFactory,
 5     QStyledItemDelegate)
 6 from PySide6.QtTest import QAbstractItemModelTester
 7 from models import CsvModel
 8 from switchcreator import SwitchCreator
 9 
10 
11 class Window(QWidget):
12     
13     def __init__(self):
14 
15         super().__init__()
16 
17         layout = QVBoxLayout()
18         self.setLayout(layout)
19 
20         model = CsvModel()
21         QAbstractItemModelTester(model,
22             QAbstractItemModelTester.FailureReportingMode.Warning)
23         
24         # 3. Create a factory and register the editor.
25         #    Create a delegate, and assign the factory to it.
26         #    Assign the delegate to the target columns.
27         
28         factory = QItemEditorFactory()
29         factory.registerEditor(
30             QMetaType.Type.Bool, SwitchCreator())
31         
32         delegate = QStyledItemDelegate()
33         delegate.setItemEditorFactory(factory)
34         
35         view = QTableView()
36         view.setModel(model)
37         
38         view.setItemDelegateForColumn(2, delegate)
39         view.setItemDelegateForColumn(3, delegate)
40         
41         view.resizeColumnsToContents()
42         layout.addWidget(view)
43         model.dataChanged.connect(self.on_data_changed)
44         
45     def on_data_changed(self, topLeft, bottomRight, roles):
46         for row in topLeft.model().csv_data:
47             print(row)
48 
49 if __name__ == '__main__':
50 
51     app = QApplication(sys.argv)
52     main_window = Window()
53     main_window.show()
54     sys.exit(app.exec())
  1. Put everything together,following the same steps as in the previous section:
    • Create a QItemEditorFactory and call registerEditor() to associate your creator with the bool type.
    • Instantiate a QStyledItemDelegate and assign the factory to it with setItemEditorFactory().
    • Call view.setItemDelegateForColumn() for each target column.

The model class remains the same as in the previous section.

25.4 Custom Display in QStyledItemDelegate

The QStyledItemDelegate.paint() method is called by the view whenever an item needs to be drawn. Its signature is:

1 `QStyledItemDelegate.paint(painter, option, index)`

where:

  • painter is a QPainter, that performs the actual drawing.
  • option is a QStyleOptionViewItem, a snapshot of the drawing parameters for this item, including the available drawing area, palette and font.
  • index is a `QModelIndex, which is used to locate the item’s data in the model.

You can subclass QStyledItemDelegate and reimplement paint() to take full control over how an item looks in the view.

An icon of a clipboard-list1

You are extending the banking application’s indicator table. Subclass QStyledItemDelegate and reimplement paint() to render boolean values in the ‘Aggregate’ and ‘Include in report’ columns as LED indicators instead of text.

To customize how items are displayed:

 1 from PySide6.QtCore import Qt, QRect
 2 from PySide6.QtGui import QPainter, QBrush, QRadialGradient, QColor
 3 from PySide6.QtWidgets import QStyledItemDelegate
 4 
 5 
 6 # 1. Subclass QStyledItemDelegate
 7 
 8 class LedDelegate(QStyledItemDelegate):
 9 
10     def paint(self, painter, option, index):
11 
12         value = index.data(Qt.ItemDataRole.EditRole)
13         self.draw_led(painter, option.rect, value)
14         super().paint(painter, option, index)
15 
16     # 2. Draw the delegate
17 
18     def draw_led(self, painter, rect, value):
19 
20         diameter = min(rect.width(), rect.height()) - 8
21         diameter = min(diameter, 24)
22         x = rect.x() + (rect.width() - diameter) // 2
23         y = rect.y() + (rect.height() - diameter) // 2
24         led_rect = QRect(x, y, diameter, diameter)
25 
26         base_color = QColor('#22cc22') if value else QColor('#cc2222')
27         dark_color = QColor('#116611') if value else QColor('#661111')
28 
29         gradient = QRadialGradient(
30             led_rect.x() + diameter * 0.35,
31             led_rect.y() + diameter * 0.30,
32             diameter * 0.65
33         )
34         gradient.setColorAt(0.0, QColor(255, 255, 255, 220))
35         gradient.setColorAt(0.25, base_color.lighter(140))
36         gradient.setColorAt(0.7, base_color)
37         gradient.setColorAt(1.0, dark_color)
38 
39         painter.save()
40         painter.setRenderHint(QPainter.RenderHint.Antialiasing)
41 
42         painter.setBrush(QBrush(QColor('#333333')))
43         painter.setPen(Qt.PenStyle.NoPen)
44         painter.drawEllipse(led_rect)
45 
46         dome_rect = led_rect.adjusted(2, 2, -2, -2)
47         painter.setBrush(QBrush(gradient))
48         painter.drawEllipse(dome_rect)
49 
50         painter.restore()
  1. Subclass QStyledItemDelegate. Create a subclass named LedDelegate and reimplement paint(). In paint():

    • Retrieve the item’s value from the model via index.data().
    • Call draw_led() to paint the LED in the item’s cell.
    • Call super.paint() to let the base class draw selection highlighting and fill the cell background when its editor is opened.
  2. Paint the LED. Extract the drawing logic into a helper method named draw_led(). In this method:

    • Use option.rect to calculate the drawing area. Cap the diameter and add padding.
    • Define two colors, green for True values and red for False.
    • Create a QGradialRadient to give the LED somewhat realistic appearance.
    • Call drawEllipse() twice, once for the dark outer ring, and once for the gradient-filled dome on top.
 1 import sys
 2 from PySide6.QtWidgets import QApplication, QTableView, QWidget, QVBoxLayout
 3 from PySide6.QtTest import QAbstractItemModelTester
 4 from models import CsvModel
 5 from delegates import LedDelegate
 6 
 7 
 8 class Window(QWidget):
 9     
10     def __init__(self):
11 
12         super().__init__()
13 
14         layout = QVBoxLayout()
15         self.setLayout(layout)
16 
17         model = CsvModel()
18         QAbstractItemModelTester(model,
19             QAbstractItemModelTester.FailureReportingMode.Warning)
20         
21         view = QTableView()
22         view.setModel(model)
23         
24         # 3. Create the custom delegate object.
25         
26         delegate = LedDelegate()
27         
28         # 4. Set the delegate for the appropriate columns.        
29         
30         view.setItemDelegateForColumn(2, delegate)
31         view.setItemDelegateForColumn(3, delegate)
32         
33         view.resizeColumnsToContents()
34         layout.addWidget(view)
35         
36         model.dataChanged.connect(self.on_data_changed)
37         
38     def on_data_changed(self, topLeft, bottomRight, roles):
39         for row in topLeft.model().csv_data:
40             print(row)
41 
42 
43 if __name__ == '__main__':
44 
45     app = QApplication(sys.argv)
46     main_window = Window()
47     main_window.show()
48     sys.exit(app.exec())
  1. Create the delegate. In the main window instantiate LedDelegate().

  2. Assign the delegate to the target columns. Call setItemDelegateForColumn() for columns 2 and 3, which hold the boolean values.

When you run the application, both boolean columns display green LEDs for True values and red LEDs for False. Double-clicking a cell still opens the default combobox editor for booleans- custom painting does not affect editing behavior.

25.5 Custom Editors in QStyledItemDelegate

In the previous section we customized how items are displayed by reimplementing paint(). To also provide a custom editor, you need to reimplement three additional methods:

  • createEditor(parent, option, index), which creates and returns the editor widget for the item.
  • setEditorData(editor, index), which populates the editor with the item’s current value from the model.
  • setModelData(editor, model, index), which reads the value from the editor and stores it in the model.
An icon of a clipboard-list1

You are extending the banking application’s indicator table. Subclass QStyledItemDelegate to provide a custom LED button widget for editing boolean values.

To provide a custom editor:

 1 from PySide6.QtCore import QRect, Qt
 2 from PySide6.QtGui import QColor, QRadialGradient, QPainter, QBrush 
 3 
 4 # 1. Extract the drawing logic into a standalone function.
 5 
 6 def draw_led(painter, rect, value):
 7 
 8     diameter = min(rect.width(), rect.height()) - 8
 9     diameter = min(diameter, 24)
10     x = rect.x() + (rect.width() - diameter) // 2
11     y = rect.y() + (rect.height() - diameter) // 2
12     led_rect = QRect(x, y, diameter, diameter)
13 
14     base_color = QColor('#22cc22') if value else QColor('#cc2222')
15     dark_color = QColor('#116611') if value else QColor('#661111')
16 
17     gradient = QRadialGradient(
18         led_rect.x() + diameter * 0.35,
19         led_rect.y() + diameter * 0.30,
20         diameter * 0.65
21     )
22     gradient.setColorAt(0.0, QColor(255, 255, 255, 220))
23     gradient.setColorAt(0.25, base_color.lighter(140))
24     gradient.setColorAt(0.7, base_color)
25     gradient.setColorAt(1.0, dark_color)
26 
27     painter.save()
28     painter.setRenderHint(QPainter.RenderHint.Antialiasing)
29 
30     painter.setBrush(QBrush(QColor('#333333')))
31     painter.setPen(Qt.PenStyle.NoPen)
32     painter.drawEllipse(led_rect)
33 
34     dome_rect = led_rect.adjusted(2, 2, -2, -2)
35     painter.setBrush(QBrush(gradient))
36     painter.drawEllipse(dome_rect)
37     painter.restore()
  1. Extract the LED drawing logic into a standalone function. Both the delegate’s paint() method and the editor widget’s button need to draw LEDs.
 1 from PySide6.QtCore import Qt, Property, Slot
 2 from PySide6.QtGui import QPainter, QKeyEvent
 3 from PySide6.QtWidgets import QWidget, QPushButton, QHBoxLayout
 4 from ledpainter import draw_led
 5 
 6 
 7 class LedButton(QPushButton):
 8     
 9     def __init__(self, led_value, parent=None):
10         super().__init__(parent)
11         self.led_value = led_value
12         self.setFocusPolicy(Qt.FocusPolicy.StrongFocus)
13     
14     def paintEvent(self, event):
15         super().paintEvent(event)
16         painter = QPainter(self)
17         painter.setRenderHint(QPainter.RenderHint.Antialiasing)
18         rect = self.rect()
19         draw_led(painter, rect, self.led_value)
20 
21 # 2. Create the custom editor widget.
22 
23 class LedWidget(QWidget):
24     
25     def __init__(self, parent=None):
26         
27         super().__init__(parent)
28         
29         self.setAutoFillBackground(True)
30         self._value = False
31         
32         self.true_button = LedButton(True)
33         self.true_button.setCheckable(True)
34         self.true_button.setAutoExclusive(True)
35 
36         self.false_button = LedButton(False)
37         self.false_button.setCheckable(True)
38         self.false_button.setAutoExclusive(True)
39         self.true_button.toggled.connect(self.on_toggled)
40         
41         layout = QHBoxLayout()
42         layout.addWidget(self.true_button)
43         layout.addWidget(self.false_button)
44         layout.setContentsMargins(0, 0, 0, 0)
45         self.setLayout(layout)
46         self.setMinimumHeight(self.sizeHint().height())
47     
48     @Slot(bool)
49     def on_toggled(self, checked):
50         self.setValue(checked)
51         
52     def getValue(self):
53         return self._value
54     
55     def setValue(self, value):
56         if value != self._value:
57             self._value = value
58         if value:
59             self.true_button.setChecked(True)
60         else:
61             self.false_button.setChecked(True)
62         
63     value = Property(bool, getValue, setValue, user=True)
64     
65     def keyPressEvent(self, event: QKeyEvent):
66         if event.key() in (Qt.Key.Key_Left, Qt.Key.Key_Right):
67             self.setValue(not self._value)
68             event.accept()
69             return
70         super().keyPressEvent(event)
  1. Create the custom editor widget. The editor widget consists of two buttons laid out horizontally, each displaying a LED - green for True on the left, red for False on the right. First create a QPushButton subclass that reimplements paintEvent() to draw a LED using draw_led() instead of showing text. Also create a QWidget subclass that holds two LedButtons in a horizontal layout. Add a value property with a getter and setter, and reimplement keyPressEvent() so the user can toggle the value by pressing the left or right arrow keys.
 1 from PySide6.QtCore import Qt
 2 from PySide6.QtWidgets import QStyledItemDelegate
 3 from widgets import LedWidget
 4 from ledpainter import draw_led
 5 
 6 # 3. Create the QStyledItemDelegate subclass
 7 #    and reimplement createEditor(), setEditorData()
 8 #    and setModelData()
 9 
10 class LedDelegate(QStyledItemDelegate):
11 
12     def createEditor(self, parent, option, index):
13         editor = LedWidget(parent)
14         return editor
15 
16     def setEditorData(self, editor, index):
17         editor.setValue(index.data(Qt.ItemDataRole.EditRole))
18     
19     def setModelData(self, editor, model, index):
20         model.setData(index, editor.getValue(),
21             Qt.ItemDataRole.EditRole)
22 
23     def paint(self, painter, option, index):
24         value = index.data(Qt.ItemDataRole.EditRole)
25         draw_led(painter, option.rect, value)
26         super().paint(painter, option, index)
  1. Subclass QStyledItemDelegate. Create LedDelegate and reimplement the four methods:

    • paint() drows the LED in the cell, as in the previous section.
    • createEditor() simply creates and returns a LedWidget.
    • setEditorData() writes the item’s current value to LedWidget.value.
    • setModelData() reads LedWidget.value and writes it back to the model via setData().
 1 import sys
 2 from PySide6.QtWidgets import QApplication, QTableView, QWidget, QVBoxLayout
 3 from PySide6.QtTest import QAbstractItemModelTester
 4 from models import CsvModel
 5 from delegates import LedDelegate
 6 
 7 
 8 class Window(QWidget):
 9     
10     def __init__(self):
11 
12         super().__init__()
13 
14         layout = QVBoxLayout()
15         self.setLayout(layout)
16 
17         model = CsvModel()
18         QAbstractItemModelTester(model,
19             QAbstractItemModelTester.FailureReportingMode.Warning)
20 
21         delegate = LedDelegate()
22         
23         view = QTableView()
24         view.setModel(model)
25         
26         view.setItemDelegateForColumn(2, delegate)
27         view.setItemDelegateForColumn(3, delegate)
28         
29         view.resizeColumnsToContents()
30         view.setCurrentIndex(model.index(0, 2))
31         view.setEditTriggers(
32             QTableView.EditTrigger.EditKeyPressed |
33             QTableView.EditTrigger.DoubleClicked)
34         
35         view.setFocus()
36         layout.addWidget(view)
37         
38         model.dataChanged.connect(self.on_data_changed)
39         
40     def on_data_changed(self, topLeft, bottomRight, roles):
41         for row in topLeft.model().csv_data:
42             print(row)
43 
44 
45 if __name__ == '__main__':
46 
47     app = QApplication(sys.argv)
48     main_window = Window()
49     main_window.show()
50     sys.exit(app.exec())

In the main window, create the model, view and delegate and assign the delegate to columns 2 and 3, as in the previous sections.

Now, when you run the application and double-click a boolean cell, the custom editor opens in place.

25.6 Combining Display and Editing in Delegates

The previous section showed how to provide both custom display and a custom editor widget. For boolean values, however, opening an editor on double-click is more ceremony that necessary - a single click or keystroke is enough to toggle the value. Qt supports this through editorEvent(), a method the delegate receives mouse and key events through even when no editor is opened.

An icon of a clipboard-list1

You are extending the banking application’s indicator table. Create a delegate that paints LED indicators for boolean columns and handles editing without creating any editor widget.

To combine display and editing in a delegate without an editor widget:

 1 from PySide6.QtCore import Qt, QEvent
 2 from PySide6.QtWidgets import QStyledItemDelegate
 3 from ledpainter import draw_led
 4 
 5 
 6 class LedDelegate(QStyledItemDelegate):
 7     
 8     # 1. Return None from createEditor.
 9 
10     def createEditor(self, parent, option, index):
11         return None
12 
13     # 2. Reimplement paint()
14     
15     def paint(self, painter, option, index):
16         value = index.data(Qt.ItemDataRole.EditRole)
17         draw_led(painter, option.rect, value)
18         super().paint(painter, option, index)
19     
20     # 3. Reimplement editorEvent()
21     
22     def editorEvent(self, event, model, option, index):
23 
24         if event.type() == QEvent.Type.MouseButtonRelease:
25             if event.button() == Qt.MouseButton.LeftButton:
26                 if option.rect.contains(event.pos()):
27                     current = model.data(index, Qt.ItemDataRole.EditRole)
28                     model.setData(index, not current,
29                         Qt.ItemDataRole.EditRole)
30                     return True
31                 
32         if event.type() == QEvent.Type.KeyPress:
33             if event.key() == Qt.Key.Key_Space:
34                 current = model.data(index, Qt.ItemDataRole.EditRole)
35                 model.setData(index, not current,
36                     Qt.ItemDataRole.EditRole)
37                 return True
38     
39         return super().editorEvent(event, model, option, index)
  1. Reimplement createEditor() to return None. This suppresses the default editor - double-clicking a cell does nothing.

  2. Reimplement paint() to draw LEDs, as in the previous sections.

  3. Reimplement editorEvent():

    • For a MouseButtonRelease event: if the left mouse button was released inside the cell (option.rect.contains(event.pos())), read the current value from the model, toggle it, and write it back.
    • For a KeyPress event: if the key is Space, read the current value from the model, toggle it, and write it back.
    • For all other events, call the base class implementation.

When you run the application, boolean cells can be toggled with a single left click or by pressing Space.

25.7 Persistent Editors in Views

In all previous sections, editors appeared only while the user was actively editing, opening on double-click or keypress and closing when done. A persistent editor stays visible in the cell at all times, with no click required to activate it. This is useful when you want certain fields to be immediatelly editable without any extra interaction.

Persistent editors are opened by calling view.openPersistentEditor() for each cell that should have one. The view than keeps the editor widget visible for the lifetime of that cell.

An icon of a clipboard-list1

You are extending the banking application’s indicator table. Make the Change (%) column always show a validated line edit so users can edit valuse directly without double-clicking.

To open persistent editors on a column:

 1 from PySide6.QtCore import Qt
 2 from PySide6.QtGui import QIntValidator
 3 from PySide6.QtWidgets import QItemEditorCreatorBase, QLineEdit
 4 
 5 
 6 class StrictIntValidator(QIntValidator):
 7     def validate(self, input_, pos):
 8         state, input_, pos = super().validate(input_, pos)
 9         if state == QIntValidator.State.Intermediate:
10             try:
11                 if int(input_) not in range(self.bottom(), self.top() + 1):
12                     state = QIntValidator.State.Invalid
13             except ValueError:
14                 pass
15         return state, input_, pos
16 
17 
18 # 1. Create the editor creator class.
19 
20 class NumericLineEditCreator(QItemEditorCreatorBase):
21 
22     def createWidget(self, parent):
23         editor = QLineEdit(parent)
24         editor.setAutoFillBackground(True)
25         editor.setValidator(StrictIntValidator(-100, 100, editor))
26         editor.setAlignment(Qt.AlignmentFlag.AlignRight)
27         return editor
28 
29     def valuePropertyName(self):
30         return 'text'
  1. Create the NumericLineEditCreator class:
    • Reimplement createWidget() to create a QLineEdit as the editor and install a custom int validator on it.
    • Reimplement valuePropertyName() to return 'text' instead of relying on a user property. This tells the delegate to read and write the line edit’s text property when transferring data to and from the model.
 1 from PySide6.QtCore import QAbstractTableModel, QModelIndex, Qt
 2 
 3 
 4 class CsvModel(QAbstractTableModel):
 5 
 6     def __init__(self, parent=None):
 7         super().__init__(parent)
 8         self.header = ['Indicator', 'Change (%)',
 9                        'Aggregate', 'Include in report']
10         self.csv_data = [
11             ['GDP', 3, True, True],
12             ['CPI', 6, True, True],
13             ['Jobs', 5, False, True],
14             ['Confidence', 75, False, True],
15             ['Industry', 92, False, True],
16             ['Retail', 4, True, True],
17         ]
18 
19     def rowCount(self, parent=QModelIndex()):
20         if parent.isValid():
21             return 0
22         return len(self.csv_data)
23 
24     def columnCount(self, parent=QModelIndex()):
25         if parent.isValid():
26             return 0
27         return len(self.header)
28 
29     def data(self, index, role):
30 
31         if not index.isValid():
32             return None
33         
34         if role in (Qt.ItemDataRole.DisplayRole,
35                     Qt.ItemDataRole.EditRole):
36             row, col = index.row(), index.column()
37             value = self.csv_data[row][col]
38             return value
39     
40     # 2. Convert the value from string to integer
41     #    and store it.
42     
43     def setData(self, index, value, role):
44         if not index.isValid():
45             return False
46 
47         row, col = index.row(), index.column()
48 
49         if role == Qt.ItemDataRole.EditRole:
50             print(value)
51             if col == 1:
52                 try:
53                     value = int(value)
54                 except (ValueError, TypeError):
55                     print(value)
56                     return False
57 
58             if self.csv_data[row][col] != value:
59                 self.csv_data[row][col] = value
60                 self.dataChanged.emit(index, index)
61                 return True
62         return False
63 
64     def flags(self, index):
65         if not index.isValid():
66             return Qt.ItemFlags()
67         flags = super().flags(index)
68         flags |= Qt.ItemFlags.ItemIsEditable
69         return flags
70 
71     def headerData(self, section, orientation, role):
72         if (orientation == Qt.Orientation.Horizontal 
73             and role == Qt.ItemDataRole.DisplayRole):
74             return self.header[section]
75         return super().headerData(section, orientation, role)
76     
  1. In the model’s setData() then convert the string back to int before storing it.
 1 import sys
 2 from PySide6.QtCore import QMetaType
 3 from PySide6.QtWidgets import (QApplication, QTableView,
 4     QWidget, QVBoxLayout, QItemEditorFactory, QStyledItemDelegate)
 5 from PySide6.QtTest import QAbstractItemModelTester
 6 from models import CsvModel
 7 from editorcreators import NumericLineEditCreator
 8 
 9 
10 class Window(QWidget):
11     def __init__(self):
12         super().__init__()
13         layout = QVBoxLayout()
14         self.setLayout(layout)
15 
16         model = CsvModel()
17         QAbstractItemModelTester(model,
18             QAbstractItemModelTester.FailureReportingMode.Warning)
19 
20         view = QTableView()
21         view.setModel(model)
22         view.resizeColumnsToContents()
23         
24         # 3. Set up the factory and the delegate.
25         
26         factory = QItemEditorFactory()
27         factory.registerEditor(QMetaType.Type.Int, NumericLineEditCreator())
28         delegate = QStyledItemDelegate()
29         delegate.setItemEditorFactory(factory)
30         view.setItemDelegateForColumn(1, delegate)
31         
32         # 4. Open a persistent editor for each cell in column 1.
33 
34         for row in range(model.rowCount()):
35             index = model.index(row, 1)
36             view.openPersistentEditor(index)
37 
38         layout.addWidget(view)
39         
40         model.dataChanged.connect(self.on_data_changed)
41         
42     def on_data_changed(self, topLeft, bottomRight, roles):
43         for row in topLeft.model().csv_data:
44             print(row)
45 
46 if __name__ == '__main__':
47     app = QApplication(sys.argv)
48     main_window = Window()
49     main_window.show()
50     sys.exit(app.exec())
  1. Set up the factory and the delegate, as in section 25.2. Register NumericLineEditorCreator for the int type and assign the delegate to column 1.

  2. Open a persistent editor for each cell in the column. After the model and view are set up, loop over the row and call view.openPersistentEditor() for each index in column 1. The view immediately instantiates the editor widget for each cell. Users can edit values directly without double-clicking.