Hatena::ブログ(Diary)

Kazzzの日記 このページをアンテナに追加 RSSフィード

2011-06-14

[]GWT Widget拡張する

GWTのWidgetは階層に基づいたクラス設計がなされている為、簡単に拡張できる。

例) input要素に追加されたplaceholder属性プロパティとして利用することのできるTextBoxを作る

元々GWTのTextBoxクラス(com.google.gwt.user.client.ui.TextBox)はinput要素のピアとして提供されている。 このクラスにplaceholderプロパテイを追加するように実装してみよう。

既存のTextBoxを拡張する

placeholderプロパティを追加したTextBox5というクラスを実装してみよう。

  • org.kazz.gwt.client.widget.TextBox5.java
public class TextBox5 extends TextBox {
    public String getPlaceHolder() {
        return DOM.getElementProperty(this.getElement(), "placeholder");
    }
    public void setPlaceHolder(String placeholder) {
        DOM.setElementProperty(this.getElement(), "placeholder", placeholder);
    }
}

簡単だ。実装のポイントはDOM.get/setElementPropertyメソッド。このメソッドでピアの要素(input要素)の属性を取得/設定することができるので、HTMLの仕様が変わっても、プラットホームによって違っても対応が可能だ。
この例の場合、placeholder属性の読み書きを行っている。

モジュール化する

順序が逆になったがGWTで使用するクラスはなんらかのモジュールに属している必要がある。本来、ある程度まとまった実装(Widget全部とか)をモジュール化するのだろうが、今回は一つのクラスだけが属しているモジュールとして、Kazz.gwt.xml作成する。

  • Kazz.gwt.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE module PUBLIC "-//Google Inc.//DTD Google Web Toolkit 2.2.0//EN" "http://google-web-toolkit.googlecode.com/svn/tags/2.2.0/distro-source/core/src/gwt-module.dtd">
<module rename-to="kazz">
  <inherits name="com.google.gwt.user.User" />
  <inherits name="com.google.gwt.i18n.I18N"/>
  <inherits name="com.google.gwt.resources.Resources" />
  <source path="client" />
</module>

基本的には必要な他のモジュールをinheritするだけで良いと思う。

他のモジュールで参照する

他のモジュールで参照する場合、Javaクラスのクラスパスとは別にモジュールをinheritする必要がある。

  • AddressBook.gwt.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE module PUBLIC "-//Google Inc.//DTD Google Web Toolkit 2.2.0//EN" "http://google-web-toolkit.googlecode.com/svn/tags/2.2.0/distro-source/core/src/gwt-module.dtd">
<module rename-to="addressbook">
  <inherits name="org.kazz.gwt.Kazz" /> <!-- 使用するモジュールをInherit -->
  <source path="client" />
  <entry-point class="net.kazzz.client.AddressBook" />
</module>


UiBinderからTextBox5を利用する

上記方法で実装、モジュール化したWidgetはGWTアプリケーションからは勿論のこと、UiBinderから使うこともできる。
f:id:Kazzz:20110614191221p:image:w640
これはGWTDesignerで拙作の住所録UIをUiBinderで作成している所だが、入力に使用しているTextBoxはTextBox5クラスを使用しているのが分かる。実際にplaceholderプロパティを設定しているのも分かるだろう。まだコンポーネントパレットに登録していないのでXML側ではネームスペースも含めて手動で追加する必要がある。

  • AddressBookUi.ui.xml
<!DOCTYPE ui:UiBinder SYSTEM "http://dl.google.com/gwt/DTD/xhtml.ent">
<ui:UiBinder
 xmlns:ui="urn:ui:com.google.gwt.uibinder"
 xmlns:g="urn:import:com.google.gwt.user.client.ui"
 xmlns:j="urn:import:org.kazz.gwt.client.widget"   <== TextBox5を使用するために追加したネームスペース
