Search code examples
pythonpyside2

QListWidgetItem's adjust to contents


How can I make it so when the user resizes the dialog the list item's scale to their. Currently they don't appear to do so... They remain at the same size as they were the the tool was launched. I have enabled word wrap. How do i fix this?

When i scale the width it looks ok as show here...

enter image description here

But looks wrong when i scale it up

enter image description here

import os
import sys
import re
import random

from PySide2 import QtGui, QtWidgets, QtCore

class ListWidgetItem(QtWidgets.QWidget):
    def __init__(self, parent=None):
        super(ListWidgetItem, self).__init__(parent)
        self.setStyleSheet('background: green;')

        self.uiTitle = QtWidgets.QLabel()
        self.uiTitle.setWordWrap(True)
        self.uiTitle.setText('Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nunc porta ornare odio sit amet suscipit. Curabitur orci urna, commodo quis urna ut, consequat sodales urna.')

        self.subtitle = QtWidgets.QLabel()
        self.subtitle.setText('Title')

        undoicon = self.style().standardIcon(QtWidgets.QStyle.SP_BrowserReload)
        self.button = QtWidgets.QPushButton('Click Me')
        self.button.setIcon(undoicon)

        self.mainLayout = QtWidgets.QVBoxLayout()
        self.mainLayout.setAlignment(QtCore.Qt.AlignTop)
        self.mainLayout.setSpacing(0)
        self.mainLayout.addWidget(self.subtitle)
        self.mainLayout.addWidget(self.uiTitle)
        self.mainLayout.addWidget(self.button)
        self.setLayout(self.mainLayout)

class ListWidget(QtWidgets.QListWidget):
    def __init__(self, parent=None):
        super(ListWidget, self).__init__(parent)
        self.resize(400,400)
        self.setSpacing(1)
        self.setSelectionMode(QtWidgets.QListView.ExtendedSelection)
        self.setVerticalScrollMode(QtWidgets.QAbstractItemView.ScrollPerPixel)
        self.verticalScrollBar().setSingleStep(10)
        self.setWordWrap(True)
        self.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
        self.setSizeAdjustPolicy(QtWidgets.QListWidget.AdjustToContents)
        self.populateItems()

    def appendListWidget(self, widget):
        lwi = QtWidgets.QListWidgetItem()
        lwi.setSizeHint(widget.sizeHint())
        self.addItem(lwi)
        self.setItemWidget(lwi, widget)

    def populateItems(self):
        for x in range(10):
            widget = ListWidgetItem()
            self.appendListWidget(widget)

def main():
    app = QtWidgets.QApplication(sys.argv)
    ex = ListWidget()
    ex.show()
    app.exec_()


if __name__ == '__main__':
    pass
    main()

Solution

  • The size hint set for a list item is static and based to the initial size hint of the widget, so it will not completely adapt, especially when the content can change its aspect ratio. It will increase its dimensions whenever required, but it won't shrink.

    The problem comes from the word wrapping, which is a known limitation of Qt layouts. From the Layout Issues section of Layout Management:

    The use of rich text in a label widget can introduce some problems to the layout of its parent widget. Problems occur due to the way rich text is handled by Qt's layout managers when the label is word wrapped.

    A possible solution is to override the resizeEvent of the list widget, and do some computation based on the contents by taking into consideration the heightForWidth().

    This requires two for loops (it could be slightly optimized with just one, but with minimal improvement, since it's unlikely that you'll have so many widgets visible in the current viewport).
    The first loop is to check whether the visible items can fit the current view without showing the scroll bar, the second is to actually set the new size hint for the widgets based on the available width.

    Obviously, if you use the ScrollBarAlwaysOn policy, you only need to do a single loop, considering the scroll bar hint width from start (do not use the actual scroll bar size, as it will return a different value when the view has not been shown yet).

    class ListWidget(QtWidgets.QListWidget):
        # ...
        def resizeEvent(self, event):
            super().resizeEvent(event)
            width = self.viewport().width() - self.frameWidth() * 2
            if width > self.minimumSizeHint().width():
                # ensure that we have enough horizontal space to display the 
                # contents by avoiding recursion of the resize event
                maxHeight = self.viewport().height()
                height = 0
                spacing = self.spacing()
                for i in range(self.count()):
                    item = self.item(i)
                    widget = self.itemWidget(item)
                    if not widget:
                        continue
                    height += widget.heightForWidth(width)
                    if height > maxHeight:
                        # we can't fit all items in the current visible rect
                        width -= self.verticalScrollBar().sizeHint().width()
                        break
                    height += spacing
            else:
                width = self.minimumSizeHint().width()
            for i in range(self.count()):
                item = self.item(i)
                widget = self.itemWidget(item)
                if widget:
                    item.setSizeHint(
                        QtCore.QSize(width, widget.heightForWidth(width))
                    )