How to make a fast QTableView with HTML-formatted and clickable cells?

后端 未结 4 1108

I\'m making a dictionary program that displays word definitions in a 3-column QTableView subclass, as user types them, taking data from a QAbstractTableModel<

相关标签:
4条回答
  • 2021-01-31 01:06

    Many thanks for these code examples, it helped me implement similar functionalaity in my application. I'm working with Python 3 and QT5 and I would like to share my Python code, is case it may be helpful implementing this in Python.

    Note that if you are using QT Designer for the UI design, you can use "promote" to change a regular "QTableView" widget to use your custom widget automatically when converting the XML to Python code with "pyuic5".

    Code as follows:

    from PyQt5 import QtCore, QtWidgets, QtGui
    
    class CustomTableView(QtWidgets.QTableView):
    
        link_activated = QtCore.pyqtSignal(str)
    
        def __init__(self, parent=None):
            self.parent = parent
            super().__init__(parent)
    
            self.setMouseTracking(True)
            self._mousePressAnchor = ''
            self._lastHoveredAnchor = ''
    
        def mousePressEvent(self, event):
            anchor = self.anchorAt(event.pos())
            self._mousePressAnchor = anchor
    
        def mouseMoveEvent(self, event):
            anchor = self.anchorAt(event.pos())
            if self._mousePressAnchor != anchor:
                self._mousePressAnchor = ''
    
            if self._lastHoveredAnchor != anchor:
                self._lastHoveredAnchor = anchor
                if self._lastHoveredAnchor:
                    QtWidgets.QApplication.setOverrideCursor(QtGui.QCursor(QtCore.Qt.PointingHandCursor))
                else:
                    QtWidgets.QApplication.restoreOverrideCursor()
    
        def mouseReleaseEvent(self, event):
            if self._mousePressAnchor:
                anchor = self.anchorAt(event.pos())
                if anchor == self._mousePressAnchor:
                    self.link_activated.emit(anchor)
                self._mousePressAnchor = ''
    
        def anchorAt(self, pos):
            index = self.indexAt(pos)
            if index.isValid():
                delegate = self.itemDelegate(index)
                if delegate:
                    itemRect = self.visualRect(index)
                    relativeClickPosition = pos - itemRect.topLeft()
                    html = self.model().data(index, QtCore.Qt.DisplayRole)
                    return delegate.anchorAt(html, relativeClickPosition)
            return ''
    
    
    class CustomDelegate(QtWidgets.QStyledItemDelegate):
    
        def anchorAt(self, html, point):
            doc = QtGui.QTextDocument()
            doc.setHtml(html)
            textLayout = doc.documentLayout()
            return textLayout.anchorAt(point)
    
        def paint(self, painter, option, index):
            options = QtWidgets.QStyleOptionViewItem(option)
            self.initStyleOption(options, index)
    
            if options.widget:
                style = options.widget.style()
            else:
                style = QtWidgets.QApplication.style()
    
            doc = QtGui.QTextDocument()
            doc.setHtml(options.text)
            options.text = ''
    
            style.drawControl(QtWidgets.QStyle.CE_ItemViewItem, options, painter)
            ctx = QtGui.QAbstractTextDocumentLayout.PaintContext()
    
            textRect = style.subElementRect(QtWidgets.QStyle.SE_ItemViewItemText, options)
    
            painter.save()
    
            painter.translate(textRect.topLeft())
            painter.setClipRect(textRect.translated(-textRect.topLeft()))
            painter.translate(0, 0.5*(options.rect.height() - doc.size().height()))
            doc.documentLayout().draw(painter, ctx)
    
            painter.restore()
    
        def sizeHint(self, option, index):
            options = QtWidgets.QStyleOptionViewItem(option)
            self.initStyleOption(options, index)
    
            doc = QtGui.QTextDocument()
            doc.setHtml(options.text)
            doc.setTextWidth(options.rect.width())
    
            return QtCore.QSize(doc.idealWidth(), doc.size().height())
    
    0 讨论(0)
  • 2021-01-31 01:08

    In your case QLabel (re)painting is slow, not QTableView. On other hand, QTableView does not support formated text at all.

    Probably, your only way, is to create your own delegate, QStyledItemDelegate, and make your own painting and click processing in it.

    PS: yes, you can use QTextDocument for rendering html inside delegate, but it will be slow too.

    0 讨论(0)
  • 2021-01-31 01:13

    I use a slighted improved solution based on Xilexio code. There is 3 fundamental differences:

    • Vertical alignment so if you put the text in a cell higher than the text it will be center aligned and not top aligned.
    • The text will be right shifted if the cell contains an icon so the icon will not be displayed above the text.
    • The widget style to highlighted cells will be followed, so you select this cell, the colors will behave similar to other cells without the delegate.

    Here is my code of the paint() function (the rest of the code remains the same).

    QStyleOptionViewItemV4 options = option;
    initStyleOption(&options, index);
    
    painter->save();
    
    QTextDocument doc;
    doc.setHtml(options.text);
    
    options.text = "";
    options.widget->style()->drawControl(QStyle::CE_ItemViewItem, &options, painter);
    
    QSize iconSize = options.icon.actualSize(options.rect.size);
    // right shit the icon
    painter->translate(options.rect.left() + iconSize.width(), options.rect.top());
    QRect clip(0, 0, options.rect.width() + iconSize.width(), options.rect.height());
    
    painter->setClipRect(clip);
    QAbstractTextDocumentLayout::PaintContext ctx;
    
    // Adjust color palette if the cell is selected
    if (option.state & QStyle::State_Selected)
        ctx.palette.setColor(QPalette::Text, option.palette.color(QPalette::Active, QPalette::HighlightedText));
    ctx.clip = clip;
    
    // Vertical Center alignment instead of the default top alignment
    painter->translate(0, 0.5*(options.rect.height() - doc.size().height()));
    
    doc.documentLayout()->draw(painter, ctx);
    painter->restore();
    
    0 讨论(0)
  • 2021-01-31 01:15

    I solved the problem by putting together few answers and looking at Qt's internals.

    A solution which works very fast for static html content with links in QTableView is as folows:

    • Subclass QTableView and handle mouse events there;
    • Subclass QStyledItemDelegate and paint the html there (contrary to RazrFalcon's answer, it is very fast, as only a small amount of cells is visible at a time and only those have paint() method called);
    • In subclassed QStyledItemDelegate create a function that figures out which link was clicked by QAbstractTextDocumentLayout::anchorAt(). You cannot create QAbstractTextDocumentLayout yourself, but you can get it from QTextDocument::documentLayout() and, according to Qt source code, it's guaranteed to be non-null.
    • In subclassed QTableView modify QCursor pointer shape accordingly to whether it's hovering over a link

    Below is a complete, working implementation of QTableView and QStyledItemDelegate subclasses that paint the HTML and send signals on link hover/activation. The delegate and model still have to be set outside, as follows:

    wordTable->setModel(&myModel);
    auto wordItemDelegate = new WordItemDelegate(this);
    wordTable->setItemDelegate(wordItemDelegate); // or just choose specific columns/rows
    

    WordView.h

    class WordView : public QTableView {
        Q_OBJECT
    
    public:
        explicit WordView(QWidget *parent = 0);
    
    signals:
        void linkActivated(QString link);
        void linkHovered(QString link);
        void linkUnhovered();
    
    protected:
        void mousePressEvent(QMouseEvent *event);
        void mouseMoveEvent(QMouseEvent *event);
        void mouseReleaseEvent(QMouseEvent *event);
    
    private:
        QString anchorAt(const QPoint &pos) const;
    
    private:
        QString _mousePressAnchor;
        QString _lastHoveredAnchor;
    };
    

    WordView.cpp

    #include <QApplication>
    #include <QCursor>
    #include <QMouseEvent>
    #include "WordItemDelegate.h"
    #include "WordView.h"
    
    WordView::WordView(QWidget *parent) :
        QTableView(parent)
    {
        // needed for the hover functionality
        setMouseTracking(true);
    }
    
    void WordView::mousePressEvent(QMouseEvent *event) {
        QTableView::mousePressEvent(event);
    
        auto anchor = anchorAt(event->pos());
        _mousePressAnchor = anchor;
    }
    
    void WordView::mouseMoveEvent(QMouseEvent *event) {
        auto anchor = anchorAt(event->pos());
    
        if (_mousePressAnchor != anchor) {
            _mousePressAnchor.clear();
        }
    
        if (_lastHoveredAnchor != anchor) {
            _lastHoveredAnchor = anchor;
            if (!_lastHoveredAnchor.isEmpty()) {
                QApplication::setOverrideCursor(QCursor(Qt::PointingHandCursor));
                emit linkHovered(_lastHoveredAnchor);
            } else {
                QApplication::restoreOverrideCursor();
                emit linkUnhovered();
            }
        }
    }
    
    void WordView::mouseReleaseEvent(QMouseEvent *event) {
        if (!_mousePressAnchor.isEmpty()) {
            auto anchor = anchorAt(event->pos());
    
            if (anchor == _mousePressAnchor) {
                emit linkActivated(_mousePressAnchor);
            }
    
            _mousePressAnchor.clear();
        }
    
        QTableView::mouseReleaseEvent(event);
    }
    
    QString WordView::anchorAt(const QPoint &pos) const {
        auto index = indexAt(pos);
        if (index.isValid()) {
            auto delegate = itemDelegate(index);
            auto wordDelegate = qobject_cast<WordItemDelegate *>(delegate);
            if (wordDelegate != 0) {
                auto itemRect = visualRect(index);
                auto relativeClickPosition = pos - itemRect.topLeft();
    
                auto html = model()->data(index, Qt::DisplayRole).toString();
    
                return wordDelegate->anchorAt(html, relativeClickPosition);
            }
        }
    
        return QString();
    }
    

    WordItemDelegate.h

    #include <QStyledItemDelegate>
    
    class WordItemDelegate : public QStyledItemDelegate {
        Q_OBJECT
    
    public:
        explicit WordItemDelegate(QObject *parent = 0);
    
        QString anchorAt(QString html, const QPoint &point) const;
    
    protected:
        void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const;
        QSize sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const;
    };
    

    WordItemDelegate.cpp

    #include <QPainter>
    #include <QTextDocument>
    #include <QAbstractTextDocumentLayout>
    #include "WordItemDelegate.h"
    
    WordItemDelegate::WordItemDelegate(QObject *parent) :
        QStyledItemDelegate(parent)
    {}
    
    QString WordItemDelegate::anchorAt(QString html, const QPoint &point) const {
        QTextDocument doc;
        doc.setHtml(html);
    
        auto textLayout = doc.documentLayout();
        Q_ASSERT(textLayout != 0);
        return textLayout->anchorAt(point);
    }
    
    void WordItemDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const {
        auto options = option;
        initStyleOption(&options, index);
    
        painter->save();
    
        QTextDocument doc;
        doc.setHtml(options.text);
    
        options.text = "";
        options.widget->style()->drawControl(QStyle::CE_ItemViewItem, &option, painter);
    
        painter->translate(options.rect.left(), options.rect.top());
        QRect clip(0, 0, options.rect.width(), options.rect.height());
        doc.drawContents(painter, clip);
    
        painter->restore();
    }
    
    QSize WordItemDelegate::sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const {
        QStyleOptionViewItemV4 options = option;
        initStyleOption(&options, index);
    
        QTextDocument doc;
        doc.setHtml(options.text);
        doc.setTextWidth(options.rect.width());
        return QSize(doc.idealWidth(), doc.size().height());
    }
    

    Note that this solution is fast only because a small subset of rows is rendered at once, and therefore not many QTextDocuments are rendered at once. Automatic adjusting all row heights or column widths at once will still be slow. If you need that functionality, you can make the delegate inform the view that it painted something and then making the view adjust the height/width if it hasn't before. Combine that with QAbstractItemView::rowsAboutToBeRemoved to remove cached information and you have a working solution. If you're picky about scrollbar size and position, you can compute average height based on a few sample elements in QAbstractItemView::rowsInserted and resize the rest accordingly without sizeHint.

    References:

    • RazrFalcon's answer for pointing me to the right direction
    • Answer with code sample to render HTML in QTableView: How to make item view render rich (html) text in Qt
    • Answer with code sample on detecting links in QTreeView: Hyperlinks in QTreeView without QLabel
    • QLabel's and internal Qt's QWidgetTextControl's source code on how to handle mouse click/move/release for links
    0 讨论(0)
提交回复
热议问题