| Tutorial | Classes | Functions | QSA Developer | Language | Library | Qt API Qt Script for Applications

[Prev: What is Qt Script for Applications] [Home] [Next: How to Design and Implement Application Objects]

How to make your Qt Application Scriptable

Overview

This chapter demonstrates how to write a C++ Qt application which integrates Qt Script for Applications to make the application extensible and customizeable through scripting. The goal is to write a simple spreadsheet application which can be extended by the user. To do this, the spreadsheet will provide interfaces to its sheets. The script code can access the sheets, so the user can write Qt Script code that presents dialogs to accept user preferences, and which can access and manipulate the spreadsheet data. The code for this example can be found in examples/spreadsheet.

Additional examples that present other aspects of Qt Script for Applications usage are also included in the examples directory.

How to make a Qt/C++ Application Scriptable

1) Include the Qt Script for Applications Library

To make a Qt/C++ application scriptable, you need the libqsa that is included in the Qt Script for Applications package. This library is installed in $QTDIR/lib, and the header files with the public API are installed in $QTDIR/include. To use libqsa and get other QSA specific build options, add following line to your .pro file:

load( qsa )

The libqsa library provides the QSInterpreter class. Only one instance of QSInterpreter may exist at one time. You don't need to new your own instance and keep the pointer, simply use QSInterpreter::defaultInterpreter(), which always returns the QSInterpreter instance.

2) Add the Application Objects to the Scripting Engine

To make application functionality available to scripts, the application must provide QObjects or QObject subclasses which implement the application's functionality. By passing an object to the scripting engine (using QSInterpreter::addObject()), this object and all its signals, slots, properties and child objects are made available to scripts. (Because Qt Script for Applications uses Qt's meta object system there is no need to implement any additional wrappers or bindings.)

If no parent object of the object which is passed into addObject() has been made available yet, the new object is added as a toplevel object to the scripting engine. This means it is available through Application.object_name to the script writer then.

If a parent object of the object has been added via addObject() already previously, the new object is not added as a toplevel object, and is available through Application.parent1.parent2.object_name (given that parent1 has been added previously via addObject()). The reason for doing that, is that such an object can be used as a namespace or context later, and code can be added in the context of that object. More details about that are given in a later chapter.

In the most cases we do not pass QObjects which are directly used in the C++ application to the scripting engine, as this would expose too many slots. Instead we implement interface QObjects which offer the signals, slots and properties that we want to offer to scripts and which will be implemented simply as forward function calls.

In our spreadsheet example we will add interface objects for the sheets, which implement slots and properties to query and set selection ranges, retrieve and set cell data, etc.

In other cases it might be possible to use an application's existing QObjects as application objects in the scripting language. An example of this approach is shown in the examples/textedit example, which is a slightly modified, scriptable, version of the Qt textedit example.

To read about how to design and implement application objects, see the How to Design and Implement Application Objects chapter.

3) Open a Scripting Project

Qt Script for Applications always works with one current scripting project that contains all the forms and files in which all the functions and classes are implemented.

A project must always be open, otherwise most of the functions in QSInterpreter will not work. In other words, only QSInterpreter::evaluate() and QSInterpreter::call() work without an open project. So unless you only want to execute code directly, always use a project.

If you work with a project, you can either choose to let QSA take care of everything (saving, loading, syncint, etc.) or to let you take care of most parts and give you more flexibility.

The first possibility is that Qt Script for Applications loads and saves the whole project from one file or one data block which you can specify. In this case all project data is compressed in that data block or file and extracted temporarily when loading. The other possibility is to open an empty project and to use QSProject's API to add sources, forms and object sources manually and retrieve them again and to save or store them however you want.

To go with the first possibility, do following:

Open the scripting project using QSProject::open().

