第 6 回 GUI

本日の内容


このドキュメントは http://edu.net.c.dendai.ac.jp/ 上で公開されています。

6-1. 定数の扱い方

Java の interface 宣言で、変数の初期化を行うと、それには public final static 修飾子が付加されます。 そのため、定数として扱うことができます。

例6-1


interface Constant {
    int WIDTH = 300;
    int HEIGHT = 200;
}

そこで、このようにして interface で定数を集め、別のクラスから implement して定数を使用するテクニックがあります。 但し、これはやってはいけません。 これは「Effective Java」の 17 章に載っている Constant Interface Antipatern と呼ばれる避けるべきテクニックです。 なぜ、やってはならないかというと、継承することにより便利に定数が使える だけではなく、他の様々な継承による影響が生じるからです。 そこで、使用するのはユーティリティクラスと呼ばれるテクニッ クです。これは、単に interface や class で定数を定義して、 継承せずにメンバーを指定して使うものです。

例6-2

悪い例

class WaruiRei implements Constant {
  public static void main(String[] arg){
    System.out.println(WIDTH);
  }
}
良い例

class Rei {
  public static void main(String[] arg){
    System.out.println(Constant.WIDTH);
  }
}

さらに Java 5 からは static メンバーの import という機能が加わりました。 これは、 package 宣言をしている必要があるのですが、次のようにして使用 します。


package project1;
public interface Constant1 {
    int WIDTH = 300;
    int HEIGHT = 200;
}

package project1; public class Constant2 { public final static String NAME = "例"; }

このようなユーティリテイクラスに対して次のように指定します。


import static project1.Constant1.*;
import static project1.Constant2.NAME;

なお、これは定数にも静的なメソッドにも使用できます。 java.lang.Math クラスのメンバーなどはこの様に import して使用す ると便利です。但し、このとき、 java.lang は省略できません。

例6-3


import static java.lang.Math.sin;
import static java.lang.Math.cos;

6-2. Java の GUI

Swing, awt, アプレット, Android

Java は当初 AWT(Abstract Window Toolkit) というクラスライブラリで GUI を実現していました。 しかし、これは OS が提供するものを呼び出すようになっていたため、抽象度 が低く、 OS の依存度が高いなど不都合がありました。 そのため、 Java 1.2 からは Swing というクラスライブラリが登場しました。 当初は速度面などで性能不足が指摘されていましたが、現在は十分な速度を持 つようになりました。 また、 Windows や MacOSX や Linux でも同じプログラムが動作するようになっ ています。 今回は主にこの Swing について学習します。 但し、 Swing は AWT を全て置き換えるものではなく、イベント処理など AWT の枠組みをそのまま使う場合もあります。

Swing はスタンドアロンの GUI のアプリケーションのために使われるだけで なく、 Web の埋め込みプログラムであるアプレットにも使用できます。 但し、アプレットはダウンロードするサーバとだけしか通信が不能など、セキュ リティ面での自由度が少ないため、現在はネットゲームなど用途が限られてい ます。

Swing の基本的な動作は、オブジェクトを生成すると、それに対応したツール が生成されます。 Swing の各パーツは javax.swing.JComponent クラスを継承していますが、 ツールの描画位置、色、フォントなどの属性などに関しては、このクラスにあ る setter, getter を用いて指定します。 これはオブジェクト指向プログラミング的には自然な実装ではありますが、プ レゼンテーションとデータ処理などのプログラム動作が分離できていないとい う側面があります。 Windows で C++ でプログラミングする場合、ウィンドウの形状などはリソー スファイルで管理します。 また、Linux などで使用される X Window System でも、リソースデータベー スで様々なレイアウトなどを管理し、これは完成したソフトウェアの見栄えな どをあとからユーザ単位でカスタマイズできます。 Google が開発した携帯電話プラットフォームである Android では、システム は Java ベースですが、 GUI は Swing ではなく Activity と呼ばれるクラス ライブラリを使用します。 Android では様々なアプリケーションの定数などは全て XML で与えます。画 面のレイアウトなども XML で与えます。

なお、今回はプログラムの作成によりデザインを行いますが、画面のデザイン を WYSIWYG(What you see is what you get) で GUI により作成することがで きます。 SUN が提供している NetBeans と呼ばれる IDE により作成します。 このチュートリアルは http://java.sun.com/docs/books/tutorial/uiswing/learn/ より基本操作を学ぶことができます。

