How to make item view render rich (html) text in Qt
Asked Answered
P

6

69

Suppose my model has items with the following string for Qt::DisplayRole

<span>blah-blah <b>some text</b> other blah</span>

I want QTreeView (actually, any item view) to render it like a rich text. Instead, item views render it like a pure text by default. How to achieve the desired rendering?


Actually, this is a search results model. User enters a text, some document is searched against that text and the user is presented with search results, where the words being searched should be bolder than surrounding text.

Phillips answered 24/12, 2009 at 3:41 Comment(3)
The Qt API is ludicrous. In 2019, this should be built-in functionality. When every Qt application that wants to format item text (...which, let's face it, is most of them) needs to manually reimplement non-trivial item delegates that no one appears to have successfully implemented in a general-purpose manner, something has gone profoundly awry.Khoisan
Note that this question was asked in the Qt4 period. Raven's answer is the best match for Qt5 (and later) users.Opheliaophelie
There's an open feature request for this in the official tracker. It might be worthwhile for those of us who want it to create an account there and vote for it. bugreports.qt.io/browse/QTBUG-14200Keith
P
28

My answer is mostly inspired by @serge_gubenko's one. However, there were made several improvements so that the code is finally useful in my application.

class HtmlDelegate : public QStyledItemDelegate
{
protected:
    void paint ( QPainter * painter, const QStyleOptionViewItem & option, const QModelIndex & index ) const;
    QSize sizeHint ( const QStyleOptionViewItem & option, const QModelIndex & index ) const;
};

void HtmlDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const
{
    QStyleOptionViewItemV4 optionV4 = option;
    initStyleOption(&optionV4, index);

    QStyle *style = optionV4.widget? optionV4.widget->style() : QApplication::style();

    QTextDocument doc;
    doc.setHtml(optionV4.text);

    /// Painting item without text
    optionV4.text = QString();
    style->drawControl(QStyle::CE_ItemViewItem, &optionV4, painter);

    QAbstractTextDocumentLayout::PaintContext ctx;

    // Highlighting text if item is selected
    if (optionV4.state & QStyle::State_Selected)
        ctx.palette.setColor(QPalette::Text, optionV4.palette.color(QPalette::Active, QPalette::HighlightedText));

    QRect textRect = style->subElementRect(QStyle::SE_ItemViewItemText, &optionV4);
    painter->save();
    painter->translate(textRect.topLeft());
    painter->setClipRect(textRect.translated(-textRect.topLeft()));
    doc.documentLayout()->draw(painter, ctx);
    painter->restore();
}

QSize HtmlDelegate::sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const
{
    QStyleOptionViewItemV4 optionV4 = option;
    initStyleOption(&optionV4, index);

    QTextDocument doc;
    doc.setHtml(optionV4.text);
    doc.setTextWidth(optionV4.rect.width());
    return QSize(doc.idealWidth(), doc.size().height());
}
Phillips answered 11/1, 2010 at 3:27 Comment(5)
note that the ctx.palette.setcolor section needs an additional nested if the account for optionV4.state being inactive. Otherwise when you move to another window the text becomes almost unreadable. Works great otherwise. ThanksDole
Text color note: Use else ctx.palette.setColor(QPalette::Text, optionV4.palette.color(QPalette::Active, QPalette::Text)); to make sure text color is properly set. Needed when using non-default text colors via stylesheet.Caslon
QTextDocument setup: If you add doc.setDocumentMargin(0); doc.setDefaultFont(optionV4.font); (add it both in paint & sizeHint) then the fonts will be correct when you change them via stylesheet. Also, the doc.setTextWidth call in the sizeHint routine doesn't seem to do anything. If you put it in both the sizeHint and the paint methods then you can have words disappear instead of being cut off when the item's column shrinks.Caslon
@Timo's comment below jbmohler's answer applies here, and is important for long text in a QListView: I'll copy it here. After line: doc.setHtml(optionV4.text), you need to set also doc.setTextWidth(optionV4.rect.width()), otherwise the delegate wont render longer content correctly in respect to target drawing area. For example does not wrap words in QListView.Burglary
This version does not seem to be able to handle the alignment that was specified for the item view. Instead this will always align top-left. If alignment is important to you, you could have a look at my answer: https://mcmap.net/q/279762/-how-to-make-item-view-render-rich-html-text-in-qtRetract
G
49

I guess you can use setItemDelegate method of the treeview to setup custom painter for your treeview items. In the delegate's paint method you can use QTextDocument to load item's text as html and render it. Please check if an example below would work for you:

treeview initialization:

...
    // create simple model for a tree view
    QStandardItemModel *model = new QStandardItemModel();
    QModelIndex parentItem;
    for (int i = 0; i < 4; ++i)
    {
        parentItem = model->index(0, 0, parentItem);
        model->insertRows(0, 1, parentItem);
        model->insertColumns(0, 1, parentItem);
        QModelIndex index = model->index(0, 0, parentItem);
        model->setData(index, "<span>blah-blah <b>some text</b> other blah</span>");
    }
    // create custom delegate
    HTMLDelegate* delegate = new HTMLDelegate();
    // set model and delegate to the treeview object
    ui->treeView->setModel(model);
    ui->treeView->setItemDelegate(delegate);
...

custom delegate implementation

class HTMLDelegate : public QStyledItemDelegate
{
protected:
    void paint ( QPainter * painter, const QStyleOptionViewItem & option, const QModelIndex & index ) const;
    QSize sizeHint ( const QStyleOptionViewItem & option, const QModelIndex & index ) const;
};

void HTMLDelegate::paint(QPainter* painter, const QStyleOptionViewItem & option, const QModelIndex &index) const
{
    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);

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

hope this helps, regards

update0: changes to HTMLDelegate to make icons visible and different pen color for selected items

void HTMLDelegate::paint(QPainter* painter, const QStyleOptionViewItem & option, const QModelIndex &index) const
{
    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);

    // shift text right to make icon visible
    QSize iconSize = options.icon.actualSize(options.rect.size());
    painter->translate(options.rect.left()+iconSize.width(), options.rect.top());
    QRect clip(0, 0, options.rect.width()+iconSize.width(), options.rect.height());

    //doc.drawContents(painter, clip);

    painter->setClipRect(clip);
    QAbstractTextDocumentLayout::PaintContext ctx;
    // set text color to red for selected item
    if (option.state & QStyle::State_Selected)
        ctx.palette.setColor(QPalette::Text, QColor("red"));
    ctx.clip = clip;
    doc.documentLayout()->draw(painter, ctx);

    painter->restore();
}
Gauvin answered 24/12, 2009 at 5:8 Comment(6)
Thanks for your reply. Actually, I was playing with overriding delegate and QTextDocument. However, there was a problem with items' size. Your answer pointed me to initStyleOption and widget->style()->drawControl. Your solution is excellent except two issues. 1. The text is being drawn over item icon 2. Selected item should have another text color. Trying to figure out how to fix them.Phillips
pls check the update0 for the original post; changes are in the HTMLDelegate::paint method. To make icons visible I just shifted text right to icon's width. As for the text color, I had to change the palette settings for the text color of the paint context object. Hope this is what you're looking for, regardsGauvin
@Anton did you figure out how to modify the selected text color?Crux
Works perfectly for tableWidgets, you're a hero.Electrothermal
How would I instruct QTextDocument to inherit the appplications stylesheet as default?Exultant
This version does not seem to be able to handle the alignment that was specified for the item view. Instead this will always align top-left. If alignment is important to you, you could have a look at my answer: https://mcmap.net/q/279762/-how-to-make-item-view-render-rich-html-text-in-qtRetract
P
28

