Javaの手帖 − GUI分冊
先日、Javaプログラミングの豆記事を載せる/見る場所を一本化するた
めに、[Javaの手帖]と題するページを新設しました。しかしGUI関連の記
事をそのページに載せていくのは、ためらいがありました。JavaのGUIプ
ログラミングは、そのたった一つの分野だけで、細かい話題が無限に(?!)
豊富にあるからです。
そこで、GUIに関しては、このような別ページを作りました。
この分野の、情報量の圧倒的な豊富さに対して、個人の細腕はまるでウ
サギに対するカメですが、重要と思われる事例をわかりやすく紹介してい
きたいと思います。
なお、JavaのGUIプログラムを書くための必読の文献の一つといえる
[Painting in AWT and Swing]の日本語訳、[AWTとSwingのペイントの仕組み]を、
2008年6月に作成しました。
0005(061125)
テーブルのカラムの幅を最多文字数の幅に合わせる(JTable)
[概要]
JTableのカラムの幅はデフォルトで一律75ピクセルなので、文字数の多い
カラムはマウスで幅を広げて見る必要があります。そこで、最初から最多文
字数のカラムに合わせた幅になるテーブルを表示させましょう。
width = 0;
cc = table.getColumnCount(); //カラム数
rc = table.getRowCount(); //行数
for (int c = 0; c < cc; ++c){
maxcw = 0;
for (int r = 0; r < rc; ++r){ //各欄のコンポーネントを見て
Component compo
= table.prepareRenderer(table.getCellRenderer(r, c), r, c);
int w = compo.getPreferredSize().width;
if (w > maxcw){
maxcw = w; // 現在の最大幅を調べる
}
}
maxcw += 7; // ややゆとりを与える
table.getColumn(table.getColumnName(c)).setPreferredWidth(maxcw);
width += maxcw; // テーブルの全幅
}
height = (table.getRowCount() + 1) * table.getRowHeight();//+1はヘッダぶん
js.setPreferredSize(new Dimension(width, height));
// jsはtableを収めるJScrollPane
テーブルヘッダ(カラムヘッダ)の幅も考慮に入れたいときは、こうなり
ます:
Component compo
= table.prepareRenderer(table.getCellRenderer(r, c), r, c);
int w = compo.getPreferredSize().width;
if (w > maxcw){
maxcw = w;
}
int hw = table.getTableHeader().getHeaderRect(c).width;
if (hw > maxcw){
maxcw = hw; //カラムヘッダの幅のほうが大きかった
}
0005(061120)
テーブルのセルをアニメーションにする(JTable)
[概要]
JTableのセルレンダラが描くコンポーネントは本当のコンポーネントではなく、
写真のようなもの(単なる静止画像)です。そこで、アニメーションするコンポ
ーネントをそのままセルに載せてもアニメは実現しません。アニメのコマが変わ
るたびに、目的のセルを強制的にリペイントする必要があります。
以下は、そういう強制リペイントを行っているサンプルプログラムです。
import javax.swing.*;
import javax.swing.table.*;
import java.awt.*;
import java.awt.event.*;
public class AnimeTableCell{
JFrame frame;
Container con;
JScrollPane scp;
JTable table;
AnimePanel anp, ant;
Image[] images;
int testRow = 3;
int testCol = 2;
public AnimeTableCell(){
frame = new JFrame();
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
con = frame.getContentPane();
images = new Image[4];
images[0] = new ImageIcon("images/a.png").getImage();
images[1] = new ImageIcon("images/b.png").getImage();
images[2] = new ImageIcon("images/c.png").getImage();
images[3] = new ImageIcon("images/d.png").getImage();
table = new JTable(10, 6){
public Component prepareRenderer
(TableCellRenderer renderer, int row, int column){
Component c = super.prepareRenderer(renderer, row, column);
if (row == testRow && column == testCol){
c = ant;
}
return c;
}
};
scp = new JScrollPane(table);
con.add(scp, BorderLayout.CENTER);
// セルレンダラ用にはリペイントを強制
ant = new AnimePanel(images, 500, true);
// 通常表示では何もリペイントしない
anp = new AnimePanel(images, 500, false);
con.add(anp, BorderLayout.SOUTH);
frame.setSize(400, 400);
frame.setVisible(true);
}
class AnimePanel extends JPanel implements ActionListener{
Image[] imgs;
int delay;
boolean trepaint;
int range;
int idx;
Timer timer;
AbstractTableModel atb;
public AnimePanel(Image[] ima, int de, boolean r){
setPreferredSize
(new Dimension(ima[0].getWidth(this), ima[0].getHeight(this)));
imgs = ima;
delay = de;
trepaint = r;
range = ima.length - 1;
idx = 0;
atb = (AbstractTableModel)(table.getModel());
timer = new Timer(delay, this);
timer.start();
}
public void actionPerformed(ActionEvent e){
++idx;
if (idx > range){
idx = 0;
}
repaint();
if (trepaint){ // テーブルのセルをリペイントする
atb.fireTableCellUpdated(3, 2);
}
}
public void paintComponent(Graphics g){
super.paintComponent(g);
g.drawImage(imgs[idx], 0, 0, this);
}
}
public static void main(String[] args){
new AnimeTableCell();
}
}
0004(060601)
プログラムからのテーブルのセル・セレクション(JTable)
話のネタはたくさんありますが、なかなかオリジナルの記事を書く時間的余裕
ができません。今回は5月の私の投稿から、比較的おもしろいものをここにリン
クします。[このスレッド]です。
[概要]
JTableは、プログラムからの行(row)と列(column)のセレクションはクラスの
メソッドを使って単純明快にできます。しかし個々の欄(cell, セル)の、それも
複数のあちこちのセルのプログラムからのセレクションはうまく行きません。こ
のフォーラム・スレッドでは、JTableのisCellSelected()メソッドをオーバライ
ドすることによって、所期の結果を得ています。
質問者はその後、カラムをソートしたあとにセレクションがリセットされる、
と問題を訴えます。私はその解決を示したあと、彼のプログラムがセル・セレク
ションではなくふつうのロウ・セレクションをやってることに気づき、最後に、
JTableのサブクラスを作らない平凡なプログラムを提示しました。
0003(060405)
テーブルに関する今後の記事(JTable)
JTableはなにしろ、話のネタが尽きません。今後は、こんな話題を考え
ています。
- 浮動カラム(freezed column)を作る。
- 行ヘッダ(row header)を作る。
- PgDwn/Upキーによるハーフページスクロール。
- カラムの幅を文字数の多い行に合わせる。
- データベースとJTableを連動させるための定石。
- なにしろカラフルにする。
- --まだまだいろいろ--
0002(060405)
テーブルカラムのグループ化(JTable)
【概要】JTable篇の第二弾として、こんなテーブルヘッダの作り方を考え
ます。
ここに解説文を書く代わりに、ソースファイルのコメントを充実させ
ました。なるべく、以下の順序で見てください(文字コードがUTF-8です
から注意してください--ブラウザの文字コードメニューを操作してくだ
さい。)。
GroupableHeaderExample.java--テストアプリケーションです。
ColumnGroup.java--複数のカラムを束ねたカラムグループとその操作を論理的に定義します。
GroupableTableHeader.java--カラムグループのあるテーブルのためのテーブルヘッダです。
GroupableTableHeaderUI.java--GroupableTableHeaderを画面に描画するUI(ユーザインタフェイス)クラスです。
なお、ソースの現状はJDK 1.5対応になっています。1.4でコンパイルす
るためには、List<Object>などの記法を、単なるListに書き換えて
ください。
0001(060308)
[Enter]キーでテーブルの次のカラムへ移動したい(JTable)
【概要】javax.swing.JTableクラスは、ご存じのとおりテーブル(表)を
作るGUIコンポーネントです。このコンポーネントのデフォルトの設定で
は、[TAB] キーが次のカラムへ行くキー、そして[Enter] キーは次の行へ
行くキーになっています。このデフォルトの設定では、ある行の各カラム
の編集がすむたびに[TAB] キーを押して次のカラムへ移動し、途中のカラ
ムから次の行へ移動したいときに[Enter] キーを押します。ところが、各
カラムの編集、つまり語句の挿入などを終わると、バーン!と[Enter] キ
ーを押したい人が世の中には多いと思います。そこで、[Enter] キーで次
のカラム、具体的には“次の編集可能なカラム”へ移動するにはどうする
か?。この記事は、SwingのGUIコンポーネントで普遍的に重要な、InputMap
とActionMapの設定の仕方のサンプルとして見ていただくと、今後の利用
価値が大きいと思います。
テーブル(JTable)とツリー(JTree)は、複雑で、プログラミングの話題も非
常に豊富ですから、まずここしばらくはテーブルのお話を連載したいと思い
ます。今回は、その初回です。
Swingのコンポーネント(JComponentの子孫たち)は、内部にInputMapオブ
ジェクトとActionMapオブジェクトを持っています(ただしどちらも、
java.util.Mapオブジェクトではありません)。InputMapは、キーの種類とア
クションの名前をマップ(対照)しています。たとえば、あるコンポーネン
トでは、[TAB] キーが"movefocus" という名前に結びついているかもしれま
せん。[F1]キーは"help"という名前に結びついているかもしれませ。キーは、
KeyStrokeというオブジェクトで表します。
一方ActionMapは、Actionオブジェクトを名前にマップしています。Action
オブジェクトは、名前のとおり、コンピュータの上で起きるアクションを…主
にactionPerformed() メソッドの内容として…表現しています。上の例で話を
続けると、"movefocus" という名前には、fowardFocusActionという(変数名の)
Actionオブジェクトが結びついているかもしれません。"help"という名前には、
popupHelpというActionが結びついているかもしれません。
このように、この仕組みは:
InputMapの中で
・キー(KeyStrokeオブジェクト) を名前に結び付け、
ActionMapの中で
・名前にアクション(Actionオブジェクト) を結び付けます。
キーに直接アクションを結び付けたほうが簡単なように思えますが、しか
しそうすると、アプリケーションの今後の成長性やカスタマイズの幅がかな
り損なわれてしまいます。上のように二段階で間接的にしておくと、名前に
対してアクションを自由に変えられるし、また逆に、ある名前と結びついて
いるアクションを起動するキーを、後で変えることも自由にできます。
今回、例題プログラムとして示すTableTraversalDemoというJavaプログラ
ムは、まさにこの仕組みをフル活用して、[Enter] キーにカラム移動アクシ
ョンを割り当てています。プログラムのかんじんの部分は、tabActionという
変数名でActionを定義しているところの、
while(! table.isCellEditable(row, column)){ではじまるループです。ここ
で、JTableのchangeSelection() メソッドにより、「次の編集可のカラム」へ
次々と移動していきます。rowを更新していることでお分かりのように、この
移動は複数の行にまたがります。
読者はテーブルのモデルを表しているMyTableModelクラスのisCellEditable()
メソッドを書き換えて、編集可能なカラムをプログラムの現状から変えてみて、
コンパイルし、プログラムを動かして見てください。
やや分かりにくいと思われる部分は、コンストラクタのはじめのほうでコ
ンボボックス( 変数名:comboBox) に対してaddActionListener() している
ところでしょう。このテーブルのカラムは単純なテキストフィールドだけで
なくて、三つめのカラム(インデクスでいうと2)をコンボボックス
(javax.swing.JComboBox) にしています。コンボボックスは、ユーザの[Enter]
キー入力によって選択(セレクト)が確定し、ActionEventがfireされます…つ
まりActionListenerのactionPerformed() メソッドが呼ばれる…。すると、
このままですと、コンボボックスのカラムでは、最初の[Enter] キーではセ
レクト決定、そしてカラムを移動するためにはもう一度[Enter] キーを押す
ことになります。
コンボボックスのカラムの上でも、一度の[Enter] キーでコンボボックス
のセレクト決定とカラム移動の両方を実現するために、ご覧のように、
actionPerformed() メソッドの最後にテーブルのchangeSelection() を呼び
出して、カラム移動を実現しています。
参考URL: http://forum.java.sun.com/thread.jspa?threadID=509913&forumID=57
---------------------------------------------------------------
/** TableTraverserDemo.java
* InputMapとActionMapを独自に設定して,
* [Enter]キーで次のエディット可のセルへ移動する.
*
* (JTableのデフォルトの機能では, [Tab]キーで隣のセルへ移動,
* [Enter]キーでは次の行へ移動します.)
*
* このプログラムのテーブル本体は
Java tutorial
* からダウンロードしたTableRender.javaのテーブルをそのまま利用しています.
*/
import javax.swing.*;
import javax.swing.table.*;
import java.awt.*;
import java.awt.event.*;
public class TableTraverserDemo extends JPanel {
JComboBox comboBox;
JTable table;
public TableTraverserDemo() {
super(new GridLayout(1, 0)); //テーブルの幅をウィンドウに追随させる
comboBox = new JComboBox();
table = new JTable(new MyTableModel());
table.setPreferredScrollableViewportSize(new Dimension(500, 70));
JScrollPane scrollPane = new JScrollPane(table);
//コンボボックスカラムを設定...
setUpSportColumn(table, table.getColumnModel().getColumn(2));
add(scrollPane);
//コンボボックスでセレクトを決定する[Enter]でも
//隣のセルへ行けるようにします.
comboBox.addActionListener(new ActionListener(){
public void actionPerformed(ActionEvent ae){
JComboBox b = (JComboBox)ae.getSource();
doComboBoxBusiness(b.getSelectedItem()); //コンボボックス関連の処理
//隣のセルへ行く
table.changeSelection(table.getSelectedRow(), 3, false, false);
}
});
//セルにフォーカスがあるときの, テーブルとスクロールペインの
//InputMapを取得する.
InputMap im
= table.getInputMap(JTable.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT);
InputMap ims
= scrollPane.getInputMap(JScrollPane.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT);
// [Enter]キーに[Tab]キーのアクション名を登録する
KeyStroke tab = KeyStroke.getKeyStroke(KeyEvent.VK_TAB, 0);
KeyStroke enter = KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0);
im.put(enter, im.get(tab)); // im.get()は既存の名前をgetする
// [→]キーのアクションを無にする
KeyStroke right = KeyStroke.getKeyStroke(KeyEvent.VK_RIGHT, 0);
im.put(right, "none"); // noneはJavaがあらかじめ定義している名前で,
// 無アクションを表します.
//[↓]キーのアクションを無にする
KeyStroke down = KeyStroke.getKeyStroke(KeyEvent.VK_DOWN, 0);
im.put(down, "none");
ims.put(down, "none");
/* [Tab]キーのための新しいアクションを定義して登録する.
* これが[Enter]キーの機能にもなります.
*/
final Action oldTabAction = table.getActionMap().get(im.get(tab));
Action tabAction = new AbstractAction(){
public void actionPerformed(ActionEvent e){
oldTabAction.actionPerformed(e);
JTable table = (JTable)e.getSource();
int rowCount = table.getRowCount();
int columnCount = table.getColumnCount();
int row = table.getSelectedRow();
int column = table.getSelectedColumn();
//次のエディット可セルへ行く
while (! table.isCellEditable(row, column) ){
column += 1;
if (column == columnCount){
column = 0;
row +=1;
}
if (row == rowCount){
row = 0;
}
if (row == table.getSelectedRow()
&& column == table.getSelectedColumn()){
break; //元へ戻った
}
}
table.changeSelection(row, column, false, false);
}
};
table.getActionMap().put(im.get(tab), tabAction);
} //コンストラクタ
/* コンボボックスカラムを設定 */
public void setUpSportColumn(JTable table, TableColumn sportColumn) {
comboBox.addItem("Snowboarding");
comboBox.addItem("Rowing");
comboBox.addItem("Knitting");
comboBox.addItem("Speed reading");
comboBox.addItem("Pool");
comboBox.addItem("None of the above");
sportColumn.setCellEditor(new DefaultCellEditor(comboBox));
DefaultTableCellRenderer renderer = new DefaultTableCellRenderer();
renderer.setToolTipText("Click for combo box");
sportColumn.setCellRenderer(renderer);
}
/* このテーブルのための“モデル”を定義する */
class MyTableModel extends AbstractTableModel {
private String[] columnNames = {
"First Name",
"Last Name",
"Sport",
"# of Years",
"Vegetarian"
};
private Object[][] data = {
{"Mary", "Campione",
"Snowboarding", new Integer(5), new Boolean(false)},
{"Alison", "Huml",
"Rowing", new Integer(3), new Boolean(true)},
{"Kathy", "Walrath",
"Knitting", new Integer(2), new Boolean(false)},
{"Sharon", "Zakhour",
"Speed reading", new Integer(20), new Boolean(true)},
{"Philip", "Milne",
"Pool", new Integer(10), new Boolean(false)}
};
public int getColumnCount() {
return columnNames.length;
}
public int getRowCount() {
return data.length;
}
public String getColumnName(int col) {
return columnNames[col];
}
public Object getValueAt(int row, int col) {
return data[row][col];
}
// BooleanタイプがチェックボックスになりIntegerの欄が右詰め
// になるためには, このメソッドの実装が必要
public Class getColumnClass(int c) {
return getValueAt(0, c).getClass();
}
public boolean isCellEditable(int row, int col) {
return (col > 1); // 名前欄(col0, col1)はエディット不可にする
}
public void setValueAt(Object value, int row, int col) {
data[row][col] = value;
fireTableCellUpdated(row, col);
}
} // class MyTableModel
private static void createAndShowGUI() {
JFrame frame = new JFrame("TableTraverserDemo");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
TableTraverserDemo newContentPane = new TableTraverserDemo();
frame.setContentPane(newContentPane);
frame.pack();
frame.setVisible(true);
}
//コンボボックスセレクションで起動されるなんらかのタスク
void doComboBoxBusiness(Object item){
System.out.println("コンボボックスから" + item + "がセレクトされました.");
}
public static void main(String[] args) {
javax.swing.SwingUtilities.invokeLater(new Runnable() {
public void run() {
createAndShowGUI();
}
});
}
}
以下の類似例は、セルエディタのエディタコンポーネントがJScrollPaneである場合
に、TABキーでそのセルへ遷移したとき、JScrollPaneの中にあるJTextAreaにキー
フォーカスがある(すぐにエディットを開始できる)状態にします。
import javax.swing.*;
import javax.swing.table.*;
import java.awt.*;
import java.awt.event.*;
import java.util.*;
public class TextAreaCellEditor extends AbstractCellEditor implements
TableCellEditor, TableCellRenderer {
private JTextArea textArea;
private JScrollPane scroll;
public TextAreaCellEditor() {
textArea = new JTextArea();
scroll = new JScrollPane(textArea);
}
public JTextArea getTextArea(){
return textArea;
}
public Object getCellEditorValue() {
return textArea.getText();
}
public boolean shouldSelectCell(EventObject anEvent) {
return true;
}
public boolean stopCellEditing() {
fireEditingStopped();
return true;
}
public void cancelCellEditing() {
fireEditingCanceled();
}
public Component getTableCellEditorComponent(JTable table, Object value,
boolean isSelected, int row, int column) {
textArea.setText((value != null) ? value.toString() : "");
textArea.setCaretPosition(textArea.getText().length());
return scroll;
}
public Component getTableCellRendererComponent(JTable table,
Object value, boolean isSelected, boolean hasFocus, int row, int column){
textArea.setText((value != null) ? value.toString() : "");
textArea.setCaretPosition(textArea.getText().length());
return scroll;
}
static void focusOnTextArea(JTable table){
InputMap im
= table.getInputMap(JTable.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT);
KeyStroke tab = KeyStroke.getKeyStroke(KeyEvent.VK_TAB, 0);
final Action oldTabAction = table.getActionMap().get(im.get(tab));
Action tabAction = new AbstractAction(){
public void actionPerformed(ActionEvent e){
oldTabAction.actionPerformed(e);
JTable table = (JTable)e.getSource();
int row = table.getSelectedRow();
int column = table.getSelectedColumn();
if (column == 1){ // このセルエディタのカラムだ
table.editCellAt(row, column); // 強制的にエディットモードにする
TextAreaCellEditor tace
= (TextAreaCellEditor)(table.getCellEditor(row, column));
JTextArea ta = tace.getTextArea();
ta.requestFocusInWindow(); // テキストエリアにキーフォーカス
}
}
};
table.getActionMap().put(im.get(tab), tabAction);
// 右矢印キーも使いたい
KeyStroke right = KeyStroke.getKeyStroke(KeyEvent.VK_RIGHT, 0);
im.put(right, im.get(tab));
}
/* test */
public static void main(String[] args){
JFrame frame = new JFrame();
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
Container con = frame.getContentPane();
JTable table = new JTable(20, 3);
table.setRowHeight(70);
JScrollPane jsp = new JScrollPane(table);
con.add(jsp, BorderLayout.CENTER);
TextAreaCellEditor tace = new TextAreaCellEditor();
TableColumn tc = table.getColumn(table.getColumnName(1));
tc.setCellEditor(tace); // for column 1
tc.setCellRenderer(tace);
focusOnTextArea(table);
frame.pack();
frame.setVisible(true);
}
}
---------------------------------------------------------------
【注記】テーブルのセル移動と一般のフォーカス移動との違い
なお、TABキー等によるJTableにおけるセル間の遷移と、複数のコンポーネント間
を遷移する通常のキーフォーカス遷移は、似ているようでいて全く別のシステムです。
Java 1.4以降は、java.awt.KeyboardFocusManagerを軸とするフォーカスサブシステ
ムによってキーフォーカスが制御されます。たとえば、デフォルトのTABキーに加え
てEnterキーでもキーフォーカスの前進ができるようにするためには、
java.awt.Componentのメソッドを次のように使います。
(URL: http://java.sun.com/docs/books/tutorial/uiswing/misc/focus.html#intro)
------------------------------------------
Set forwardKeys = getFocusTraversalKeys
(KeyboardFocusManager.FORWARD_TRAVERSAL_KEYS); //現在の前進キーを取得
Set newForwardKeys = new HashSet(forwardKeys); //それらを含むHashSetを作る
//HashSetにEnterキーを加える
newForwardKeys.add(KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0));
//そしてHashSetを前進キー集合として登録する
setFocusTraversalKeys
(KeyboardFocusManager.FORWARD_TRAVERSAL_KEYS, newForwardKeys);
------------------------------------------
(参考URL)
How to Use the Focus Subsystem
Focus Specification