更新: 2011/11/08
Note
本ページは業務で調べた内容をまとめたページです。 株式会社ディー・エヌ・エーの提供でお送りしております。
QtにはQtScriptなるものがついてきます。
せっかくなので、いろいろQtScriptで遊んでみます。
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の名前空間に要素を登録するのに、よく使うメソッドは次の通りです:
まず、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 を継承したオブジェクトを簡単に登録できるようになっていたりします。
1ファイルで全部のコードを書ききるのは難しいです。サーバサイドのJavaScriptの共通規格のCommonJSで定義されている、 require() を定義して、他のファイルを読み込めるようにします。
基本戦略としては:
まず、 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);
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
このウインドウが表示されればビルドが成功しています。おめでとうございます。
Note
参考: Lively for Qt: http://lively.cs.tut.fi/qt/installation.html