>
 <g:HTMLPanel styleName="{style.AddressInfo}">
   <g:Grid ui:field="pnlAddressInfo">
     <g:row>
       <g:customCell><g:Label ui:field="lblFullName" text="氏名"/></g:customCell>
       <g:customCell><j:TextBox5 ui:field="txtFullName" visibleLength="20" placeHolder="例) 山田太郎" /></g:customCell>
     </g:row>
     <g:row>
       <g:customCell><g:Label ui:field="lblAddress" text="住所"/></g:customCell>
       <g:customCell><j:TextBox5 ui:field="txtAddress" visibleLength="50" placeHolder="例) 東京都千代田区外神田"/></g:customCell>
     </g:row>
     <g:row>
       <g:customCell><g:Label ui:field="lblEmail" text="e-mail"/></g:customCell>
       <g:customCell><j:TextBox5 ui:field="txtEmail" visibleLength="25" placeHolder="例) mail@example.com"/></g:customCell>
     </g:row>
     :
     :
</ui:UiBinder> 

ここで素晴らしいのは拡張したクラスに合わせて追加したクラス(TextBox5)のためのXML名前空間もきちんと補完されることだ。

前回のエントリで解説したように、UiBinderで定期したクラスはui:fieldで指定したフィールド名で参照することができる。

  • AddressBookUi.java
    @UiField Label lblFullName;
    @UiField TextBox5 txtFullName;
@UiField Label lblAddress; @UiField TextBox5 txtAddress;
@UiField Label lblEmail; @UiField TextBox5 txtEmail;


実行結果 (PC Chrome12)

f:id:Kazzz:20110614185720p:image

実行結果 (Android 2.3.3 Webkit Emulator)

f:id:Kazzz:20110614185721p:image

実行結果を見ると、空のinput要素にプレースホルダ(placeholder)属性が設定されているのが分かるだろう。※


以上、GWTのルールに従うという面倒さはあるものの、GWT Widgetの拡張自体は非常に簡単にできる。これで業務に合わせたWidgetクラスをどんどん作れそうだ。

※placeholder属性は残念ながらIEでは動作しない(無視される)

2011-06-08

[]UiBinderその2

GWTのUiBinderは単にGUIレイアウトXML記述することに留まらない素晴らしい機能がある。

AddressBookUi.xml (抜粋)
<!DOCTYPE ui:UiBinder SYSTEM "http://dl.google.com/gwt/DTD/xhtml.ent">
<ui:UiBinder xmlns:ui="urn:ui:com.google.gwt.uibinder" xmlns:g="urn:import:com.google.gwt.user.client.ui">
 <g:HTMLPanel>
   <g:Grid>
     <g:row>
       <g:customCell><g:Label ui:field="lblFullName"/></g:customCell>
       <g:customCell><g:TextBox ui:field="txtFullName" visibleLength="20"/></g:customCell>
     </g:row>
   </g:Grid>
 </g:HTMLPanel>
</ui:UiBinder> 

ここではuiネームスペースのfield属性に注目しておいてほしい。

このようなレイアウトがあるとする。UiBinderのUi.XMLにはそれをパースしてWidgetに実体化するための、対になるクラスが必要だ。(ウィザード等でUiBinderを生成すると自動的にクラスがテンプレートとして生成される)

AddressBookUi.java
public class AddressBookUi extends Composite  {
    private static AddressBookUiUiBinder uiBinder = GWT.create(AddressBookUiUiBinder.class);
    interface AddressBookUiUiBinder extends UiBinder<Widget, AddressBookUi> {
    }
    public AddressBookUi() {
        initWidget(uiBinder.createAndBindUi(this));
    }

Ui.XMLに記述されたUiBinderのWidgetはデフォルトコンストラクタコードに埋め込まれたAddressBookUiUiBinder#createAndBindUiメソッドにより実体化される。

ではこのコード中で実体化された"UiBinder Widget"にアクセスするにはどうしたら良いのだろう。

ここで先ほどUi.XMLの例を紹介した際のuiネームスペースのfield属性が登場する。実はこの属性で記述した名前を、クラス中のフィールド名として参照するのである。 そして参照するフィールドには@UiFieldアノテーションを記述するのだ。

AddressBookUi.java ( UiFieldアノテーション記述 )
public class AddressBookUi extends Composite  {
    private static AddressBookUiUiBinder uiBinder = GWT
            .create(AddressBookUiUiBinder.class);
interface AddressBookUiUiBinder extends UiBinder<Widget, AddressBookUi> { } @UiField Label lblFullName; @UiField TextBox txtFullName; : public AddressBookUi() { this.initWidget(uiBinder.createAndBindUi(this)); this.lblFullName.setText("氏名"); }

このように記述するとGWTはUi.XMLから実体化されたWidgetのインスタンスを識別して参照を@UiFieldでアノテートされたフィールドに注入する。(これが"UiBinder"と呼ばれる所以だろう。) また、これだけであればリフレクションでもなんとかなるが、GWTはコンパイル時にui.XMLとフィールド名の相互をチェックしており、合致しない場合エラーが発生させることが出来るのである。

Ui.XMLのui:fieldと@UiFiedを記述したフィールド名が合致せずエラーが発生している

f:id:Kazzz:20110608180703p:image

GWTはコンパイル時にui.xmlをパースして内部にGUIウィジェットリポジトリを生成し、ランタイム時にはオーナクラスのフィールドに参照を注入する。 まるでDIコンテナのようだ。

2011-06-06

[]UiBinder

UiBinder以前、GWTアプリケーションで画面レイアウトを作るにはHTML外では全てJavaコードで書く必要があった。

  • コードで書くレイアウト
  private VerticalPanel mainPanel = new VerticalPanel();
  private FlexTable stocksFlexTable = new FlexTable();
  private HorizontalPanel addPanel = new HorizontalPanel();
  private TextBox newSymbolTextBox = new TextBox();
  private Button addStockButton = new Button("Add");
  private Label lastUpdatedLabel = new Label();
  
  public void onModuleLoad() {
    :
    addPanel.add(newSymbolTextBox);
    addPanel.add(addStockButton);
mainPanel.add(stocksFlexTable); mainPanel.add(addPanel); mainPanel.add(lastUpdatedLabel);
RootPanel.get("stockList").add(mainPanel); }

この方法はJFC/Swing等と同様「手続き型」と呼ばれる方法であり、どのようにUIが構成されているか順を追って分かりやすい反面、コードとUIが同居しているためデザインと処理を分離するのが困難になっている。また、この方法ではWISYWIGで画面をデザインする際にコードとの同期を取る必要があるため、GUIデザイナとの連携がどうしても難しいものとなる。※1

開発時に画面を出来るだけWISYWIGでプレビューできるように、コードとレイアウトを分離するために考え出されたのが「宣言型」と呼ばれる方法であり、画面のレイアウトや部品の配置をXML等のマークアップで実現するのが最近の主流になっている。この方法ではデザイナとコードの同期が必要無く自由度が高くなる。またASP.NETのコードビハインドのようにコンテキストを共有できるものもある※2

UiBinderはそれまで手続き型でしか出来なかった画面のレイアウトを宣言型で可能とし、更にASP.NET コードビハインドのようにレイアウトとコードの連携を可能にした画期的な機能である。GWTではversion 2.0からUiBinderをサポートしている。

UiBinderの狙い

<!DOCTYPE ui:UiBinder SYSTEM "http://dl.google.com/gwt/DTD/xhtml.ent">
<ui:UiBinder xmlns:ui="urn:ui:com.google.gwt.uibinder" xmlns:g="urn:import:com.google.gwt.user.client.ui" xmlns:a="urn:import:net.kazzz.client"
>
 <ui:style></ui:style>
 <g:HTMLPanel styleName="AddressInfo">
   <g:Grid ui:field="pnlAddressInfo" >
     <g:row>
       <g:customCell><g:Label ui:field="lblFullName" text="氏名"/></g:customCell>
       <g:customCell><g:TextBox ui:field="txtFullName" visibleLength="20"/></g:customCell>
     </g:row>
     <g:row>
       <g:customCell><g:Label ui:field="lblAddress" text="住所"/></g:customCell>
       <g:customCell><g:TextBox ui:field="txtAddress" visibleLength="60"/></g:customCell>
     </g:row>
     <g:row>
       <g:customCell><g:Label ui:field="lblEmail" text="e-mail"/></g:customCell>
       <g:customCell><g:TextBox ui:field="txtEmail" visibleLength="20"/></g:customCell>
     </g:row>
     <g:row>
       <g:customCell><g:Label ui:field="lblMarried" text="既婚"/></g:customCell>
       <g:customCell><g:CheckBox ui:field="chkMarried" checked="false"/></g:customCell>
     </g:row>
     <g:row>
       <g:customCell><g:Label ui:field="lblGender" text="性別"/></g:customCell>
       <g:customCell>
         <g:HorizontalPanel>
           <g:RadioButton ui:field="rdoMale" name="gender" text="男"/>
           <g:RadioButton ui:field="rdoFemale" name="gender" text="女"/>
         </g:HorizontalPanel>
       </g:customCell>
     </g:row>
   </g:Grid>
 </g:HTMLPanel>
</ui:UiBinder> 

実際に使ってみれば分かるが、UiBinderはこのようにXMLで宣言的にレイアウトを作ることが出来、とても便利である。

日本では情報が少ないせいか、一部を除いてあまり利用が進まないGWTだが、非常に優れた面があり個人的にはこれからどんどん使っていきたいと考えている。

GWTのUiBinderには他にも有用な機能が幾つかあるが、それは次回以降に紹介したいと思う。

※1 古くはJavaのJBuilderや最近ではNetBeans等のように手続き型のGUIであってもGUIデザイナで画面をデザインできる物もあるが、拡張ライブラリィが必要だったり独自の実装が必要だったりとどうしても複雑になってしまう。

※2 XAMLAndroidもこの方式である

2011-06-03

[]ClientBundleのリソースはpublicディレクトリに配置できない

ClientBundleにアクセサを書く場合、通常はリソースのベース名と同じ名前定義するが、違う名前のアクセサを定義したい場合は、@Sourceアノテーションを使用してリソースのファイル名を直接書くことができる。