Swing の基本構造

Swing で主に使用するオブジェクトクラスの関係を示します。

GUI の 外枠であるフレーム(JFrame)にある contentPane に部品を貼り込みます。 contentPane は java.awt.Container 型です。

フレーム

Swing では javax.swing.JFrame というクラスが、画面にフレームを表示させ ます。

例6-4


import javax.swing.*;
interface Constant {
    int WIDTH = 300;
    int HEIGHT = 200;
}
class Rei {
    public static void main(String[] arg){
	final JFrame frame = new JFrame();
	frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
	frame.setSize(Constant.WIDTH,Constant.HEIGHT);
	frame.setVisible(true);
    }
}

例にあるように、 JFrame を生成するにはオブジェクトを作成し、 visible 属性を true にします。 さらに、 Close ボタンを押したときの動作、画面サイズの指定などを setter を通じて行う必要があります。 なお、タイトルバーに文字列を表示するとき、 setTitle でも指定できますが、 コンストラクタに文字列を入れても指定できます。

生成したフレームには様々な区画(ペイン) が含まれています。 通常はコンテントペイン という区画に様々な部品を貼り付けて画 面を構成します。 コンテントペインの他には、グラスペイン、レイヤードペインなど複数のペイ ンや、メニューバーを持っていますが、この講義ではコンテントペインのみを 扱います。 getContentPane で java.awt.Container 型のコンテントペインを返します。

コンテナとコンポーネント

javax.swing.JComponent はトップレベル以外の GUI の部品の基底クラ スです。 javax.swing.JPanel, javax.swing.JLabel などは JComponent を継承してい ます。 また、このクラスは java.awt.Container を、 java.awt.Container は java.awt.Component を継承しています。

JFrame のコンテントペインなどにコンポーネントを配置するには、コンテン トペインは( java.awt.Container を継承しているので) add メソッドでコン ポーネントを加えます。

また、主な get, set 可能なものには、 BackGround, Border, Font, ForeGround, PreferredSize などがあります。

JComponent を継承している代表的なオブジェクトクラスには次のものがあり ます。

JPanel
JLabel
JButton

パネル

木構造

JPanel は何も見えないコンポーネントです。 コンテントペインなどに複数配置するこ とにより、コンテントペインを分割することができます。 また、これは java.awt.Container を継承していますので、さまざまなコンポー ネントや、さらに JPanel の中に JPanel を含ませることができます。 そのため、 GUI の外観は特定の JPanel の包含関係で表すことができます。 包含関係で表せるということは、 JFrame を根とする木構造で表現できること を意味しています。

レイアウトマネージャ

さて、GUI を構成する各コンポーネントには木構造を作ることがわかりました。 それらはどのように配置されるのでしょうか? これにはレイアウトマネージャが使われます。

レイアウトマネージャは複数提供されています。 これは java.awt.LayoutManager インタフェースを実装したクラスとして提供され ています。

レイアウトマネージャは java.awt.Container の setLayout メソッドで指定 します。 一方、コンポーネントを追加する際に、 add(コンポーネント, 制約) という形で表示位置を指定します。 この制約は各レイアウトマネージャにより指定方法が変わります。 また、デフォルトの制約が規定されているレイアウトマネージャでは制約を省 略した add(コンポーネント) で画面を構成することができます。

java.awt.BorderLayout

BorderLayout は JFrame のコンテントペインのデフォルトのレイアウトマネー ジャです。 これはコンテントペインを 5 つの区画に分けて指定します。

NORTH
WESTCENTEREAST
SOUTH

例6-5


import java.awt.*;
import javax.swing.*;
import javax.swing.border.*;
interface Constant {
    int WIDTH = 300;
    int HEIGHT = 200;
    String[] constraint = new String[]
	{ BorderLayout.NORTH, BorderLayout.WEST, 
	  BorderLayout.CENTER, BorderLayout.EAST, BorderLayout.SOUTH };
    Color[] color = new Color[]
	{ Color.BLUE, Color.RED, Color.PINK, Color.GREEN, Color.CYAN };
}
class MyFrame extends JFrame {
    public MyFrame(){
        super();
	setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
	setSize(Constant.WIDTH,Constant.HEIGHT);
    }
}
class Rei {
    public static void main(String[] arg){
	final MyFrame frame = new MyFrame();
	Container contentPane = frame.getContentPane();
	for(int i=0; i < Constant.constraint.length ; i++){
	    JLabel label = new JLabel(Constant.constraint[i]);
	    contentPane.add(label,Constant.constraint[i]);
	    label.setBorder(new LineBorder(Constant.color[i]));
	}
	frame.setVisible(true);
    }
}

