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 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.