Qt/QML: 在 Loader 加载后访问 ListView 以跳转到特定 item/page

Qt/QML: Accessing a ListView after a Loader has loaded it to jump to a specific item/page

我有一个 QML 应用程序,它基本上只是一个显示多个 "chapters" 的 ListView,其中每个又包含一个或多个 "pages"。

由于我不知道 QML 在生产中可能有多少章节和页面,我使用 Loader 按需加载页面,这应该可以节省一些内存。

所以问题是我想 "jump" 按一下按钮就可以转到某个故事和页面。我在示例中添加了几个按钮,它们已经跳转到不同的章节了。

您可以在章节之间垂直轻拂,也可以在每章的页面之间水平轻拂。

我遇到的问题是,我无法弄清楚如何让 ListView 包含的页面在 switching/loading 特定章节之后跳转到特定页面。基本上我遗漏了 pagesView.currentPage = 5 或类似的东西。

实现此功能的好方法是什么?

对应的QML。您可以 运行 使用 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
                        }
                    }
                }
            }

        }
    }
}

也许是这样的?

    // 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))

这样当加载程序加载了项目时,将调用访问项目的函数。

同时公开加载程序以便可以访问它:

Rectangle {
    //...
    property Loader loader : l
    Loader { 
        id: l
        // ...
    }
}

这是我的做法:

  1. pagesView 中定义允许它更新其外观和状态的属性:

    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. 定义将存储整个事物状态(即当前章节和页面)的属性:

    property int selectedChapter: 0
    property int selectedPage: 0
    
  3. 根据selectedChapter更新顶级ListView位置 属性:

    onSelectedChapterChanged: positionViewAtIndex(selectedChapter, ListView.Beginning)
    
  4. 创建时设置 pagesView 的属性:

    Loader {
        id: pagesViewLoader
        anchors.fill: parent
        sourceComponent: pageViewComponent
        onLoaded: {
            item.chapterIndex = Qt.binding(function() { return modelData })
            item.chapterView = chapterView
        }
    }
    
  5. 触发章节和页面更改自 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)
        }
    }
    

重要提示

注释 1

在第4步中modelData并没有直接设置,而是包装到Qt.binding函数调用中。这就是您绑定到来自 JavaScript 的值的方式。这是必要的,因为 ListView 重用它的委托实例,并且 pagesView 实例可以在单个 onLoaded 消息后以不同的 modelData 值重用。

注2

起初我在 onClicked 中使用了两个 属性 赋值,而没有将它们包装在 goTo 函数中:

chapterView.selectedChapter = chapter
chapterView.selectedPage = page

但这导致第二行出错 (chapterView is not defined)。当第一行执行时,chapterView 滚动到选定的章节,并且当前的委托项目从场景中删除,因为它不再需要。这个"removed from the scene"在技术上是怎么做的我也说不清楚,但是结果是在第一行之后chapterView就不再定义了。因此需要将这两个赋值包装在一个函数调用中。