0

I have a QML application that is basically just a ListView which displays a number of "chapters", which in turn each include one or more "pages".

Since I don't know how many chapters and pages a QML might have in production, I'm using a Loader to load the pages on demand, which should save some memory.

So the problem is that I want to "jump" to a certain story and page at the push of a button. I added a few buttons to the example which jump to different chapters already.

You can vertically flick between chapters and horizontally between pages of each chapter.

The problem I have is that I can't figure out how to get the ListView containing the pages to jump to a specific page after switching/loading a specific chapter. Basically I'm missing something like pagesView.currentPage = 5 or something similar.

What would be a good way to get this working?

Screenshot

The corresponding QML. You can run this with qmlscene.

import QtQuick 2.4
import QtQuick.Controls 1.2


ApplicationWindow {
    width: 1024
    height: 768


    Component {
        id: pageViewComponent
        ListView {
            id: pagesView
            property int storyIndex: chapterView.modelData
            orientation: ListView.Horizontal; clip: true
            model: 20; snapMode: ListView.SnapToItem
            delegate: Rectangle {
                width: pagesView.width; height: pagesView.height
                color: Qt.rgba(Math.random(),Math.random(),Math.random(),1)
                border.color: "black"
                Text { text: "Page " + modelData; anchors.centerIn: parent; color: "white" }
            }

        }
    }


    Rectangle {
        color: "black"
        anchors.fill: parent

        // Chapters 
        ListView {
            id: chapterView
            model: 8
            anchors.fill: parent
            snapMode: ListView.SnapToItem

            delegate: Rectangle {
                color: Qt.rgba(Math.random(),Math.random(),Math.random(),1)
                width: chapterView.width; height: chapterView.height

                Rectangle {
                    width: parent.width * 0.6; height: parent.height * 0.6
                    anchors.centerIn: parent
                    Loader { 
                        anchors.fill: parent
                        sourceComponent: pageViewComponent 
                    }
                }


                Text {
                    x: 50; y: 50
                    color: "white"; font.pointSize: 30
                    text: "Chapter " + modelData
                }

                Flow {
                    Button {
                        text: "Go to Chapter 2, Page 7"
                        onClicked: { 
                            chapterView.positionViewAtIndex(2, ListView.Beginning)
                            //
                            //
                            // After jumping to the correct chapter, we obviously have to jump
                            // to the correct page after the Loader for that specific chapter has
                            // completed loading the pages of the chapter.
                            //
                            //
                        }
                    }

                    Button {
                        text: "Go to Chapter 1, Page 1"
                        onClicked: {
                            chapterView.positionViewAtIndex(1, ListView.Beginning)
                            // dito
                        }
                    }

                    Button {
                        text: "Go to Chapter 5, Page 2"
                        onClicked: { 
                            chapterView.positionViewAtIndex(5, ListView.Beginning)
                            // dito
                        }
                    }
                }
            }

        }
    }
}
BastiBen
  • 19,679
  • 11
  • 56
  • 86
  • Use [Loader.item](http://doc.qt.io/qt-5/qml-qtquick-loader.html#item-prop) to access the dynamically created `pagesView`. So you can `idOfTheLoader.item.positionViewAtIndex(7, ListView.Beginning)` to change pages. – mcchu Apr 04 '15 at 13:24
  • @mcchu - that would access the loader of the current item, not the one you are going to. Plus the call to it must be scheduled to run when it has loaded its content, with a lambda and parameter capture. – dtech Apr 04 '15 at 13:36

2 Answers2

2

Here's my way of doing this:

  1. Define properties in pagesView that will allow it to update it's appearance and state:

    Component {
        id: pageViewComponent
        ListView {
            id: pagesView
    
            // Begin inserted code
            property int chapterIndex: -1
            property ListView chapterView: null
            Connections {
                target: chapterView
                onSelectedPageChanged: {
                    if (chapterIndex === chapterView.selectedChapter)
                        pagesView.positionViewAtIndex(chapterView.selectedPage, ListView.Beginning)
                }
            }
            onChapterIndexChanged: {
                if (chapterView && chapterIndex === chapterView.selectedChapter)
                    pagesView.positionViewAtIndex(chapterView.selectedPage, ListView.Beginning)
            }
            // End inserted code
    
            orientation: ListView.Horizontal; clip: true
            model: 20; snapMode: ListView.SnapToItem
            delegate: Rectangle {
                width: pagesView.width; height: pagesView.height
                color: Qt.rgba(Math.random(),Math.random(),Math.random(),1)
                border.color: "black"
                Text { text: "Page " + modelData; anchors.centerIn: parent; color: "white" }
            }
        }
    }
    
  2. Define the properties that will store the whole thing's state (that is, current chapter & page):

    property int selectedChapter: 0
    property int selectedPage: 0
    
  3. Update top-level ListView position according to selectedChapter property:

    onSelectedChapterChanged: positionViewAtIndex(selectedChapter, ListView.Beginning)
    
  4. Setup pagesView's properties when it's created:

    Loader {
        id: pagesViewLoader
        anchors.fill: parent
        sourceComponent: pageViewComponent
        onLoaded: {
            item.chapterIndex = Qt.binding(function() { return modelData })
            item.chapterView = chapterView
        }
    }
    
  5. Trigger chapter and page change from Button:

    // Define method in top-level ListView
    ListView {
        id: chapterView
        function goTo(chapter, page) {
            selectedChapter = chapter
            selectedPage = page
        }
    // ...
    
    // Call method from onClicked handler
    Button {
        text: "Go to Chapter 2, Page 7"
        onClicked: {
            chapterView.goTo(/* chapter */ 2, /* page */ 7)
        }
    }
    

Important Notes

Note 1

In the 4th step modelData is not set directly, instead it is wrapped into Qt.binding function call. This is how you bind to value from JavaScript. This is necessary because ListView reuses it's delegate instances, and pagesView instance can be reused with different modelData values after single onLoaded message.

Note 2

At first I used two property assignments in onClicked without wrapping them in goTo function:

chapterView.selectedChapter = chapter
chapterView.selectedPage = page

but this led to error in the second line (chapterView is not defined). When first line executes, chapterView scrolls to the selected chapter, and current delegate item is removed from the scene since it is no longer needed. I can't tell how this "removed from the scene" is done technically, but the result is that after the first line chapterView is no longer defined. Hence the need to wrap these two assignments in a single function call.

tonytony
  • 1,994
  • 3
  • 20
  • 27
  • Nice answer. I'll give it a try and get back to you! QML sometimes can be a bit hard to wrap your head around if you are used to develop applications using traditional tools. ;) – BastiBen Apr 06 '15 at 10:27
  • Update: Works from what I can see. I learned a few things from your answer that will help me in the future: 1) Qt.binding() -- how did I miss this?; 2) Custom `function`s in QML objects. Thank you! – BastiBen Apr 06 '15 at 10:43
  • 1
    For completeness I have made this gist with a working example: https://gist.github.com/bastibense/7f949e2f791b4db27c6f – BastiBen Apr 06 '15 at 10:45
0

Something like this maybe?

    // go to chapter 2 page 7
    chapterView.positionViewAtIndex(2, ListView.Beginning)
    var loader = chapterView.currentItem.loader
    loader.loaded.connect(function(l, p) {
        return function() {
           l.item.positionViewAtIndex(p, ListView.Beginning)}
    }(loader, 7))

This way when the loader has loaded the item, the function to access the item will be invoked.

Also expose the loader so it can be accessed:

Rectangle {
    //...
    property Loader loader : l
    Loader { 
        id: l
        // ...
    }
}
dtech
  • 47,916
  • 17
  • 112
  • 190