Star Delegate Example

Demonstrates Qt’s itemview architecture

This example demonstrates the Qt model view architecture.

Star Delegate Screenshot
from PySide6.QtWidgets import QStyledItemDelegate, QStyle

from starrating import StarRating
from stareditor import StarEditor


class StarDelegate(QStyledItemDelegate):
    """ A subclass of QStyledItemDelegate that allows us to render our
        pretty star ratings.
    """

    def __init__(self, parent=None):
        super().__init__(parent)

    def paint(self, painter, option, index):
        """ Paint the items in the table.

            If the item referred to by <index> is a StarRating, we handle the
            painting ourselves. For the other items, we let the base class
            handle the painting as usual.

            In a polished application, we'd use a better check than the
            column number to find out if we needed to paint the stars, but
            it works for the purposes of this example.
        """
        if index.column() == 3:
            star_rating = StarRating(index.data())

            # If the row is currently selected, we need to make sure we
            # paint the background accordingly.
            if option.state & QStyle.State_Selected:
                # The original C++ example used option.palette.foreground() to
                # get the brush for painting, but there are a couple of
                # problems with that:
                #   - foreground() is obsolete now, use windowText() instead
                #   - more importantly, windowText() just returns a brush
                #     containing a flat color, where sometimes the style
                #     would have a nice subtle gradient or something.
                # Here we just use the brush of the painter object that's
                # passed in to us, which keeps the row highlighting nice
                # and consistent.
                painter.fillRect(option.rect, painter.brush())

            # Now that we've painted the background, call starRating.paint()
            # to paint the stars.
            star_rating.paint(painter, option.rect, option.palette)
        else:
            QStyledItemDelegate.paint(self, painter, option, index)

    def sizeHint(self, option, index):
        """ Returns the size needed to display the item in a QSize object. """
        if index.column() == 3:
            star_rating = StarRating(index.data())
            return star_rating.sizeHint()
        else:
            return QStyledItemDelegate.sizeHint(self, option, index)

    # The next 4 methods handle the custom editing that we need to do.
    # If this were just a display delegate, paint() and sizeHint() would
    # be all we needed.

    def createEditor(self, parent, option, index):
        """ Creates and returns the custom StarEditor object we'll use to edit
            the StarRating.
        """
        if index.column() == 3:
            editor = StarEditor(parent)
            editor.editing_finished.connect(self.commit_and_close_editor)
            return editor
        else:
            return QStyledItemDelegate.createEditor(self, parent, option, index)

    def setEditorData(self, editor, index):
        """ Sets the data to be displayed and edited by our custom editor. """
        if index.column() == 3:
            editor.star_rating = StarRating(index.data())
        else:
            QStyledItemDelegate.setEditorData(self, editor, index)

    def setModelData(self, editor, model, index):
        """ Get the data from our custom editor and stuffs it into the model.
        """
        if index.column() == 3:
            model.setData(index, editor.star_rating.star_count)
        else:
            QStyledItemDelegate.setModelData(self, editor, model, index)

    def commit_and_close_editor(self):
        """ Erm... commits the data and closes the editor. :) """
        editor = self.sender()

        # The commitData signal must be emitted when we've finished editing
        # and need to write our changed back to the model.
        self.commitData.emit(editor)
        self.closeEditor.emit(editor, QStyledItemDelegate.NoHint)


if __name__ == "__main__":
    """ Run the application. """
    from PySide6.QtWidgets import (QApplication, QTableWidget, QTableWidgetItem,
                                   QAbstractItemView)
    import sys

    app = QApplication(sys.argv)

    # Create and populate the tableWidget
    table_widget = QTableWidget(4, 4)
    table_widget.setItemDelegate(StarDelegate())
    table_widget.setEditTriggers(QAbstractItemView.DoubleClicked |
                                QAbstractItemView.SelectedClicked)
    table_widget.setSelectionBehavior(QAbstractItemView.SelectRows)
    table_widget.setHorizontalHeaderLabels(["Title", "Genre", "Artist", "Rating"])

    data = [ ["Mass in B-Minor", "Baroque", "J.S. Bach", 5],
             ["Three More Foxes", "Jazz", "Maynard Ferguson", 4],
             ["Sex Bomb", "Pop", "Tom Jones", 3],
             ["Barbie Girl", "Pop", "Aqua", 5] ]

    for r in range(len(data)):
        table_widget.setItem(r, 0, QTableWidgetItem(data[r][0]))
        table_widget.setItem(r, 1, QTableWidgetItem(data[r][1]))
        table_widget.setItem(r, 2, QTableWidgetItem(data[r][2]))
        item = QTableWidgetItem()
        item.setData(0, StarRating(data[r][3]).star_count)
        table_widget.setItem(r, 3, item)

    table_widget.resizeColumnsToContents()
    table_widget.resize(500, 300)
    table_widget.show()

    sys.exit(app.exec())
