Sencha Desktop PackagerでGXTを利用する
こんにちは、ゼノフィnakamuraです。
Sencha Desktop PackagerでGXTを使う
先月AriyaがWebアプリケーションをラッピングして、ネイティブソリューションとして提供するツール、
Sencha Desktop Packager
についての
記事
を書きました。
このブログ投稿では、そのツールを利用して、GWTアプリケーションをパッケージングすることについて説明します。
GWTアプリケーションはJavaで作成されていますが、JavaScriptで実行されるので、従来のブラウザ無しでデスクトップのJavaを実行するためにパッケージ化したい場合があります。 このようなアプリケーションを構造する利点があるかもしれません:Web向けに製品を作成し、ファイルシステムやメニューアクセスするためのより良いローカルソリューションを提供できます。 既に対応しているブラウザの指定されたバージョンと一緒に提供できるのは利点です。 さらにそのブラウザは他のサイトにアクセスできませんし、アップデートによってアプリケーションが動かなくなることもありません。 この記事では、3つの異なるサンプルアプリケーションを検討します:ネイティブ機能付きの単純なhello world、GXT Explorerに基づいたもの、Quake2 GWTデモに基づいたもの。
Hello!
Desktop Packagerを開始するために、Eclipseで新しいGWTアプリケーションを作成してHelloDesktopという名前を付けました。アプリケーションに必要な出発点になるため、デフォルトのhtmlファイルをindex.htmlに名前を変更しました。
サーバーとの通信
Desktop Packagerは実際のWebサーバーを実行しないということを意識する必要があるので、集約されたアプリケーションサーバーを呼び出す必要がある場合は、そのサーバーを直接指定するように構成する必要があります。 RPC の場合は、 setServiceEntryPoint を呼び出しますので、 RequestFactory 用にDefaultRequestTransportを作成して、実際のサーバーURLを指定して setRequestUrl を呼び出します。
この要件のため、サンプルのRPCの呼び出しを削除して、 EntryPointを変更し、 正確に動作していることを確認できるように ページに単一のメッセージを表示するようにしました。
1 2 3 4 5 6 | public class HelloDesktop implements EntryPoint { @Override public void onModuleLoad() { Window.alert("Hello, desktop!"); } } |
変更後、サンプルプロジェクトはこのようになりました:

GWTアプリケーションのコンパイルおよびパッケージング
アプリケーションをパッケージングする最初の段階はマニフェストファイルの作成です。マニフェストファイルはパッケージするリソースをどこで見つけられるのかということと、アプリケーションを起動する方法を説明します。ほとんどのGWTプロジェクトはwar/directoryをパッケージングするか、もしMavenを利用するなら、target/<project>-<version>/ directoryです。HelloDesktopという新しいGWTアプリケーションをEclipseで作成して、次をマニフェストファイルとして利用し、プロジェクトのルートにHelloDesktop.jsonという名前で保存しました:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | { "organizationName": "Sencha", "applicationName": "Hello Desktop", "versionString": "1.0", "outputPath": "packaged app", "webAppPath": "war/", "settings": { "mainWindow": { "autoShow": true }, "security": { "allowCrossSite": true } } } |
アプリケーションを実際にパッケージ化する前に、まだ大事な手順が残っています:アプリケーションをJavaScriptにコンパイルする。現在はまだDesktop PackagerのDev Modeプラグインはありませんので、デバッグ用には二つのメインオプションがあります:
- GWT Dev ModeをChromeで実行する。Sencha Desktop PackagerはChromiumの変更されたバージョンを利用しているので、Ion APIなしのChromeのように動作するはずです。
- PRETTYにセットされているスタイルシートでコンパイルして、リモートJSデバッグ機能を利用してデバッグを実行して、ログされているメッセージを読み込みます。
最後にionpackagerコマンドを実行して、アプリケーションを開くと、スクリーンにアラートが出ました。

