プログラミング応用b 第13回 『GUIその3 (AWT/Swingによる応用的イベント処理)』

引き続き,Java GUI のイベント処理について紹介していく。


【メニューとイベント処理の例】

 ActionListener リスナで対処できる標準的なイベント処理の例として,Swing版メニューのイベント処理の例を紹介する。

 メニューが選択された時のイベントも,メニューの標準的なイベントとして, ActionListener リスナと actionPerformed( )
メソッドのオーバライドで対応できる
。List 1 を実行して,ファイルメニューから"開く"または"閉じる"というメニュー項目を
選択すると,各メニュー項目に応じたメッセージウィンドウが開く。 もう気づいたと思うが,main( ) メソッドが処理を終了しても,
ウィンドウは開いたままであり,メニュー処理も実行され続けている。このように,ウィンドウなどを表示すると,GUIを制御する
スレッドがメインスレッドとは別に生成・実行され,main( )メソッドが終了した後も処理が実行され続けるのである。


 ところで,リスナクラスとしてActionListenerの匿名サブクラスを定義し,actionPerformed( )メソッドのオーバライドをしている
部分が33〜37行目,42〜46行目であることは,前回の学習で理解できているだろう。そして,それぞれ定義したその場でオブジェクト
を生成して(33行目と42行目のnew演算子による),addActionListener( )メソッドへ実引数として和すことによって,イベントリスナ
として登録している。

  なお,35行目と44行目で,簡単なメッセージ表示用のウィンドウを表示する JOptionPane.showMessageDialog( ) メソッド
を利用している。このメソッドは,第1引数に親ウインドウ(特に指定しなければ null でも良い),第2引数にメッセージ
文字列を渡すようになっている。static メソッドなので,何かしらオブジェクトを生成する必要も無く使用できるので手軽に
利用できる。

List 1 SwingMenuEventTest.java ソースコード表示範囲の右上のコピーアイコンでソースをコピーできます。

import javax.swing.JFrame;
import javax.swing.JMenu;
import javax.swing.JMenuBar;
import javax.swing.JMenuItem;
import javax.swing.JOptionPane;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;

class SwingMenuEventTest {
    public static void main( String args[ ]) {

        // ウィンドウの作成
        final JFrame f  = new JFrame( "Swing Men" ); // Java 8 (JDK1.8) でコンパイルする場合は final は要らない。
        f.setSize( 200, 100 );

        // "ファイル"メニューの作成
        JMenu fileMenu = new JMenu( "ファイル" );
    
        // メニュー項目"開く"の作成と"ファイル"メニューへの登録
        JMenuItem miOpen = new JMenuItem( "開く" );
        fileMenu.add( miOpen );
            
        // メニュー項目"閉じる"の作成と"ファイル"メニューへの登録
        JMenuItem miClose = new JMenuItem( "閉じる" );
        fileMenu.add( miClose );    
    
        // メニューバーの作成とメニューの登録
        JMenuBar mbar = new JMenuBar();
        mbar.add( fileMenu );

        // イベントリスナを”開く”メニューに登録する
        miOpen.addActionListener(  
            new ActionListener() {
                public void actionPerformed( ActionEvent e ) {
                    JOptionPane.showMessageDialog( f.getContentPane( ), "\"開く\"が選ばれた。" );
                }
            }
        );
    
        // イベントリスナを”閉じる”メニューに登録する
        miClose.addActionListener(  
            new ActionListener() {
                public void actionPerformed( ActionEvent e ) {
                    JOptionPane.showMessageDialog( f.getContentPane( ), "\"閉じる\"が選ばれた。" );
                }
            }
        );

        // メニューバーをウィンドウに追加
        f.setJMenuBar( mbar );
        // ウィンドウを可視化する
        f.setVisible( true );
    }
}

上級トピック

 ここで注目して欲しいのは,こうして定義・生成した ActionListenerの匿名サブクラスのactionPerformed( )メソッド内で,
 
   JOptionPane.showMessageDialog( f.getContentPane( ), "\"開く\"が選ばれた。" ); // 35行目
   JOptionPane.showMessageDialog( f.getContentPane( ), "\"閉じる\"が選ばれた。" ); // 44行目