The QSProject::open() function can be called in one of two ways:

  1. Call open() with a filename (including the path). Qt Script for Applications will open the specified project file (or create a new one if the file doesn't exist) and will automatically take care of saving any changes.

  2. Call open() with a byte array that contains the scripting project's data. This is useful if you don't want to save the project as an individual file on disk; for example, you may prefer to keep the project data embedded in the document. If this approach is taken the data block which contains the project is passed to open(). If an empty byte-array is passed, a new project will be created. Use QSProject::projectData() to retrieve a project as a byte-array suitable for use with this open() overload.

If you want to go with the second approach, do following:

Open an empty scripting project using QSProject::open() by passing an empty QByteArray and a project name.

Later add code and forms via QSProject::addSource(), QSInterpreter::setObjectSource() or QSProject::addForm().

Similar you can retrieve the data again.

Of course it is possible to use a combination of both approaches, e.g. using the first approach, but adding code and forms programmatically later.

In the spreadsheet example we use one scripting project for the whole application, and we let Qt Script for Applications save this to disk as an individual file that is opened on startup. The examples/usermanagement example shows how to save scripting projects by attaching them to a document, and how to load scripting projects that are embedded in a document. The usermanagement example also shows how to use multiple scripting projects in one application. The ######## example shows how to use the more flexible approach of manually adding code and forms.

In the spreadsheet example the following code is used to initialize QSInterpreter (adding application objects and opening the project):

    void SpreadSheet::init()
    {
        currentSheet = sheet1;

        // create a sheet interface object for each sheet, and add it as
        // application object to the engine
        interpreter.addObject( new SheetInterface( sheet1, this, "sheet1" ) );
        interpreter.addObject( new SheetInterface( sheet2, this, "sheet2" ) );

        setupSheet( sheet1 );
        setupSheet( sheet2 );

        // open the project which the user will work in all the time
        interpreter.currentProject()->open( "sheet.qsa" );
    }

4) Allow the User to Create and Edit Scripts

Qt Script for Applications includes an IDE (Qt Script for Applications Developer) for writing and editing scripts with a GUI builder and debugger. To open Qt Script for Applications Developer, call QSProject::openDeveloper() and to close it again, call QSProject::closeDeveloper(). In the IDE, the script writer can add global functions and classes to the project, design forms, write the forms code, and debug their project.

In some cases it might be sufficient to simply offer a basic editor widget which allows the user to write code. One reason to do that might be because you want to embed the code editor directly into your application user interface, and don't want to open another toplevel window (which the Qt Script for Applications Developer is) for the user. But if the users should be able to debug scripts, have intelligent completion and hints in the editor and be able to use GUI controls in the script, the IDE included in Qt Script for Applications is the most appropriate solution. Even if the user isn't permitted to create GUI controls and forms via scripting, the IDE can still be used for code editing, by e.g. passing QSProject::OnlyEditorAndDebugger as the flag parameter into the QSProject::openDeveloper() function. This will ensure that Qt Script for Applications Developer does not provide the GUI builder functionality, but will still allow the script writer to make use of the other benefits of the IDE, such as the intelligent editor with completion and syntax highlighting, as well as the Qt Script debugger. There are more different modes you can open Qt Script for Applications Developer with (w.g. without debugger, read-only editors, etc.)

There are two ways in which the end user might want to use the scripting IDE.

  1. Open the IDE and create or edit a script.

  2. Define a macro.

Depending on the application it can be very different how you provide the scripting functionality to the end user. For a typical end-user application you might offer one or both of the approaches to scripting mentioned above. For example we might provide a menu option and a toolbar button to launch Qt Script for Applications Developer, and an editable combobox which lists all the global functions. If the user enters a function name that doesn't exist they would be given the opportunity to create a new function of that name; and if they chose an existing function the Qt Script for Applications Developer would be launched with the cursor at that function ready for editing. A 'Run' toolbar button could be placed beside the combobox, so that the user could choose a function and click 'Run' to execute it.

Other approaches might be that a user can define functions to validate data of data entry forms, customize the functionality of an editor, customize the user interface of a complex 3D graphics application or provide scripting modules for an image processing application.

