GWT:任意のタグウィジェットを作成する方法
GWTには、画面構築に最低限必要と思われるパネル類は備わっており、これは内部的には主にdivタグで表現されている。しかし、内部的に任意のhtmlタグを持つウィジェットは用意されていない。ここでは、任意のhtmlタグを持つウィジェットの作成方法について述べるが、とりあえず利用できるというレベルなので、本当はもっとやることがあるのかもしれない。
最終的な目的
目的としては、プログラム側からul, ol, liタグを持つウィジェットを自由にその場で作り出すことである。適用するスタイルシートの都合上、これらのタグがどうしても必要だった。
しかし、これらのタグを持つGWTのウィジェットというものは存在しない。そこで、専用のウィジェットを作成する必要がある。
基本的なコード
基本的には以下になる。
HtmlList.java
import com.google.gwt.dom.client.*;
import com.google.gwt.user.client.ui.*;
public class HtmlList extends Widget {
private final Element element;
private final Set<ListItem>items = new HashSet<>();
public HtmlList(boolean ordered) {
if (!ordered) element = Document.get().createULElement();
else element = Document.get().createOLElement();
setElement(element);
}
/** 追加するのはListItemのみ */
public void add(ListItem item) {
element.appendChild(item.getElement());
}
}
ListItem.java
public class ListItem extends Widget {
final Element element;
public ListItem() {
element = Document.get().createLIElement();
setElement(element);
}
/** 任意のウィジェットを追加可能 */
public void add(Widget widget) {
element.appendChild(widget.getElement());
}
}
何のことは無い、Widgetを継承し、その中でDocument.get().create**を呼び出してタグを作成し、それをsetElementで設定するだけである。
アタッチされていない問題
しかしここに問題がある。先のHtmlListをGWTのパネル等に付け加えると、このHtmlList自体はイベントを取得することができるが(例えばマウスクリック)、それ以下のウィジェットは全くイベントを受け取れないのである。これらのウィジェットがすべて表示されているにもかかわらずだ。
この理由としては、こうだ。
- element.appendChildによってブラウザ中のDOMツリーは作成されるため、表示はされる。
- しかし、これとは別にGWTのツリーが構成されなければならない。
GWTでは、DOMツリーを構築するほかに、「アタッチ」という作業を行わなければならないのだが、これはツリーのトップから呼び出され、下の階層に伝播される。この例では、HtmlList(ULもしくはLI)のonAttachが呼び出され、最初のHtmlListはアタッチされるのだが、それ以下のListItem等はアタッチされていない。
これがために、それ以下ではイベントを受け取る準備ができていない。どうすればいいのか。
onAttachを呼び出す
HtmlListのonAttachが呼び出されたら、それ以下のListItemのonAttachを呼び出し、さらにListItemに追加されているウィジェットについてのonAttachを呼び出さねばならない。
つまり、HtmlListは以下のようにしないといけない。つまり、追加されたListItemを覚えておき、onAttachが呼び出されたら、これらのListItemについてonAttachを呼び出す。
public class HtmlList extends Widget {
private final Element element;
private final Set<ListItem>items = new HashSet<>();
public HtmlList(boolean ordered) {
if (!ordered) element = Document.get().createULElement();
else element = Document.get().createOLElement();
setElement(element);
}
public void add(ListItem item) {
assert !items.contains(item);
element.appendChild(item.getElement());
items.add(item);
}
/** このメソッドはWidget#onAttach()から呼び出される */
@Override
protected void doAttachChildren() {
items.forEach(item->item.onAttach()); // ※実際にはこの呼出はできない。
}
ListItemも同様である。
public class ListItem extends Widget {
final Element element;
Set<Widget>widgets = new HashSet<>();
public ListItem() {
element = Document.get().createLIElement();
setElement(element);
}
public void add(Widget widget) {
assert !widgets.contains(widget);
element.appendChild(widget.getElement());
widgets.add(widget);
}
/** これはWidget#onAttachから呼び出される。 */
@Override
protected void doAttachChildren() {
widgets.forEach(item->item.onAttach()); // ※実際にはこの呼出はできない。
}
しかし、※の部分は実際にはこのように書けない。onAttachはprotectedメソッドだからである。つまり、Widgetと同じcom.google.gwt.user.client.uiというパッケージのクラスからは呼び出せるが、それ以外のパッケージからアクセスするには、すべてのウィジェットをサブクラス化してonAttach()をpublicにしてしまうしか方法が無い。
GWTではリフレクションは使えないので、このいずれかの条件を満たすしかないのである。
後者の方法ではいかにも面倒だ。前者の方法を使うしかない。
リフレクション無しでパッケージプライベートを突破する方法
リフレクション無しでパッケージプライベートを突破する方法としては、故意にそのパッケージのクラスを作成してしまうことである。つまりこうだ、
package com.google.gwt.user.client.ui; // 故意にこのパッケージにする
public class AttachAccess {
public static void onAttach(Widget w) {
// アクセス可能になる
w.onAttach();
}
public static void onDetach(Widget w) {
// アクセス可能になる
w.onDetach();
}
}
したがって、先のアクセスは以下になる。
items.forEach(item->AttachAccess.onAttach(item));
デタッチも同様の処理を行う
ここではアタッチのみについて取り上げたが、これとは逆のデタッチについても同じ処理が必要になる。