というように, main( )メソッドの局所変数 f (13行目で宣言)を利用していることである。

 局所変数の寿命は一時的で,宣言文が実行されるとその都度生成され,所属しているメソッド(やブロック文)の実行が終了
するとその都度消えて無くなる。上述したように,この35行目と44行目の処理が実行されるときには,すでにmain( )メソッド
は終了しているので,main( )メソッドの局所変数 f はすでに消滅しているはず
である。プログラムを起動して,「開く」メニュー
を選んだ場合を例に取ると,時間の流れは

  (1) main( )メソッドの処理が開始され,13行目の変数宣言が実行されて局所変数 f が生成される。
  (2) 32行目の miOpen.addActionListener( ) メソッドの呼び出しにより,ActionListenerの匿名サブクラスの
   オブジェクトがリスナとして登録される。(34〜36行目は匿名サブクラスのメソッドactionPerformed( )の定義で,
   この時はまだ実行はされない。)
  (3) main( )メソッドが終わり,main( )メソッドの局所変数 f が消滅する。 (あれ…!?)
  (4) 「開く」メニュー(オブジェクトはmiOpen)を選ぶと,miOpenに登録されたリスナのactionPerformed( )メソッドが
   呼び出され,35行目の
      JOptionPane.showMessageDialog( f.getContentPane( ), "\"開く\"が選ばれた。" ); // 35行目
   が実行されて,main( ) メソッドの局所変数 f を利用する。 (おいおい,もう局所変数fは消えてるはずじゃん!?!?)

となる。どうして(4)の様にリスナのメソッドで消滅済みの局所変数fを利用することができているのだろうか? (まさか
fのゾンビ!?)


 実は,13行目で局所変数fを宣言しているときに, final として宣言しているのがミソである。final宣言された変数は定数と
なるので,値が変化することは無いことが保証される。ということは,局所変数 f が消える前に, こっそり f の内容を複製して
fの代わりに使い続けてもかまわない
ということになる。(もし f が final でなかったら, f の複製を作った後にオリジナルの f
の値が変化してしまう可能性がある。その場合, オリジナルのfの値と複製したfの値が異なってしまって矛盾が生じるので,
この手は使えない)

 そこで,このような final 局所定数 を匿名クラスのメソッドから利用している場合,Javaはfinal 局所定数の複製をこっそり
作って,匿名クラスのメソッド内ではその複製を利用するようになっている
のである。(35行目と44行目のfの正体は,fのゾンビ
じゃなくて,クローンなんです)

  なお,Java 8 (JDK1.8) では, 局所変数をfinal で宣言しなくても,意味的に問題が無い(その局所変数の値を変化させる
処理が書かれていない)場合には,擬似的に final と見なしてコンパイルできるようになっている。List 1 を Java 7 (JDK1.7)
以前のコンパイラでコンパイルするときには,13行目の final が無いとエラーになるが, Java 8 (JDK1.8) でコンパイルする
場合には,13行目の final は省略してもエラーにならない。




【チェックボックスとイベント処理の例】

 Swing版チェックボックスのイベント処理の例を紹介する。実行すると,2個のチェックボックスが配置された
ウィンドウが表示され,それぞれのチェックボックスをクリックして選択・解除するごとに,コンソール出力にその
内容が表示される。

 前回紹介した表
で,チェックボックスの選択を変化させたときに発生するイベントは ItemEvent 型オブジェクトで,
対応するリスナは, ItemListener である。呼ばれるメソッドは itemStateChanged( ) となる。

 List 2 の 9〜32行目がリスナの実装クラスで, itemStateChanged( ) をオーバライドしている。

 13行目で ItemEvent型の getItemSelectable( ) メソッドを呼んでいるが,このメソッドはイベントソースの選択項目
オブジェクトを ItemSelectable 型で返す。前回紹介した継承・実装関係の図(中央やや左)を見ると分かるが,
ItemSelectable は,「選択可能な項目」の性質を定めているインタフェイスで, JCheckBox も間接的にこのインタフェイス
を実装している。

  チェックボックスが選択されているかどうかは JCheckBoxがスーパークラスのスーパークラス AbstractButton
