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?
When the size hint of an index changes, you need to emit
MessageDelegate.sizeHintChangedto 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 inDialog.resizeEventand connect it toMessageDelegate.sizeHintChanged. For thisDialogwould need to be modified according to something like thisIn the code above I didn't modify anything else in the code, but you could opt to get rid of the global
window_widthvariable by modifying the custom signal to emit the new width and create a slot inMessageDelegateto assign the new width to an instance variable.