Locked History Actions

GWT/Widget/FlexTable

FlexTable

レイアウト上のバグとの対策

現象

FlexTableには非常に大きなバグがある。しかし、FlexTableの構造上、不可避のものなので「仕様」と言ってもよいかもしれない。

例えば以下のコードを実行すると、Bはなぜか2列目にはならず、三列目になってしまう。 つまり、(0,0)位置のセルのRowSpanを2にすると、二行目の列位置がずれてしまう。

    FlexTable table = view.getFlexTable();    
    table.setText(0, 0, "X");
    table.getFlexCellFormatter().setRowSpan(0,  0, 2);    
    table.setText(0,  1, "A");
    table.setText(1,  1, "B");

以下のように、後から(0,0)位置のセルを入れても同じ現象になる。

    FlexTable table = view.getFlexTable();    
    table.setText(0,  1, "A");
    table.setText(1,  1, "B");
    table.setText(0, 0, "X");
    table.getFlexCellFormatter().setRowSpan(0,  0, 2);     

原因

これは、実際に生成されるtableタグでの解釈と、FlexTableでのセル位置指定にズレがあるからである。 例えば、上の例の場合、いずれも

<tr>
 <td rowspan="2">X</td>
 <td>A</td>
</tr>
<tr>
  <td></td>
  <td><B</td>
</tr>

というタグが生成される。FlexTable側としては指示された場所にデータを入れているつもりだろうが、 tableタグの解釈としては、これはプログラマの意図とは異なるものになってしまう。 つまり、AとBは同じ列にはならない。

対策

FlexTableをそのまま使う上での対策としては、(この例の場合では)プログラマ側が、どの列が一つ多くなっているかを認識し、その列より右側の列位置を指定する再に、列インデックスを減産するというものである。しかしこれはいかにも面倒であろう。

もう一つの対策としては、FlexTableを継承した新たなクラスを作成し、RowSpanあるいはColSpanが指定された場合には、それがどの列に影響を及ぼすかを保持しておき、プログラマ側から示されたセル位置指定を自動的に補正するというものである。

ただし、この場合、上の例の二つ目で示したように、「後から左側のセルのデータを入れ、そのRowSpanを指定した場合には、結局ずれてしまう」という問題を解決しなければならない。

つまり、この問題を根本的に解決するには、現在のFlexTableが行なっているような、「setText等のメソッドがよびだされたら、その場でDOM操作を行なってタグを作成する」を改め、いったん内部的なバッファにデータ指定をため込み、最後にその位置を調整しながらタグを作成するといったことを行わなければならない。

次のようなクラスを書いてみる。

import java.util.*;

import com.google.gwt.user.client.ui.*;
import com.google.gwt.user.client.ui.FlexTable.FlexCellFormatter;

/**
 * FlexTableビルダ
 */
public class FlexTableBuilder {

  private static final boolean DEBUG = true;
  
  public abstract static class Room {    
    
    final int row;
    final int col;
    final int rowSpan;
    final int colSpan;    
    
    protected Room(int row, int col, int rowSpan, int colSpan) {
      this.row = row;
      this.col = col;
      this.rowSpan = rowSpan;
      this.colSpan = colSpan;
    }

    /** FlexTable上の指定位置のセルに、このデータを設定する */
    abstract void setDataToFlexTable(FlexTable flexTable, int htmlRow, int htmlCol);
    
    /** FlexTable上の指定位置のセルにrowSpan/colSpanを設定する */
    void setSpansToFlexTable(FlexTable flexTable, int htmlRow, int htmlCol) {
      FlexCellFormatter formatter = flexTable.getFlexCellFormatter();
      if (rowSpan > 1) {
        formatter.setRowSpan(htmlRow,  htmlCol, rowSpan);
      }
      if (colSpan > 1) {
        formatter.setColSpan(htmlRow,  htmlCol, colSpan);
      }
    }
  }

  /** Text用のセルデータ */
  public static class TextRoom extends Room {
    
    /** セルデータテキスト */
    String text;
    
    /** 表示上の行・列とデータを指定 */
    public TextRoom(int row, int col, String text) {
      this(row, col, text, 1, 1);
    }
    
