跨平台编程的利器—Qt:与Javascript的交互[为程序添加动态脚本]
原文链接:https://blog.youkuaiyun.com/guxch/article/details/7656846
因为业务需要,作者比较关心一种语言或技术的用户自定义能力,用户自定义的“最高境界”就是能将脚本嵌入到程序中,从而改变或控制程序的运行。
从Qt4.3起,就支持脚本了,Qt中的脚本被称为Qt Script,它是基于ECMAScript的,因此与我们通常用的javascript相同,据文章中介绍,Qt中的脚本引擎器采用的是Google的产品,也就是Chrome的JavaScript engine。
一、相关类的介绍
下表是Qt Script技术中有关的类及其简介。
大致上,主要用到的类有(按使用频度):QScriptEngine、QScriptValue、QScriptClass、QScriptContext,QScriptSyntaxCheckResult(如果需要检查语法)。
二、与脚本的交互
作者曾经写过一篇C#与Python交互的文章“在C#环境中动态调用IronPython脚本”,本文也按照该篇的情况,介绍Qt与脚本的交互。
1.从Scrip环境中返回数据
代码如下。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++程序可以调用并返回结果。
2.Script环境中调用Qt环境中的函数或类
代码如下,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中创建的,这是一种简单有效的方式。
3.Qt环境中调用Script环境中的数据或类
代码如下。对于简单变量,直接设置其值就可以了,如果该变量不存在,会自动添加,对于复杂变量,例如代码中脚本环境中定义的类comx,当它作为调用参数调用Qt中的函数时,对它的解析不同样简单的变量。
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();
}
4.一个为Script中类添加方法的例子
代码如下,该例子为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();
}
5.Script句法检查
当允许用户自定义脚本时,好的程序应检查其正确性。正确性包括两个方面:运行前静态的语法检查;运行中能捕获运行中的错误并返回给用户。
语法检查可以调用
QScriptSyntaxCheckResult QScriptEngine::checkSyntax ( const QString & program )
该方法是一个静态方法。
运行时,如果有错误,QScriptEngine.evaluate方法将返回一个Error对象,但由于QScriptValue可以封装任何数据类型,所以我们需要有其它的函数来便利地判断是否有异常发生,比较健壮的代码应该像下面的样子:
...
QScriptValue result = myEngine.evaluate(...);
if(myEngine.hasUncaughtException)
{
//错误处理
int errlinenumber =myEngine.uncaughtExceptionLineNumber();
...
}
else
{
... //继续处理
}