I solved the problem by putting together few answers and looking at Qt's internals.
A solution which works very fast for static html content with links in QTableView
is as folows:
- Subclass
QTableView
and handle mouse events there;
- Subclass
QStyledItemDelegate
and paint the html there (contrary to RazrFalcon's answer, it is very fast, as only a small amount of cells is visible at a time and only those have paint()
method called);
- In subclassed
QStyledItemDelegate
create a function that figures out which link was clicked by QAbstractTextDocumentLayout::anchorAt()
. You cannot create QAbstractTextDocumentLayout
yourself, but you can get it from QTextDocument::documentLayout()
and, according to Qt source code, it's guaranteed to be non-null.
- In subclassed
QTableView
modify QCursor
pointer shape accordingly to whether it's hovering over a link
Below is a complete, working implementation of QTableView
and QStyledItemDelegate
subclasses that paint the HTML and send signals on link hover/activation. The delegate and model still have to be set outside, as follows:
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 fast only because a small subset of rows is rendered at once, and therefore not many QTextDocument
s are rendered at once. Automatic adjusting all row heights or column widths at once will still be slow. If you need that functionality, you can make the delegate inform the view that it painted something and then making the view adjust the height/width if it hasn't before. Combine that with QAbstractItemView::rowsAboutToBeRemoved
to remove cached information and you have a working solution. If you're picky about scrollbar size and position, you can compute average height based on a few sample elements in QAbstractItemView::rowsInserted
and resize the rest accordingly without sizeHint
.
References: