Qt Quick中有很多方法进行场景的切换,比如说使用Loader切换source的办法,还有StackView中对界面item进行push和pop进行操作。甚至自己也可以写方法,对item的显隐进行操作。当然上述方法或多或少我都尝试过,但这些我都感到不太满意,我于是想自己写一个场景的管理类,来解决这个问题。
蒋彩阳原创文章,首发地址:http://blog.csdn.net/gamesdev/article/details/48139563。欢迎同行前来探讨。
在制作这个管理类之前,我提出了这些需求:
1、 必须做到懒加载。在之前的解决方案中,Loader和StackView只在通过设置容器的内容同时对内容进行一次实例化,这样符合懒加载的策略。而自己以前的界面切换方法(详细代码参见我以前的博客)将界面的初始化方法放在了程序初始化的时候,没有做到需要才加载,所以以前的方案不符合。
2、 界面实例删除的时机应该是程序关闭时。这个在Loader和StackView是不满足的,因为这些当进行界面切换的时候会将原有界面的实例删除,假设界面构造的成本很高(指的是花的时间很长),而且要重复使用,切换的时候就删除了,这样不划算。而自己以前的界面切换方法是满足的。
要满足这些需求,就要尝试在Qt Quick中寻找能够类似C++的new和delete这样的方法。好在Qt Quick提供了在Javascript中创建对象的方法Qt.createComponent和component.createObject,而创建了的对象想要删除也很方便,凡是这样创建的对象,都附带了destroy()方法,十分方便。
于是我花了一些时间制作了这样一个简易的界面切换类,提供类似StackView的使用方法,让我们感觉切换起来很方便。下面是该类的QML代码:
// ViewManager.qml import QtQuick 2.5 import "App.js" as App Item { id: viewManager property var initialItem property Item currentItem property var items: [ ] QtObject { id: internal property int duration: 500 property var fadeAnimation: ParallelAnimation { running: false OpacityAnimator { id: fadeInAnimator from: 0 to: 1 duration: internal.duration } OpacityAnimator { id: fadeOutAnimator from: 1 to: 0 duration: internal.duration } onStopped: { internal.transitionEnd( fadeInAnimator.target, fadeOutAnimator.target ); } } function transition( enterItem, properties ) { if ( viewManager.currentItem === enterItem ) return;// 没有任何操作 if ( properties && properties.animate === false ) { transitionEnd( enterItem, viewManager.currentItem ); } else// 默认使用动画效果 { enterItem.opacity = 0; enterItem.visible = true; viewManager.currentItem.opacity = 1; fadeInAnimator.target = enterItem; fadeOutAnimator.target = viewManager.currentItem; fadeAnimation.start( ); } } function transitionEnd( enterItem, exitItem ) { // 直接进行转换 if ( exitItem ) { exitItem.visible = false; exitItem.anchors.fill = undefined; } viewManager.currentItem = enterItem; } } function transition( itemURL, properties ) { // 判断是否在items中 for ( var i in items ) { if ( items[i].url === itemURL ) { internal.transition( items[i].object, properties ); return; } } var component = Qt.createComponent( itemURL ); if ( properties && properties.sync )// 仅限同步 { if ( component.status === Component.Ready ) { var item = component.createObject( parent, { "anchors.fill": parent } ); var info = { "url": itemURL, "component": component, "object": item }; if ( !items ) items = [ ];// 修复起始页面的时候items为undefined报的错误 items.push( info ); internal.transition( item, properties ); } else if ( component.status === Component.Error ) { App.log( component.errorString( ) ); } } else// 异步的 { var incubator = component.incubateObject( parent, { "anchors.fill": parent } ); if ( !incubator ) { App.log( component.errorString( ) ); } if ( incubator.status !== Component.Ready ) { incubator.onStatusChanged = function( status ) { if ( status === Component.Ready ) { var info = { "url": itemURL, "component": component, "object": incubator.object }; items.push( info ); internal.transition( incubator.object, properties ); } else if ( status === Component.Error ) { App.log( component.errorString( ) ); } } } else { // 这里可以执行一些等待载入的操作 } } } onInitialItemChanged: { if ( typeof( initialItem ) == "string" ) { transition( initialItem, { "animate": false } ); } else if ( typeof( initialItem ) == "object" ) { internal.transition( initialItem, { "animate": false } ); } } Component.onDestruction: { for ( var i in items ) { items[i].component.destroy( ); items[i].object.destroy( ); } } }
下面是使用的方法:
ViewManager { id: viewManager anchors.fill: parent initialItem: Qt.resolvedUrl( "SomePage.qml" ) Component.onCompleted: { Settings.viewManager = this; } }
切换的时候也很方便:
Settings.viewManager.transition(Qt.resolvedUrl( "TempMainView.qml" ), {"sync": true } );
上面的调用方法中,第二个参数是传入的额外的属性,其中{"sync": true }表示的是同步载入的方法,由于默认的是异步的载入方法,异步载入使用的是component.incubateObject()函数,这个函数目前的bug很多,比如说在构建绑定超过一定限制的对象实例以及界面元素过多过复杂界面的时候调用就会失败。因此有必要采用同步的方法载入界面,于是提供了{"sync": true }这样的属性设置方法。