The question comes from a simple demand while making an selection window:

As you guys can see its a QListView showing a list of files, then select files needed by user, confirm & close.
The native QListView only shows one line of data, which in Qt.DisplayRole. I need to add a second line of text showing something like file size, modified time, directory, etc.
So after asking ChatGPT and searching, I knew that I need to store my own data in Qt.UserRole apply an custom QStyledItemDelegate to QListView, re-write its paint() method.
The Native paint() handle checkbox and decoration icon well. All i need is to add a second line while rendering Qt.DisplayRole. Then i found the paint() render the item as a whole, I must write everything from zero.
I can only find C++ src from here:
/*!
Renders the delegate using the given \a painter and style \a option for
the item specified by \a index.
This function paints the item using the view's QStyle.
When reimplementing paint in a subclass. Use the initStyleOption()
to set up the \a option in the same way as the
QStyledItemDelegate.
Whenever possible, use the \a option while painting.
Especially its \l{QStyleOption::}{rect} variable to decide
where to draw and its \l{QStyleOption::}{state} to determine
if it is enabled or selected.
After painting, you should ensure that the painter is returned to
the state it was supplied in when this function was called.
For example, it may be useful to call QPainter::save() before
painting and QPainter::restore() afterwards.
\sa QItemDelegate::paint(), QStyle::drawControl(), QStyle::CE_ItemViewItem
*/
void QStyledItemDelegate::paint(QPainter *painter,
const QStyleOptionViewItem &option, const QModelIndex &index) const
{
Q_ASSERT(index.isValid());
QStyleOptionViewItem opt = option;
initStyleOption(&opt, index);
const QWidget *widget = QStyledItemDelegatePrivate::widget(option);
QStyle *style = widget ? widget->style() : QApplication::style();
style->drawControl(QStyle::CE_ItemViewItem, &opt, painter, widget);
}
From PySide6 document:
If the delegate does not support painting of the data types you need or you want to customize the drawing of items, you need to subclass QStyledItemDelegate , and reimplement paint() and possibly sizeHint() . The paint() function is called individually for each item, and with sizeHint() , you can specify the hint for each of them.
When reimplementing paint() , one would typically handle the datatypes one would like to draw and use the superclass implementation for other types.
The painting of check box indicators are performed by the current style. The style also specifies the size and the bounding rectangles in which to draw the data for the different data roles. The bounding rectangle of the item itself is also calculated by the style. When drawing already supported datatypes, it is therefore a good idea to ask the style for these bounding rectangles. The QStyle class description describes this in more detail.
If you wish to change any of the bounding rectangles calculated by the style or the painting of check box indicators, you can subclass QStyle . Note, however, that the size of the items can also be affected by reimplementing sizeHint() .
As a new to QT, I cannot handle so many StateFlags and themestyles, tried to write a "dead" ui without any Hover / Selected / OutOfFocus style changes.
I finally achieved my demand in a stupid but effective way:
- Add an
'\n'afterQt.DisplayRolefor an item, - Use Native Alignment for list item (
AlignVCenter | AlignLeft), - Call
super().paint(painter, option, index)
then Qt will render DisplayRole in 2x line height, but line 2 empty.
- Use
painter.drawText()to draw additional text withoption.font,option.fontMetrics, QRect calculated fromoption.rect

I've also succeed to draw checkbox by following this guide, re-write the cpp code logic in python. Turns out that theres no response after selecting. Its state always 'unchecked'.
Update: My Approach so far
class CustomListDelegate(QStyledItemDelegate):
def paint(self, painter, option, index, PySide6_QtCore_QModelIndex=None,
PySide6_QtCore_QPersistentModelIndex=None):
# print(index.column())
if index.column() == 0:
# Get UserRole data
data = json.loads(index.data(Qt.UserRole))
size = bytes_to_readable(int(data['size']))
modified = datetime.fromtimestamp(int(data['modify_time']))
modified_s = 'Modified at ' + modified.strftime("%Y-%m-%d %H:%M:%S")
dir = data['dir']
# Prepare for text overflow
fm = option.fontMetrics
description_text = ' · '.join([size, modified_s, dir])
# Prepare for text position
style = QApplication.style() if option.widget is None else option.widget.style()
text_rect = style.subElementRect(QStyle.SE_ItemViewItemText, option)
width = text_rect.width()
height = int(option.rect.height() / 2)
top = option.rect.top() + height
description_elided = fm.elidedText(description_text, Qt.ElideRight, width)
# Draw original item
option.font.setPointSize(option.font.pointSize() + 1)
super().paint(painter, option, index)
# Draw description
option.font.setPointSize(option.font.pointSize() -1)
painter.setPen(option.palette.color(QPalette.PlaceholderText))
painter.drawText(QRect(text_rect.left(), top, width, height), description_elided)
else:
super().paint(painter, option, index)


It seems that, style.subElementRect(QStyle.SE_ItemViewItemText, option) cannot get the correct position of DisplayRole, why?
My question is, how to implement my need as "natively" as I can?
Similar approach: