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

Xil*_*xio 10 c++ qt qtableview qt5 qtstylesheets

我正在制作一个字典程序,在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);
};
Run Code Online (Sandbox Code Playgroud)

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
    }
}
Run Code Online (Sandbox Code Playgroud)

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

我的要求是:

  • 在~0.2s内添加/删除大约1000行,其中大约30行将同时可见
  • <a>每个单元格中都有可点击的多个内部链接(?)(例如QLabel,那个,QItemDelegate可能很快,但我不知道如何获取我点击那里的链接的信息)
  • 格式允许不同的字体大小和颜色,自动换行,不同的单元格高度
  • 我不是真的死机QTableView,看起来像一个可滚动的表,看起来与Qt图形一致是可以的

笔记:

  • 我尝试用HTML制作单个标签<table>,但速度并不快.似乎QLabel不是要走的路.
  • 样本中的数据由JMdict项目提供.

Xil*_*xio 12

我通过汇总几个答案并查看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
Run Code Online (Sandbox Code Playgroud)

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;
};
Run Code Online (Sandbox Code Playgroud)

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();
}
Run Code Online (Sandbox Code Playgroud)

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;
};
Run Code Online (Sandbox Code Playgroud)

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());
}
Run Code Online (Sandbox Code Playgroud)

请注意,此解决方案很快,因为一次只渲染一小部分行,因此QTextDocument一次渲染的s 不多.一次自动调整所有行高或列宽仍然会很慢.如果您需要该功能,您可以让委托通知视图它绘了一些东西,然后让视图调整高度/宽度(如果之前没有).结合使用它QAbstractItemView::rowsAboutToBeRemoved来删除缓存的信息,你就有了一个可行的解决方案.如果您对滚动条的大小和位置感到挑剔,可以根据几个样本元素计算平均高度,QAbstractItemView::rowsInserted并相应地调整其余部分sizeHint.

参考文献:


ake*_*j74 6

非常感谢这些代码示例,它帮助我在我的应用程序中实现了类似的功能。我正在使用 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())
Run Code Online (Sandbox Code Playgroud)

  • 完全是我的一天,我正在为此寻找 pyqt5 实现。谢谢你。 (2认同)