java.awt.FlowLayout

FlowLayout は JPanel のデフォルトのレイアウトマネージャです。 これは add の操作で次々にコンポーネントを一列に並べます。 コンストラクタで左から右、左右中央揃えなどを指定できます。

その他のレイアウトマネージャ

その他のレイアウトマネージャは java.awt.LayoutManager を実装しているク ラスを参照すれば良いですが、以下にいくつか挙げておきます。

javax.swing.BoxLayout
長方形を区切った区画の中にウィジェットを配置する
java.awt.GridBagLayout
区切られた領域をまたいで配置する
javax.swing.GroupLayout
X軸、Y軸で区画を作り、グループを定義して、そこに配置する。

イベント処理

以上で静的なグラフィック画面が表示できました。 しかし、これだけでは何の操作もできません。 ボタンを押したら押されたボタンに割り当てた値を得たり、得た値をラベルに 書いたというイベントとそれに応じたアクションを指定します。 そのため、使用するデザインパターンがオブザーバデザインパター ンですが、 Java ではリスナーと呼ぶことが多いようです。 値を受ける側で java.awt.event.ActionListener 型のオブジェクトを作り、 値を発生する側に setActionListener メソッドで登録します。 なお ActionListener は FunctionalInterface なので、 ラムダ式でも定義できます。

はじめにボタンのイベント処理を考えましょう。

例6-6

ここでは abc, def, ghi の名前のついた 3 つのボタンを配置します。 そして、各ボタンが押されたら、そのボタンにより生じた ActionEvent(これ もオブジェクト)の内容を標準出力に表示するプログラムを示します。


import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
interface Constant {
    int WIDTH = 300;
    int HEIGHT = 200;
    String[] titles = new String[]{ "abc", "def", "ghi" };
}
class MyFrame extends JFrame {
    public MyFrame(){
        super();
	setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
	setSize(Constant.WIDTH,Constant.HEIGHT);
    }
}
class ButtonListener implements ActionListener {
    @Override
    public void actionPerformed(ActionEvent e){//プログラムが長いので
	System.out.println(e.getActionCommand());//ラムダ式を使わず
	System.out.println(e.getModifiers());
	System.out.println(e.getWhen());
	System.out.println(e.paramString());
    }
}	    
class Rei {
    public static void main(String[] arg){
	final MyFrame frame = new MyFrame();
	Container contentPane = frame.getContentPane();
	JPanel panel = new JPanel();
	ActionListener listener = new ButtonListener();
	for(String title : Constant.titles){
	    JButton button = new JButton(title);
	    button.addActionListener(listener);
	    panel.add(button);
	}
	contentPane.add(panel);
	frame.setVisible(true);
    }
}

さて、逆に GUI において、値を表示することを考えます。 これは何らかの表示する値が生じたときにイベントを発生し、 GUI 側で表示 させるようにします。

例6-7

標準入力を一行ずつラベルに表示するようなプログラムを考えましょう。 これには、まず、得られた値でラベルを更新するような ActionListener を作 る必要があります。 そして、標準入力が一行ずつ得られる度に、ActionEvent を生成して ActionListener に渡すようにします。


import java.io.*;
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
interface Constant {
    int WIDTH = 300;
    int HEIGHT = 200;
}
class MyFrame extends JFrame {
    public MyFrame(){
        super();
	setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
	setSize(Constant.WIDTH,Constant.HEIGHT);
    }
}
class MyLabel extends JLabel {
    public ActionListener getActionListener(){
	return (e)->setText(e.getActionCommand());
    }
}
class Rei {
    public static void main(String[] arg) throws IOException {
	final MyFrame frame = new MyFrame();
	final Container contentPane = frame.getContentPane();
	final MyLabel label = new MyLabel();
	final ActionListener listener = label.getActionListener();
	contentPane.add(label,BorderLayout.CENTER);
	frame.setVisible(true);
	final BufferedReader br 
           = new BufferedReader(new InputStreamReader(System.in));
	String line;
	while((line=br.readLine())!=null){
		ActionEvent e = new ActionEvent(br,
                     ActionEvent.ACTION_PERFORMED,line);
		listener.actionPerformed(e);
	}
	frame.dispose();
    }
}

