跨平台编程的利器—Qt:与Javascript的交互(为程序添加动态脚本)

【在写本文的时候,看到新闻说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与脚本的交互。

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
 {
    ...  //继续处理
 }

为简单起见,本文以上的代码并未按上面的格式。



你可能感兴趣的:(跨平台编程的利器—Qt:与Javascript的交互(为程序添加动态脚本))