このようなプロジェクトにGXTを追加するのは非常に簡単で、GXTのsetup.txtの指示に従うか、新しい オンラインGXTガイドにある「getting started guide 」を読むだけです。一度構成すると、レイアウト、データウィジェットなどを利用して、普段と同じようにアプリケーションを作成できるようになります。
Ionとブラウザの違い
パッケージ化されたアプリケーションと実際のブラウザでの実行にはいくつか異なる部分がありますので、アプリケーションに小さな変更を行う必要があるかもしれません。まずアドレスバー、ブックマーク、バックボタン、リフレッシュボタンはありません。その結果、Window.Locationクラスを利用してアプリケーションをナビゲートすることはできませんし、Historyクラスも利用できません。内部的な状態を保つためにはアプリケーションを違う構成にして、urlを変更する呼び出しをしないようにする必要があります(変更するURLがないため、エラーが発生する場合もあります)。
二つ目の違いはファイルはWebサーバーからダウンロードするのではなく、パッケージされたアプリケーションの中にあります。通常これはあまり大きな影響はありませんが、アプリケーションによってはファイルをロードする時にサーバーから「200 OK」を受け取ろうとしても、それが使用可能ではなかったら「404 File Not Found」が返されます。その代わりに、全てのファイルのロードはステータスコード0を返します。ファイルが見つからなかった場合は、その中身がない状態になります。あまり心配はいりませんが、この状況が時々発生します。
GWTでIon APIを利用する
Ion APIへのアクセスを提供するGWTモジュールのベータ版を公開しました。このモジュールは生成されたjarでまだ早い段階に入っていて、APIドキュメンテーションに基づいています。自動的に他の内容をJavadocsとして含めていますが、時々どのような引数を中にパスされるか戻されるかは明確ではありません。Desktop PackagerのフォーラムにIonバインディングに関するバグレポートと質問をのせて下さい。
上記で書かれたように、利用するurlがないため、GXT Explorerをポートした時に、アドレスバーと通信しないようにPlaceHistoryHandlerを外しました。こうするとアプリケーション内でバック・フォワードのボタン、またはブックマークが無くてもActivitiesとPlacesを利用し続けるようになります。アプリケーションは現在の位置をローカル保管に設置して、アプリケーションを起動した時点で最後のトークンを PlaceHistoryHandler.handleCurrentHistory() メッソドで読み込み、カスタムのPlaceChangeHandlerと交換できます。ネイティブのアプリケーションはアドレスバーが無いように、アプリケーションのある部分に簡単に移動できるようメニューバーがあります。なので、ヒストリー操作を全て可能となる例を一覧するネイティブメニューバーに切り替えました:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | // Attach the Examples menu to the menubar Menu m = Ui.mainWindowStatic().menuBar().addMenu("Examples"); // Add an overview item // (example is a reference to the overview) m.addMenuItem("Overview", new Function(loadExampleFunc( placeController, new ExamplePlace(example.getId()))), ""); // For each category, List<Category> cats = exampleModel.getCategories(); for (int i = 0; i < cats.size(); i++) { Category c = cats.get(i); if (!ExplorerApp.OVERVIEW.equalsIgnoreCase(c.getName())) { // add a new menu, and iterate through the examples there, Menu catMenu = m.addMenu(c.getName()); List<Example> examples = c.getExamples(); for (int j = 0; j < examples.size(); j++) { Example ex = examples.get(j); if (!ExplorerApp.OVERVIEW.equalsIgnoreCase(examples.get(j).getName())) { // adding a reference to each as we go catMenu.addMenuItem(ex.getName(), new Function( loadExampleFunc(placeController, new ExamplePlace(ex.getId()))), ""); } } } } |
このコードはIon JavaScript APIを使用し、当社のGWTコードを呼び出せる関数を作成するJSNIで書かれたloadExampleFunc
というヘルパーメソッドの使用が必要となります:
1 2 3 4 5 | private native JavaScriptObject loadExampleFunc(PlaceController placeController, ExamplePlace examplePlace) /*-{ return $entry(function() { placeController.@com.google.gwt.place.shared.PlaceController::goTo(*)(examplePlace); }); }-*/; |
最後の引数は(このサンプルでは空白になっている)、キーボードのショートカットを利用できるようになります。多くのオプションがあるexplorerのようなアプリケーションでは、ショートカットを利用してスキップし、ユーザーがオプションを見て探しているものを見つけるようにしておく方法が最も効率的です。

