我正在尝试在QTreeView中显示可点击的超链接。
根据此问题的建议,我能够使用QLabels和QTreeView.setIndexWidget执行此操作。
不幸的是,我的QTreeView可能相当大(1000个项目),并且创建1000个QLabel很慢。
好处是我可以在我的QTreeView中使用Delegate来绘制看起来的文本,就像超链接一样。这非常快。
现在的问题是我需要它们像超链接一样响应(例如鼠标悬停手形光标,响应点击等),但我不确定最好的方法是什么。
我已经能够通过连接到QTreeView的clicked()信号来伪装它,但它并不完全相同,因为它响应整个单元格,而不仅仅是单元格内的文本。
答案 0 :(得分:2)
最简单的方法似乎是通过子类化QItemDelegate
,因为文本是由一个单独的虚函数drawDisplay
绘制的(QStyledItemDelegate
你几乎不得不重绘从头开始的项目,你需要一个派生自QProxyStyle
)的附加类:
QTextDocument
和QTextDocument.documentLayout().draw()
,drawDisplay
,我们在绘制文本时保存位置(因此保存的位置始终是项目文本的位置鼠标是哪个),editorEvent
中使用该位置来获取鼠标在文档中的相对位置,并使用QAbstractTextDocumentLayout.anchorAt
获取文档中该位置的链接。import sys
from PySide.QtCore import *
from PySide.QtGui import *
class LinkItemDelegate(QItemDelegate):
linkActivated = Signal(str)
linkHovered = Signal(str) # to connect to a QStatusBar.showMessage slot
def __init__(self, parentView):
QItemDelegate.__init__(self, parentView)
assert isinstance(parentView, QAbstractItemView), \
"The first argument must be the view"
# We need that to receive mouse move events in editorEvent
parentView.setMouseTracking(True)
# Revert the mouse cursor when the mouse isn't over
# an item but still on the view widget
parentView.viewportEntered.connect(parentView.unsetCursor)
# documents[0] will contain the document for the last hovered item
# documents[1] will be used to draw ordinary (not hovered) items
self.documents = []
for i in range(2):
self.documents.append(QTextDocument(self))
self.documents[i].setDocumentMargin(0)
self.lastTextPos = QPoint(0,0)
def drawDisplay(self, painter, option, rect, text):
# Because the state tells only if the mouse is over the row
# we have to check if it is over the item too
mouseOver = option.state & QStyle.State_MouseOver \
and rect.contains(self.parent().viewport() \
.mapFromGlobal(QCursor.pos())) \
and option.state & QStyle.State_Enabled
if mouseOver:
# Use documents[0] and save the text position for editorEvent
doc = self.documents[0]
self.lastTextPos = rect.topLeft()
doc.setDefaultStyleSheet("")
else:
doc = self.documents[1]
# Links are decorated by default, so disable it
# when the mouse is not over the item
doc.setDefaultStyleSheet("a {text-decoration: none}")
doc.setDefaultFont(option.font)
doc.setHtml(text)
painter.save()
painter.translate(rect.topLeft())
ctx = QAbstractTextDocumentLayout.PaintContext()
ctx.palette = option.palette
doc.documentLayout().draw(painter, ctx)
painter.restore()
def editorEvent(self, event, model, option, index):
if event.type() not in [QEvent.MouseMove, QEvent.MouseButtonRelease] \
or not (option.state & QStyle.State_Enabled):
return False
# Get the link at the mouse position
# (the explicit QPointF conversion is only needed for PyQt)
pos = QPointF(event.pos() - self.lastTextPos)
anchor = self.documents[0].documentLayout().anchorAt(pos)
if anchor == "":
self.parent().unsetCursor()
else:
self.parent().setCursor(Qt.PointingHandCursor)
if event.type() == QEvent.MouseButtonRelease:
self.linkActivated.emit(anchor)
return True
else:
self.linkHovered.emit(anchor)
return False
def sizeHint(self, option, index):
# The original size is calculated from the string with the html tags
# so we need to subtract from it the difference between the width
# of the text with and without the html tags
size = QItemDelegate.sizeHint(self, option, index)
# Use a QTextDocument to strip the tags
doc = self.documents[1]
html = index.data() # must add .toString() for PyQt "API 1"
doc.setHtml(html)
plainText = doc.toPlainText()
fontMetrics = QFontMetrics(option.font)
diff = fontMetrics.width(html) - fontMetrics.width(plainText)
return size - QSize(diff, 0)
只要你没有启用自动列大小调整内容(它会为每个项目调用sizeHint),它似乎没有委托更慢。
使用自定义模型,可以通过直接缓存模型中的某些数据来加速它(例如,通过使用和存储QStaticText用于非悬停项而不是QTextDocument)。
答案 1 :(得分:1)
可能可以避免使用QLabel,但它可能会影响代码的可读性。
可能无需立即填充整棵树。您是否考虑过根据需要生成QLabel?使用 expand 和 expandAll 信号分配足够的子树以覆盖子树。您可以通过创建QLabel池并根据需要更改其文本(以及它们的使用位置)来扩展它。
答案 2 :(得分:0)
感谢您提供此代码,我在网上找到了更好的代码。 我在项目中使用了您的代码,但是我需要使用qss样式表,并且您的代码无法正常工作。 我将QItemDelegate替换为QStyledItemDelegate并修改您的代码(html链接上的垂直对齐方式,也许您可以找到另一个更简单的方法),并且仅在字符串以'
class LinkItemDelegate(QStyledItemDelegate):
linkActivated = pyqtSignal(str)
linkHovered = pyqtSignal(str) # to connect to a QStatusBar.showMessage slot
def __init__(self, parentView):
super(LinkItemDelegate, self).__init__(parentView)
assert isinstance(parentView, QAbstractItemView), \
"The first argument must be the view"
# We need that to receive mouse move events in editorEvent
parentView.setMouseTracking(True)
# Revert the mouse cursor when the mouse isn't over
# an item but still on the view widget
parentView.viewportEntered.connect(parentView.unsetCursor)
# documents[0] will contain the document for the last hovered item
# documents[1] will be used to draw ordinary (not hovered) items
self.documents = []
for i in range(2):
self.documents.append(QTextDocument(self))
self.documents[i].setDocumentMargin(0)
self.lastTextPos = QPoint(0,0)
def drawDisplay(self, painter, option, rect, text):
# Because the state tells only if the mouse is over the row
# we have to check if it is over the item too
mouseOver = option.state & QStyle.State_MouseOver \
and rect.contains(self.parent().viewport() \
.mapFromGlobal(QCursor.pos())) \
and option.state & QStyle.State_Enabled
# Force to be vertically align
fontMetrics = QFontMetrics(option.font)
rect.moveTop(rect.y() + rect.height() / 2 - fontMetrics.height() / 2)
if mouseOver:
# Use documents[0] and save the text position for editorEvent
doc = self.documents[0]
self.lastTextPos = rect.topLeft()
doc.setDefaultStyleSheet("")
else:
doc = self.documents[1]
# Links are decorated by default, so disable it
# when the mouse is not over the item
doc.setDefaultStyleSheet("a {text-decoration: none; }")
doc.setDefaultFont(option.font)
doc.setHtml(text)
painter.save()
painter.translate(rect.topLeft())
ctx = QAbstractTextDocumentLayout.PaintContext()
ctx.palette = option.palette
doc.documentLayout().draw(painter, ctx)
painter.restore()
def editorEvent(self, event, model, option, index):
if event.type() not in [QEvent.MouseMove, QEvent.MouseButtonRelease] \
or not (option.state & QStyle.State_Enabled):
return False
# Get the link at the mouse position
# (the explicit QPointF conversion is only needed for PyQt)
pos = QPointF(event.pos() - self.lastTextPos)
anchor = self.documents[0].documentLayout().anchorAt(pos)
if anchor == "":
self.parent().unsetCursor()
else:
self.parent().setCursor(Qt.PointingHandCursor)
if event.type() == QEvent.MouseButtonRelease:
self.linkActivated.emit(anchor)
return True
else:
self.linkHovered.emit(anchor)
return False
def sizeHint(self, option, index):
# The original size is calculated from the string with the html tags
# so we need to subtract from it the difference between the width
# of the text with and without the html tags
size = super(LinkItemDelegate, self).sizeHint(option, index)
if option.text.startswith('<a'):
# Use a QTextDocument to strip the tags
doc = self.documents[1]
html = index.data() # must add .toString() for PyQt "API 1"
doc.setHtml(html)
plainText = doc.toPlainText()
fontMetrics = QFontMetrics(option.font)
diff = fontMetrics.width(html) - fontMetrics.width(plainText)
size = size - QSize(diff, 0)
return size
def paint(self, painter, option, index):
if (index.isValid()):
text = None
options = QStyleOptionViewItem(option)
self.initStyleOption(options,index)
if options.text.startswith('<a'):
text = options.text
options.text = ""
style = options.widget.style() if options.widget.style() else QApplication.style()
style.drawControl(QStyle.CE_ItemViewItem, options, painter, options.widget)
if text:
textRect = style.subElementRect(QStyle.SE_ItemViewItemText, options, options.widget)
self.drawDisplay(painter, option, textRect, text)
别忘了连接项目委托人:
linkItemDelegate = LinkItemDelegate(self.my_treeView)
linkItemDelegate.linkActivated.connect(self.onClicLink)
self.my_treeView.setItemDelegate(linkItemDelegate) # Create custom delegate and set model and delegate to the treeview object
效果很好!