前の「JavaFX プロジェクトの作成、Scene Builder」では Scene Builder で BorderPane を親(root)コンテナとして作成したが、そのコンテナを処理するコントローラーは BorderPane の派生クラスではなかった。 今回は、VBox を親(root)コンテナとして作成し、それを処理するコントローラーを VBox の派生クラスとする。こうすることの利点は、この親(root)コンテナが再利用可能になること、コントローラーにコンストラクタの引数を通じてコントローラーの処理に必要となる情報を渡すこと、である。
作成するのは LaunchAnotherStage で、stage(ウィンドウ)内に自分に関する情報、「私は (何番目)/(総数) です。」を表示する。「起動」ボタンを押すことにより、高々1個だけ、同様な stage(ウィンドウ)である自分の子を起動できる。ウィンドウ右上にある閉じるボタン「X」を押すと、自分と(あれば、子ウィンドウ)を終了させる。下図は2個起動した状態、更に3個、合計5個を起動した状態、4個目の閉じるボタン「X」を押して、4個目と5個目を終了させた状況である。注意点は「私は (何番目)/(総数) です。」の(何番目)は変化しないが、(総数)が変化することである。
Scene Builder で VBox を親(root)コンテナとして利用し下図のように作成する。(LaunchAnotherStage の Eclipse のプロジェクトsrcのzipファイルはこちら。)
今は共に1が入っているが、後で変更する必要がある「1 / 1」の左のラベルを labelMe、右を labelTotal と名づける。「起動」ボタンを buttonLaunch と名づける。
Scene Builder で作成される fxml ファイルを LaunchAntherStageMainVBox.fxml とする。このファイルを VBox を派生したクラスファイル(LaucheAnotherStageMainVBox.java とする)で利用できるように、下図のように、Controller class に何も記入せずに、Use fx:root construct をチェックする。
以上で、Scene Builder での作業は終わった。
上述の VBox のクラスファイル(LaucheAnotherStageMainVBox.java)の説明は後に回し、この VBox を利用する、Main クラスの start(Stage) 関数を見てみる。
public void start(Stage primaryStage) {
LaunchAnotherStageMainVBox root = new LaunchAnotherStageMainVBox(primaryStage);
Scene scene = new Scene(root, 290, 100);
primaryStage.setScene(scene);
primaryStage.setTitle("Launch Another Stage");
primaryStage.show();
}
root に上述の VBox のインスタンスを代入し、それを Scene に入れ、それを primaryStage に入れ、タイトルを設定し、表示している。注意点は上述の VBox のコンストラクタに引数を利用して、LaunchAnotherStageMainVBox に primaryStage を渡している所である。これをどのように利用するか?は後述する。
LaucheAnotherStageMainVBox.java の説明に移る。コンストラクタでは引数で与えられた stage を後で利用するために、インスタンス変数に保存している。それ以降の処理で Scene Builder で作成した VBox のクラスが LaunchAnotherStageMainVBox であり、そこにあるラベルやボタン等の処理をするのも LaunchAnotherStageMainVBox であるように設定している。try 文にある fxmlLoader.load() 辺りで @FXML の次行以降に書かれた LaunchAnotherStageMainVBox を初期化する initialize() が呼ばれる。従って、必要な初期化、イベントハンドラー、チェンジリスナーなどをここに記述すればよい。
public LaunchAnotherStageMainVBox(Stage stage) {
this.stage = stage;
FXMLLoader fxmlLoader = new FXMLLoader(getClass().getResource("LaunchAnotherStageMainVBox.fxml"));
fxmlLoader.setRoot(this);
fxmlLoader.setController(this);
try {
fxmlLoader.load();
} catch (IOException exception) {
throw new RuntimeException(exception);
}
}
「起動」ボタンを押したときに起動される Stage(ウィンドウ)をクラスとして下記のように定義する。
public class LaunchAnotherStage extends Stage {
public LaunchAnotherStage() { // same as the start(stage) of Main class
try {
LaunchAnotherStageMainVBox root = new LaunchAnotherStageMainVBox(this);
Scene scene = new Scene(root, 290, 100);
this.setScene(scene);
this.setTitle("Launch Another Stage");
this.show();
} catch(Exception e) {
e.printStackTrace();
}
}
}
内容は Main クラスの start(primaryStage) と同じである。これにより、最初のウィンドウはこのstart(primaryStage)によるもの、それ以降のウィンドウは LaunchAnotherStage のインスタンスであり、厳密にいえば異なるが、目に見えるウィンドウは LaunchAnotherStageMainVBox のインスタンスであるので、同じである。
クラス LaunchAnotherStageMainVBox で処理をする必要があるのは
- 「私は (何番目)/(総数) です。」の(何番目)と(総数)を適切に求め、ラベルに設定する。
- 「起動」ボタンが押されたら、自分の子として、クラス LaunchAnotherStage の新たなインスタンスを作成し anotherStage に代入し、適切に表示し、「起動」ボタンを利用不可能にする。また、anotherStage が閉じられる場合には anotherStage を null にし、「起動」ボタンを再度利用可能にする。
- ある LaunchAnotherStage のインスタンスが(右上の閉じるボタン「X」を押されたことにより)閉じられたら、その子がある場合にはその子の stage を終了し、(総数)を適切に修正する。
(1)
(何番目)を保持する変数 me と(総数)を保持する変数 total を宣言する。me の方はインスタンス変数で良いが、 total の方はどのインスタンスでも同じ値なので、static 変数(クラス変数)とする。また、me が意味する(何番目)は変化しないが、total が意味する(総数)は表示されているウィンドウの個数が変われば変化し、それに応じて labelTotal を書き換える必要がある。そのために、total を IntegerProperty として定義すれば、total の変化を自動的に追跡してくれるので、そのチェンジリスナーで labelTotal を書き換えることが可能になる、ことを利用する。まず、宣言の部分は、
public static IntegerProperty total = new SimpleIntegerProperty(0); // initial value is zero
public IntegerProperty totalProperty() {
return total;
}
public int getTotal() {
return total.get();
}
public void setTotal(int i) {
total.set(i);
}
private int me = 0;
total のチェンジリスナーの部分と total、me、labetTotal、labelMe の初期化の部分は、次の通りである。ただし、実際は、関数 initialize() の中に記述する。
total.addListener(new ChangeListener<Number>() {
@Override
public void changed(ObservableValue<? extends Number> ov, Number oldValue, Number newValue) {
labelTotal.setText(""+newValue.intValue()); // set labelTotal to be the current value of total
}
});
total.set(total.get()+1); // increase total by one
me = total.get(); // me is the same as total
labelTotal.setText(""+total.get());
labelMe.setText(""+me);
すなわち、1つ目は、total の値が変化したら呼び出される関数 changed() で、変化後の新しい値を labelTotal に表示するように、チェンジリスナーを設定している。次の2行は total と me の初期値を、最後の2行はこれらに対応するラベルに値を、代入している。
さて、次の(2)で必要となる、ウィンドウに関する基本的なことを述べる。これに関しては「JavaFXとウィンドウ」を参照させていただいた。
ある stage が閉じる前に何か処理をさせるには、
stage.showingProperty().addListener(new ChangeListener<Boolean>() {
@Override
public void changed(ObservableValue<? extends Boolean> ov, Boolean oldValue, Boolean newValue) {
if (oldValue && !newValue) {
// stage is closing
// do what you want
}
}
});
の // do what you want の所に行いたい処理を記述する。
ある stage が他の anotherStage を閉じるには、
anotherStage.fireEvent(new WindowEvent(stage, WindowEvent.WINDOW_CLOSE_REQUEST));
とすればよい。これらを利用するために、次のインスタンス変数を宣言する。stage にはコンストラクタ LaunchAnotherStageMainVBox(stage) の中で有効な値が代入される。
private Stage stage = null; // parent stage of myself private LaunchAnotherStage anotherStage = null; // child stage of myself if buttonLaunch is pressed
(2)
「起動」ボタンが押された時のイベントハンドラーの設定は次のようになる。
buttonLaunch.setOnAction(new EventHandler<ActionEvent>() {
@Override
public void handle(ActionEvent event) {
buttonLaunch.setDisable(true);
anotherStage = new LaunchAnotherStage();
// move anotherStage to the right and to the down from the original center position
anotherStage.setX(anotherStage.getX()+120*(total.get()-1));
anotherStage.setY(anotherStage.getY()+120*(total.get()-1));
anotherStage.showingProperty().addListener(new ChangeListener<Boolean>() {
@Override
public void changed(ObservableValue<? extends Boolean> ov, Boolean oldValue, Boolean newValue) {
if (oldValue && !newValue) {
// anotherStage is closing
anotherStage = null;
buttonLaunch.setDisable(false);
}
}
});
}
});
すなわち、「起動」ボタンを利用不可能にし、LaunchAnotherStageの新しいインスタンスを anotherStage に代入し、表示位置を(余り重ならないように)修正し、この anotherStage が閉じられる時に、anotherStage に null を代入し、「起動」ボタンを再度利用可能に戻す。
(3)
LaunchAnotherStageMainVBox の親 stage が閉じる時の処理を行うチェンジリスナーの設定は次の通りである。
stage.showingProperty().addListener(new ChangeListener<Boolean>() {
@Override
public void changed(ObservableValue<? extends Boolean> ov, Boolean oldValue, Boolean newValue) {
if (oldValue && !newValue) {
// stage is closing
if (anotherStage != null) {
// close anotherStage
anotherStage.fireEvent(new WindowEvent(stage, WindowEvent.WINDOW_CLOSE_REQUEST));
}
// revise total properly
total.set(me-1);
}
}
});
すなわち、この親 stage が閉じられる時に、anotherStage が起動されていたら、この anotherStage を閉じるようする。また、この親 stage が終了するので、それに応じて、total を me – 1 に変更する。
この LaunchAnotherStage の Eclipse のプロジェクトsrcのzipファイルはこちら。実行ファイルはこちら。




