QtScript

更新: 2011/11/08

Note

本ページは業務で調べた内容をまとめたページです。 株式会社ディー・エヌ・エーの提供でお送りしております。

QtにはQtScriptなるものがついてきます。

  • ECMA-262標準に準拠。つまり、DOMのないJavaScript。
  • 現行バージョンはそこそこ高速らしい(現在は JavaScriptCoreを使用している とか、 今V8を調べているよ とか)
  • Qt用のコンポーネントになっていて、簡単に作れる
  • C++から、JavaScriptの名前空間にオブジェクトやら関数を簡単に追加できる
  • C++から、JavaScriptの名前空間にアクセスしてオブジェクトを取得できる
  • Qtのアプリからデバッガのウインドウを開いて、QtScriptのエンジンにアタッチできる
  • Qtのアプリからエディタを開ける(まだ試してない)

せっかくなので、いろいろQtScriptで遊んでみます。

QtScriptをC++から使う方法いろいろ

Note

4.7.4のMacで検証しています。

QtScriptエンジンはQtのクラスです。C++からいろいろいじれるようになっています。

QtScriptを使う

ビルドするためには、プロジェクトのファイルにQtScript関連のライブラリをリンクするように追記する必要があります。

QT +=  \
    script \
    scripttools

前者の script がQtScriptです。後者の scripttools はQtScriptのデバッガを使いたい人向けです。 main.cpp あたりにでも、次のように #include 文を追加します。デバッガは4.0.5以降でしか使えないので、プリプロセッサで分けています。

#include <QtScript/QScriptEngine>
#include <QtScript/QScriptValue>
#ifndef QT_NO_SCRIPTTOOLS
#include <QtScriptTools/QScriptEngineDebugger>
#endif

あとはつぎのような感じで使えます。

QScriptEngine engine;
QScriptValue ret = engine.evaluate("return Math.pow(2, 10)");
qDebug() << ret.toInteger();

デバッガを起動する

3行でアタッチできます。

QScriptEngineDebugger* debugger = new QScriptEngineDebugger();
debugger->attachTo(&engine);
debugger->action(QScriptEngineDebugger::InterruptAction)->trigger();

ファイルに保存してあるスクリプトを読み込む

QtのファイルAPIを使って読みこめば、外部に保存してあるファイルを読み込むことも可能です。

QString fileName = "ファイルパス";
QFile scriptFile(fileName);
scriptFile.open(QIODevice::ReadOnly);
QTextStream stream(&scriptFile);
QString contents = stream.readAll();
scriptFile.close();

engine.evaluate(contents, fileName);

JavaScriptのソースファイルをQtのプログラムと一緒に配布したい場合には、プロジェクトファイルに配布したいファイルとして登録します。

folder_01.source = js
folder_01.target = .
DEPLOYMENTFOLDERS = folder_01

MacOSXの場合には、 .app ファイルの中の Contents/Resources/js の中にコピーされます。他の環境は試していないですが、他のサンプルを見るかぎり、Windowsの場合は実行ファイルと同じパスに、Unix系OSの場合は、実行ファイルが /usr/local/bin にあれば、 /usr/local/share/アプリ名 以下にあることを想定すれば良いみたいです。

QString adjustPath(const QString &path)
{
    #ifdef Q_OS_UNIX
    #ifdef Q_OS_MAC
    if (!QDir::isAbsolutePath(path))
        return QCoreApplication::applicationDirPath()
               + QLatin1String("/../Resources/") + path;
    #else
    const QString pathInShareDir = QCoreApplication::applicationDirPath()
          + QLatin1String("/../share/")
          + QFileInfo(QCoreApplication::applicationFilePath()).fileName()
          + QLatin1Char('/') + path;
    if (QFileInfo(pathInShareDir).exists())
        return pathInShareDir;
    #endif
    #endif
    return path;
}

この関数を通せば、アプリケーションと一緒に配布するJavaScriptのファイルのパスを取得することができます。例えば、ファイル名が main.js なら

QString fileName = adjustPath("main.js");

でOKです。

関数やオブジェクトを追加する

