Гиперссылки в QTreeView без QLabel - PullRequest
4 голосов
/ 18 августа 2011

Я пытаюсь отобразить кликабельные гиперссылки в моем QTreeView.

Мне удалось сделать это с помощью QLabels и QTreeView.setIndexWidget согласно рекомендациям из этого вопроса.

Гиперссылки в QTreeView

К сожалению, мой QTreeViewможет быть довольно большим (1000 элементов), и создание 1000 QLabels идет медленно.

Преимущество состоит в том, что я могу использовать Delegate в своем QTreeView для рисования текста, который выглядит как гиперссылки.Это очень быстро.

Проблема сейчас в том, что мне нужно, чтобы они отвечали, как гиперссылки (то есть курсор мыши при наведении курсора, реагирует на щелчки и т. Д.), Но я не уверен, что лучший способ сделать этото есть.

Я смог разобрать его, просто подключившись к сигналу clicked () QTreeView, но это не совсем то же самое, потому что он реагирует на всю ячейку, а не толькотекст внутри ячейки.

Ответы [ 3 ]

2 голосов
/ 29 августа 2011

Самый простой способ сделать это - создать подкласс QItemDelegate, потому что текст рисуется отдельной виртуальной функцией drawDisplayQStyledItemDelegate вам почти придется перерисовать элемент с нуля, и вы бынужен дополнительный класс, производный от QProxyStyle):

  • текст HTML рисуется с 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)

Пока вы неt Включив автоматическое изменение размера столбца к содержимому (которое будет вызывать sizeHint для каждого элемента), он не будет медленнее, чем без делегата.
В случае с пользовательской моделью можно ускорить ее, кэшируя непосредственно некоторые данные внутри модели (например, используя и сохраняя QStaticText для не обнаруживаемых элементов вместо QTextDocument).

1 голос
/ 22 августа 2011

Вероятно, можно избежать использования QLabels, но это может повлиять на читабельность кода.

Может не потребоваться заполнять все дерево сразу.Рассматривали ли вы создание QLabels по мере необходимости?Выделите достаточно, чтобы покрыть поддерево сигналами expand и expandAll .Вы можете расширить это, создав пул QLabels и изменив их текст (и где они используются) по мере необходимости.

0 голосов
/ 05 апреля 2019

Спасибо за этот код, тем лучше я нашел в Интернете.Я использую ваш код в своем проекте, но мне нужно использовать таблицу стилей 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

И это прекрасно работает!

...