QSA - Qt Script for Applications

Jiří Kosina


1. Overview

QSA (Qt Script for Applications, http://www.trolltech.com/products/qsa/ je scripting toolkit pro aplikace, založený na Qt. Jeho cílem je umožnit programátorům psát Qt/C++aplikace, které si budou uživatele moci snadno upravovat/skriptovat.

QSA je interpretovaný skriptovací jazyk, založený na standardu EMCAScript (Javascript a Microsoft JScript jsou také založeny na tomto standardu).

QSA umožňuje vývojářům učinit některé z objektů v aplikaci (které jsou zároveň podtřidy QObjectu) zviditelnit pro skritpovací engine a tím umožnit ovlivňování jejich chování uživatelem. Toto zviditelnění se děje prostým předáním tohoto objektu funkci, která je součastí QSA knihovny.

2. Komponenty

2.1 QSA lib
Hlavní knihovna která umožňuje vytvářet skriptovatelné aplikace založené na toolkitu Qt.

2.2 Qt Script
Samotný skritpovací jazyk QSA.

2.3 QSA workbench
Vývojové prostředí, které autor programu může distribuovat společně se svým programem a umožnit tak uživatelům snadnější výtváření skriptů v QSA pro aplikaci.

2.4 Input dialog framework
Vysokoúrovňové API pro ovládání GUI, které dovoluje uživatelům píšícím skripty pro aplikaci skriptovat dialogy které přijímají vstup od uživatele aplikace.

3. Licence a platformy

QSA je poskytován ve dvou licenčních variantách, jednak pod komerční licencí firmy Trolltech pro Linux/Unix, Windows a Mac OS. Ovšem pro vývoj GPL software je také šířena verze pod GNU GPL licencí, pro platformy Linux/Unix a Mac OS (pro windows zdá se je QSA šířeno pouze pod komerční licencí)

4. Jak udělat Qt/C++ aplikaci skriptovatelnou

4.1 Overview
Tato kapitola demonstruje část postupu jak vytvořit Qt/C++ aplikaci, která používá QSA k tomu aby si uživatel mohl aplikaci přizpůsobovat a skriptovat. Zabývá se příkladem který lze nalézt přimo v distribuci QSA - jednoduchou rozšiřitelnou spreadsheetovou aplikací. Rozšiřitelnou tím způsobem, že aplikace bude poskytovat interface k jednotlivým sheetům. Kód skriptu pak může k jednotlivým sheetům přistupovat a může si napsat Qt Script, který vytváří dialogy pro komunikaci s uživatelem a který může manipulovat s daty v jednotlivých sheetech. Zájemci kompletní kód spreadsheetu mohou najít v adresáři examples/spreadsheet v distribuci QSA, která je volně přistupná.

4.2 Předat objekty aplikace skriptovacímu engine
Aby byla funkcionalita aplikace zpístupněna skriptům, musí aplikace poskytovat podtřidy QObject, které funkcionalitu aplikace implementují. Předání tohoto objektu skriptovacímu engine (pomocí QSInterpreter::addTransientObejct()) způsobí, že tento objekt a všechny jeho signály, sloty, properties a potomci jsou zpřistupněny skriptům.
Typicky se ovśem nepředává celý objekt tak jak ho používáme v aplikaci, ale pouze potomek, který "vystavuje" jen ty sloty které si z objektu programátor přeje zviditelnit pro scripting engine, a požadavky na tyto jsou jen forwardovány původnímu objektu.

V příkladu spreadsheetu přidáme interface pro objekty reprezentující sheety. Tyto interfacové objekty implementují sloty a properties potřebné pro zjišťování a nastavování rozsahů výběru, čtení a ukládání dat do buněk, atd.

4.3 Skriptovací projekt
QSA vždy pracuje s jedním scripting projektem, který obsahuje všechny formuláře a soubory ve kterých jsou implementovány funkce a třidy které používá. Projekt je reprezentován objektem QSProject, který ma implementováu metodu 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. S projektem je možné pracovat ve dvou režimech - buď nechat QSA ať se "o všechno postará" (ukládání a nahrávání projektu), nebo si může programátor příslušné funkce volat sám ručně. Při automatickém režimu se projekt ukládá komprimovaně na disk do souboru, v druhém režimu lze volat ukládání libovolně, protože programátor si sám projekt nahrává.
    void SpreadSheet::init()
    {
        currentSheet = sheet1;

        project = new QSProject( this, "spreadsheet_project" );
        interpreter = project->interpreter();
        QSInputDialogFactory *fac = new QSInputDialogFactory; /* toto je potreba kvuli input dialog framework - aby si skript mohl vytvaret UI */
        interpreter->addObjectFactory( fac );

        project->addObject( new SheetInterface( sheet1, this, "sheet1" ) );
        project->QaddObject( new SheetInterface( sheet2, this, "sheet2" ) );
        setupSheet( sheet1 );
        setupSheet( sheet2 );

        project->load( "spreadsheet.qsa" );
        connect( project, SIGNAL( QprojectEvaluated() ),
                 project, SLOT( Qsave() ) );
    }

4.3.1 Umožnit uživateli vytvářet a editovat skripty
Někdy může postačovat poskytnout uživateli pro vytváření skriptů pouhý editor widget, ve kterém může uživatel psát kód skriptu (napřiklad pokud si programátor přeje mít editor kódu přimo embedovaný do aplikace, bez otevírání dalšího okna (což vyžaduje QSA workbench)).
V ostatních případech může být vhodné umožnit uživatelům editaci QSA skriptu v komfortnějším IDE s možností pohodlně přidávat/odebírat skripty, použivat doplňování a hinty v editoru, skritptovat pohodlně GUI a podobně. K tomu slouží QSA workbench, který může být zahrnut přimo do aplikace.

Existují dva způsoby jak uživatel může použít QSA Workbench:
  1. Otevřít QSA Workbenchvytvořit/editovat skript.

  2. Definovat makro.

Existují různé způsoby jak nabízet možnost skriptování koncovým uživatelům - vhodnost použití závisí na typu aplikace (z menu spustit QSA Workbench + editovatelný combobox s globálními funkcemi. Když uživatel vloží jméno funkce které neexistuje, může vytvořit novou funkci tohoto jména, jinak se spustí QSA workbench pro editaci funkce + tlačítko RUN na spuštění funkce)

4.3.2 Makra
Makrem rozumíme samostatnou globální funkci. K vytvoření skriptu v globálním kontextu se používá funkce QSProject::createScript(). Do tohoto skriptu pak lze pomocí QScript::addFunction() a pomocí editoru pak tuto funkci editovat.
    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();
    }