    /** 表示上の行・列とデータ、rowSpan/colSpanを指定 */
    public TextRoom(int row, int col, String text, int rowSpan, int colSpan) {
      super(row, col, rowSpan, colSpan);
      this.text = text;
    }
    
    /** FlexTable上の指定位置のセルに、このテキストデータを設定する */
    @Override
    void setDataToFlexTable(FlexTable flexTable, int htmlRow, int htmlCol) {
      flexTable.setText(htmlRow, htmlCol, text);
    }

    /** デバッグ用 */
    @Override
    public String toString() {
      return "(" + row + "," + col + ") " + text + " (" + rowSpan + "," + colSpan + ")";
    }
  }
  
  private List<List<Room>>rows = new ArrayList<List<Room>>();

  /** Roomを設定する */
  public void set(Room room) {
    int rowStart = room.row;
    int rowEnd = room.row + room.rowSpan - 1;
    int colStart = room.col;
    int colEnd = room.col + room.colSpan - 1;
    
    // 必要な行をすべて確保する。ただし、列データはnullでもよい
    while (rows.size() <= rowEnd) rows.add(null);    
    
    // 指定された行の指定された列にcellを格納する。
    for (int row = rowStart; row <= rowEnd; row++) {
      List<Room>cols = rows.get(row);
      if (cols == null) {
        cols = new ArrayList<Room>();
        rows.set(row,  cols);
      }
     
      // 必要な列をすべて確保する。ただし、セルデータはnullでもよい。
      while (cols.size() <= colEnd) cols.add(null);
      
      // 指定された位置にCellを格納する
      // ただし、先客のいる場合は例外にする。
      for (int col = colStart; col <= colEnd; col++) {
        if (cols.get(col) != null) throw new RuntimeException();
        cols.set(col, room);
      }
    }
  }
  
  /** 
   * FlexTableの内容を一旦クリアし、
   * このビルダで指定されたすべてのセルを設定する。 */
  public void build(FlexTable flexTable) {
    flexTable.clear();  
    
    for (int row = 0; row < rows.size(); row++) {
      List<Room>cols = rows.get(row);
      
      // 列データがなければ何もしなくてよい。
      if (cols == null) continue;
      
      // これはhtmlタグ上の位置。列データ内容によって進み方が変わる。
      int htmlCol = 0;
      
      // 各列について処理
      for (int col = 0; col < cols.size(); col++) {
        Room room = cols.get(col);
        
        // この表示行・列にセルデータが無い場合、html上の列は進める。
        if (room == null) {
          htmlCol++;
          continue;
        }

        // この表示行・列にデータはあるが、それは他の表示行・列のrowSpanあるいはcolSpan
        // html上の列は進めない。
        if (room.row != row || room.col != col) {
          continue;
        }

        // この表示行・列にデータがある。
        room.setDataToFlexTable(flexTable, row, htmlCol);
        room.setSpansToFlexTable(flexTable, row, htmlCol);
        htmlCol++;
      }
    }
  }
  
  @Override
  public String toString() {
    StringBuilder b = new StringBuilder();
    for (int row = 0; row < rows.size(); row++) {
      b.append("row:" + row + "\n");
      List<Room>builderCols = rows.get(row);
      for (int col = 0; col < builderCols.size(); col++) {
        b.append(" " + builderCols.get(col) + "\n");
      }
    }
    return b.toString();
  }
}

使い方は以下のとおり

    FlexTable table = view.getFlexTable();    
    table.setBorderWidth(1);
    
    FlexTableBuilder builder = new FlexTableBuilder();
    
    builder.set(new FlexTableBuilder.TextRoom(0, 0, "A", 2, 1));
    builder.set(new FlexTableBuilder.TextRoom(0, 1, "B", 1, 2));
    builder.set(new FlexTableBuilder.TextRoom(1, 2, "C", 2, 1));
    builder.set(new FlexTableBuilder.TextRoom(2, 0, "D", 1, 2));
    builder.set(new FlexTableBuilder.TextRoom(1, 1, "E", 1, 1));