from PySide6.QtWidgets import (QWidget)
from PySide6.QtGui import (QPainter)
from PySide6.QtCore import Signal

from starrating import StarRating


class StarEditor(QWidget):
    """ The custom editor for editing StarRatings. """

    # A signal to tell the delegate when we've finished editing.
    editing_finished = Signal()

    def __init__(self, parent=None):
        """ Initialize the editor object, making sure we can watch mouse
            events.
        """
        super().__init__(parent)

        self.setMouseTracking(True)
        self.setAutoFillBackground(True)
        self.star_rating = StarRating()

    def sizeHint(self):
        """ Tell the caller how big we are. """
        return self.star_rating.sizeHint()

    def paintEvent(self, event):
        """ Paint the editor, offloading the work to the StarRating class. """
        painter = QPainter(self)
        self.star_rating.paint(painter, self.rect(), self.palette(), isEditable=True)
        # QPainter needs an explicit end() in PyPy. This will become a context manager in 6.3.
        painter.end()

    def mouseMoveEvent(self, event):
        """ As the mouse moves inside the editor, track the position and
            update the editor to display as many stars as necessary.
        """
        star = self.star_at_position(event.x())

        if (star != self.star_rating.star_count) and (star != -1):
            self.star_rating.star_count = star
            self.update()

    def mouseReleaseEvent(self, event):
        """ Once the user has clicked his/her chosen star rating, tell the
            delegate we're done editing.
        """
        self.editing_finished.emit()

    def star_at_position(self, x):
        """ Calculate which star the user's mouse cursor is currently
            hovering over.
        """
        star = (x / (self.star_rating.sizeHint().width() /
                     self.star_rating.MAX_STAR_COUNT)) + 1
        if (star <= 0) or (star > self.star_rating.MAX_STAR_COUNT):
            return -1

        return star
from math import (cos, sin, pi)

from PySide6.QtGui import (QPainter, QPolygonF)
from PySide6.QtCore import (QPointF, QSize, Qt)

PAINTING_SCALE_FACTOR = 20


class StarRating(object):
    """ Handle the actual painting of the stars themselves. """

    def __init__(self, starCount=1, maxStarCount=5):
        self.star_count = starCount
        self.MAX_STAR_COUNT = maxStarCount

        # Create the star shape we'll be drawing.
        self._star_polygon = QPolygonF()
        self._star_polygon.append(QPointF(1.0, 0.5))
        for i in range(1, 5):
            self._star_polygon.append(QPointF(0.5 + 0.5 * cos(0.8 * i * pi),
                                    0.5 + 0.5 * sin(0.8 * i * pi)))

        # Create the diamond shape we'll show in the editor
        self._diamond_polygon = QPolygonF()
        diamond_points = [QPointF(0.4, 0.5), QPointF(0.5, 0.4),
                         QPointF(0.6, 0.5), QPointF(0.5, 0.6),
                         QPointF(0.4, 0.5)]
        self._diamond_polygon.append(diamond_points)

    def sizeHint(self):
        """ Tell the caller how big we are. """
        return PAINTING_SCALE_FACTOR * QSize(self.MAX_STAR_COUNT, 1)

    def paint(self, painter, rect, palette, isEditable=False):
        """ Paint the stars (and/or diamonds if we're in editing mode). """
        painter.save()

        painter.setRenderHint(QPainter.Antialiasing, True)
        painter.setPen(Qt.NoPen)

        if isEditable:
            painter.setBrush(palette.highlight())
        else:
            painter.setBrush(palette.windowText())

        y_offset = (rect.height() - PAINTING_SCALE_FACTOR) / 2
        painter.translate(rect.x(), rect.y() + y_offset)
        painter.scale(PAINTING_SCALE_FACTOR, PAINTING_SCALE_FACTOR)

        for i in range(self.MAX_STAR_COUNT):
            if i < self.star_count:
                painter.drawPolygon(self._star_polygon, Qt.WindingFill)
            elif isEditable:
                painter.drawPolygon(self._diamond_polygon, Qt.WindingFill)
            painter.translate(1.0, 0.0)

        painter.restore()