Qt modelview: delegates

From wikinotes

Delegates allow you to add customized representations of model-data within your view. Some reasons this may be useful:

  • you want to customize the item's editor (ex: add qvalidator, launch file-browser, ...)
  • you want to customize how the item is painted (ex: five star-rating, locked/unlocked)
  • you want to show data differently than it is stored in model (ex: dates in local timezone, de-serialized json, ...)


WARNING:

Apparently, I have two pages dedicated to this. See Python qt: delegates

Painting

Repainting Cell

You can emit model.dataChanged(index, index) to force a repaint of that cell in all views.


Painting Widgets

If you are imitating a real Qt widget, there are tools to assist with painting them.

  • QStyle.drawControl()
  • QStyle.drawPixmap() draw custom pixmap (see QPixmap.grabWidget()(Qt4) and QWidget.grab()(Qt5))
  • QStyle.drawItemText() writes text
  • ... etc ...

TODO:

Document use of styleoption for creating entirely default painted objects.

def paint(self, painter, option, index):
    qapp = QtWidgets.QApplication.instance()
    qstyle = qapp.style()

    button = QtWidgets.QStyleOptionButton()
    button.rect = self._get_button_qrect(option)
    button.styleObject = qstyle
    button.palette = option.palette
    button.fontMetrics = option.fontMetrics
    button.text = index.data()

    # manipulate button-state
    button.state = option.state | qstyle.State_Sunken | qstyle.Enabled

Normal

class Delegate(QtWidgets.QStyledItemDelegate):
    def paint(self, painter, option, index):
        """ Paints the model-data text.
        """
        qapp = QtWidgets.QApplication.instance()
        qstyle = qapp.style()

        # get rect of text (within margins)
        text_rect = qstyle.subElementRect(qstyle.SE_ItemViewItemText, option)

        # paint text, observing view's text-alignment
        painter.drawText(text_rect, option.displayAlignment, index.data())

QProgressbar

class Delegate(QtWidgets.QStyledItemDelegate):
    def paint(self, painter, option, index):
        current_progress = float(index.data())

        styleopts = QtWidgets.QStyleOptionProgressBar()
        styleopts.rect = option.rect
        styleopts.minimum = 0.0
        styleopts.maximum = 100.0
        styleopts.progress = current_progress

        QtWidgets.QApplication.style().drawControl(
            QtWidgets.QStyle.CE_ProgressBar,
            styleopts,
            painter,
        )

See Also

QPushButton

class Delegate(QtWidgets.QStyledItemDelegate):
    def paint(self, painter, option, index):
        qapp = QtWidgets.QApplication.instance()
        qstyle = qapp.style()

        # core
        styleopts = QtWidgets.QStyleOptionButton()
        styleopts.rect = option.rect
        styleopts.text = index.data(QtCore.Qt.DisplayRole)
        styleopts.icon = index.data(QtCore.Qt.DecorationRole)

        # style/state
        styleopts.state = option.state         # State_Enabled, State_Sunken, State_Raised, State_MouseOver, ...
        styleopts.styleObject = qstyle
        styleopts.fontMetrics = option.fontMetrics
        styleopts.palette = option.palette

        qstyle.drawControl(QtWidgets.QStyle.CE_PushButton,
                          styleopts,
                          painter)

QComboBox


Copy Widget Pixmap (best -- closest to real qss stylesheet)

class Delegate(QtWidgets.QStyledItemDelegate):
    def paint(self, painter, option, index):
        """ paints combobox using a real QComboBox, styled exactly
        exactly like a normal qcombobox.
        """
        # build widget
        text = index.data(QtCore.Qt.DisplayRole)
        combo = QtWidgets.QComboBox()
        combo.addItems([text])
        combo.resize(option.rect.width(), option.rect.height())

        # steal pixmap of widget
        pixmap = widgetcompatUtils.grab(combo)

        # paint pixmap
        painter.drawPixmap(
            option.rect,   # target-rect
            pixmap,        # pixmap
            combo.rect(),  # source-rect
        )

Or you may paint using drawComplexControl

class Delegate(QtGui.QStyledItemDelegate):
    def paint(self, painter, option, index):
        styleopts = QtWidgets.QStyleOptionComboBox()
        styleopts.rect = option.rect
        styleopts.palette = option.palette

        style = QtWidgets.QApplication.instance().style()

        style.drawComplexControl(
            QtWidgets.QStyle.CC_ComboBox,
            styleopts,
            painter,
        )

        style.drawItemText(
            painter,
            option.rect,
            option.displayAlignment,
            option.palette,
            True,
            index.data(QtCore.Qt.DisplayRole),
        )

QPixmap


using QStyle.drawItemPixmap() (simple)