My answer is mostly inspired by @serge_gubenko's one. However, there were made several improvements so that the code is finally useful in my application.

class HtmlDelegate : public QStyledItemDelegate
{
protected:
    void paint ( QPainter * painter, const QStyleOptionViewItem & option, const QModelIndex & index ) const;
    QSize sizeHint ( const QStyleOptionViewItem & option, const QModelIndex & index ) const;
};

void HtmlDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const
{
    QStyleOptionViewItemV4 optionV4 = option;
    initStyleOption(&optionV4, index);

    QStyle *style = optionV4.widget? optionV4.widget->style() : QApplication::style();

    QTextDocument doc;
    doc.setHtml(optionV4.text);

    /// Painting item without text
    optionV4.text = QString();
    style->drawControl(QStyle::CE_ItemViewItem, &optionV4, painter);

    QAbstractTextDocumentLayout::PaintContext ctx;

    // Highlighting text if item is selected
    if (optionV4.state & QStyle::State_Selected)
        ctx.palette.setColor(QPalette::Text, optionV4.palette.color(QPalette::Active, QPalette::HighlightedText));

    QRect textRect = style->subElementRect(QStyle::SE_ItemViewItemText, &optionV4);
    painter->save();
    painter->translate(textRect.topLeft());
    painter->setClipRect(textRect.translated(-textRect.topLeft()));
    doc.documentLayout()->draw(painter, ctx);
    painter->restore();
}

