我想在 QTableView 中显示带有星级的文件列表。为此,我使用以下委托:
class StarRatingDelegate(QStyledItemDelegate):
def __init__(self, parent=None):
super().__init__(parent)
def paint(self, painter, option, index):
file: File = index.data(Qt.UserRole)
star_rating_widget = StarRatingWidget(10, self.parent())
star_rating_widget.set_rating(file.rating)
star_rating_widget.render(painter, option.rect.topLeft())
StarRatingWidget
是一个简单的 QWidget,在 QHBoxLayout 中包含 5 个 QLable。
到目前为止,这一切都有效,但所有 StarRatingWidget 都移至左上角:
第一列以数字形式显示评级。您可以看到所有星星都稍微向左移动,并且比顶部的行高稍微多一点。
测试显示
option.rect
返回的坐标为 (0, 0) 为第一个单元格的左上角,但 star_rating_widget.render
处理的坐标为 (0, 0) 为窗口的左上角。因此,小部件会根据表格和窗口边框之间的空间以及表格标题的高度进行移动。
在有人问之前,这里是完整的代码。它需要 pyside6 才能运行。
#!/usr/bon/env python
from PySide6.QtCore import Qt, Signal, QAbstractItemModel, QModelIndex, QEvent
from PySide6.QtGui import QMouseEvent
from PySide6.QtWidgets import QApplication, QLabel, QTableView, QMainWindow, QSizePolicy, QHBoxLayout, QWidget, QStyledItemDelegate
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.setGeometry(100, 100, 250, 600)
self.central_widget = QWidget()
self.main_layout = QHBoxLayout()
self.central_widget.setLayout(self.main_layout)
self.setCentralWidget(self.central_widget)
self.list = QTableView()
self.list.setSelectionBehavior(QTableView.SelectionBehavior.SelectRows)
self.list.setSelectionMode(QTableView.SelectionMode.SingleSelection)
self.list.horizontalHeader().setStretchLastSection = True
self.list.verticalHeader().hide()
self.list.show_grid = False
self.list.setItemDelegateForColumn(1, StarRatingDelegate(self.list))
self.list.setModel(ListModel())
self.main_layout.addWidget(self.list)
class ListModel(QAbstractItemModel):
def __init__(self):
super().__init__()
self.horizontal_header_labels = ['Number', 'Stars']
def rowCount(self, parent=QModelIndex()):
return 50
def columnCount(self, parent=QModelIndex()):
return len(self.horizontal_header_labels)
def data(self, index, role):
if not index.isValid():
return None
if role == Qt.DisplayRole:
rating = (index.row() - 2) % 7
return None if rating >= 5 else rating + 1
return None
def headerData(self, section, orientation, role):
if orientation == Qt.Horizontal and role == Qt.DisplayRole:
return self.horizontal_header_labels[section]
return None
def index(self, row, column, parent=QModelIndex()):
if self.hasIndex(row, column, parent):
return self.createIndex(row, column)
return QModelIndex()
def parent(self, index):
return QModelIndex()
class StarRatingWidget(QWidget):
rating_changed = Signal(int)
def __init__(self, font_size, parent=None):
super().__init__(parent)
self.rating = 0
self.hovered_star: int|None = None
self.stars: List[QLabel] = []
self.font_size: int = font_size
self.init_ui()
def star_mouse_event(self, i: int):
def event(event: QMouseEvent):
if event.type() == QEvent.Enter:
self.hovered_star = i
self.update()
elif event.type() == QEvent.Leave:
self.hovered_star = None
self.update()
return event
def init_ui(self):
layout = QHBoxLayout()
for i in range(5):
star = QLabel()
star.mousePressEvent = lambda _, i=i: self.set_rating(i + 1)
star.enterEvent = self.star_mouse_event(i)
star.leaveEvent = self.star_mouse_event(i)
star.setSizePolicy(QSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed))
layout.addWidget(star)
self.stars.append(star)
self.setLayout(layout)
self.update()
def set_rating(self, rating: int|None):
if rating != self.rating:
self.rating = rating
self.update()
self.rating_changed.emit(rating)
def update(self):
for i, star in enumerate(self.stars):
rating = self.rating if self.rating is not None else 0
if i < rating:
star.setText('★')
else:
star.setText('☆')
if self.rating is None:
color = 'gray'
weight = 'normal'
elif i == self.hovered_star:
color = 'blue'
weight = 'bold'
else:
color = 'yellow'
weight = 'normal'
star.setStyleSheet(f'font-size: {self.font_size}px; color: {color}; font-weight: {weight}')
class StarRatingDelegate(QStyledItemDelegate):
def __init__(self, parent=None):
super().__init__(parent)
def paint(self, painter, option, index):
rating = index.data()
star_rating_widget = StarRatingWidget(10, self.parent())
star_rating_widget.set_rating(rating)
star_rating_widget.render(painter, option.rect.topLeft())
def main():
app = QApplication([])
main_window = MainWindow()
main_window.show()
QApplication.exec()
if __name__ == '__main__':
main()
您所看到的情况是由未解决的错误引起的,但是您的代码中也存在一些重要的问题和误解,如果解决得当,可以轻松地避免该问题,同时还可以改进其他方面。
这个错误(QTBUG-26694)出人意料地古老,是由于
render()
函数不考虑画家上设置的deviceTransform()
而引起的。渲染小部件时,绘制器有一个相对于窗口原点(即左上角)的内部“设备变换”。
由于上述错误,
render()
函数内不考虑设备变换,因此小部件是相对于窗口坐标系绘制的。
一个可能的解决方案是在渲染之前显式翻译画家。请注意,您没有考虑项目矩形,因此您还必须在渲染之前调用
resize()
:
def paint(self, painter, option, index):
rating = index.data()
star_rating_widget = StarRatingWidget(10, self.parent())
star_rating_widget.set_rating(rating)
star_rating_widget.resize(option.rect.size())
painter.save()
painter.translate(option.rect.topLeft())
star_rating_widget.render(painter)
painter.restore()
也就是说,这样做在原则上是错误的,而且原因有很多。
首先,委托的
paint()
函数可能被调用很多次,如果样式也使用悬停效果或简单地通过滚动,可能会调用数百次,这意味着对于 每个 调用 paint()
你正在创建一个新的 StarRatingWidget,您只使用一次。
这是低效且完全错误的。当你的程序启动时,我可以看到
paint
被调用超过 150 次(尽管只有大约 20 个项目可见),并且只需移动鼠标几秒钟就可以轻松达到 1000 次调用。
不仅每个小部件都是为了一次渲染而创建的,而且它们永远不会被销毁。让你的程序运行几分钟,你最终会得到数以万计的小部件,它们只是浪费内存。
至少,您应该选择以下两个选项之一:
star_rating_widget.deleteLater()
;__init__
中),然后在必要时更新和渲染它;第二种显然更合适、更有效:
class StarRatingDelegate(QStyledItemDelegate):
def __init__(self, parent=None):
super().__init__(parent)
self.star_rating_widget = StarRatingWidget(10, parent)
self.star_rating_widget.hide()
def paint(self, painter, option, index):
rating = index.data()
self.star_rating_widget.set_rating(rating)
painter.save()
painter.translate(option.rect.topLeft())
self.star_rating_widget.resize(option.rect.size())
self.star_rating_widget.render(painter)
painter.restore()
请注意,在委托中使用自定义小部件进行自定义绘制并不是真正有效。您可能认为它会优化渲染速度,但在某些情况下这只是部分正确:在您的情况下,它实际上更糟,因为每次要渲染小部件时您都间接调用
setStyleSheet()
,并且该调用结果如果您只想绘制 5 颗星,那么在很多的内部计算中,这些计算是完全不必要的,并且调整小部件的大小也可能使布局无效,从而导致在渲染实际发生之前进行进一步的计算。
此外,您尝试提供悬停功能是完全无用的,因为该小部件是静态呈现的并且不接收任何实际的用户事件。
实现您想要的效果的唯一合适的方法是在
paint()
内对星星进行完整渲染并覆盖委托的 editorEvent()
,以便即使在悬停时星星也会正确更新。
进一步说明:
setStretchLastSection
是一个函数,但是你用 True
覆盖了它,这显然是错误的,毫无意义;show_grid
,正确的call是self.list.setShowGrid(True)
;因为这也是默认值,无论如何它都是毫无意义的;update()
是所有QWidget通用的槽(并与同名的其他函数重载共享),用另一个函数覆盖该函数名称是完全不合适的;data()
和headerData()
都期望role
默认为Qt.ItemDataRole.DisplayRole
;index()
和 parent()
;