Swing基本Tips

澤田聡司 Satoshi Sawada http://homepage3.nifty.com/satoshis/
中央電子(株) 九州テクノロジーセンタ http://www.cec.co.jp/




はじめに

最近では、SwingによるGUIアプリケーション (以下、Swingアプリケーション)を目にすることが多くなってきました。 例えば、オブジェクト倶楽部によるUMLモデリングツールのJudeなどは 読者の中にも利用している方がいるのではないかと思います。

使う機会は徐々に増えているSwingアプリケーションですが、 いざ作ろうと思ったときに情報不足を感じる人は多いのではないでしょうか。 Swingに関する書籍や雑誌の記事の大半は、 各コンポーネントのリファレンスと簡単なサンプルコードレベルです。 Swingアプリケーションを作ろうとしたときに、 Swingに含まれているいろいろなクラスを効果的に利用する方法についての 情報はなかなか手に入りません。

本稿では、Swingアプリケーションを開発するにあたって、 「知っておかないと苦労する」あるいは「知っていれば楽ができる」 基本的な情報のいくつかを紹介していこうと思います。

マルチスレッドプログラミング

快適に使用できるアプリケーションを作るには、 マルチスレッドでの動作は必要条件です。

例えば、シングルスレッドで動作しているWebブラウザがあると仮定し、 どのように動作するのかを想像してみましょう。 ユーザがWebブラウザ上のリンクをクリックしたとします。 一連の処理はシングルスレッド上で実行されるため、 リンクをクリックしてからレンダリングが完了するまでの間は、 Webブラウザはユーザの操作を一切受け付けません。 ネットワークの問題などのために通信ができなくなった場合も、 通信タイムアウトが発生するまでブロックされ続けます。 時間がかかる処理のキャンセルもできないし、 何かの処理中はアプリケーションを終了させることすらできません。

このようなアプリケーションが実用に耐えられないことは容易に想像できます。 マルチスレッドで動作することは、ほとんどのアプリケーションに対する要求 であるといっても過言ではありません。

Swingのスレッドポリシー

Swingプログラミングにおいて知っておかなければならない 重要な情報のひとつに、Swingのスレッドポリシーがあります。 Swingのスレッドポリシーに従っていないと、 最悪の場合はGUIがデッドロックを起こしてしまう可能性もあります。 まず最初のセクションでは、シングルスレッドルールとも呼ばれている Swingのスレッドポリシーについて説明します。

Swingアプリケーションは次のルールを守らなければなりません。

可視コンポーネントの「状態に依存する」または「状態に影響を与える」 コードは全て、イベントディスパッチスレッド上で動作しなければならない
脚注1
イベントディスパッチスレッドとは、マウスやキーボードからの入力を受け取 り、プログラマがコンポーネントに登録したイベントリスナーに対して、 それらの入力をイベントとして伝達する役割を持ったスレッドです。

このルールは、Swingプログラミングに非常に大きな制約を与えます。

なぜなら、Swingコンポーネントが持つメソッドのほとんどが、 コンポーネントの状態に依存するかまたは状態に影響を与えるのです。 それを考慮すると、Swingのコンポーネントに安全にアクセスするには、 不可視(つまり非表示状態の)コンポーネントにアクセスするか、または、 すべてのコンポーネントへのアクセスするコードの大半を スレッドセーフな方法で記述するのが適切です。

Sunのサイトには、スレッドセーフであるメソッドとはどういうものなのか、 を解説しているページ(Threads and Swing)が存在します。 そこでは、以下の要件のいずれかを満たすメソッドだけが スレッドセーフであると記載されています。

Sunのサイトにある情報の要約
 
  • SwingコンポーネントのAPIドキュメントに
    「このメソッドはスレッドに対して安全ですが、 ほとんどのSwingメソッドは違います。」
    と記載があるメソッド。
  • アプレットのinit()メソッド
  • JComponentの以下のメソッド
        
    • repaint()   
    • revalidate()   
    • invalidate()
  • リスナーの追加と削除

Swingコンポーネントのメソッドの大半は コンポーネントの状態を設定したり取得することが目的なので、 スレッドセーフなメソッドが少ないのも考えてみれば当然ですね。

SwingUtilitiesクラスのinvokeLater()メソッド

Swingアプリケーションを作ろうとすると、 ユーザの入力や確認を求めるためにダイアログを表示する処理は、 プログラム中に数多く出てきます。 たとえば、この「ダイアログの表示」に対応する JDialogのインスタンスを可視化する処理を、 イベントディスパッチスレッド以外のスレッドから行うことは、 Swingのスレッドポリシーに反することになります。 このような場合にダイアログの可視化を安全に行うには SwingUtilitiesクラスのinvokeLater()メソッドを使用するのが適切です。

invokeLater()メソッドの引数はRunnableを要求しています。 安易な解決方法としては、引数に渡すRunnableを匿名クラスにし、 Runnableインタフェースが要求するrun()メソッド内に 可視化の処理を記述する方法がありますが、 本稿ではRunnableインタフェースを実装した、 便利なWindowInvokerクラスを作ることにします。(リスト1)

脚注2
可視化のたびに匿名クラスを記述すると、 似たような匿名クラスがプログラムのあちこちに大量生産されてしまいます。 WindowInvokerクラスを作っておくことは、 そのようなコードの重複を防止する効果も期待できます。
リスト1: WindowInvoker.java
import java.awt.Window;

public class WindowInvoker implements Runnable {
    private Window window;
    public WindowInvoker(Window window) {
        this.window = window;
    }
    public void run() {
        window.pack();
        window.show();
    }
}

このWindowInvokerクラスを使用すると、 スレッドに対して安全にJFrameやJDialogを表示できます。 リスト2が、invokeLater()メソッドの使用例です。 以降で紹介するサンプルプログラムでもWindowInvokerを使用しています。

リスト2: invokeLater() メソッドの使用例
    public static void main(String args[]) {
        JFrame f = new JFrame("Window Title");
        // JFrame にいろいろなコンポーネントを追加する
        // 不可視な間は安全にアクセスできる
        SwingUtilities.invokeLater(new WindowInvoker(f));
    }

現実的な問題

Swingコンポーネントにアクセスするときは、SwingUtilitiesクラスの invokeLater()メソッドを使えばよいことはわかりました。 しかし、Swingコンポーネントへのアクセスは、 アプリケーションの中のさまざまな場所で発生します。

すべてのSwingコンポーネントへのアクセスを、 Runnableインタフェースを実装したクラスでラップして invokeLater()メソッドから起動することも可能ですが、 そのために書かなければならないコードの量を考えると あまり現実的であるとは言えません。 残念ながら、筆者もこの問題をうまく解決する方法を知りません。

何か良い解決策が見つかったときには、ぜひその方法をご紹介ください。



コンポーネントのレイアウト

GridBagLayoutは非常に柔軟なレイアウトマネージャです。 しかしながら、GridBagLayoutに関する情報も、 Swingのスレッドポリシーに関する情報の少なさと同じくらい なかなか入手することができません。

このセクションでGridBagLayoutの基本的な使い方を紹介することで、 そのレイアウトの自由度の高さと使いやすさを知っていただこうと思います。

レイアウトマネージャ

GridBagLayoutの説明に入る前に、 レイアウトマネージャにはどのようなものがあるのかを見ておきましょう。 これを確認するには、Javaのドキュメントを見るのが手っ取り早いでしょう。 LayoutManagerのドキュメントにある 「既知の実装クラスの一覧」(図1)を見れば、 どのようなレイアウトマネージャがあるのかを簡単に確認できます。

図1:レイアウトマネージャ

「既知の実装クラスの一覧」に挙げられている区明日のちの大半は、 特定のSwingコンポーネントのデザインを構成するためのレイアウトマネージャです。 通常のプログラミングでは、それらを除いた汎用的なレイアウトマネージャ (以下)を使用します。

このうち、多くの記事が取り上げているのはBorderLayout・FlowLayout・ GridLayoutの3つです。 しかしながら、これらのレイアウトマネージャはあまり使い道がありません。 なぜなら、これらのレイアウトマネージャによるコンポーネントの配置は、 あまりにも単純で融通が利かないのです。 単純ではない、しかし決して複雑ではない画面を作ろうとしたときでさえ、 プログラマーの意図するようにコンポーネントを配置するのは非常に困難です。

本稿では、柔軟なレイアウトマネージャであるGridBagLayoutを紹介します。 このレイアウトマネージャを使うには、 他のレイアウトマネージャと比べるとコード量が若干多くなりまが、 プログラマは、思い通りにレイアウトできないストレスから解放されますし、 エンドユーザはスマートなGUIを得られるので一石二鳥です。

脚注3
SpringLayoutも便利なレイアウト・マネージャです。 SpringLayoutの利用方法は参考文献に紹介した IBM developerWorksのドキュメントが参考になります。

GridBagLayoutの特徴

GridBagLayoutを使ったGUIのデザインは表計算ソフトに似ています。 GridBagLayoutは次の特徴を持っています。

この特徴を見るとHTMLのTABLEにも似ていることが分かると思います。 任意の行数とカラム数でセルを構成できる点や、 特に指定しなければ 行の高さやカラム幅が適切なサイズに自動調整される点も似ています。 4番目の特徴に書いた隣接するセルの結合は TDタグのcolspan・rowspanと同じ結果が得られますし、 5番目の特徴に書いたセル内のコンポーネントの位置指定は align・valign属性に対応しています。

GridBagLayoutを使用したサンプル

さっそく、GridBagLayoutでコンポーネントをレイアウトしてみましょう。

まず最初に、サンプルプログラムで表示するコンポーネントを用意します。 イメージとテキストを簡単に表示できるようにした ImageLabel(リスト3)を、JLabelを継承して作成しておきます。 コンポーネントの境界(セルの境界)の表示を分かりやすくするため、 背景を白に、ボーダをEtchedBorderに指定しています。

リスト3:ImageLabel.java
import java.awt.Color;
import javax.swing.*;
import javax.swing.border.EtchedBorder;

public class ImageLabel extends JLabel {
    public ImageLabel(String image, String text) {
        super(text);
        setBorder(new EtchedBorder());
        setIcon(new ImageIcon(image));
        setBackground(Color.white);
        setOpaque(true);
    }
}

次に、複数のImageLabelクラスを画面上に配置するプログラムを作ります。 このサンプルプログラムでは、セルの位置指定とセルの結合を行います。

コンポーネントのレイアウト方法の指定は GridBagConstraintsクラスを使用します。 GridBagConstraintsにはレイアウト方法を指定するための publicフィールドが多数用意されています。

GridBagLayoutクラスを使用するときは、セルの位置を指定したり、 セルの位置とセルの結合を指定するメソッドを作っておくと便利です。 以下の例ではComplexCellsクラスのクラスメソッドとして add()メソッドを作成していますが、再利用性を考慮すると 汎用的なユーティリティクラスのクラスメソッドにする方がよいでしょう。 add()メソッドの引数とGridBagConstraintsのフィールドとの対応を 表1に示します。

表1: add()メソッドの引数
引数 引数の説明 GridBagConstraintsのフィールド
pane コンポーネントの追加先となるコンテナを指定します。 -
comp 第1引数で指定したコンテナに追加するコンポーネントを指定します。 -
x コンポーネントを追加するセルのx座標を指定します。 gridx
y コンポーネントを追加するセルのy座標を指定します。 gridy
w コンポーネントの追加先を結合した複数のセルにしたいとき、 x軸方向にセルを結合させる個数を指定します。 gridwidth
h コンポーネントの追加先を結合した複数のセルにしたいとき、 y軸方向にセルを結合させる個数を指定します。 gridheight

リスト4は、やや複雑なセル構造を作った例です。 それぞれのセルの中にImageLabelを表示しています。

脚注4
add()メソッドの中ではGridBagConstraintsクラスのfillフィールドに GridBagConstraints.BOTHを指定しています。 この指定により、セルに追加されたコンポーネントのサイズは セルと同じサイズに調整されます。
リスト4: ComplexCells.java
import java.awt.*;
import javax.swing.*;

public class ComplexCells {
    public static void main(String args[]) {
        JFrame f = new JFrame("ComplexCells");
        f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        Container pane = f.getContentPane();
        pane.setLayout(new GridBagLayout());
        // イメージファイル名
        String swing = "swing.png";
        // コンポーネントを追加                  x  y  w  h
        add(pane, new ImageLabel(swing, "1:0,0"), 0, 0, 4, 1);
        add(pane, new ImageLabel(swing, "2:0,1"), 0, 1, 2, 2);
        add(pane, new ImageLabel(swing, "3:0,3"), 0, 3, 1, 1);
        add(pane, new ImageLabel(swing, "4:1,3"), 1, 3, 1, 1);
        add(pane, new ImageLabel(swing, "5:2,1"), 2, 1, 1, 1);
        add(pane, new ImageLabel(swing, "6:2,2"), 2, 2, 1, 1);
        add(pane, new ImageLabel(swing, "7:2,3"), 2, 3, 1, 1);
        add(pane, new ImageLabel(swing, "8:3,1"), 3, 1, 1, 3);
        // 前述した WindowInvoker を利用
        SwingUtilities.invokeLater(new WindowInvoker(f));
    }

    /**
     * セルの位置(x, y)とセルの結合方法(width, height)を指定して
     * コンテナ pane にコンポーネント comp を追加します。
     */
    static void add(Container pane,
                    Component comp,
                    int x,
                    int y,
                    int width,
                    int height) {
        GridBagConstraints c = new GridBagConstraints();
        c.fill = GridBagConstraints.BOTH;
        c.gridx = x;
        c.gridy = y;
        c.gridwidth = width;
        c.gridheight = height;
        pane.add(comp, c);
    }
}

座標の指定内容と表示位置を対比させやすいよう、 テキストの部分にはコンポーネントを追加した順番と、 コンポーネントの座標(gridx,gridy)を表示するようにしました。 このアプリケーションを動作させると、次の画面(図2)を表示します。

図2: ComplexCellsの実行結果

この実行結果は、コンポーネントの表示位置を自由に制御できることや、 複数のセルを結合した大きなセルにコンポーネントを配置できることを 示しています。

もっと柔軟なレイアウトも可能

GridBagLayoutのよさはセルの結合だけではありません。 GridBagConstraintsクラスのanchorフィールドで セル内のコンポーネントの位置を指定できますし、 GridBagConstraintsクラスのinsetsフィールドにより、 各セル内でコンポーネントの境界からの距離も指定できます。

紙面の都合もありますので、 GridBagLayoutの柔軟なレイアウト機能のすべてを ここで紹介することはできません。 さらなる使い方については、読者のみなさんの練習問題に残しておきます。



コマンドの実装

多くのGUIアプリケーションは、ユーザとの対話のインタフェースとして メニューバー・ツールバーや画面上のボタンなどにより、 アプリケーションに対するコマンドを提供しています。 このセクションでは、ユーザにアプリケーションの機能を提供するための コマンドの実装方法を紹介します。

GUIアプリケーションがユーザと対話する部分では、 MVC(Model-View-Controller)アーキテクチャのController にあたるクラスが重要な役割を果たします。 MVCのうちのModelとViewは通常は静的です。 ユーザの操作によりControllerが動作し、Modelの状態が変更され、 新しいModelの状態がViewに反映されます。 その一連の動作を実現しているのがGUIアプリケーションなのです。

脚注5
MVCアーキテクチャについては参考文献 「UMLによるアーキテクチャパターン」に良い解説があります。

Swingでは、このようなユーザとの対話の実現に、 Commandパターンを使用したアーキテクチャを採用しています。 そのアーキテクチャにおいて、 ActionインタフェースとActionインタフェースのデフォルト実装である AbstractActionクラスが中心的な役割を担っています。

AbstractAction

Actionインタフェースは ActionListenerインタフェースのサブインタフェースですので、 AbstractActionクラスはActionEventを受信するための クラスであることが分かります。

さっそく、AbstractActionを使用する単純なサンプルプログラムを 作ってみることにしましょう。

多くのGUIアプリケーションに存在するのがファイルを開く操作です。 この「ファイルを開く操作」を例として AbstractActionの利用方法を紹介します。 ファイルを開くためのJFileChooserのダイアログを表示するアクション、 OpenFileActionクラス(リスト5)を作成します。

リスト5: OpenFileAction.java
import java.awt.Component;
import java.awt.event.ActionEvent;
import javax.swing.*;

public class OpenFileAction extends AbstractAction {
    public void actionPerformed(ActionEvent e) {
        Component src = (Component)e.getSource();
        JFileChooser fc = new JFileChooser();
        fc.showOpenDialog(src);
        // JFileChooser の結果を参照
    }
}
脚注6
一般にAWTEventを受信するメソッドはイベントディスパッチスレッド上から 呼び出されます。ActionEventはAWTEventのサブクラスです。 actionPerformed()もAWTEventを受信するメソッドうちのひとつですので、 このメソッド内からはSwingコンポーネントに対して安全にアクセスできます。 つまり、最初の項で述べたinvokeLater()メソッドを使う必要はありません。

ボタンからアクションを起動する

OpenFileSample1.java(リスト6)は、 画面上にJButtonを配置し、 そこからOpenFileActionを起動するプログラムです。

リスト6: OpenFileSample1.java
import javax.swing.*;

public class OpenFileSample1 {
    public static void main(String args[]) {
        JFrame f = new JFrame("OpenFileSample1");
        f.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);

        JButton b = new JButton("開く");
        b.addActionListener(new OpenFileAction());
        f.getContentPane().add(b);
        SwingUtilities.invokeLater(new WindowInvoker(f));
    }
}

このアプリケーションを動作させると画面(図3)を表示します。 画面上の「開く」ボタンをクリックすると JFileChooserのダイアログが表示されます。

図3: OpenFileSample1の実行結果

メニューバーからアクションを起動する

次にメニューバーをこの画面に追加します。 OpenFileSample2.java(リスト7)では、 メニューバーに「ファイル」メニューを用意し、 そこにメニュー項目「開く」を追加しています。

リスト7: OpenFileSample2.java
import javax.swing.*;

public class OpenFileSample2 {
    public static void main(String args[]) {
        Action open = new OpenFileAction();
        JFrame f = new JFrame("OpenFileSample2");
        f.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);

        JMenuBar bar = new JMenuBar();
        JMenu menu = new JMenu("ファイル");
        JMenuItem item = new JMenuItem("開く");
        item.addActionListener(open);

        bar.add(menu);
        menu.add(item);
        f.setJMenuBar(bar);

        JButton b = new JButton("開く");
        b.addActionListener(open);
        f.getContentPane().add(b);

        SwingUtilities.invokeLater(new WindowInvoker(f));
    }
}

OpenFileSample2.javaを動作させると画面(図4)を表示します。 このサンプルでは、メニューバーとボタンの両方からOpenFileAction クラスを起動し、JFileChooserのダイアログを表示できるようになりました。

図4: OpenFileSample2の実行結果

ツールバーからアクションを起動する

さらにツールバーを画面に追加します。

脚注7
Javaのドキュメント(The Java Tutorial の How to Use Tool Bars)には ツールバーを使う場合はBorderLayoutをレイアウトマネージャに指定するよう 記載されているので、サンプルコードもそれに従っています。

OpenFileSample3.java(リスト8)では、画面上のボタンとメニューバーに加え ツールバーからもJFileChooserのダイアログを表示できます。

リスト8: OpenFileSample3.java
import java.awt.*;
import javax.swing.*;

public class OpenFileSample3 {
    public static void main(String args[]) {
        Action open = new OpenFileAction();
        JFrame f = new JFrame("OpenFileSample3");
        f.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
        Container pane = f.getContentPane();
        pane.setLayout(new BorderLayout());

        JMenuBar menuBar = new JMenuBar();
        JMenu menu = new JMenu("ファイル");
        JMenuItem item = new JMenuItem("開く");
        item.addActionListener(open);

        menuBar.add(menu);
        menu.add(item);
        f.setJMenuBar(menuBar);

        JToolBar toolBar = new JToolBar();
        toolBar.add(open);
        pane.add(toolBar, BorderLayout.PAGE_START);

        JButton b = new JButton("開く");
        b.addActionListener(open);
        pane.add(b, BorderLayout.CENTER);

        SwingUtilities.invokeLater(new WindowInvoker(f));
    }
}

図5がOpenFileSample3の実行結果です。 一応ツールバーらしいものは追加されたようですが、 これではツールバーのボタンが何なのか分かりませんね。

図5: OpenFileSample3の実行結果(その1)

アイコンの表示

AbstractActionには便利な機能がたくさんあります。 そのひとつがアイコンのサポートです。 AbstractActionのインスタンスを生成するときにアイコンを指定しておくと、 メニューバーやツールバーに自動的に反映してくれます。

OpenFileActionクラスのコンストラクタをリスト9のように変更すると、 ツールバーのボタンにアイコンが表示されました。(図6) アイコンとして表示するために必要なopen.gif も忘れずに用意しておきます。

リスト9: 変更したOpenFileAction.java
import java.awt.Component;
import java.awt.event.ActionEvent;
import javax.swing.*;

public class OpenFileAction extends AbstractAction {
    public OpenFileAction() {
        super("開く", new ImageIcon("open.gif"));
    }

    public void actionPerformed(ActionEvent e) {
        Component src = (Component)e.getSource();
        JFileChooser fc = new JFileChooser();
        fc.showOpenDialog(src);
	// JFileChooser の結果を参照
    }
}

図6: OpenFileSample3.javaの実行結果(その2)

OpenFileActionのコンストラクタでイメージを指定するだけで、 ツールバーのボタンにアイコンが表示されました。 このことから推測すると、ツールバーと同じようにコーディングしておけば、 メニューバーやボタンでもアイコンが表示されることが期待できます。