QSize HtmlDelegate::sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const
{
    QStyleOptionViewItemV4 optionV4 = option;
    initStyleOption(&optionV4, index);

    QTextDocument doc;
    doc.setHtml(optionV4.text);
    doc.setTextWidth(optionV4.rect.width());
    return QSize(doc.idealWidth(), doc.size().height());
}
Phillips answered 11/1, 2010 at 3:27 Comment(5)
note that the ctx.palette.setcolor section needs an additional nested if the account for optionV4.state being inactive. Otherwise when you move to another window the text becomes almost unreadable. Works great otherwise. ThanksDole
Text color note: Use else ctx.palette.setColor(QPalette::Text, optionV4.palette.color(QPalette::Active, QPalette::Text)); to make sure text color is properly set. Needed when using non-default text colors via stylesheet.Caslon
QTextDocument setup: If you add doc.setDocumentMargin(0); doc.setDefaultFont(optionV4.font); (add it both in paint & sizeHint) then the fonts will be correct when you change them via stylesheet. Also, the doc.setTextWidth call in the sizeHint routine doesn't seem to do anything. If you put it in both the sizeHint and the paint methods then you can have words disappear instead of being cut off when the item's column shrinks.Caslon
@Timo's comment below jbmohler's answer applies here, and is important for long text in a QListView: I'll copy it here. After line: doc.setHtml(optionV4.text), you need to set also doc.setTextWidth(optionV4.rect.width()), otherwise the delegate wont render longer content correctly in respect to target drawing area. For example does not wrap words in QListView.Burglary
This version does not seem to be able to handle the alignment that was specified for the item view. Instead this will always align top-left. If alignment is important to you, you could have a look at my answer: https://mcmap.net/q/279762/-how-to-make-item-view-render-rich-html-text-in-qtRetract
M
21

Here's the PyQt conversion of the combination of the above answers that worked for me. I would expect this to work virtually identically for PySide as well.

from PyQt4 import QtCore, QtGui

class HTMLDelegate(QtGui.QStyledItemDelegate):
    def paint(self, painter, option, index):
        options = QtGui.QStyleOptionViewItemV4(option)
        self.initStyleOption(options,index)

        style = QtGui.QApplication.style() if options.widget is None else options.widget.style()

        doc = QtGui.QTextDocument()
        doc.setHtml(options.text)

        options.text = ""
        style.drawControl(QtGui.QStyle.CE_ItemViewItem, options, painter);

        ctx = QtGui.QAbstractTextDocumentLayout.PaintContext()

        # Highlighting text if item is selected
        #if (optionV4.state & QStyle::State_Selected)
            #ctx.palette.setColor(QPalette::Text, optionV4.palette.color(QPalette::Active, QPalette::HighlightedText));

        textRect = style.subElementRect(QtGui.QStyle.SE_ItemViewItemText, options)
        painter.save()
        painter.translate(textRect.topLeft())
        painter.setClipRect(textRect.translated(-textRect.topLeft()))
        doc.documentLayout().draw(painter, ctx)

        painter.restore()

    def sizeHint(self, option, index):
        options = QtGui.QStyleOptionViewItemV4(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())