4.3.3 QSA workbench
Následující jednoduchý přiklad ukazuje jak se v spreadsheetovem přikladu volá 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
    }
4.3.4 Přidávání a editace maker
V spreadsheetovém QSA examplu je ukázáno jak umožnit uživateli přidávat makra do toolbaru a menu, která mohou asociovat s funkcí skriptu, která bude zavolána při aktivaci akce (kliknutí na přiadnou ikonu, apod.). K přidávání maker je poskytnut dialog skrz který uživatel může vybrat existující globální funkci nebo přidat novou. Při přidání nové funkce se vytvoří nová akce a ikona, společně s volbou v menu a tlačítkem v toolbaru.

Následující funkce se používá v macro-dialogu k incicializaci comboboxu se seznamem funkcí
    void AddScriptDialog::init()
    {
        // List all global functions of the project
        QSProject *project = ( (SpreadSheet*) parent() )->project;
        comboFunction->insertStringList( project->interpreter()->functions() );
    }

Pokud v tomto dialogu skriptující uživatel odsouhlasí jméno funkce, spustí se následující kód:
    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();
    }

Na konci je vyslán signal newScript(). Tato funkce vytvoří akci která bude spojena s makrem a přidá položku do menu a tlačítko do toolbaru. Dále je k signálu activated() připojen k runScript(). Aby bylo jasné kterou funkci bude toto makro (akce) volat, je akce a její asociovaná funkce vložena do 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() ) );
    }
