Search code examples
pythonpyqtpysidepyside6pyqt6

how to change word wrap mode for QListView?


I created a QComboBox and set a custom view for it:

self.list_view = QListView()
self.list_view.setWordWrap(True)
self.list_view.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)

self.settings = QComboBox()
self.settings.setFixedHeight(28)
self.settings.setView(self.list_view)

How can I set a specific word wrapping mode for a QListView or a QComboBox?

In the documentation I found a QTextOption class that has a .setWrapMode(wrapmode: PySide6.QtGui.QTextOption.WrapMode) method, but didn't find a way to set a QTextOption for a QListView/QComboBox like a QListView for a QComboBox.


Solution

  • The only way to achieve so while maintaining the model data is to implement a custom item delegate that uses QTextDocument for both sizeHint() and paint().

    The combo must also be a subclass that overrides showPopup(), which is required to update the default (assumed) width of the view and compute correct sizes. This step is fundamental, as before showing itself, the doesn't know yet if the scroll bars will be visible or not (which could depend on the maxVisibleItems() property but also the screen height).

    Note that this approach is not always perfect: if the items have very long text and the combo is very narrow, the scroll bar might be shown even if the model has fewer items than the maxVisibleItems. As a small workaround, I ensured that the painter always clips to the contents of the option. The only alternative solution is to compute the size hint of the first ten items, based on the current combo width, then check if the sum of the heights of those hints exceeds the current screen and eventually change again the reference width before actually showing the popup.

    from random import choice
    from PyQt5 import QtGui, QtWidgets
    
    letters = 'abcdefhgijklmnopqrstuvwxyz0123456789' * 2 + ' '
    
    class WrapDelegate(QtWidgets.QStyledItemDelegate):
        referenceWidth = 0
        wrapMode = QtGui.QTextOption.WrapAtWordBoundaryOrAnywhere
        def sizeHint(self, opt, index):
            doc = QtGui.QTextDocument(index.data())
            textOption = QtGui.QTextOption()
            textOption.setWrapMode(self.wrapMode)
            doc.setDefaultTextOption(textOption)
            doc.setTextWidth(self.referenceWidth)
            return doc.size().toSize()
    
        def paint(self, qp, opt, index):
            self.initStyleOption(opt, index)
            style = opt.widget.style()
            opt.text = ''
            style.drawControl(style.CE_ItemViewItem, opt, qp, opt.widget)
            doc = QtGui.QTextDocument(index.data())
            textOption = QtGui.QTextOption()
            textOption.setWrapMode(textOption.WrapAtWordBoundaryOrAnywhere)
            doc.setDefaultTextOption(textOption)
            doc.setTextWidth(opt.rect.width())
            qp.save()
            qp.translate(opt.rect.topLeft())
            qp.setClipRect(0, 0, opt.rect.width(), opt.rect.height())
            doc.drawContents(qp)
            qp.restore()
    
    
    class ComboWrap(QtWidgets.QComboBox):
        def __init__(self, *args, **kwargs):
            super().__init__(*args, **kwargs)
            self.delegate = WrapDelegate(self.view())
            self.view().setItemDelegate(self.delegate)
            self.view().setResizeMode(QtWidgets.QListView.Adjust)
    
        def showPopup(self):
            container = self.view().parent()
            l, _, r, _ = container.layout().getContentsMargins()
            margin = l + r + container.frameWidth() * 2
            if self.model().rowCount() > self.maxVisibleItems():
                margin += self.view().verticalScrollBar().sizeHint().width()
            self.delegate.referenceWidth = self.width() - margin
            super().showPopup()
    
    
    app = QtWidgets.QApplication([])
    combo = ComboWrap()
    combo.addItems([''.join(choice(letters) for i in range(100)) for i in range(20)])
    combo.show()
    app.exec()