JavaでSwingを利用したGUIアプリケーションの作成5(JListの利用とスレッド処理)
Swingを利用したウィンドウの表示、ボタンやラベルの配置、テキスト入力、イベント処理と一通り、GUIアプリケーションに必要な機能を作成してきたので、最後に複合的なGUIアプリケーションを作成する。
ToDoリストアプリケーションの作成
今回作成するアプリケーションは以下の機能を提供する。また、ToDoリストの一覧部分は“JList”を使用して表現する。
- ToDoの一覧を表示する
- ToDoを一覧に追加できる
- 一覧のToDoを編集できる
- 一覧のToDoを削除できる
JList
リストボックスを表現するコンポーネント。
複数のオブジェクトを一覧表示することができ、また、動的に一覧へのオブジェクトの追加、変更、削除を行うことができる。
JListの生成
JListは5つの種類のコンストラクタを持っている。
今回は、5つのうちのListModelを渡すタイプのコンストラクタを使用する。
このコンストラクタを使用すると、渡したリストモデルが管理しているデータを表示するリストボックスが生成される。
1 | JList(ListModel dataModel) |
ListModel
JListの表示データを管理しているクラス。
JList内の表示データの操作はListModelに対して行う。
JListが表示データの管理をListModelに委譲しているのは、データを管理するクラスと、ビューを管理するクラスを分離するというSwingの設計思想によるもの。
また、JListは.ListDataListenerを使用してListModelの内容を監視しており、ListModelに変更があった場合は、自身のビューを更新する。
この仕組みにより、表示データの操作やデータモデルの拡張はListModelに対してのみ行えばよく、ビューの問題に関しては意識する必要がない。
DefaultListModel
ListModelの基本的な機能を実装したクラス。
このクラスはListModelへの動的な追加、編集、削除に対応している。
今回作成するToDoリストは追加、編集、削除機能を提供するため、DefaultListModelを使用する。
プロジェクトの作成
プロジェクト名「ToDoList」を作成する。
1.[ファイル]⇒[新規]⇒[プロジェクト]を選択
2.[Javaプロジェクト]を選択し[次へ]をクリック
3.プロジェクト名等を入力し[完了]をクリック
- プロジェクト名に「ToDoList」を入力
- プロジェクト・レイアウトで[プロジェクト・フォルダーを・・・]を選択
SwingAppMainクラスの作成
GUIアプリケーションの起動および土台となるフレームの生成を担当するクラス「SwingAppMain」を作成する。
1.[ファイル]⇒[新規]⇒[クラス]を選択
2.[新規Javaクラス]ダイアログで各項目を入力し[完了]をクリック
- パッケージに「swing.sample」を入力
- 名前に「SwingAppMain」を入力
- public static void main(String[] args)をチェック
3.作成したクラスのソースコードを以下のように変更
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 | package swing.sample; import java.awt.BorderLayout; import java.awt.Container; import javax.swing.JComponent; import javax.swing.JFrame; import javax.swing.SwingUtilities; public class SwingAppMain { /** * アプリケーションの起動 */ public static void main(String[] args) { System.out.println("main : " + SwingUtilities.isEventDispatchThread()); SwingUtilities.invokeLater(new Runnable() { public void run() { createAndShowTodoList(); } }); } /** * ToDoリストの生成と表示を行います。 */ private static void createAndShowTodoList() { System.out.println("createAndShowTodoList :" + SwingUtilities.isEventDispatchThread()); JFrame mainFrame = new JFrame("ToDoリスト"); mainFrame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); Container contentPane = mainFrame.getContentPane(); // ToDoリストを生成 JComponent newContentPane = new ToDoListPane(); contentPane.add(newContentPane, BorderLayout.CENTER); // Windowサイズを調整 mainFrame.pack(); // 表示 mainFrame.setVisible(true); } } |
ToDoListPaneクラスの作成
ToDoリストのすべての機能を提供するクラス「ToDoListPane」を作成する。
1.[ファイル]⇒[新規]⇒[クラス]を選択
2.[新規Javaクラス]ダイアログで各項目を入力し[完了]をクリック
- パッケージに「swing.sample」を入力
- 名前に「ToDoListPane」を入力
- public static void main(String[] args)をチェック
3.作成したクラスのソースコードを以下のように変更
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 | package swing.sample; import java.awt.BorderLayout; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import javax.swing.DefaultListModel; import javax.swing.JButton; import javax.swing.JList; import javax.swing.JPanel; import javax.swing.JScrollPane; import javax.swing.JTextField; import javax.swing.SwingUtilities; import javax.swing.event.ListSelectionEvent; import javax.swing.event.ListSelectionListener; /** * ToDoリスト */ public class ToDoListPane extends JPanel { private static final long serialVersionUID = 1L; private JList toDoList; private DefaultListModel toDoListModel; private JTextField toDoInputField; private JButton addButton; private JButton modifyButton; private JButton removeButton; /** * */ public ToDoListPane() { super(new BorderLayout()); // 一覧を生成 toDoListModel = new DefaultListModel(); toDoList = new JList(toDoListModel); JScrollPane listScrollPane = new JScrollPane(toDoList); // TODOリストにリスナを設定 toDoList.addListSelectionListener(new ToDoListSelectionHandler()); // ToDo追加用テキストフィールドの生成 toDoInputField = new JTextField(); // 各ボタンの生成 JPanel buttonPanel = new JPanel(); addButton = new JButton("追加"); modifyButton = new JButton("編集"); removeButton = new JButton("削除"); // ボタンにリスナを設定 addButton.addActionListener(new AddActionHandler()); modifyButton.addActionListener(new ModifyActionHandler()); removeButton.addActionListener(new RemoveActionHandler()); buttonPanel.add(addButton); buttonPanel.add(modifyButton); buttonPanel.add(removeButton); add(listScrollPane, BorderLayout.NORTH); add(toDoInputField, BorderLayout.CENTER); add(buttonPanel, BorderLayout.SOUTH); } /** * TODOリスト選択アクションのハンドラ */ private class ToDoListSelectionHandler implements ListSelectionListener { public void valueChanged(ListSelectionEvent arg0) { // TODOリストのどの行も選択されていない場合や、 // 複数行が選択されている場合は、 // 何もしません if (toDoList.getSelectedIndices().length!=1) { return; } toDoInputField.setText((String)toDoList.getSelectedValue()); } } /** * 追加ボタンアクションのハンドラ */ private class AddActionHandler implements ActionListener { public void actionPerformed(ActionEvent e) { // テキストフィールドの内容をリストモデルに追加 toDoListModel.addElement(toDoInputField.getText()); } } /** * 編集ボタンアクションのハンドラ */ private class ModifyActionHandler implements ActionListener { public void actionPerformed(ActionEvent e) { // テキストフィールドの内容でリストモデルを編集 // TODOリストのどの行も選択されていない場合や、 // 複数行が選択されている場合は、 // 何もしません if (toDoList.getSelectedIndices().length != 1) { return; } toDoListModel.set(toDoList.getSelectedIndex(), toDoInputField.getText()); } } /** * 削除ボタンアクションのハンドラ */ private class RemoveActionHandler implements ActionListener { public void actionPerformed(ActionEvent e) { // テキストフィールドの内容をリストモデルに追加 // TODOリストのどの行も選択されていない場合や、 // 複数行が選択されている場合は、 // 何もしません if (toDoList.getSelectedIndices().length != 1) { return; } setButtonsEnabled(false); Thread removeThread = new RemoveThread(toDoList.getSelectedIndex()); removeThread.start(); } } /** * ボタンの状態を更新します */ private void setButtonsEnabled(boolean enabled) { addButton.setEnabled(enabled); modifyButton.setEnabled(enabled); removeButton.setEnabled(enabled); } /** * 削除処理を行うクラス */ class RemoveThread extends Thread { int index; RemoveThread(int index) { this.index = index; } public void run() { // 時間のかかる処理を実行 doLongTask(); // イベントディスパッチスレッドでボタンを変更 SwingUtilities.invokeLater(new Runnable() { public void run() { toDoListModel.remove(index); setButtonsEnabled(true); } }); } } /** * 時間のかかる処理 */ private void doLongTask() { try { Thread.sleep(10000); } catch (InterruptedException ex) { } } } |
アプリケーションの実行
実行すると一覧部、入力テキストフィールド、追加・編集・削除ボタンが配置されたウィンドウが開く。
[追加]ボタンをクリックすると、テキストフィールドに入力されている文字列(ToDo)を一覧に追加。
一覧でアイテムを選択し、[編集]ボタンをクリックすると選択されている1行の文字列がテキストフィールドに表示されている文字列に変更される。
一覧でアイテムを選択し、[削除]ボタンをクリックすると選択されている1行が削除される。
SwingAppMainとToDoListPaneの2つのクラス、それぞれの役割
プログラムの解説
ToDoリストが提供する機能はToDoListPaneクラスが担当し、アプリケーションの起動等の機能はSwingAppMainクラスが担当するという構成になっている。
JListの生成
ToDoリストは追加/編集/削除機能を持つため、ListModel も状態の変更を受けて更新する処理が必要。
ここでは、これらの機能を満たしているDefaultListModelを生成している。(ここでは標準ライブラリに用意されている実装を利用しているが、ListModelを実装することで、自由に実装することが可能)
1 2 3 4 | // 一覧を生成 toDoListModel = new DefaultListModel(); toDoList = new JList(toDoListModel); JScrollPane listScrollPane = new JScrollPane(toDoList); |
2行目、3行目でDefaultListModelを使用してJListを生成し、JListにスクロール機能を与えている。
最後にaddメソッドによりToDoListPaneに追加し、ToDo一覧の作成が完了する。
1 | add(listScrollPane, BorderLayout.NORTH); |
ToDo追加機能の実装(AddActionHandler)
AddActionHandlerクラスは、[追加]ボタンのイベントに対する処理を担当するインナークラス。
「toDoListModel.addElement();」にて、toDoListModelへパラメータで指定したデータを追加することができる。
追加されたデータの情報は、ListModelの監視の仕組みにより、JListのビューへ反映される。
1 2 3 4 5 6 7 8 9 | /** * 追加ボタンアクションのハンドラ */ private class AddActionHandler implements ActionListener { public void actionPerformed(ActionEvent e) { // テキストフィールドの内容をリストモデルに追加 toDoListModel.addElement(toDoInputField.getText()); } } |
ToDo編集機能の実装(ModifyActionHandler)
ModifyActionHandlerクラスは、編集ボタンのイベントに対する処理を担当するインナークラス。
「toDoListModel. set(int index, Object target);」にて、toDoListModelのindex番目のデータ、targetを設定することができる。
ToDo削除機能の実装(RemoveActionHandler)
RemoveActionHandlerクラスは、削除ボタンのイベントに対する処理を担当するインナークラス。
「setButtonsEnabled(false)」にて、すべてのボタンを非活性化し、その後doLongTaskにて、10秒待つ。
その後、「toDoListModel.remove(int index);」にて、toDoListModelのindex番目のデータを削除する。
削除した後、「setButtonsEnabled(true)」にて、すべてのボタンを活性化する。
ToDo選択行反映機能の実装(TodoListSelectionHandler)
TodoListSelectionHandlerクラスは、ToDoリストで選択した行の内容をテキストフィールドに反映する。
イベントディスパッチスレッド
削除処理にあえて10秒間待つ処理を入れた。
もし単純にこれを処理してしまうと、削除処理中は何も操作できなくなってしまう。
このような事態を避けるために、時間のかかる処理を別のスレッドに任せて、ユーザーが操作を行えるようにしながらバックグラウンドで処理を行う。
ここでは、RemoveThreadクラス内で別スレッドを呼び出し、時間のかかる処理(doLongTask)と削除処理を実行させることによって実現している。
時間のかかる処理を行った後、最後にSwingUtilities.invokeLater()を呼び出している。
これは、「toDoListModel.remove(int index);」および「setButtonsEnabled(true)」の2つの処理をイベントディスパッチスレッドで実行させるために使っている。
Swingのスレッドポリシーとして、“コンポーネントの「状態に依存する」あるいは「状態に影響を与える」処理はすべてイベントディスパッチスレッド上で動作しなければならない“というものがある。
これはいい換えると、Swingの描画に関連する処理は、シングルスレッドで処理しなくてはならないということ。
そして、そのシングルスレッドがイベントディスパッチスレッドになる。
このルールに則るため、わざわざSwingUtilities.invokeLater()を使っている。
今回の実装でも、sleepは描画に関係がないため別スレッドを生成して実行させたが、描画に関する処理(toDoListModel.remove、setButtonsEnabled)はイベントディスパッチスレッドで実行させるようにしている。
SwingAppMainクラス
SwingUtilities.invokeLater
「SwingAppMain」クラスのmainメソッド内ではSwingUtilities.invokeLater を呼び出し、Frameを描画する処理をイベントディスパッチスレッドに実行させている。
mainメソッドのスレッドが描画しているわけではない。
SwingUtilities.isEventDispatchThread
「SwingAppMain」クラスのmainメソッドとcreateAndShowTodoListメソッドでは、SwingUtilities.isEventDispatchThreadというメソッドが呼び出されている。
これは、実行されているスレッドがイベントディスパッチスレッドであればtureを、別のスレッドであればfalseを返す。
「SwingAppMain」を実行すると、コンソールに以下のように表示される。
1 2 | main : false createAndShowTodoList : true |
createAndShorTodoListメソッドは、イベントディスパッチスレッド上で実行されていることが分かる。