qtlistviewqmlqtquick2pyside6

Combining Alternating Row Colors with a Smooth Highlight Rectangle in QML ListView


I am currently struggling to combine two seemingly simple concepts: alternating row colors and a smooth moving highlight item in QML ListViews.

Consider this simple example:


import sys

from PySide6.QtCore import QUrl
from PySide6.QtQml import QQmlApplicationEngine
from PySide6.QtWidgets import QApplication

if __name__ == '__main__':
    app = QApplication([])
    engine = QQmlApplicationEngine()

    engine.load(QUrl.fromLocalFile('main.qml'))

    if not engine.rootObjects():
        sys.exit(-1)

    app.exec()
import QtQuick
import QtQuick.Controls.Material
import QtQuick.Layouts


ApplicationWindow {
    id: root

    width: 400
    height: 500
    visible: true
    font.family: 'Noto Sans'

    ListView {
        id: listView

        width: root.width
        height: root.height

        clip: true
        focus: true
        reuseItems: true
        boundsBehavior: Flickable.StopAtBounds

        model: [
            "Wikipedia is a free online encyclopedia",
            "Wikipedia is a free online encyclopedia",
            "Wikipedia is a free online encyclopedia",
            "Wikipedia is a free online encyclopedia",
            "Wikipedia is a free online encyclopedia",
            "Wikipedia is a free online encyclopedia",
            "Wikipedia is a free online encyclopedia",
            "Wikipedia is a free online encyclopedia",
            "Wikipedia is a free online encyclopedia"
        ]

        delegate: Rectangle {
            height: 40
            width: parent.width
            color: ListView.isCurrentItem ? Material.accent : index % 2 === 0 ? palette.base : palette.alternateBase

            MouseArea {
                anchors.fill: parent

                onClicked: {
                    listView.currentIndex = index
                }
            }

            RowLayout {
                height: parent.height

                Label {
                    text: index
                    horizontalAlignment: Qt.AlignHCenter
                    verticalAlignment: Qt.AlignVCenter
                    Layout.preferredWidth: 100
                }

                Label {
                    text: modelData
                    Layout.preferredWidth: 300
                }
            }
        }

    }


}

Here, we have a ListView and a delegate with alternating row colors. We can highlight the item by either clicking on it or using the arrow keys:

alternating row colors gif

It's also quite simple to achieve a smooth highlighting (without alternating row colors) by swapping out just a few lines of code:

// ...

highlight: Rectangle {
    height: 40
    width: parent.width
    color: Material.accent
}

delegate: Rectangle {
    height: 40
    width: parent.width
    color: 'transparent'

    // ...
}

// ...

smooth highlight

In isolation, both of these concepts are easy to apply but when combining them, the struggle gets real 😅

Attempt #1

// ...

highlight: Rectangle {
    height: 40
    width: parent.width
    color: Material.accent
}

delegate: Rectangle {
    height: 40
    width: parent.width
    color: ListView.isCurrentItem ? 'transparent' : index % 2 === 0 ? palette.base : palette.alternateBase

    // ...
}

// ...

results in

attempt-1

Attempt #2

// ...

highlight: Rectangle {
    height: 40
    width: parent.width
    color: Material.accent
    z: 1
}

delegate: Rectangle {
    height: 40
    width: parent.width
    color: index % 2 === 0 ? palette.base : palette.alternateBase

    // ...
}

// ...

results in

attempt-2

I often find myself believing that this task shouldn't be overly difficult to accomplish, yet I can't shake the feeling that I might be overlooking something. Has anyone else encountered a similar issue and discovered a solution?


Solution

  • Interesting question!

    The difficulty is due to the z stacking order of items. Z orders are resolved by subtrees, this means that an item can't stack itself between a sibling and the sibling's children. It will be either above the sibling and all of its descendants, or below all of them.

    To achieve what you want, we need to separate the background from the content of your delegate so they aren't part of the same subtree anymore. For that you can explicitly set the parent of the delegate's background (and its geometry). I also refactored the delegate to use ItemDelegate to make the code a little clearer:

            highlight: Rectangle {
                height: 40
                width: parent.width
                color: Material.accent
            }
    
            delegate: ItemDelegate {
                id: delegate
                height: 40
                width: parent.width
                background: Rectangle {
                    parent: delegate.parent
                    y: delegate.y
                    height: delegate.height
                    color: index % 2 === 0 ? palette.base : palette.alternateBase
                }
    
                onClicked: listView.currentIndex = index
    
                contentItem: RowLayout {
                    Label {
                        text: index
                        horizontalAlignment: Qt.AlignHCenter
                        verticalAlignment: Qt.AlignVCenter
                        Layout.preferredWidth: 100
                    }
                    Label {
                        text: "Wikipedia is a free online encyclopedia"
                        Layout.preferredWidth: 300
                    }
                }
            }
    

    One could be more explicit by setting a z of 0 for the backgrounds, 1 for the highlight and 2 for the delegates but the default order seems to work out here.

    This results in:
    background delegate animation

    Using GammaRay we can even see the items layers: