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

后端 未结 4 1112

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条回答
  •  梦毁少年i
    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 
    #include 
    #include 
    #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(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 
    
    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 
    #include 
    #include 
    #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

提交回复
热议问题