|Home | Tutorial | Classes | Functions | QSA Workbench | Language | Qt API | QSA Articles | Qt Script for Applications | ![]() |
[Prev: What is Qt Script for Applications] [Home] [Next: How to Design and Implement Application Objects]
This chapter demonstrates how to write a C++ Qt application which integrates Qt Script for Applications to make the application extensible and customizable 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 demonstrate other Qt Script for Applications usage are also included in the examples directory.
To make a Qt/C++ application scriptable, you need the libqsa library that is included in the Qt Script for Applications package. To use libqsa and get other QSA specific build options, add the following line to your .pro file:
load( qsa )
The libqsa library provides the QSInterpreter class. A default instance of QSInterpreter is available, for convenience, by using the function QSInterpreter::defaultInterpreter().
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::addTransientObject() or QSProject::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 that is passed into the interpreter has been made available yet, the new object is added as a toplevel object to the scripting engine. This means that it is available through Application.object_name to the script writer.
If a parent object of the object has been previously added via a addObject(), 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 because the object can be used as a namespace or context later, and code can be added in the context of that object.
In most cases we do not pass QObjects which are directly used in the C++ application to the scripting engine because this would expose too many slots. Instead we implement interface QObjects which offer the signals, slots, and properties that we want to offer to the scripts and which will be simply implemented as forward function calls.
In our spreadsheet example we will add interface objects for the sheets. The interface objects 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.
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.
An instance of QSInterpreter can be used on its own, but to get full access to the functionality of Qt Script for Applications, use QSProject. To use a stand alone QSInterpreter use QSInterpreter::defaultInterpreter() or QSInterpreter::QSInterpreter(). To create an interpreter that runs with a project, create the project using QSProject::QSProject() and access its interpreter using QSProject::interpreter()
If you work with a project, you can either choose to use functionality in Qt Script for Applications to take care of everything (saving, loading, etc.) or you can decide to take care of most functionality yourself, giving you more flexibility.
If you choose to have Qt Script for Applications take care of everything for you, Qt Script for Applications then 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 the data block or file and extracted temporarily when loading. If you choose to take care of the functionality yourself, then open an empty project and use QSProject's API to add scripts manually and then retrieve them and save or store them however you'd like.
If you choose the first option, do the following:
Create a new QSProject and load in a scripting project using QSProject::load().
Loading a project can be called in one of two ways:
Call load() with a filename (including the path). Qt Script for Applications will open the specified project file.
Call loadFromData() 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 might 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 load(). If an empty byte-array is passed, a new project will be created. Use QSProject::saveToData() to retrieve a project as a byte-array suitable for use with the loadFromData() overload.
If you choose the second option, simply create a new QSProject and use its functions to add scripts and save the project at your convenience.
Of course it is possible to use a combination of both approaches, e.g. using the first approach, but later adding script code programmatically.
Note that when saving a project, the functions in QSInterpreter will add transient content to the interpreter, while the functions in the project will add persistent content to the interpreter. This means that content that is added to the project can be saved and will remain even if the interpreter state is cleared, while the content added using the QSInterpreter will not be saved and will be cleared along with the interpreter state.
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 following code is used to initialize QSProject (adding application objects and opening the project):
void SpreadSheet::init() { currentSheet = sheet1; project = new QSProject( this, "spreadsheet_project" ); interpreter = project->interpreter(); QSInputDialogFactory *fac = new QSInputDialogFactory; interpreter->addObjectFactory( fac ); project->addObject( new SheetInterface( sheet1, this, "sheet1" ) ); project->addObject( new SheetInterface( sheet2, this, "sheet2" ) ); setupSheet( sheet1 ); setupSheet( sheet2 ); project->load( "spreadsheet.qsa" ); connect( project, SIGNAL( projectEvaluated() ), project, SLOT( save() ) ); }
In some cases it might be sufficient to offer a basic editor widget which allows the user to write code. For example, you might want to embed the code editor directly into your application's user interface, and you don't want to open another toplevel window (such as QSA Workbench) for the user. In other cases, you want the users to be able to add and remove scripts, to have intelligent completion and hints in the editor, and to use GUI controls in the script. For these cases, including QSA Workbench is the best option.
There are two ways for the end user to use QSA Workbench.
Open QSA Workbench and create or edit a script.
Define a macro.
There are different ways to provide the scripting functionality to the end user depending on the type of application. For a typical end-user application you can offer one or both of the approaches to scripting mentioned above. For example you can provide a menu option and a toolbar button to launch QSA Workbench, and an editable combobox which lists all the global functions. If the user enters a function name that doesn't exist they are given the opportunity to create a new function of that name. If the user chooses an existing function, QSA Workbench is launched with the cursor at that function, ready for editing. A 'Run' toolbar button can be placed beside the combobox, so that the user can choose a function and click 'Run' to execute it.
Other approaches include enabling a user to: define functions to validate data of data entry forms, to customize the functionality of an editor, to customize the user interface of a complex 3D graphics application or to provide scripting modules for an image processing application.
The usage of application scripting can greatly vary depending on the type of application. The spreadsheet application described in this chapter is an example of a typical end-user application. This example will make you familiar with most of the important scripting concepts. Following this example will teach you how to use Qt Script for Applications to make your applications scriptable, even if the way your end users will use application scripting might be very different from what we describe here.
We define a macro as a stand-alone global function. Create a QSScript using QSProject::createScript( const QString & ) to create a script in global context. Call QSScript::addFunction() to add a new macro to the script. You can then open the editor and edit the newly created function.
void AddScriptDialog::addScript() { QSInterpreter *script = ( (SpreadSheet*) parent() )->interpreter; QString func = comboFunction->currentText(); if ( script->functions().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 ) { QSScript *sc = script->project()->script( "main.qs" ); if( !sc ) sc = script->project()->createScript( "main.qs" ); sc->addFunction( func ); ( (SpreadSheet*) parent() )->showFunction( sc, func ); } } emit newScript( func, editName->text(), *labelPixmap->pixmap() ); accept(); }
In the spreadsheet example the following slot is called to open QSA Workbench:
void SpreadSheet::openIDE() { #ifndef QSA_NO_IDE // open the QSA Workbench if ( !spreadsheet_ide ) spreadsheet_ide = new QSWorkbench( project, this, "qside" ); spreadsheet_ide->open(); #else QMessageBox::information( this, "Disabled feature", "QSA Workbench has been disabled. Reconfigure to enable", QMessageBox::Ok ); #endif }
In our spreadsheet example we want to enable the user to add macros as actions to the toolbar and menu which they can associate with a function that will be called when they activate the action. To add macros, 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 the user adds 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 combo box which lets the user choose a script function:
void AddScriptDialog::init() { // List all global functions of the project QSProject *project = ( (SpreadSheet*) parent() )->project; comboFunction->insertStringList( project->interpreter()->functions() ); }
When the user clicks OK in this dialog, the following slot is executed. If the function the user specified doesn't exist, the user is asked if this function should be added to the project, in which case, addFunction() is called:
void AddScriptDialog::addScript() { QSInterpreter *script = ( (SpreadSheet*) parent() )->interpreter; QString func = comboFunction->currentText(); if ( script->functions().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 ) { QSScript *sc = script->project()->script( "main.qs" ); if( !sc ) sc = script->project()->createScript( "main.qs" ); sc->addFunction( func ); ( (SpreadSheet*) parent() )->showFunction( sc, func ); } } emit newScript( func, editName->text(), *labelPixmap->pixmap() ); accept(); }
At the end of the function, the newScript() signal which is connected to the addScript() slot in the spreadsheet is emitted. 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) will 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() ) ); }
It would be tedious for users if they had to launch QSA Workbench and click Run every time they want 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 acheived 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. 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::functions(). 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 runScript() slot is triggered by the action, and we have to find which function should then be executed. For every slot, we call sender() (implemented in QObject), to find out what action 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 project->commitEditorContents(); interpreter->call( s, QValueList<QVariant>() ); }
It is possible to connect script functions to an application object's signals by letting the user edit scripts in QSA Workbench. These connections are established when the project is evaluated. When the user opens QSA Workbench, the project is paused for as long as QSA Workbench is open, and no connections are active during this time. When a scripting function is executed while QSA Workbench is opened or when play is pressed, the project is run again each time so that changes to the script become active. When QSA Workbench is closed again, the project is re-run once more and all connections are re-established.
If an error occurs, the QSInterpreter emits a QSInterpreter::error() signal.
We have shown that script programmers can easily access application instances of QObject subclasses if the class is made available to the interpreter. 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.
Qt Script for Applications automatically wraps every QObject you pass to it. It also wraps every QObject which is returned from a slot or passed into a slot. But you often 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 efficiency point of view, this is a bad way to go; imagine the effects of having every item of a listview being a QObject subclass.
Qt Script for Applications provides an innovative solution by offering the QSWrapperFactory class. This class allows you to define non-QObjects that you can wrap. A QSWrapperFactory basically offers a QObject which can wrap a known C++ datatype. If Qt Script runs accross an unknown C++ datatype it will ask all installed QSWrapperFactories if it knows the type. If one of the QSWrapperFactories knows the datatype, a wrapper for that datatype is instantiated and used.
We have demonstrated the flexibility that Qt Script offers for making applications scriptable. In the next chapter, we'll extend the application's functionality to end users by teaching them to create scripts with a simple, but complete example.
[Prev: What is Qt Script for Applications] [Home] [Next: How to Design and Implement Application Objects]
Copyright © 2001-2006 Trolltech | Trademarks | QSA version 1.1.5
|