class ToolLockDelegate(QtWidgets.QStyledItemDelegate):
    def __init__(self, *args, **kwargs):
        super(ToolLockDelegate, self).__init__(*args, **kwargs)
        self._pixmap = QtGui.QPixmap('file.png')

    def paint(self, painter, option, index):
        QtWidgets.QApplication.style().drawItemPixmap(
            painter,
            option.rect,
            QtCore.Qt.AlignCenter,
            self._pixmap,
        )

By hand (complex).

(NOTE - pixmap is not centered, and scales larger than it should).

class LockDelegate(QtWidgets.QStyledItemDelegate):
    clicked = QtCore.Signal(QtCore.QModelIndex)

    def __init__(self, *args, **kwargs):
        super(LockDelegate, self).__init__(*args, **kwargs)

        general_icons = icons.GeneralIcons()
        self._pixmap = QtGui.QPixmap('some/file.png')

    def paint(self, painter, option, index):
        painter_opacity = painter.opacity()

        # paint the pixmap
        height = option.rect.height()
        scaled_pixmap = self._pixmap.scaledToHeight(height)
        width = scaled_pixmap.rect().width()
        painter.drawPixmap(option.rect.x(), option.rect.y(), width, height, scaled_pixmap)

        # restore painter opacity
        painter.setOpacity(painter_opacity)

Painting Selection

In simple cases, this should not be required.
If you are replacing the contents of the cell with a pixmap or the like, this is how to render the current selection.

class Delegate(QtWidgets.QStyledItemDelegate):
    def paint(self, painter, option, index):
        style = QtWidgets.QApplication.style()
        
        # paint selection
        if option.state & QtWidgets.QStyle.State_Selected:
            painter.fillRect(option.rect, option.palette.highlight())

        # paint mouseover
        elif option.state & QtWidgets.QStyle.State_MouseOver:
            base_qcolor = option.palette.base().color()
            mouseover_qcolor = base_qcolor.lighter()
            mouseover_brush = QtGui.QBrush(mouseover_qcolor)
            painter.fillRect(option.rect, mouseover_brush)

        # ... your custom paint instructions ...

Registering Clicks

Normally, the view decides the type of event that triggers editor-creation (ex: qlineedit, calendar, etc).
You do not have to be restricted to this to register single-clicks.
QtWidgets.QItemDelegate.editorEvent() is called on mouseup/mousedown on the delegate.

Example Button within Cell


def Delegate(QtWidgets.QItemDelegate):
    _button_width = 80

    def paint(self, painter, option, index):
        self._paint_text(painter, option, index)
        self._paint_button(painter, option, index)

    def editorEvent(self, event, model, option, index):
        """ Prints for single-clicks within button qrect.
        """
        # ignore if not a single click
        if event.type() != QtCore.QEvent.MouseButtonRelease:
            return True

        # confirm click on region with button
        qrect = self._get_button_rect(option)
        if qrect.contains(event.pos()):
            print('single click on button region!')

    # =====================
    # \/ private methods \/
    # =====================

    def _paint_button(self, painter, option, index):
        """ Adds button to rightmost 80px of cell.
        """
        qapp = QtWidgets.QApplication.instance()
        qstyle = qapp.style()
        
        button = QtWidgets.QStyleOptionButton()
        button.rect = self._get_button_rect(option)
        button.text = 'click me!'
        button.state = option.state
        button.styleObject = qstyle
        button.palette = option.palette
        button.fontMetrics = option.fontMetrics

        qstyle.drawControl(qstyle.CE_PushButton, button, painter)

    def _paint_text(self, painter, option, index):
        """ Paints the model-data text.
        """
        qapp = QtWidgets.QApplication.instance()
        qstyle = qapp.style()
    
        # get rect of text (within margins)
        text_rect = qstyle.subElementRect(qstyle.SE_ItemViewItemText, option)
    
        # paint text, observing view's text-alignment
        painter.drawText(text_rect, option.displayAlignment, index.data())

    def _get_button_rect(self, option):
        """ Generates button qrect from QStyleOptionViewItem obtained by paint/editorEvent.
        """
        x = option.rect.left() + option.rect.right() - self._button_width
        y = option.rect.top()
        qrect = QtCore.QRect(x, y, self._button_width, option.rect.height())
        return qrect

Custom Editor

QStyledItemDelegate methods
createEditor(self, parent, option, index) returns editor widget
setEditorData(self, editor, index) load the editor with your data (performing any adjustments required)
setModelData(self, editor, model, index) save the editor's data into the model (performing any adjustments/validation required)

Buttons in DataCell

See https://stackoverflow.com/questions/11777637/adding-button-to-qtableview