Hyperlinks in QTreeView without QLabel

I am trying to display interactive hyperlinks in my QTreeView.

I was able to do this using QLabels and QTreeView.setIndexWidget in accordance with the recommendations on this.

Hyperlinks in QTreeView

Unfortunately, my QTreeView can be quite large (1000 items), and creating 1000 QLabels is slow.

The surface is that I can use a delegate in my QTreeView to draw text that looks like hyperlinks. It's very fast.

Now the problem is that I need them to react like hyperlinks (like mouse cursor, mouse pointer, etc.), but I'm not sure what the best way to do this.

I managed to fake it by simply connecting to the click () QTreeView signal, but this is not quite the same, because it responds to the entire cell, not just the text inside the cell.

+4
source share
2 answers

The easiest way to do this is to subclass QItemDelegate , because the text is drawn by a separate virtual function drawDisplay (with QStyledItemDelegate you will almost need to redraw the element from scratch, and you need an additional class derived from QProxyStyle ):

  • HTML text is drawn using QTextDocument and QTextDocument.documentLayout().draw() ,
  • when the mouse enters an element, the same element is redrawn and drawDisplay is drawDisplay , we save the position, we draw the text (so the saved position is always the text position for the element, the mouse),
  • this position is used in editorEvent to get the relative position of the mouse inside the document and get the link at that position in the document using 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) 

As long as you don't turn on automatic resizing of the column for the content (which will call sizeHint for each element), it does not look slower than without a delegate.
Using a custom model, you could speed it up by caching some data directly inside the model (for example, using and saving a QStaticText for non-hanging elements instead of a QTextDocument).

+2
source

It is probably possible to avoid using QLabels, but this may affect the readability of the code.

It is not possible to immediately fill the entire tree. Did you consider that you generate QLabels as needed? Highlight enough to cover the subtree with the expand and expandAll signals. You can expand this by creating a QLabels pool and changing their text (and where they are used) as needed.

+1
source

Source: https://habr.com/ru/post/1243001/


All Articles