Search code examples
delegatesqmlcustomization

Customizing PageIndicator to show an image for each page it indicates


I'm trying to customize the PageIndicator to do what I currently have three buttons do, namely:

  • Make it possible to change pages by clicking on the respective item inside the PageIndicator (currently I have 3 pages, one button per page with onClicked set to change the SwipeView to a predefined index — no PageIndicator)
  • Display custom image for each page indicator item (currently I have 3 pages, one button per page with a custom image — no PageIndicator)

My SwipeView has the following structure:

SwipeView {
    id: detailsView
    Layout.fillWidth: true
    Layout.preferredHeight: layoutTopLevel.height * .9
    Page {
        id: captureViewPage
        header: Text {
            text: "Capture view"
            horizontalAlignment: Text.AlignHCenter
            font.pixelSize: 20
        }
    }
    Page {
        id: helpViewPage
        header: Text {
            text: "Help view"
            horizontalAlignment: Text.AlignHCenter
            font.pixelSize: 20
        }

        footer: TabBar {
            id: helpViewSubCategories
            currentIndex: 0
            TabButton {
                text: qsTr("Gestures")
            }
            TabButton {
                text: qsTr("General")
            }
        }
    }
    Page {
        id: settingsViewPage
        header: Text {
            text: "Settings view"
            horizontalAlignment: Text.AlignHCenter
            font.pixelSize: 20
        }

        footer: TabBar {
            id: settingsViewSubCategories
            currentIndex: 0
            TabButton {
                text: qsTr("Language")
            }
            TabButton {
                text: qsTr("Device")
            }
        }
    }
}

Both the SwipeView and the PageIndicator (see below) are part of a ColumnLayout and are sublings:

ColumnLayout {
    id: layoutDetailsAndMenu
    spacing: 0
    SwipeView { ... }
    PageIndicator { ... }
}

First of all, I have researched how the delegate property of the PageIndicator works. For example, the following QML code:

PageIndicator {
    id: detailsViewIndicator
    count: detailsView.count
    currentIndex: detailsView.currentIndex
    interactive: true
    anchors.bottom: detailsView.bottom
    anchors.bottomMargin: -40
    anchors.horizontalCenter: parent.horizontalCenter


    delegate: Rectangle {
        implicitWidth: 15
        implicitHeight: 15

        radius: width
        color: "#21be2b"

        opacity: index === detailsView.currentIndex ? 0.95 : pressed ? 0.7 : 0.45

        Behavior on opacity {
            OpacityAnimator {
                duration: 100
            }
        }
    }
}

produces the following greenish result:

enter image description here


Make it possible to change pages by clicking on the respective item inside the PageIndicator

For some reason, the inactive doesn't work at all. I will quote the documentation what this property is supposed to do:

interactive : bool

This property holds whether the control is interactive. An interactive page indicator reacts to presses and automatically changes the current index appropriately. The default value is false.

