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関連のライブラリをリンクするように追記する必要があります。
.. code-block:: make
QT += \
script \
scripttools
前者の ``script`` がQtScriptです。後者の ``scripttools`` はQtScriptのデバッガを使いたい人向けです。 :file:`main.cpp` あたりにでも、次のように ``#include`` 文を追加します。デバッガは4.0.5以降でしか使えないので、プリプロセッサで分けています。
.. code-block:: cpp
#include
#include
#ifndef QT_NO_SCRIPTTOOLS
#include
#endif
あとはつぎのような感じで使えます。
.. code-block:: cpp
QScriptEngine engine;
QScriptValue ret = engine.evaluate("return Math.pow(2, 10)");
qDebug() << ret.toInteger();
デバッガを起動する
------------------
3行でアタッチできます。
.. code-block:: cpp
QScriptEngineDebugger* debugger = new QScriptEngineDebugger();
debugger->attachTo(&engine);
debugger->action(QScriptEngineDebugger::InterruptAction)->trigger();
ファイルに保存してあるスクリプトを読み込む
------------------------------------------
QtのファイルAPIを使って読みこめば、外部に保存してあるファイルを読み込むことも可能です。
.. code-block:: cpp
QString fileName = "ファイルパス";
QFile scriptFile(fileName);
scriptFile.open(QIODevice::ReadOnly);
QTextStream stream(&scriptFile);
QString contents = stream.readAll();
scriptFile.close();
engine.evaluate(contents, fileName);
JavaScriptのソースファイルをQtのプログラムと一緒に配布したい場合には、プロジェクトファイルに配布したいファイルとして登録します。
.. code-block:: make
folder_01.source = js
folder_01.target = .
DEPLOYMENTFOLDERS = folder_01
MacOSXの場合には、 :file:`.app` ファイルの中の :file:`Contents/Resources/js` の中にコピーされます。他の環境は試していないですが、他のサンプルを見るかぎり、Windowsの場合は実行ファイルと同じパスに、Unix系OSの場合は、実行ファイルが :file:`/usr/local/bin` にあれば、 :file:`/usr/local/share/アプリ名` 以下にあることを想定すれば良いみたいです。
.. code-block:: cpp
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のファイルのパスを取得することができます。例えば、ファイル名が :file:`main.js` なら
.. code-block:: cpp
QString fileName = adjustPath("main.js");
でOKです。
関数やオブジェクトを追加する
----------------------------
``console.log`` を追加してみます。JavaScriptの名前空間に要素を登録するのに、よく使うメソッドは次の通りです:
* ``QScriptEngine::newFunction()``: 関数作成
* ``QScriptEngine::newObject()``: オブジェクト作成
* ``QScriptEngine::globalObject()``: グローバル名前空間の取得
* ``QScriptValue::property()``: プロパティ取得
* ``QScriptValue::setProperty()``: プロパティ設定
まず、JavaScript側から呼び出される関数をC++で作ります。
.. code-block:: cpp
static QScriptValue consoleLogForJS(QScriptContext* context, QScriptEngine* engine)
{
QStringList list;
for(int i=0; iargumentCount(); ++i)
{
QScriptValue param(context->argument(i));
list.append(param.toString());
}
qDebug() << list.join(" ");
return engine->undefinedValue();
}
1つめのパラメータの ``context`` を使うと、引数情報を得ることができます。文字列にしてつなげて ``qDebug`` に投げています。
登録は ``QScriptEngine`` オブジェクトのメソッドをいくつか呼べばできます。
.. code-block:: cpp
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`` してしまっても問題はありません。
.. code-block:: cpp
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の配列の操作をする参考にもなると思います。
.. code-block:: cpp
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をインストールしたら、ホーム直下の :file:`QtSDK` フォルダにインストールされました。このフォルダにあるデスクトップ版のライブラリを対象にバインディングを作ってみます。作業は `このページ `_ を参考にしました。付属READMEの説明だけでは無理で、環境変数を定義しないとうまくいきません。リンク先のページではWindowsで実施した結果が乗っていますが、同じやり方でMacでもいけました。
まずは環境変数を定義します。
.. code-block:: bash
$ export QTDIR=~/QtSDK/Desktop/Qt/474/gcc/
$ export PATH=$PATH:$QTDIR/bin
masterブランチのファイルを取ってきて展開します。 `このページ `_ の右側の ``Download master as tar.gz`` のリンクから取ってきてもいいし、gitを使ってもいいと思います。僕は :file:`.tar.gz` を落としてきました。展開したら、カレントディレクトリに :file:`qt-labs-qtscriptgenerator` というフォルダができました。
.. code-block:: bash
$ 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とか出ていると失敗です。成功するとこのぐらいの数値が最後に出ます。
.. code-block:: bash
$ cd ../qtbindings/
$ qmake
$ make
(しばらく待つ)
Qtのクラスが使えるJavaScriptインタプリタもテスト用にできるので、これを使うと簡単にうまくいったか確認できます。引き続き、同じ :file:`qtbindings` フォルダにいるとします。
.. code-block:: bash
$ qs_eval/qs_eval ../examples/AnalogClock.js
.. image:: qtanalogclock.png
このウインドウが表示されればビルドが成功しています。おめでとうございます。
.. note::
参考: Lively for Qt: `http://lively.cs.tut.fi/qt/installation.html `_
QtScript情報
------------
* `Scripting Your Qt Application `_ (slideshare)