QTreeView中没有QLabel的超链接

4

我正在尝试在我的QTreeView中显示可点击的超链接。

根据这个问题的建议,我使用QLabels和QTreeView.setIndexWidget来实现这一点。

QTreeView中的超链接

不幸的是,我的QTreeView可能相当大(有数千个项目),创建数千个QLabels很慢。

好处是,我可以在我的QTreeView中使用委托来绘制看起来像超链接的文本。这非常快。

现在的问题是,我需要让它们像超链接一样响应(即鼠标悬停手形光标,响应点击等),但我不确定最好的方法是什么。

我已经通过连接到QTreeView的clicked()信号来模拟它,但它并不完全相同,因为它响应整个单元格,而不仅仅是单元格内的文本。

3个回答

2
似乎最简单的方法是通过子类化 QItemDelegate,因为文本是由一个单独的虚拟函数 drawDisplay 绘制的(使用 QStyledItemDelegate ,您几乎需要从头开始重新绘制项目,并且您需要另一个派生自 QProxyStyle 的类):
  • HTML文本使用 QTextDocumentQTextDocument.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)来加速它。

太棒了,我会试试这个!顺便说一下,我实际上正在使用QStyledItemDelegate。我并没有在模型中设置文本。模型项具有数据变量,文本(加上图标、着色、网格线等)都是基于数据绘制的。所以,我已经基本上从头开始重新绘制了该项。我很乐意使用QProxyStyle,尽管我以前从未使用过它,而且似乎它甚至没有被PyQt包装。为什么我需要QProxyStyle呢? - user297250
@Brendan 因为 QStyledItemDelegate 使用 QStyle 函数来绘制项,而 QProxyStyle 允许通过重用另一个样式类的部分来编写 QStyle 派生类。 - alexisdm

1

也许可以避免使用QLabels,但这可能会影响代码的可读性。

没有必要一次填充整个树。您考虑过按需生成QLabels吗?分配足够的QLabels以覆盖带有expandexpandAll信号的子树。您可以通过创建QLabels池并根据需要更改其文本(以及使用它们的位置)来扩展此池。


0

Thanks for this code, the better I found on the web. I use your code in my project but I need to use qss style sheet and your code doesn't work. I replace QItemDelegate by QStyledItemDelegate and modify your code (vertical alignment on html link, may be you can find another simpler workaroud), and make computations only when string begins with '

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)

Don't forget to connect item delegate :

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

And it's work great !


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接