HTML5を使う(ゲーム)
Sencha Desktop PackagerはChromiumを利用していますので、私達のアプリケーションはLocalStorage、Canvas、WebGL、WebSocketsなど現代のブラウザにある機能をどれでも利用できます。この記事を書き始めた時、デスクトップアプリケーションとしてラッピングするために利用するモダンブラウザ機能のGWTプロジェクトを思い出すと、 Quake 2 GWTポート は楽しくて情報の濃い選択になると思います。
最初は指示された通りにプロジェクトの作成を始めて、FirefoxとChromeの現在のバージョンで正確に動作することを確認しました。ゲームは動作しましたが、マルチプレイヤーを正確に動作できなかったので、少し調べたら最近のブラウザではnightly jettyビルドのWeb Socket実装はあまり動かないことがわかりました。
次、war/ directoryを指すマニフェストを作成し、問題を探し出すようにリモートデバッグを使用可能にし、アプリケーションをコンパイルして実行しました。アプリケーションは実行して、スタート画面まで進めましたが、ゲーム自体は起動しませんでした。コンソールを見たら、たくさんのファイルを正確にロードしていなくて、アプリケーションにも問題がありました。ファイルが200ステータスコード無しでロードした場合に対処するためにGwtResourceLoaderImpl.loadResourceAsync でファイルシステムコードを変更しました:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | //... String response; // if (xhr.getStatus() != 200) { // If we get a 200, then we loaded over http successfully // If we get a 0 and are in the the finished readyState, then: // - file size is not 0, and we loaded successfully // - file size is zero, and there was an error if ((xhr.getStatus() != 200 && xhr.getStatus() != 0) || xhr.getResponseText().length() == 0) { Com.Printf("Failed to load file #" + mySequenceNumber + ": " + path + " status: " + xhr.getStatus() + "/" + xhr.getStatusText() + "\n"); ResourceLoader.fail(new IOException("status = " + xhr.getStatus())); response = null; } else { response = xhr.getResponseText(); Com.Printf("Received response #" + mySequenceNumber + ": " + path + "\r"); } //... |
変更を行うとゲームが動きました!音は正確に動作していませんでした:これは対応されてないコーデック、別のファイルローディング問題、<audio>タグのpackager toolのバグの問題だったかどうかはまだわかりませんでした。それとSencha Desktop Packager 1.1では、プレイヤーと他のキャラクターの皮膚がレンダリングされてなかったのです:これはDesktop Packager 1.2では解決されました。時々マウスとキーボードが反応しない問題もありましたが、ウィンドウをクリックしたらそれも解決しました。

最後に、Quakeの体験を取得するには、最後の変更をしました:起動した時点で全画面表示にすること。今回は、GWTモジュールにIonの引き継ぎをしないで、一つのメソッドを呼び出していたので、トリガーするには直接JSNIに呼び出しました。最初はGwtQuake.java でこの新しいJSNIメソッドを生成しました:
1 2 3 | private native void fullscreen() /*-{ $wnd.Ion.ui.mainWindow.fullscreen(); }-*/; |
そして、onModuleLoadの最初に呼び出しました:
1 2 3 4 5 6 | public void onModuleLoad() { fullscreen(); // Initialize drivers. Document doc = Document.get(); doc.setTitle("GWT Quake II"); //... |
これでアプリケーションが起動した時点で全画面表示に変更します。これはQuake 2の体験にはるかに近づきますが、マウスがウィンドウの枠で止まるように mouse lock API を確認しました。まだフォーカス問題が発生するかもしれません:もしmouselook、またはキーが動作してなかったら画面をクリックして下さい。
自分自身で構築する
Sencha Desktop Packagerを自分自身で試して下さい!もし質問がありましたらドキュメンテーションや、フォーラムを訪ねて下さい。またダウンロード して自分で試して下さい!もし質問がありましたらドキュメンテーションや、フォーラムを訪ねて下さい。
アップデート: Desktop PackagerフォーラムにあなたのGWTプロジェクトでIon APIを使用可能とするjarファイルといくつかのセットアップ手順とドキュメンテーションがあります。