pythonpyqtpyqt5qstyleditemdelegate

QStyledItemDelegate's option is not updating


I ran into a problem while using PyQt5. I have a list with QStyledItemDelegate class painting its items. Here is the minimal reproducible example:

import sys
from PyQt5.QtCore import (
    QAbstractListModel,
    Qt,
    QSize,
    QRect,
    QRectF,
)
from PyQt5.QtGui import (
    QPainter,
    QFontMetrics,
    QFont,
    QTextDocument,
    QTextOption,
    QPen,
)
from PyQt5.QtWidgets import (
    QApplication,
    QListView,
    QMainWindow,
    QStyledItemDelegate,
)

window_width = 0

class MessageDelegate(QStyledItemDelegate):
    WINDOW_PADDING = 30
    font = QFont("Times", 14)

    def __init__(self, *args, **kwargs):
        super(MessageDelegate, self).__init__(*args, **kwargs)

    def paint(self, painter, option, index):
        msg = index.model().data(index, Qt.DisplayRole)
        print("paint " + str(index.row()) + " " + str(option.rect.top()))
        field = QRect(option.rect)
        doc = QTextDocument(msg)
        doc.setDocumentMargin(0)
        opt = QTextOption()
        opt.setWrapMode(opt.WrapAtWordBoundaryOrAnywhere)
        doc.setDefaultTextOption(opt)
        doc.setDefaultFont(self.font)
        doc.setTextWidth(field.size().width())
        field.setHeight(int(doc.size().height()))
        field.setWidth(int(doc.idealWidth()))
        painter.setPen(Qt.gray)
        painter.setFont(self.font)
        painter.translate(field.x(), field.y())
        textrectf = QRectF(field)
        textrectf.moveTo(0, 0)
        doc.drawContents(painter, textrectf)
        painter.translate(-field.x(), -field.y())

    def sizeHint(self, option, index):
        global window_width
        msg = index.model().data(index, Qt.DisplayRole)
        doc = QTextDocument(msg)
        doc.setDocumentMargin(0)
        opt = QTextOption()
        opt.setWrapMode(opt.WrapAtWordBoundaryOrAnywhere)
        doc.setDefaultTextOption(opt)
        doc.setDefaultFont(self.font)
        doc.setTextWidth(window_width - self.WINDOW_PADDING)
        print("sizeHint " + str(index.row()) + " " + str(int(doc.size().height())))
        return QSize(0, int(doc.size().height()))


class MessageModel(QAbstractListModel):
    def __init__(self, *args, **kwargs):
        super(MessageModel, self).__init__(*args, **kwargs)
        self.messages = []

    def data(self, index, role):
        if role == Qt.DisplayRole:
            return self.messages[index.row()]

    def rowCount(self, index):
        return len(self.messages)

    def add_message(self, text):
        if text:
            self.messages.append(text)
            self.layoutChanged.emit()


class Dialog(QMainWindow):

    def __init__(self):
        global window_width
        super(Dialog, self).__init__()
        self.setMinimumSize(int(QApplication.primaryScreen().size().width() * 0.1), int(QApplication.primaryScreen().size().height() * 0.2))
        self.resize(int(QApplication.primaryScreen().size().width() * 0.3), int(QApplication.primaryScreen().size().height() * 0.5))
        window_width = int(QApplication.primaryScreen().size().width() * 0.3)
        self.messages = QListView()
        self.messages.setItemDelegate(MessageDelegate())
        self.model = MessageModel()
        self.messages.setModel(self.model)
        self.model.add_message("qwerty qwerty qwerty qwerty qwerty qwerty qwerty qwerty qwerty qwerty qwerty qwerty qwerty qwerty")
        self.model.add_message("abcdef")
        self.setCentralWidget(self.messages)

    def resizeEvent(self, event):
        global window_width
        super(Dialog, self).resizeEvent(event)
        window_width = self.size().width()

app = QApplication(sys.argv)
window = Dialog()
window.show()
app.exec_()

As you can see I am printing the height of each item before returning it in sizeHint. I also print the Y coordinate of option.rect received in paint. As I have only two items, I expect the coordinate of the item1 to be equal to the height of item0. And at first it seems to be working out:

sizeHint 0 23
paint 0 0
sizeHint 1 23
paint 1 23

However, as I narrow down the window the height in sizeHint starts to grow (because the narrow window can't fit all the contents) but the Y coordinate of option.rect stays the same:

sizeHint 0 46
paint 0 0
sizeHint 1 23
paint 1 23

Even when I get to the third line the position of option.rect is not updating:

sizeHint 0 69
paint 0 0
sizeHint 1 23
paint 1 23

As a result of that item1 overlaps item0 instead of moving down.

Is there a way to update option.rect position as soon as the size of one of previous items changes?


Solution

  • When the size hint of an index changes, you need to emit MessageDelegate.sizeHintChanged to let the layout manager of the items know that it needs to redistribute the items. In this case the height of the items only changes when the window is resized, so what you could do is to emit a (custom) signal in Dialog.resizeEvent and connect it to MessageDelegate.sizeHintChanged. For this Dialog would need to be modified according to something like this

    from PyQt5.QtCore import pyqtSignal, QModelIndex
    
    class Dialog(QMainWindow):
    
        # custom signal for signalling when window is resized
        width_changed = pyqtSignal()
    
        def __init__(self):
            ... as before ...
            self.messages = QListView()
            delegate = MessageDelegate()
            self.messages.setItemDelegate(delegate)
            # Connect custom signal to delegate.sizeHintChanged. 
            # This signal expects a ModelIndex for which we take the root, i.e. QModelIndex()  
            self.width_changed.connect(lambda : delegate.sizeHintChanged.emit(QModelIndex()))
            .... rest as before ....
    
        def resizeEvent(self, event):
            global window_width
            super(Dialog, self).resizeEvent(event)
            window_width = self.size().width()
            # emit the custom signal
            self.width_changed.emit()
    

    In the code above I didn't modify anything else in the code, but you could opt to get rid of the global window_width variable by modifying the custom signal to emit the new width and create a slot in MessageDelegate to assign the new width to an instance variable.