On the Fast Track to Application Scripting

Материал из Wiki.crossplatform.ru

Перейти к: навигация, поиск
Image:qt-logo_new.png Image:qq-title-article.png
Qt Quarterly | Выпуск 24 | Документация


by Kent Hansen

With the introduction of Qt Script as a standard part of Qt in version 4.3,Qt/C++ application developers have a seamlessly integrated solution foradding scripting to their applications. Qt Script is a simple, yet powerfulway of providing script authors---who don't necessarily know C++---withan interface for working in the context of your Qt application. Inthis article, we give an overview of the main steps that are involved whenembedding Qt Script into an application, and use a small example to showcasethe key techniques.

Содержание


The Embedding Python into Qt Applications article in Qt Quarterly issue 23gives a good overview of the benefits of scripting, as well as applicationscripting and the creation of such APIs in general. Like PythonQt (the solutiondescribed in that article), the purpose of Qt Script is to provide a scriptingenvironment that application authors can embed into their Qt applications; it isnot geared towards "pure" script-based application development.

In embedded scripting, you typically make one or more "host objects"(comprising your application's scripting API) available to the scriptingenvironment; you then provide one or more mechanisms for scripts to be executedin this environment.As we shall see, Qt Script's tight integration with Qt's meta-object systemmakes both the configuration process and the subsequent communication betweenC++ and scripts straightforward.

[править] Getting Started

In order to use Qt Script in your application, you must first add thefollowing line to your qmake project (.pro) file:

    QT += script

You may then include the main QtScript header in your C++ code to gainaccess to the full Qt Script API:

    #include <QtScript>

The QScriptEngine class provides your application with an embeddedscripting environment. You can have as many script engines as you want inyour application; each engine is in effect a lightweight, self-containedvirtual machine. To execute scripts, you call the engine's evaluate()function, as in the following simple example:

    QScriptEngine engine;
    QScriptValue result = engine.evaluate("(1+2)*3");
    qDebug() << "Result as float:" << result.toNumber();

Thankfully, the Qt Script language is capable of more than just adding andmultiplying constants. The scripting language is based on the ECMAScript 3standard (ECMA-262), and thus sports the same core language features asJavaScript 1.x, a language that is widely used in Web-based development.

[править] Scripting Qt Objects

With Qt Script, scripting QObjects is easy. The following C++ codeshows how to make an instance of a standard QPushButton widget availableto scripts:

    QPushButton button;
    QScriptValue scriptButton = engine.newQObject(&amp;button);
    QScriptValue global = engine.globalObject();
    global.setProperty("button", scriptButton);

QScriptEngine::newQObject() returns a proxy script object for thegiven Qt C++ object.

In the above example, we chose to make the proxy objectavailable to scripts as a global variable. Scripts can subsequently manipulateproperties of the button and call its slots through the proxy, as in thefollowing script snippet:

    button.checkable = true;
    button.show();

Additionally, scripts can connect to the button's signals:

    function clickHandler(checked) {
      print("button checked: " + checked);
    }
 
    button.clicked.connect(clickHandler);

To define our own custom scripting API, we design QObject-basedclasses, expressing the API in terms of Qt properties, signals andslots. To configure the Qt Script environment, we just need to wrapthe scripting-related objects using new QObject() and make theresulting proxy objects available to scripts, like we did withthe QPushButton above.

In the remainder of this article, we will study a small exampleapplication, CubbyHolistic, and see how we can make it scriptable.This will illustrate the main aspects of how Qt Script and Qt/C++ playtogether.

[править] The CubbyHolistic Application

The original CubbyHolistic C++ application is a simpleimplementation of the producer-consumer pattern. There is a resource,a CubbyHole object, that is shared between two threads: aProducerThread and a ConsumerThread. The CubbyHole classdeclaration looks like this:

    class CubbyHole : public QObject
    {
    public:
        CubbyHole(QObject *parent = 0);
 
        void depositValue(const QVariant &amp;value);
        QVariant withdrawValue();
 
    private:
    // implementation details ...
    };

The CubbyHole class uses standard synchronization mechanisms in Qt toimplement the depositValue() and withdrawValue() functions;i.e., blocking the producer and consumer when necessary.

The run() reimplementations of ProducerThread and ConsumerThreadtypically look like this:

    void ProducerThread::run()
    {
      for (int i = 0; i < 10; ++i)
        cubbyHole->depositValue(i * 4);
    }
 
    void ConsumerThread::run()
    {
        for (int i = 0; i < 10; ++i) {
            QVariant v = cubbyHole->withdrawValue();
            /* Do something with the value */
        }
    }

The application's main() function constructs a CubbyHole object andgets the producer and consumer going:

    int main(int argc, char **argv)
    {
        QCoreApplication app(argc, argv);
 
        CubbyHole hole;
        ProducerThread producer(&amp;hole);
        ConsumerThread consumer(&amp;hole);
 
        producer.start();
        consumer.start();
        producer.wait();
        consumer.wait();
        return 0;
    }

[править] Scripting CubbyHolistic

As a fun little experiment, let's change CubbyHolistic so that the producerand consumer can be provided as scripts, rather than C++ classes; that is,C++ code will no longer have to be (re)compiled in order to support differentproducers and consumers that use the common CubbyHole interface.

The CubbyHole object itself will remain wholly implemented in C++, but wewill expose a scripting API to it. To do this, we can modify the CubbyHoledeclaration [1]so that depositValue() and withdrawValue() become slots:

    class CubbyHole : public [[Qt:Документация_4.3.2/qobject  | QObject]]
    {
        Q_OBJECT
    ...
    public slots:
        void depositValue(const QVariant &amp;value);
        QVariant withdrawValue();
    ...
    };

This will allow scripts to call these two functions. Next, we declare a QThread subclass that will be used to evaluate a script in a separatethread, in the context of a given CubbyHole object:

    class ScriptThread : public QThread
    {
    public:
        ScriptThread(const QString &amp;fileName,
                     CubbyHole *hole);
    protected:
        void run();
 
    private:
        QString m_fileName;
        CubbyHole *m_hole;
    };

The producer and consumer will both be represented as instances ofScriptThread; it's merely a matter of passing the appropriate file nameto the constructor. All the work happens in the run() reimplementation:

    void ScriptThread::run()
    {
      QScriptEngine engine;
 
      QScriptValue scriptHole = engine.newQObject(m_hole);
      QScriptValue global = engine.globalObject();
      global.setProperty("cubbyHole", scriptHole);
 
      QFile file(m_fileName);
      file.open(QIODevice::ReadOnly);
      QString contents = QTextStream(&amp;file).readAll();
      file.close();
 
      engine.evaluate(contents);
 
      if (engine.hasUncaughtException()) {
        QStringList bt = engine.uncaughtExceptionBacktrace();
        qDebug() << bt.join("\n");
      }
    }

Each thread has its own script engine. The shared CubbyHole C++ objectis exposed as a global variable in the engine, and the contents of the scriptfile are read and passed to evaluate(). There's also a check to see if theevaluation caused a script exception (e.g., due to a syntax error in thescript); in that case, we print a backtrace to aid in debugging the problem.

The final step C++-wise is to change the application's main() functionto use the ScriptThread class to provide the producer and consumer (forsimplicity, fixed file names are used).

    int main(int argc, char **argv)
    {
      QCoreApplication app(argc, argv);
      CubbyHole hole;
      ScriptThread producer("./producer.qs", &amp;hole);
      ScriptThread consumer("./consumer.qs", &amp;hole);
 
      producer.start();
      consumer.start();
      producer.wait();
      consumer.wait();
      return 0;
    }

Now we are ready to start writing scripts! This is the originalProducerThread behavior expressed as a script in producer.qs:

    for (var i = 0; i < 10; ++i)
      cubbyHole.depositValue(i * 4);

And here is the equivalent code in consumer.qs:

    for (var i = 0; i < 10; ++i) {
      var v = cubbyHole.withdrawValue();
      /* Do something with the value */
    }

Here's a producer script that will generate a random sequence of Fibonaccinumbers:

    function fibonacci(n) {
      if (n <= 1)
        return n;
      return fibonacci(n-1) + fibonacci(n-2);
    }
 
    for (var i = 0; i < 10; ++i) {
      var arg = Math.round(Math.random() * 20);
      var value = fibonacci(arg);
      cubbyHole.depositValue(value);
    }

With a little more effort, the scripts can become "pluggable"; for example,the application could scan a folder of scripts and have the user pick orcreate the producer and consumer he or she wants to use.

[править] Further Reading

Qt Script has features that haven't been discussed in this article, butwhich can be useful in more complex applications---such as the ability toscript non- QObject types. That being said, the Qt Script C++ API itselfis quite lightweight thanks to its tight integration with Qt as a whole.The Qt documentation and examples cover the use of Qt Script in detail,including interesting use cases like how to combine Qt Script and Qt Designer forms.
[1] Modifying the C++ class API directly has been donehere for the sake of simplicity; in your application, you may want to haveseparate C++ and scripting APIs.

Обсудить...