How does the native QStyledItemDelegate.draw() works during rendering an QListView with an QStandardItemModel?

189 Views Asked by At

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

image of ui

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:

  1. Add an '\n' after Qt.DisplayRole for an item,
  2. Use Native Alignment for list item (AlignVCenter | AlignLeft),
  3. Call super().paint(painter, option, index)

then Qt will render DisplayRole in 2x line height, but line 2 empty.

  1. Use painter.drawText() to draw additional text with option.font, option.fontMetrics, QRect calculated from option.rect

image of code

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)

image of window

image of debugging

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:

How to paint() with QStyledItemDelegate

0

There are 0 best solutions below