(参考)ラムダ式を使わない例


class MyLabel extends JLabel {
    class DummyName implements ActionListener {
        @Override
        public void actionPerformed(ActionEvent e){
            setText(e.getActionCommand());
        }
    }
    public ActionListener getActionListener(){
	return new DummyName();
    }
}

なお、このように ActionListener はイベントを受け取ったとき、特定のオブ ジェクトの値を変更したりします。 本来このオブザーバデザインパターンはメソッドを直接渡すことができないの で、代わりにオブジェクトを渡すものです。 そのため、渡すオブジェクトは本来のオブジェクトクラスの所有物である方が カプセル化がうまくいきます。 そのため、内部クラスを使用します。 なお、無名クラスも使用できますが、リスナーのリファクタリングが効率よく できないので推奨しません。

例6-8

前の例を合わせ、ボタンが押されたらボタンのラベルを表示するプログ ラムを示します。


import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
interface Constant {
    int WIDTH = 300;
    int HEIGHT = 200;
    String[] titles = new String[]{ "abc", "def", "ghi" };
}
class MyFrame extends JFrame {
    public MyFrame(){
        super();
	setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
	setSize(Constant.WIDTH,Constant.HEIGHT);
    }
}
class MyLabel extends JLabel {
    public ActionListener getActionListener(){
	return (e)->setText(e.getActionCommand());
    }
}
class Rei {
    public static void main(String[] arg) {
	final MyFrame frame = new MyFrame();
	final Container contentPane = frame.getContentPane();
	final JPanel panel = new JPanel();
	final MyLabel label = new MyLabel();
	final ActionListener listener = label.getActionListener();
	for(String title : Constant.titles){
	    JButton button = new JButton(title);
	    button.addActionListener(listener);
	    panel.add(button);
	}
	contentPane.add(label,BorderLayout.CENTER);
	contentPane.add(panel,BorderLayout.SOUTH);
	frame.setVisible(true);
    }
}

なお、このように、操作や表示などを全てリスナーで分離して別々のオブジェ クトとして管理するのを MVC(Model, View, Control) と呼んだりします。

Java8

Java8 では新たな機能として、interface に一つだけ abstract メソッドがあ る時、 FunctionalInterface と呼び、ラムダ式と呼ばれる簡略表現を使うこ とが出来ます。

上記の例だと LabelListener は FunctionalInterface である ActionListener のメソッド actionPerformed のみを実装しています。 そして、 MyLabel はその actionPerformed にしか関与していません。 このような場合に、クラスを定義して、インナークラスを使用する方法の他に、 ラムダ式を使用することができます。

ラムダ式の基本的な書式は、 abstractメソッド名と戻り値の型指定を省略し、 引数をくくる丸括弧とブロックの間に->を入れます。 さらに、次のような省略構文が使えます。

  1. 引数の型宣言を省略出来ます
  2. ブロックが一命令しか無い場合は、中括弧を外すことが出来ます
  3. ブロックが return からなる一つの式の値を返す構文の場合、 return も 省略出来ます

上記の例だと、 MyLabel の宣言をすべて無くし、 main メソッドを次の様に することができます。


class Rei {
    public static void main(String[] arg) {
	final MyFrame frame = new MyFrame();
	final Container contentPane = frame.getContentPane();
	final JPanel panel = new JPanel();
	final JLabel label = new JLabel();
	for(String title : Constant.titles){
	    JButton button = new JButton(title);
	    button.addActionListener((e)->label.setText(e.getActionCommand()));
	    panel.add(button);
	}
	contentPane.add(label,BorderLayout.CENTER);
	contentPane.add(panel,BorderLayout.SOUTH);
	frame.setVisible(true);
    }
}

6-3. キーボードの作成

概要