console.log を追加してみます。JavaScriptの名前空間に要素を登録するのに、よく使うメソッドは次の通りです:

  • QScriptEngine::newFunction(): 関数作成
  • QScriptEngine::newObject(): オブジェクト作成
  • QScriptEngine::globalObject(): グローバル名前空間の取得
  • QScriptValue::property(): プロパティ取得
  • QScriptValue::setProperty(): プロパティ設定

まず、JavaScript側から呼び出される関数をC++で作ります。

static QScriptValue consoleLogForJS(QScriptContext* context, QScriptEngine* engine)
{
    QStringList list;
    for(int i=0; i<context->argumentCount(); ++i)
    {
        QScriptValue param(context->argument(i));
        list.append(param.toString());
    }
    qDebug() << list.join(" ");
    return engine->undefinedValue();
}

1つめのパラメータの context を使うと、引数情報を得ることができます。文字列にしてつなげて qDebug に投げています。

登録は QScriptEngine オブジェクトのメソッドをいくつか呼べばできます。

QScriptEngine engine;
QScriptValue globalObject = engine.globalObject();
QScriptValue console = engine.newObject();
globalObject.setProperty("console", console);
QScriptValue consoleLog = engine.newFunction(consoleLogForJS);
console.setProperty("log", consoleLog);

ここではまず、 console という名前で空のオブジェクトを登録し、 log という名前で先程作った関数を登録しています。ここでは紹介していませんが、QtScriptでは QObject を継承したオブジェクトを簡単に登録できるようになっていたりします。

CommonJSのrequire()を使えるようにする

1ファイルで全部のコードを書ききるのは難しいです。サーバサイドのJavaScriptの共通規格のCommonJSで定義されている、 require() を定義して、他のファイルを読み込めるようにします。

基本戦略としては:

  • require 関数をグローバルに追加。 require() には paths という配列があり、読み込み可能なパスのリストを保持(規格どおり)
  • $MODULES というグローバル変数に読み込んだモジュール一覧を入れておくようにする(実装上の都合)
  • 子供のスクリプトを読み込む時に、 exports というオブジェクトを追加して、処理後にこのオブジェクトを $MODULES に格納する

まず、 require の元になる関数を作ります。ちょっと長いですが。 paths の配列を取得して、先頭にカレントのディレクトリを追加して、ループしながらファイルを見つけて読み込んで処理をしています。

property の返り値は QScriptValue という型のオブジェクトですが、これはスクリプト側のオブジェクトそのものではなく、値のコピーです。そのため、 setProperty をしない限りはJavaScript側の値は変わりません。そのため、 paths を変換した配列の先頭に push_front してしまっても問題はありません。

static QScriptValue requireForJS(QScriptContext* context, QScriptEngine* engine)
{
    QString requiredPath;
    bool found = false;
    QString param = context->argument(0).toString();
    param.push_back(".js");
    QStringList paths = engine->globalObject().property("require").property("paths").toVariant().toStringList();

    QFileInfo requiredFileInfo(param);
    QScriptContextInfo contextInfo(context->parentContext());
    QString parentFileName(contextInfo.fileName());
    paths.push_front(QFileInfo(parentFileName).dir().path());

    foreach(QString includePath, paths)
    {
        qDebug() << includePath;
        QFileInfo includePathInfo(includePath);
        if (includePathInfo.exists())
        {
            QDir includePathDir(includePath);
            includePathDir.cd(requiredFileInfo.dir().path());
            requiredPath = QDir::cleanPath(includePathDir.absoluteFilePath(requiredFileInfo.fileName()));

            if(QFileInfo(requiredPath).exists())
            {
                qDebug() << requiredPath;
                found = true;
                break;
            }
        }
    }
    if (!found)
    {
        qDebug() << "require() file not found: " << param;
        return engine->undefinedValue();
    }
    QScriptValue modules = engine->globalObject().property("$MODULES");
    QScriptValue existedModule = modules.property(requiredPath);
    if (existedModule.isValid())
    {
        return existedModule;
    }
    else
        QFile requireFile(requiredPath);
        requireFile.open(QIODevice::ReadOnly);

        QScriptContext* newContext = engine->pushContext();
        QScriptValue exportsObject = engine->newObject();
        newContext->activationObject().setProperty("exports", exportsObject);
        engine->evaluate(requireFile.readAll(), requiredPath);
        exportsObject = newContext->activationObject().property("exports");
        engine->popContext();
        requireFile.close();
        modules.setProperty(requiredPath, exportsObject);
        return exportsObject;
    }
}