Millda answered 26/3, 2011 at 14:58 Comment(2)
What a hack! Ugh, but thanks. Highlighting: if options.state & QtGui.QStyle.State_Selected: ctx.palette.setColor(QtGui.QPalette.Text, options.palette.color(QtGui.QPalette.Active, QtGui.QPalette.HighlightedText))Cerracchio
After line: doc.setHtml(options.text), you need to set also doc.setTextWidth(option.rect.width()), otherwise the delegate wont render longer content correctly in respect to target drawing area. For example does not wrap words in QListView.Feudatory
R
6

Writing up yet another answer for how this can be done in C++. The difference to the answers provided so far is that this is for Qt5 and not Qt4. Most importantly however the previous answers neglected that the item delegate should be able to align the text as specified (e.g. in a QTreeWidget). Additionally I also implemented a way to elide rich text in order to get a consistent feeling with plaintext delegates (in ItemViews).

So without further ado, here is my code for a RichTextDelegate:

void RichTextItemDelegate::paint(QPainter *painter, const QStyleOptionViewItem &inOption,
                                 const QModelIndex &index) const {
    QStyleOptionViewItem option = inOption;
    initStyleOption(&option, index);

    if (option.text.isEmpty()) {
        // This is nothing this function is supposed to handle
        QStyledItemDelegate::paint(painter, inOption, index);

        return;
    }

    QStyle *style = option.widget ? option.widget->style() : QApplication::style();

    QTextOption textOption;
    textOption.setWrapMode(option.features & QStyleOptionViewItem::WrapText ? QTextOption::WordWrap
                                                                            : QTextOption::ManualWrap);
    textOption.setTextDirection(option.direction);

    QTextDocument doc;
    doc.setDefaultTextOption(textOption);
    doc.setHtml(option.text);
    doc.setDefaultFont(option.font);
    doc.setDocumentMargin(0);
    doc.setTextWidth(option.rect.width());
    doc.adjustSize();

    if (doc.size().width() > option.rect.width()) {
        // Elide text
        QTextCursor cursor(&doc);
        cursor.movePosition(QTextCursor::End);

        const QString elidedPostfix = "...";
        QFontMetrics metric(option.font);
#if QT_VERSION >= QT_VERSION_CHECK(5, 11, 0)
        int postfixWidth = metric.horizontalAdvance(elidedPostfix);
#else
        int postfixWidth = metric.width(elidedPostfix);
#endif
        while (doc.size().width() > option.rect.width() - postfixWidth) {
            cursor.deletePreviousChar();
            doc.adjustSize();
        }

        cursor.insertText(elidedPostfix);
    }

    // Painting item without text (this takes care of painting e.g. the highlighted for selected
    // or hovered over items in an ItemView)
    option.text = QString();
    style->drawControl(QStyle::CE_ItemViewItem, &option, painter, inOption.widget);

    // Figure out where to render the text in order to follow the requested alignment
    QRect textRect = style->subElementRect(QStyle::SE_ItemViewItemText, &option);
    QSize documentSize(doc.size().width(), doc.size().height()); // Convert QSizeF to QSize
    QRect layoutRect = QStyle::alignedRect(Qt::LayoutDirectionAuto, option.displayAlignment, documentSize, textRect);

    painter->save();

    // Translate the painter to the origin of the layout rectangle in order for the text to be
    // rendered at the correct position
    painter->translate(layoutRect.topLeft());
    doc.drawContents(painter, textRect.translated(-textRect.topLeft()));

    painter->restore();
}