さて、GUI でボタンを連続で押すとそのボタンの文字の列が連続して得られる ようなオブジェクトを考えます。 つまり、ボタンを押すと、得られるのが Stream になるようなプログラムを作 成します。 それにはスケルトンクラスである java.io.InputStream を継承します。 また作成する GUI ですが、キーボード単独で出力がないようなアプリケーショ ンは考えづらいので、 JFrame を作成するのではなく、 JPanel を作成するこ とにします。 つまり、大まかなクラスの構成は次のようになります。


import java.io.*;
import java.awt.*;
import javax.swing.*;
public class Keyboard extends InputStream {
    public JPanel getPanel(){...}
    @Override int read(){...}
}

InputStream は read を持っています。 そして、ファイルが終了するときに -1 を返します。 これを実現するには、 やはりオブザーバデザインパターンを使用します。 あらかじめオブザーバ(リスナ) を JFrame に渡しておきます。 そして、 close ボタンが押されたらイベントを生成し、 Keyboard オブジェクトはそのイベントにより -1 を返すようにします。 キーボードにおける入力の終了を考えると close ボタンなど外部からの要因 しか考えられません。 そのため、 Keyboard オブジェクトは close に反応する ActionListener を 生成する必要があります。 これをどのように JFrame 側で処理するかは後述します。

画面の構成

それではキーボードを作成しましょう。 コンストラクタは、キートップの定数に対してそのボタンを生成する作業と ActionListener の生成を行います。 これには文字配列に対して、次々と JButton オブジェクトを生成させ、 JPanel に配置します。

なお、今回は特にレイアウトマネージャを指定しませんでしたが、本物の電卓 のようなキーボードを実現するには、適切な配置を指定できるレイアウトマネー ジャを使用し、ボタンの配置をしていするようにします。

InputStream の構成

さらに Keyboard 内に read メソッドの実装を行います。 この read メソッドの呼び出しと、キーボードのボタンを押すのは非同期に行 われます。 そして、キーボードの押されたボタンのデータを保存して read に渡す必要が あります。 そのため、キューを作成します。 つまり java.util.LinkedList に文字を蓄えて、 read が呼ばれる度にその文 字を一つずつ読み出します。 なお、キューに文字が溜まっていないときは read をブロックする必要があり ます。 単純にキューが isEmpty() を調べつづけるのは非常に無駄がありますので、 一定間隔ごとに調べることにします。 そのために java.lang.Thread でスレッドを作成して、 sleep, join で時間 待ちをさせることにします。

一方、キー一つ押す度にパーサーを呼び出させたいので、 available メソッ ドは常に 0 を返し、まとめて処理をさせないようにします。 さらに、 java.io.InputStream の int read(byte[] b, int off, int len) の説明にあるように、抽象メソッドではありませんが、1 文字だけしか読まな いようにオーバーライドします。

以上を実装したのが下記のプログラムです。


import java.io.*;
import java.util.*;
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
interface KeyboardConstant {
    char[] keys = new char[]
	{'0','1','2','3','4','5','6','7','8','9','+','='};
    long INTERVAL = 100;
}
public class Keyboard extends InputStream {
    final private JPanel panel= new JPanel();
    public Keyboard(){
	super();
	for(char c : KeyboardConstant.keys){
	    JButton button = new JButton(String.valueOf(c));
	    button.addActionListener((event)->
	    queue.addLast((int)(event.getActionCommand().charAt(0))));
	    panel.add(button);
	}
    }
    public JPanel getPanel(){
	return panel;
    }
    public ActionListener getCloseAction(){
	return (e)->queue.addLast(-1);
    }
    private LinkedList<Integer> queue= new LinkedList<Integer>();
    @Override
    public int read() throws IOException {
	try {
	    while(queue.isEmpty()){
		Thread.sleep(KeyboardConstant.INTERVAL);
            }
	}catch(InterruptedException e){
	}
	return queue.remove();
    }
    @Override
    public int available() throws IOException {
	return 0;
    }
    @Override
    public int read(byte[] b, int off, int len) throws IOException {
	if(len==0) return 0;
	int c = read();
	if(c==-1) return -1;
	b[off]=(byte) c;
	return 1;
    }
}

このプログラムをテストするプログラムを次に示します。

テストプログラム

