0

I am attempting to use a QStyledItemDelegate with my QListView to display rich text in the items.

The first time the item is painted its height is too small. If I then mouse over the item it gets repainted with the correct height. Below are screenshots of the initial paint and repaint.

How can I get the initial paint to be the right height?

Initial height is too small. After first repaint the height is just right.

Example code that demonstrates the issue:

from PySide2 import QtCore, QtGui, QtWidgets


class RichTextItemDelegate(QtWidgets.QStyledItemDelegate):
    def __init__(self, parent=None):
        super(RichTextItemDelegate, self).__init__(parent)
        self.doc = QtGui.QTextDocument(self)

    def paint(self, painter, option, index):
        painter.save()

        self.initStyleOption(option, index)
        self.doc.setHtml(option.text)

        option_no_text = QtWidgets.QStyleOptionViewItem(option)
        option_no_text.text = ''
        style = QtWidgets.QApplication.style() if option_no_text.widget is None else option_no_text.widget.style()
        style.drawControl(QtWidgets.QStyle.CE_ItemViewItem, option_no_text, painter)

        margin_top = (option.rect.height() - self.doc.size().height()) // 2
        text_rect = style.subElementRect(QtWidgets.QStyle.SE_ItemViewItemText, option_no_text, None)
        text_rect.setTop(text_rect.top() + margin_top)

        painter.translate(text_rect.topLeft())
        painter.setClipRect(text_rect.translated(-text_rect.topLeft()))

        context = QtGui.QAbstractTextDocumentLayout.PaintContext()
        self.doc.documentLayout().draw(painter, context)

        painter.restore()

    def sizeHint(self, option, index):
        other = super().sizeHint(option, index)
        w = min(self.doc.idealWidth(), other.width())
        h = max(self.doc.size().height(), other.height())
        return QtCore.QSize(w, h)


class ExampleWidget(QtWidgets.QWidget):
    def __init__(self):
        super().__init__()

        item = QtGui.QStandardItem()
        item.setText('Example<br><span style="font-size: 14pt; font-weight: bold;">Example<br>Example<br>Example</span>', )

        model = QtGui.QStandardItemModel()
        model.appendRow(item)

        self.listview = QtWidgets.QListView(parent=self)
        self.listview.setModel(model)

        delegate = RichTextItemDelegate(self.listview)
        self.listview.setItemDelegate(delegate)


app = QtWidgets.QApplication([])
example = ExampleWidget()
example.resize(320, 240)
example.show()
app.exec_()
eyllanesc
  • 235,170
  • 19
  • 170
  • 241
sourcenouveau
  • 29,356
  • 35
  • 146
  • 243
  • I do not reproduce your problem, you could indicate the characteristics of your environment: python version, pyside2 version, OS, etc. – eyllanesc Aug 17 '20 at 16:39
  • Windows 10, Python 3.7.6, PySide2 5.15.0. – sourcenouveau Aug 17 '20 at 16:40
  • I see `sizeHint()` get called 5 times before `paint()` gets called... I just don't know what to do about that. – sourcenouveau Aug 17 '20 at 16:49
  • mmm, it seems to be a bug, what do you get by adding `print(w, h)`? Try setting the delegate before setting the model – eyllanesc Aug 17 '20 at 16:53
  • Moving `setItemDelegate` has no effect. I see a call sequence like: `sizeHint: 8.0, 24.0; paint; sizeHint: 107.0, 108.0;`. Since `self.doc.setHtml` doesn't get called until `paint` I think that is what I would expect. I wonder how I can set the doc text before paint is called. – sourcenouveau Aug 17 '20 at 17:25

1 Answers1

0

So I believe the initial paint was sized wrong because the sizeHint() function can't return good values until it knows what the item text is. In my original code the text wasn't set until paint() was called.

I used the following for sizeHint() and it seems to work:

def sizeHint(self, option: QtWidgets.QStyleOptionViewItem, index: QtCore.QModelIndex) -> QtCore.QSize:
    self.doc.setHtml(index.data())
    doc_size = self.doc.size()
    doc_width = int(math.ceil(doc_size.width()))
    doc_height = int(math.ceil(doc_size.height()))

    item_size = super().sizeHint(option, index)

    w = min(doc_width, item_size.width())
    h = max(doc_height, item_size.height())

    return QtCore.QSize(w, h)

Be warned... I do not know what the implications of calling self.doc.setHtml() inside of sizeHint() are. Also, for some reason super().sizeHint() returns a very large width value for me, and I'm not sure why, which is why I call min() instead of max().

sourcenouveau
  • 29,356
  • 35
  • 146
  • 243