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.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 inDialog.resizeEvent
and connect it toMessageDelegate.sizeHintChanged
. For thisDialog
would 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_width
variable by modifying the custom signal to emit the new width and create a slot inMessageDelegate
to assign the new width to an instance variable.