し上を行く JTable を表示したい,の巻
2002/07/23 新規
2003/11/14 追記

「ある数値群の一覧表をユーザに提示するとき、どんなコンポーネントを使う?」 と言われたら、「 JTable クラス が良いんじゃないか」と答える人は多いはず。

けれども、この JTable の機能。 無駄に「カラム(列)の入れ替え」なんていう、高度なくせに普段あまり使われないモノが実装されているにも関わらず。 それでいて実用度抜群のソートは、一切 用意されてない困りもの。
ちなみに、ここでいうソートというのは。 テーブル・ヘッダにある任意のカラムを指定すると、その該当するカラムの値で順位付けをしてロー(行)が並び替えられるっていう感じ。 文章だとわかりづらい?そんな人は

内容(順番)がヘコヘコ変わっている有り様を観察すればOK。 それのコト。

つまり。 今回の「少し上を行く JTable を表示したい」は、ソート(行の並び替え)機能を持ったクールな JTable を実現しようというのが趣旨です。

 

そして、さっそく実践へ。
ちなみに今回はサンプル・ソースの公開は無しです。もったいぶってます。 だから「実践」とは言いつつも、大まかな概念と各種ポイントだけです。

まずは、JTable のヘッダ。 こいつがクリックされたかどうかを検知する仕組みが必要です。 マウス・クリックのイベントを捕まえるには MouseListener を登録しますが、それを JTable に登録しちゃうのはダメです。 一覧表のヘッダ部とデータ部は分離されているので、正解は JTableHeader のほうへリスナを登録します。
こんな感じになります。
例1

sampleTable=new JTable();
sampleTable.getTableHeader().addMouseListener(
  new MouseAdapter() {
    public void mouseClicked(MouseEvent e) {
      // ほにゃららぷ〜
      sampleTable.revalidate();
    }
  }
);
わざわざ専用クラスを作るのもメンドイので、リスナには匿名内部クラスを使ってます。 もちろん「ほにゃららぷ〜」の箇所には、実際の動作を書きましょう。

ほにゃららぷ〜」の動作は

です。 クリックされた時点でのマウス位置から、カラムの位置を割り出します。
例2

sampleTable=new JTable();
sampleTable.getTableHeader().addMouseListener(
  new MouseAdapter() {
    public void mouseClicked(MouseEvent e) {
      int columnIndex=sampleTable.getTableHeader().columnAtPoint(e.getPoint());
      // はらほろひれはれ
      sampleTable.revalidate();
    }
  }
);
さっきの例に対して、指定されたカラムを判別する動作を追加してみました。
MouseEvent のインスタンスから座標を取得し、JTableHeader のメソッド columnAtPoint() でカラム番号を得ます。 「はらほろひれはれ」の箇所は、実際にソートを実行するメソッドの呼び出しなどを記述しましょう。 なお、初めの例から書いてある「sampleTable.revalidate();」は、表示の更新だと思ってください。

さて、肝心の「実際にソートを実行する機構」ですが。

MVC (Model - View - Controller)の概念に基づいて、JTable は表示部とデータ保持部が分離しています。 表示する View 部とデータを保持する Model 部の間に1つフィルタを挟んで、そこでソートを行うのがスマートな方法でしょう。 こうすると実データは一切いぢらないので、きっと恐らく良い感じに効率的です。

なんちゃって概念図

自分はめんどくさがりなので、ソートを行う部分も含んだ TableModel を作ってしまいました。 もちろん DefaultTableModel クラスから継承しているので、基本動作部分もバッチリです。
当然ですが、Model 部にソート機能を盛り込んだといっても、実データの入れ替えはしていません。 ソートに際して、実データをいぢくり廻すのはNGです。

「ロー:2でカラム:3の位置にあるデータをくれ」と要求されたとき、このローに対する変換テーブルを新たに持ちます。 ソートによって変動するのは、この変換テーブルの値ということになります。 このテーブルで「ロー:2に対応する実データの行番号は5だ」というような変換がなされ、 実データで5行3列目にある内容がユーザには2行3列目に表示される仕組みです。

