Chapter 22. Application Scripting

Application Scripting

Scripts are programs written in an interpreted language that extend the facilities of an existing system. Some scripts are run as stand-alone applications, whereas others execute embedded in applications. Starting with version 4.3, Qt includes QtScript, a module that can be used to make Qt applications scriptable using ECMAScript, the standardized version of JavaScript. This module is a complete rewrite of the earlier Trolltech product Qt Script for Applications (QSA) and provides full support for ECMAScript Edition 3.

ECMAScript is the official name of the language standardized by Ecma International. It forms the basis of JavaScript (Mozilla), JScript (Microsoft), and ActionScript (Adobe). Although the language’s syntax is superficially similar to C++ and Java, the underlying concepts are radically different and set it apart from most other object-oriented programming languages. In the first section of this chapter, we will quickly review the ECMAScript language and show how to run ECMAScript code using Qt. If you already know JavaScript or any other ECMAScript-based language, you can probably just skim this section.

In the second section, we will see how to add scripting support to a Qt application. This makes it possible for users to add their own functionality in addition to what the application already provides. This approach is also frequently used to make it easier to support users; technical support staff can supply bug fixes and workarounds in the form of scripts.

In the third section, we will show how to develop GUI front-ends by combining ECMAScript code and forms created using Qt Designer. This technique is appealing to developers who dislike the “compile, link, run” cycle associated with C++ development and prefer the scripting approach. It also enables users with JavaScript experience to design fully functional GUI interfaces without having to learn C++.

Finally, in the last section, we will show how to develop scripts that rely on C++ components as part of their processing. This can be done with arbitrary C++ and C++/Qt components, which don’t need to have been designed with scriptability in mind. This approach is especially useful when several programs need to be written using the same basic components, or when we want to make C++ functionality available to non-C++ programmers.

Overview of the ECMAScript Language

This section presents a brief introduction to the ECMAScript language so that you can understand the code snippets presented in the rest of the chapter and can start writing your own scripts. The Mozilla Foundation’s web site hosts a more complete tutorial at http://developer.mozilla.org/en/docs/Core_JavaScript_1.5_Guide, and David Flanagan’s JavaScript: The Definitive Guide (O’Reilly, 2006) is recommended both as a tutorial and as a reference manual. The official ECMAScript specification is available online at http://www.ecmainternational.org/publications/standards/Ecma-262.htm.

The basic ECMAScript control structures—if statements, for loops, and while loops—are the same as in C++ and Java. ECMAScript also provides more or less the same assignment, relational, and arithmetic operators. ECMAScript strings support concatenation with + and appending with +=.

To get a feel for the ECMAScript syntax, we will start by studying the following program, which prints a list of all the prime numbers less than 1000:

const MAX = 1000;
var isPrime = new Array(MAX);

for (var i = 2; i < MAX; ++i)
    isPrime[i] = true;

for (var i = 2; i < MAX; ++i) {
    if (isPrime[i]) {
        for (var j = i; i * j < MAX; ++j)
            isPrime[i * j] = false;
    }
}

for (var i = 2; i < MAX; ++i) {
    if (isPrime[i])
        print(i);
}

From a C++ programmer’s perspective, probably the most striking feature of ECMAScript is that variables are not explicitly typed; the var keyword is all that is required to declare a variable. Read-only variables are declared with const instead of var. Another noteworthy feature of the preceding program is that there is no main() function. Instead, code that is located outside any function is executed immediately, starting from the top of the file and working down to the bottom.

Unlike in C++, semicolons at the end of statements are generally optional in ECMAScript. Using sophisticated rules, the interpreter can insert most missing semicolons itself. Despite this, typing semicolons ourselves is recommended, to avoid unpleasant surprises.

To run the above program, we can use the qscript interpreter located in Qt’s examples/script/qscript directory. If the interpreter is called with a file name as a command-line argument, the file is taken to be ECMAScript and is executed; otherwise, an interactive session begins.

If we don’t provide an initial value when declaring a variable with var, the default value is undefined, a special value of type Undefined. We can later assign any value of any type to the variable using the assignment operator (=). Consider the following examples:

var x;
typeof x;          // returns "undefined"

x = null;
typeof x;          // returns "null"

x = true;
typeof x;          // returns "boolean"

x = 5;
typeof x;          // returns "number"

x = "Hello";
typeof x;          // returns "string"

The typeof operator returns a lowercase string representation of the data type associated with the value stored in a variable. ECMAScript defines five primitive data types: Undefined, Null, Boolean, Number, and String. The Undefined and Null types are special types for the undefined and null constants, respectively. The Boolean type consists of two values, true and false. The Number type stores floating-point numbers. The String type stores Unicode strings.

Variables can also store objects and functions, corresponding to the data types Object and Function. For example:

x = new Array(10);
typeof x;          // returns "object"

x = print;
typeof x;          // returns "function"

Like Java, ECMAScript distinguishes between primitive data types and object types. Primitive data types behave like C++ value types, such as int and QString. These are created without the new operator and are copied by value. In contrast, object types must be created using the new operator, and variables of these types store only a reference (a pointer) to the object. When allocating objects with new, we do not need to worry about releasing their memory, since the garbage collector does this automatically.

If we assign a value to a variable without declaring it first using the var keyword, the variable will be created as a global variable. And if we try to read the value of a variable that doesn’t exist, we get a ReferenceError exception. We can catch the exception using a try ... catch statement, as follows:

try {
    print(y);
} catch (e) {
    print(e.name + ": " + e.message);
}

If the variable y does not exist, the message “ReferenceError: y is not defined” is printed on the console.

If undefined variables can cause havoc in our programs, so can variables that are defined but that hold the undefined constant—the default value if no initializer is provided when declaring a variable using var. To test for undefined, we can use the strict comparison operators === or !==. For example:

var x;
...
var y = 0;
if (x !== undefined)
    y = x;

The familiar == and != comparison operators are also available in ECMAScript, but unlike === and !==, they sometimes return true when the compared values have different types. For example, 24 == "24" and null == undefined return true, whereas 24 === "24" and null === undefined return false.

We will now review a more complex program that illustrates how to define our own functions in ECMAScript:

function square(x)
{
    return x * x;
}

function sumOfSquares(array)
{
    var result = 0;
    for (var i = 0; i < array.length; ++i)
        result += square(array[i]);
    return result;
}

var array = new Array(100);
for (var i = 0; i < array.length; ++i)
    array[i] = (i * 257) % 101;

print(sumOfSquares(array));

Functions are defined using the function keyword. In keeping with ECMAScript’s dynamic nature, the parameters are declared with no type, and the function has no explicit return type.

By looking at the code, we can guess that square() should be called with a Number and that sumOfSquares() should be called with an Array object, but this doesn’t have to be the case. For example, square("7") will return 49, because ECMAScript’s multiplication operator will convert strings to numbers in a numeric context. Similarly, the sumOfSquare() function will work not only for Array objects but also for other objects that have a similar interface.

In general, ECMAScript applies the duck typing principle: “If it walks like a duck and quacks like a duck, it must be a duck”. This stands in contrast to the strong typing used by C++ and Java, where parameter types must be declared and arguments must match the declared types.

In the preceding example, sumOfSquares() was hard-coded to apply square() on each element of the array. We can make it more flexible by letting it accept a unary function as the second argument and renaming it sum():

function sum(array, unaryFunc)
{
    var result = 0;
    for (var i = 0; i < array.length; ++i)
        result += unaryFunc(array[i]);
    return result;
}

var array = new Array(100);
for (var i = 0; i < array.length; ++i)
    array[i] = (i * 257) % 101;

print(sum(array, square));

The call sum(array, square) is equivalent to sumOfSquares(array). Instead of defining a square() function, we can also pass an anonymous function to sum():

print(sum(array, function(x) { return x * x; }));

And instead of defining an array variable, we can pass an array literal:

print(sum([4, 8, 11, 15], function(x) { return x * x; }));

ECMAScript lets us supply more arguments to a function than there are parameters declared. The extra arguments are accessible through the arguments array. Consider the following example:

function sum(unaryFunc)
{
    var result = 0;
    for (var i = 1; i < arguments.length; ++i)
        result += unaryFunc(arguments[i]);
    return result;
}

print(sum(square, 1, 2, 3, 4, 5, 6));

Here, the sum() function is defined to take a variable number of arguments. The first argument is the function that we want to apply. The other arguments are the numbers that we want to sum. When iterating over the arguments array, we must skip the item at index position 0 because it corresponds to unaryFunc, the unary function. We could also omit the unaryFunc parameter from the parameter list and extract it from the arguments array:

function sum()
{
    var unaryFunc = arguments[0];
    var result = 0;
    for (var i = 1; i < arguments.length; ++i)
        result += unaryFunc(arguments[i]);
    return result;
}

The arguments array can be used to overload the behavior of functions based on the types of the arguments or on their number. For example, suppose that we want to let sum() take an optional unary argument followed by a list of numbers, allowing us to invoke it as follows:

print(sum(1, 2, 3, 4, 5, 6));
print(sum(square, 1, 2, 3, 4, 5, 6));

Here’s how we can implement sum() to support this:

function sum()
{
    var unaryFunc = function(x) { return x; };
    var i = 0;

    if (typeof arguments[0] == "function") {
        unaryFunc = arguments[0];
        i = 1;
    }

    var result = 0;
    while (i < arguments.length)
        result += unaryFunc(arguments[i++]);
    return result;
}

If we supply a function as the first argument, that function is applied to each number before it is added to the sum; otherwise, we use the identity function function(x) { return x; }.

For C++ programmers, arguably the most difficult aspect of ECMAScript is its object model. ECMAScript is an object-based object-oriented language, setting it apart from C++, C#, Java, Simula, and Smalltalk, which are all class-based. Instead of a class concept, ECMAScript provides us with lower-level mechanisms that let us achieve the same results.

The first mechanism that lets us implement classes in ECMAScript is that of a constructor. A constructor is a function that can be invoked using the new operator. For example, here is a constructor for a Shape object:

function Shape(x, y) {
    this.x = x;
    this.y = y;
}

The Shape constructor has two parameters and initializes the new object’s x and y properties (member variables) based on the values passed to the constructor. The this keyword refers to the object being created. In ECMAScript, an object is essentially a collection of properties; properties can be added, removed, or modified at any time. A property is created the first time it is set, so when we assign to this.x and this.y in the constructor, the x and y properties are created as a result.

A common mistake for C++ and Java developers is to forget the this keyword when accessing object properties. In the preceding example, this would have been unlikely, because a statement such as x = x would have looked very suspicious, but in other examples this would have led to the creation of spurious global variables.

To instantiate a Shape object, we use the new operator as follows:

var shape = new Shape(10, 20);

If we use the typeof operator on the shape variable, we obtain Object, not Shape, as the data type. If we want to determine whether an object has been created using the Shape constructor, we can use the instanceof operator:

var array = new Array(100);
array instanceof Shape;            // returns false

var shape = new Shape(10, 20);
shape instanceof Shape;            // returns true

ECMAScript lets any function serve as a constructor. However, if the function doesn’t perform any modifications to the this object, it doesn’t make much sense to invoke it as a constructor. Conversely, a constructor can be invoked as a plain function, but again this rarely makes sense.

In addition to the primitive data types, ECMAScript provides built-in constructors that let us instantiate fundamental object types, notably Array, Date, and RegExp. Other constructors correspond to the primitive data types, allowing us to create objects that store primitive values. The valueOf() member function lets us retrieve the primitive value stored in the object. For example:

var boolObj = new Boolean(true);
typeof boolObj;                    // returns "object"

var boolVal = boolObj.valueOf();
typeof boolVal;                    // returns "boolean"

Figure 22.1 lists the built-in global constants, functions, and objects provided by ECMAScript. In the next section, we will see how to supplement this built-in functionality with additional, application-specific components written in C++.

Constants

NaN

IEEE 754 Not-a-Number (NaN) value

Infinity

Positive infinity (+)

undefined

Default value for uninitialized variables

Functions

print(x)[*]

Prints a value on the console

eval(str)

Executes an ECMAScript program

parseInt(str, base)

Converts a string to an integer value

parseFloat(str)

Converts a string to a floating-point value

isNaN(n)

Returns true if n is NaN

isFinite(n)

Returns true if n is a number other than NaN, +∞, or −

decodeURI(str)

Converts an 8-bit-encoded URI to Unicode

decodeURIComponent(str)

Converts an 8-bit-encoded URI component to Unicode

encodeURI(str)

Converts a Unicode URI to an 8-bit-encoded URI

encodeURIComponent(str)

Converts a Unicode URI component to 8-bit-encoded

[*] Not specified by the ECMAScript standard.

Classes (Constructors)

Object

Provides functionality common to all objects

Function

Encapsulates an ECMAScript function

Array

Represents a resizable vector of items

String

Stores a Unicode string

Boolean

Stores a Boolean value (true or false)

Number

Stores a floating-point number

Date

Stores a date and time

RegExp

Provides regular expression pattern matching

Error

Base type for error types

EvalError

Raised when using eval() wrongly

RangeError

Raised when a numeric value is outside the legal range

ReferenceError

Raised when trying to access an undefined variable

SyntaxError

Raised when a syntax error is detected by eval()

TypeError

Raised when an argument has the wrong type

URIError

Raised when URI parsing fails

Object

Math

Provides mathematical constants and functions

Figure 22.1. Built-in properties of the global object

We have seen how to define a constructor in ECMAScript and how to add member variables to the constructed object. Normally, we also want to define member functions. Because functions are treated as first-class citizens in ECMAScript, this turns out to be surprisingly easy. Here’s a new version of the Shape constructor, this time with two member functions, manhattanPos() and translate():

function Shape(x, y) {
    this.x = x;
    this.y = y;

    this.manhattanPos = function() {
        return Math.abs(this.x) + Math.abs(this.y);
    };
    this.translate = function(dx, dy) {
        this.x += dx;
        this.y += dy;
    };
}

We can then invoke the member functions using the . (dot) operator:

var shape = new Shape(10, 20);
shape.translate(100, 100);
print(shape.x + ", " + shape.y + " (" + shape.manhattanPos() + ")");

With this approach, each Shape instance has its own manhattanPos and translate properties. Since these properties should be identical for all Shape instances, it is desirable to store them only once rather than in every instance. ECMAScript lets us achieve this by using a prototype. A prototype is an object that serves as a fallback for other objects, providing an initial set of properties. One advantage of this approach is that it is possible to change the prototype object at any time and the changes are immediately reflected in all objects that were created with that prototype.

Consider the following example:

function Shape(x, y) {
    this.x = x;
    this.y = y;
}

Shape.prototype.manhattanPos = function() {
    return Math.abs(this.x) + Math.abs(this.y);
};

Shape.prototype.translate = function(dx, dy) {
    this.x += dx;
    this.y += dy;
};

In this version of Shape, we create the manhattanPos and translate properties outside the constructor, as properties of the Shape.prototype object. When we instantiate a Shape, the newly created object keeps an internal pointer back to Shape.prototype. Whenever we retrieve the value of a property that doesn’t exist in our Shape object, the property is looked up in the prototype as a fallback. Thus, the Shape prototype is the ideal place to put member functions, which should be shared by all Shape instances.

It might be tempting to put all sorts of properties that we want to share between Shape instances in the prototype, similar to C++’s static member variables or Java’s class variables. This idiom works for read-only properties (including member functions) because the prototype acts as a fallback when we retrieve the value of a property. However, it doesn’t work as expected when we try to assign a new value to the shared variable; instead, a fresh variable is created directly in the Shape object, shadowing any property of the same name in the prototype. This asymmetry between read and write access to a variable is a frequent source of confusion for novice ECMAScript programmers.

In class-based languages such as C++ and Java, we can use class inheritance to create specialized object types. For example, we would define a Shape class and then derive Triangle, Square, and Circle from Shape. In ECMAScript, a similar effect can be achieved using prototypes. The following example shows how to define Circle objects that are also Shape instances:

function Shape(x, y) {
    this.x = x;
    this.y = y;
}

Shape.prototype.area = function() { return 0; };

function Circle(x, y, radius) {
    Shape.call(this, x, y);
    this.radius = radius;
}

Circle.prototype = new Shape;
Circle.prototype.area = function() {
    return Math.PI * this.radius * this.radius;
};

We start by defining a Shape constructor and associate an area() function with it, which always returns 0. Then we define a Circle constructor, which calls the “base class” constructor using the call() function defined for all function objects (including constructors), and we add a radius property. Outside the constructor, we set the Circle’s prototype to be a Shape object, and we override Shape’s area() function with a Circle-specific implementation. This corresponds to the following C++ code:

class Shape
{
public:
    Shape(double x, double y) {
        this->x = x;
        this->y = y;
    }

    virtual double area() const { return 0; }

    double x;
    double y;
};

class Circle : public Shape
{
public:
    Circle(double x, double y, double radius)
        : Shape(x, y)
    {
        this->radius = radius;
    }

    double area() const { return M_PI * radius * radius; }

    double radius;
};

The instanceof operator walks through the prototype chain to determine which constructors have been invoked. As a consequence, instances of a subclass are also considered to be instances of the base class:

var circle = new Circle(0, 0, 50);
circle instanceof Circle;          // returns true
circle instanceof Shape;           // returns true
circle instanceof Object;          // returns true
circle instanceof Array;           // returns false

This concludes our short introduction to ECMAScript. In the following sections, we will show how to use this language in conjunction with C++/Qt applications to provide more flexibility and customizability to end-users or simply to speed up the development process.

Extending Qt Applications with Scripts

Using the QtScript module, we can write C++ applications that can execute ECMAScript. Scripts can be used to extend application functionality without requiring the application itself to be rebuilt and redeployed. We can limit the scripts to a hard-coded set of ECMAScript files that are provided as part of the application and that can be replaced with new versions independently of new releases of the application, or we can make the application able to use arbitrary ECMAScript files.

Executing a script from a C++ application typically involves the following steps:

  1. Load the script into a QString.

  2. Create a QScriptEngine object and set it up to expose application-specific functionality.

  3. Execute the script.

To illustrate this, we will study the Calculator application shown in Figure 22.2. The Calculator application lets the user provide custom buttons by implementing their functionality in scripts. When the application starts up, it traverses the scripts subdirectory, looking for script files, and creates calculator buttons associated with these scripts. By default, Calculator includes the following scripts:

  • cube.js computes the cube of the current value (x3).

  • factorial.js computes the factorial of the current value (x!).

  • pi.js overwrites the current value with an approximation of π.

The Calculator application

Figure 22.2. The Calculator application

Most of the Calculator application’s code is the same kind of C++/Qt code that we have seen throughout the book. Here, we will review only those parts of the code that are relevant to scripting, starting with the createCustomButtons() private function, which is called from the Calculator constructor:

void Calculator::createCustomButtons()
{
    QDir scriptsDir = directoryOf("scripts");
    QStringList fileNames = scriptsDir.entryList(QStringList("*.js"),
                                                 QDir::Files);
    foreach (QString fileName, fileNames) {
        QString text = fileName;
        text.chop(3);
        QToolButton *button = createButton(text,
                                           SLOT(customButtonClicked()));
        button->setStyleSheet("color: rgb(31, 63, 127)");
        button->setProperty("scriptFileName",
                            scriptsDir.absoluteFilePath(fileName));
        customButtons.append(button);
    }
}

The createCustomButtons() function uses a QDir object to traverse the application’s scripts subdirectory, looking for files with a .js extension. It uses the same directoryOf() function we used in Chapter 17 (p. 410).

For each .js file, we create a QToolButton by calling the private createButton() function. This function also connects the new button’s clicked() signal to the customButtonClicked() slot. Then we set the button’s style sheet to make the foreground text blue, to distinguish the custom buttons from the built-in buttons.

The call to QObject::setProperty() dynamically creates a new scriptFileName property for each QToolButton. We use this property in the customButtonClicked() slot to determine which script should be executed.

Finally, we add the new button to the customButtons list. The Calculator constructor uses this list to add the custom buttons to the window’s grid layout.

For this application, we have chosen to traverse the scripts directory just once, at application startup. An alternative would be to use a QFileSystemWatcher to monitor the scripts directory, and update the calculator whenever the directory’s content changes, allowing the user to add new scripts and remove existing scripts without having to restart the application.

void Calculator::customButtonClicked()
{
    QToolButton *clickedButton = qobject_cast<QToolButton *>(sender());
    QFile file(clickedButton->property("scriptFileName").toString());
    if (!file.open(QIODevice::ReadOnly)) {
        abortOperation();
        return;
    }

    QTextStream in(&file);
    in.setCodec("UTF-8");
    QString script = in.readAll();
    file.close();

    QScriptEngine interpreter;
    QScriptValue operand(&interpreter, display->text().toDouble());
    interpreter.globalObject().setProperty("x", operand);
    QScriptValue result = interpreter.evaluate(script);
    if (!result.isNumber()) {
        abortOperation();
        return;
    }

    setDisplayValue(result.toNumber());
    waitingForOperand = true;
}

In the customButtonClicked() slot, we first call QObject::sender() to determine which button was clicked. Then we extract the scriptFileName property to retrieve the name of the .js file associated with the button. Next, we load the file’s contents into a string called script.

