如何使用 HTML 格式和可点击的单元格制作快速的 QTableView?

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

我正在制作一个字典程序,当用户键入它们时,它在 3 列 QTableView 子类中显示单词定义,从 QAbstractTableModel 子类中获取数据。类似的东西:

我想为文本添加各种格式,我正在使用 QAbstractItemView::setIndexWidget 在数据输入时向每个单元格添加 QLabel:

WordView.h

#include <QTableView>

class QLabel;

class WordView : public QTableView {
    Q_OBJECT

public:
    explicit WordView(QWidget *parent = 0);

    void rowsInserted(const QModelIndex &parent, int start, int end);

private:
    void insertLabels(int row);
    void removeLabels(int row);
};

WordView.cpp

#include <QLabel>
#include "WordView.h"

WordView::WordView(QWidget *parent) :
    QTableView(parent)
{}

void WordView::rowsInserted(const QModelIndex &parent, int start, int end) {
    QTableView::rowsInserted(parent, start, end);

    for (int row = start; row <= end; ++row) {
        insertLabels(row);
    }
}

void WordView::insertLabels(int row) {
    for (int i = 0; i < 3; ++i) {
        auto label = new QLabel(this);
        label->setTextFormat(Qt::RichText);
        label->setAutoFillBackground(true);
        QModelIndex ix = model()->index(row, i);
        label->setText(model()->data(ix, Qt::DisplayRole).toString()); // this has HTML
        label->setWordWrap(true);
        setIndexWidget(ix, label); // this calls QAbstractItemView::dataChanged
    }
}

但是,这非常慢 - 像这样刷新 100 行(删除所有行,然后添加 100 行)大约需要 1 秒。使用原始 QTableView,它工作得很快,但我没有格式化和添加 links(字典中的交叉引用)的能力。 如何让它更快?或者我可以使用什么其他小部件来显示该数据?

我的要求是:

备注:

在你的情况下,QLabel(重新)绘制很慢,而不是 QTableView。 另一方面,QTableView 根本不支持格式化文本。

可能,你唯一的方法是创建你自己的委托,QStyledItemDelegate,并在其中制作你自己的绘画和点击处理。

PS: 是的,您可以使用 QTextDocument 在委托中呈现 html,但它也会很慢。

我通过整理几个答案并查看 Qt 的内部结构解决了这个问题。

对于 QTableView 中具有 links 的静态 html 内容非常快速的解决方案如下:

  • 子类 QTableView 并在那里处理鼠标事件;
  • 子类 QStyledItemDelegate 并在那里绘制 html (与 RazrFalcon 的回答相反,它非常快,因为一次只能看到少量的单元格并且只有那些 paint() 调用的方法);
  • 在子类 QStyledItemDelegate 中创建一个函数,计算出 QAbstractTextDocumentLayout::anchorAt() 单击了哪个 link。您不能自己创建 QAbstractTextDocumentLayout,但您可以从 QTextDocument::documentLayout() 获取它,并且根据 Qt 源代码,它保证是非空的。
  • 在子类 QTableView 中根据指针是否悬停在 link
  • 上相应地修改 QCursor 指针形状

下面是 QTableViewQStyledItemDelegate 子类的完整有效实现,它们绘制 HTML 并在 link hover/activation 上发送信号。 delegate和model还是要在外面设置,如下:

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());
}

请注意,此解决方案之所以快,只是因为一次呈现了一小部分行,因此一次呈现的 QTextDocument 并不多。一次自动调整所有行高或列宽仍然很慢。如果你需要那个功能,你可以让委托通知视图它画了一些东西,然后让视图调整 height/width 如果它之前没有。将其与 QAbstractItemView::rowsAboutToBeRemoved 结合使用以删除缓存的信息,您就有了一个可行的解决方案。如果您对滚动条的大小和位置很挑剔,您可以根据 QAbstractItemView::rowsInserted 中的一些示例元素计算平均高度,并相应地调整其余部分的大小,而无需 sizeHint.

参考文献:

  • RazrFalcon 的回答为我指明了正确的方向
  • 用代码示例回答以在 QTableView 中呈现 HTML:How to make item view render rich (html) text in Qt
  • 关于在 QTreeView 中检测 links 的代码示例的回答:Hyperlinks in QTreeView without QLabel
  • QLabel 和内部 Qt QWidgetTextControl 关于如何处理鼠标 click/move/release for links
  • 的源代码

我使用了基于 Xilexio 代码的稍微改进的解决方案。有 3 个根本区别:

  • 垂直对齐,因此如果您将文本放在高于文本的单元格中,它将居中对齐而不是顶部对齐。
  • 如果单元格包含图标,文本将右移,因此图标不会显示在文本上方。
  • 将遵循突出显示单元格的小部件样式,因此您 select 这个单元格的颜色将与没有委托的其他单元格类似。

这是我的 paint() 函数代码(其余代码保持不变)。

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();

非常感谢这些代码示例,它帮助我在我的应用程序中实现了类似的功能。我正在使用 Python 3 和 QT5,我想分享我的 Python 代码,如果在 Python.

中实现它可能会有所帮助

请注意,如果您使用 QT Designer 进行 UI 设计,您可以使用“升级”将常规“QTableView”小部件更改为在转换 XML 时自动使用您的自定义小部件Python 使用“pyuic5”编码。

代码如下:

    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())