全局变量,顾名思义就是在程序中到处都能使用的变量。这在一定程度上违背了“模块化设计”这个思想。在笔者刚接触编程的时候老师就说过全局变量有害,就跟goto
一样;但在实际工程中它其实很有用,使用得当的话反而能让整个软件结构更清晰、紧凑。本文结合实际经验向大家介绍在QML程序中如何有效使用全局变量。
全局变量的作用
首先要说明的是,我们这里说的全局变量不是整数、浮点数这样的简单变量,而是复杂的类对象。那什么时候会用到全局变量呢?在笔者的实践中,一般下面几种情况会用到全局变量:
- 资源共享、重用。整个应用程序相关的设置,比如说程序的版本、风格(theme)、字体资源等,这些数据适合放入一个全局变量,从而可以在整个程序的任何地方反复使用;
- 数据传递。全局变量在代码里都能访问到,相当于一块共享的内存空间,所以可以在不同的地方传递数据。但如果是多线程环境下的话,需要考虑好互斥;
- 事件驱动。我们可以将该变量申明为
QObject
子类,然后定义好信号与槽,然后在程序需要的地方连接这些信号或者调用槽函数,这样我们的程序就通过这个全局变量连接起来了。这就相当于有了一个核心驱动,比如只要一个信号发出来,程序中所有连接的槽函数就都会被调用,而无需关心是否漏掉了一个。
这几个特点加起来其实也就是Facebook所推崇的Flux设计模块,核心思想简单讲就是把程序的层级关系变得简单,单一驱动。如果你被综错复杂的模块化设计弄糊涂了,那可以试试Flux这种清晰明了的设计思路。
和C++中的使用全局变量相比,QML中使用起来更加方便,因为QML中有属性绑定,尤其是上面第三点,确实可以让我们的程序“智能化”很多。同时也要说明的是,这里说的“全局”并不是真的全局,而是指QML执行环境中的全局;C++中的全局变量不在本文讨论范围之内。
QML中定义全局变量
我们知道QML是需要QML引擎(即QQmlEngine
)来解释执行的,所以QML中的全局变量本质是QML执行上下文(QQmlContext
)的属性。定义QML全局变量也就是把我们的对象设置为QML执行上下文的属性。
有两种方式:单独定义,或者批量定义,其中批量定义又可分为C++形式和QML形式。我们把这些方法都介绍下。
单独定义
该方法主要步骤是:
定义一个
QObject
的子类,设计好它的信号、槽还有属性;在
main
函数里构建对象;-
在
QQmlEngine
构建之后还未加载任何QML文件之前,将该对象设置为执行上下文的属性,并取一个合理的名字:engine->rootContext()->setContextProperty("$hub", cppBackend);
这样$hub
就成为了QML中的全局变量,你可以直接使用它内部的各种元数据(信号、槽、属性、枚举类型等等)。
这里我们约定,用$
作为全局变量的开头字母,这个在JavaScript和QML中是合法的,便于我们区分普通局部变量和全局变量。
C++形式批量定义
如果我们的程序比较复杂,把功能都放在一个全局变量里不合适,我们可以将它们拆开来,用不同的C++类来实现,然后定义一个总的C++类,将这些功能类作为这个总类的属性,主要步骤是:
-
根据功能定义不同类,例如:
程序设置类:class Settings : public QObject{ Q_OBJECT public: Q_PROPERTY(QString appName MEMBER m_appName) private: QString m_appName = "MyApp"; }
和网络类:
class Networks : public QObject{ Q_OBJECT }
-
定义一个总的类:
class GlobalObject : public QObject{ Q_OBJECT public: Q_PROPERTY(Settings* settings MEMBER m_settings) Q_PROPERTY(Networks* networks MEMBER m_networks) private: Settings* m_settings; Networks* m_networks; }
-
在
main
函数中创建总类的对象:auto globalObject = new GlobalObject();
-
在
QQmlEngine
构建之后还未加载任何QML文件之前,将该对象设置为执行上下文对象:engine->rootContext()->setContextObject(globalObject);
QML中约定,contextObject
的所有属性都自动变为contextProperty
,就像他们用第一种方法单独定义一样。所以如果需要的功能比较多,建议使用批量定义的方法,更方便快捷。
需要注意,如果一个程序中同时使用了这两种方法定义全局变量而且有变量重名了,那么以单独定义的优先。
QML形式批量定义
可以用QML文件来代替上面的C++总类。
在定义好Settings
和Networks
之后,接下去的步骤改为:
-
将这些C++类注册到QML中:
qmlRegisterType<Settings>("MyCppBackend", 1, 0, "Settings"); qmlRegisterType<Networks>("MyCppBackend", 1, 0, "Networks");
-
然后新建一个QML文件:
//globalobject.qml import QtQuick 2.7 import MyCppBackend 1.0 QtObject { id: root property var $settings: Settings{} property var $networks: Networks{} }
-
将该QML文件作为QML引擎加载的第一个文件:
engine->load(QUrl("qrc:///globalobject.qml"));
QML引擎约定,加载的第一个QML文件就是contextObject
,所以和C++定义类似,它的属性也就成了contextProperty
。
和C++批量定义相比,QML批量定义有如下优势:
变量名前面可以加
$
,从而方便区分全局变量和局部变量,这个在C++定义属性的时候是不允许的;-
如果某个全局变量(一般是QML对象)构造很慢,可以通过QML中的
Loader
来很方便异步构造,从而加速程序启动:property var loader: Loader{ asynchronous: true source: "qrc:/UI/Main.qml" onLoaded: { // 当加载完毕会进入这里 } }
QML全局变量的中枢作用
最后我们结合批量定义中的例子来看下QML中全局变量起的数据、消息中枢作用。这个主要是利用了QML的属性绑定特性(Property Binding)。
假如说我们有两个QML文件:
//View1
import QtQuick 2.7
Item{
id: root
Text{
text: $settings.appName
}
}
和:
//View2
import QtQuick 2.7
import QtQuick.Controls 2.2
Rectangle{
id: root
TextField{
text: $settings.appName
}
Button{
text: "Click!"
onClick:{
$settings.appName = "New Name!";
}
}
}
View1
和View2
中的text都和$settings
中的appName
这个属性做了绑定。当我点击View2
中的按钮,$settings.appName
被修改,所有绑定的属性也就会自动更新,不会遗忘。由于$settings
是全局变量,这种用法可以深入到任意复杂、任意层级的界面中,非常方便。