Pokud nechceme, aby uživatel musel po každém spuštění skriptu v QSA Workbench klikat na "Run", je možné použít následující přístup. V předchozím kódu došlo k asociaci makra (globální funkce) a akce. Když uživatel vyvolá akci, dojde ke spuštění runScript() a je nutné zjistit, která funkce se má pro danou akci spustit (a pak ji pomocí QSInterpreter::call() spustit). Každý slot v Qt může zavolat sender() (který je implementován v hlavním Qt objektu QObject) aby zjistil jaká action vyvolala slot. Můžeme přetypovat sender() na QAction (v tomto případě víme, že je to QAction) a pak tento pointer vyhledat v mapě scripts.
    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>() );
    }
5. Zpřístupněnní C++ objektů skriptům napsaným v QSA

Zpřístupnění C++ tříd a objektů QSA skriptům není triviální záležitost, protože skriptovací jazyk je "dynamičtější" než C++ a vyžaduje provádět s objekty operace které standardní C++ neumožňuje (ptát se na informace o jménu funkce, prototypu, properties, atd. - v runtime).

Této funkcionality lze dosáhnout rozšířením syntaxe C++ - Meta Object System Qtčka poskytuje dosatečnou funkcionalitu - dovoluje psát v rozšířené C++ syntaxi, ale konvertuje ji do standardního C++ pomocí překladače moc(Meta Object Compiler). Třídy které chtějí využívat výhod meta object systému musí být bud potomci QObjectu nebo používat Q_OBJECT makro (moc potom z takového zdrojáku udělá standardní C++ - původní motivace pro Meta Object System byla, že C++ je nepohiodlné pro programování GUI, protože potřebuje v runtime zjištovat informace, které standardní C++ v runtime zjišťovat nedokáže)

5.1 Zpřístupnění C++ member funkcí
The meta object system makes information about slots dynamically available at runtime. This means that for QObject derived classes, only the slots are automatically made available to scripts. This is very convenient, because in practice we normally only want to make specially chosen functions available to scripters. Meta Object System dynamicky zpřistupňuje informace o slotech v runtime, což znamená že pro potomky QObject jsou (pouze) sloty zpřístupňeny pro skripty. Což je výhodné, protože typicky chceme skriptům zpřśitupnit skriptům jen některé metody třidy.
class MyObject : public QObject
{
    Q_OBJECT

public:
    MyObject( ... );

    void aNonScriptableFunction();

public slots: // these functions (slots) will be available in Qt Script
    void calculate( ... );
    void setEnabled( bool enabled );
    bool isEnabled() const;

private:
   ....

};
V tomto příkladu budou poslední tři metody viditelné pro QSA skript, protože jsou definované jako sloty, ale metoda aNonScriptableFunction() nikoliv.

5.2 Zpřístupnění properties C++ třidy
V předchozím přikladu pokud bychom chtěli umožnit skriptu změnit property Qt Scriptu, bylo by to potŕeba udělat nějak takto:
var obj = new MyObject;
obj.setEnabled( true );
debug( "obj is enabled: " + obj.isEnabled() );
Ovšem pokud bychom toto chtěli nechat dělat programátora skriptu "přímo", tj:
var obj = new MyObject;
obj.enabled = true;
debug( "obj is enabled: " + obj.enabled );
musíme zajistit (opět prostředky Meta Object Systemu), aby Qt skript měl přistup k property enabled. Aby bylo toto možné, je potřeba definovat properties v potomku třidy QObject způsobem ukázaným v následujícím příkladu - deklarovat property enabled typu bool, který bude používat funkci setEnabled(bool) jako setter a isEnabled() jako getter.
class MyObject : public QObject
{
    Q_OBJECT
    // define the enabled property
    Q_PROPERTY( bool enabled WRITE setEnabled READ isEnabled )

public:
    MyObject( ... );