QSize RichTextItemDelegate::sizeHint(const QStyleOptionViewItem &inOption, const QModelIndex &index) const {
    QStyleOptionViewItem option = inOption;
    initStyleOption(&option, index);

    if (option.text.isEmpty()) {
        // This is nothing this function is supposed to handle
        return QStyledItemDelegate::sizeHint(inOption, index);
    }

    QTextDocument doc;
    doc.setHtml(option.text);
    doc.setTextWidth(option.rect.width());
    doc.setDefaultFont(option.font);
    doc.setDocumentMargin(0);

    return QSize(doc.idealWidth(), doc.size().height());
}
Retract answered 28/2, 2021 at 19:6 Comment(7)
Note that doc.setDocumentMargin(1); does better resemble the default spacing between items.Opheliaophelie
Note that text elision is slow for large trees.Opheliaophelie
Strange... in my case (this is PyQt5) option.text is always an empty string, and the (marked-up) text to be displayed is found in index.data(), so I have to omit that if clause there.Sadesadella
option.rect.width() is either empty or the current grid size, in a QListView, so this won't adapt to a growing QListView.Dygall
The code is not working correctly when a qtreeview has the option wordwrap enabled, any idea in how i could fix it?Zanezaneski
@Natalia no idea, sorry. Maybe try removing the code that does the text eliding - perhaps that's enough to get the wrapping behavior?Retract
Even removing it, it still draw the wrapped text wrongZanezaneski
C
5

This one is in PySide. Rather than doing a lot of custom drawing, I pass the QPainter to the QLabel and make it draw itself. Highlighting code borrowed from other answers.

from PySide import QtGui

class TaskDelegate(QtGui.QItemDelegate):
    #https://doc.qt.io/archives/qt-4.7/qitemdelegate.html#drawDisplay
    #https://doc.qt.io/archives/qt-4.7/qwidget.html#render
    def drawDisplay(self, painter, option, rect, text):
        label = QtGui.QLabel(text)

        if option.state & QtGui.QStyle.State_Selected:
            p = option.palette
            p.setColor(QtGui.QPalette.WindowText, p.color(QtGui.QPalette.Active, QtGui.QPalette.HighlightedText))

            label.setPalette(p)

        label.render(painter, rect.topLeft(), renderFlags=QtGui.QWidget.DrawChildren)
Cerracchio answered 7/11, 2011 at 12:41 Comment(2)
Didn't work for me, I only see a very small part of the text, randomly, and one some entries.Leggett
For those who need it: I modified @Cerracchio answer a bit to also cover multiline labels in https://mcmap.net/q/281828/-how-to-format-the-list-items-of-qcompleter-39-s-popup-list-properlyLennox
S
0

Just a slight update from jbmohler's answer, for PyQt5: some classes have apparently been shifted to QtWidgets.

This is way beyond my paygrade (i.e. knowledge of the nuts and bolts behind PyQt5).

I echo the sentiment expressed in Cecil Curry's comment to the question. It is now 2021, and we appear still to have to struggle with this sort of hack. Ridiculous. I've been impressed by Qt5 to date, as compared to JavaFX for example. This deficiency is a let-down.

    class HTMLDelegate( QtWidgets.QStyledItemDelegate ):
        def __init__( self ):
            super().__init__()
            # probably better not to create new QTextDocuments every ms
            self.doc = QtGui.QTextDocument()
    
        def paint(self, painter, option, index):
            options = QtWidgets.QStyleOptionViewItem(option)
            self.initStyleOption(options, index)
            painter.save()
            self.doc.setTextWidth(options.rect.width())                
            self.doc.setHtml(options.text)
            self.doc.setDefaultFont(options.font)
            options.text = ''
            options.widget.style().drawControl(QtWidgets.QStyle.CE_ItemViewItem, options, painter)
            painter.translate(options.rect.left(), options.rect.top())
            clip = QtCore.QRectF(0, 0, options.rect.width(), options.rect.height())
            painter.setClipRect(clip)
            ctx = QtGui.QAbstractTextDocumentLayout.PaintContext()
            ctx.clip = clip
            self.doc.documentLayout().draw(painter, ctx)
            painter.restore()
    
        def sizeHint( self, option, index ):
            options = QtWidgets.QStyleOptionViewItem(option)
            self.initStyleOption(option, index)
            self.doc.setHtml(option.text)
            self.doc.setTextWidth(option.rect.width())
            return QtCore.QSize(self.doc.idealWidth(), self.doc.size().height())
Sadesadella answered 7/2, 2021 at 18:39 Comment(0)

© 2022 - 2025 — McMap. All rights reserved.