Depending on the application, the usage of application scripting can vary a lot. This chapter describes application scripting using the example of a spreadsheet application, which is a typical end-user application. This makes you familiar with most of the important concepts, so that it will be possible for you to use Qt Script for Applications to make your application scriptable, even if the way application scripting will be used there might be very different from what we describe here.

Macros

We define a macro as a stand-alone global function. Call QSProject::addFunction() to add a new macro (global function) to the script. You can then open the editor with the cursor positioned at the new (empty) function, ready for the user to complete.

    void AddScriptDialog::addScript()
    {
        QSInterpreter *script = QSInterpreter::defaultInterpreter();
        QString func = comboFunction->currentText();
        if ( script->globalFunctions().findIndex( func ) == -1 ) {
            QString msg = tr( "The function <b>%1</b> doesn't exist. "
                              "Do you want to add it?" ).arg( func );
            if ( QMessageBox::information( 0, tr( "Add Function" ), msg,
                                           tr( "&Yes" ), tr( "&No" ),
                                           "", 0, 1 ) == 0 ) {
                // if the function doesn't exist yet, add it to the
                // project and open the IDE with it
                script->currentProject()->addFunction( func );
            }
        }
        emit newScript( func, editName->text(), *labelPixmap->pixmap() );
        accept();
    }
Launching the IDE to Create or Edit a Script

In the spreadsheet example the following slot is called to open Qt Script for Applications Developer:

    void SpreadSheet::openDeveloper()
    {
        // open the Qt Script for Applications Developer
        interpreter.currentProject()->openDeveloper();
    }
Adding and Editing Macros

In our spreadsheet example we want to allow the user to add macros as actions to the toolbar and menu, which they can associate with a function which should be called when they activate the action. For this we provide a dialog through which the user can either choose an existing global function to edit or add a new function (as described in Macros). If they add a new function a new action and icon are created along with a menu option and toolbar button.

The following code is used in the macro dialog to initialize the combobox which lets the user choose a script function:

    void AddScriptDialog::init()
    {
        // List all global functions of the project
        comboFunction->insertStringList( QSInterpreter::defaultInterpreter()->
                                         globalFunctions() );
    }

When the user clicks OK in this dialog, the following slot is executed. If the function the user specified doesn't exist, they are asked if this function should be added to the project, in which case, addFunction() is called:

    void AddScriptDialog::addScript()
    {
        QSInterpreter *script = QSInterpreter::defaultInterpreter();
        QString func = comboFunction->currentText();
        if ( script->globalFunctions().findIndex( func ) == -1 ) {
            QString msg = tr( "The function <b>%1</b> doesn't exist. "
                              "Do you want to add it?" ).arg( func );
            if ( QMessageBox::information( 0, tr( "Add Function" ), msg,
                                           tr( "&Yes" ), tr( "&No" ),
                                           "", 0, 1 ) == 0 ) {
                // if the function doesn't exist yet, add it to the
                // project and open the IDE with it
                script->currentProject()->addFunction( func );
            }
        }
        emit newScript( func, editName->text(), *labelPixmap->pixmap() );
        accept();
    }

At the end of the function the newScript() signal is emitted, which is connected to the addScript() slot in the SpreadSheet. The addScript() function creates an action for the macro and adds a menu option and toolbar button for the macro. In addition, the action's activated() signal is connected to runScript(). To find out which function this macro (action) should call, the action and its associated function are inserted into the scripts map:

    void SpreadSheet::addScript( const QString &function, const QString &name, const QPixmap &pixmap )
    {
        // Add a new action for the script
        QAction *a = new QAction( name, pixmap, name, 0, this, name.latin1() );
        a->addTo( scriptsToolbar );
        a->addTo( scriptsMenu );
        // associate the action with the function name
        scripts.insert( a, function );
        connect( a, SIGNAL( activated() ), this, SLOT( runScript() ) );
    }

5) Allow the User to Run Scripts Directly from the Application

It would be tedious for users if they had to launch Qt Script for Applications Developer and click Run every time they wanted to execute a script. For this reason it is normal practice to provide a means by which the user can execute a function from within the application itself. How this is achieved depends to some extent on the application and on the functionality of the script.

