JTableについて
JTableはちょっとした表を作るには非常に便利だ。これを自分で作るのはかなりの負担がかかる。
ただ、高機能に設計されている分、複雑になっていて実装するにもいくつか方法があり、使い方が難しい。
ここでは自分流の使い方を説明する。
サンプルコードだけを見るのではなく、説明まで全部読んで欲しい。
理屈がわかればもっと複雑なものも作れるようになるはずだ。
●TableModelの構造
Swingコンポーネントは全てMVCで設計されているがJTableも例外ではない。
モデルに相当するのはTableModelオブジェクトだ。
これは、次のことを要求される。
・指定セルの値の取得
・指定セルの値の設定
・カラム数の取得
・行数の取得
・指定セルが編集可能か
・リスナーの追加
まあ、表形のデータを操作するにはこのぐらいの機能は必要だ。
最後の2つだけ補足しておく。
「指定セルが編集可能か」
指定セルの値の設定をするのならこの機能は必要になる。もし、読み出し用モデル書き込み用モデルとに分けたら
書き込み用モデルの機能になる。
「リスナーの追加」
モデルの内容に変化があった場合、モデルを参照しているオブジェクトに対し変化したことを報告しなければならない。
これをしないと、モデルの内容と表示内容に食い違いが起こる。
この作業を自動的にできないのかというと、それは無理だ。
どうしてもやるとなれば、表示側は常にモデルをスレッドを使って監視するような仕組みが必要だ。
それは非常に効率が悪い。
さて、TableModelというのはインターフェースだ。インスタンスを作るには実装しなければならないが、
ある程度まで実装してあるクラスが用意されている。
AbstractTableModelだ。
なお、細かいことは気にせず、全てデフォルトでよいのならDefaultTableModelというのもある。
こちらは、すぐにインスタンス化できる。
●レンダラーとエディター
TableModelというのがデータを表しているのはわかったと思う。
次にレンダラーとエディターを説明する。
JTableの表示のおおまかな仕組みは、
1.TableModelからデータのサイズ(縦と横)を取得
2.サイズだけ次を繰り返す
・その中のセルの内容を取得
・セルの内容を表示
となっている。
このときの、「セルの内容を表示」とうことを行うのが、TableCellReandererだ。
つまり、
TableModelに対し「5行目3列目のデータを下さい」といってデータをもらい、
TableCellReandererに対し「5行目3列目にこれを表示してください」という。
モデルとビューが分離したMVCならではの構造だ。
次にエディターだがこちらは、セルの内容を編集する際に起動される。
レンダラーとエディターが別になっているところがポイントで、これによって表示と編集を別のタイプで
行うことができる。
細かい話はサンプルで説明するので、ここでは概略だけしっておけばよい。
●方針
さて、JTableの使い方の方針だが、
・JTableはカスタマイズしない
・TableModelはデザインパターンでいうアダプターとして使う。
・JTableの設定や制御を行うためのメソッド類はTableModelに一緒に詰め込む。
という方針で行く。
3番目に異論があるかもしれないが、自分の経験上これがいいと思う。
というのは、2番目で言っているように、TableModelをアダプターとして使うので、
フィールドに関する情報などをこのオブジェクトで一元管理できるからだ。
アダプターとして使うというのがどういうことなのかを説明しておく。
その前に、そもそもJTableでどんなものを表示するのか。
最初から表型のデータなんてそうそうあるものではない。
JTableで表示するのはレコード群だろう。
つまり、横方向にレコードの項目、縦方向にレコードという形だ。
JTable自体もそういうものを表示することを前提に設計されているような節がある。
レコード群の表示だとすると、もとのデータはレコードのオブジェクトだ。
そのままでは表示できないので、セル位置をレコード・フィールドに変換する。
例えば、5行目2列目と言われたら、5レコード目の「名前」を返すといった具合だ。
ちなみにアダプターでないやり方というと、レコード群をすべて表形のデータに移し替えて表示するということになる。
更新があった場合は、最後に表型のデータをレコード群へ移し替える。
これよりはアダプターの方が美しいしすっきりする。
アダプターとして使うと言うことは、ここで座標とフィールドを結びつけるわけで、
それらの情報が存在している。それならば他の情報、例えば列名や列の幅などもここで管理した方が
わかりやすい。
というわけで、TableModelオブジェクトで一元管理するのをお勧めする。
●前提条件
今回のサンプルとしては、同好会メンバーの一覧を表示するということにする。
つまり、他のコードで作られたClubMemberオブジェクトがjava.util.ArrayListに格納されている。
ClubMemberオブジェクトは次の通り。
/** 同好会メンバークラス
*/
class ClubMember
{
public static final int TYPE_NORMAL = 1;
public static final int TYPE_SPECIAL = 2;
/**会員番号
*/
public int no;
/**会員氏名
*/
public String name;
/**会員種別
*/
public int type;
/**会員住所
*/
public String address;
}
●コード
まずは、レコード群とアダプターを結びつけるコードを作る。
作るときは、AbstractTableModelをベースにする。
ほとんどの場合レコード群はコンテナに格納されているはずだが、このコンテナの種類によって少し替える必要がある。
java.util.Listのようにランダムアクセスのできるものであれば、コンテナをそのままアダプターにセットするようにすればいい。
しかし、Iteratorのようなものだと、Listオブジェクトに格納し直す必要がある。
java.util.Listの場合
public void setContainer(List list){
this.list = list;
fireTableAllUpdated();
}
Iteratorの場合
public void setContainer(Iterator it){
this.list = new ArrayList();
while(it.hasNext()){
list.add(it.next());
}
fireTableAllUpdated();
}
今回はjava.util.Listに格納されているという前提なので前者でよい。
●TableModelの実装
さてメインの部分だ。
・指定セルの値の取得
・指定セルの値の設定
・カラム数の取得
・行数の取得
は絶対に実装する必要がある。
「行数の取得」はList#size()をそのまま返せばよい。
public int getRowCount(){
return list.size();
}
「カラム数の取得」だが、クラス内に定数を定義して返すようにすること。
カラム数が増えたときなどにバグを防げる。
public static finalint COLUMN_COUNT = 4;
public int getColumnCount(){
return COLUMN_COUNT;
}
「指定セルの値の取得」、「指定セルの値の設定」だがこれもカラム位置を定数定義しておく。
public static final int COL_NO = 0;
public static final int COL_NAME = 1;
public static final int COL_TYPE = 2;
public static final int COL_ADDRESS = 3;
public Object getValueAt(int row, int col){
ClubMember mem = (ClubMember)list.get(row);
switch(col){
case COL_NO: return new Integer(mem.no);
case COL_NAME: return mem.name;
case COL_TYPE: return new Integer(mem.type);
case COL_ADDRESS: return mem.address;
}
return "";
}
ここでポイントは会員種別だ。もしこの項目がJTableから変更されることがないのなら、
typeに応じた文字列を返してもよい。
この要領で、設定も作ってしまおう。
public void setValueAt(int row, int col, Object value){
ClubMember mem = (ClubMember)list.get(row);
switch(col){
case COL_NO:
mem.no = ((Integer)value).intValue();
return;
case COL_NAME:
mem.name = (String)value;
return;
case COL_TYPE:
mem.type = ((Integer)value).intValue:
return;
case COL_ADDRESS:
mem.address = (String)value;
return;
}
}
さて、これだけでも一応は表示される。しかし、会員種別が数字で表示されるのはイマイチだ。
会員種別を数字ではなく、文字で表示されるようにしよう。
数値のデータを文字列に変換する必要がある。
こういうのは、いろいろと使う可能性があるので、ClubMemberクラスのクラスメソッドとして定義しておく。
<ClubMember.javaの一部>
-------------------------------
static public String GetMemberTypeString(int type){
if(type == TYPE_NORMAL){
return "通常会員";
}else{
return "特別会員";
}
}
-------------------------------
これを、レンダラーで使う。レンダラーはTableModelオブジェクトに実装してしまう。
class CMemTableModel
extends AbstractTableModel
implements TableCellRenderer
{
....
....
public Component getRendererComponent(JTable tbl, Object value, int row, int col. boolean isSelected, boolean hasFocus){
}
}
さてここでTableCellRendererについて説明しなければならない。
TableCellRendererはインターフェースだ。何を要求されるかというと、
「指定されたセルの内容を適切な形で表示できるような状態にあるComponentオブジェクトを返す」
ということだ。
もう少しわかりやすく説明すると、
「5行目3列目のデータがtrue(Boolean)ですが表示してください」
と言われるので、それに合ったComponentオブジェクトを返す。
このとき文字列で'true'とするのか、チェックボックスをオンにして返すのか
具体的な表示方法はこのレンダラーが決める。
逆にいえばどんな表示方法も可能だということだ。
文字列で表示するのならJLabelに文字をセットして返せばいいし、チェックボックスなら
JCheckBoxをセレクト状態にして返せばいい。
ここで返したComponentオブジェクトをJTableはpaintメソッドを使って内容を表示する。
つまり、ゴム印のような使い方をするだけだ。
そのため、同じインスタンスを使い回しても全く問題ない。
引数について説明しておこう。
JTableは呼び出し元のJTableオブジェクトだ。
色の情報などはこれを使って取得できる。
valueは表示するセルの内容。
rowとcolは表示するセルの位置だ。
isSelectedはこのセルが選択されているかを示す。
hasFocusはこのセルがフォーカス状態にあるかを示す。
さて今回の場合は、「会員種別」が特別なだけで他の項目はデフォルトにしてしまう。
デフォルトで表示するために、DefaultTableCellRendererというのを使おう。これはJLabelをカスタマイズしたものだ。
つまり、表示対象カラムが会員種別でなければDefaultTableCellRendererにすべて任せてしまう。
if(col != COL_TYPE){
Component comp = defCellRenderer.getRendererComponent(tbl, value, row, col, isSelected, hasFocus);
return comp;
}
としておけばよい。
defCellRendererというのは、DefaultTableCellRendererのインスタンスだ。
毎回インスタンスを生成するのは効率が悪いのでインスタンス変数として持たせている。
さて会員種別のときだが、コードを文字列に変換し、その文字列をdefCellRendererに渡してしまおう。
int type = ((Integer)value).intValue();
String typeString = ClubMember.GetMemberTypeString(type);
Component comp = defCellRenderer.getRendererComponent(tbl, typeString, row, col, isSelected, hasFocus);
return comp;
これでTableCellRendererの実装はできた。
整理を兼ねてここまでのコードを全部示しておく。
class CMemTableModel
extends AbstractTableModel
implements TableCellRenderer
{
public static finalint COLUMN_COUNT = 4;
public static final int COL_NO = 0;
public static final int COL_NAME = 1;
public static final int COL_TYPE = 2;
public static final int COL_ADDRESS = 3;
ArrayList list = new ArrayList();
DefaultTableCellRenderer defCellRenderer = new DefaultTableCellRenderer();
public void setContainer(List list){
this.list = list;
fireTableAllUpdated();
}
public int getRowCount(){
return list.size();
}
public int getColumnCount(){
return COLUMN_COUNT;
}
public Object getValueAt(int row, int col){
ClubMember mem = (ClubMember)list.get(row);
switch(col){
case COL_NO: return new Integer(mem.no);
case COL_NAME: return mem.name;
case COL_TYPE: return new Integer(mem.type);
case COL_ADDRESS: return mem.address;
}
return "";
}
public void setValueAt(int row, int col, Object value){
ClubMember mem = (ClubMember)list.get(row);
switch(col){
case COL_NO:
mem.no = ((Integer)value).intValue();
return;
case COL_NAME:
mem.name = (String)value;
return;
case COL_TYPE:
mem.type = ((Integer)value).intValue:
return;
case COL_ADDRESS:
mem.address = (String)value;
return;
}
}
public Component getRendererComponent(JTable tbl, Object value, int row, int col. boolean isSelected, boolean hasFocus){
if(col != COL_TYPE){
Component comp = defCellRenderer.getRendererComponent(tbl, value, row, col, isSelected, hasFocus);
return comp;
}
int type = ((Integer)value).intValue();
String typeString = ClubMember.GetMemberTypeString(type);
Component comp = defCellRenderer.getRendererComponent(tbl, typeString, row, col, isSelected, hasFocus);
return comp;
}
}