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:
Otevřít QSA Workbenchvytvořit/editovat
skript.
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/