The ECMAScript standard requires that interpreters support Unicode, but it does not mandate any particular encoding to use for storing scripts on disk. We have chosen to assume that our .js files use UTF-8, a superset of plain ASCII.

Once we have the script in a QString, we create a QScriptEngine to execute it. A QScriptEngine instance represents an ECMAScript interpreter and holds a current state. We can have any number of QScriptEngines at the same time for different purposes, each with its own state.

Before we can run the script, we must make it possible for the script to retrieve the current value displayed by the calculator. The approach we have chosen here is to create an ECMAScript global variable called x—or, more precisely, we have added a dynamic property called x to the interpreter’s global object. From script code, this property is available directly as x.

The value we set for x must be of type QScriptValue. Conceptually, a QScriptValue is similar to QVariant in that it can store many data types, except that it is tailored to store ECMAScript data types.

Finally, we run the script using QScriptEngine::evaluate(). The result is the value returned by the script, or an exception object if an error occurred. (In the next section, we will see how to report errors to the user in a message box.) A script’s return value is the value explicitly returned by a return call; if return is omitted, the return value is the result of the last expression evaluated in the script. Once we have the return value, we check whether it is a number, and if it is, we display it.

For this example, we evaluate a script every time the user presses its corresponding button. Since this step involves loading and parsing the entire script, it is often preferable to use a different approach, where the script doesn’t directly perform an operation, but rather returns a function or an object that can be used later on. We will use this alternative approach in the next section.

To link against the QtScript library, we must add this line to the application’s .pro file:

QT += script

The example scripts are quite simple. Here’s the one-line pi.js:

return 3.14159265358979323846;

Notice that we ignore the calculator’s x value. The cube.js script is also a one-liner, but it does make use of the x value:

return x * x * x;

The factorial.js script defines a function and calls it:

function factorial(n)
{
    if (n <= 1) {
        return 1;
    } else {
        return n * factorial(n - 1);
    }
}

return factorial(Math.floor(x));

The standard factorial function only operates on integers, so we have used the Math.floor() function to convert x to an integer.

We have now seen the fundamentals of the QtScript module: the QScriptEngine, which represents an interpreter with its current state, and QScriptValue, which stores an ECMAScript value.

In the Calculator example, there was very little interaction between the scripts and the application: The scripts take only one parameter from the application and return a single value. In the following sections, we will see more advanced integration strategies and show how to report exceptions to the user.

Implementing GUI Extensions Using Scripts

Providing scripts to compute values, as we did in the preceding section, is useful but limited. Often, we want to access some of the application’s widgets and other components directly from the script. We might also want to provide additional dialogs by combining ECMAScript files with Qt Designer .ui files. Using these techniques, it is possible to develop applications mostly in ECMAScript, which is appealing to some programmers.

In this section, we will look at the HTML Editor application shown in Figure 22.3. This application is a plain text editor that highlights HTML tags using a QSyntaxHighlighter. What makes the application special is that it provides a Scripts menu that is populated with extensions provided as .js scripts, along with corresponding .ui dialogs, in the application’s scripts subdirectory. The dialogs let the user parameterize the operation they want performed.

The HTML Editor application

Figure 22.3. The HTML Editor application

We have provided two extensions: a Statistics dialog and a Reformat Text dialog, both shown in Figure 22.4. The Statistics dialog is purely informative. It counts the number of characters, words, and lines in a document and presents the totals to the user in a modal dialog. The Reformat Text dialog is more sophisticated. It is a modeless dialog, which means that the user can continue to interact with the application’s main window while the dialog is shown. The dialog can be used to reindent the text, to wrap long lines, and to standardize the case used for tags. All these operations are implemented in ECMAScript.

The Statistics and Reformat Text dialogs

Figure 22.4. The Statistics and Reformat Text dialogs

The heart of the application is the HtmlWindow class, a QMainWindow subclass that uses a QTextEdit as its central widget. Here, we will review only those parts of the code that are relevant to application scripting.

When the application starts up, we must populate the Scripts menu with actions corresponding to the .js and .ui files found in the scripts subdirectory. This is quite similar to what we did in the Calculator application’s createCustomButtons() function in the preceding section:

void HtmlWindow::createScriptsMenu()
{
    scriptsMenu = menuBar()->addMenu(tr("&Scripts"));

    QDir scriptsDir = directoryOf("scripts");
    QStringList jsFileNames = scriptsDir.entryList(QStringList("*.js"),
                                                   QDir::Files);
    foreach (QString jsFileName, jsFileNames)
        createScriptAction(scriptsDir.absoluteFilePath(jsFileName));

    scriptsMenu->setEnabled(!scriptsMenu->isEmpty());
}

For each script, we call the createScriptAction() function to create an action and add it to the Scripts menu. If no scripts are found, we disable the menu.

The createScriptAction() function performs the following steps:

  1. Load and evaluate the script, storing the resulting object in a variable.

  2. Construct a dialog from the .ui file using QUiLoader.

  3. Make the dialog accessible to the script.

  4. Expose application-specific functionality to the script.

  5. Create a QAction to make the script accessible to the user.

The function has to do a lot of work and is quite long, so we will review it in parts.