から継承した isSelected( ) メソッドを使って調べる事ができる。

  また,MyApplicationクラスのコンストラクタ(34〜57行目)でチェックボックスにリスナを登録しているが(42,46行目),
選択項目に ItemListener 型のリスナオブジェクトを登録するには, addItemListener( ) メソッドを利用する。

List 2 SwingCheckBoxTest.java ソースコード表示範囲の右上のコピーアイコンでソースをコピーできます。

import java.awt.*;
import java.awt.event.*;
import javax.swing.*;

class MyApplication  {
    JFrame f;
    JCheckBox jcb1, jcb2;

    class MyItemListener implements ItemListener {
        public void itemStateChanged( ItemEvent e ) {
            String msg = "";
            
            if( e.getItemSelectable() == jcb1 ) {
                if( jcb1.isSelected() ) {
                    msg = "選択肢1が選択された";
                }
                else {
                    msg = "選択肢1の選択が解除された";
                }
            }
            if( e.getItemSelectable() == jcb2 ) {
                if( jcb2.isSelected() ) {
                    msg = "選択肢2が選択された";
                }
                else {
                    msg = "選択肢2の選択が解除された";
                }
            }
            
            System.out.println( msg );
        }
    }

    public MyApplication( String str ) {
        f = new JFrame( str );
        f.setSize(350, 80);
        Container c = f.getContentPane( );
        c.setLayout( new FlowLayout( ) );

        jcb1 = new JCheckBox( "選択肢1" );
        c.add( jcb1 );
        jcb1.addItemListener( new MyItemListener( ) );

        jcb2 = new JCheckBox( "選択肢2" );
        c.add( jcb2 );
        jcb2.addItemListener( new MyItemListener( ) );

        // ウィンドウを閉じたときにプログラムも終了するように設定する。
        f.addWindowListener(
            new WindowAdapter( ) {
                public void windowClosing( WindowEvent e ) {
                    System.exit( 0 );
                }
            }
        );
        f.setVisible( true );
    }
}

class SwingCheckBoxTest {
    public static void main( String args[ ] ) {
        MyApplication app = new MyApplication( "チェックボックスの使用例" );
    }
}

【GUIプログラミングととスレッド】

  クリティカルセクションに対して同期処理を施し,デッドロックなどもきっちり対処している事を,スレッドに対して安全である
(スレッドセーフ)と言う。
 しかし,Swing/AWTが提供するAPI は,実行効率が重視されていることもあって,スレッドに対して完全に安全ではない
(部分的にはスレッドセーフとされる箇所もあるが,全体的にはスレッドセーフではない。) つまり,JavaではGUIコンポー
ネントの描写などを複数の別個のスレッドから同時並行的に実行すると,競合が起こってしまう可能性がある。そのため,
マルチスレッド機能を利用しているプログラムでは,Swingの描写処理を特定の一つのスレッドに任せる仕組みが用意され
ている。ここでは詳しくは解説しないが,Javaでマルチスレッドを活用したGUIプログラムを作成する際は,この事を思い出
して,やり方を調べてみて欲しい。


【まとめ】

 JavaのGUIの基本的な使い方を習得すれば,Javaによるプログラミングも幅が広がる。
紹介できなかったGUI機能についても,是非,積極的に調べて活用してみよう。

残った時間は,前回出題された15パズルの課題がまだ合格していない場合は完成させてみよ。
また,合格した場合はさらに改善してみよ(パズルが解けたかどうかの判定を入れる等)。

【次回は】
 次回から,2回にわたって,多くのデータを管理するための便利なクラス群(コレクション)について紹介する。



【提出課題(第13回 12/21出題)】 〜 第14回 1/11の授業開始時に印刷済みのソースプログラムをレポートとして提出すること。複数枚になる場合はホッチキスなどでとめること。

●課題
Swing のボタン(JButton)とチェックボックス(JCheckBox)のイベント処理を使ったサンプルソフトを作成してみよ。
ボタンは2個以上,チェックボックスは3個以上とする。
(例:選択項目にしたがって占い結果を選び,「表示」ボタンでメッセージを表示する,「解除」ボタンで選択をすべて解除する,など。)


戻る