One approach to providing the user with access to their script functions is to provide a list, e.g. in a popup list, from which they can pick the function they wish to execute. (This approach is taken in the textedit example.) A list of existing global functions in the current project is obtained by calling QSInterpreter::globalFunctions(). To call a script function, use QSInterpreter::call().

In the spreadsheet example we have seen that each macro (global function) is associated with an action and has a corresponding menu option and toolbar button. Now we'll see how clicking a macro menu option or toolbar button will cause the macro to be executed.

When the user invokes an action, the runScrip() slot is triggered by the action, and we have to find which function should be executed. In every slot we can call sender() (implemented in QObject), to find out who triggered that slot. We cast the sender() to a QAction pointer (since we know it is a QAction) and then look up this pointer in the scripts map. Each action is mapped to the name of the function that it is associated with, so we can now call QSInterpreter::call() with the action's associated function name to execute it:

    void SpreadSheet::runScript()
    {
        // find the function which has been associated with the activated
        // action (the action is the sender())
        QString s = *scripts.find( (QAction*)sender() );
        // and call that function
        interpreter.call( s, QValueList<QVariant>() );
    }

Establishing Connections to Signals and Running the Interpreter

By letting the user edit scripts in the Qt Script for Applications Developer, it is possible to connect script functions to applications object signals. These connections are established when the project is opened via QSProject::open(). When the user opens the Qt Script for Applications Developer, the project is halted for as long as the IDE is open, and during this time, no connections are active. When a scripting function is executed, while the IDE is opened or play is pressed in the IDE, the project is re-run every time, so that changes to the script become active. When the IDE is closed again, the project is re-run again and all connections are re-established.

Error Handling and Breakpoints

If an error occurs, QSInterpreter emits an QSInterpreter::error() signal.

If the IDE is open and an error occurs or a breakpoint is hit, the IDE goes into debug mode to allow the user debug the code. Until the user exits the debugger, no other scripting code can be executed.

When an error occurs during the execution of a script the script is stopped. What other behavior should occur is determined by the error mode which is set by calling QSInterpreter::setErrorMode(). See the documentation of that function for details. After the error has occurred, when the next QSInterpreter::call() or e.g. QSInterpreter::execute() is called, the script will be re-run. If you want to re-run the script immediately (e.g. to re-establish the connections), call QSProject::reset().

Instantiating QObjects from Qt Script

We have seen that script programmers can easily access application instances (of QObject subclasses), if the class is made available with an addObject() call. This is sufficient for most situations, but sometimes it may be desirable to allow script programmers to instantiate their own object instances. One solution is to expose an application object which has a slot that acts as a factory function, returning new QObject instances. Another solution is to allow the script writer to directly instantiate their own objects from C++ classes, with script code like this:

var a = new SomeCppObject( arg1, arg2 );

To make a QObject subclass available as a constructable object in Qt Script, use the QSObjectFactory class. This class makes it possible to create new C++ data-types and make them available to Qt Script.

Wrapping non-QObject C++ datatypes

Qt Script for Applications automatically wraps every QObject you pass it, and even ever QObject which is returned from a slot or passed into a slot. But often you have non-QObject datatypes in C++ which you want to make available to the script writer as well.

One possibility is to change your C++ API and convert all those datatypes to QObject subclasses. From a design and efficience point of view this is bad- imagine every item of a listview being a QObject subclass.

For that purpose Qt Script for Applications offers the class QSWrapperFactory. This class allows you to define non-QObject you can wrap. A QSWrapperFactory basically offers an QObject which can wrap a known C++ datatype. If Qt Script runs accross an unknown C++ datatype it will ask all installed QSWrapperFactories if one knows the type, and if this is the case, a wrapper for that datatype is instatiated and used.

[Prev: What is Qt Script for Applications] [Home] [Next: How to Design and Implement Application Objects]


Copyright © 2001-2002 TrolltechTrademarks
QSA version 1.0.0-beta1