如何制作一个快速的 QTableView,其中包含 HTML 格式和可点击的单元格?

16

我正在制作一个词典程序,它将单词定义以三列QTableView子类的形式显示出来,当用户键入它们时,从QAbstractTableModel子类中提取数据。就像这样:

Table and user input screenshot

我想对文本添加各种格式,我正在使用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速度很快,但我没有格式和添加链接(词典中的交叉引用)的能力。

如何使其更快?或者我可以使用哪个小部件来显示数据?

我的要求是:

  • 在约0.2秒内添加/删除大约1000行,其中一次只有约30行可见
  • 每个单元格内有可点击的多个内部链接(<a>?)(例如QLabel具有该功能,QItemDelegate可能很快,但我不知道如何获取我在那里单击的链接的信息)
  • 允许不同字体大小和颜色、换行以及不同单元格高度的格式
  • 我并不是特别坚定于使用 QTableView,任何看起来像可滚动表格并且符合Qt图形外观的东西都可以。

注意:

  • 我尝试使用HTML <table>制作一个带有单个标签,但它并没有更快。似乎QLabel不是正确的选择。
  • 样本中的数据由JMdict项目提供。
4个回答

21
我通过整合一些答案并查看Qt的内部解决了这个问题。
对于具有链接的静态html内容在 QTableView 中非常快速地工作的解决方案如下:
  • 子类化 QTableView 并在那里处理鼠标事件;
  • 子类化 QStyledItemDelegate,并在那里绘制html(与 RazrFalcon 的答案相反,非常快,因为仅有少量单元格是可见的,且仅对其调用了 paint() 方法);
  • 在子类化的 QStyledItemDelegate 中创建一个函数,通过QAbstractTextDocumentLayout::anchorAt() 找出哪个链接被点击。您无法自己创建 QAbstractTextDocumentLayout,但可以从 QTextDocument::documentLayout() 获取它,并且据 Qt 源代码保证不为 null。
  • 在子类化的 QTableView中根据鼠标是否悬停在链接上修改 QCursor 指针形状
以下是完整、可工作的 QTableViewQStyledItemDelegate 子类实现,用于绘制HTML并在链接悬停/激活时发送信号。委托和模型仍需在外部设置,如下所示:
wordTable->setModel(&myModel);
auto wordItemDelegate = new WordItemDelegate(this);
wordTable->setItemDelegate(wordItemDelegate); // or just choose specific columns/rows

WordView.h

->

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

=>

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());
}
请注意,此解决方案之所以快速,是因为一次仅呈现了一个小子集的行,因此没有同时呈现许多。一次自动调整所有行高或列宽仍然很慢。如果您需要该功能,可以使委托通知视图它绘制了某些内容,然后使视图调整高度/宽度(如果之前没有)。将其与结合使用以删除缓存信息,您就有了一个可行的解决方案。如果您对滚动条大小和位置挑剔,您可以基于中的几个示例元素计算平均高度,并相应地调整剩余部分而不使用。
参考资料:
- RazrFalcon的答案指导了我正确的方向。 - 在QTableView中呈现HTML代码示例的答案:How to make item view render rich (html) text in Qt - 在QTreeView中检测链接的代码示例的答案:Hyperlinks in QTreeView without QLabel - QLabel和内部Qt的QWidgetTextControl的源代码,用于处理链接的鼠标单击/移动/释放

10
感谢您提供这些代码示例,它帮助我在我的应用程序中实现了类似的功能。我正在使用Python 3和QT5,并且我想分享我的Python代码,以便在Python中实现此功能时有所帮助。
请注意,如果您使用QT Designer进行UI设计,则可以使用“promote”将常规的“QTableView”小部件更改为在使用“pyuic5”将XML转换为Python代码时自动使用您的自定义小部件。
以下是代码:
    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())

2
完全让我开心,我一直在寻找一个pyqt5的实现。谢谢你。 - Saelyth

2
在您的情况下,QLabel重新绘制速度较慢,而不是QTableView。另一方面,QTableView根本不支持格式化文本。
可能,您唯一的方法是创建自己的委托QStyledItemDelegate,并在其中进行自己的绘制和单击处理。
PS:是的,您可以在委托内部使用QTextDocument来呈现HTML,但这也会很慢。

但是在我渲染QStyledItemDelegate中的项目后,它们无法被点击。如何检测链接呈现的位置,以便我可以自己处理它? - Xilexio
1
再次强调,你可能需要通过将其分成块来简化格式。因此,当你绘制你的项目时,你可以计算这些块,例如QRect,并将其存储为类变量,然后在点击事件中,你可以轻松地检测点击位置及其数据。 但这只是我会做的事情。也许有更简单的解决方案。 - RazrFalcon
我猜如果我没有得到更好的答案,我会尝试那个方法。问题是处理自动换行和按顺序显示不同大小的文本会需要相当多的工作量。而我需要这个来找出我手动放置链接的位置。或者我可以做一个不太正规的解决方法,比如检查所点击区域的颜色是否具有唯一的链接颜色。 - Xilexio
1
对于单词换行,您可以使用QStaticText。它快速且可以获得结果QRect。而且它支持HTML格式。 - RazrFalcon
谢谢您指引我正确的方向。子类化QStyledItemDelegate是正确的途径。然而,手动绘制链接和计算它们的位置将会非常复杂,特别是当链接在换行文本的行内时。QTextDocument速度很快,并且具有内置的查找锚点的解决方案。此外,如果我需要选择文本,也可以帮助我使用hitTest。我仍然不知道为什么QLabel如此缓慢,但分析显示它与sizeHint有关,可能还与不断调用dataChanged有关。 - Xilexio

2

我使用了基于Xilexio代码的改进方案。以下是三个基本差异:

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

这是我的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();

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接