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));

デタッチも同様の処理を行う

ここではアタッチのみについて取り上げたが、これとは逆のデタッチについても同じ処理が必要になる。