  • @Sourceアノテーションにファイル名を記述する
@Source("splash.gif")
ImageResource splash();

また、この@Sourceアノテーションは相対パスを許可しており(絶対パスは×)、モジュールルートからのパスを記述することもできる。

  • @Sourceアノテーションに相対パスを記述する
@Source("../client/css/splash.gif")
ImageResource splash();

相対パスを記述する場合、起点は<モジュール名>.gwt.xmlを配置しているパッケージ(ディレクトリである)

とこれは非常に便利なのだが、融通が効かない面もある。

例えば、"public" という名前のディレクトリは@Sourceアノテーションで指定することができず、コンパイルを待たずエラーになってしまう。

  • publicディレクトリは指定できない

f:id:Kazzz:20110603185156p:image

この名前を持つディレクトリは静的なコンテンツを格納するためにGWTに予約されたディレクトリ名でありClientBundleの対象にできないようだ。

2011-06-02

[]ClientBundle その3

ClientBundleにより参照されたリソースはGWTコンパイル時にクライアント側(HTML、スクリプト)に埋め込まれてキャッシュされることになるが、その形式により格納の方法が変わる。

最も典型的なのはImageResourceであり、前回紹介したスプラッシュのカーソルとして使用しているgifイメージをClientBundle中に定義した場合

public interface AddressBookResources extends ClientBundle {
    public static final AddressBookResources INSTANCE = 
        GWT.create(AddressBookResources.class);
@Source("../client/css/splash.gif") ImageResource splash(); : }

GWTコンパイルによりモジュールパッケージ下にハッシュ化された名前(この場合324225BFCE5DD1E8FDFA674C6AFEE915.cache.gif)でgifイメージはコピーされる。
f:id:Kazzz:20110602190511p:image

そして、このイメージが直接使用されるのではなく、実際にはjavascriptのデータスキーマURIに変換されて埋め込まれるのだ。