    void aNonScriptableFunction();

public slots: // these functions (slots) will be available in Qt Script
    void calculate( ... );
    void setEnabled( bool enabled );
    bool isEnabled() const;

private:
   ....

};
5.3 Zpřístupnění properties C++ třidy
V objektovém modelu Qt se používají signály jako mechanismus pro notifikace mezi jednotlivými QObjecty. To znamená že jeden objekt může připojit signal ke slotu jiného objektu a pokaždé když je signál vyvolán (pomocí emit ) je zavolán příslušný napojený slot. Toto propojení se navazuje pomocí QObject::connect() - to už jsme viděli výše u příkladu v kapitole 4.3. Tento mechanismus je také dostupný pro programátory Qt skriptů a to stejným způsobem jako v Qt:
class MyObject : public QObject
{
    Q_OBJECT
    // define the enabled property
    Q_PROPERTY( bool enabled WRITE setEnabled READ isEnabled )

public:
    MyObject( ... );

    void aNonScriptableFunction();

public slots: // these functions (slots) will be available in Qt Script
    void calculate( ... );
    void setEnabled( bool enabled );
    bool isEnabled() const;

signals: // the signals
    void enabledChanged( bool newState );

private:
   ....

};
Programátor skriptu pak může udělat
function enabledChangedHandler( b )
{
    debug( "state changed to: " + b );
}

function init()
{
    var obj = new MyObject;
    // connect a script function to the signal
    connect( obj, "enabledChanged(bool)", this, "enabledChangedHandler" );
    obj.enabled = true;
    debug( "obj is enabled: " + obj.enabled );
}

čímž dosáhl toho že kdykoliv je vyvolána metoda enabledChanged() objektu MyObject tak je zavolána funkce enabledChangedHander() - naprosto stejným mechanismem jako by tomu bylo v Qt.

6. Příkald vytváření skriptu v aplikaci spreadhseet
6.1 Vytvoření nového makra

V tomto příkladu bude z rychlíku demonstrováno jak pomocí QSA vložit do naší spreadsheetové aplikace, o které se mluvilo v předchozích kapitolách a která je jako example distribuována s QSA, doskriptovat konvertor měn, který bude pracovat s údaji ve sloupcích našeho sheetu.


V aplikaci je (výše popsaným způsobem) možnost v menu "Create a new macro". Po vytvoření makra se na toolbaru objeví ikonka, která bude fungovat jako shortcut pro spuštění makra (viz 4.3.4). Po vyplnění jména funkce a případně vybrání ikony která se má zobrazovat na toolbaru se objeví následující dialog (který jsme naprogramovali v kapitole 4.3.4)



