PyQt 預設的 View 顯示雖然能用,但如果想要實現進階的顯示方式,就必須使用 Delegate 來做客制化。PyQt 中的 Delegate 類別允許我們控制 Painter 在 Context(畫布)上手動繪製 GUI,就像用程式操控小畫家的畫筆一樣,移動到某個位置,然後繪製。
當然你也可以一筆一畫從頭畫到尾,但更聰明的方式是搭配預設控制項來繪製。
用文字寫可能有看沒有懂,直接從實例來解説吧。
VerticalGridDelegate
讓表格只繪製直綫的 Delegate。
class VerticalGridDelegate(QtWidgets.QStyledItemDelegate):
# https://www.appsloveworld.com/python/1244/how-to-have-vertical-grid-lines-in-qtablewidget
def paint(self, painter, option, index):
super().paint(painter, option, index)
line = QtCore.QLine(option.rect.topRight(), option.rect.bottomRight())
color = option.palette.color(QtGui.QPalette.Midlight)
painter.save()
painter.setPen(QtGui.QPen(color))
painter.drawLine(line)
painter.restore()
HtmlRichTextDelegate
來練習新增一個 Delegate 類別可以顯示 HTML 的 RichText 内容。
class RichTextDelegate(QtWidgets.QStyledItemDelegate):
def __init__(self):
super().__init__()
opt = QtGui.QTextOption()
opt.setWrapMode(opt.WrapMode.NoWrap)
self.doc = QtGui.QTextDocument()
self.doc.setDefaultTextOption(opt)
def paint(self, painter, option, index):
painter.save()
opt = QtWidgets.QStyleOptionViewItem(option)
self.initStyleOption(opt, index)
self.doc.setTextWidth(opt.rect.width())
self.doc.setDefaultFont(opt.font)
# 假設你的 ItemModel 會在 UserRole 回傳 HTML 格式的文字
display_text = index.data(Qt.ItemDataRole.UserRole)
# 將 HTML 内容帶入臨時的 doc 實體以便使用其内建的繪製方法
self.doc.setHtml(display_text)
opt.text = ""
style = QtGui.QApplication.style() if opt.widget is None else opt.widget.style()
# 繪製一個不含文字的格子(文字的部分在前2行清掉了)
style.drawControl(QtWidgets.QStyle.ControlElement.CE_ItemViewItem, opt, painter)
if opt.features & QtWidgets.QStyleOptionViewItem.ViewItemFeature.HasCheckIndicator:
# 如果項目被 flag 標記為 Checkable,需要繪製一個 Checkbox
opt.rect = style.subElementRect(
QtWidgets.QStyle.SubElement.SE_CheckBoxContents, opt
)
# 調整繪製裁切範圍
ctx = QtGui.QAbstractTextDocumentLayout.PaintContext()
if opt.displayAlignment == Qt.AlignmentFlag.AlignRight:
ctx.clip = QtCore.QRectF(
opt.rect.right() - opt.fontMetrics.boundingRect(display_text).width(),
0,
opt.rect.width(),
opt.rect.height(),
)
else:
ctx.clip = QtCore.QRectF(0, 0, opt.rect.width(), opt.rect.height())
# 調整 y offset 到項目格子的正中間
content_height = self.doc.size().height()
offset_y = (option.rect.height() - content_height) / 2
painter.translate(opt.rect.left(), opt.rect.top() + offset_y)
# 繪製 doc 的内容
self.doc.documentLayout().draw(painter, ctx)
painter.restore()
def sizeHint(self, option, index):
opt = QtWidgets.QStyleOptionViewItem(option)
self.initStyleOption(opt, index)
self.doc.setTextWidth(opt.rect.width())
self.doc.setDefaultFont(opt.font)
# 假設你的 ItemModel 會在 UserRole 回傳 HTML 格式的文字
display_text = index.data(Qt.ItemDataRole.UserRole)
# 將 HTML 内容帶入臨時的 doc 實體以便使用其内建的繪製方法
self.doc.setHtml(display_text)
return QSize(int(self.doc.idealWidth()), int(self.doc.size().height()))
SyntaxHighlightDelegate
需搭配 syntax highlighter。
class SyntaxHighlightDelegate(QtWidgets.QStyledItemDelegate):
def __init__(self, highlighter: syntax.AbsHighlighter, color_theme):
super().__init__()
self._app = QApplication.instance()
self.doc = QtGui.QTextDocument()
self.docEllipse = QtGui.QTextDocument()
self.docEllipse.setPlainText("…")
self._initTextOption(self.doc)
self.marker = highlighter(self.doc, syntax.THEMES[color_theme])
def _initTextOption(self, document):
opt = QtGui.QTextOption()
opt.setWrapMode(opt.WrapMode.NoWrap)
document.setDefaultTextOption(opt)
def isDarkMode(self):
bg = self._app.palette().color(QPalette.ColorRole.Window)
return bg.rgb() < 0xFF808080
def paint(self, painter, option, index):
painter.save()
opt = QtWidgets.QStyleOptionViewItem(option)
self.initStyleOption(opt, index)
display_text = opt.fontMetrics.elidedText(
opt.text, opt.textElideMode, opt.rect.width()
)
self.doc.setTextWidth(opt.rect.width())
self.docEllipse.setTextWidth(opt.rect.width())
self.doc.setDefaultFont(opt.font)
self.docEllipse.setDefaultFont(opt.font)
self.doc.setPlainText(display_text)
opt.text = ""
style = QtGui.QApplication.style() if opt.widget is None else opt.widget.style()
# print(index.row(), hex(int(opt.state)), hex(int(opt.backgroundBrush.color().rgb())))
# style.drawPrimitive(QtWidgets.QStyle.PrimitiveElement.PE_PanelItemViewItem, opt, painter)
if opt.state & QtWidgets.QStyle.State_HasFocus:
# remove dotted border of selected item
# https://stackoverflow.com/questions/9795791/removing-dotted-border-without-setting-nofocus-in-windows-pyqt
opt.state ^= QtWidgets.QStyle.State_HasFocus
elif opt.state ^ QtWidgets.QStyle.State_Active:
if opt.state & QtWidgets.QStyle.State_MouseOver and not self.isDarkMode():
color = (
"#DCEAF6" if opt.features & QStyleOptionViewItem.Alternate else "#E5F3FF"
)
opt.backgroundBrush = QBrush(QColor(color))
style.drawControl(QtWidgets.QStyle.ControlElement.CE_ItemViewItem, opt, painter)
ctx = QtGui.QAbstractTextDocumentLayout.PaintContext()
ctx.clip = QtCore.QRectF(0, 0, opt.rect.width(), opt.rect.height())
content_height = self.doc.size().height()
offset_y = (option.rect.height() - content_height) / 2
# ellipsis_width = int(self.docEllipse.idealWidth())
painter.translate(opt.rect.left(), opt.rect.top() + offset_y)
self.marker.rehighlight()
self.doc.documentLayout().draw(painter, ctx)
painter.restore()
if (
not self.isDarkMode()
and opt.state & QtWidgets.QStyle.State_Selected
and opt.state & QtWidgets.QStyle.State_MouseOver
):
# draw top/bottom border of selection
painter.save()
painter.setPen(QtGui.QPen(QColor("#99D1FF")))
line = QtCore.QLine(option.rect.topLeft(), option.rect.topRight())
painter.drawLine(line)
line = QtCore.QLine(option.rect.bottomLeft(), option.rect.bottomRight())
painter.drawLine(line)
painter.restore()
References
- qt – PyQt listview with html rich text delegate moves text bit out of place(pic and code included) – Stack Overflow
- Star Delegate Example – Qt for Python
- QItemDelegate — Qt for Python