【在写本文的时候,看到新闻说Nokia准备放弃Qt,看来Qt要成为昨日黄花了,如果没有大公司支持,任何语言都不会有强的生命力。】
(本文接上篇“一个MDI图形应用框架)
因为业务需要,作者比较关心一种语言或技术的用户自定义能力,用户自定义的“最高境界”就是能将脚本嵌入到程序中,从而改变或控制程序的运行。
从Qt4.3起,就支持脚本了,Qt中的脚本被称为Qt Script,它是基于ECMAScript的,因此与我们通常用的javascript相同,据文章中介绍,Qt中的脚本引擎器采用的是Google的产品,也就是Chrome的JavaScript engine。
下表是Qt Script技术中有关的类及其简介。
QScriptClass | 是对script中类的封装,可以在C++代码中对script类进行调用。 |
QScriptClassPropertyIterator | 该类似乎用途不大,只有在你基础QScriptClass时,并且需要遍历类的属性时需要。 |
QScriptContext | 这个类比较有用,通常作为参数传递到你的C++程序,可以得到调用的上下文环境:用来封装script函数调用(指调用C++的函数)的,基本上功能有两个:一是提供this指针,二是提供调用的arguments |
QScriptContextInfo | QScriptContext的辅助信息 |
QScriptEngine | 脚本解析引擎,创建一个脚本运行环境时必须使用。 |
QScriptEngineAgent | 该类是调试Script的基础工具类,可以通过设置Engine的Agent,捕获特定的事件进行处理 |
QScriptEngineDebugger | Script的调试类,一般情况下不用。 |
QScriptProgram | 对Script运行程序的封装,如果我们有一段写好的脚本,并且它多次运行(QScriptProgram会将脚本编译),则可以使用该类。 |
QScriptString | 它用作一个对QScriptEngine中字符串的句柄,是对字符串的一个封装。 |
QScriptSyntaxCheckResult | 在用户可以编写自己的脚本时,该类比较有用,可以得到Script语法的检查结果。 |
QScriptValue | 有点类似于windows中的Variant类型,用于对脚本语言所有数据类型的封装。 |
QScriptValueIterator | 为QScriptValue提供了一种类似Java-style的迭代器(当QScriptValue是集合类型时)。 |
QScriptable | 提供了一种在Qt C++环境中对脚本的控制方式,似乎与Qt中特有的消息触发机制connect, slot等有关。 |
大致上,主要用到的类有(按使用频度):QScriptEngine、QScriptValue、QScriptClass、QScriptContext,QScriptSyntaxCheckResult(如果需要检查语法)。
作者曾经写过一篇C#与Python交互的文章“在C#环境中动态调用IronPython脚本”,本文也按照该篇的情况,介绍Qt与脚本的交互。
代码如下。Script中定义了两个函数cube和mysqrt,在C++环境中调用这两个函数。
void testScript() { QScriptEngine myEngine; myEngine.evaluate("function cube(x) { return x * x * x; } function mysqrt(x) { return Math.sqrt(x);} "); qDebug()<< myEngine.evaluate("mysqrt(cube(3))").toNumber(); }
以下的代码显示了从Script中返回一个复杂的数据类型。
Q_DECLARE_METATYPE(QList<int>) void testInvokeScript() { QScriptEngine myEngine; qScriptRegisterSequenceMetaType<QList<int> >(&myEngine); QScriptValue global = myEngine.globalObject(); myEngine.evaluate("var pack= new Array(); pack[0]=1; pack[1]='I am here.'; pack[2]=3;"); myEngine.evaluate("var intarr= new Array(); intarr[0]=1; intarr[1]=2; intarr[2]=3;"); QScriptValue vv = global.property("pack"); QScriptValue vi = global.property("intarr"); for (int i = 0; i < vlist.size(); ++i) qDebug()<<vlist.at(i); QList<int> ilist = qscriptvalue_cast<QList<int> >(vi); for (int i = 0; i < ilist.size(); ++i) qDebug()<<ilist.at(i); }
这是比较简单的一种交互场景:用户定义一个“封闭”的函数或类型,C++程序可以调用并返回结果。
代码如下,Qt中定义了一个类myclass,一个函数myAdd。在脚本中调用类方法和函数,相当于:myclass.GetId()和myAdd()。
class myclass : public QObject { Q_OBJECT Q_PROPERTY(QString focus READ hasFocus) public: explicit myclass(QObject *parent = 0) { m_id =10; } Q_INVOKABLE int Test() { return m_id;} Q_INVOKABLE QString GetName(QString prefix) { if(name == "a") return m_id+10; if(name =="b" ) return m_id+20; return m_id; } QString hasFocus() const { return "abcde";} signals: public slots: int GetId(QString name) { return "Hello " + prefix; } private: int m_id; }; QScriptValue MainWindow::myAdd(QScriptContext *context, QScriptEngine *engine, void *pargs) { QScriptValue a = context->argument(0); QScriptValue b = context->argument(1); myclass *pmc =(myclass *)pargs; int i = pmc->GetId("c"); return a.toNumber() + b.toNumber() + i; } void MainWindow::testInvokeFunction() { QScriptEngine myEngine; //修改button上的文字 QScriptValue scriptButton = myEngine.newQObject(ui->btnOK); myEngine.globalObject().setProperty("button", scriptButton); myEngine.evaluate("button.text = \"true\""); //调用myAdd方法 myclass *mc = new myclass(this); QScriptValue fun = myEngine.newFunction(myAdd,mc); myEngine.globalObject().setProperty("myAdd", fun); QScriptValue vv = myEngine.evaluate("myAdd(2,3)"); //调用类myclass的GetId方法 myclass *mc2 = new myclass(this); QScriptValue qso = myEngine.newQObject(mc2); myEngine.globalObject().setProperty("qso",qso); QScriptValue ii = myEngine.evaluate("qso.GetId('aaa')"); qDebug()<<"vv="<<vv.toNumber(); qDebug()<<"ii="<<ii.toString(); }
测试代码中,包含3部分内容:1.将button上的文字在脚本中修改了。2.脚本中调用myAdd方法,注意,脚本中所有的调用参数,都封装到了QScriptContext类中,我们只能从该类中得到,QScripEngine.newFunction方法可以给传到脚本环境中的方法额外的参数,即方法中第2个参数,应用该参数,有时会使代码更清晰。本文中,给myAdd方法的额外参数是一个myclass类。3.脚本中定义了一个全局变量qso,它的类型就是Qt中的myclass,接着调用它的GetId方法。
上面代码中,可以看出,如果需要将Qt中的类(本文是从QObject继承的类,如不是QObject的子类,则比较麻烦)或方法传递到Script环境中,需要用到QScriptEngine的newQObject和newFunction两个方法对类型实例进行封装,然后再脚本环境中设置一个变量“指向”该类型实例,脚本就可以用了。
以上代码中,myclass的实例是在Qt环境中创建的,如果需要在Script环境中创建一个myclass实例,可以采用如下的方法。
QScriptValue MainWindow::createObject(QScriptContext *context, QScriptEngine *engine) { QScriptValue a = context->argument(0); if(a.isString()) { QString stringname=a.toString(); if(stringname == "myclass") { myclass *mc2 = new myclass(0); return engine->newQObject(mc2); } } return NULL; } void MainWindow::testCreateObject() { QScriptEngine myEngine; QScriptValue fun = myEngine.newFunction(createObject); myEngine.globalObject().setProperty("createObject", fun); myEngine.evaluate("var obj=createObject('myclass');"); QScriptValue ii = myEngine.globalObject().property("obj"); qDebug()<<myEngine.evaluate("obj.GetName('me')").toString(); }本质上,myclass实例依然是在Qt中创建,但在脚本中,通过createObject函数,似乎是在Script中创建的,这是一种简单有效的方式。
QScriptValue MainWindow::complexAdd(QScriptContext *context, QScriptEngine *engine) { QScriptValue a = context->argument(0); if(a.isObject()) { QScriptClass *pc = a.scriptClass(); if(pc == NULL) qDebug()<<"pc is null"; else { qDebug()<<pc->name(); } QScriptValue val(engine, 123); a.setProperty("x", val); } return NULL; } void MainWindow::testQSCriptClass() { QScriptEngine myEngine; myEngine.evaluate("var ii=3; function cube(x) { return x * x * x; }"); myEngine.globalObject().setProperty("myNumber", 12); myEngine.globalObject().setProperty("ii", 5); qDebug()<<myEngine.evaluate("cube(myNumber + 1)").toNumber(); qDebug()<<myEngine.evaluate("cube(ii)").toNumber(); myEngine.evaluate("var comx={x:1, y:2}; var aobj= new Object(); aobj.x=1; aobj.y=2;"); QScriptValue fun = myEngine.newFunction(complexAdd); myEngine.globalObject().setProperty("complexAdd", fun); myEngine.evaluate("complexAdd(aobj);"); qDebug()<< myEngine.evaluate("aobj.x").toString(); }
代码如下,该例子为Script中的类person,添加了一个方法,fullName,实际上该方法在Qt环境中定义,注意此例中QScriptContext的用法。
QScriptValue MainWindow::Person_prototype_fullName(QScriptContext *context, QScriptEngine *engine) { QScriptValue self = context->thisObject(); QString result; result += self.property("firstName").toString(); result += QLatin1String(" "); result += self.property("lastName").toString(); return result; } void MainWindow::testQSCriptContext() { QScriptEngine myEngine; QScriptValue fun = myEngine.newFunction(Person_prototype_fullName); myEngine.globalObject().setProperty("Person_prototype_fullName", fun); myEngine.evaluate("var person={firstName:'Wang', lastName:'Yi',fullName:Person_prototype_fullName};"); //one way to use QScriptValue who = myEngine.globalObject().property("person"); qDebug()<< fun.call(who).toString(); //another way to use qDebug()<< myEngine.evaluate("person.fullName()").toString(); }
当允许用户自定义脚本时,好的程序应检查其正确性。正确性包括两个方面:运行前静态的语法检查;运行中能捕获运行中的错误并返回给用户。
语法检查可以调用
QScriptSyntaxCheckResult QScriptEngine::checkSyntax ( const QString & program )该方法是一个静态方法。
运行时,如果有错误,QScriptEngine.evaluate方法将返回一个Error对象,但由于QScriptValue可以封装任何数据类型,所以我们需要有其它的函数来便利地判断是否有异常发生,比较健壮的代码应该像下面的样子:
... QScriptValue result = myEngine.evaluate(...); if(myEngine.hasUncaughtException) { //错误处理 int errlinenumber =myEngine.uncaughtExceptionLineNumber(); ... } else { ... //继续处理 }