IdentifiantMot de passe
Loading...
Mot de passe oublié ?Je m'inscris ! (gratuit)

QtScript : utilisation des prototypes 3

Par Rémi Achard

 

Cet article présente l'utilisation de QtScript dans le but de rendre une API C++ scriptable. (### trop court et peu informatif)

       Version PDF   Version hors-ligne   Version eBooks
Viadeo Twitter Facebook Share on Google+        





1. Présentation de QtScript

Le module QtScript à été introduit dans la version 4.3.0 de Qt, au détriment (### bof. "pour remplacer" ?) QSA (Qt Script for Applications), ancienne librairie liée à Qt qui est maintenant dépréciée.
QtScript s'appuie sur le standard ECMAScript qui est langage de script standardisé, utilisé entre autre par JavaScript et ActionScript (Flash).

(### quels sont les possiblités de QtScript ? : créer des QObjects, appeler les fonctions, gérer les signaux/slots, modifier les variables membres, etc.) (### comment invoquer des scripts ? : dans QScriptEngine) Pour utiliser QtScript dans un projet, il faut ajouter la ligne suivante dans le .pro :

QT += script
Pour inclure les définitions des classes :

#include <QtScript>

2. Les prototypes

Dans le contexte QtScript, un prototype définit le comportement d'un ensemble d'objets QtScript. Les objets basés sur le même prototype sont définis comme appartenant à la même classe. (A ne pas confondre avec les classes de langages orientés objets comme le C++, ECMAScript ne contient pas de telles structures).

Pour interfacer un type C++ standard avec l'environnement QtScript, il est donc necessaire de définir un prototype qui sera appliqué à tout les objets de ce type, comme nous le verrons dans la suite, ceci se fait grâce à la méthode QScriptEngine::setDefaultPrototype(). Un prototype est en quelque sorte un patron de classe.

Définir une classe destinée à être utilisée dans l'environnement QtScript se fait en deux étapes :

  1. Créer un prototype de la classe T et l'associer par défault à tout objet de type T crée dans le script par la suite.
  2. Définir un constructeur qui initialisera l'objet T et le retournera à l'environnement de script dans un type approprié.

3. Cas d'utilisation

Voici un exemple d'utilisation des prototypes pour interfacer une API C++ avec QtScript. L'API C++ utilisée dans l'exemple se compose d'une classe, ImageTransformation qui appliquera un traitement sur toutes les images d'un dossier, dans notre cas un redimenssionnement. Cette classe sera interfacée avec QtScript à l'aide d'un prototype. Enfin un widget permettra à l'utilisateur de saisir son script puis de l'exécuter.


3-A. API C++

L'API C++ se compose d'une classe, ImageTransformation. Cette classe travail sur un repertoire et effectue des opérations sur les images s'y trouvant. Pour cet article, seul la méthode de redimenssionnement à été implémentée. On remarque qu'aucune dépendance à Qt n'est necessaire (hormis QString).

#include <QString>

class ImageTransformation
{
private:
    QString m_directory;

public:
    ImageTransformation() {}
    ImageTransformation(const QString& dir) : m_directory(dir) {}

public:
    QString getDirectory() const { return m_directory; }
    void setDirectory(const QString& dir) { m_directory = dir; }

    void rescale(const quint16 xres, const quint16 yres);
};
La méthode de redimenssionnement parcours le repertoire à l'aide d'un objet QDir. Elle effectue ensuite le redimenssionnement sur chaque image trouvées et sauvegarde ces images en écrasant les anciennes.

void ImageTransformation::rescale(const quint16 xres, const quint16 yres)
{
    QDir dir(m_directory);

    QStringList filters;
    filters << "*.jpg" << "*.jpeg" << "*.png" << "*.png";
    dir.setNameFilters(filters);

    QStringList img_files = dir.entryList();

    for (int i = 0; i < img_files.size(); ++i) {
        QImage current_img(dir.filePath(img_files[i]));
        current_img = current_img.scaled(xres, yres);
        current_img.save(dir.filePath(img_files[i]));
    }
}

3-B. Prototype


La classe ImageTransformationPrototype est responsable de l'interfacage de la classe ImageTransformation avec QtScript. QtScript utilise le MOC (Meta Object Compiler) pour interfacer le code C++ au langage de script : la declaration des types utilisés est indispensable et se fait à l'aide de la macro Q_DECLARE_METATYPE. Le type pointeur est aussi enregistré et sera utilisé dans la classe prototype ### par un objet this personnalisé ### (pas clair).

Q_DECLARE_METATYPE(ImageTransformation)
Q_DECLARE_METATYPE(ImageTransformation*)
La classe prototype doit impérativement hériter de QObject (### et avoir la macro Q__OBJECT) pour que le MOC fonctionne (### le moc fonctionnera toujours... mais il ajoutera pas ce qui est nécessaire aux signaux/slots et script). La classe QScriptable permet au prototype d'accéder à l'environnement de script, notamment aux objets en cours de manipulation à l'aide de la méthode thisObject() (### à détailler).

class ImageTransformationPrototype : public QObject, public QScriptable
{
Les propriétés Qt (déclarées à l'aide de la macro Q_PROPERTY (### rappel de la syntaxe de Q_PROPERTY) ) permettent d'acceder aux attributs de l'objet manipulé. Par exemple : objet.directory = '/home/myDirectory'; ou print(objet.directory); (### il faut faire apparaitre très clairement si tu donnes du code c++ ou du code script. par exemple ici : "Les propriétés permettent d'accéder dans le script aux variables membres des objets Qt manipulés. Par exemple, le script 'object.directory ...' permet de lire ou d'écrire dans la variable membre 'directory' définit dans le code suivant :").

Q_OBJECT
Q_PROPERTY(QString directory READ directory WRITE setDirectory)
Le prototype étant simplement un patron de classe, il n'a pas accès à l'objet dont il définit le comportement. (### patron de classe ? quel objet dont il définit le comportement ? object c++ ou script ?) Pour accéder à cet objet, il faut le recupérer depuis l'environnement de script grâce à la méthode QScriptable::thisObject(). Cette méthode retourne un objet de type QScriptValue, il faut donc effectuer un cast dans le type attendu ImageTransformation*. Ce cast est réalisé grâce à la fonction template qscriptvalue_cast. La méthode thisImgTrs agit donc comme un simulacre (### bof bof comme terme) du pointeur this.

protected:
    ImageTransformation* thisImgTrs() const { return qscriptvalue_cast<ImageTransformation*>(thisObject()); }
Les propriétés (### non : les méthodes pour accéder à la variable membre 'directory') sont implémentées, puis la méthode de redimenssionnement déclarée. A noter que les slots Qt sont par défaut appelables depuis l'environnement script. Pour rendre une méthode "scriptable", il faut ajouter la macro Q_INVOKABLE devant sa déclaration (### oui mais ton code n'utilise pas cet macro. à expliquer). La méthode thisImgTrs() est bien utilisée pour acceder à l'objet.

public:
    ImageTransformationPrototype(QObject *parent = 0) : QObject(parent) { }

    QString directory() const { return thisImgTrs()->getDirectory(); }
    void setDirectory(const QString &dir) { thisImgTrs()->setDirectory(dir); }

public slots:
    void rescale(const quint16 xres, const quint16 yres) { thisImgTrs()->rescale(xres, yres); }

};
Pour instancier des objets au sein même du script (à l'aide du mot clé new), il faut définir soi même le constructeur. En effet, QtScript ne travaille qu'avec des objets de type QScriptValue, le constructeur C++ par défault n'est donc pas valable. La fonction ImgTrans_ctor prend en paramètre le contexte dans lequel elle a été appelée ainsi que l'environnement de script en cours d'execution. Elle vérifie ensuite que le nombre d'arguments est correct puis retourne l'objet ImageTransformation nouvellement crée sous forme de QScriptValue grâce à la méthode toScriptValue de la classe QScriptEngine. (### comment le lien est fait entre 'new ImageTransformation()' dans le script et cette fonction ? EDIT : expliqué après)

QScriptValue ImgTrans_ctor(QScriptContext *context, QScriptEngine *engine)
{
    if (context->argumentCount() == 1)
    {
        QString dir = context->argument(0).toString();
        return engine->toScriptValue(ImageTransformation(dir)); (### création unamed object temporaire, passé par référence = appel contructeur de copie)
    }
    else {
        return context->throwError(QScriptContext::SyntaxError, "ImageTransformation constructor requires 1 parameters (directory path)!");
    }
}
La dernière étape consiste à lier le prototype que nous avons crée à tous objets de type ImageTransformation qui seront crée dans le script. La méthode setDefaultPrototype prend en paramètre le type à lier et le prototype correspondant. Ne pas oublier de déclarer la fonction "constructeur" au sein de l'environnement script en créeant une propriété de type fonction à l'aide de la méthode newFunction(ptr_func).

void ImgTrans_register(QScriptEngine *engine)
{
    ImageTransformationPrototype* img_trans_proto = new ImageTransformationPrototype();

    engine->setDefaultPrototype(qMetaTypeId<ImageTransformation>(), engine->newQObject(img_trans_proto));
    engine->setDefaultPrototype(qMetaTypeId<ImageTransformation*>(), engine->newQObject(img_trans_proto));
    
    engine->globalObject().setProperty("ImageTransformation", engine->newFunction(ImgTrans_ctor));
}

3-C. Application

Pour tester le bon fonctionnement de notre API sous QtScript, un Widget personnalisé est crée. Ce Widget contient un éditeur de texte permettant de définir un script et un bouton pour lancer l'exécution du script. L'interprétation du script est réalisée par l'attribut m_engine de type QScriptEngine (### ce n'est pas un attribut, c'est un objet de type QScriptEngine).

class ScriptWidget : public QWidget
{
    Q_OBJECT

private:
    QScriptEngine m_engine;

    QVBoxLayout m_layout;
    QTextEdit m_text_edit;
    QPushButton m_launch_button;

public:
    ScriptWidget(QWidget *parent = 0);

public slots:
    void executeScript();
};
Le constructeur met en place l'environnement de script en appelant la fonction ImgTrans_register() détaillée plus haut (### non, l'envirronement d'exécution du script est initialisé lors de la création de l'objet m_engine, la fonction ImgTrans_register permet de créer un type ImageTransformation dans le script et l'associer avec le type ImageTransformationPrototype dans le code c++). L'interface graphique est ensuite mise en place.

ScriptWidget::ScriptWidget(QWidget *parent)
    : QWidget(parent)
{
    // Script engine setup
    ImgTrans_register(&m_engine);

    // Gui setup
    m_launch_button.setText("Launch script");

    this->setLayout(&m_layout);
    m_layout.addWidget(&m_text_edit);
    m_layout.addWidget(&m_launch_button);

    QObject::connect(&m_launch_button, SIGNAL(clicked()), this, SLOT(executeScript()));
}
Le slot executeScript() se charge de l'execution du script.

void ScriptWidget::executeScript()
{
    m_engine.evaluate(m_text_edit.toPlainText());
}
La fonction main() se charge simplement de créer le widget et de l'afficher :

int main(int argc, char *argv[])
{
    QApplication a(argc, argv);

    ScriptWidget script_widget;
    script_widget.show();

    return a.exec();
}
Le script suivant redimenssionnera toutes les images présentes dans le repertoire Repertoire en résolution 100 x 100 pixels.


4. Le debuggeur QEngineScriptDebugger

Qt met à disposition du developpeur un outil de débogage pour QtScript : QScriptEngineDebugger. Cette classe implémente un debugger de script similaire au debugger de QtCreator (fonction de pas à pas, breakpoints,...).
La classe QScriptEngineDebugger se situe dans le module scripttools, il faut donc rajouter dans le .pro de tout projet l'utilisant :

QT += scripttools
La définition de la classe se fait avec l'include suivant :

#include <QScriptEngineDebugger>
L'utilisation du debugger est très simple. Il faut dans un premier temps préciser au debugger le QScriptEngine utilisé pour exécuter le script avec la méthode attachTo(). De cette façon, tout script executé dans le QScriptEngine grâce à la méthode evaluate() apparaitra dans le debugger.

QScriptEngine engine;
QString script = "...";

QScriptEngineDebugger debugger;
debugger.attachTo(&engine);

engine.evaluate(script);
Pour afficher le debugger, il faut appeler la méthode standardWindow() qui renvoie un pointeur sur un objet de type QMainWindow qui contient la fenêtre du debugger. Ensuite afficher cette fenêtre avec la méthode show().

QMainWindow *window = debugger.standardWindow();
window->show();
Il est possible de placer un point d'arrêt avant l'execution de la première ligne du script de façon a pouvoir procéder à une execution pas à pas. Pour cela il faut déclencher une interruption avant l'appel à la méthode evaluate() de la façon suivante :

debugger.action(QScriptEngineDebugger::InterruptAction)->trigger();
Voici l'interface du debugger. A noter qu'il possible d'exécuter des commandes QtScript interactivement dans la Console en bas de la fenêtre. Les affichages apparaissent dans l'onglet DebugOutput.


5. Reférences




               Version PDF   Version hors-ligne   Version eBooks

Valid XHTML 1.0 TransitionalValid CSS!