自分なりの TableModel を作成するとき、実装が強制されるのは以下の3つのメソッドです。

このことから、View 部である JTable から「y行x列目にあるデータをよこせ」と要求があったとき、 呼び出されるのはメソッド getValueAt() です。 このメソッドの中で、変換テーブルを活用するようにします。

 

変換テーブルを活用するのは良いとして、実はまだソート自体を行っていません。 「データをよこせ」要求があるたびにソートを行っていては、かなり非効率的でダメダメなことは一目瞭然です。 ソートを行うタイミングは、先の例で示した「はらほろひれはれ」の位置となります。 ユーザが指定したカラムの番号を確保したら、「その番号を基にソートしろ」という指示を出しましょう。

以上で、ソート機能付きテーブルを実現させる手法の説明はおしまいです。

・・・で。
これで終わりと思いきや、実はそのまま実装するとアレが悪さをしてくれます。 アレというのは、初期装備のムダな機能「カラムの入れ替え」です。

カラム番号を取得する JTableHeader のメソッド columnAtPoint() は、とてもクソ真面目にインデクスを返してくれます。 ユーザがグリグリとカラムの入れ替えを行ったところで、マウス座標から算出したカラム番号しか取得できません。
つまり元のカラムが「AAA−BBB−CCC」という並びで、ユーザが「BBB−CCC−AAA」と入れ替えたとします。 そこで「BBB」をクリックしたとき、もちろん期待する挙動は「BBB」の値を基にしたソートですが。 実際は「BBB」の見た目位置が0番目にあるため、「AAA」の値で並び替えが起こってしまいます。

これを抑制するには、JTableHeader のメソッド setReorderingAllowed() で入れ替え不可設定にしましょう。 これで万事OKということになります。 というのは、もちろんウソです。

せっかく装備されている機能は、とことん活用するのがスジです。
「View 部での変更が Model 部には影響が及ばない」というのであれば、その変更されている View 部から値を引っ張ってきましょう。
例3

sampleTable=new JTable();
sampleTable.getTableHeader().addMouseListener(
  new MouseAdapter() {
    public void mouseClicked(MouseEvent e) {
      int columnIndex=sampleTable.getTableHeader().columnAtPoint(e.getPoint());
      String columnName=sampleTable.getColumnName(columnIndex);
      // はらほろひれはれ
      sampleTable.revalidate();
    }
  }
);
JTable のメソッド getColumnName() で、ユーザに見えてるカラム名を取得しています。 このカラム名がある列の値でソートを行うようにしましょう。 そうすれば「BBB−CCC−AAA」となっていて0番目の「BBB」をクリックしても、「BBB」で並び替えるよう指示されます。 これで見た目の対象とソート対象が一致して、ホントに万事OKです。

 

ココまで書いて、な〜んか思いついたコト。
ユーザが指定したカラム名と一致するカラムの値でソート、ってことは。 もし全く同じカラム名があった場合、実装法にもよるけど、初めに一致したモノがいつも対象となるわけで。 さてさて、それはどうしたものだろう。

DefaultTableColumnModel のメソッド getColumnIndex() では

equals を使って比較したときに識別子が identifier と等しい、 tableColumns 配列にある最初の列のインデックスを返します。
という記述があるけど、それで良い? ひとつのテーブルで同じ項目(=カラム名)が存在するのは変、だから良い?良いか。


2003/10/03 に、TAMURA さんよりメールをいただきました。
Musi_chan

int columnIndex=sampleTable.getTableHeader().columnAtPoint(e.getPoint());
String columnName=sampleTable.getColumnName(columnIndex);
自分は上記の方法(カラム名を取得し、その名前の列でソート)を取っていますが、 TAMURA さんは次のような手法を示してくださいました。
TAMURA

int aColIndex = aHeader.columnAtPoint(inEvent.getPoint());
aColIndex = aHeader.getColumnModel().getColumn(aColIndex).getModelIndex();
このようにモデル・データの実際のインデクスを取得してソートすることで、 名前のバッティングを気にしなくても済むということです。

とても勉強になります。ありがとうございました!


戻る