I don't know if this is a bug or I'm missing something but even without the customization via delegate clicking on a page indicator item does NOT change the current page (the number of these items does equal the number of pages so clicking on an index that doesn't have a page assigned to it is out of question).

So in order to make my first wish come true, I added a MouseArea:

PageIndicator {
    // ...
    delegate: Rectangle {
        MouseArea {
            anchors.fill: parent
            onClicked: {
                if(index !== detailsView.currentIndex) {
                    detailsView.setCurrentIndex(index);
                }
            }
        }
    }
}

As you can see, I'm using the onClicked event handler (or whatever these things are called) and check whether the current index of the PageIndicator equals the page in my SwipeView. If that's not the case, I use setCurrentIndex(index) to set my SwipeView to the selected index.

After I wrote this, I was pretty satisfied that it worked as I envisioned it (though the interactive thing is still bothering me). Next thing was to add the images...


Display custom image for each page indicator item

First let me show you how I'd like things to look (this is actually the final result but more on that — later down the line):

NOTE: I've used the Qt logo, which I don't own. It's for demonstration purposes.

enter image description here

From left to right, the QRC URLs are:

  • qrc:/icons/qtlogo.png

    enter image description here

  • qrc:/icons/qtlogo1.png

    enter image description here

  • qrc:/icons/qtlogo2.png

    enter image description here

The source for this is as follows:

delegate: Image {
    source: detailsViewIndicator.indicatorIcons[detailsView.currentIndex]
    height: 30
    width: 30
    opacity: index === detailsView.currentIndex ? 0.95 : pressed ? 0.7 : 0.45
    MouseArea {
        anchors.fill: parent
        onClicked: {
            if(index !== detailsView.currentIndex) {
                detailsView.setCurrentIndex(index);
                source = detailsViewIndicator.indicatorIcons[detailsView.currentIndex];
            }
        }
    }
}

where indicatorIcons is a variant property of my PageIndicator:

property variant indicatorIcons: [
            "qrc:/icons/qtlogo.png",
            "qrc:/icons/qtlogo1.png",
            "qrc:/icons/qtlogo2.png"
        ]

I've used an array of string objects for the QRC URLs since it seems impossible to do

delegate: detailsViewIndicator.indicatorImages[detailsView.currentIndex]

with indicatorImages being:

property list<Image> indicatorImages: [
  Image { source: "..." },
  Image { source: "..." },
  Image { source: "..." }
]

The issues I'm having are with the actual loading of the images, and I have the feeling that it has something to do with the list problem I've described above. With the code:

delegate: Image {
    source: detailsViewIndicator.indicatorIcons[detailsView.currentIndex]
    // ...
}

first time I run my application, I get:

enter image description here

This is due to the fact that the initially selected index is 0 so an Image with source: "qrc:/icons/qtlogo.png" is generated and all page indicator items are populated with it. If I select one of the other two as the initially selected page, I will get qrc:/icons/qtlogo1.png and qrc:/icons/qtlogo2.png respectively.

Swiping in the SwipeView (not clicking on the PageIndicator) leads to

enter image description here

and

enter image description here

This only in one direction (index-wise from left to right). If I go backwards I get the same results but in the opposite order.

Clicking makes things more interesting. In the screenshot below, I've clicked on the second page indicator item (with qrc:/icons/qtlogo1.png as source for the Image) after the initialization:

enter image description here

Next, I clicked on the third page indicator item:

enter image description here

After some more clicking, I got:

enter image description here

Obviously, this is not how things are supposed to work. I'd like to have the final result all the time from start to end and no matter which page has been swiped away or page indicator item clicked.

Has anyone done anything like this? Is this even possible?


Solution

  • For some reason the inactive doesn't work at all

    I'm assuming this is a typo, as using the interactive property works for me (Qt 5.7):

    import QtQuick 2.7
    import QtQuick.Controls 2.0
    
    ApplicationWindow {
        visible: true
    
        PageIndicator {
            count: 3
            interactive: true
            onCurrentIndexChanged: print(currentIndex)
        }
    }
    

    As does a simplified version of your first example:

    import QtQuick 2.7
    import QtQuick.Controls 2.0
    
    ApplicationWindow {
        visible: true
    
        PageIndicator {
            id: detailsViewIndicator
            count: 3
            currentIndex: 0
            interactive: true
    
            delegate: Rectangle {
                implicitWidth: 15
                implicitHeight: 15
    
                radius: width
                color: "#21be2b"
    
                opacity: index === detailsViewIndicator.currentIndex ? 0.95 : pressed ? 0.7 : 0.45
    
                Behavior on opacity {
                    OpacityAnimator {
                        duration: 100
                    }
                }
            }
        }
    }
    

    So, if the SwipeView isn't changing pages, the problem is likely there, and we'd need to see that code.

    I'd like to have the final result all the time from start to end and no matter which page has been swiped away or page indicator item clicked.

    I think the problem is with the index you're using for the icon array:

    source: detailsViewIndicator.indicatorIcons[detailsView.currentIndex]
    

    That's using the currently selected index, whereas it seems like you want to use the index of that delegate:

    source: detailsViewIndicator.indicatorIcons[index]
    

    By the way, a simpler way of specifying the icon source would be to use string concatenation (and giving your files the appropriate naming to match):

    source: "qrc:/icons/qtlogo" + index + ".png"
    

    In the code you added, the SwipeView doesn't set its currentIndex to the currentIndex of the PageIndicator. You can do so like this:

    currentIndex: detailsViewIndicator.currentIndex
    

    The full example:

    import QtQuick 2.5
    import QtQuick.Layouts 1.1
    import QtQuick.Controls 2.0
    
    ApplicationWindow {
        visible: true
        width: 640
        height: 480
        title: qsTr("Hello World")
    
        ColumnLayout {
            anchors.fill: parent
    
            SwipeView {
                id: detailsView
                currentIndex: detailsViewIndicator.currentIndex
                Layout.fillWidth: true
                Layout.fillHeight: true
    
                Page {
                    id: captureViewPage
                    header: Text {
                        text: "Capture view"
                        horizontalAlignment: Text.AlignHCenter
                        font.pixelSize: 20
                    }
                }
                Page {
                    id: helpViewPage
                    header: Text {
                        text: "Help view"
                        horizontalAlignment: Text.AlignHCenter
                        font.pixelSize: 20
                    }
    
                    footer: TabBar {
                        id: helpViewSubCategories
                        currentIndex: 0
                        TabButton {
                            text: qsTr("Gestures")
                        }
                        TabButton {
                            text: qsTr("General")
                        }
                    }
                }
                Page {
                    id: settingsViewPage
                    header: Text {
                        text: "Settings view"
                        horizontalAlignment: Text.AlignHCenter
                        font.pixelSize: 20
                    }
    
                    footer: TabBar {
                        id: settingsViewSubCategories
                        currentIndex: 0
                        TabButton {
                            text: qsTr("Language")
                        }
                        TabButton {
                            text: qsTr("Device")
                        }
                    }
                }
            }
    
            PageIndicator {
                id: detailsViewIndicator
                count: detailsView.count
                currentIndex: detailsView.currentIndex
                interactive: true
                anchors.horizontalCenter: parent.horizontalCenter
            }
        }
    }
    

    Another note: using vertical anchors (anchors.bottom, anchors.top) in a ColumnLayout won't work. Only horizontal anchors can be used in the immediate child of a vertical layout, and vice versa.