Как сделать быстрый QTableView с HTML-форматированными и кликабельными ячейками?
я делаю программу словаря, которая отображает определения слов в 3-столбце QTableView
подкласс, как пользователь вводит их, взяв данные из QAbstractTableModel
подкласс. Что-то вроде этого:--13-->
я хочу добавить в текст различные форматирования, я использую 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());
}
обратите внимание, что это решение быстро только потому, что небольшое подмножество строк отображается сразу, и поэтому не много QTextDocument
s отображаются сразу. Автоматическая регулировка все высоты строк или ширины столбцов сразу по-прежнему будут медленными. Если вам нужна эта функциональность, вы можете заставить делегата сообщить представлению, что он что-то нарисовал, а затем настроить высоту/ширину, если это не было раньше. Объедините это с QAbstractItemView::rowsAboutToBeRemoved
удалить кэшированную информацию и у вас есть рабочее решение. Если вы придирчивы к размеру и положению полосы прокрутки, вы можете вычислить среднюю высоту на основе нескольких элементов выборки в QAbstractItemView::rowsInserted
и измените размер остальных соответственно без sizeHint
.
ссылки:
- RazrFalcon для указывая мне в правильном направлении
- ответ с образцом кода для отображения HTML в QTableView:как сделать вид элемента render rich (html) текст в Qt
- ответ с образцом кода при обнаружении ссылок в
QTreeView
: гиперссылки в QTreeView без QLabel -
QLabel
и внутренние QtQWidgetTextControl
исходный код о том, как обращаться щелчок мыши / перемещение / освобождение для ссылок
в вашем случае картина 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())