【问题标题】:Hyperlinks in QTreeView without QLabel没有 QLabel 的 QTreeView 中的超链接
【发布时间】:2011-10-29 11:10:33
【问题描述】:

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

根据这个问题的建议,我能够使用 QLabels 和 QTreeView.setIndexWidget 做到这一点。

Hyperlinks in QTreeView

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

好处是我可以在我的 QTreeView 中使用 Delegate 来绘制 看起来 像超链接的文本。这超级快。

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

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

【问题讨论】:

    标签: python qt hyperlink pyqt qtreeview


    【解决方案1】:

    最简单的方法似乎是继承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?
    • @Brendan 因为 QStyledItemDelegate 使用 QStyle 函数来绘制项目,并且 QProxyStyle 允许通过重用另一个样式类的部分来编写 QStyle 派生类。
    【解决方案2】:

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

    可能不需要一次填满整棵树。您是否考虑过根据需要生成 QLabels?分配足够的空间以使用 expandexpandAll 信号覆盖子树。您可以通过创建一个 QLabel 池并根据需要更改其文本(以及使用它们的位置)来扩展它。

    【讨论】:

      【解决方案3】:

      感谢这段代码,我在网上找到的越多越好。 我在我的项目中使用了你的代码,但我需要使用 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
      

      而且效果很好!

      【讨论】:

        猜你喜欢
        • 2011-10-28
        • 1970-01-01
        • 1970-01-01
        • 2012-01-15
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2021-11-19
        相关资源
        最近更新 更多