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);
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: