[Java How To Programming] [Home on 246net] [Home on Alles net]
Powered by SmartDoc

Swing

2012/02/19 GMT
Toru Takahashi
JDK1.2から標準搭載されたGUIツールキット(クラス・ライブラリ)。従来のAWTに比べて機能が大幅に増えました。AWT1.1から取り入れられたライトウェイトコンポーネントがほぼ全面的に使われている他、ダブルバッファリング、実行時に変更可能なLook&Feelなどが特徴です。

目次

はじめに

実行環境

JDK1.2以降では標準APIとなっています。

Swingの性能

メーリングリスト、雑誌記事等では「Swingは遅い」とよく語られているが、本当のところどうなのだろうか?

"私のFreeBSD環境では、・・・200MHz Pentium MMXではさほどストレスは感じませんでした。また、Windows95環境のNetscape上でも動作をさせてますが、こちらはPentium MMX200MHz+64MBという環境でも実用的と感じる速度では動作しませんでした。"

(引用:技術評論社JAVA PRESS誌Vol1、「JFCのすべて」田口毅著)

同じCPUパワーだとWindows95の方が重く、UNIXのX Window上の方が軽快に動くようです。世の中Windows95ユーザ数が多いので、必然的に遅いという声が大きくなるのかもしれません。

1998.4にリリースされたJDK1.1.6はパフォーマンス・チューンが進み、またJITコンパイラを標準装備しており、Swingの実行性能も前に比べて大幅に向上しています。それでももったりとした重さは残っていますが・・・。

1999.8にプレリリースされたJDK1.3(β)ではHotSpot Client VMが搭載され、Swingライブラリもチューニングが進み、かなり性能が向上しています。また、マシン性能も1998初頭のPentium II 333MHzから1999夏にはPentium III 600MHzと倍以上に速くなっていることもあり、体感性能は大幅に向上していると考えられます。

2001.8にプレリリースされたJDK1.4β2ではSwing廻りのパフォーマンス向上が行われています。マシン性能も、CPUクロックについてはPentium IV 2.0GHzと1999年中頃からは3倍強、PCのネックであったメモリバスの帯域もRDRAM/DDRの登場で大きくなっています。

3Dグラフィックス・アクセラレータの急速な性能向上・低価格化により、WindowsならDirect3D、UnixならOpenGLを用いた高速な描画処理環境が広まっています。3Dグラフィックス・アクセラレータの性能はこの5年で10倍の向上が実現されており、Swing(Java2D)においてもこのグラフィックス・アクセラレータを内部で利用してより描画の高速化を実現するようになっています。

Swingは単一スレッド

Swingライブラリは、スレッドセーフではありません。単一スレッド(イベントディスパッチスレッド)からのみアクセス可能な設計となっています。ただし、JComponentのrepaint(), revalidate(), invalidate()は任意のスレッドから呼び出せます。通常は、イベントリスナインタフェースで定義されるイベントハンドラメソッド(例えばactionPerformed, propertyChanged, paint, update)からだけSwingコンポーネントにアクセスするようにプログラムを記述します。もし、どうしても別スレッドからSwingにアクセスするには、SwingUtilitiesクラスのinvokeLater(),またはinvokeAndWait()を使います。

Swingプログラム

GUIアプリケーションでは、プラットフォームが使用しているウィンドウシステム(UNIXならX Window)上に、何らかのウィンドウを作成する必要があります。このウィンドウはJavaではなくOSもしくはウィンドウシステムを呼び出して作成するため、ヘビーウェイトコンポーネントと呼ばれます。(したがって、ヘビーウェイト/ライトウェイトは、処理が速い/遅いという意味ではありません)

Swingでは、以下の4つのコンポーネントがヘビーウェイトに該当します。プログラムではまずこのいずれかのクラスをインスタンス化し、その上にライトウェイトなGUI部品を貼っていくことになります。

JWindow
ウィンドウシステム上(画面上)でウィンドウの移動・リサイズ枠・クローズ・アイコン化ボタンといったデコレーションがつかない。
JFrame
アプリケーションが使用するもっとも一般的なウィンドウ。
JDialog
ダイアログ表示のためのウィンドウ。
JApplet
アプレットプログラムのときはこれを使用する。

Swingアプリケーション(JFrame版)の基本構造

ここでは、JFrameを使ったGUIアプリケーションの基本パターンを見ます。

JFrameの使用

JFrameクラスを継承または合成の形で使用します。継承の方が簡単ですが、その分制約もあります。

public class MyApplication extends JFrame {
      :
}

Swingクラスは、javax.swingパッケージとして提供されます。

ウィンドウクローズ処理

JFrameインスタンスに、ウィンドウクローズ時の処理方法を設定します。ウィンドウシステム上でクローズボタンもしくはウィンドウメニュー上でクローズを選んだ時にアプリケーションを終了させるためです。

JFrame frame = new MyApplication();
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

setDefaultCloseOperationで指定可能な定数を以下に示します。これらの定数は、javax.swing.WindowConstantsインタフェースで定義されます。しかし、JFrameクラスがWindowConstantsインタフェースをimplementsしているので、ものの本などではJFrameの定数としてコーディングされることが多い(むしろこちらの方が主流)です。

ウィンドウクローズ時の操作を指定するWindowConstans定数
定数 内容 windowClosing呼出し windowClosed呼出し
DISPOSE_ON_CLOSE (JInternalFrameのデフォルト)ウィンドウを消去し、ウィンドウのリソースを破棄する。WindowListenerのwindowClosingとwindowClosedが呼ばれる。
DO_NOTHING_ON_CLOSE 何もしない。WindowListenerのwindowClosingが呼ばれる。windowsClosedが呼ばれるには、windowClosingの中でdispose()を呼ぶ必要がある。
EXIT_ON_CLOSE JVMを終了する。WindowListenerのwindowClosingが呼ばれる。
HIDE_ON_CLOSE (JFrame/JDialogのデフォルト)ウィンドウの表示を消去するがウィンドウそのものは存続し再表示が可能。WindowListenerのwindowClosingが呼ばれる。

JFrameにGUIコンポーネントを配置

ボタン、ラベル、リストなどのSwingコンポーネント(ライトウェイト)をJFrameのようなヘビーウェイトに貼る際は、AWT1.1以前のように直接addメソッドを呼ばず、JFrameが持つcontentPaneを取得し、このcontentPaneに対してaddメソッドを呼び出す必要があります(1)

frame.getContentPane().add(button);

SwingコンポーネントはJContainerクラス上に配置できますが、ヘビーウェイトなウィンドウ(JFrame,JWindow, JDialog, JApplet)はJContainerを継承していません。

そこでこれらのクラスは内部にSwingコンポーネントを配置するためのJContainerを継承しているcontentPaneを保持し、コンポーネントの管理・レイアウトを行うようになっています。※JInternalFrameはライトウェイトコンポーネントですが、同様にcontentPaneに管理を委ねています。

  1. JDK1.5(J2SE 5.0:"Tiger")からは、直接JFrameオブジェクトのaddメソッドを呼び出してもいいように改良されてます。IBM developerWorksのサイトで公開されている記事「Tigerを使いこなす:Tigerで痛み止め」に詳しく解説されているので参考にされたい。

JFrameのウィンドウサイズ

コンポーネントを配置後、必要最小限の大きさとします。

frame.pack();

また、サイズを明示的に指定することも可能です。

frame.setSize(800, 600);

画面上の表示位置を指定することも可能です。指定する座標は、画面上におけるJFrameウィンドウの左上隅の位置です。

frame.setLocation(100, 100);

サイズと位置の両方を一度に明示的に指定することも可能です。

frame.setBounds(800, 600, 100, 100);

位置を指定しない場合、画面左上に表示されますが、以下の指定により画面中央に表示させることができます。

frame.setLocationRelativeTo(null);

JFrameのウィンドウを画面上へ表示する

ヘビーウェイトなウィンドウを、ウィンドウシステム上で可視化(表示)します。

frame.setVisible(true);

プログラムコード例

JDK 1.5 (Java 5.0)以降用のコード例。

HelloSwing
/**
 * MyApplication.java
 *   for Java 2 SE, v.1.5
 * 
 * @author Toru Takahashi
 * @version $Revision: 1.3 $
 */
import javax.swing.JFrame;
import javax.swing.JButton;
import javax.swing.SwingUtilities;
import java.awt.event.ActionListener;
import java.awt.event.ActionEvent;

public class MyApplication extends JFrame {

    public MyApplication() {
	super("MyApplication");
	helloButton = new JButton("Hello, new swing!");
	helloButton.addActionListener(new ActionListener() {
		public void actionPerformed(ActionEvent ev) {
		    System.out.println("Hello, Kestrel!");
		}
	    });
	add(helloButton);
    }
    
    private static void createAndShowGui() {
	JFrame frame = new MyApplication();
	frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
	frame.pack();
	frame.setVisible(true);
    }

    public static void main(String[] args) {
        SwingUtilities.invokeLater(new Runnable() {
            public void run() {
                createAndShowGui();
            }
        });
    }

    private JButton helloButton= null;
} // MyApplication
mainスレッドからGUIを構築するのは危険

イベントディスパッチスレッド以外でSwingコンポーネントをアクセスするのはスレッド安全性上危険。mainメソッドでSwingを書かない訳を参照。

以下のコードは危険な例。

    public static void main(String[] args) {
	JFrame frame = new MyApplication();
	frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
	frame.pack();
	frame.setVisible(true);
    }

モーダル制御

あるウィンドウを表示している時、特にダイアログで、他のウィンドウへのユーザー操作をできないように操作をブロックしたいことがあります。この制御をモーダル制御といいます。JDK 6から、ダイアログのプロパティModalTypeに以下のモーダル種類を設定することができるようになっています(java.awt.Dialog.ModalityType列挙型)。コンストラクタの引数またはsetModalityTypeメソッドを使います。

モーダル種類(ModalityType)
種類 内容 指定
モードレス 他のウィンドウをブロックしない ModalityType.MODELESS
ドキュメント・モーダル 同じドキュメント(トップレベル・ウィンドウからの親子階層)の親方向のウィンドウをブロックする ModalityType.DOCUMENT_MODAL
アプリケーション・モーダル 同じアプリケーションのウィンドウをブロックする(親方向のウィンドウを除く) ModalityType.APPLICATION_MODAL
ツールキット・モーダル 同じツールキットのウィンドウをブロックする(親方向のウィンドウを除く) ModalityType.TOOLKIT_MODAL

JDK 5までのモーダル指定は、アプリケーション・モーダルとして扱われます。

モーダルの影響を特定ウィンドウだけ除外する

特定のウィンドウをモーダルによるブロックから除外したい場合、そのウィンドウのModalExclusionTypeプロパティに除外を設定します(java.awt.Dialog.ModalExclusionType列挙型)。設定には、java.awt.WindowクラスのsetModalExclusionTypeメソッドを使います。

参考文献

スクロール表示を行う

画面上に表示する大きさよりも大きいテキスト、イメージ、リストなどを表示したいときに一般的にスクロール機能付きコンポーネントが使われます。SwingではJScrollPaneクラスが提供されています。

JButtonのフォーカス枠表示

JButtonクラスを使ったボタンは、デフォルトではフォーカスを示す枠が表示されます。下記の画面例で、ボタンの文字"I'm a Swing button!"を囲んでいる薄紫色の枠です。

このフォーカス枠表示は、AbstractButtonクラスの次のメソッドで有効/無効とすることができます。

public void setFocusPainted(boolean b)
        b - true  フォーカス枠表示有功
            false フォーカス枠表示無効

また、フォーカスを持つコンポーネントをプログラム中から指定することもできます。JComponentクラスのメソッド、requestFocus()です。

ダブルバッファリング

Swingでは、描画を更新するときに表示がちらつかないようにダブルバッファリングを行う機能を保持しています。Swingのダブルバッファは画面全体をカバーする単一のオフスクリーンバッファによって実現され、RepaintManagerが管理する(2)。ダブルバッファリングは個々のコンポーネント毎には制御されません。コンポーネントの収まるコンテナ階層の親の1つでダブルバッファリングが行われる場合、コンポーネントはRepaintManagerのオフスクリーンバッファに描画されてからオンスクリーンにコピーされます。

JComponentにsetDoubleBuffered(boolean)というメソッドがあるので、個々のコンポーネント毎にダブルバッファの制御が行えると思ってしまうが、それは誤りです。デフォルトでは、JRootPaneとJPanelだけがダブルバッファリング有効となっています。他のコンポーネントは大抵の場合このどちらかのクラスのコンテナ階層にaddされるので、自動的にダブルバッファリングされます。(例えそのコンポーネント自身をsetDoubleBuffered()メソッドで無効にしたとしても)

ダブルバッファリングを無効にするには、

  1. コンテナ階層の上位にいるJRootPaneまたはJPanelのダブルバッファリングを無効にする
  2. RepaintManagerにダブルバッファリングを無効にしてもらう
RepaintManager rm = RepaintManager.currentManager(component);
rm.setDoubleBufferingEnabled(false);
  1. JDK 6からは、ウィンドウ毎に別々のオフスクリーンバッファを持つように改善されています。

オフスクリーンの注意点

いろいろ実験していて妙な現象に出逢いました。ダブルバッファリングが有効な時は、コンポーネントのpaint()メソッドに渡されるGraphicsオブジェクトは、SwingのRepaintManagerが管理しているオフスクリーンバッファ(BufferedImageオブジェクト)のGraphicsオブジェクトです。これは前述のとおり画面全体で1つだけ存在するので、全てのコンポーネントがこのオフスクリーンバッファへ描画していることになります。

部分書き換え処理時のゴースト描画

そのせいか、paint()で受け取ったGraphicsオブジェクトのうち一部分だけ書き変える処理をすると、画面に描画されるときに本来そこにはないコンポーネントが表示されたりすることがあります。

import java.awt.BorderLayout;
import java.awt.Graphics;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.SwingUtilities;
import javax.swing.JPanel;
import javax.swing.RepaintManager;

/**
 * DoubleBufferTest.java
 *
 *
 * Created: Wed Nov 15 01:03:05 2000
 *
 * @author 
 * @version
 */
public class DoubleBufferTest extends JFrame {
    JButton button = new JButton("Push Me");
    final TestPanel panel = new TestPanel("Hello, Swing world!");

    public DoubleBufferTest() {
	super("Swing double buffer test");
	button.addActionListener(new ActionListener() {
		public void actionPerformed(ActionEvent ev) {
		    panel.setVisible(true);
		}
	    });
	getContentPane().add(button, BorderLayout.SOUTH);
	getContentPane().add(panel, BorderLayout.CENTER);
    }

    private static void createAndShowGui() {
	JFrame frame = new DoubleBufferTest();
	frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
	frame.setBounds(100, 100, 300, 160);
	frame.setVisible(true);
    }

    public static void main(String[] args) {
        SwingUtilities.invokeLater(new Runnable() {
            public void run() {
                createAndShowGui();
            }
        });
    }

    class TestPanel extends JPanel {
	String message = null;
	TestPanel(String aMessage) {
	    RepaintManager.currentManager(this)
		.setDoubleBufferingEnabled(true);
	    setOpaque(true);
	    message = aMessage;
	    setVisible(false);
	}

	public void paintComponent(Graphics g) {
	    g.drawString(message, 100, 60);
	}
    }
} // DoubleBufferTest
setDoubleBufferingEnabled setOpaque 表示
true true ボタンの幻影が現れる
true false ボタンの幻影が現れない
false true ボタンの幻影が現れない
false false ボタンの幻影が現れない

paintComponentをオーバーライドするときに、super.paintComponent(g)を喚ぶように修正すると、幻影は現れなくなります。不透明なコンポーネント(setOpaque==true)は、paintComponentをオーバーライドするときには、必ずsuper.paintComponentを喚ぶようにするという必要があることが結論です。

BugId4272402

"RepaintManager.getOffscreenBuffer not erasing reused offscreen buffer"という問題が、SunのBug Databaseに登録されています。なお、Closed, will not be fixedなので、これはこのままということになっています。

Graphics2Dでのアフィン変換

JFrame等に自前のJava2D描画コンポーネントを貼ったします。オーバーライドしたpaintComponent(Graphics g)に渡されるGraphicsオブジェクトがデフォルトで保持しているAffineTransformオブジェクトの内容は、直感的に予想される[0 1 0][1 0 0](恒等変換)ではないのです。

JFrame等の描画領域全体をスクリーン座標系として使用していると思われ、引数で渡されてくるGraphics2DオブジェクトからgetTransform()で取り出したAffineTransformは、JFrameの全体(RootPane?)の中で自前コンポーネントが位置する座標分の並行移動が設定されています。

Graphics2DのデフォルトAffineTransform
public MyCanvas extends JComponent {
    ...
    public void paintComponent(Graphics g) {
        super.paintComponent(g);
        Graphics2D g2d = (Graphics2D)g;

        Logger.global.info(g2d.getTransform().toString());
        ...
        g2d.dispose();
    }
    ...
}

上述でログ出力を見ると例えば以下のような出力が得られます。

情報: AffineTransform[[1.0, 0.0, 0.0], [0.0, 1.0, 23.0]]
2005/06/19 12:03:52 \
    jp.gr.java_conf.torutk.exp.java2d.affine.AffinePanel \
    paintComponent

このデフォルトのアフィン変換では、自前コンポーネントの左上隅(0, 0)が(0, 23)に座標変換されていることになります。ちなみに上記の例は、JFrameのcontentPaneでCENTERに自前コンポーネントを貼り、setJMenuBarメソッドでJFrameにメニューバーを設定した場合である。丁度JMenuBarのメニュー分だけずれています。したがって、以下のように渡されてきたアフィン変換設定を無効にするコーディングはやってはいけません。

やってはいけない例
    public void paintComponent(Graphics g) {
        super.paintComponent(g);
        Graphics2D g2d = (Graphics2D)g;
        g2d.setTransform(
	    AffineTransform.getRotateInstance(Math.toRadians(30))
        );
        ...

渡されてきたアフィン変換に結合させるようにアフィン変換を設定しなくてはなりません。

こうすればよい例
    public void paintComponent(Graphics g) {
        super.paintComponent(g);
        Graphics2D g2d = (Graphics2D)g;
        g2d.transform(
	    AffineTransform.getRotateInstance(Math.toRadians(30))
        );
        ...

canvasをswingで実現

AWTには、Canvasというコンポーネントがあり、グラフィックスを描画するのに定番となっていました。ところが、SwingになるとCanvasに相当するクラスが見当たらりません。どうしたらよいのでしょうか?

Swingでは、JPanelを継承してCanvas相当のクラスとして使うのが簡単です。グラフィックス描画処理が、AWTのCanvasと違ってpaintメソッドではなく、paintComponentメソッドとなっている点に注意します。また、不透明(opaqueがtrue)であるコンポーネントの場合、paintComponentメソッドの中でまずsuper.paintComponent(g);を喚びだして背景描画を行った後、アプリケーションの描画処理を記述します。

Emacs JDEを開発に使用しているならば、FileメニューのJDE New→Other...を選び、ミニバッファでTemplate名を聞いてくるのでSwing Appと入力すると、Swingプログラムの雛型が生成されます。この雛型には、JPanelを継承したCanvasクラスが含まれているので参考になります。

JDEで生成したSwingアプリの雛型
package myswing;

import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Color;
import java.awt.geom.Ellipse2D;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JMenuBar;
import javax.swing.JMenu;
import java.awt.event.ActionEvent;
import javax.swing.AbstractAction;

/**
 * MySwingApp.java
 *
 *
 * Created: Wed Mar 07 03:09:40 2001
 *
 * @author 
 * @version
 */

public class MySwingApp extends JFrame {
    class Canvas extends JPanel {
	public Canvas() {
	    setSize(getPreferredSize());
	    Canvas.this.setBackground(Color.white);
	}
	
	public Dimension getPreferredSize() {
	    return new Dimension(600, 600);
	}
	
	public void paintComponent(Graphics g) {
	    super.paintComponent(g);
	    Graphics2D g2d = (Graphics2D) g;
	    Ellipse2D circle = new Ellipse2D.Double(0d, 0d, 100d, 100d);
	    g2d.setColor(Color.red);
	    g2d.translate(10, 10);
	    g2d.draw(circle);
	    g2d.fill(circle);
	}
    }
    
    public MySwingApp() {
	super("Swing Application generate by JDE");
	setSize(300, 300);
	addWindowListener(new WindowAdapter() {
		public void windowClosing(WindowEvent e) {System.exit(0);}
		public void windowOpened(WindowEvent e) {}
	    });
	setJMenuBar(createMenu());
	getContentPane().add(new JScrollPane(new Canvas()));
    }
    
    public static void main(String[] args) {
	MySwingApp f = new MySwingApp();
	f.show();
    }
    
    protected JMenuBar createMenu() {
	JMenuBar mb = new JMenuBar();
	JMenu menu = new JMenu("File");
	menu.add(new AbstractAction("Exit") {
		public void actionPerformed(ActionEvent e) {
		    System.exit(0);
		}
	    });
	mb.add(menu);
	return mb;
    }
} // MySwingApp

階層表示を行う

Windows等のGUIデスクトップでは、ウィンドウは階層的に重なり合っています。また、地図上にシンボルを表示するような場合、地図表示とシンボル表示を別々に扱うことができると便利であるし、再利用性が高くなります。このような表示には、JLayeredPaneを使うと便利です。

JLayeredPaneの注意点

layoutManagerはnull

JLayeredPaneは、レイアウトマネージャが存在しない(null)ため、JLayeredPaneに登録するコンポーネントは明示的に大きさと位置を与えておかなければなりません。

add(Component c, int i)でもエラーとならない

JLayeredPaneオブジェクトにコンポーネントを追加するときは、add(Component c, Integer layer)メソッドでレイヤー番号をIntegerオブジェクトで指定します。このとき、Integer型ではなくint型でコーディングしてしまっても、コンパイルエラーとはならないので、注意が必要です。うっかりミスの範疇ですが、よく陥るので心されたし。

ルックアンドフィールの入れ換え

Swingのルックアンドフィールは動的に入れ換えができるような設計となっています。ルックアンドフィールは、GUI部品(ボタン、ウィンドウ枠、メニュー、ツリーなど)の見栄え、見た目の振舞いのことです。

LookAndFeelの種類

JDKにはプラットフォームによって多少の差異があります。以下、OSとjavax.swing.UIManagerで取得したLookAndFeelを示します。

OS毎のLookAndFeel取得結果(JDK 6)
LookAndFeel種類 Windows Vista Solaris 10 x86 Linux(Cent OS 4.4)
Metal V V V
Nimbus V V V
CDE/Motif V V V
GTK+ - V V
Windows V - -
Windows Classic V - -

Metalは、Java独自のルックアンドフィールを定義したもので、デフォルトのルックアンドフィールとなっています。また、テーマ(Theme)を設定することが可能で、使用するカラーセット、フォントをカスタマイズすることができるようになっています。

Nimubsは、JDK 6 Update5から追加されたJava独自のルックアンドフィールを定義したもので、Metalに比べて洗練されたデザインとなっています。

CDE/Motifは、X Window System上で採用されているMotifツールキットが提供するルックアンドフィールに似せたものです。CDEは、Common Desktop Environmentの略で、Motifをベースに幾つかのUNIXベンダー(IBM, HP, Sun)がデクストップ環境を統一した規格です。

GTK+は、X Window System上のGNOME等で使用されているツールキットが提供するルックアンドフィールに似せたものです。

Windowsは、MicrosoftのWindows XPに使用しているルックアンドフィールです。ClassicはWindows 2000スタイルです。

JDKに標準で添付されているデモプログラム(SwingSet2)を実行してみると、詳しい違いを見てとれますので、ぜひ試してください。

LookAndFeelの指定方法(プログラム上で)

ルックアンドフィールを指定するには、javax.swing.UIManagerクラスを知る必要があります。

ルックアンドフィール一覧を得る

実行しているJava環境が提供しているルックアンドフィールの一覧を次のように取得します。下記は、JDEEに標準添付のBeanShellを使ってJavaクラスにアクセスした例です。

BeanShell 1.1a16 - by Pat Niemeyer (pat@pat.net)
bsh % lafs = UIManager.getInstalledLookAndFeels();
bsh % print(lafs);
Array: [Ljavax.swing.UIManager$LookAndFeelInfo;@329f3d {
   javax.swing.UIManager$LookAndFeelInfo[Metal javax.swing.plaf.
metal.MetalLookAndFeel]
   javax.swing.UIManager$LookAndFeelInfo[CDE/Motif com.sun.java.
swing.plaf.motif.MotifLookAndFeel]
   javax.swing.UIManager$LookAndFeelInfo[Windows com.sun.java.
swing.plaf.windows.WindowsLookAndFeel]
}
bsh %

ここで得られる情報は、UIManagerクラスのインナークラスLookAndFeelInfo型の配列であり、さらにgetClassNameメソッドでクラス名文字列を得ています。

指定した種類のルックアンドフィールを取得する

javax.swing.UIManagerクラスには目的に応じた種類のルックアンドフィールを取得するメソッドが用意されています。

前者は、Java独自のルックアンドフィールであるMetalを提供するクラス名を返却するメソッドであり、後者はプログラムが実行されているマシン(プラットフォーム)に近いルックアンドフィールを提供するクラス名を返却するメソッドです。

Windowsマシン上で実行
BeanShell 1.1a16 - by Pat Niemeyer (pat@pat.net)
bsh % lafName = UIManager.getCrossPlatformLookAndFeelClassName();
bsh % print(lafName);
javax.swing.plaf.metal.MetalLookAndFeel
bsh % lafName = UIManager.getSystemLookAndFeelClassName();
bsh % print(lafName);
com.sun.java.swing.plaf.windows.WindowsLookAndFeel
bsh % 
Linux(GNOME)上で実行
BeanShell 1.1a16 - by Pat Niemeyer (pat@pat.net)
bsh % lafName = UIManager.getCrossPlatformLookAndFeelClassName();
bsh % print(lafName);
javax.swing.plaf.metal.MetalLookAndFeel
bsh % lafName = UIManager.getSystemLookAndFeelClassName();
bsh % print(lafName);
javax.swing.plaf.metal.MetalLookAndFeel
bsh %

Linux(GNOME)上で実行すると、SystemLookAndFeelはMetalになっているのがわかります。Solarisで実行した場合、CDE/Motifになっていました。この方法を使うと、どのプラットホーム上で実行されているかを事前に決め打ちせずに、実行時に指定することができます。

指定したルックアンドフィールへの変更

では、上述の方法で取得したルックアンドフィールへ切り換えてみます。

LookAndFeelの切替
UIManager.setLookAndFeel("com.sun.java.swing.plaf.windows.WindowsLookAndFeel");
SwingUtilities.updateComponentTreeUI(frame);

ルックアンドフィールの切替は、javax.swing.UIManagerクラスを使用します。setLookAndFeel()メソッドで、指定するルックアンドフィールクラスのクラス名を引数に渡すか、あるいはLookAndFeelクラス自身を渡します。それから、画面に表示されている各GUI部品を切替えたルックアンドフィールで描画し直すために、javax.swing.SwingUtilitiesクラスのupdateComponentTreeUI()メソッドを呼びます。このとき引数には、UI部品の階層上最も上位のオブジェクト(例えばJFrameオブジェクト)を渡します。

ルックアンドフィール切替テストプログラム
/**
 * LookAndFeelTest.java
 *
 * Created: Sun Oct 07 22:51:09 2001
 *
 * @author <a href="mailto:torutk@alles.or.jp">Toru TAKAHASHI</a>
 * @version
 */
import java.awt.BorderLayout;
import java.awt.event.ActionEvent;
import javax.swing.AbstractAction;
import javax.swing.JButton;
import javax.swing.JCheckBox;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JMenu;
import javax.swing.JMenuBar;
import javax.swing.JTextField;
import javax.swing.SwingUtilities;
import javax.swing.UIManager;

public class LookAndFeelTest extends JFrame {
    /** Look And Feel情報を保持する */
    UIManager.LookAndFeelInfo[] lookAndFeelInfos;
    // デモ用にいくつかのUIコンポーネントを貼り付ける
    JButton button = new JButton("Push Me!");
    JLabel label = new JLabel("Swing Look And Feel Selecting Demo");
    JCheckBox checkBox = new JCheckBox("Check Me!");
    JTextField textField;

    public LookAndFeelTest() {
        super("Look & Feel Test");
        setDefaultCloseOperation(EXIT_ON_CLOSE);
        lookAndFeelInfos =
		    UIManager.getInstalledLookAndFeels();
        setJMenuBar(createMenu());

        getContentPane().add(label, BorderLayout.NORTH);
        getContentPane().add(button, BorderLayout.SOUTH);
        getContentPane().add(checkBox, BorderLayout.WEST);
        textField = new JTextField(
		    UIManager.getLookAndFeel().getDescription());
        getContentPane().add(textField, BorderLayout.CENTER);
    }
    /**
	 * Look&Feel切替を行うメニュー作成
     * 実行中のJava環境が持つLookAndFeel一覧を取得し、
     * それをメニューに加える。
     */
    public JMenuBar createMenu() {
        JMenuBar menuBar = new JMenuBar();
        JMenu lafMenu = new JMenu("LookAndFeel");
        for (int i=0; i<lookAndFeelInfos.length; i++) {
            final String lafClassName =
			    lookAndFeelInfos[i].getClassName();
            lafMenu.add(new AbstractAction(lookAndFeelInfos[i].getName()) {
                    public void actionPerformed(ActionEvent ev) {
                        changeLookAndFeel(lafClassName);
                    }
                });
        }
        menuBar.add(lafMenu);
        return menuBar;
    }
	/**
     * ルックアンドフィールを切替るメソッド。
     * 引数に指定された文字列のルックアンドフィールクラスをUIManagerに
	 * 登録し、画面を描画し直す。
	 */
    private void changeLookAndFeel(String lafClassName) {
        try {
            UIManager.setLookAndFeel(lafClassName);
            SwingUtilities.updateComponentTreeUI(this);
            textField.setText(UIManager.getLookAndFeel().getDescription());
            this.pack();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    private static void createAndShowGui() {
        JFrame window = new LookAndFeelTest();
        window.pack();
        window.setVisible(true);
    }

    public static void main(String[] args) {
        SwingUtilities.invokeLater(new Runnable() {
            public void run() {
                createAndShowGui();
            }
        });
    }
} // LookAndFeelTest

LookAndFeelの指定方法(プロパティ上で)

前節では、プログラム上でルックアンドフィールを切り換える方法を示します。それ以外にも、システム・プロパティに設定することでルックアンドフィールを指定する方法があります。

JREのシステムプロパティファイルに記述

起動時に、デフォルトのLookAndFeelを指定するには、swing.propertiesファイルを作成し、そこに記述します。ファイルは、Java Runtime Environmentディレクトリ下のlibディレクトリに置きます(フォント関係のプロパティファイルがある場所です)。例えばJDK1.4をC:\jdk1.4にインストールした場合、C:\jdk1.4\jre\libの下となります。

swing.properties設定例
# Swing properties
swing.defaultlaf=com.sun.java.swing.plaf.motif.MotifLookAndFeel

JavaVM起動時のコマンドラインに指定

デフォルトフォントを指定してプログラムを起動
classes> java \
    -Dswing.defaultlaf=com.sun.java.swing.plaf.windows.WindowsLookAndFeel \
    alpha.AppMain

Themeのカスタマイズ(Metal Look&Feelのみ)

Metal Look&Feelでは、使用するカラーセット(背景色、表示色など)やフォントをカスタマイズすることができます。デフォルトで適用されるテーマであるjavax.swing.plaf.metal.DefaultMetalThemeでは以下のような設定となっています。

DefaultMetalTheme設定項目
設定項目 デフォルト
コントロールテキストフォント コントロールまたはDialg-BOLD 12ポイント
メニューテキストフォント コントロールまたはDialg-BOLD 12ポイント
サブテキストフォント スモールまたはDialog-PLAIN 10ポイント
システムテキストフォント システムまたはDialog-PLAIN 12ポイント
ユーザテキストフォント ユーザまたはDialog-PLAIN 12ポイント
ウィンドウタイトルフォント コントロールまたはDialg-BOLD 12ポイント
プライマリカラー1 #666699
プライマリカラー2 #9999CC
プライマリカラー3 #CCCCFF
セカンダリカラー1 #666666
セカンダリカラー2 #999999
セカンダリカラー3 #CCCCCC

自分好みのテーマを記述するには、テーマを記述したMetalThemeクラスのサブクラスを作成します。設定を楽にするには、DefaultMetalThemeクラスのサブクラスを作成し、変更したい項目だけオーバーライドするとよいでしょう。

MyTheme.java
public class MyTheme extends DefaultMetalTheme {
    public String getName() { return "Mine"; }

    private final ColorUIResource primary1 =
        new ColorUIResource(102, 102, 153);
    private final ColorUIResource primary2 =
        new ColorUIResource(128, 128, 192);
    private final ColorUIResource primary3 =
        new ColorUIResource(159, 159, 235);

    private FontUIResource controlFont =
        new FontUIResource("Dialog", Font.PLAIN, 18);
    private FontUIResource systemFont =
        new FontUIResource("Dialog", Font.PLAIN, 18);
    private FontUIResource userFont =
        new FontUIResource("Dialog", Font.PLAIN, 18);
    private FontUIResource smallFont =
        new FontUIResource("Dialog", Font.PLAIN, 14);

    protected ColorUIResource getPrimary1() {
        return primary1;
    }
    protected ColorUIResource getPrimary2() {
        return primary2;
    }
    protected ColorUIResource getPrimary3() {
        return primary3;
    }

    public FontUIResource getControlTextFont() {
        return controlFont;
    }
    public FontUIResource getSystemTextFont() {
        return systemFont;
    }
    public FontUIResource getUserTextFont() {
        return userFont;
    }
    public FontUIResource getMenuTextFont() {
        return controlFont;
    }
    public FontUIResource getWindowTitleFont() {
        return controlFont;
    }
    public FontUIResource getSubTextFont() {
        return smallFont;
    }
}

そして、GUIを具現化する前に、Themeを設定します。

MetalLookAndFeel.setCurrentTheme(new MyTheme());

もし、GUIが具現化された後にThemeを変更するには、UIManager.setLookAndFeel()とSwingUtilities.updateComponentTreeUI()あたりを使って通知してあげなくてはならないようです。

画像ファイルを読み書きする

Swingにおいて画像ファイルを読み書きする方法にはいくつかの方法があります。

HTMLテキストを利用してSwingコンポーネントに画像を表示

画像を表示する(読み込みだけ)ならば、SwingのコンポーネントにHTML形式テキストを与えて、HTMLレンダリング機能を使って画像を表示させることができます。

例えば、JLabelに通常のテキストの代わりにHTMLテキストを与えると、そのHTMLテキストを表示するのではなく、HTMLテキストに従ってレンダリングした結果を表示します。画像ファイルをJLabelを使って表示するなら、下記のようになります。

JLabelにHTMLテキストを指定
JLabel imageLabel =
  new JLabel("<html><img src=\"http://somewhere/sample.jpeg\">");

ここで、JLabelのHTMLレンダリング機能を利用してファイル選択ダイアログで選択した画像ファイルを表示するプログラム例を紹介します。

HTMLテキストを利用した画像表示プログラム
/**
 * ImageViewer.java
 *
 * Created: Sun Mar 17 16:09:56 2002
 *
 * @author <a href="mailto:torutk@alles.or.jp">Toru TAKAHASHI</a>
 * @version
 */
import java.awt.BorderLayout;
import java.awt.event.ActionEvent;
import java.io.File;
import javax.swing.AbstractAction;
import javax.swing.JButton;
import javax.swing.JFileChooser;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;

/**
 * 画像ファイルを表示するビューア
 */
public class ImageViewer extends JFrame {
    JLabel imageLabel; // 画像を表示するラベル

    public ImageViewer(String title) {
        super(title);
        imageLabel = new JLabel();
        getContentPane().add(imageLabel, BorderLayout.CENTER);
        // 画像ファイル選択ダイアログを起動するボタン
        JButton openButton = new JButton("Open File");
        openButton.addActionListener(new OpenAction());
        getContentPane().add(openButton, BorderLayout.SOUTH);
    }

    /**
     * 選択された画像ファイルを表示する
     */
    void readFromFile(File file) {
        String text = "<html><body><img src=\"file:" + file.getPath() +
            "\"></body>";
        imageLabel.setText(text);
        pack();
    }
    
    public static void main(String[] args) {
        JFrame f = new ImageViewer("Image Viewer");
        f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        f.pack();
        f.setVisible(true);
    }

    /**
     * ファイル選択ダイアログを表示する。
     * ファイルが選択されたらreadFromFileメソッドを起動する
     */
    class OpenAction extends AbstractAction {
        JFileChooser imageChooser = new JFileChooser();

        OpenAction() {
            imageChooser.setMultiSelectionEnabled(false);
        }

        public void actionPerformed(ActionEvent ev) {
            int ret = imageChooser.showDialog(ImageViewer.this,
                                              "Select Image File");
            if (ret == JFileChooser.APPROVE_OPTION) {
                readFromFile(imageChooser.getSelectedFile());
            }
        }
    }
} // ImageViewer

APIを利用して画像を読み書きする

読み込んだ画像に加工を行いたい場合や、画像を別なファイルに書き込む場合は、HTMLレンダリング機能では不十分です。このときは、画像処理を行うために画像ファイルからImageオブジェクトへ読み込んだり、Imageオブジェクトから指定したフォーマットで画像ファイルに書き出したりするAPIを使用します。

Java 2 SE, ver.1.4のImageI/Oを利用する

Java 2 Standard Edition標準の画像I/O用APIです。対応フォーマットは少ないですが、プラグイン対応になっているため、今後サードパーティ等から多種のフォーマット用プラグインが登場すると思われます。

Java ImageI/O APIによる画像ファイル読み書き
import javax.imageio.ImageIO;
      :
    File inFile = new File("sample.gif");
    File outFile = new File("sample.png");
    BufferedImage image = ImageIO.read(inFile);
    ImageIO.write(image, "PNG", outFile);

入力ファイルの形式はファイル名の拡張子ではなくちゃんとデータフォーマットを見て判断します。出力ファイルのフォーマットを指定する文字列には以下のものが指定可能です。このリストはjavax.imageio.ImageIO#getImageWriterFormatNamesメソッドで取り出すことができます。

JAI:Java Advanced Imaging APIを利用する

Jimi:Java Image Management Interface APIを利用す る

JIMIを使うと、多種の画像フォーマットのファイルを簡単に読み書きできます。最も簡単なAPIは、com.sun.jimi.coreパッケージのJimiクラスが持つgetImageおよびputImageメソッドを使用するものです。引数に指定したファイル名の文字列の拡張子部分によって画像フォーマットを識別し、java.awt.Imageオブジェクトとファイルとの変換を行います。

Jimi APIによる画像ファイル読み書き
import com.sun.jimi.core.Jimi;
      :
    String inFileName = "sample.jpeg";
    String outFileName = "sample.tiff";
    Image image = Jimi.getImage(inFileName);
    Jimi.putImage(image, outFileName);

レイアウト

Swingには、コンポーネントを配置する際に絶対座標ではなく、一定のルールに沿って配置するレイアウトを使用するのが一般的である。画面サイズやフォントは可変であり、その変更に柔軟に対応することができる。

Swing標準レイアウト・マネージャ

Swingの標準レイアウト・マネージャ
種類 内容 備考
FlowLayout
CardLayout
BorderLayout
GridLayout
GridBagLayout
BoxLayout
OverlayLayout
SpringLayout J2SE 5.0(Tiger)から追加
GroupLayout JavaSE 6(Mustang)から追加

SpringLayout

HelloAgainプロジェクト Desktop編のアプリケーションで使用しており、レイアウト・マネージャ節に使用例を解説している。

GroupLayout

Swing Application Framework(JSR-296)の解説ページで作成したサンプル・プログラムのひとつで使用しており、「GroupLayoutを用いた画面レイアウト」節に使用例を解説している。

JTreeのモデル

ツリー構造のデータを表現するには、JTreeコンポーネントを使用すると便利です。身近なツリー構造のデータと言えば、ファイルシステムがそうです。JTreeコンポーネントに表示するデータにはいくつかの種類があります。簡単なものなら配列やVector型のデータを与えてもよいですが、複雑なデータの場合はインタフェースTreeModelまたはTreeNodeを実装するクラスを作成してJTreeに与えることになるでしょう。

JTable

JTableで列をソートする

Java 2 SE 5.0までのSwingライブラリのJTableにはある列をキーにソートする機能は含まれておりません。しかしながら、ソートはニーズが高い機能です。Java SE 6からは、javax.swing.table.TableRowSorterクラスが追加され、JTableをソートする機能が装備されるようになりました。

Java 2 SE 5.0までのJTableでソート機能を付加する方法については、SunのTutorial文書で紹介されています。

http://java.sun.com/docs/books/tutorial/uiswing/components/table.html#sorting

JTableは、Model-View分離構造で設計されています。Modelは、javax.swing.table.TableModelインタフェースを実装するクラス、Viewはjavax.swing.JTableクラスです。ソート機能を汎用的に付加する方法の一つとして、ModelとViewの間にソート機能を持つTableSorterクラスを挟んであげる方法が上記チュートリアルで紹介されている方法です。このTableSorterクラス自身もTableModelであり、これはデザインパターンのひとつデコレータパターンを適用した方法です。ソースコードTableSorter.javaは、Sunの上記チュートリアルからダウンロードできます。

簡単なTableSorterの使用例(断片)
TableModel model = new MyTableModel();
TableSorter sorter = new TableSorter(model);
JTable table = new JTable(sorter);
sorter.setTableHeader(table.getTableHeader());

文字列ではなく数値大小でのソート

AbstractTableModelを派生してTableModel実装を定義した際に、数値の列をソートすると、数値の大小ではなく、文字列としてソートされてしまいます。例えば、100より20の方が大きいと判断されます。これは、javax.swing.table.AbstractTableModelクラスの"Class getColumnClass(int columnIndex)"メソッドの実装が、常にObject.classを返却するためです。結果的に、ソート時に、toStringで取得した文字列を比較することになります。そこで、TableModel実装時に、このgetColumnClassを列に応じたクラスオブジェクトを返却するように定義します。次に、TableSorterクラスの"void setColumnComparator(Class, Comparator)"メソッドを呼んで、クラスオブジェクトとそれに対応する比較オブジェクトを設定します。

TableSorterにIntegerの比較方法を設定
sorter.setColumnComparator(Integer.class, new Comparator() {
    public int compare(Object o1, Object o2) {
        return ((Integer)o1).compareTo((Integer)o2);
    }
});

TableSorter改良

Keith R Bennett氏の記事"JTables with Class - MVC, Renderers, and the TableSorter"において、TableSorterを改良したNewTableSorterの例を示し、またそのソースコードNewTableSorter.javaが公開されています。

JTextField

編集中のJTextFieldの背景色を変更する

編集中のJTextFieldについて、背景色を編集中であることが一目瞭然となるように変更します。また、[Enter]キーを押さなくてもフォーカスが別のGUIコンポーネントに移った時点でActionイベントを発生させるようにします。

ここでは、以上の機能を追加したJTextField拡張クラスを作成します。

まず、編集中の状態は、JTextFieldにフォーカスが当たっている状態とします。フォーカスの状態は、FocusListenerをJTextFieldに登録することによって知ることができます。

FocusListnerの定義
    private class TextFieldFocusListener implements FocusListener {

        /**
         * フォーカスが当たったときは編集中として背景色を変更する。
         * @param anEvent <code>FocusEvent</code>オブジェクト
         */
        public final void focusGained(final FocusEvent anEvent) {
            setBackground(editBackground);
        }

        /**
         * フォーカスを失ったときは編集完了として背景色を本来に戻し、
         * テキスト入力完了イベント([Enter]キー入力状態)を発生させる。
         *
         * @param anEvent <code>FocusEvent</code>オブジェクト
         */
        public final void focusLost(final FocusEvent anEvent) {
            Color background = (Color)UIManager.get("TextField.background");
            setBackground(background);
            postActionEvent();
        }
    }

フォーカスが外れたときに、背景色を元の色に戻します。元の色はUIManagerから取得しています。あるいは、フォーカスが当たったときに(focusGainedメソッドで)JTextFieldからgetBackgroundメソッドで元の背景色を取得する方法もあります。

FocusListnerの登録
    private final void init() {
        addFocusListener(new TextFieldFocusListener());
        editBackground = Color.PINK; // デフォルトの編集中背景色
        addActionListener(new ActionListener() {
                public void actionPerformed(ActionEvent anEvent) {
                    transferFocus();
                }
            }
        );
    }

TextFieldの初期化時に、上述のFocusListener実装を登録するとともに、編集完了時に背景色を元に戻すため、ActionListenerでフォーカスを次のGUIコンポーネントへ移す処理を入れています。

時間のかかる処理を行う

Swingはシングルスレッドモデルです。したがって、Swingのイベントからディスパッチされたら速やかに処理を終えてスレッドをSwingに戻してあげなくてはなりません。どうしても、時間のかかる処理を行う必要がある場合には、別にスレッドを設けてそのスレッドに処理を行わせます。別スレッドからSwingを呼び出すときは、invokeLater()などを使ってEDT上で実行するようにします。

なお、JDK 6からはSwingWorkerクラスが追加され、別スレッドでの処理実行と経過および結果をSwingへ反映するのに便利な機能が利用できます。

JFileChooser

ファイル選択ダイアログです。アプリケーション内で、ユーザーにディレクトリやファイルを指定してもらう場合に使用します。

デフォルトのファイル選択ダイアログ

デフォルトのファイル選択ダイアログの使い方です。次のサンプルコードは、ファイルを開く用途でファイル選択ダイアログを表示します。

JFileChooser デフォルトの使用
import javax.swing.JFileChooser;
   :
JFileChooser fileChooser = new JFileChooser();
int ret = fileChooser.showOpenDialog(frame);
if (ret == JFileChooser.APPROVE_OPTION) {
    File file = fileChooser.getSelectedFile();
    ...
}  

ファイルを保存するときは、上述のコードで呼び出すメソッド"showOpenDialog"を"showSaveDialog"に変更します。

この使用方法では、ユーザーのホームディレクトリ(Windowsならユーザーのマイドキュメント)を初期ディレクトリとし、可視なファイル・ディレクトリをすべて表示し、選択可能なのはファイルで、同時選択可能数は1つです。

JFileChooserの動作制御

デフォルトのディレクトリを指定した場所にしたい
JFileChooserのコンストラクタ引数に指定する場所をFile型で渡します。
ディレクトリだけを選択できるようにしたい
setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY)メソッドを呼びます。ファイルとディレクトリ両方を選択できるようにする場合は引数にJFileChooser.FILES_AND_DIRECTORIESを指定します。
ダイアログのタイトルを指定した文字列にしたい
setDialogTitle("保存するファイルを設定してください")メソッドを呼びます。引数は任意の文字列です。
複数ファイル(またはディレクトリ)を選択可能にしたい
setMultiSelectionEnabled(true)メソッドを呼びます。この場合、選択されたファイル(複数)を取得するために、getSelectedFiles()メソッドを使用します。戻り値型はFile[]です。
所定の拡張子のファイルだけ表示したい
javax.swing.filechooser.FileNameExtensionFilterクラスのインスタンスを生成し、setFileFilterメソッドの引数に指定します。なお、setFileFilterではなくaddChoosableFileFilterで設定した場合、指定したfilterに対応するファイルのタイプがファイルのタイプリストに登場しますが、デフォルトは「すべてのファイル」となっているため、表示されるファイルはすべてとなっています。なお、ファイルのタイプに複数のフィルタを追加するが、デフォルトのフィルタにしない場合はaddChoosableFileFilterで設定します。

Tips集

ウィンドウ操作関係

ウィンドウのサイズが変わったら何か処理をする

ユーザがウィンドウの大きさを変更したことを感知して処理をしたい場合、java.awt.event.ComponentListenerを使う。componentResized()メソッドが呼ばれる。

JFrame f = new JFrame("ResizeAnnounce");
f.addComponentListener(new ComponentAdapter() {
    public void componentResized(ComponentEvent ev) {
      // リサイズされた時の処理
    }
  });

フォント関係

デフォルトのフォントを変更する

フォントを変更したい場合、コンポーネントのsetFontメソッドを呼んで個別に変更することができます。しかし、画面全体のフォントを変更することは大変です。

Metal Look&Feelでは、次の方法で全体を変更することができます。

Metal Look & Feelにおけるシステムプロパティ変更

Metal Lookl&Feelでは、以下の4種類の用途に使用するフォントがシステムプロパティで定義されています。

MetalLook&Feelのデフォルトフォント
種類 プロパティ名 デフォルト 用途
コントロール swing.plaf.metal.controlFont 12ptボールド チェックボックス、タイトル
スモール swing.plaf.metal.smallFont 10ptプレーン ツールティップ、キーボードショートカット
システム swing.plaf.metal.systemFont 12ptプレーン ツリービュー
ユーザ swing.plaf.metal.userFont 12ptプレーン テキストフィールド、テーブル

システムプロパティに、プロパティ名と指定したいフォントを設定することができます。フォントの指定方法は以下のとおりです。

デフォルトフォントを指定してプログラムを起動
classes> java -Dswing.plaf.metal.controlFont=Dialog-14 \
    alpha.AppMain

Metal Look & FeelにおけるTheme設定

多言語を同時に表示させる

多言語環境のプログラムを作成する場合、日本語ロケールでは日本語以外の言語の文字が□(俗称トーフ)で表示されてしまいます。ここでは、日本語と韓国語の場合について試してみました。

日本語Windows 2000で韓国語表示/入力を追加

日本語Windows 2000では、主ロケールの日本語以外に複数の言語環境を扱うことができます。ここでは韓国語の例を見てみます。まず、コントロールパネルの「地域のオプション」からシステムの言語設定のリストの韓国語にチェックを入れます。するとOSのメディアからフォント等をインストールします。また、韓国語を入力するために、韓国語IMEを同様に「地域のオプション」の[入力ロケール]タブにある[追加]ボタンを押し、入力ロケールとキーボードレイアウト/入力システムの双方に韓国語を選択して追加します。

Javaプログラムに日本語・韓国語表示

まず、ワードパッド上に日本語、韓国語混じりの文字列を用意します。

ワードパッド上の日本語・韓国語混じり文字列画面例

この文字列をWindowsのクリップボードへコピーし、Swingで作ったJavaプログラム上のテキストフィールドに貼り付けます。

韓国語が表示されない画面

これは、Javaプログラム(Swingライブラリ)の中で使用している論理フォント(Dialog等)を、実際のフォントにマッピングする設定が不足しているためです。日本語ロケールでJavaを実行する場合、[Java Runtime Directory]/lib/font.properties.jaというファイルに記述される設定が使用されます。そこで、ここに、韓国語の設定を追加することにします。

例えば、日本語用フォント・プロパティ・ファイルの一部を見ると次のようになっています。

Windows用のfont.properties.jaの一部
dialog.plain.0=Arial,ANSI_CHARSET
dialog.plain.1=\uff2d\uff33 \u30b4\u30b7\u30c3\u30af,SHIFTJIS_CHARSET
dialog.plain.2=WingDings,SYMBOL_CHARSET
dialog.plain.3=Symbol,SYMBOL_CHARSET

[中略]

filename.\uff2d\uff33_\u660e\u671d=MSMINCHO.TTC
filename.\uff2d\uff33_\u30b4\u30b7\u30c3\u30af=MSGOTHIC.TTC

filename.Arial=ARIAL.TTF

[中略]

filename.WingDings=WINGDING.TTF
filename.Symbol=SYMBOL.TTF

[中略]

fontcharset.dialog.1=sun.io.CharToByteMS932
fontcharset.dialog.2=sun.awt.windows.CharToByteWingDings
fontcharset.dialog.3=sun.awt.CharToByteSymbol

Javaプログラム中で論理フォントdialog(.plain)が指定されていると、Arial,MSゴシック、WingDings、Symbolの順に該当する文字のフォントを調べていきます。ここで韓国語の文字に対応するフォントが見つからないため、□(トーフ)が表示されてしまいます。そこで、このフォントリストに韓国語のフォントを追加します。

Windows用のfont.properties.jaに韓国語フォント指定を追加(一部)
dialog.plain.0=Arial,ANSI_CHARSET
dialog.plain.1=\uff2d\uff33 \u30b4\u30b7\u30c3\u30af,SHIFTJIS_CHARSET
dialog.plain.2=WingDings,SYMBOL_CHARSET
dialog.plain.3=Symbol,SYMBOL_CHARSET
dialog.plain.4=\uad74\ub9bc,HANGEUL_CHARSET

[中略]

filename.\uad74\ub9bc=gulim.TTC

[中略]

fontcharset.dialog.4=sun.io.CharToByteMS949

dialog.pain.4に、韓国語のフォント設定を追加しています。追加した設定については、Java 2に付属しているfont.properties.koから流用しています。

さきほどのプログラムを再起動すると、以下のように日本語と韓国語の混在した文字列が表示されます。

日本語・韓国語が表示される画面

テキストフィールドに入力された日韓混在文字列を、java.lang.String型のインスタンスに代入してダイアログに表示させてもここでは正しく表示されています。

日本語・韓国語混在文字列のダイアログ画面
JDK/JREの2箇所に設定があるのにご用心

普通、Java SDKをインストールすると合わせてJREもインストールされます。上記のフォント設定は、JDK側にもJRE側にもあるので、片方だけ変更しても実行するJavaコマンドがもう一方の方であれば設定が反映されません。

漢字表示の調整

Swing GUIにおいて、漢字表示は永遠の鬼門です。Swingでは、デフォルトでボールド表示を行うものが多いのですが、漢字のボールド表示は汚く見えます。

JDK6における漢字表示が汚ない(Windows Vista)

まずは、以下にWindows Vista上でJDK 6のデフォルト状態でアプリケーションを実行したときの画面を示します。(Metal Look and Feelのとき)

JDK6onWindowsVista_デフォルト設定

Windows Vistaでは、デフォルトでCleartypeが有効になっています。しかし、Java/SwingのGUIを表示させると日本語がCleartypeになっていないようです。

これは、Java実行環境が日本語表示で使用するフォント(MS明朝、MSゴシック)について、Windows OSでは小さいポイントのときにアンチエイリアスではなく埋め込みのビットマップ表示をするためです。(実験では24ポイント以上のときにアンチエイリアスが有効)。

対策として考えられるのは以下です。

それぞれの対策について調査していきます。

ボールド体表示しないようにする

システムプロパティ swing.boldMetal=false を設定する

アンチエイリアスを有効(ただしClearTypeではない)にする

システムプロパティ awt.useSystemAAFontSettings=onを設定すると、フォントがスムージング(ClearTypeではなく)されるようになります。ただし、システムプロパティを設定するため、そのJavaVM上で表示される全てに影響するため、きれいにClearType表示されている英数字フォントもClearTypeではなくなってしまいます。

Javaが使用するフォントを変更

JDK1.6はフォントの設定をJDK1.6をインストールしたディレクトリの下、jre/lib/fontconfig.bfcファイルから読み取って使用します。fontconfig.bfcファイルはバイナリですが、同じディレクトリにあるfontconfig.properties.srcから生成された内容です。

JDKのドキュメントによれば、テキストで記述できるフォント設定ファイル名fontconfig.propertiesが存在すれば、fontoconfig.bfcより優先されます。

そこで、ユーザーでフォント設定を変更するときは、fontconfig.properties.srcをfontconfig.propertiesにファイル名を変更して内容を修正します。

以下変更点を記載します。

fontconfig.propertiesの変更点
設定内容 元の設定 修正した変更
sansserif.plain.japanese MS Gothic Meieryo
sansserif.bold.japanese MS Gothic Meiryo
sansserif.italic.japanese MS Gothic Meiryo
sansserif.bolditalic.japanese MS Gothic Meiryo
monospaced.plain.japanese MS Gothic Meiryo
monospaced.bold.japanese MS Gothic Meiryo
monospaced.italic.japanese MS Gothic Meiryo
monospaced.bolditalic.japanese MS Gothic Meiryo
dialog.plain.japanese MS Gothic Meiryo
dialog.bold.japanese MS Gothic Meiryo
dialog.italic.japanese MS Gothic Meiryo
dialog.bolditalic.japanese MS Gothic Meiryo
dialoginput.plain.japanese MS Gothic Meiryo
dialoginput.bold.japanese MS Gothic Meiryo
dialoginput.italic.japanese MS Gothic Meiryo
dialoginput.bolditalic.japanese MS Gothic Meiryo
filename.Meiryo - MEIRYO.TTC

エラー・例外関係

EDTで発生した例外を捕捉する

AWT/Swingのイベント・ディスパッチ・スレッドで実行される処理において捕捉されない例外がスローされると、イベント・ディスパッチ・スレッドは停止し、コンソール(があれば)にスタックトレースが出力されます。イベント・ディスパッチ・スレッドは新しく生成し直されます。

このデフォルトの振舞いでは、コンソールがない場合にエラーの発生を検出しその原因を突き止めることが困難です。イベント・ディスパッチ・スレッドの中で未捕捉の例外が発生したときにでも、それを検知しアプリケーション側で任意のエラー処理をしたいところです。

手段としては以下が考えられます。

  1. イベント・ディスパッチ・スレッドを自前のものに置き変える
  2. SunのJavaVM限定(非公式API)だが、システムプロパティsun.awt.exception.handlerで未捕捉例外が発生した通知を受け取るクラスを指定する
  3. アスペクト指向系のツールまたはJVMTI(Java Virtual Machine Tool Interface)を用いて例外発生を捕捉する

イベント・ディスパッチ・スレッドの置き換え

sun.awt.exception.handler

SunのJavaVMでは、イベント・ディスパッチ・スレッドで発生した例外をシステム・プロパティsun.awt.exception.handlerで指定したクラス名のvoid handle(Throwable)メソッドに渡して処理させる機構が用意されています。

簡単なサンプルを作成しました。画面にメッセージ文とボタンを表示し、ボタンを押すとそのリスナーで例外をスローします。

EDTで例外を発生するサンプル
import javax.swing.JFrame;
import javax.swing.JButton;
import javax.swing.JTextArea;
import javax.swing.SwingUtilities;
import java.awt.BorderLayout;
import java.awt.event.ActionListener;
import java.awt.event.ActionEvent;

public class ExceptionInEdt extends JFrame {
    private JTextArea explainTextArea = new JTextArea(
        "[例外発生]ボタンを押すと、EDT(Event Dispatch Thread)で実行される" +
        "ボタンのアクション処理で例外をスローします。"
    );
    private JButton causeExceptionButton = new JButton("例外発生");

    public ExceptionInEdt() {
        setLayout(new BorderLayout());
        explainTextArea.setLineWrap(true);
        add(explainTextArea, BorderLayout.CENTER);
        causeExceptionButton.addActionListener(new ActionListener() {
                public void actionPerformed(ActionEvent ev) {
                    System.out.println(
                        "ボタンが押されたイベントを受け取りました。" +
                        "このスレッドのIDは " + Thread.currentThread().getId() +
                        " です。今から例外を発生します。");
                    throw new IllegalStateException("in EDT");
                }
            });
        add(causeExceptionButton, BorderLayout.SOUTH);
    }

    private static void createShowGui() {
        JFrame frame = new ExceptionInEdt();
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.setBounds(120, 80, 480, 320);
        frame.setVisible(true);
    }

    public static void main(String[] args) {
        SwingUtilities.invokeLater(new Runnable() {
            public void run() {
                createShowGui();
            }
        });
    }
}

このプログラムをコンソール(コマンドプロンプト等)から実行すると、[例外実行]ボタンを押すたびに、例外のスタックトレースがコンソールに表示されます。また、ボタンのActionListenerが呼ばれるときのスレッドIDが毎回変わっているのが分かります。イベント・ディスパッチ・スレッドが未捕捉の例外スローで停止するたびに、新しいスレッドが作られています。

次に、例外ハンドラーを作成し登録して実行します。例外ハンドラーの例として以下のコードを作成します。

EDTの例外ハンドラーサンプル
Can not access : src/swing/EdtExceptionHandler.java

システム・プロパティsun.awt.exception.handlerにこのクラス名を設定します。実行時にJavaVMオプションで指定するのが簡単です。

sun.awt.exception.handlerを指定して実行
$ java -Dsun.awt.exception.handler=EdtExceptionHandler \
    ExceptionInEdt

[例外実行]ボタンを押すたびに、エラーダイアログを表示します。なお、このとき、イベント・ディスパッチ・スレッドのスレッドIDは変化していません。

リンク集

FAQ集

  1. Java GUIプログラミングの、とてもBasicなFAQ

オンライン記事

IBM developerWorks公開記事

  1. 今まで知らなかった5つの事項:Swingを強化する - そうです、Swingを使用して素晴しいユーザー・インタフェースを作成することができます

    Swingを洗練する4つの無料コンポーネント(Substance, SwingX, RSyntaxTextArea, Java look and feel Graphics Repository)の紹介と、Swingのスレッド処理について解説。

  2. Swingコードをデバッグし、テストする - 他の人が作成したSwingコードを理解するためのツールと手法

    Swing ExplorerでSwing GUIの内部を可視化したりSwingのスレッド処理をモニターする、FEST-Swingを使用してGUIの機能テストを作成する、などの解説があります。

  3. Swingによる動的インタフェース設計 ー Swing APIの外部領域への旅

    ウィジェットの無効化(setEnable)、数値ウィジェット(SpinnerやSlider)の範囲を動的に調整、メニューの更新、ウィンドウのサイズ変更

  4. 進歩したSynth - 最新のSwingルック・アンド・フィールを使うとカスタムUIが手軽に

    Synthは、XMLで見た目を定義します。基礎、色とフォントの変更、画像、状態の処理、コンポーネント固有のプロパティ、カスタム・ペインター、パフォーマンスなどを解説しています。

  5. Tigerを使いこなす:成長するAWT - マウスの位置決めとZオーダー

    新たに追加されたクラスPointerInfo、MouseInfo、Zオーダーに関してコンテナーに追加されたメソッドsetComponentZOrder、getComponentZOrderについて解説しています。

  6. Tigerを使いこなす:OceanとSynthがMetalに出会う ー JDK 5.0での新しいルック・アンド・フィール2点

    Metal Look and Feelのデフォルトテーマが JDK 5.0において、従来のSteelからOceanになりました。また、新しいルック・アンド・フィール Synthが加わりました。

  7. Merlinの魔術: Swingの新たなSpinnerコンポーネント - JSpinnerを使用して、ユーザーがピック・リストから素早く日付、数値および選択肢を選べるようにする
  8. Merlinの魔術:Swingのオーディオ - オーディオ・キューの実装によるユーザー・インタフェースの向上
  9. Merlinの魔術:Swingの新たなJFormattedTextFieldコンポーネント - フォーマット・テキストを受け入れる入力フィールドを、最小限の労力で作成する方法
  10. Merlinの魔術:タブ付きペインのスクロール - 大き過ぎて収まりきらない場合にどうするか
  11. Merlinの魔術:もう1つのシンプルなフレーム - AWT Frameを最大化し、その装飾を取り外す
  12. Merlinの魔術:AWTの概要 - サイジング、カラー、マウスホイール、および入力イベントの変更に関する詳細
  13. Merlinの魔術:フォーカス、フォーカス、フォーカス ー フォーカス管理システムが改訂されました
  14. Merlinの魔術:不確定なプログレス・バー - 微妙な、でも重要な、JProgressBarの更新

    基本的な使用方法の説明、新機能の不確定モードの解説。

  15. Merlinの魔術:SpringLayoutマネージャ - GridBagLayoutでは不十分なとき
  16. SwingのJTableコンポーネントでセルを描く

    カスタムレンダラー、JTableのデフォルトレンダラー、テーブルセルの編集

  17. Java 2が新規フォーカス・サブシステムを実装 - JDK 1.4の新機能とその使用法
  18. Swingモデルのフィルター処理 ー フィルターオブジェクトを使ってデータモデルと状態モデルを再解釈する