PySide QListWidgetItem with Label wrap not shrinking

Question:

I’m using pyside2 and python. I have a QListWidget with a custom widget being added to the listwidget. However for some reason when the main dialog is resized the listwidgetItems do not adjust their height to properly shrink to fit the available space, instead i have these huge gaps of empty space. how do i fix this??

Image on the left is what i get, the image on the right is what i want.

enter image description here

import os
import sys
from PySide2 import QtCore, QtGui, QtWidgets


class NoteWidget(QtWidgets.QWidget):
    def __init__(self, noteItem):
        super(NoteWidget, self).__init__()
        self.setStyleSheet('border: 1px solid red;')

        # controls
        self.uiCategory = QtWidgets.QLabel()
        self.uiCategory.setTextFormat(QtCore.Qt.PlainText)
        self.uiCategory.setStyleSheet('font-family: Arial; font-weight: bold; font-size: 14px;')
        self.uiCategory.setText(noteItem['category'])

        self.uiDescription = QtWidgets.QLabel()
        self.uiDescription.setWordWrap(True)
        self.uiDescription.setText(noteItem['description'])
        self.uiDescription.adjustSize()
        # self.uiDescription.setFixedHeight(100)

        # layout
        self.mainLayout = QtWidgets.QVBoxLayout()
        self.mainLayout.setContentsMargins(10,10,10,10)
        self.mainLayout.setSpacing(0)
        self.mainLayout.setSizeConstraint(QtWidgets.QLayout.SetMinimumSize)

        self.mainLayout.setAlignment(QtCore.Qt.AlignTop)
        self.mainLayout.addWidget(self.uiCategory)
        self.mainLayout.addWidget(self.uiDescription)
        self.setLayout(self.mainLayout)


class FileNotesDialog(QtWidgets.QMainWindow):

    def __init__(self, parent=None):
        super(FileNotesDialog, self).__init__(parent)

        self.uiList = QtWidgets.QListWidget()
        self.uiList.setSpacing(0)
        self.uiList.setFocusPolicy(QtCore.Qt.NoFocus)
        self.uiList.setVerticalScrollMode(QtWidgets.QAbstractItemView.ScrollPerPixel)
        self.uiList.verticalScrollBar().setSingleStep(10)
        self.uiList.setUniformItemSizes(False)
        self.uiList.setResizeMode(QtWidgets.QListWidget.Adjust)
        self.uiList.setSizeAdjustPolicy(QtWidgets.QListWidget.AdjustToContentsOnFirstShow)
        self.uiList.setSelectionMode(QtWidgets.QListView.ExtendedSelection)

        self.mainLayout = QtWidgets.QVBoxLayout()
        self.mainLayout.setSpacing(6)
        self.mainLayout.setContentsMargins(6, 6, 6, 6)
        self.mainLayout.addWidget(self.uiList)

        self.mainWidget = QtWidgets.QWidget()
        self.mainWidget.setLayout(self.mainLayout)
        self.setCentralWidget(self.mainWidget)

        self.populateNotes()


    def populateNotes(self):
        self.uiList.clear()
        notes = [
            {
                'category': 'Misc', 'description': 'Here is a simple reminder'
            },
            {
                'category': 'Today', 'description': 'Here is a longer reminder that will use wordwrap because it is a really long description provided by the user for reading when they have time. It should not be ignored.'
            },
            {
                'category': 'Notes', 'description': 'Here is another simple reminder for you.'
            },
            {
                'category': 'General', 'description': 'I think this will fix the problem if you have to time implement this when you are working today.'
            }
        ]

        for obj in notes:
            nw = NoteWidget(obj)
            lwi = QtWidgets.QListWidgetItem()
            self.uiList.addItem(lwi)
            self.uiList.setItemWidget(lwi, nw) 
            lwi.setSizeHint(nw.sizeHint())


def main():
    app = QtWidgets.QApplication(sys.argv)
    ex = FileNotesDialog()
    ex.resize(500,700)
    ex.show()
    sys.exit(app.exec_())


if __name__ == '__main__':
    main()
Asked By: JokerMartini

||

Answers:

The problem is that you’re setting a fixed size hint for the items based on the widgets, but that’s not a viable option for two reasons:

  1. those widgets are being created without any constraint, so they have a variable size hint based on their contents (try to print(nw.sizeHint()) and you’ll see);
  2. whenever the view is resized, it will keep considering the original hint set for the items, completely ignoring the requirements of "dynamic size" widgets like QLabels that use word wrapping;

The solution is to not set the size hint directly, but create a subclass for QListWidget and update the size hint both when items are added and when the view is resized.

Also note that you should avoid setting the alignment of the layout, as it creates issues with QLabel (see the documentation). Instead, force the maximum height of the "header" based on that label size hint, and let the bottom label take care of itself.

class NoteWidget(QtWidgets.QWidget):
    def __init__(self, noteItem):
        # ...
        self.uiCategory.setFixedHeight(self.uiCategory.sizeHint().height())
        # ...

        # remove the following lines
        # self.mainLayout.setSizeConstraint(QtWidgets.QLayout.SetMinimumSize)
        # self.mainLayout.setAlignment(QtCore.Qt.AlignTop)


class ListWidget(QtWidgets.QListWidget):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.hintTimer = QtCore.QTimer(self, singleShot=True, 
            interval=0, timeout=self.updateHints)

    def updateHints(self):
        width = self.viewport().width()
        for i in range(self.count()):
            item = self.item(i)
            widget = self.itemWidget(item)
            if widget is None or not widget.hasHeightForWidth():
                continue
            height = widget.heightForWidth(width)
            item.setSizeHint(QtCore.QSize(width, height))

    def setItemWidget(self, item, widget):
        super().setItemWidget(item, widget)
        self.hintTimer.start()

    def resizeEvent(self, event):
        super().resizeEvent(event)
        self.updateHints()


class FileNotesDialog(QtWidgets.QMainWindow):
    def __init__(self, parent=None):
        # ...
        self.uiList = ListWidget()
        # ...

    def populateNotes(self):
        # ...
        for obj in notes:
            nw = NoteWidget(obj)
            lwi = QtWidgets.QListWidgetItem()
            self.uiList.addItem(lwi)
            self.uiList.setItemWidget(lwi, nw)
            # no lwi.setSizeHint() here

Here is the result, comparing the view as shown on startup and after resizing it:

Screenshot of the result

Answered By: musicamante
Categories: questions Tags: ,
Answers are sorted by their score. The answer accepted by the question owner as the best is marked with
at the top-right corner.