Search code examples
pythonpyqtpyqt5qlistwidgetqstyleditemdelegate

QStyledItemDelegate truncates text horizontally and doesn't add a horizontal scroll bar


I got the following code, which I slightly modified, as an answer on how to use HTML formatted text in a QListWidget.

main.ui

<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
 <class>Dialog</class>
 <widget class="QDialog" name="Dialog">
  <property name="geometry">
   <rect>
    <x>0</x>
    <y>0</y>
    <width>341</width>
    <height>161</height>
   </rect>
  </property>
  <property name="windowTitle">
   <string>Dialog</string>
  </property>
  <widget class="QWidget" name="verticalLayoutWidget">
   <property name="geometry">
    <rect>
     <x>10</x>
     <y>10</y>
     <width>321</width>
     <height>141</height>
    </rect>
   </property>
   <layout class="QVBoxLayout" name="verticalLayout">
    <item>
     <widget class="QListWidget" name="lwOptions"/>
    </item>
   </layout>
  </widget>
 </widget>
 <resources/>
 <connections/>
</ui>

test.py

import os
import html
from PyQt5 import uic
from PyQt5.Qt import (QApplication, QDialog, QListWidget, 
    QListWidgetItem, QStyleOptionViewItem, 
    QStyledItemDelegate, QStyle)
from PyQt5.QtGui import QPalette, QAbstractTextDocumentLayout, QTextDocument
from PyQt5.QtCore import QSize

class HTMLDelegate(QStyledItemDelegate):
    def __init__(self, parent=None):
        super(HTMLDelegate, self).__init__(parent)
        self.doc = QTextDocument(self)

    def paint(self, painter, option, index):
        painter.save()
        options = QStyleOptionViewItem(option)
        self.initStyleOption(options, index)
        self.doc.setHtml(options.text)
        options.text = ""
        style = QApplication.style() if options.widget is None \
            else options.widget.style()
        style.drawControl(QStyle.CE_ItemViewItem, options, painter)

        ctx = QAbstractTextDocumentLayout.PaintContext()
        if option.state & QStyle.State_Selected:
            ctx.palette.setColor(QPalette.Text, option.palette.color(
                QPalette.Active, QPalette.HighlightedText))
        else:
            ctx.palette.setColor(QPalette.Text, option.palette.color(
                QPalette.Active, QPalette.Text))
        textRect = style.subElementRect(QStyle.SE_ItemViewItemText, options, None)
        if index.column() != 0:
            textRect.adjust(5, 0, 0, 0)
        constant = 4
        margin = (option.rect.height() - options.fontMetrics.height()) // 2
        margin = margin - constant
        textRect.setTop(textRect.top() + margin)

        painter.translate(textRect.topLeft())
        painter.setClipRect(textRect.translated(-textRect.topLeft()))
        self.doc.documentLayout().draw(painter, ctx)
        painter.restore()

    def sizeHint(self, option, index):
        return QSize(self.doc.idealWidth(), self.doc.size().height())

class GUI(QDialog):
    def __init__(self):
        super(GUI, self).__init__()
        dirname = os.path.dirname(os.path.abspath(__file__))
        uic.loadUi(os.path.join(dirname,'main.ui'), self)

        delegate = HTMLDelegate(self.lwOptions)
        self.lwOptions.setItemDelegate(delegate)

        for ordinal in ['first', 'second', 'third', 'fourth', 'fifth', 'sixth', 'seventh', 'eighth', 'ninth']:
            item = QListWidgetItem('This is the <b>{}</b> word. This is the <b>{}</b> word. This is the <b>{}</b> word.'.format(ordinal, ordinal, ordinal))
            self.lwOptions.addItem(item)


if __name__ == '__main__':
    import sys
    app = QApplication(sys.argv)
    window = GUI()
    window.show()
    sys.exit(app.exec_())

The code works fine, however, text that is longer than the width of the QListWidget is truncated and no horizontal scroll bar is displayed. (The vertical scroll bar is automatically added when needed.)

I tried forcing the display via ScrollBarAlwaysOn, which only displayed a non-working horizontal scroll bar.

How do I need to change the code to automatically add a horizontal scroll bar when needed?


Solution

  • The calculation I made in my previous answer was incorrect since the size that returns sizeHint() is relative to the previous item. A simple solution is to use the Qt::SizeHintRole role to set the size.

    import os
    from PyQt5 import QtCore, QtGui, QtWidgets, uic
    
    class HTMLDelegate(QtWidgets.QStyledItemDelegate):
        def __init__(self, parent=None):
            super(HTMLDelegate, self).__init__(parent)
            self.doc = QtGui.QTextDocument(self)
    
        def paint(self, painter, option, index):
            painter.save()
            options = QtWidgets.QStyleOptionViewItem(option)
            self.initStyleOption(options, index)
            self.doc.setHtml(options.text)
            options.text = ""
            style = QApplication.style() if options.widget is None \
                else options.widget.style()
            style.drawControl(QtWidgets.QStyle.CE_ItemViewItem, options, painter)
    
            ctx = QtGui.QAbstractTextDocumentLayout.PaintContext()
            if option.state & QtWidgets.QStyle.State_Selected:
                ctx.palette.setColor(QtGui.QPalette.Text, option.palette.color(
                    QtGui.QPalette.Active, QtGui.QPalette.HighlightedText))
            else:
                ctx.palette.setColor(QtGui.QPalette.Text, option.palette.color(
                    QtGui.QPalette.Active, QtGui.QPalette.Text))
            textRect = style.subElementRect(QtWidgets.QStyle.SE_ItemViewItemText, options, None)
            if index.column() != 0:
                textRect.adjust(5, 0, 0, 0)
            constant = 4
            margin = (option.rect.height() - options.fontMetrics.height()) // 2
            margin = margin - constant
            textRect.setTop(textRect.top() + margin)
    
            painter.translate(textRect.topLeft())
            painter.setClipRect(textRect.translated(-textRect.topLeft()))
            self.doc.documentLayout().draw(painter, ctx)
            painter.restore()
            s = QtCore.QSize(self.doc.idealWidth(), self.doc.size().height())
            index.model().setData(index, s, QtCore.Qt.SizeHintRole)
    
    class GUI(QtWidgets.QDialog):
        def __init__(self):
            super(GUI, self).__init__()
            dirname = os.path.dirname(os.path.abspath(__file__))
            uic.loadUi(os.path.join(dirname,'main.ui'), self)
    
            delegate = HTMLDelegate(self.lwOptions)
            self.lwOptions.setItemDelegate(delegate)
    
            for ordinal in ['first', 'second', 'third', 'fourth', 'fifth', 'sixth', 'seventh', 'eighth', 'ninth']:
                item = QtWidgets.QListWidgetItem('This is the <b>{}</b> word. This is the <b>{}</b> word. This is the <b>{}</b> word.'.format(ordinal, ordinal, ordinal))
                self.lwOptions.addItem(item)
    
    if __name__ == '__main__':
        import sys
        app = QtWidgets.QApplication(sys.argv)
        window = GUI()
        window.show()
        sys.exit(app.exec_())
    

    enter image description here