できた関数をグローバル関数として登録します。 paths に探索パスを追加すると、そこも探索するようになります。

この処理はJavaScriptの配列の操作をする参考にもなると思います。

QScriptEngine engine;
QScriptValue modules = engine.newObject();
globalObject.setProperty("$MODULES", modules);

ScriptValue require = engine.newFunction(requireForJS);
gobalObject.setProperty("require", require);
ScriptValue paths = engine->newArray();
QScriptValue push = paths.property("push");
push.call(paths, QScriptValueList() << QScriptValue("読み込み元で使いたいパス"));
require.setProperty("paths", paths);

QtScriptからQtのクラスを使えるようにする

Note

4.7.4のMacで検証しています。

素のQtScriptは本当に素のECMAエンジンです。ECMAスクリプトの組み込みのオブジェクト(Array, Math, RegExpなど)以外にはQt固有のメソッドなどは一切ありません。つまり、Qtのフォームをロードして表示したり、というのを素のQtScriptエンジンだけで行うことはできません。QtScriptはC++で拡張できるのですが、Qtのオブジェクトを使うには、外部から登録してやる必要があります。

自分で1つずつやっていってもいいのですが、QtScript Binding Generatorを使えば、Webで検索すると、古いQtLabのサイトやら、Google Codeのページが引っかかりますが、最新は GioriousのQtLabのページ のようです。

僕はMacOSXを使っているので、最新(2011/11現在)の4.7.4のQtをインストールしたら、ホーム直下の QtSDK フォルダにインストールされました。このフォルダにあるデスクトップ版のライブラリを対象にバインディングを作ってみます。作業は このページ を参考にしました。付属READMEの説明だけでは無理で、環境変数を定義しないとうまくいきません。リンク先のページではWindowsで実施した結果が乗っていますが、同じやり方でMacでもいけました。

まずは環境変数を定義します。

$ export QTDIR=~/QtSDK/Desktop/Qt/474/gcc/
$ export PATH=$PATH:$QTDIR/bin

masterブランチのファイルを取ってきて展開します。 このページ の右側の Download master as tar.gz のリンクから取ってきてもいいし、gitを使ってもいいと思います。僕は .tar.gz を落としてきました。展開したら、カレントディレクトリに qt-labs-qtscriptgenerator というフォルダができました。

$ cd qt-labs-qtscriptgenerator/generator
$ qmake
$ make
(しばらく待つ)
$ ./generator --include-paths=~/QtSDK/Desktop/Qt/474/gcc/include
Please wait while source files are being generated...
(警告がいくつか出る、しばらく待つ)
Classes in typesystem: 639
Generated:
- classes...: 609 (607)
- header....: 410 (408)
- impl......: 410 (408)
- modules...: 22 (21)
- pri.......: 11 (11)

Done, 6 warnings (1220 known issues)

classesのところが7とか出ていると失敗です。成功するとこのぐらいの数値が最後に出ます。

$ cd ../qtbindings/
$ qmake
$ make
(しばらく待つ)

Qtのクラスが使えるJavaScriptインタプリタもテスト用にできるので、これを使うと簡単にうまくいったか確認できます。引き続き、同じ qtbindings フォルダにいるとします。

$ qs_eval/qs_eval ../examples/AnalogClock.js
../_images/qtanalogclock.png

このウインドウが表示されればビルドが成功しています。おめでとうございます。

QtScriptのエンジンにQt用のモジュールを読みこませる

QtScriptでメニューを登録する

QtScriptからQWebViewのHTMLを表示させる

QWebViewのJavaScriptと連携する

Note

参考: Lively for Qt: http://lively.cs.tut.fi/qt/installation.html

QtScript情報

Navi

Table Of Contents

Latest Photos

www.flickr.com
This is a Flickr badge showing public photos and videos from shibukawa.yoshiki. Make your own badge here.

Blog entries

RSS表示パーツ
無料 無料 無料

Previous topic

QtScript

Next topic

JavaScriptで全角/半角判定

This Page