import java.awt.*;
import javax.swing.*;
import java.io.*;
interface Constant {
    int WIDTH = 300;
    int HEIGHT = 200;
}
class Test {
    public static void main(String[] arg) throws IOException {
	final JFrame frame = new JFrame();
	frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
	frame.setSize(Constant.WIDTH,Constant.HEIGHT);
	final Keyboard key = new Keyboard();
	frame.getContentPane().add(key.getPanel());
	frame.setVisible(true);
	int c;
	while((c=key.read())!=-1){
	    System.out.println((char)c);
	}
    }
}

6-4. 簡易電卓の作成

さてここでは簡易的な電卓を考えましょう。 「自然数("+"自然数)*"="」という構文のみを解釈して足し算の結果だけを表 示するものです。

表示部

電卓の計算の結果を示す部分を JLabel で作成します。 この表示部はオブザーバデザインパターンを使います。 つまり、 ActionListener を生成し、ActionEvent が生成されたら、得られた 値に表示を書き換えます。 これを実現するため、表示を書き換える actionPerfomed メソッ ドを持つ内部クラスを作成します。



interface LabelConstant {
    int LABELWIDTH = 200;
}
class MyLabel extends JLabel {
    public MyLabel(){
	super("0",RIGHT);
	Dimension d = getSize();
	d.width = LabelConstant.LABELWIDTH;
	setSize(d);
    }
    public ActionListener getActionListener(){
	return (event)->setText(event.getActionCommand());
    }
}

計算部

数式を Keyboard オブジェクトから読み込み、計算結果を MyLabel オブジェ クトに与えるプログラムを JavaCC を用いてパーサとして作ります。 パーサの入力は Keyboard クラスが java.io.InputStream を継承していますので、そ のまま与えます。 パーサの出力に関しては構文解析で指定するアクションでリスナを呼ぶように します。 そのため、パーサ自体に MyLabel の生成するリスナを登録できるようにしま す。


options {
   STATIC = false;
}
PARSER_BEGIN(Parser)
import java.io.*;
import java.awt.event.*;
class Parser {
      private ActionListener listener;
      public void setActionListener(ActionListener a){
          listener = a;
      }
      private void output(int value){
          listener.actionPerformed(
               new ActionEvent(this,
                               ActionEvent.ACTION_PERFORMED,
                               String.valueOf(value)));
      }
      
}
PARSER_END(Parser)
TOKEN : {
    <NUM : ["1"-"9"](["0"-"9"])*>
 |  <PLUSOP : "+" > 
 |  <EQOP : "=" > 
}
SKIP : {
    " " | "\n" | "\r"
}
public void start() :
{
    Token token;
    int num;
}
{
    (token=<NUM> {num = Integer.parseInt(token.image);
                 output(num);
                }
    ( <PLUSOP> token=<NUM> {num += Integer.parseInt(token.image);
                            output(num);
       }
     )*
     <EQOP> )*
}

このパーサをテストするプログラムを次に示します。

テストプログラム


import java.awt.event.*;
class Test {
    public static void main(String[] arg) throws ParseException {
	Parser parser = new Parser(System.in);
	parser.setActionListener((e)->System.out.println(e.getActionCommand()));
	parser.start();
    }
}

フレームの作成

Keyboard を使うフレームを作成する際、 close ボタンを押したときに Keyboard に終了を伝え、 Parser に EOF を与える必要があります。 そのため、setCloseOperation では WindowConstants.EXIT_ON_CLOSE の代わりに WindowConstants.DISPOSE_ON_CLOSE を与 え、ウィンドウは破棄しても、すべてのプロセスが終了しないようにします。 そして、 close ボタンを押したときに ActionListener を起動するようにす るために、 WindowListener を作成します。 WindowListener を実装しているスケルトンクラスに java.awt.event.WindowAdapter があります。 これを継承して、 windowClosed メソッドだけオーバライドしたクラス MyWindowListener を作成します。 フレームのオブジェクトに close ボタン用の ActionListener を渡し、これ を MyWindowListener 内の windowClosed メソッドが呼び出すようにしますの で、 MyWindowListener はそのフレームの内部クラスとして作成することにし ます。 すると、 ActionListener のリストが private でも、 MyWindowListener ク ラスからアクセスできるようになります。

ActionListener のリストを private で作成し、外部から追加するメソッド setCloseActionListener を public で作成します。