実際にどうなるか、OpenFileSample2.javaを変更したOpenFileSample3.java (リスト10)で試してみましょう。 JMenuItemとJButtonのコンストラクタの引数に OpenFileActionを指定します。

リスト10: OpenFileSample3.java(変更後)
import java.awt.*;
import javax.swing.*;

public class OpenFileSample3 {
    public static void main(String args[]) {
        Action open = new OpenFileAction();
        JFrame f = new JFrame("OpenFileSample");
        f.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
        Container pane = f.getContentPane();
        pane.setLayout(new BorderLayout());

        JMenuBar menuBar = new JMenuBar();
        JMenu menu = new JMenu("ファイル");
        JMenuItem item = new JMenuItem(open);

        menuBar.add(menu);
        menu.add(item);
        f.setJMenuBar(menuBar);

        JToolBar toolBar = new JToolBar();
        toolBar.add(open);
        pane.add(toolBar, BorderLayout.PAGE_START);

        JButton b = new JButton(open);
        pane.add(b, BorderLayout.CENTER);

        SwingUtilities.invokeLater(new WindowInvoker(f));
    }
}

動作結果は図7と図8のとおりです。 期待どおりに、ボタンとニュー項目にイメージが反映されました!

図7: OpenFileSample3の実行結果(その3)
図8: OpenFileSample3の実行結果(その4)

もっと便利なAbstractAction

AbstractActionを使うことで、 MVCアーキテクチャのControllerにあたる部分を 容易にかつ自然に実装することができました。

AbstractActionはこの他にも多くの機能をサポートしています。 AbstractActionのenabled属性をfalseに指定すると、 メニューバーのメニュー項目やツールバーのボタンがグレーに変化して そのコマンドが使用可能でなくなったことが画面上に反映されます。 ショートカットキーの定義もAbstractActionで指定可能です。

Swingアプリケーションでユーザの操作を受け付ける部分の コーディングを行うときには、 AbstractActionクラスとActionインタフェースのドキュメントを 一読しておくことをお勧めします。



コンポーネントの設計と使用

Swingコンポーネントの大半はMVCアーキテクチャを採用しています。 Swingコンポーネントの設計を十分理解した上で使用することこそ、 Swingコンポーネントを上手に使いこなす秘訣です。

このセクションでは、JListコンポーネントを例にして、 Swingコンポーネントの使用方法を説明します。

典型的な構造

まずはSwingコンポーネントの典型的な構造を見てみましょう。 JListコンポーネントに関連するいくつかのクラスの構造を クラス図(図9)にあらわしました。

図9: JListに関連するクラス

このクラス図は、ListModelインタフェースが存在し、 その抽象実装としてabstract classである AbstractListModelクラスが用意されていることを示しています。 さらに、AbstractListModelクラスを継承して、 DefaultListModelクラスが提供されていることもわかります。

この構造は、他のSwingコンポーネントでも採用されています。 いくつかの例を表2に示します。

表2: JListに近い構造を持つコンポーネント
コンポーネント インタフェース 抽象実装モデル デフォルト実装モデル
JComboBox ComboBoxModel
ListModel
AbstractListModel DefaultComboBoxModel
JList ListModel AbstractListModel DefaultListModel
JSpinner SpinnerModel AbstractSpinnerModel SpinnerDateModel
SpinnerListModel
SpinnerNumberModel
JTable TableModel AbstractTableModel DefaultTableModel

Swingコンポーネントの典型的な構造をひとつ理解しておけば、 他のコンポーネントにも容易に応用できます。 機能がシンプルなJListコンポーネントは、 使用方法を理解するのに適しています。

List要素の変更

JListコンポーネントはコンストラクタの引数に配列を渡すことにより、 表示するための要素を初期化できますが、 要素を追加したり削除したりするメソッドは存在しません。 しかし、実際は要素の追加や削除は可能です。 そのヒントは、MVCアーキテクチャにあります。

JListコンポーネントは、MVCアーキテクチャのViewです。 ViewはModelの状態を表示するためのクラスであり、 実際のデータはModelに対応するクラスが持っています。 つまり、Modelのデータを変更すればViewに反映されることが期待できます。 表2からデフォルト実装モデルは DefaultListModelクラスであることがわかります。 DefaultListModelを使用したサンプルで、 要素を動的に変更する例を示します。

画面上にJListとテキストを入力するためのJTextFieldを表示し、 「追加」ボタンを押すとJTextFieldに入力した文字列が JListに追加されるようなコンポーネントを作成します。 新しいコンポーネントは、JListを編集する機能を持っているとも言えるので JListEditorと呼ぶことにします。 リスト12はJListEditorのコードです。

