2012年3月16日金曜日

java : JTableの列幅変更で最終列だけに調整を適用する

ユーザーがJTableの列をドラッグしてサイズ変更するときの振る舞いについて。 最終列だけに調整を適用するにはAutoResizeModeプロパティにAUTO_RESIZE_LAST_COLUMNを設定すればOK。 ただ、これだけではこちらの望んだ振る舞いにはなりませんでした。 JTableのサイズが変更された場合、全部の列が拡大縮小されてしまうんですよね。 なんでこんな仕様になっているのやら...

仕方がないのでJTableを継承してホントにAUTO_RESIZE_LAST_COLUMNになるコンポーネントを作成しました。 基本方針は、

  • TableHeader上にマウスカーソルがあるとき列幅が変更可。
  • それ以外の場合は列幅は固定。
  • テーブルの幅が縮められたときは列幅の自動調整だけでは吸収できず、テーブルがコンテナからはみ出してしまうことがあるけど、それは仕様。

列幅の固定は、TableColumnのMinWidthとMaxWidthを現在の幅にすることで実現です。 で、↑の様なコードだけだとTableHeaderからマウスが離れた瞬間に列幅が変更できなくなってしまいました。 ドラッグ中のマウスの扱いについても考えなくてはならないようですね。

色々やってそんなこんなで、こういうコードになりました。

import java.awt.Rectangle;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import javax.swing.JTable;
import javax.swing.table.TableColumn;
import javax.swing.table.TableColumnModel;

public class TestTable extends JTable
{
    public static final int MINIMUM_COLMUN_WIDTH = 48;

    public TestTable(int numRows, int numColumns)
    {
        super(numRows, numColumns);

        TableColumnModel colModel = getColumnModel();
        int colCount = getColumnCount();
        for(int i = 0; i < colCount; i++)
        {
            TableColumn col = colModel.getColumn(i);
            col.setMinWidth(MINIMUM_COLMUN_WIDTH);
        }

        getTableHeader().addMouseListener(_headerMouseListener);
        setAutoResizeMode(JTable.AUTO_RESIZE_LAST_COLUMN);
        fixColumnWidth(true);
    }

    private MouseListener _headerMouseListener = new MouseAdapter()
    {
        // マウスカーソルがTableHeader上にあるときだけ列幅の変更ができる
        @Override public void mouseEntered(MouseEvent event)
        {
            fixColumnWidth(false);
        }

        @Override public void mouseExited(MouseEvent event)
        {
            // マウスカーソルがTableHeaderから出たらfix。
            // ドラッグ中はリサイズしててもしてなくてもfixしない。
            // マウスカーソルがTableHeaderの矩形上にあるのにmouseExitedが
            // 発行されたときは、別のコンポーネントやウィンドウに乗ったときなのでfix。
            if(!isColumnResizing() || isOnHeader(event) )
                fixColumnWidth(true);
        }

        @Override public void mouseReleased(MouseEvent event)
        {
            // マウスドラッグ中にTableHeader以外の場所でドラッグが終了したらfix。
            // マウスカーソルがTableHeader上にある場合はここではfixせず、mouseExitedでfix。
            // そうしないとクリックしただけでもfixされてしまう。
            if(!isOnHeader(event) )
                fixColumnWidth(true);
        }
    };

    private boolean isColumnResizing()
    {
        return getTableHeader().getResizingColumn() != null;
    }

    // マウスの座標がTableHeaderの上にあるか?
    // 注) 別ウィンドウの下にあっても座標さえあっていればtrueを返す。
    private boolean isOnHeader(MouseEvent event)
    {
        Rectangle r = getTableHeader().getBounds();
        return r.contains(event.getPoint() );
    }

    // 最後の列以外の列幅を固定する。
    private void fixColumnWidth(boolean fix)
    {
        TableColumnModel colModel = getColumnModel();
        int targetNum = getColumnCount() - 1; // 最後の列は除く
        if(fix)
        {
            for(int i = 0; i < targetNum; i++)
            {
                TableColumn col = colModel.getColumn(i);
                int width = col.getWidth();
                col.setMinWidth(width);
                col.setMaxWidth(width);
            }
        }
        else
        {
            for(int i = 0; i < targetNum; i++)
            {
                TableColumn col = colModel.getColumn(i);
                col.setMinWidth(MINIMUM_COLMUN_WIDTH);
                col.setMaxWidth(Integer.MAX_VALUE); // AUTO_RESIZE_LAST_COLUMNと組み合わせるのでMAX_VALUEでよい
            }
        }
    }
}

簡単なサンプルと言うことで、コンストラクタは1つしか継承していません。 あと、これを使ったコードで列幅を変更するときは一時的にfixColumnWidth(false)をしなければならないので注意。

fixColumnWidth(false);
列幅変更コード
fixColumnWidth(true);

とりあえずこんなコードで軽く動作チェックしました。 簡単なチェックしかしていないので不具合が残ってるかもしれません。

import javax.swing.JFrame;
import javax.swing.JScrollPane;

public class AutoResizeLastColumnTableTest
{
    public static void main(String[] args)
    {
        JFrame frame = new JFrame();
        JScrollPane scrollPane = new JScrollPane(new TestTable(16, 4) );
        frame.getContentPane().add(scrollPane);
        frame.setSize(640, 480);
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

        frame.setVisible(true);
    }
}