How to quickly create a QTableView with HTML formatted and interactive cells?

I am making a dictionary program that displays word definitions in a QTableView subclass with three columns as the user types them by taking data from a QAbstractTableModel subclass. Something like that:

Screenshot of table and user

I want to add different formatting to the text, I use QAbstractItemView::setIndexWidget to add a QLabel to each cell when entering data:

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

However, this is very slow - it takes about 1 second to update 100 rows (delete all and then add 100 new ones). It worked quickly with the original QTableView, but I did not have the formatting or the ability to add links (cross-references in the dictionary). How to do it much faster? Or what other widget can I use to display this data?

My requirements:

  • Add / delete about 1000 lines in ~ 0.2 s, where about 30 will be visible immediately
  • Several internal links are available in each cell ( <a> ?) (For example, QLabel has that QItemDelegate could be fast, but I don’t know how to get the information that I clicked on the link) / li>
  • Formatting that allows you to use different sizes and colors of fonts, word wrap, different cell heights.
  • I am not really configured for QTableView , everything that looks like a scrollable table and looks in accordance with the Qt graphics, in order

Notes:

  • I tried making a single label with HTML <table> , but it was not much faster. QLabel doesn't seem to be the way to go.
  • Data in the sample provided by the JMdict project.
+5
source share
4 answers

I solved the problem by collecting some answers and looking at the insides of Qt.

A solution that works very fast for static html content with links in a QTableView is as follows:

  • Subclass QTableView and handle mouse events there;
  • Subclass QStyledItemDelegate and draw the html there (contrary to the RazrFalcon answer, it is very fast, since only a small number of cells are visible at a time, and only those that call the paint() method);
  • In the QStyledItemDelegate subclass QStyledItemDelegate create a function that determines which link was clicked by QAbstractTextDocumentLayout::anchorAt() . You cannot create a QAbstractTextDocumentLayout yourself, but you can get it from QTextDocument::documentLayout() and, according to the Qt source code, it is guaranteed to be non-zero.
  • In a QTableView subclass, change the shape of the QCursor pointer to match whether it will hang by reference

Below is a complete, working implementation of the QTableView and QStyledItemDelegate , which draw HTML and send signals when hovering / activating links. The delegate and model must still be installed externally as shown below:

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

Note that this solution is quick to execute just because a small subset of the lines is displayed immediately, and therefore there are not many QTextDocument images. Automatically adjusting all row heights or column widths at the same time will be slow. If you need this functionality, you can force the delegate to report the view that he has drawn something, and then adjust the height / width view if it was not there before. Combine this with QAbstractItemView::rowsAboutToBeRemoved to remove cached information, and you have a working solution. If you are picky about the size and position of the scroll bar, you can calculate the average height based on several selection elements in QAbstractItemView::rowsInserted and resize the rest accordingly without sizeHint .

Literature:

+6
source

In your case, the QLabel (re) picture is slow, not a QTableView. QTableView, on the other hand, does not support rich text at all.

Perhaps your only way is to create your own QStyledItemDelegate delegate and do your own image and click processing.

PS: yes, you can use QTextDocument to render html inside the delegate, but it will be slow too.

+2
source

I use a weak, improved solution based on Xilexio code. There are 3 fundamental differences:

  • Vertical alignment, so if you put text in a cell above the text, it will be centered and not aligned in height.
  • The text will be shifted to the right if the cell contains an icon so that the icon does not appear above the text.
  • The widget style will be executed for the selected cells, so you select this cell, the colors will behave similarly to other cells without a delegate.

Here is my paint () function code (the rest of the code remains the same).

 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(); 
+2
source

Thanks a lot for these code examples, it helped me implement a similar function in my application. I work with Python 3 and QT5 and I would like to share my Python code, maybe it will be useful to implement this in Python.

Please note that if you use QT Designer for user interface design, you can use “promote” to change the regular “QTableView” widget to automatically use your custom widget when converting XML to Python using “pyuic5”.

Enter the code as follows:

 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()) 
+1
source

Source: https://habr.com/ru/post/1242999/


All Articles