  • splash.gifがデータスキーマURIに変換された結果

f:id:Kazzz:20110602190510p:image
Chromeのデベロッパーツール、Element解析でその時点のHTMLをダンプしているが、データスキーマ形式に変換されていることが分かるだろう。

目的は勿論サーバとのラウンドトリップを減らして全体の性能を上げることだが、どんなサイズのイメージも同様に変換されるのかは不明だ。

2011-05-31

[]ClientBundle その2

ClientBundleも以前に紹介した国際化の時同様に、GWTの様式である

1. 用途に合わせた(ClientBundle)インタフェース作成
2. 必要なオブジェクトを取得するためのアクセサ定義
3. アプリケーション中でClientBundleの実体化

という手順で使用する。

ClientBundleインタフェース作成

ClientBundleインタフェースを拡張してその中にアプリケーションで使用するリソースを包含するようなインタフェースを作成する。

  • AddressBookResource.java
public interface AddressBookResources extends ClientBundle {
    public static final AddressBookResources INSTANCE = 
        GWT.create(AddressBookResources.class);
    :
    リソースを取得するためのアクセサ1
    リソースを取得するためのアクセサ2
    :
}

public static finalなフィールドにインスタンスを生成してセットしておくのは、GWTによく見られる一種のシングルトンパターンだ。

AddressBookResources res = AddressBookResources.INSTANCE;

別に定義しなくても問題は無いが、このように都度インスタンスを生成せずに参照するためのイディオムである。

必要なオブジェクトを取得するためのアクセサ定義

ClientBundleがアクセサとして扱えるリソースの型は ResourcePrototype及びResourceGeneratorインタフェースを実装したジェネレータ型に依存するが、一般的なリソース型の実装が既に提供されており通常はこれらを使うだけで良い。

  • ImageResource : javax.imageioでサポートするイメージデータを提供する(jpg, png, bmp, wbmp, gif)
  • CssResource : CSSスタイルシートを提供する
  • TextResource : 内部埋め込み又は個別ファイルに格納されるテキストデータを提供する
  • DataResource : テキスト以外のデータを提供する
  • GWTCreateResource : GWT.create()メソッドによって生成されたリソース型を提供する
  • ClientBundleの派生型 : ネストしたClientBundle

以下一般的なものだけ紹介する。

ImageResource

参照されるイメージを取得するためのアクセサの戻り値として定義する。アクセサは通常リソース名(ファイルのベース名)と同じにするが、変更する場合は@Sourceアノテーションで実際のリソースのファイル名を記述する。

    // splash.gifを取得する
    ImageResource splash();
@Source("icon.png") ImageResource favicon();

CssResource

参照されるCSSを取得するためのアクセサの戻り値として定義する。アクセサはCSSリソース名(ファイルのベース名)と同じにするが、変更する場合は@Sourceアノテーションで実際のリソースのファイル名を記述する。

CssResource自体はGWTコンパイル時にCSS内に記述されたセレクタ全てをメソッドとして定義したインタフェースを実装したものである必要がある。

例えばこのようなスタイルシートをCssResourceを戻すアクセサとして定義した場合、

  • AddressBook.css
#loading {
     display: block;
     position: absolute;
     top: 50%;
     left: 50%;
     text-align: center;
     margin-left: -100px;
  }
div, .contentContainer{
    margin: 5px;
  }

idセレクタであるcontentContainerをアクセサとして使用する、以下のインタフェースを別途用意する必要がある。

public interface AddressBookCSS extends CssResource {
    String contentContainer();
}


ClientBundleでは、このAddressBookCSSを戻すアクセサとして定義するのである。

