AWT/SwingアプリのメニューからJavaFXで作成したダイアログを表示、ダイアログで入力された情報を取得する。
AWTのSystemTrayを使ってタスクトレイにアイコンを表示し、メニューが選択されるとダイアログを表示するというAWT/Swingアプリがあったので、そのダイアログ部分をJavaFX 2.1を利用する形に書き換えてみた。
何が変化するかというと
- JavaFXのほうが今風のかっこいい見栄えになる。好みですが。
- JavaFX Scene Builderを使ってFXMLで画面を定義できる。
- 動作は若干AWT/Swingより重いかも。特にJavaFXの初期化あたりで。ただ、最近の普通のPCなら気にならない程度でしょう。
メイン処理
- メニューが選択されるとSwingのJFrameを作成。
- JFrameにJavaFXのJFXPanelを入れる。
- FXMLで書いた画面定義をFXMLLoaderでロードする。
- ロードした画面定義をJFXPanelでsetScene()する。
メイン処理とJFXPanel画面とのデータのやり取り
本当はどこかに書いてあるのかもしれないが、ちょっと探したくらいではドキュメント化された情報がなかったので、適当に以下のようにした。
- FXMLLoaderのgetController()でコントローラを取得する。コントローラというのはFXMLの画面定義に結び付けられるJavaのクラスで、画面で入力されたデータを取得できる。
- コントローラにメイン処理の参照を渡して、コールバックしてもらう。
以下、メイン処理のコード片
package xxxx; import javafx.application.Platform; import javafx.embed.swing.JFXPanel; import javafx.fxml.FXMLLoader; import javafx.scene.Parent; import javafx.scene.Scene; public class Main { private JFrame settingFrame; private SettingDialog settingDialog; private Main referThis = this; /** * メニューで設定コマンドが選択された場合の処理 */ private ActionListener settingListener = new ActionListener() { @Override public void actionPerformed(ActionEvent e) { // 二回目の実行では単に画面をVisibleにするだけ(これでいいのか?) if (settingFrame != null) { if (settingFrame.isVisible() == false) { settingFrame.setVisible(true); } return; } settingFrame = new JFrame("設定画面"); final JFXPanel fxPanel = new JFXPanel(); settingFrame.add(fxPanel); settingFrame.setModalExclusionType(Dialog.ModalExclusionType.APPLICATION_EXCLUDE); Platform.runLater(new Runnable() { @Override public void run() { try { FXMLLoader loader = new FXMLLoader(getClass().getResource("SettingDialog.fxml")); Parent root = (Parent)loader.load(); // コントローラにメイン処理への参照をセット settingDialog = (SettingDialog)loader.getController(); settingDialog.setMain(referThis); fxPanel.setScene(new Scene(root)); // 画面サイズの調整(fxPanelサイズにJFrameのタイトルの高さを加える) Dimension frameSize = fxPanel.getPreferredSize(); settingFrame.setVisible(true); frameSize.height += settingFrame.getHeight(); settingFrame.setSize(frameSize); } catch (IOException ex) { Logger.getLogger(Main.class.getName()).log(Level.SEVERE, null, ex); } } }); } }; /** * 設定画面でOK/Cancelボタンが押された場合に呼ばれる。 * @param setting 画面で入力された情報。Cancelの場合はnull。 */ public void onCloseSettingDialog(SettingBean setting) { if (setting != null) { // 入力結果の取得 settingBean = setting; // ファイルへ保存 saveSetting(); } // 画面を消去 settingFrame.setVisible(false); // disposeすると二回目でJavaFXのエラーになる //settingFrame.dispose(); //settingFrame = null; } }
コントローラ クラス
- FXMLの中で fx:controller="xxxx.SettingDialog"のようにしてコントローラ クラスを指定する。
<?xml version="1.0" encoding="UTF-8"?> <?import java.lang.*?> <?import java.util.*?> <?import javafx.collections.*?> <?import javafx.scene.control.*?> <?import javafx.scene.layout.*?> <?import javafx.scene.paint.*?> <AnchorPane id="AnchorPane" maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" prefHeight="176.0" prefWidth="422.0" xmlns:fx="http://javafx.com/fxml" fx:controller="xxxx.SettingDialog">
- またFXMLの画面中でコントローラ クラスで参照したいものには fx:id を指定する。ちなみに fx のない id だと駄目(ちょっとハマった)。
<TextField fx:id="textField" /> <CheckBox fx:id="checkBox" /> <ComboBox fx:id="comboBox" />
- ボタンを押したときにメソッドを呼んでもらいたいような時は onAction で # 付きで指定するようだ。
<Button text="OK" onAction="#handleButtonOkAction" /> <Button text="キャンセル" onAction="#handleButtonCancelAction" />
以下、コントローラのコード片
- privateメンバの場合、@FXMLを付ける。
- 後は勝手にバインディングされてるので、ちょっと感激。
package xxxx; import java.net.URL; import java.util.ResourceBundle; import javafx.event.ActionEvent; import javafx.fxml.FXML; import javafx.fxml.Initializable; import javafx.scene.control.CheckBox; import javafx.scene.control.ComboBox; import javafx.scene.control.TextField; public class SettingDialog implements Initializable { /** * メイン処理への参照 */ private Main referMain; @FXML private TextField textField; @FXML private CheckBox checkBox; @FXML private ComboBox comboBox; @Override public void initialize(URL url, ResourceBundle rb) { } /** * OKボタン */ @FXML private void handleButtonOkAction(ActionEvent event) { if (referMain != null) { SettingBean setting = new SettingBean(); setting.setText(textField.getText()); setting.setCheckBox(checkBox.isSelected()); setting.setComboBox(comboBox.getPromptText()); referMain.onCloseSettingDialog(setting); } } /** * キャンセル ボタン */ @FXML private void handleButtonCancelAction(ActionEvent event) { if (referMain != null) { referMain.onCloseSettingDialog(null); } } public void setMain(Main referMain) { this.referMain = referMain; } }
二回disposeしてしまうと二回目でエラーになる。
メイン処理で最初OK/Cancelボタンが押されたらJFrameをdispose()していたのだが、そうすると一回目はいいが、二回目の画面表示でエラーになってしまった。
JavaFXのスレッドが終了してしまったからのようだ。なので、今はJFrameを残したまま非表示にしているのだが、本当はdispose()したいような?
Exception in thread "AWT-EventQueue-0" java.lang.IllegalStateException: Platform.exit has been called at com.sun.javafx.application.PlatformImpl.startup(Unknown Source) at javafx.embed.swing.JFXPanel.initFx(Unknown Source) at javafx.embed.swing.JFXPanel.<init>(Unknown Source)