bool HtmlWindow::createScriptAction(const QString &jsFileName)
{
    QFile jsFile(jsFileName);
    if (!jsFile.open(QIODevice::ReadOnly)) {
        QMessageBox::warning(this, tr("HTML Editor"),
                             tr("Cannot read file %1:
%2.")
                             .arg(strippedName(jsFileName))
                             .arg(jsFile.errorString()));
        return false;
    }

    QTextStream in(&jsFile);
    in.setCodec("UTF-8");
    QString script = in.readAll();
    jsFile.close();

    QScriptValue qsScript = interpreter.evaluate(script);
    if (interpreter.hasUncaughtException()) {
        QMessageBox messageBox(this);
        messageBox.setIcon(QMessageBox::Warning);
        messageBox.setWindowTitle(tr("HTML Editor"));
        messageBox.setText(tr("An error occurred while executing the "
                              "script %1.")
                           .arg(strippedName(jsFileName)));
        messageBox.setInformativeText(
                tr("%1.").arg(interpreter.uncaughtException()
                              .toString()));
        messageBox.setDetailedText(
                interpreter.uncaughtExceptionBacktrace().join("
"));
        messageBox.exec();
        return false;
    }

We begin by reading in the .js file. Since we need to use only one interpreter, we have a single QScriptEngine member variable called interpreter. We evaluate the script and store its return value as a QScriptValue called qsScript.

If the script cannot be evaluated (e.g., due to a syntax error), the QScriptEngine::hasUncaughtException() function will return true. In this case, we report the error using a QMessageBox.

For the scripts used by this application, we have adopted the convention that each script must return an ECMAScript Object when it is evaluated. This Object must provide two properties: a string called text that holds the text to be used in the Scripts menu to identify the script, and a function called run() that should be called when the user chooses the script from the Scripts menu. We store the Object in the qsScript variable. The main benefit of this approach is that we need to read and parse the scripts only once, at startup.

    QString uiFileName = jsFileName;
    uiFileName.chop(3);
    uiFileName += ".ui";

    QFile uiFile(uiFileName);
    if (!uiFile.open(QIODevice::ReadOnly)) {
        QMessageBox::warning(this, tr("HTML Editor"),
                             tr("Cannot read file %1:
%2.")
                             .arg(strippedName(uiFileName))
                             .arg(uiFile.errorString()));
        return false;
    }

    QUiLoader loader;
    QWidget *dialog = loader.load(&uiFile, this);
    uiFile.close();
    if (!dialog) {
        QMessageBox::warning(this, tr("HTML Editor"),
                             tr("Error loading %1.")
                             .arg(strippedName(uiFileName)));
        return false;
    }

Another convention we have adopted is that each script must have a corresponding .ui file to provide the script with a GUI dialog. The .ui file must have the same base name as the script.

We attempt to read the .ui file and to dynamically create a QWidget that contains all the widgets, layouts, and connections specified in the .ui file. The widget’s parent is given as the second argument to the load() call. If an error occurs, we warn the user and return.

    QScriptValue qsDialog = interpreter.newQObject(dialog);
    qsScript.setProperty("dialog", qsDialog);

    QScriptValue qsTextEdit = interpreter.newQObject(textEdit);
    qsScript.setProperty("textEdit", qsTextEdit);
    QAction *action = new QAction(this);
    action->setText(qsScript.property("text").toString());
    action->setData(QVariant::fromValue(qsScript));
    connect(action, SIGNAL(triggered()),
            this, SLOT(scriptActionTriggered()));

    scriptsMenu->addAction(action);

    return true;
}

Once we have successfully read the script and its user interface file, we are almost ready to add the script to the Scripts menu. But first, there are a few details that we must attend to. We want the run() function of our script to have access to the dialog we just created. In addition, the script should be allowed to access the QTextEdit that contains the HTML document being edited.

We begin by adding the dialog to the interpreter as a QObject *. In response, the interpreter returns the Object that it uses to represent the dialog. We store this in qsDialog. We add the qsDialog object to the qsScript object as a new property called dialog. This means that the script can access the dialog, including its widgets, through the newly created dialog property. We use the same technique to provide the script with access to the application’s QTextEdit.

Finally, we create a new QAction to represent the script in the GUI. We set the action’s text to qsScript’s text property, and the action’s “data” item to qsScript itself. Lastly, we connect the action’s triggered() signal to a custom scriptActionTriggered() slot, and add the action to the Scripts menu.

void HtmlWindow::scriptActionTriggered()
{
    QAction *action = qobject_cast<QAction *>(sender());
    QScriptValue qsScript = action->data().value<QScriptValue>();
    qsScript.property("run").call(qsScript);
}

When this slot is called, we begin by finding out which QAction was triggered. Then we extract the action’s user data using QVariant::value<T>() to cast it to a QScriptValue, which we store in qsScript. Then we invoke qsScript’s run() function, passing qsScript as a parameter; this will make qsScript the this object inside the run() function.[*]

QAction’s “data” item mechanism is based on QVariant. The QScriptValue type is not one of the data types that QVariant recognizes. Fortunately, Qt provides a mechanism for extending the types that QVariant can handle. At the beginning of htmlwindow.cpp, after the #includes, we have the following line:

Q_DECLARE_METATYPE(QScriptValue)

This line should appear after the custom data type it refers to has been declared, and can be done only for data types that have a default constructor and a copy constructor.

Now that we have seen how to load a script and a user interface file, and how to provide an action that the user can trigger to run the script, we are ready to look at the scripts themselves. We will begin with the Statistics script since it is the easiest and shortest, reviewing it in parts.

var obj = new Object;

obj.text = "&Statistics...";

We begin by creating a new Object. This is the object we will add properties to and that we will return to the interpreter. The first property we set is the text property, with the text that we want to appear in the Scripts menu.

obj.run = function() {
    var text = this.textEdit.plainText;
    this.dialog.frame.charCountLineEdit.text = text.length;
    this.dialog.frame.wordCountLineEdit.text = this.wordCount(text);
    this.dialog.frame.lineCountLineEdit.text = this.lineCount(text);
    this.dialog.exec();
};

The second property we create is the run() function. The function reads the text from the dialog’s QTextEdit, populates the dialog’s widgets with the results of the calculations, and finishes by modally showing the dialog.

This function can work only if the Object variable, obj, has suitable textEdit and dialog properties, which is why we needed to add them at the end of the createScriptAction() function. The dialog itself must have a frame object (in this case a QFrame, but the type does not matter), with three child widgets—charCountLineEdit, wordCountLineEdit, and lineCountLineEdit, each with a writable text property. Instead of this.dialog.frame.xxxCountLineEdit, we could also write findChild(" xxx CountLineEdit"), which performs a recursive search and is therefore more robust if we choose to change the dialog’s design.

obj.wordCount = function(text) {
    var regExp = new RegExp("\w+", "g");
    var count = 0;
    while (regExp.exec(text))
        ++count;
    return count;
};

obj.lineCount = function(text) {
    var count = 0;
    var pos = 0;
    while ((pos = text.indexOf("
", pos)) != -1) {
        ++count;
        ++pos;
    }
    return count + 1;
    };

    return obj;

The wordCount() and lineCount() functions have no external dependencies and work purely in terms of the String passed in to them. Note that the wordCount() function uses the ECMAScript RegExp class, not Qt’s QRegExp class. At the end of the script file, the return statement ensures that the Object with the text and run() function properties is returned to the interpreter, ready to be used.

The Reformat script follows a similar pattern to the Statistics script. We will look at it next.

var obj = new Object;

obj.initialized = false;

obj.text = "&Reformat...";

obj.run = function() {
    if (!this.initialized) {
        this.dialog.applyButton.clicked.connect(this, this.apply);
        this.dialog.closeButton.clicked.connect(this, this.dialog.close);
        this.initialized = true;
    }
    this.dialog.show();
};

obj.apply = function() {
    var text = this.textEdit.plainText;

    this.textEdit.readOnly = true;
    this.dialog.applyButton.enabled = false;

    if (this.dialog.indentGroupBox.checked) {
        var size = this.dialog.indentGroupBox.indentSizeSpinBox.value;
        text = this.reindented(text, size);
    }
    if (this.dialog.wrapGroupBox.checked) {
        var margin = this.dialog.wrapGroupBox.wrapMarginSpinBox.value;
        text = this.wrapped(text, margin);
    }
    if (this.dialog.caseGroupBox.checked) {
        var lowercase = this.dialog.caseGroupBox.lowercaseRadio.checked;
        text = this.fixedTagCase(text, lowercase);
    }

    this.textEdit.plainText = text;
    this.textEdit.readOnly = false;
    this.dialog.applyButton.enabled = true;
};

obj.reindented = function(text, size) {
    ...
};

obj.wrapped = function(text, margin) {
    ...
};

obj.fixedTagCase = function(text, lowercase) {
    ...
};

return obj;

We use the same pattern as before, creating a featureless Object, adding properties to it, and returning it to the interpreter. In addition to the text and run() properties, we add an initialized property. The first time run() is called, initialized is false, so we set up the signal–slot connections that link button clicks in the dialog to functions defined in the script.

The same kinds of assumptions apply here as applied to the Statistics script. We assume that there is a suitable dialog property and that it has buttons called applyButton and closeButton. The apply() function interacts with the dialog’s widgets, in particular with the Apply button (to disable and enable it) and with the group boxes, checkboxes, and spin boxes. It also interacts with the main window’s QTextEdit from where it gets the text to work on, and to which it gives the text that results from the reformatting.

We omitted the code for the reindented(), wrapped(), and fixedTagCase() functions used internally by the script, since the actual computations are not relevant to understanding how to make Qt applications scriptable.

We have now completed our technical review of how to use scripts within C++/Qt applications, including ones that have their own dialogs. In applications such as HTML Editor, where the scripts interact with application objects, we must also consider licensing issues. For open source applications, there are no constraints beyond those imposed by the requirements of the open source license itself. For commercial applications, the story is slightly more complicated. Those who write scripts for commercial applications, including an application’s end-users, are free to do so if their scripts use only built-in ECMAScript classes and application-specific APIs, or if they use the Qt API to perform minor extensions or modifications to existing components. But any script writer whose scripts implement core GUI functionality must have a commercial Qt license. Commercial Qt users should contact their Trolltech sales representative if they have licensing questions.

Automating Tasks through Scripting

Sometimes we use GUI applications to manipulate data sets in the same way each time. If the manipulation consists of invoking many menu options, or interacting with a dialog, not only does it become tedious but there is a risk that on some occasions we may miss steps, or transpose the order of a couple of steps—and perhaps not realize that a mistake has been made. One way to make things easier for users is to allow them to write scripts to perform sequences of actions automatically.

In this section, we will present a GUI application that offers a command-line option, -script, that lets the user specify a script to execute. The application will then start up, execute the script, and terminate, with no GUI appearing at all.

The application we will use to illustrate this technique is called Gas Pump. It reads in lists of transactions recorded by a trucker gas station’s pumps and presents the data in a tabular format, as shown in Figure 22.5.

The Gas Pump application

Figure 22.5. The Gas Pump application

Each transaction is recorded by date and time, and by the pump, the quantity taken, the company ID and user ID of the trucker, and the transaction’s status. The Gas Pump application can be used to manipulate the data in quite sophisticated ways, sorting it, filtering it, computing totals, and converting between liters and gallons.

The Gas Pump application supports transaction data in two formats: “Pump 2000”, a plain text format with extension .p20, and “XML Gas Pump”, an XML format with extension .gpx. The application can load and save in both formats, so it can be used to convert between them, simply by loading in one format and saving in the other.

The application is supplied with four standard scripts:

  • onlyok.js removes all transactions whose status is not “OK”.

  • p20togpx.js converts a Pump 2000 file to the XML Gas Pump file format.

  • tohtml.js produces reports in HTML format.

  • toliters.js converts the units from gallons to liters.

The scripts are invoked using the -script command-line option followed by the name of the script, and then the name of the files to operate on. For example:

gaspump -script scripts/toliters.js data/2008q2.p20

Here, we run the toliters.js script from the scripts subdirectory on the 2008q2.p20 Pump 2000 data file from the data subdirectory. The script converts all the quantity values from gallons to liters, changing the file in-place.

The Gas Pump application is written just like any other C++/Qt application. In fact, its code is very similar to the Spreadsheet example from Chapters 3 and 4. The application has a QMainWindow subclass called PumpWindow that provides the application’s framework, including its actions and menus. (The menus are shown in Figure 22.6.) There is also a custom QTableWidget called PumpSpreadsheet for displaying the data. And there is a QDialog subclass, FilterDialog shown in Figure 22.7, that the user can use to specify their filter options. Because there are a lot of filter options, they are stored together in a class called PumpFilter. We will very briefly review these classes, and then we will see how to add scripting support to the application.

class PumpSpreadsheet : public QTableWidget
{

    Q_OBJECT
    Q_ENUMS(FileFormat Column)

public:
    enum FileFormat { Pump2000, GasPumpXml };
    enum Column { Date, Time, Pump, Company, User, Quantity, Status,
                  ColumnCount };

    PumpSpreadsheet(QWidget *parent = 0);

public slots:
    void clearData();
    bool addData(const QString &fileName, FileFormat format);
    bool saveData(const QString &fileName, FileFormat format);
    void sortByColumn(Column column,
                      Qt::SortOrder order = Qt::AscendingOrder);
    void applyFilter(const PumpFilter &filter);
    void convertUnits(double factor);
    void computeTotals(Column column);
    void setText(int row, int column, const QString &text);
    QString text(int row, int column) const;

private:
    ...
};
The Gas Pump application’s menus

Figure 22.6. The Gas Pump application’s menus

The Filter dialog

Figure 22.7. The Filter dialog

The PumpSpreadsheet holds the data and provides the functions (which we have made into slots) that the user can use to manipulate the data. The slots are accessible through the user interface, and will also be available for scripting. The Q_ENUMS() macro is used to generate meta-information about the FileFormat and Column enum types; we will come back to this shortly.

The QMainWindow subclass, PumpWindow, has a loadData() function that makes use of some PumpSpreadsheet slots:

void PumpWindow::loadData()
{
    QString fileName = QFileDialog::getOpenFileName(this,
                               tr("Open Data File"), ".",
                               fileFilters);
    if (!fileName.isEmpty()) {
        spreadsheet->clearData();
        spreadsheet->addData(fileName, fileFormat(fileName));
    }
}

The PumpSpreadsheet is stored in the PumpWindow as a member variable called spreadsheet. The PumpWindow’s filter() slot is less typical:

void PumpWindow::filter()
{
    FilterDialog dialog(this);
    dialog.initFromSpreadsheet(spreadsheet);
    if (dialog.exec())
    spreadsheet->applyFilter(dialog.filter());
}

The initFromSpreadsheet() function populates the FilterDialog’s comboboxes with the pumps, company IDs, user IDs, and status codes that are in use in the current data set. When exec() is called, the dialog shown in Figure 22.7 pops up. If the user clicks OK, the FilterDialog’s filter() function returns a PumpFilter object that we pass on to PumpSpreadsheet::applyFilter().

class PumpFilter
{
public:
    PumpFilter();

    QDate fromDate;
    QDate toDate;
    QTime fromTime;
    QTime toTime;
    QString pump;
    QString company;
    QString user;
    double fromQuantity;
    double toQuantity;
    QString status;
};

The purpose of PumpFilter is to make it easier to pass around filter options as a group rather than having ten separate parameters.

So far, all we have seen has been unsurprising. The only barely noticeable differences are that we have made all the PumpSpreadsheet functions that we want scriptable into public slots, and we have used the Q_ENUMS() macro. To make Gas Pump scriptable, we must do two more things. First, we must change main.cpp to add the command-line processing and to execute the script if one is specified. Second, we must make the application’s functionality available to scripts.

The QtScript module provides two general ways of exposing C++ classes to scripts. The easiest way is to define the functionality in a QObject class and to expose one or more instances of that class to the script, using QScriptEngine::newQObject(). The properties and slots defined by the class (and optionally by its ancestors) are then available to scripts. The more difficult but also more flexible approach is to write a C++ prototype for the class and possibly a constructor function, for classes that need to be instantiated from the script using the new operator. The Gas Pump example illustrates both approaches.

Before we study the infrastructure used to run scripts, let us look at one of the scripts that is supplied with Gas Pump. Here is the complete onlyok.js script:

if (args.length == 0)
    throw Error("No files specified on the command line");

for (var i = 0; i < args.length; ++i) {
     spreadsheet.clearData();
    if (!spreadsheet.addData(args[i], PumpSpreadsheet.Pump2000))
        throw Error("Error loading Pump 2000 data");

    var filter = new PumpFilter;
    filter.status = "OK";

    spreadsheet.applyFilter(filter);
    if (!spreadsheet.saveData(args[i], PumpSpreadsheet.Pump2000))
        throw Error("Error saving Pump 2000 data");

    print("Removed erroneous transactions from " + args[i]);
}

This script relies on two global variables: args and spreadsheet. The args variable returns the command-line arguments supplied after the -script option. The spreadsheet variable is a reference to a PumpSpreadsheet object that we can use to perform various operations (file format conversion, unit conversion, filtering, etc.). The script also calls some slots on the PumpSpreadsheet object, instantiates and initializes PumpFilter objects, and uses the PumpSpreadsheet::FileFormat enum.

We begin with a simple sanity check, and then for each file name listed on the command line we clear the global spreadsheet object and attempt to load in the file’s data. We assume that the files are all in Pump 2000 (.p20) format. For each successfully loaded file, we create a new PumpFilter object. We set the filter’s status property and then call the PumpSpreadsheet’s applyFilter() function (which is accessible because we made it a slot). Finally, we save the updated spreadsheet data back to the original file, and output a message to the user.

The other three scripts have a similar structure; they are included with the book’s source code.

To support scripts such as the onlyok.js script, we need to perform the following steps in the Gas Pump application:

  1. Detect the -script command-line option.

  2. Load the specified script file.

  3. Expose a PumpSpreadsheet instance to the interpreter.

  4. Expose the command-line arguments to the interpreter.

  5. Expose the FileFormat and Column enums to the interpreter.

  6. Wrap the PumpFilter class so that its member variables can be accessed from the script.

  7. Make it possible to instantiate PumpFilter objects from the script.

  8. Execute the script.

The relevant code is located in main.cpp, scripting.cpp, and scripting.h. Let’s begin with main.cpp:

int main(int argc, char *argv[])
{
    QApplication app(argc, argv);
    QStringList args = QApplication::arguments();
    if (args.count() >= 3 && args[1] == "-script") {
        runScript(args[2], args.mid(3));
        return 0;
    } else if (args.count() == 1) {
        PumpWindow window;
        window.show();
        window.resize(600, 400);
        return app.exec();
    } else {
        std::cerr << "Usage: gaspump [-script myscript.js <arguments>]"
                  << std::endl;
        return 1;
    }
}

The command-line arguments are accessible through the QApplication::arguments() function, which returns a QStringList. The first item in the list is the application’s name. If there are at least three arguments and the second argument is -script, we assume that the third argument is a script name. In this case, we call runScript() with the script’s name as its first argument and the rest of the string list as its second parameter. Once the script has been run, the application terminates immediately.

If there is just one argument, the application’s name, we create and show a PumpWindow, and start off the application’s event loop in the conventional way.

The application’s scripting support is provided by scripting.h and scripting.cpp. These files define the runScript() function, the pumpFilterConstructor() support function, and the PumpFilterPrototype supporting class. The supporting function and class are specific to the Gas Pump application, but we will still review them since they illustrate some general points about making applications scriptable.

We will review the runScript() function in several parts, since it contains several subtle details.

bool runScript(const QString &fileName, const QStringList &args)
{
    QFile file(fileName);
    if (!file.open(QIODevice::ReadOnly)) {
        std::cerr << "Error: Cannot read file " << qPrintable(fileName)
                  << ": " << qPrintable(file.errorString())
                  << std::endl;
        return false;
    }

    QTextStream in(&file);
    in.setCodec("UTF-8");
    QString script = in.readAll();
    file.close();

We start by reading the script into a QString.

    QScriptEngine interpreter;

    PumpSpreadsheet spreadsheet;
    QScriptValue qsSpreadsheet = interpreter.newQObject(&spreadsheet);
    interpreter.globalObject().setProperty("spreadsheet",
                                           qsSpreadsheet);

Once we have the script in a QString, we create a QScriptEngine and a PumpSpreadsheet instance. We then create a QScriptValue to refer to the PumpSpreadsheet instance, and set this as a global property of the interpreter, making it accessible inside scripts as the spreadsheet global variable. All the PumpSpreadsheet’s slots and properties are available through the spreadsheet variable to any script that cares to use them.

    QScriptValue qsArgs = qScriptValueFromSequence(&interpreter, args);
    interpreter.globalObject().setProperty("args", qsArgs);

The (possibly empty) args list of type QStringList that is passed to the runScript() function contains the command-line arguments the user wants to pass to the script. To make these arguments accessible to scripts, we must, as always, create a QScriptValue to represent them. To convert a sequential container such as QList<T> or QVector<T> to a QScriptValue, we can use the global qScriptValueFromSequence() function provided by the QtScript module. We make the arguments available to scripts as a global variable called args.

    QScriptValue qsMetaObject =
            interpreter.newQMetaObject(spreadsheet.metaObject());
    interpreter.globalObject().setProperty("PumpSpreadsheet",
                                           qsMetaObject);

In pumpspreadsheet.h, we defined the FileFormat and Column enums. In addition we also included a Q_ENUMS() declaration that specified these enums. It is rare to use Q_ENUMS() in general Qt programming; its main use is when we are creating custom widgets that we want to make accessible to Qt Designer. But it is also useful in a scripting context, since we can make the enums available to scripts by registering the meta-object of the class that contains them.

By adding the PumpSpreadsheet’s meta-object as the PumpSpreadsheet global variable, the FileFormat and Column enums are made accessible to scripts. Script writers can refer to enum values by typing, say, PumpSpreadsheet.Pump2000.

    PumpFilterPrototype filterProto;
    QScriptValue qsFilterProto = interpreter.newQObject(&filterProto);
    interpreter.setDefaultPrototype(qMetaTypeId<PumpFilter>(),
                                    qsFilterProto);

Because ECMAScript uses prototypes rather than classes in the C++ sense, if we want to make a custom C++ class available for scripting, we must take a rather round-about approach. In the Gas Pump example, we want to make the PumpFilter class scriptable.

One approach would be to change the class itself and have it use Qt’s meta-object system to export its data members as Qt properties. For the Gas Pump example, we have chosen to keep the original application intact and create a wrapper class, PumpFilterPrototype, that can hold and provide access to a PumpFilter, to show how it’s done.

The call to setDefaultPrototype() shown earlier tells the interpreter to use a PumpFilterPrototype instance as the implicit prototype for all PumpFilter objects. This prototype is derived from QObject and provides Qt properties for accessing the PumpFilter data members.

    QScriptValue qsFilterCtor =
            interpreter.newFunction(pumpFilterConstructor,
                                    qsFilterProto);
    interpreter.globalObject().setProperty("PumpFilter", qsFilterCtor);

We register a constructor for PumpFilter so that script writers can instantiate PumpFilter. Behind the scenes, accesses to PumpFilter instances are mediated through PumpFilterPrototype.

The preliminaries are now complete. We have read the script into a QString, and we have set up the script environment, providing two global variables, spreadsheet and args. We have also made the PumpSpreadsheet meta-object available and provided wrapped access to PumpFilter instances. Now we are ready to execute the script.

    interpreter.evaluate(script);
    if (interpreter.hasUncaughtException()) {
        std::cerr << "Uncaught exception at line "
                  << interpreter.uncaughtExceptionLineNumber() << ": "
                  << qPrintable(interpreter.uncaughtException()
                                           .toString())
                  << std::endl << "Backtrace: "
                  << qPrintable(interpreter.uncaughtExceptionBacktrace()
                                           .join(", "))
                  << std::endl;
        return false;
    }

    return true;
}

As usual, we call evaluate() to run the script. If there are syntax errors or other problems, we output suitable error information.

Now we will look at the tiny supporting function, pumpFilterConstructor(), and at the longer (but simple) supporting class, PumpFilterPrototype.

QScriptValue pumpFilterConstructor(QScriptContext * /* context */,
                                   QScriptEngine *interpreter)
{
    return interpreter->toScriptValue(PumpFilter());
}

The constructor function is invoked whenever the script creates a new object using the ECMAScript syntax new PumpFilter. The arguments passed to the constructor are accessible using the context parameter. We simply ignore them here and create a default PumpFilter object, wrapped in a QScriptValue. The toScriptValue<T>() function is a template function that converts its argument of type T to a QScriptValue. The type T (in our case, PumpFilter) must be registered using Q_DECLARE_METATYPE():

Q_DECLARE_METATYPE(PumpFilter)

Here’s the prototype class’s definition:

class PumpFilterPrototype : public QObject, public QScriptable
{
    Q_OBJECT
    Q_PROPERTY(QDate fromDate READ fromDate WRITE setFromDate)
    Q_PROPERTY(QDate toDate READ toDate WRITE setToDate)
    ...
    Q_PROPERTY(QString status READ status WRITE setStatus)

public:
    PumpFilterPrototype(QObject *parent = 0);

    void setFromDate(const QDate &date);
    QDate fromDate() const;
    void setToDate(const QDate &date);
    QDate toDate() const;
    ...
    void setStatus(const QString &status);
    QString status() const;

private:
    PumpFilter *wrappedFilter() const;
};

The prototype class is derived from both QObject and QScriptable. We have used Q_PROPERTY() for every getter/setter accessor pair. Normally, we bother using Q_PROPERTY() only to make properties available to custom widget classes that we want to integrate with Qt Designer, but they are also useful in the context of scripting. When we want to make functions available for scripting, we can make them either public slots or properties.

All the accessors are similar, so we will just show one typical example pair:

void PumpFilterPrototype::setFromDate(const QDate &date)
{
    wrappedFilter()->fromDate = date;
}

QDate PumpFilterPrototype::fromDate() const
{
    return wrappedFilter()->fromDate;
}

And here’s the wrappedFilter() private function:

PumpFilter *PumpFilterPrototype::wrappedFilter() const
{
    return qscriptvalue_cast<PumpFilter *>(thisObject());
}

The QScriptable::thisObject() function returns the this object associated with the interpreter’s currently executing function. It is returned as a QScriptValue, and we cast it to the C++/Qt type it represents, in this case a PumpFilter *. The cast will work only if we register PumpFilter * using Q_DECLARE_METATYPE():

Q_DECLARE_METATYPE(PumpFilter *)

Finally, here’s the PumpFilterPrototype constructor:

PumpFilterPrototype::PumpFilterPrototype(QObject *parent)
    : QObject(parent)
{
}

In this example, we don’t let script writers instantiate their own PumpSpreadsheet objects; instead, we provide a global singleton object, spreadsheet, that they can use. To allow script writers to instantiate PumpSpreadsheets for themselves, we would need to register a pumpSpreadsheetConstructor() function, like we did for PumpFilter.

In the Gas Pump example, it was sufficient to provide scripts with access to the application’s widgets (e.g., to PumpSpreadsheet) and to the application’s custom data classes such as PumpFilter. Although not necessary for the Gas Pump example, it is sometimes also useful to make functions in C++ available to scripts. For example, here is a simple function defined in C++ that can be made accessible to a script:

QScriptValue square(QScriptContext *context, QScriptEngine *interpreter)
{
    double x = context->argument(0).toNumber();
    return QScriptValue(interpreter, x * x);
}

The signature for this and other functions intended for script use is always

QScriptValue myFunc(QScriptContext *context, QScriptEngine *interpreter)

The function’s arguments are accessible through the QScriptContext::argument() function. The return value is a QScriptValue, and we create this with the QScriptEngine that was passed in as its first argument.

The next example is more elaborate:

QScriptValue sum(QScriptContext *context, QScriptEngine *interpreter)
{
    QScriptValue unaryFunc;
    int i = 0;

    if (context->argument(0).isFunction()) {
        unaryFunc = context->argument(0);
        i = 1;
    }

    double result = 0.0;
    while (i < context->argumentCount()) {
        QScriptValue qsArg = context->argument(i);
        if (unaryFunc.isValid()) {
            QScriptValueList qsArgList;
            qsArgList << qsArg;
            qsArg = unaryFunc.call(QScriptValue(), qsArgList);
        }
        result += qsArg.toNumber();
        ++i;
    }
    return QScriptValue(interpreter, result);
}

The sum() function can be called in two different ways. The simple way is to call it with numbers as arguments. In this case, unaryFunc will be an invalid QScriptValue, and the action performed will be simply to sum the given numbers and return the result. The subtler way is to call the function with an ECMAScript function as the first argument, followed by any number of numeric arguments. In this case, the given function is called for each number, and the sum of the results of these calls is accumulated and returned. We saw this same function written in ECMAScript in the first section of this chapter (p. 514). Using C++ rather than ECMAScript to implement low-level functionality can sometimes lead to significant performance gains.

Before we can call C++ functions from a script, we must make them available to the interpreter using newFunction() and setProperty():

QScriptValue qsSquare = interpreter.newFunction(square);
interpreter.globalObject().setProperty("square", qsSquare);

QScriptValue qsSum = interpreter.newFunction(sum);
interpreter.globalObject().setProperty("sum", qsSum);

We have made both square() and sum() available as global functions to the interpreter. Now we can use them in scripts, as the following snippet shows:

interpreter.evaluate("print(sum(1, 2, 3, 4, 5, 6));");
interpreter.evaluate("print(sum(square, 1, 2, 3, 4, 5, 6));");

This concludes our coverage of making Qt applications scriptable using the QtScript module. The module is provided with extensive documentation including a broad overview, and detailed descriptions of the classes it provides, including QScriptContext, QScriptEngine, QScriptValue, and QScriptable, all of which are worth reading.



[*] Qt 4.4 is expected to provide a qScriptConnect() function that will allow us to establish C++-to-script connections. Using this function, we could then connect the QAction’s triggered() signal directly to the qsScript’s run() function as follows:

qScriptConnect(action, SIGNAL(triggered()), qsScript, qsScript.property("run"));
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset