Star Delegate Example¶
Demonstrates Qt’s itemview architecture
This example demonstrates the Qt model view architecture.
# Copyright (C) 2010 Hans-Peter Jansen <hpj@urpla.net>
# Copyright (C) 2011 Arun Srinivasan <rulfzid@gmail.com>
# Copyright (C) 2022 The Qt Company Ltd.
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
from __future__ import annotations
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())
# Copyright (C) 2010 Hans-Peter Jansen <hpj@urpla.net>
# Copyright (C) 2011 Arun Srinivasan <rulfzid@gmail.com>
# Copyright (C) 2022 The Qt Company Ltd.
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
from __future__ import annotations
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. """
with QPainter(self) as painter:
self.star_rating.paint(painter, self.rect(), self.palette(), isEditable=True)
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
# Copyright (C) 2010 Hans-Peter Jansen <hpj@urpla.net>
# Copyright (C) 2011 Arun Srinivasan <rulfzid@gmail.com>
# Copyright (C) 2022 The Qt Company Ltd.
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
from __future__ import annotations
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.RenderHint.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()