- 归属
Qt Script在商业许可下,由Qt公司提供。此外,它还在自由软件许可下可用。自从Qt 5.4版本开始,这些自由软件许可证是GNU Lesser General Public License, version 3或 GNU General Public License, version 2。有关详细信息,请参阅Qt许可证。
此外,Qt 5.15.17中的Qt Script确实包含以下许可下的第三方模块:
GNU 库通用公共许可证 v2 或更高版本 |
以下类为Qt应用程序添加了脚本功能。
用于定义(一类)Qt Script对象的自定义行为的接口 | |
自定义Qt脚本对象的迭代器接口 | |
表示Qt脚本函数调用 | |
有关 QScriptContext 的其他信息 | |
执行Qt脚本代码的环境 | |
用于报告与 QScriptEngine 执行相关的事件的接口 | |
QScriptEngine 调试器 | |
封装Qt脚本程序 | |
充当 QScriptEngine 中“中间”字符串的句柄 | |
脚本语法检查的结果 | |
充当Qt Script 数据类型的容器 | |
QScriptValue的Java样式迭代器 | |
从Qt C++成员函数访问Qt脚本环境 |
Qt Script基于ECMAScript脚本语言,如标准ECMA-262中定义的那样。Microsoft的JScript和Netscape的JavaScript也基于ECMAScript标准。有关 ECMAScript 的概述,请参阅 ECMAScript reference .如果您不熟悉 ECMAScript 语言,则有几本涵盖此主题的现有教程和书籍,例如 JavaScript: The Definitive Guide
若要执行脚本代码,请创建一个QScriptEngine,并调用其evaluate()函数,将要计算的脚本代码(文本)作为参数传递。
QScriptEngine engine
qDebug() << "the magic number is:" << engine.evaluate("1 + 2").toNumber();
返回值将是计算的结果(表示为对象);这可以转换为标准的 C++ 和 Qt 类型。
通过向脚本引擎注册自定义属性,可以使脚本可用。通过设置脚本引擎的全局对象的属性,最容易做到这一点:
engine.globalObject().setProperty("foo", 123);
qDebug() << "foo times two is:" << engine.evaluate("foo * 2").toNumber();
这会将属性放置在脚本环境中,从而使它们可用于脚本代码。
任何基于QObject的实例都可以与脚本一起使用。
当一个 QObject对象被传递给QScriptEngine::newQObject()函数时,将创建一个Qt脚本包装器对象,该对象可用于使QObject的信号、插槽、属性和子对象可供脚本使用。
下面是一个示例,它使QObject子类的实例可用于名称为:"myObject"
QScriptEngine engine;
QObject *someObject = new MyObject;
QScriptValue objectValue = engine.newQObject(someObject);
engine.globalObject().setProperty("myObject", objectValue);
这将创建一个在脚本环境中调用的全局变量。该变量用作底层C++对象的代理。请注意,脚本变量的名称可以是任何名称;即它不依赖于QObject::objectName(),也就是myObject。
newQObject()函数接受两个额外的可选参数:一个是所有权模式,另一个是选项的集合,这些选项允许您控制包装 应该如何表现的某些方面。我们稍后将回到这些参数的用法。
Qt Script将Qt的核心功能用于脚本编写。在Qt Script中使用信号和插槽有三种主要方法:
- 混合 C++/脚本:C++应用程序代码将信号连接到脚本函数。例如,脚本函数可以是用户键入的函数,也可以是从文件中读取的函数。如果您有但不想将对象本身公开给脚本环境,则此方法非常有用;您只需要一个脚本能够定义信号应该如何响应,并将其留给应用程序的C++端来建立连接。
- 混合脚本/C++:脚本可以连接信号和插槽,以在应用程序向脚本环境公开的预定义对象之间建立连接。在此方案中,插槽本身仍是用C++ 编写的,但连接的定义是完全动态的(脚本定义)。
- 纯脚本定义:脚本既可以定义信号处理函数(实际上是“用Qt脚本编写的插槽”),也可以设置使用这些处理程序的连接。例如,脚本可以定义一个将处理 QL i呢Edit::returnPressed() 信号的函数,然后将该信号连接到脚本函数。
使用 qScriptConnect()函数将C++信号连接到脚本函数。在以下示例中,定义了一个脚本信号处理程序,用于处理QLineEdit::textChanged()信号:
QScriptEngine eng;
QLineEdit *edit = new QLineEdit(...);
QScriptValue handler = eng.evaluate("(function(text) { print('text was changed to', text); })");
qScriptConnect(edit, SIGNAL(textChanged(const QString &)), QScriptValue(), handler);
qScriptConnect()的前两个参数与传递给 QObject::connect()以建立正常 C++连接的参数相同。第三个参数是脚本对象,在调用信号处理程序时将充当对象;在上面的示例中,我们传递了一个无效的脚本值,因此该对象将是全局对象。第四个参数是脚本函数(“slot”)本身。下面的示例演示如何使用该参数:
QLineEdit *edit1 = new QLineEdit(...);
QLineEdit *edit2 = new QLineEdit(...);
QScriptValue handler = eng.evaluate("(function() { print('I am', this.name); })");
QScriptValue obj1 = eng.newObject();
obj1.setProperty("name", "the walrus");
QScriptValue obj2 = eng.newObject();
obj2.setProperty("name", "Sam");
qScriptConnect(edit1, SIGNAL(returnPressed()), obj1, handler);
qScriptConnect(edit2, SIGNAL(returnPressed()), obj2, handler);
我们创建两个QLineEdit对象并定义一个信号处理函数。连接使用相同的处理程序函数,但根据触发的对象信号,该函数将使用不同的对象调用,因此每个 print()语句的输出也不同。
在脚本代码中,Qt Script使用与熟悉的C++语法不同的语法来连接和断开信号;即QObject::connect()。若要连接到信号,请将相关信号引用为发送方对象的属性,并调用其函数。有三个重载,每个重载都有相应的重载。以下各小节介绍这三种形式。
connect():信号到功能连接
connect():信号到成员功能连接
disconnect():断开连接
信号到功能连接
connect(function)
在这种连接形式中,参数to是连接到信号的函数。connect()
function myInterestingScriptFunction() {
// ...
}
// ...
myQObject.somethingChanged.connect(myInterestingScriptFunction);
参数可以是Qt脚本函数,如上面的示例所示,也可以是插槽,如以下示例所示:
myQObject.somethingChanged.connect(myOtherQObject.doSomething);
当参数是QObject的槽函数时,信号和槽函数的参数类型不一定兼容;如有必要,Qt Script将执行信号参数的转换,以匹配插槽的参数类型。
要断开与信号的连接,请调用信号的函数,并将要断开连接的函数作为参数传递:disconnect()
myQObject.somethingChanged.disconnect(myInterestingFunction);
myQObject.somethingChanged.disconnect(myOtherQObject.doSomething);
当调用脚本函数以响应信号时,该对象将是全局对象。
信号到成员功能连接
connect(thisObject, function)
在这种形式的函数中,当调用使用第二个参数指定的函数时,第一个参数是将绑定到变量 的对象。connect()
如果窗体中有一个按钮,您通常希望执行与表单相关的操作以响应按钮的信号;在这种情况下,将表单作为对象传递是有意义的。
var obj = { x: 123 };
var fun = function() { print(this.x); };
myQObject.somethingChanged.connect(obj, fun);
要断开与信号的连接,请将相同的参数传递给:disconnect()
myQObject.somethingChanged.disconnect(obj, fun);
向命名成员函数连接发出信号
connect(thisObject, functionName)
在这种形式的函数中,第一个参数是当调用函数以响应信号时将绑定到变量 的对象。第二个参数指定连接到信号的函数的名称,这指的是作为第一个参数传递的对象的成员函数(在上面的方案中)。
请注意,该函数在建立连接时解析,而不是在发出信号时解析。
var obj = { x: 123, fun: function() { print(this.x); } };
myQObject.somethingChanged.connect(obj, "fun");
要断开与信号的连接,请将相同的参数传递给:disconnect()
myQObject.somethingChanged.disconnect(obj, "fun");
错误处理
当 或 成功时,函数将返回;否则,它将引发脚本异常。您可以从生成的对象中获取错误消息。例:connect()disconnect()undefinedError
try {
myQObject.somethingChanged.connect(myQObject, "slotThatDoesntExist");} catch (e) {
print(e);}
从脚本发出信号
要从脚本代码发出信号,您只需调用 signal 函数,并传递相关参数:
myQObject.somethingChanged("hello");
目前无法在脚本中定义新信号;即,所有信号都必须由 C++ 类定义。
过载信号和槽函数
当信号或槽函数过载时,Qt Script将尝试根据函数调用中涉及的参数的实际类型来选择正确的重载。例如,如果您的类具有 slots 和 ,则以下脚本代码将合理地运行:myOverloadedSlot(int)myOverloadedSlot(QString)
myQObject.myOverloadedSlot(10); // will call the int overloadmyQObject.myOverloadedSlot("10"); // will call the QString overload
可以通过使用 C++ 函数作为属性名称的数组样式属性访问来指定特定的重载:
myQObject['myOverloadedSlot(int)']("10"); // call int overload; the argument is converted to an intmyQObject['myOverloadedSlot(QString)'](10); // call QString overload; the argument is converted to a string
如果重载具有不同数量的参数,Qt Script将选择与传递给插槽的实际参数数最匹配的参数计数的重载。
对于过载的信号,如果您尝试按名称连接到信号,Qt Script将抛出错误;您必须参考具有要连接的特定过载的完整归一化特征的信号。
的属性可用作相应Qt Script对象的属性。在脚本代码中操作属性时,将自动调用该属性的 C++ get/set 方法。例如,如果 C++ 类具有声明如下的属性:
Q_PROPERTY(bool enabled READ enabled WRITE setEnabled)
然后,脚本代码可以执行如下操作:
myQObject.enabled = true;
// ...
myQObject.enabled = !myQObject.enabled;
默认情况下,每个命名的子项(即()不是空字符串)都可以作为Qt脚本包装器对象的属性使用。例如,如果您有一个 with a 子小部件,其属性为 ,则可以通过表达式在脚本代码中访问此对象objectName"okButton"
myDialog.okButton
由于 本身就是 ,因此可以在脚本代码中操作名称,例如,重命名对象:objectName
myDialog.okButton.objectName = "cancelButton";// from now on, myDialog.cancelButton references the button
您还可以使用这些函数并查找子项。这两个函数的行为分别与 () 和 () 相同。findChild()findChildren()
例如,我们可以使用这些函数来查找使用字符串和正则表达式的对象:
var okButton = myDialog.findChild("okButton");if (okButton != null) {
// do something with the OK button}
var buttons = myDialog.findChildren(RegExp("button[0-9]+"));for (var i = 0; i < buttons.length; ++i) {
// do something with buttons[i]}
在操作使用嵌套布局的窗体时,通常要使用;这样,脚本就与有关小部件位于哪个特定布局的详细信息隔离开来。findChild()
t Script使用垃圾回收来回收脚本对象在不再需要时使用的内存;当对象的内存不再在脚本环境中的任何位置被引用时,可以自动回收该对象的内存。Qt Script允许您控制回收包装器对象时底层C++会发生什么(即是否删除);在创建对象时,通过将所有权模式作为第二个参数传递给 () 来执行此操作。
了解Qt Script如何处理所有权很重要,因为它可以帮助您避免C++对象在应该删除时未删除(导致内存泄漏)的情况,或者在不应该删除时删除C++对象的情况(如果C++代码稍后尝试访问该对象,通常会导致崩溃)。
Qt所有权
默认情况下,脚本引擎不获取传递给 () 的 ;对象根据 Qt 的对象所有权进行管理(参见 )。例如,当包装属于应用程序核心的 C++ 对象时,此模式是合适的;也就是说,无论脚本环境中发生什么,它们都应该持久存在。另一种说法是,C++对象应该比脚本引擎更长久。
脚本所有权
指定为所有权模式将导致脚本引擎在确定这样做是安全的时(即,当脚本代码中没有更多对它的引用时)获得完全所有权并将其删除。如果 没有父对象,和/或 是在脚本引擎的上下文中创建的,并且不打算比脚本引擎更长,则此所有权模式是合适的。
例如,构造仅用于脚本环境的 QObject 的构造函数是一个很好的候选函数:
QScriptValue myQObjectConstructor(QScriptContext *context, QScriptEngine *engine){
// let the engine manage the new object's lifetime.
return engine->newQObject(new MyQObject(), QScriptEngine::ScriptOwnership);}
自动所有权
所有权取决于是否有父母。如果Qt脚本垃圾回收器发现在脚本环境中不再引用,则仅当它没有父级时才会被删除。
当其他人删除 QObject 时会发生什么?
包装可能会在Qt Script的控制之外被删除;即,不考虑指定的所有权模式。在这种情况下,包装器对象仍将是一个对象(与它包装的 C++ 指针不同,脚本对象不会变为 null)。但是,任何访问脚本对象属性的尝试都会导致引发脚本异常。
请注意,对于已删除的 ,() 仍将返回 true,因为它测试脚本对象的类型,而不是内部指针是否为非 null。换句话说,如果 () 返回 true,但 () 返回 null 指针,则表明 Qt 脚本之外的指针已被删除(可能是偶然的)。
QScriptEngine::newQObject()可以采用第三个参数,该参数允许您通过它返回的 QtScript包装器对象控制对Qt 脚本包装器对象的访问的各个方面。
QScriptEngine::ExcludeChildObjects指定的子对象不应显示为包装对象的属性。
QScriptEngine::ExcludeSuperClassProperties和QScriptEngine::ExcludeSuperClassMethods可用于避免公开从QOjbect的超类继承的成员。这对于定义“纯”接口很有用,从脚本的角度来看,继承的成员没有意义;例如,您不希望脚本作者能够更改对象的属性或调用插槽。
QScriptEngine::SkipMethodsInEnumeration 指定QObject中尚不存在的属性应创建为 的动态属性,而不是 Qt 脚本包装器对象的属性。如果希望新属性真正成为 的持久属性,而不是与包装对象一起销毁的属性(如果用 () 多次包装,则不会共享这些属性),则应使用此选项。
QScriptEngine::SkipMethodsInEnumeration指定在 for-in脚本语句中枚举QObject包装器的属性时应跳过信号和插槽。这在定义原型对象时很有用,因为按照惯例,原型的函数属性不应是可枚举的。
() 函数用于包装现有实例,以便它可用于脚本。另一种情况是,您希望脚本能够构造新对象,而不仅仅是访问现有对象。
Qt元类型系统目前不为基于-的类提供构造函数的动态绑定。如果你想从脚本中使这样的类成为新的,Qt Script可以为你生成一个合理的脚本构造函数;请参见 ()。
也可以用 () 包装自己的工厂函数,并添加到脚本环境中;有关示例,请参见 ()。
使用 Q_ENUMS 声明的枚举的值不能用作单个包装对象的属性;相反,它们是可以使用 () 创建的包装对象的属性。
下表描述了从QScriptValue到 C++类型的默认转换。
C++ 类型 | 默认转换 |
bool | |
int | |
uint | |
float | float(QScriptValue::toNumber()) |
double | |
short | short(QScriptValue::toInt32()) |
ushort | |
char | char(QScriptValue::toInt32()) |
uchar | unsigned char(QScriptValue::toInt32()) |
long | long(QScriptValue::toInteger()) |
ulong | ulong(QScriptValue::toInteger()) |
qlonglong | qlonglong(QScriptValue::toInteger()) |
qulonglong | qulonglong(QScriptValue::toInteger()) |
An empty string if the QScriptValue is null or undefined; QScriptValue::toString() otherwise. | |
QScriptValue::toDateTime().date() | |
如果QScriptValue是字符串,则结果是字符串的第一个字符,如果字符串为空,则结果为 null;否则,结果是Qchar构造自Unicode,它是通过将QSscriptValue转换为ushort。 | |
如果QScriptValue是数组,则结果是由每个数组元素的 () 的结果构造的;否则,结果为空 . | |
如果QScriptValue是数组,则结果是由每个数组元素的 () 的结果构造的;否则,结果为空 . | |
如果QScriptValue是一个对象,则结果是一个 (键,值)对的形式 (propertyName, propertyValue.toVariant()),用于遍历对象的属性。 | |
如果QScriptValue是数组,则结果是由每个数组元素的 () 的结果构造的;否则,结果为空 . | |
QList<int> | 如果QScriptValue是数组,则结果是一个 <int>,由每个数组元素的 () 的结果构造;否则,结果为空 <int>。 |
此外,Qt Script将处理以下情况:
- 如果 是 a 且目标类型名称以 (即,它是一个指针) 结尾,则指针将强制转换为带有 () 的目标类型。*
- 如果 是 a 且目标类型名称以 结尾(即,它是一个指针),而 的 是目标类型指向的类型,则结果是指向 的数据的指针。*
- 如果 是 a 并且可以转换为目标类型(根据 ()),则将使用 () 强制转换为目标类型。
6.2 从C++到Qt脚本的默认转换
下表描述了从 C++类型构造QScriptValue时的默认行为:
C++ 类型 | 默认构造 |
void | |
bool | QScriptValue(engine, value) |
int | QScriptValue(engine, value) |
uint | QScriptValue(engine, value) |
float | QScriptValue(engine, value) |
double | QScriptValue(engine, value) |
short | QScriptValue(engine, value) |
ushort | QScriptValue(engine, value) |
char | QScriptValue(engine, value) |
uchar | QScriptValue(engine, value) |
QScriptValue(engine, value) | |
long | 如果输入适合 int, (engine, int(value));否则,(engine, double(value))。请注意,后一种转换可能是有损的。 |
ulong | 如果输入适合 uint,(engine, uint(value));否则,(engine, double(value))。请注意,后一种转换可能是有损的。 |
qlonglong | (引擎,qsreal(值))。请注意,转换可能会导致精度损失,因为并非所有 64 位整数都可以使用 qsreal 类型表示。 |
qulonglong | (引擎,qsreal(值))。请注意,转换可能会导致精度损失,因为并非所有 64 位无符号整数都可以使用 qsreal 类型表示。 |
QScriptValue(this, value.unicode()) | |
QScriptEngine::newDate(value) | |
QScriptEngine::newDate(value) | |
QScriptEngine::newRegExp(value) | |
QScriptEngine::newQObject(value) | |
QScriptEngine::newQObject(value) | |
QScriptEngine::newVariant(value) | |
一个新的脚本数组(使用 () 创建),其元素是使用 ( *, ) 构造函数为列表的每个元素创建的。 | |
一个新的脚本数组(使用 () 创建),其元素是使用 () 为列表的每个元素创建的。 | |
一个新的脚本对象(使用 () 创建),其属性根据映射的 (键、值) 对进行初始化。 | |
一个新的脚本数组(使用 () 创建),其元素是使用 () 为列表的每个元素创建的。 | |
QList<int> | 一个新的脚本数组(使用 () 创建),其元素是使用 ( *, int) 构造函数为列表的每个元素创建的。 |
其他类型(包括自定义类型)将使用 () 进行包装。对于任何类型的空指针,结果为 ()。
使 C++ 类和对象可用于脚本语言并非易事,因为脚本语言往往比C++更具动态性,并且必须能够内省对象(在运行时查询函数名称、函数签名、属性等信息)。标准 C++ 不提供这方面的功能。
我们可以通过扩展C++来实现我们想要的功能,使用C++自己的工具,所以我们的代码仍然是标准的C++。Qt元对象系统提供了必要的附加功能。它允许我们使用扩展的 C++ 语法进行编写,但使用一个名为Moc (Meta-Object Compiler) 的小型实用程序将其转换为标准C++。希望利用元对象工具的类要么是 的子类,要么是使用宏。Qt已经使用这种方法很多年了,它已被证明是可靠和可靠的。Qt Script使用这种元对象技术为脚本编写者提供对C++类和对象的动态访问。
要完全理解如何使C++对象可用于Qt脚本,Qt元对象系统的一些基本知识非常有帮助。我们建议您阅读Qt对象模型和元对象系统 ,它们对于理解如何实现应用程序对象很有用。
然而,在最简单的情况下,这些知识并不是必需的。要使对象在Qt脚本中可用,它必须派生自QObject。所有派生自QObject的类都可以进行自省,并且可以在运行时提供脚本引擎所需的信息;例如,类名、函数、签名。因为我们在运行时动态获取了我们需要的关于类的信息,所以不需要为QObject派生类编写包装器。
元对象系统还可以在运行时动态提供有关信号和插槽的信息。默认情况下,对于QObject子类,只有信号和插槽会自动提供给脚本。这非常方便,因为在实践中,我们通常只想让脚本编写者使用专门选择的函数。创建QObject子类时,请确保要向Qt Script公开的函数是公共插槽。
例如,以下类定义仅允许为某些函数编写脚本:
class MyObject : public QObject{
Q_OBJECT
public:
MyObject( ... );
void aNonScriptableFunction();
public slots: // these functions (slots) will be available in Qt Script
void calculate( ... );
void setEnabled( bool enabled );
bool isEnabled() const;
private:
....
};
在上面的例子中,aNonScriptableFunction()没有被声明为插槽,所以它在Qt脚本中不可用。其他三个函数将自动在Qt Script中可用,因为它们在类public slots定义部分中声明。可以通过在声明函数时指定修饰符Q_INVOKABLE来使任何函数脚本可调用:
class MyObject : public QObject{
Q_OBJECT
public:
Q_INVOKABLE void thisMethodIsInvokableInQtScript();
void thisMethodIsNotInvokableInQtScript();
...};
一旦声明为 ,就可以从 Qt 脚本代码中调用该方法,就像它是一个插槽一样。虽然这样的方法不是槽,但你仍然可以在脚本代码中的调用中将其指定为目标函数; 接受本机和非本机函数作为目标。
如QtScript转C++ 类型中所述,Qt Script处理许多C++类型的转换。如果你的函数接受Qt Script不处理转换的参数,你需要提供转换函数。这是使用 qScriptRegisterMetaType() 函数完成的。
在前面的示例中,如果我们想使用Qt Script获取或设置属性,则必须编写如下代码:
var obj = new MyObject;
obj.setEnabled( true );
print( "obj is enabled: " + obj.isEnabled() );
脚本语言通常提供一种属性语法来修改和检索对象的属性(在本例中为启用状态)。许多脚本程序员都希望像这样编写上面的代码:
var obj = new MyObject;
obj.enabled = true;
print( "obj is enabled: " + obj.enabled );
为此,必须在 C++ QObject子类中定义属性。例如,下面的类声明声明一个名为enabled的布尔属性,该属性使用该函数作为其 setter 函数和 getter 函数:
class MyObject : public QObject{
Q_OBJECT
// define the enabled property
Q_PROPERTY( bool enabled WRITE setEnabled READ isEnabled )
public:
MyObject( ... );
void aNonScriptableFunction();
public slots: // these functions (slots) will be available in Qt Script
void calculate( ... );
void setEnabled( bool enabled );
bool isEnabled() const;
private:
....
};
与原始代码的唯一区别是使用宏Q_PROPERTY,该宏采用属性的类型和名称,以及 setter 和 getter 函数的名称作为参数。
如果您不希望在 Qt Script 中访问类的属性,请在声明该属性时将SCRIPTABLE该属性设置为false;默认情况下,该属性为true。
Q_PROPERTY(int nonScriptableProperty READ foo WRITE bar SCRIPTABLE false)
在Qt对象模型中,信号被用作QObject之间的通知机制。这意味着一个对象可以将信号连接到另一个对象的槽函数中,并且每次发射信号时,都会调用该槽函数。此连接是使用QObject::connect()函数建立的。
信号和插槽机制也可供Qt脚本程序员使用。在C++中声明信号的代码是相同的,无论信号是连接到 C++还是 Qt 脚本中的插槽。
class MyObject : public QObject{
Q_OBJECT
// define the enabled property
Q_PROPERTY( bool enabled WRITE setEnabled READ isEnabled )
public:
MyObject( ... );
void aNonScriptableFunction();
public slots: // these functions (slots) will be available in Qt Script
void calculate( ... );
void setEnabled( bool enabled );
bool isEnabled() const;
signals: // the signals
void enabledChanged( bool newState );
private:
....
};
我们在上一节中对代码所做的唯一更改是声明一个包含相关信号的信号部分。现在,脚本编写者可以定义一个函数并连接到对象,如下所示:
function enabledChangedHandler( b ){
print( "state changed to: " + b );}
function init(){
var obj = new MyObject();
// connect a script function to the signal
obj["enabledChanged(bool)"].connect(enabledChangedHandler);
obj.enabled = true;
print( "obj is enabled: " + obj.enabled );}
上一节描述了如何实现可以在Qt脚本中使用的C++对象。 应用程序对象是同一种对象,它们使应用程序的功能可供Qt脚本编写者使用。由于 C++ 应用程序已经是用 Qt 编写的,因此许多对象已经是 QObject。最简单的方法是简单地将所有这些 QObject 作为应用程序对象添加到脚本引擎中。对于小型应用程序,这可能就足够了,但对于大型应用程序,这可能不是正确的方法。问题在于,这种方法暴露了太多的内部 API,并允许脚本程序员访问不应公开的应用程序内部。
通常,使脚本编写者可以使用应用程序功能的最佳方式是编写一些 QObject,这些 QObject 使用信号、插槽和属性定义应用程序公共 API。这使您可以完全控制应用程序提供的功能。这些对象的实现只是调用应用程序中执行实际工作的函数。因此,与其将所有 QObject 都提供给脚本引擎,不如添加包装器 QObjects。
返回 QObject 指针
如果你有一个返回指针的插槽,你应该注意,默认情况下,Qt Script只处理QObject*和QWidget*类型的转换。这意味着,如果你的插槽是用“MyObject* getMyObject()”这样的签名声明的,Qt Script不会自动知道MyObject*应该以与QObject*和QWidget*相同的方式处理。解决此问题的最简单方法是在脚本接口的方法签名中仅使用QObject * 和 QWidget*。
或者,您可以使用 qScriptRegisterMetaType() 函数注册自定义类型的转换函数。通过这种方式,可以在 C++ 声明中保留精确键入,同时仍允许指向自定义对象的指针在 C++ 和脚本之间无缝流动。例:
class MyObject : public QObject{
Q_OBJECT
...};
Q_DECLARE_METATYPE(MyObject*)
QScriptValue myObjectToScriptValue(QScriptEngine *engine, MyObject* const &in){ return engine->newQObject(in); }
void myObjectFromScriptValue(const QScriptValue &object, MyObject* &out){ out = qobject_cast<MyObject*>(object.toQObject()); }
...
qScriptRegisterMetaType(&engine, myObjectToScriptValue, myObjectFromScriptValue);
在Qt Script中,函数是第一类值;它们是可以具有自己的属性的对象,就像任何其他类型的对象一样。它们可以存储在变量中,并作为参数传递给其他函数。当您想要定义和使用自己的脚本函数时,了解Qt脚本中的函数调用行为非常有用。本节讨论这个问题,并说明如何实现本机函数;也就是说,用C++编写的Qt脚本函数,而不是用脚本语言本身编写的函数。即使你主要依赖Qt Script提供的动态绑定,了解这些强大的概念和技术对于理解执行脚本函数时的实际情况也很重要。
从 C++ 调用 Qt 脚本函数
从 C++ 调用 Qt 脚本函数是通过 () 函数实现的。一个典型的方案是,你计算一个定义函数的脚本,在某个时候你想从 C++ 调用该函数,也许向它传递一些参数,然后处理结果。以下脚本定义了一个具有 toKelvin() 函数的 Qt 脚本对象:
({ unitName: "Celsius",
toKelvin: function(x) { return x + 273; }
})
toKelvin() 函数以开尔文为单位的温度作为参数,并返回转换为摄氏度的温度。以下代码片段显示了如何从 C++ 获取和调用 toKelvin() 函数:
QScriptValue object = engine.evaluate("({ unitName: 'Celsius', toKelvin: function(x) { return x + 273; } })");QScriptValue toKelvin = object.property("toKelvin");QScriptValue result = toKelvin.call(object, QScriptValueList() << 100);qDebug() << result.toNumber(); // 373
如果脚本定义了全局函数,则可以将该函数作为 () 的属性进行访问。例如,以下脚本定义了一个全局函数 add():
function add(a, b) {
return a + b;}
C++ 代码可能会按如下方式调用 add() 函数:
QScriptValue add = engine.globalObject().property("add");qDebug() << add.call(QScriptValue(), QScriptValueList() << 1 << 2).toNumber(); // 3
如前所述,函数只是Qt Script中的值;函数本身并不“绑定”到特定对象。这就是为什么您必须指定一个对象(()的第一个参数)应该应用该函数的原因。this
如果函数应该充当方法(即它只能应用于特定类别的对象),则由函数本身来检查它是否与兼容对象一起调用。this
将无效的参数传递给 () 表示应将全局对象用作对象;换句话说,该函数应作为全局函数调用。thisthis
对象this
当从脚本调用Qt脚本函数时,调用它的方式决定了执行函数体时的对象,如以下脚本示例所示:this
var getProperty = function(name) { return this[name]; };
name = "Global Object"; // creates a global variableprint(getProperty("name")); // "Global Object"
var myObject = { name: 'My Object' };print(getProperty.call(myObject, "name")); // "My Object"
myObject.getProperty = getProperty;print(myObject.getProperty("name")); // "My Object"
getProperty.name = "The getProperty() function";getProperty.getProperty = getProperty;getProperty.getProperty("name"); // "The getProperty() function"
需要注意的重要一点是,在Qt脚本中,与C++和Java不同,对象不是执行范围的一部分。这意味着成员函数(即操作的函数)必须始终使用关键字来访问对象的属性。例如,以下脚本可能无法执行所需的操作:thisthisthis
var o = { a: 1, b: 2, sum: function() { return a + b; } };print(o.sum()); // reference error, or sum of global variables a and b!!
您将收到一个引用错误,指出“a 未定义”,或者更糟糕的是,两个完全不相关的全局变量将用于执行计算(如果它们存在)。相反,脚本应如下所示:ab
var o = { a: 1, b: 2, sum: function() { return this.a + this.b; } };print(o.sum()); // 3
对于习惯了 C++ 和 Java 范围规则的程序员来说,意外省略关键字是一个典型的错误来源。this
包装本机函数
Qt Script提供()作为包装C++函数指针的一种方式;这使您能够在 C++ 中实现函数并将其添加到脚本环境中,以便脚本可以像调用“普通”脚本函数一样调用函数。以下是在 C++ 中编写上一个函数的方法:getProperty()
QScriptValue getProperty(QScriptContext *ctx, QScriptEngine *eng){
QString name = ctx->argument(0).toString();
return ctx->thisObject().property(name);}
调用 () 来包装函数。这将生成一种特殊类型的函数对象,该对象在内部携带指向 C++ 函数的指针。一旦生成的包装器被添加到脚本环境中(例如,通过将其设置为全局对象的属性),脚本就可以调用该函数,而不必知道也不必关心它实际上是一个本机函数。
请注意,C++ 函数的名称在脚本意义上无关紧要;脚本调用函数的名称仅取决于对存储函数包装器的脚本对象属性的调用。
目前无法包装成员函数;即,需要对象的 C++ 类的方法。this
QScriptContext 对象
A 保存与函数的特定调用关联的所有状态。通过 ,您可以:
- 获取传递给函数的参数。
- 获取对象。this
- 找出函数是否与运算符一起调用(其重要性将在后面解释)。new
- 引发脚本错误。
- 获取正在调用的函数对象。
- 获取激活对象(用于保存局部变量的对象)。
以下各节介绍如何使用此功能。
处理函数参数
关于函数参数,有两件事值得注意:
- 任何脚本函数(包括本机函数)都可以使用任意数量的参数进行调用。这意味着,如有必要,由函数本身检查参数计数,并采取相应的行动(例如,如果参数数量太大,则抛出错误,如果数量太少,则准备默认值)。
- 任何类型的值都可以作为任何函数的参数提供。这意味着,如有必要,您可以检查参数的类型,并采取相应的行动(例如,如果参数不是某种类型的对象,则抛出错误)。
总之:Qt Script不会自动对函数调用中涉及的参数数量或类型施加任何约束。
形式参数和 Arguments 对象
原生Qt脚本函数类似于脚本函数,它不定义任何形式参数,仅使用内置变量来处理其参数。为了看到这一点,让我们首先考虑脚本通常如何定义一个函数,该函数接受两个参数,将它们相加并返回结果:argumentsadd()
function add(a, b) {
return a + b;}
当使用形式参数定义脚本函数时,它们的名称可以被视为对象属性的别名;例如,在定义的函数体中,并引用相同的变量。这意味着该函数可以等效地编写如下:argumentsadd(a, b)aarguments[0]add()
function add() {
return arguments[0] + arguments[1];}
后一种形式与本机实现的典型外观非常匹配:
QScriptValue add(QScriptContext *ctx, QScriptEngine *eng){
double a = ctx->argument(0).toNumber();
double b = ctx->argument(1).toNumber();
return a + b;}
检查参数数
同样,请记住,函数定义中存在(或缺少)正式参数名称不会影响函数的调用方式; 是引擎允许的,就像.就函数而言,该函数确实需要两个参数才能做一些有用的事情。这可以通过脚本定义表示如下:add(1, 2, 3)add(42)add()
function add() {
if (arguments.length != 2)
throw Error("add() takes exactly two arguments");
return arguments[0] + arguments[1];}
如果脚本使用两个参数以外的任何参数进行调用,这将导致引发错误。可以修改本机函数以执行相同的检查:add()
QScriptValue add(QScriptContext *ctx, QScriptEngine *eng){
if (ctx->argumentCount() != 2)
return ctx->throwError("add() takes exactly two arguments");
double a = ctx->argument(0).toNumber();
double b = ctx->argument(1).toNumber();
return a + b;}
检查参数的类型
除了期望一定数量的参数之外,函数还可能期望这些参数具有某些类型(例如,第一个参数是数字,第二个参数是字符串)。此类函数应显式检查参数类型和/或执行转换,或者在参数类型不兼容时引发错误。
事实上,上面所示的本机实现与脚本对应物的语义并不完全相同;这是因为Qt Script运算符的行为取决于其操作数的类型(例如,如果其中一个操作数是字符串,则执行字符串连接)。为了给脚本函数提供更严格的语义(即,它应该只添加数字操作数),可以测试参数类型:add()+
function add() {
if (arguments.length != 2)
throw Error("add() takes exactly two arguments");
if (typeof arguments[0] != "number")
throw TypeError("add(): first argument is not a number");
if (typeof arguments[1] != "number")
throw TypeError("add(): second argument is not a number");
return arguments[0] + arguments[1];}
然后,像这样的调用将导致抛出错误。add("foo", new Array())
C++ 版本可以调用 () 来执行类似的测试:
QScriptValue add(QScriptContext *ctx, QScriptEngine *eng){
if (ctx->argumentCount() != 2)
return ctx->throwError("add() takes exactly two arguments");
if (!ctx->argument(0).isNumber())
return ctx->throwError(QScriptContext::TypeError, "add(): first argument is not a number");
if (!ctx->argument(1).isNumber())
return ctx->throwError(QScriptContext::TypeError, "add(): second argument is not a number");
double a = ctx->argument(0).toNumber();
double b = ctx->argument(1).toNumber();
return a + b;}
不太严格的脚本实现可能会满足于在应用运算符之前执行显式到数字的转换:+
function add() {
if (arguments.length != 2)
throw Error("add() takes exactly two arguments");
return Number(arguments[0]) + Number(arguments[1]);}
在本机实现中,这相当于调用 () 而不先执行任何类型测试,因为 () 将在必要时自动执行类型转换。
为了检查参数是否属于某个对象类型(类),脚本可以使用运算符(例如,如果第一个参数是 Array 对象,则计算结果为 true);本机函数可以调用 ()。instanceof"arguments[0] instanceof Array"
要检查参数是否为自定义 C++ 类型,通常使用 () 并检查结果是否有效。对于对象类型,这意味着强制转换为指针并检查它是否为非零;对于值类型,类应具有 或类似的方法。或者,由于大多数自定义类型都是以 s 为单位传输的,因此可以检查脚本值是否为 using (),然后检查是否可以使用 () 将 转换为您的类型。isNull()isValid()
具有可变参数数的函数
由于内置对象的存在,实现采用可变数量的参数的函数很简单。事实上,正如我们所看到的,从技术意义上讲,所有Qt脚本函数都可以看作是变量参数函数。例如,考虑一个 concat() 函数,该函数接受任意数量的参数,将参数转换为它们的字符串表示并连接结果;例如,将返回“Qt Script 101”。的脚本定义可能如下所示:argumentsconcat("Qt", " ", "Script ", 101)concat()
function concat() {
var result = "";
for (var i = 0; i < arguments.length; ++i)
result += String(arguments[i]);
return result;}
下面是一个等效的本机实现:
QScriptValue concat(QScriptContext *ctx, QScriptEngine *eng){
QString result = "";
for (int i = 0; i < ctx->argumentCount(); ++i)
result += ctx->argument(i).toString();
return result;}
可变数量的参数的第二个用例是实现可选参数。以下是脚本定义通常执行此操作的方式:
function sort(comparefn) {
if (comparefn == undefined)
comparefn = fn; /* replace fn with the built-in comparison function */
else if (typeof comparefn != "function")
throw TypeError("sort(): argument must be a function");
// ...}
这是本机等价物:
QScriptValue sort(QScriptContext *ctx, QScriptEngine *eng){
QScriptValue comparefn = ctx->argument(0);
if (comparefn.isUndefined())
comparefn = /* the built-in comparison function */;
else if (!comparefn.isFunction())
return ctx->throwError(QScriptContext::TypeError, "sort(): argument is not a function");
...}
可变数量的参数的第三个用例是模拟 C++ 重载。这涉及检查函数体开头的参数数量和/或其类型(如已显示),并采取相应措施。在执行此操作之前,可能值得三思而后行,而是选择唯一的函数名称;例如,具有单独的函数而不是泛型函数。在调用方端,这使得脚本更难意外调用错误的重载(因为它们不知道或不理解您的自定义复杂重载解析规则),而在被调用方端,您可以避免需要潜在的复杂(阅读:容易出错)检查来解决歧义。processNumber(number)processString(string)process(anything)
访问 Arguments 对象
大多数本机函数使用 () 函数来访问函数参数。但是,也可以通过调用 () 函数来访问内置对象本身(脚本代码中变量引用的对象)。这有三个主要应用:argumentsarguments
- 该对象可用于轻松地将函数调用转发到另一个函数。在脚本代码中,这是它通常的样子:arguments
function foo() {
// Let bar() take care of this.
print("calling bar() with " + arguments.length + "arguments");
var result = bar.apply(this, arguments);
print("bar() returned" + result);
return result;}
例如,将导致函数执行等效的 。如果你想在调用函数时执行一些特殊的预处理或后处理(例如,记录调用而不必修改函数本身,如上面的例子),或者如果你想从具有完全相同“签名”的原型函数调用“基本实现”,这将非常有用。在 C++ 中,转发函数可能如下所示:foo(10, 20, 30)foo()bar(10, 20, 30)bar()bar()
QScriptValue foo(QScriptContext *ctx, QScriptEngine *eng){
QScriptValue bar = eng->globalObject().property("bar");
QScriptValue arguments = ctx->argumentsObject();
qDebug() << "calling bar() with" << arguments.property("length").toInt32() << "arguments";
QScriptValue result = bar.apply(ctx->thisObject(), arguments);
qDebug() << "bar() returned" << result.toString();
return result;}
- arguments 对象可以用作 的输入,提供一种循环访问参数的通用方法。例如,调试器可以使用它来在通用的“Qt Script Object Explorer”中显示参数对象。
- arguments 对象可以序列化(例如,使用 JSON)并传输到另一个实体(例如,在另一个线程中运行的脚本引擎),其中对象可以反序列化并作为参数传递给另一个脚本函数。
构造函数
某些脚本函数是构造函数;它们应初始化新对象。以下代码片段是一个小示例:
function Book(isbn) {
this.isbn = isbn;}
var coolBook1 = new Book("978-0131872493");var coolBook2 = new Book("978-1593271473");
构造函数没有什么特别之处。事实上,任何脚本函数都可以充当构造函数(即,任何函数都可以充当 的操作数)。某些函数的行为会有所不同,具体取决于它们是否作为表达式的一部分被调用;例如,表达式将创建一个 Number 对象,而将执行类型转换。其他函数,如 ,将始终创建并初始化一个新对象(例如,并具有相同的效果)。newnewnew Number(1)Number("123")Array()new Array()Array()
原生Qt脚本函数可以调用()函数来确定它是作为构造函数还是作为常规函数调用。当函数作为构造函数调用时(即,它是表达式中的操作数),这有两个重要的含义:new
- 对象 () 包含要初始化的新对象;引擎会在调用函数之前自动创建此新对象。这意味着,当本机构造函数作为构造函数调用时,通常不必(也不应该)创建新对象,因为引擎已经准备了一个新对象。相反,您的函数应该对提供的对象进行操作。thisthis
- 构造函数应返回一个未定义的值 (),以告诉引擎该对象应该是运算符的最终结果。或者,该函数可以返回对象本身。thisnewthis
当 () 返回 false 时,构造函数如何处理这种情况取决于您想要的行为。如果与内置函数一样,普通函数调用应执行其参数的类型转换,则执行转换并返回结果。另一方面,如果希望构造函数的行为就像是作为构造函数调用的一样 (with ),则必须显式创建一个新对象(即忽略该对象),初始化该对象并返回它。Number()newthis
下面的示例实现一个构造函数,该函数始终创建并初始化新对象:
QScriptValue Person_ctor(QScriptContext *ctx, QScriptEngine *eng){
QScriptValue object;
if (ctx->isCalledAsConstructor()) {
object = ctx->thisObject();
} else {
object = eng->newObject();
object.setPrototype(ctx->callee().property("prototype"));
}
object.setProperty("name", ctx->argument(0));
return object;}
给定此构造函数,脚本将能够使用表达式或创建新对象;两者的行为方式相同。new Person("Bob")Person("Bob")Person
对于脚本代码中定义的函数,没有等效的方法来确定它是否作为构造函数被调用。
请注意,即使它不被认为是好的做法,但当函数作为构造函数调用并创建自己的对象时,没有什么可以阻止您选择忽略默认构造的 () 对象;只需让构造函数返回该对象即可。该对象将“覆盖”引擎构建的默认对象(即,默认对象将在内部被丢弃)。this
将数据与函数关联
即使一个函数是全局的(即,不与任何特定(类型的)对象关联),你可能仍然希望将一些数据与它相关联,以便它变得独立;例如,该函数可能具有指向它需要访问的某些 C++ 资源的指针。如果您的应用程序仅使用单个脚本引擎,或者可以/应该在所有脚本引擎之间共享相同的 C++ 资源,则只需使用静态 C++ 变量并从本机 Qt 脚本函数中访问它。
如果静态 C++ 变量或单一实例类不合适,可以在函数对象上调用 (),但请注意,脚本代码也可以访问这些属性。另一种方法是使用 ();此数据不可通过脚本访问。实现可以通过 () 函数访问此内部数据,该函数返回正在调用的函数对象。以下示例演示如何使用它:
QScriptValue rectifier(QScriptContext *ctx, QScriptEngine *eng){
QRectF magicRect = qscriptvalue_cast<QRectF>(ctx->callee().data());
QRectF sourceRect = qscriptvalue_cast<QRectF>(ctx->argument(0));
return eng->toScriptValue(sourceRect.intersected(magicRect));}
...
QScriptValue fun = eng.newFunction(rectifier);QRectF magicRect = QRectF(10, 20, 30, 40);
fun.setData(eng.toScriptValue(magicRect));
eng.globalObject().setProperty("rectifier", fun);
本机函数作为函数的参数
如前所述,一个函数对象可以作为参数传递给另一个函数;当然,对于本机函数也是如此。例如,下面是一个本机比较函数,它以数字方式比较其两个参数:
QScriptValue myCompare(QScriptContext *ctx, QScriptEngine *eng){
double first = ctx->argument(0).toNumber();
double second = ctx->argument(1).toNumber();
int result;
if (first == second)
result = 0;
else if (first < second)
result = -1;
else
result = 1;
return result;}
上述函数可以作为参数传递给标准函数,以对数组进行数字排序,如以下 C++ 代码所示:Array.prototype.sort
QScriptEngine eng;QScriptValue comparefn = eng.newFunction(myCompare);QScriptValue array = eng.evaluate("new Array(10, 5, 20, 15, 30)");
array.property("sort").call(array, QScriptValueList() << comparefn);
// prints "5,10,15,20,30"qDebug() << array.toString();
请注意,在本例中,我们真正将本机函数对象视为一个值,即,我们不将其存储为脚本环境的属性,我们只是将其作为“匿名”参数传递给另一个脚本函数,然后忘记它。
激活对象
每个Qt脚本函数调用都有一个与之关联的激活对象;此对象可通过 () 函数访问。激活对象是一个脚本对象,其属性是与调用关联的局部变量(包括脚本函数具有相应正式参数名称的参数)。因此,从 C++ 获取、修改、创建和删除局部变量是使用常规 () 和 () 函数完成的。激活对象本身不能从脚本代码直接访问(但每当读取或写入局部变量时,都会隐式访问它)。
对于 C++ 代码,激活对象有两个主要应用程序:
- 激活对象提供了一种标准方法来遍历与函数调用关联的变量,方法是将其用作 的输入。这对于调试目的很有用。
- 激活对象可用于准备在内联计算脚本时应可用的局部变量;这可以看作是将参数传递给脚本本身的一种方式。此技术通常与 () 结合使用,如以下示例所示:
QScriptContext *ctx = eng.pushContext();QScriptValue act = ctx->activationObject();
act.setProperty("digit", 7);
qDebug() << eng.evaluate("digit + 1").toNumber(); // 8
eng.popContext();
我们创建一个临时执行上下文,为其创建一个局部变量,评估脚本,最后恢复旧上下文。
属性采纳器和设置器
脚本对象属性可以根据 getter/setter 函数进行定义,类似于 Qt C++ 属性具有与之关联的读写函数的方式。这使得脚本可以使用表达式,例如 而不是 ;每当访问该属性时,都会隐式调用 的 Getter/setter 函数。对于脚本,该属性的外观和行为与常规对象属性类似。object.xobject.getX()x
单个Qt脚本函数可以充当属性的getter和setter。当它作为 getter 调用时,参数计数为 0。当它作为 setter 调用时,参数计数为 1;参数是属性的新值。在以下示例中,我们定义了一个本机组合的 getter/setter,它稍微转换了值:
QScriptValue getSet(QScriptContext *ctx, QScriptEngine *eng){
QScriptValue obj = ctx->thisObject();
QScriptValue data = obj.data();
if (!data.isValid()) {
data = eng->newObject();
obj.setData(data);
}
QScriptValue result;
if (ctx->argumentCount() == 1) {
QString str = ctx->argument(0).toString();
str.replace("Roberta", "Ken");
result = str;
data.setProperty("x", result);
} else {
result = data.property("x");
}
return result;}
该示例使用对象的内部数据来存储和检索转换后的值。或者,该属性可以存储在对象本身的另一个“隐藏”属性中(例如,)。本机函数可以自由地实现它想要的任何存储方案,只要属性本身的外部行为是一致的(例如,脚本不应该能够将其与常规属性区分开来)。__x__
以下 C++ 代码演示如何根据本机 getter/setter 定义对象属性:
QScriptEngine eng;QScriptValue obj = eng.newObject();
obj.setProperty("x", eng.newFunction(getSet),
QScriptValue::PropertyGetter|QScriptValue::PropertySetter);
访问该属性时,如以下脚本所示,geter/setter 在后台执行其工作:
obj.x = "Roberta sent me";print(obj.x); // "Ken sent me"obj.x = "I sent the bill to Roberta";print(obj.x); // "I sent the bill to Ken"
注意: 重要的是,setter 函数(而不仅仅是 getter)返回属性的值;即,setter 不应返回 .这是因为属性赋值的结果是 setter 返回的值,而不是右侧表达式。另请注意,通常不应尝试读取 getter 在 getter 本身中修改的相同属性,因为这将导致递归调用 getter。
您可以通过调用 () 来删除属性 getter/setter,将无效的作为 getter/setter 传递。请记住指定 / 标志,否则唯一会发生的事情是 setter 将被调用,其参数为 invalid!
也可以通过脚本代码定义和安装属性 getter 和 setter,如以下示例所示:
obj = {};obj.__defineGetter__("x", function() { return this._x; });obj.__defineSetter__("x", function(v) { print("setting x to", v); this._x = v; });obj.x = 123;
Getter 和 setter 只能用于实现“先验属性”;也就是说,该技术不能用于对对象尚不具有的属性的访问做出反应。要以这种方式获得对属性访问的完全控制,您需要将 子类化 。
在 ECMAScript 中,继承基于共享原型对象的概念;这与 C++ 程序员熟悉的基于类的继承完全不同。使用Qt Script,您可以使用()将自定义原型对象与C++类型相关联;这是为该类型提供脚本接口的关键。由于Qt脚本模块建立在Qt的元类型系统之上,因此可以对任何C++类型完成此操作。
您可能想知道何时需要在应用程序中使用此功能;()提供的自动绑定还不够吗?不,并非在所有情况下。首先,并非每个 C++ 类型都派生自 ;不是QObject的类型不能通过Qt的元对象系统进行内省(它们没有属性、信号和插槽)。其次,即使类型是 -派生的,您想要向脚本公开的功能也可能并非全部可用,因为将每个函数定义为插槽是不寻常的(并且更改 C++ API 并非总是可能/可取的)。
使用“传统”C++技术完全可以解决这个问题。例如,通过创建具有 、 、属性等的基于 C++ 的包装类,可以有效地使该类成为可编写脚本的类,该类将属性访问和函数调用转发到包装值。然而,正如我们将看到的,通过利用 ECMAScript 对象模型并将其与 Qt 的元对象系统相结合,我们可以得到一个更优雅、一致和轻量级的解决方案,并由一个小的 API 支持。xywidth
本节介绍基于原型的继承的基本概念。一旦理解了这些概念,相关的实践就可以在整个Qt Script API中应用,以便创建与C++的良好,一致的绑定,这将很好地适应ECMAScript世界。
在试验Qt脚本对象和继承时,使用Qt脚本示例中包含的交互式解释器可能会有所帮助,该解释器位于.examples/script
原型对象和共享属性
Qt Script原型对象的目的是定义应由一组其他Qt Script对象共享的行为。我们说共享相同原型对象的对象属于同一个类(同样,在技术方面,这不应该与 C++ 和 Java 等语言的类结构混淆;ECMAScript 没有这样的构造)。
基于原型的基本继承机制的工作原理如下:每个Qt Script对象都有一个指向另一个对象的内部链接,即其原型。当在对象中查找属性,而对象本身没有该属性时,将改为在原型对象中查找该属性;如果原型具有该属性,则返回该属性。否则,将在原型对象的原型中查找属性,依此类推;这个对象链构成了一个原型链。遵循原型对象的链,直到找到属性或到达链的末端。
例如,当您通过表达式创建一个新对象时,生成的对象将具有标准原型 ;通过此原型关系,新对象继承了一组属性,包括 function 和 function:new Object()ObjectObject.prototypehasOwnProperty()toString()
var o = new Object();o.foo = 123;print(o.hasOwnProperty('foo')); // trueprint(o.hasOwnProperty('bar')); // falseprint(o); // calls o.toString(), which returns "[object Object]"
函数本身没有定义(因为我们没有给 )赋值任何东西,所以取而代之的是调用标准原型中的函数,它返回 (“[object Object]”) 的高度通用字符串表示形式。toString()oo.toStringtoString()Objecto
请注意,原型对象的属性不会复制到新对象;仅保留从新对象到原型对象的链接。这意味着对原型对象所做的更改将立即反映在将修改对象作为其原型的所有对象的行为中。
在基于原型的 Universe 中定义类
在Qt Script中,没有显式定义类;没有关键字。相反,您可以分两个步骤定义一个新类:class
- 定义一个将初始化新对象的构造函数。
- 设置定义类接口的原型对象,并将此对象分配给构造函数的公共属性。prototype
通过这种安排,构造函数的公共属性将自动设置为通过将运算符应用于构造函数而创建的对象的原型;例如,创建的对象的原型将是 的值。prototypenewnew Foo()Foo.prototype
不对对象进行操作的函数(“静态”方法)通常存储为构造函数的属性,而不是原型对象的属性。常量(如枚举值)也是如此。this
下面的代码为一个名为 :Person
function Person(name){
this.name = name;}
接下来,你要设置为你的原型对象;即,定义所有对象都应该通用的接口。Qt Script自动为每个脚本函数创建一个默认的原型对象(通过表达式);您可以向此对象添加属性,也可以分配自己的自定义对象。(一般来说,任何Qt Script对象都可以作为任何其他对象的原型。Person.prototypePersonnew Object()
下面是一个示例,说明您可能希望重写继承自 的函数,以便为您的对象提供更合适的字符串表示形式:toString()Person.prototypeObject.prototypePerson
Person.prototype.toString = function() { return "Person(name: " + this.name + ")"; }
这类似于在 C++ 中重新实现虚拟函数的过程。从此以后,当在对象中查找命名的属性时,它将在 中解析,而不是像以前那样在 中解析:toStringPersonPerson.prototypeObject.prototype
var p1 = new Person("John Doe");var p2 = new Person("G.I. Jane");print(p1); // "Person(name: John Doe)"print(p2); // "Person(name: G.I. Jane)"
关于一个对象,我们还可以了解其他一些有趣的事情:Person
print(p1.hasOwnProperty('name')); // 'name' is an instance variable, so this returns trueprint(p1.hasOwnProperty('toString')); // returns false; inherited from prototypeprint(p1 instanceof Person); // trueprint(p1 instanceof Object); // true
该函数不是从 继承的,而是从 继承的,而 是 本身的原型;即,对象的原型链后面跟着 。这个原型链建立了一个类层次结构,如应用运算符所示; 通过遵循左侧对象的原型链,检查右侧构造函数的公共属性的值是否达到。hasOwnProperty()Person.prototypeObject.prototypePerson.prototypePersonPerson.prototypeObject.prototypeinstanceofinstanceofprototype
在定义子类时,可以使用一种通用模式。下面的示例显示了如何创建 called 的子类:PersonEmployee
function Employee(name, salary){
Person.call(this, name); // call base constructor
this.salary = salary;}
// set the prototype to be an instance of the base classEmployee.prototype = new Person();
// initialize prototypeEmployee.prototype.toString = function() {
// ...}
同样,您可以使用 来验证 和 之间的类关系是否已正确建立:instanceofEmployeePerson
var e = new Employee("Johnny Bravo", 5000000);print(e instanceof Employee); // trueprint(e instanceof Person); // trueprint(e instanceof Object); // trueprint(e instanceof Array); // false
这表明对象的原型链与对象的原型链相同,但添加到链的前面。EmployeePersonEmployee.prototype
使用Qt Script C++ API进行基于原型的编程
您可以使用 () 来包装本机函数。实现构造函数时,还可以将原型对象作为参数传递给 ()。您可以调用 () 来调用构造函数,如果需要调用基类构造函数,则可以在本机构造函数中使用 ()。
该类提供了一种根据 C++ 插槽和属性实现原型对象的便捷方法。看看这是如何完成的。或者,原型功能可以根据独立的本机函数来实现,这些函数用 () 包装,并通过调用 () 设置为原型对象的属性。
在原型函数的实现中,使用 () (或 ()) 来获取对正在操作的引用;然后调用 () 将其强制转换为 C++ 类型,并使用该类型的常用 C++ API 执行相关操作。
通过调用 (),将原型对象与 C++ 类型相关联。一旦建立了这种映射,当这种类型的值被包装在一个 ;显式调用 () 时,或者从 C++ 插槽返回此类类型的值并由引擎在内部传递回脚本代码时。这意味着,如果使用此方法,则不必实现包装类。
举个例子,让我们考虑一下如何根据Qt Script API实现上一节中的类。我们从本机构造函数开始:Person
QScriptValue Person_ctor(QScriptContext *context, QScriptEngine *engine){
QString name = context->argument(0).toString();
context->thisObject().setProperty("name", name);
return engine->undefinedValue();}
下面是我们之前看到的函数的原生等效物:Person.prototype.toString
QScriptValue Person_prototype_toString(QScriptContext *context, QScriptEngine *engine){
QString name = context->thisObject().property("name").toString();
QString result = QString::fromLatin1("Person(name: %0)").arg(name);
return result;}
然后,可以按如下方式初始化该类:Person
QScriptEngine engine;QScriptValue ctor = engine.newFunction(Person_ctor);
ctor.property("prototype").setProperty("toString", engine.newFunction(Person_prototype_toString));QScriptValue global = engine.globalObject();global.setProperty("Person", ctor);
子类的实现与此类似。我们使用 () 来调用超类 (Person) 构造函数:Employee
QScriptValue Employee_ctor(QScriptContext *context, QScriptEngine *engine){
QScriptValue super = context->callee().property("prototype").property("constructor");
super.call(context->thisObject(), QScriptValueList() << context->argument(0));
context->thisObject().setProperty("salary", context->argument(1));
return engine->undefinedValue();}
然后,可以按如下方式初始化该类:Employee
QScriptValue empCtor = engine.newFunction(Employee_ctor);
empCtor.setProperty("prototype", global.property("Person").construct());global.setProperty("Employee", empCtor);
在实现类的原型对象时,您可能希望使用该类,因为它使您能够根据 Qt 属性、信号和插槽定义脚本类的 API,并自动处理 Qt 脚本和 C++ 端之间的值转换。
为基于值的类型实现原型对象
当为基于值的类型实现原型对象时(例如 ),相同的通用技术适用;您可以使用应在实例之间共享的功能填充原型对象。然后,通过调用 () 将原型对象与类型相关联。这确保了例如,当相关类型的值从插槽返回到脚本时,脚本值的原型链接将被正确初始化。
当自定义类型的值存储在QVariants中时 - Qt Script默认执行此操作 --, () 使您能够安全地将脚本值转换为指向C++类型的指针。这使得执行类型检查变得容易,并且对于应修改基础 C++ 值的原型函数,允许您修改脚本值中包含的实际值(而不是脚本值的副本)。
Q_DECLARE_METATYPE(QPointF)
Q_DECLARE_METATYPE(QPointF*)
QScriptValue QPointF_prototype_x(QScriptContext *context, QScriptEngine *engine){
// Since the point is not to be modified, it's OK to cast to a value here
QPointF point = qscriptvalue_cast<QPointF>(context->thisObject());
return point.x();}
QScriptValue QPointF_prototype_setX(QScriptContext *context, QScriptEngine *engine){
// Cast to a pointer to be able to modify the underlying C++ value
QPointF *point = qscriptvalue_cast<QPointF*>(context->thisObject());
if (!point)
return context->throwError(QScriptContext::TypeError, "QPointF.prototype.setX: this object is not a QPointF");
point->setX(context->argument(0).toNumber());
return engine->undefinedValue();}
实现基于值的类型的构造函数
可以通过包装本机工厂函数来实现基于值的类型的构造函数。例如,以下函数实现一个简单的构造函数:
QScriptValue QPoint_ctor(QScriptContext *context, QScriptEngine *engine){
int x = context->argument(0).toInt32();
int y = context->argument(1).toInt32();
return engine->toScriptValue(QPoint(x, y));}
...
engine.globalObject().setProperty("QPoint", engine.newFunction(QPoint_ctor));
在上面的代码中,我们稍微简化了一些事情,例如,我们没有检查参数计数来决定使用哪个 C++ 构造函数。在你自己的构造函数中,你必须自己做这种类型的解析,即通过检查传递给本机函数的参数数,和/或检查参数的类型并将参数转换为所需的类型。如果检测到参数有问题,则可能需要通过抛出脚本异常来发出信号;请参见 ()。
管理非基于 QObject 的对象
对于基于值的类型(例如),当Qt脚本对象被垃圾回收时,C++对象将被销毁,因此管理C++对象的内存不是问题。对于QObjects,Qt脚本提供了几种用于管理底层C++对象生存期的替代方案;请参阅该部分。但是,对于不继承自 的多态类型,并且当您不能(或不会)将类型包装在 中时,您必须自己管理 C++ 对象的生存期。
当 Qt 脚本对象包装 C++ 对象时,通常合理的行为是,当 Qt 脚本对象被垃圾回收时,C++ 对象被删除;当对象可以由脚本构造时,通常就是这种情况,而不是应用程序为脚本提供预制的“环境”对象。使 C++ 对象的生存期遵循 Qt 脚本对象的生存期的一种方法是使用共享指针类(如 )来保存指向对象的指针;当包含 的 Qt 脚本对象被垃圾回收时,如果没有对该对象的其他引用,则底层 C++ 对象将被删除。
以下代码片段显示了一个构造函数,该函数构造使用以下方法存储的对象:
typedef QSharedPointer<QXmlStreamReader> XmlStreamReaderPointer;
Q_DECLARE_METATYPE(XmlStreamReaderPointer)
QScriptValue constructXmlStreamReader(QScriptContext *context, QScriptEngine *engine){
if (!context->isCalledAsConstructor())
return context->throwError(QScriptContext::SyntaxError, "please use the 'new' operator");
QIODevice *device = qobject_cast<QIODevice*>(context->argument(0).toQObject());
if (!device)
return context->throwError(QScriptContext::TypeError, "please supply a QIODevice as first argument");
// Create the C++ object
QXmlStreamReader *reader = new QXmlStreamReader(device);
XmlStreamReaderPointer pointer(reader);
// store the shared pointer in the script object that we are constructing
return engine->newVariant(context->thisObject(), QVariant::fromValue(pointer));}
原型函数可以使用 () 将对象转换为正确的类型:this
QScriptValue xmlStreamReader_atEnd(QScriptContext *context, QScriptEngine *){
XmlStreamReaderPointer reader = qscriptvalue_cast<XmlStreamReaderPointer>(context->thisObject());
if (!reader)
return context->throwError(QScriptContext::TypeError, "this object is not an XmlStreamReader");
return reader->atEnd();}
原型和构造函数对象的设置方式通常如下:
QScriptEngine engine;
QScriptValue xmlStreamReaderProto = engine.newObject();
xmlStreamReaderProto.setProperty("atEnd", engine.newFunction(xmlStreamReader_atEnd));
QScriptValue xmlStreamReaderCtor = engine.newFunction(constructXmlStreamReader, xmlStreamReaderProto);
engine.globalObject().setProperty("XmlStreamReader", xmlStreamReaderCtor);
脚本现在可以通过调用构造函数来构造对象,当Qt脚本对象被垃圾回收(或脚本引擎被销毁)时,该对象也会被销毁。XmlStreamReader
在某些情况下,() 提供的动态绑定或 () 提供的手动绑定都不够。例如,您可能希望实现基础对象的动态脚本代理;或者,您可能希望实现一个类似数组的类(即,对作为有效数组索引的属性和属性“length”进行特殊处理)。在这种情况下,您可以对子类来实现所需的行为。
允许您通过虚拟 get/set 属性函数处理脚本对象(类)的所有属性访问。该类还支持自定义属性的迭代;这意味着您可以通告要由 for-in 脚本语句和 .
脚本中的语法错误将在评估脚本后立即报告;() 将返回一个 SyntaxError 对象,您可以将该对象转换为字符串以获取错误说明。
() 函数为您提供最后一个未捕获异常的人类可读回溯。为了在回溯中获取有用的文件名信息,在评估脚本时,应将正确的文件名传递给 ()。
通常,在评估脚本时不会发生异常,而是在实际执行脚本定义的函数时发生异常。对于 C++ 信号处理程序,这很棘手;考虑以下情况:按钮的 clicked() 信号连接到脚本函数,并且该脚本函数在处理信号时会导致脚本异常。该脚本异常传播到哪里?
解决方案是连接到()信号;当信号处理程序导致异常时,这将向您发出通知,以便您可以找出发生的情况和/或从中恢复。
在Qt 4.4中引入了该类。提供用于在脚本引擎中报告低级“事件”的接口,例如在输入函数或到达新脚本语句时。通过子类化,您可以收到这些事件的通知,并根据需要执行一些操作。它本身不提供任何特定于调试的功能(例如设置断点),但它是提供这些功能的工具的基础。
该模块提供了一个可以嵌入到您的应用程序中。
重新定义 print()
Qt Script提供了一个内置的print()函数,可用于简单的调试目的。内置的 print() 函数写入标准输出。您可以重新定义 print() 函数(或添加您自己的函数,例如 debug() 或 log()),将文本重定向到其他位置。以下代码显示了一个自定义 print(),该 print() 将文本添加到 .
QScriptValue myPrintFunction(QScriptContext *context, QScriptEngine *engine){
QString result;
for (int i = 0; i < context->argumentCount(); ++i) {
if (i > 0)
result.append(" ");
result.append(context->argument(i).toString());
}
QScriptValue calleeData = context->callee().data();
QPlainTextEdit *edit = qobject_cast<QPlainTextEdit*>(calleeData.toQObject());
edit->appendPlainText(result);
return engine->undefinedValue();}
以下代码显示了如何初始化和使用自定义 print() 函数。
int main(int argc, char **argv){
QApplication app(argc, argv);
QScriptEngine eng;
QPlainTextEdit edit;
QScriptValue fun = eng.newFunction(myPrintFunction);
fun.setData(eng.newQObject(&edit));
eng.globalObject().setProperty("print", fun);
eng.evaluate("print('hello', 'world')");
edit.show();
return app.exec();}
指向 的指针存储为脚本函数本身的内部属性,以便在调用函数时可以检索它。
) 函数可用于将插件加载到脚本引擎中。插件通常会为引擎添加一些额外的功能;例如,插件可能会为Qt Arthur绘画API添加完整绑定,以便可以从Qt Script脚本中使用这些类。目前没有 Qt 附带的脚本插件。
如果你正在实现一些你希望其他Qt应用程序开发人员能够使用的Qt脚本功能, (例如通过子类化)是值得研究的。
从Qt 4.5开始,Qt Script通过构建C++国际化功能来支持脚本的国际化(请参阅)。
无论脚本使用“引号文本”来表示将呈现给用户的文本,请确保它由 () 函数处理。基本上,实现此目的所需的只是使用 qsTr() 脚本函数。例:
myButton.text = qsTr("Hello world!");
这占您可能编写的用户可见字符串的 99%。
qsTr() 函数使用脚本文件名的基名(参见 ()) 作为翻译上下文;如果文件名在项目中不唯一,则应使用 qsTranslate() 函数并传递合适的上下文作为第一个参数。例:
myButton.text = qsTranslate("MyAwesomeScript", "Hello world!");
如果您需要将可翻译文本完全放在函数之外,有两个函数可以提供帮助:() 和 ()。它们只是标记文本,以便由下面描述的实用程序提取。在运行时,这些函数只是返回文本进行翻译,而未修改。lupdate
示例 ():
FriendlyConversation.prototype.greeting = function(type){
if (FriendlyConversation['greeting_strings'] == undefined) {
FriendlyConversation['greeting_strings'] = [
QT_TR_NOOP("Hello"),
QT_TR_NOOP("Goodbye")
];
}
return qsTr(FriendlyConversation.greeting_strings[type]);}
示例 ():
FriendlyConversation.prototype.greeting = function(type){
if (FriendlyConversation['greeting_strings'] == undefined) {
FriendlyConversation['greeting_strings'] = [
QT_TRANSLATE_NOOP("FriendlyConversation", "Hello"),
QT_TRANSLATE_NOOP("FriendlyConversation", "Goodbye")
];
}
return qsTranslate("FriendlyConversation", FriendlyConversation.greeting_strings[type]);}
将 String.prototype.arg() 用于动态文本
String.prototype.arg() 函数(以 () 为模型)提供了一种替换参数的简单方法:
FileCopier.prototype.showProgress = function(done, total, currentFileName){
this.label.text = qsTr("%1 of %2 files copied.\nCopying: %3")
.arg(done)
.arg(total)
.arg(currentFileName);}
在整个脚本中使用 qsTr() 和/或 qsTranslate() 后,就可以开始在程序中生成用户可见文本的翻译。
提供了有关 Qt 翻译工具、Qt Linguist 和 .lupdatelrelease
Qt Script脚本的翻译过程分为三个步骤:
运行以从 Qt 应用程序的脚本源代码中提取可翻译的文本,从而生成一个供翻译人员的消息文件(TS 文件)。该实用程序可识别 qsTr()、qsTranslate() 和上述函数并生成 TS 文件(通常每种语言一个)。lupdateQT_TR*_NOOP()
使用Qt Linguist为TS文件中的源文本提供翻译。由于 TS 文件是 XML 格式,因此您也可以手动编辑它们。
运行以从 TS 文件中获取轻量级消息文件(QM 文件),仅适用于最终用途。将 TS 文件视为“源文件”,将 QM 文件视为“目标文件”。翻译人员编辑 TS 文件,但应用程序的用户只需要 QM 文件。这两种类型的文件都与平台和区域设置无关。lrelease
通常,您将对应用程序的每个版本重复这些步骤。该实用程序会尽最大努力重用以前版本中的翻译。lupdate
运行时,必须指定脚本的位置以及要生成的 TS 文件的名称。例子:lupdate
lupdate myscript.qs -ts myscript_la.ts
将从中提取可翻译文本并创建翻译文件。myscript.qsmyscript_la.qs
lupdate -extensions qs scripts/ -ts scripts_la.ts
将从文件夹中以结尾的所有文件中提取可翻译文本并创建翻译文件。.qsscriptsscripts_la.qs
或者,您可以创建一个单独的 qmake 项目文件来适当地设置 and 变量;然后使用项目文件作为输入运行。SOURCESTRANSLATIONSlupdate
lrelease myscript_la.ts
运行时,必须指定 TS 输入文件的名称;或者,如果使用 QMake 项目文件来管理脚本翻译,请指定该文件的名称。 将创建 ,翻译的二进制表示。lreleaselreleasemyscript_la.qm
在应用程序中,必须使用 () 加载适合用户语言的翻译文件,并使用 () 安装它们。最后,必须调用 () 以使脚本转换函数 (qsTr()、qsTranslate() 和 ) 可用于随后由 () 计算的脚本。对于使用 qsTr() 函数的脚本,必须将正确的文件名作为第二个参数传递给 ()。QT_TR*_NOOP()
linguist,并安装在安装Qt的基本目录的子目录中。单击“帮助”|”Qt Linguist中的手册,用于访问用户手册;它包含一个教程来帮助您入门。lupdatelreleasebin
另请参见 .
Qt Script实现了标准中定义的所有内置对象和属性;有关概述,请参阅。
- __proto__
对象的原型 (()) 可以通过其在脚本代码中的属性进行访问。此属性设置了标志。例如:__proto__
var o = new Object();(o.__proto__ === Object.prototype); // this evaluates to true
- Object.prototype.__defineGetter__
此函数为对象的属性安装 getter 函数。第一个参数是属性名称,第二个参数是要调用以获取该属性值的函数。调用该函数时,该对象将是访问其属性的对象。例如:this
var o = new Object();o.__defineGetter__("x", function() { return 123; });var y = o.x; // 123
- Object.prototype.__defineSetter__
此函数为对象的属性安装 setter 函数。第一个参数是属性名称,第二个参数是要调用的函数,用于设置该属性的值。调用该函数时,该对象将是访问其属性的对象。例如:this
var o = new Object();o.__defineSetter__("x", function(v) { print("and the value is:", v); });o.x = 123; // will print "and the value is: 123"
- Function.prototype.connect
此功能将信号连接到插槽。此函数的用法在 一节中进行了介绍。 - Function.prototype.disconnect
此功能断开信号与插槽的连接。此函数的用法在 一节中进行了介绍。 - QObject.prototype.findChild
此函数在语义上等价于 ()。 - QObject.prototype.findChildren
此函数在语义上等价于 ()。 - QObject.prototype.toString
此函数返回 . - gc
此函数调用垃圾回收器。 - Error.prototype.backtrace
此函数以字符串数组的形式返回人类可读的回溯。 - Error 对象具有以下附加属性:
- lineNumber:发生错误的行号。
- fileName:发生错误的文件名(如果文件名已传递给 ())。