Poté se spustí QSA workbench ve kterém implementujeme kód samotného makra.
6.2 Implementace makra a dialogu V této kapitole bude demonstrována implementace makra convert-to-euro, které bude spouštěno dialogem ukázaným na začátku této kapitoly - v tomto dialogu bude uživatel mít možnost 1) vložit rozsah buněk ze kterých se mají data číst 2) kam se mají zapisovat výsledky 3) ze které měny se má konvertovat. Nastavíme defaulty (pokud je v sheetu něco označeno, tak místo na 1 inicializujeme na tento výběr - aplikace zpřístupňuje skriptům properties svých třid. V Qt Scriptu existuje několik globálních proměnných - jedna z nich je Application a její objekty jsou objekty které aplikace zpřístupňuje skriptu - v našem případě sheet1.
        
	var inputCol = 1;
        var startRow = 1;
        var endRow = 1;
        var outputCol = 1;
        if ( Application.sheet1.numSelections > 0 ) {
            var sel = Application.sheet1.selection( 0 );
            inputCol = sel.x + 1;
            outputCol = inputCol + 1;
            startRow = sel.y + 1;
            endRow = sel.bottom + 1;
        }

Vytvoříme dialogbox (bez vysvětlování, je to zřejmé i bez znalosti pŕesné syntaxe):
        d = new Dialog;
        d.caption = "Settings for Euro Converter";
        d.okButtonText = "Calculate";
        d.cancelButtonText = "Cancel";
        var g = new GroupBox;
        g.title = "Conversion Range:";
        d.add( g );
        var spinColumn = new SpinBox;
        spinColumn.label = "Column:";
        spinColumn.minimum = 1;
        spinColumn.maximum = 100;
        spinColumn.value = inputCol;
        g.add( spinColumn );

        var spinStartRow = new SpinBox;
        spinStartRow.label = "Start at Row:";
        spinStartRow.minimum = 1;
        spinStartRow.maximum = 100;
        spinStartRow.value = startRow;
        g.add( spinStartRow );

        var spinEndRow = new SpinBox;
        spinEndRow.label = "End at Row:";
        spinEndRow.minimum = 1;
        spinEndRow.maximum = 100;
        spinEndRow.value = endRow;
        g.add( spinEndRow );

        var spinOutput = new SpinBox;
        spinOutput.label = "Output Column:";
        spinOutput.minimum = 1;
        spinOutput.maximum = 100;
        spinOutput.value = outputCol;
        g.add( spinOutput );
        d.newColumn();
        var g = new GroupBox;
        g.title = "Conversion Range:";
        d.add( g );
        var radioUSD = new RadioButton;
        radioUSD.text = "USD";
        radioUSD.checked = true;
        g.add(radioUSD);
        var radioYEN = new RadioButton;
        radioYEN.text = "YEN";
        g.add(radioYEN);
        var radioNOK = new RadioButton;
        radioNOK.text = "NOK";
        g.add(radioNOK);
Pak v QSA Workbenchi pomocí tlačítka Call function můžeme definovat kód který se má vykonat při stisknutí tlačítka OK v tomto dialogu.

První blok kódu zjistí jaký radio button pro kterou měnu uživatel vybral a nastaví odpovídajícím způsobem dělitele
        var divisor;

        if( radioUSD.checked )
            divisor = 0.930492;
        else if( radioYEN.checked )
            divisor = 0.007677;
        else if ( radioNOK.checked )
            divisor = 0.133828;
Další blok kódu inicializuje proměnné ohraničující buňky ve kterých jsou údaje určené k převodu
        inputCol = spinColumn.value - 1;
        outputCol = spinOutput.value - 1;
        startRow = spinStartRow.value - 1;
        endRow = spinEndRow.value - 1;
Pokud je rozash neplatný, skript ohlásí chybu
        if ( endRow < startRow ) {
            MessageBox.critical( "Cannot calculate!", "Error" );
            return;
        }
Poslední blok provede aktuální výpočet
        for ( var i = startRow; i <= endRow; ++i ) {
            var input = Application.sheet1.text( i, inputCol );
            Application.sheet1.setText( i, outputCol,
                                        Math.round( input / divisor * 100 ) / 100 );
        }
A pak už lze v aplikaci vyplnit do buněk čísla, kliknout na ikonku našeho skriptu který jsme právě vytvořili a nechat hodnoty převést do jiné měny. Tuto konverzi v původní spreadsheetové aplikaci udělal kompletně skript, který si koncový uživatel do aplikace naprogramoval.

Možnosti tohoto skriptovacího jazyka jsou daleko bohatší, jak lze zjistit letmým pohledem do dokumentace.

7. Reference

[1] Společnost Trolltech, autor. http://www.trolltech.com/
[2] Download free verze http://www.trolltech.com/download/qsa.html
[3] Dokumentace ke QSA http://doc.trolltech.com/qsa/
[4] Dokumentace ke QT http://doc.trolltech.com/