リスト12: JListEditor.java
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;

public class JListEditor extends JPanel {
    private DefaultListModel listModel = new DefaultListModel();
    private JList list = new JList();
    private JButton addButton = new JButton("追加");
    private JTextField textField = new JTextField(18);

    /**
     * JListEditor コンポーネントを構築します。
     */
    public JListEditor() {
        // ListModel を設定
        list.setModel(listModel);

        // JScrollPane に JList を入れる
        JScrollPane sp = new JScrollPane(list);

        // サイズを調整
        Dimension d = new Dimension(200, 100);
        sp.setPreferredSize(d);

        // ボタンを押されたときのイベント処理
        addButton.addActionListener(new AddListener());

        // 画面にコンポーネントを追加
        GridBagConstraints c = new GridBagConstraints();
        setLayout(new GridBagLayout());
        c.gridx = 0;
        c.gridy = 0;
        add(sp, c);
        c.gridy++;
        add(textField, c);
        c.gridx++;
        add(addButton, c);
    }

    /**
     * 「追加」ボタンが押されたときに、JTextField内のテキストを
     * JList内に追加し、JTextFieldをクリアします。
     */
    private class AddListener implements ActionListener {
        public void actionPerformed(ActionEvent e) {
            listModel.addElement(textField.getText());
            textField.setText("");
        }
    }
}

JListEditorクラスは単なるコンポーネントなので単体では動作しません。 JListEditorを利用するサンプルとしてJListEditorSampleクラスを作成します。 JListEditorSampleクラスのコードはリスト13のとおりです。

リスト13: JListEditorSample.java
import javax.swing.*;

public class JListEditorSample {
    public static void main(String args[]) {
        JFrame f = new JFrame("JListEditorSample");
        f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        JListEditor editor = new JListEditor();
        f.getContentPane().add(editor);

        // WindowInvoker を利用
        SwingUtilities.invokeLater(new WindowInvoker(f));
    }
}

図10がJListEditorSampleの実行結果です。

図10: JListEditorSampleの実行結果

入力フィールドに文字列を入力した後、 追加ボタンを押すと画面上側に表示しているJListコンポーネントに 文字列が追加されていくのがわかります。(図11)

図11: JListEditorSampleの動作
文字列を入力した
追加ボタンをクリックした
それを何度か繰り返した

JListEditorのコードを見直してみましょう。 追加ボタンが押されたときに動作するのは、リスト14の部分だけです。

リスト14: 追加ボタン操作時に動作するコード
        public void actionPerformed(ActionEvent e) {
            listModel.addElement(textField.getText());
            textField.setText("");
        }

ここで行っている処理は、DefaultListModelに要素を追加するとともに、 入力フィールドをクリアしているだけです。 JListコンポーネントに対しては一切アクセスしていませんが、 画面上のJListコンポーネントには次々に要素が追加されます。 つまり、Modelの要素を変更すると ViewであるJListに自動的に反映されるのです。

表示方法の変更

前項では、JListコンポーネントに表示する要素を 動的に変更する方法を紹介しました。 サンプルでは文字列を扱いましたが、 実際のプログラムではいろいろなオブジェクトを扱うことになると思います。 JListコンポーネントに文字列以外の要素を追加したときは、 デフォルトではそのオブジェクトのtoString()メソッドで取得した、 そのオブジェクトの文字列表現で各要素を表示します。 しかし、実際のGUIアプリケーションでは文字列による表示だけでなく、 アイコンなどを表示したい場合もあります。

JListコンポーネントのListCellRendererを setCellRenderer()メソッドで置き換えれば、 JListコンポーネント内に表示する要素の表示方法を変更できます。 表示方法のアルゴリズムを交換可能にするため GoFデザインパターンのStrategyパターンが使われています。

ListCellRendererはインタフェースであり、 このインタフェースを実装したDefaultListCellRendererクラスが Swingに含まれています。 DefaultListCellRendererはJLabelコンポーネントに ListCellRendererインタフェースが要求するgetListCellRendererComponent() メソッドが追加されている程度の比較的単純なクラスです。 実際に表示方法を変更するには、 DefaultListCellRendererクラスを継承したサブクラスで getListCellRendererComponent()メソッドをオーバーライドし、 独自の表示方法を記述するのが簡単です。 オーバーライドする場合は、継承元の getListCellRendererComponent()メソッドを、オーバーライドしたメソッド の先頭で呼んでおくことがポイントです。 こうしておけば、継承元での表現をそのまま利用しつつ 独自の表現を追加できます。

