Как сделать быстрый QTableView с HTML-форматированными и кликабельными ячейками?

я делаю программу словаря, которая отображает определения слов в 3-столбце QTableView подкласс, как пользователь вводит их, взяв данные из QAbstractTableModel подкласс. Что-то вроде этого:--13-->

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
    }
}

однако, это очень медленно - это занимает около 1 секунды, чтобы обновить 100 строк (удалить все, а затем добавить 100 новых), как это. С оригинальным QTableView он работал быстро, но у меня не было форматирования и возможности добавлять ссылки (перекрестные ссылки в словаре). как сделать это гораздо быстрее? Или какой другой виджет я могу использовать для отображения этих данных?

мои требования:

  • добавление / удаление около 1000 строк в ~0.2 s, где около 30 будет видно на после
  • кликабельные, несколько внутренних ссылок (<a>?) в каждой ячейке (например,QLabel есть, QItemDelegate может быть, быстро, но я не знаю, как получить информацию, по какой ссылке я нажал там)
  • форматирование, которое позволяет различные размеры шрифта и цвета, перенос слов, различные высоты ячеек
  • я на самом деле не зациклен на QTableView, все, что выглядит как прокручиваемая таблица и выглядит в соответствии с графикой Qt ладно!--33-->

Примечания:

  • я попытался сделать одну метку с HTML <table> вместо этого, но это было не намного быстрее. Кажется QLabel это не тот путь.
  • данные в образце любезно предоставлены проектом JMdict.

4 ответов


я решил проблему, собрав несколько ответов и посмотрев на внутренние компоненты Qt.

решение, которое работает очень быстро для статического содержимого html со ссылками в QTableView как folows:

  • подкласс QTableView и там обрабатывать события мыши;
  • подкласс QStyledItemDelegate и нарисуйте html там (вопреки ответу RazrFalcon, это очень быстро, так как за раз видно только небольшое количество ячеек, и только те имеют paint() способ звали);
  • в подклассы QStyledItemDelegate создайте функцию, которая выясняет, какая ссылка была нажата QAbstractTextDocumentLayout::anchorAt(). Нельзя создать QAbstractTextDocumentLayout себя, но вы можете получить его от QTextDocument::documentLayout() и, согласно исходному коду Qt, он гарантированно будет иметь значение null.
  • в подклассы QTableView изменить QCursor форма указателя соответственно тому, зависает ли он над ссылкой

Ниже приведена полная, рабочая реализация QTableView и QStyledItemDelegate подклассы раскрасьте HTML и отправьте сигналы по ссылке hover / activation. Делегат и модель по-прежнему должны быть установлены снаружи, следующим образом:

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

обратите внимание, что это решение быстро только потому, что небольшое подмножество строк отображается сразу, и поэтому не много QTextDocuments отображаются сразу. Автоматическая регулировка все высоты строк или ширины столбцов сразу по-прежнему будут медленными. Если вам нужна эта функциональность, вы можете заставить делегата сообщить представлению, что он что-то нарисовал, а затем настроить высоту/ширину, если это не было раньше. Объедините это с QAbstractItemView::rowsAboutToBeRemoved удалить кэшированную информацию и у вас есть рабочее решение. Если вы придирчивы к размеру и положению полосы прокрутки, вы можете вычислить среднюю высоту на основе нескольких элементов выборки в QAbstractItemView::rowsInserted и измените размер остальных соответственно без sizeHint.

ссылки:


в вашем случае картина QLabel (re)медленная, а не QTableView. С другой стороны, QTableView вообще не поддерживает форматированный текст.

вероятно, ваш единственный способ-создать свой собственный делегат, QStyledItemDelegate, и сделать свою собственную картину и нажмите "обработка" в ней.

PS: да, вы можете использовать QTextDocument для рендеринга html внутри делегата, но он также будет медленным.


Я использую пренебрежительное улучшенное решение на основе кода Xilexio. Есть 3 принципиальных отличия:

  • вертикальное выравнивание поэтому, если вы поместите текст в ячейку выше текста, он будет выровнен по центру, а не сверху.
  • текст будет смещен вправо, если ячейка содержит значок, поэтому значок не будет отображаться над текстом.
  • стиль виджета для выделенных ячеек будет следовать, поэтому вы выбираете эту ячейку, цвета будут вести себя аналогично другим ячейкам без делегата.

вот мой код функции 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();

большое спасибо за эти примеры кода, это помогло мне реализовать аналогичный functionalaity в моем приложении. Я работаю с Python 3 и QT5, и я хотел бы поделиться своим кодом Python, если это может быть полезно для реализации этого в Python.

обратите внимание, что если вы используете Qt Designer для дизайна пользовательского интерфейса, вы можете использовать "promote" для изменения обычного виджета "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())