  • AddressBookResource.java
public interface AddressBookResources extends ClientBundle {
    public static final AddressBookResources INSTANCE = 
        GWT.create(AddressBookResources.class);
// splash.gifを取得する ImageResource splash();
@Source("icon.png") ImageResource favicon();
//AddressBook.cssを取得する AddressBookCSS addressBookCSS();

このように使用するスタイルシートに対してインタフェースを用意しないとGWTコンパイル時に以下のようにエラーになってしまう。

Compiling module net.kazzz.AddressBook
   Scanning for additional dependencies: file:/E:/gwt/project/addressbook/src/net/kazzz/client/AddressBook.java
      Computing all possible rebind results for 'net.kazzz.client.AddressBookResources'
         Rebinding net.kazzz.client.AddressBookResources
            Invoking generator com.google.gwt.resources.rebind.context.InlineClientBundleGenerator
               Creating assignment for css()
                  Replacing CSS class names
                     [ERROR] The following unobfuscated classes were present in a strict CssResource:
                        [ERROR] contentContainer
                        Fix by adding String accessor method(s) to the CssResource interface for obfuscated classes, or using an @external declaration for unobfuscated classes.
               [ERROR] Generator 'com.google.gwt.resources.rebind.context.InlineClientBundleGenerator' threw an exception while rebinding 'net.kazzz.client.AddressBookResources'
com.google.gwt.resources.css.ast.CssCompilerException: Missing a CSS replacement

GWTコンパイルの際にスタイルシートに対して上記のように厳密な参照のチェックとアクセサの有無を検査するのが嫌なのであれば、アノテーション@CssResource.NotStrictを使用して定義することでエラーを抑制することもできる。
    //AddressBook.cssを取得する
    @CssResource.NotStrict
    CssResource addressBookCSS();

また、様々なアノテーションをCSS中に記述することで、GWTコンパイル時のCSSパース時様々な処理を行うこともできるが、非常に多くの種類があるのでここでは割愛する。

TextResource

参照されるテキストデータを取得するためのアクセサの戻り値として定義する。アクセサはリソース名(ファイルのベース名)と同じにするが、変更する場合は@Sourceアノテーションで実際のリソースのファイル名を記述する。

その記述方法はImageResourceと同様なので割愛するが、たくさんのテキストデータがあって、いちいちファイル毎にアクセサを追加していくのが面倒な場合、Eclipseであればプラグインが提供するウィザードで一気にデータを追加することができる。

f:id:Kazzz:20110531185936p:image
コンテキストメニュー→Google→Add to ClientBundleから

f:id:Kazzz:20110531185938p:image
ウィザード中で追加したいデータをプロジェクトツリーから複数選択する
f:id:Kazzz:20110531185937p:image

OKボタン押下後、AddressBookResourcesには以下の行が追加されているはずだ。

  • AddressBookResources.java
    @Source("net/kazzz/client/text/a.txt")
    TextResource a();
@Source("net/kazzz/client/text/b.txt") TextResource b();
@Source("net/kazzz/client/text/c.pdf") DataResource c();
@Source("net/kazzz/client/text/d.xml") DataResource d();

テキストデータだからといって拡張子が.txtである必要は無いが、ウィザードで処理するとこのようにDataResourceとして処理されてしまうようだ。

2011-05-30

[]RootPanel

GWTのRootPanel(com.google.gwt.user.client.ui.RootPanel)クラスはHTML上のbodyタグに相当するGWTパネルオブジェクトでありコンテキスト上に唯一のものと思っていたのだが、そうではないようだ。

通常、RootPanelは他のWidgetを追加していくコンテナとしてコーディングする。

RootPanel rootPanel = RootPanel.get();
rootPanel.add(new Label());
rootPanel.add(new TextBox());

ところがEntryPointクラス中、任意のid値で識別されるdiv要素を取得したい場合、以下のように書けるのだ。
RootPanel rootPanel = RootPanel.get(id値);

つまりはRootPanelはルートという名前ながら実際にはdiv要素のピアとして振る舞う訳で、例えば前回の「div要素を使ったスプラッシュ表示」でHTML上に記述したスプラッシュにコード上で画像を追加する場合は以下のように書ける。

  • AddressBook.html
  <body>
    <div id="loading"><br>...ロードしています</br></div>
  </body>

RootPanel.get("loading").add(new Image("splash.gif"));

つまりはRootPanelクラスを使うことでHTML内のdiv要素全てにアクセスできる訳で、非常に便利だ。