interface FrameConstant {
    int WIDTH = 300;
    int HEIGHT = 200;
    String TITLE = "例";
}
class MyFrame extends JFrame {
    public void setCloseActionListener(ActionListener a){
	closeActionList.add(a);
    }
    private final LinkedList<ActionListener> closeActionList
	= new LinkedList<ActionListener>();
    class MyWindowListener extends WindowAdapter {
        @Override
	public void windowClosed(WindowEvent e){
	    for(ActionListener listener : closeActionList){
		listener.actionPerformed(
                   new ActionEvent(
                     this,ActionEvent.ACTION_PERFORMED,"close"));
	    }
	}
    }
    public MyFrame(){
        super();    
	setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE);
	addWindowListener(new MyWindowListener());
	setSize(FrameConstant.WIDTH,FrameConstant.HEIGHT);
	setTitle(FrameConstant.TITLE);
    }
}

主プログラム

さて、以上をまとめ、動作するプログラムを作ります。

  1. フレームを作成します。
  2. Keyboard オブジェクトを作成します。
  3. close ボタンが押された際に対応する Keyboard のアクションリスナを取 り出し、フレームに登録します。
  4. コンテントペインをフレームから取り出します。
  5. コンテントペインに Keyboard のパネルを貼り付けます。
  6. 表示ラベルオブジェクトを生成します。
  7. コンテントペインに表示ラベルを貼り付けます。
  8. フレームを表示します。
  9. Keyboard オブジェクトを与えたパーサオブジェクトを作成します。
  10. 表示ラベルのアクションリスナをパーサに登録し、パーサの結果が表示さ れるようにします。
  11. パーサを起動します。

class Main {
    public static void main(String[] arg) throws ParseException {
	final MyFrame frame = new MyFrame();
	final Keyboard keyboard = new Keyboard();
	frame.setCloseActionListener(keyboard.getCloseAction());
	final Container container = frame.getContentPane();
	container.add(keyboard.getPanel(), BorderLayout.CENTER);
	final MyLabel label = new MyLabel();
	container.add(label,BorderLayout.NORTH);
	frame.setVisible(true);
	Parser parser = new Parser(keyboard);
	parser.setActionListener(label.getActionListener());
	parser.start();
    }
}

6-5. 付録

稼働する簡易電卓

Keyboard.java
keisan.jj
main.java

DummyInputStream

InputStream のサブクラスをデバッグするためのクラスを作成しましたので、 デバッグなどに活用してください。


import java.io.*;
public class DummyInputStream extends InputStream {
    private InputStream is;
    public DummyInputStream(InputStream is){
	this.is = is;
    }
    @Override
    public int available() throws IOException {
	System.out.println("available is called");
	int result = is.available();
	System.out.println(result);
	return result;
    }
     @Override 
     public void close() throws IOException {
	System.out.println("close is called");
	is.close();
    }
    @Override
	public void	mark(int readlimit) {
	System.out.println("mark("+readlimit+") is called");
	is.mark(readlimit);
    }
    @Override
    public boolean markSupported(){
	System.out.println("markSupported is called");
	return is.markSupported();
    }
    @Override
    public int 	read() throws IOException {
	System.out.println("read is called");
	return is.read();
    }
    @Override
    public int read(byte[] b) throws IOException {
	System.out.println("read(byte[]) is called");
	return is.read(b);
    }
    @Override
    public int read(byte[] b, int off, int len) throws IOException {
	System.out.println("read(byte[],"+off+","+len+") is called");
	return is.read(b,off,len);
    }
    @Override
    public void	reset() throws IOException {
	System.out.println("reset is called");
	is.reset();
    }
    @Override
    public long skip(long n) throws IOException {
	System.out.println("skip("+n+") is called");
	return is.skip(n);
    }
}

メインプログラム


import java.awt.event.*;
class Test {
    public static void main(String[] arg) throws ParseException {
	Parser parser = new Parser(new DummyInputStream(System.in));
	parser.setActionListener( new ActionListener(){
		public void actionPerformed(ActionEvent e){}
	    });
	parser.start();
    }
}

Java 8


import java.awt.event.*;
class Test {
    public static void main(String[] arg) throws ParseException {
	Parser parser = new Parser(new DummyInputStream(System.in));
	parser.setActionListener( (e)->{});
	parser.start();
    }
}

坂本直志 <sakamoto@c.dendai.ac.jp>
東京電機大学工学部情報通信工学科