DefaultListCellRendererクラスを継承し、 フィールドに入力された文字列が画像ファイルのファイル名と一致した場合は、 セルにそのイメージを表示するImageCellRenderer(リスト15)を作ってみます。

リスト15: ImageCellRenderer.java
import java.awt.*;
import javax.swing.*;

public class ImageCellRenderer extends DefaultListCellRenderer {
    public Component getListCellRendererComponent(JList list,
                                                  Object value,
                                                  int index,
                                                  boolean isSelected,
                                                  boolean cellHasFocus) {
        super.getListCellRendererComponent(list,
                                           value,
                                           index,
                                           isSelected,
                                           cellHasFocus);
        if (value instanceof String) {
            String s = ((String)value).toLowerCase();
            if (s.endsWith(".png") || s.endsWith(".gif")) {
                ImageIcon icon = new ImageIcon(s);
                setIcon(icon);
            }
        }
        return this;
    }
}

JListEditorコンポーネントにも、内部に保持しているJListコンポーネント に独自のListCellRendererを設定できるよう、setCellRenderer()メソッド (リスト16)を追加し、 JListにImageCellRendererを設定するコードを JListEditorSampleに追加します。(リスト17)

リスト16: JListEditor.javaに追加するコード
    /**
     * ListCellRenderer を設定します。
     * @param renderer 設定するListCellRenderer
     */
    public void setCellRenderer(ListCellRenderer renderer) {
        list.setCellRenderer(renderer);
    }
リスト17: JListEditorSample.java(変更後)
import javax.swing.*;

public class JListEditorSample {
    public static void main(String args[]) {
        JFrame f = new JFrame("JListEditorSample");
        f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        JListEditor editor = new JListEditor();
        editor.setCellRenderer(new ImageCellRenderer());
        f.getContentPane().add(editor);

        // WindowInvoker を利用
        SwingUtilities.invokeLater(new WindowInvoker(f));
    }
}

gihyo.pngとjavapress.gifという二つのイメージファイルも用意しておきます。 ImageCellRendererは、文字列が.pngまたは.gifで終わる場合は イメージを表示するようにしました。 ListCellRendererを追加したコードを動作させてみましょう。 入力フィールドに「gihyo.png」と「javapress.gif」を入力し、 追加した結果が図12です。 期待したとおり、JListの要素にイメージも表示されるようになりました。

図12: JListEditorSample(修正後)の実行結果

他のコンポーネントにも応用してみよう!

ここで例として取り上げたJListのように、 GUIコンポーネント(すなわちView)に対するモデルが存在し、 モデル側のデータを変更すると コンポーネントの表示が自動的に更新されるといった振る舞いや、 StrategyパターンによるRendererクラスを変更することで 描画方式を変更可能にした設計は、 JListに限らず他の多くのSwingコンポーネントで採用されています。

このJListで学んだ方法は、 他のSwingコンポーネントを使おうとするときにも容易に応用できる、 Swingコンポーネントの一般的な利用方法なのです。



まとめ

本稿では、「Swingのスレッドポリシー」「GrigBagLayout」「AbstractAction」 「コンポーネントの設計と使用」の4つのポイントに焦点を絞って Swingプログラミングに役立つ知識を紹介してきました。

Swingをあまり知らなくても、 それなりのアプリケーションを作ることは可能です。 しかし、Swingについてのちょっとした知識があれば、 プログラミング・スタイルが大きく変わるのを読者の皆様に 実感してもらいたい、というのを目標にしてこの記事を執筆してみましたが、 いかがだったでしょうか。

Swingのプログラミング・スタイルが分かってくると、 Swingアプリケーションを作るのが非常に楽しくなります。 みなさんも、SwingでGUIにチャレンジしてみませんか?






参考文献
日本語版J2SE1.4.0ドキュメント: http://java.sun.com/j2se/1.4/ja/docs/ja/index.html
Threads and Swing : http://java.sun.com/products/jfc/tsc/articles/threads/threads1.html
SpringLayoutマネージャ(IBM developerWorks) : http://www-6.ibm.com/jp/developerworks/java/031114/j_j-mer09173.html
UMLによるアーキテクチャパターン http://www.mamezou.com/tec/Tips/umlForumJp2001/umlArchi.html
How to Use Tool Bars : http://java.sun.com/docs/books/tutorial/uiswing/components/toolbar.html




HOME お勧め書籍 / Profile / リンク