JavaFX アプリケーション

JavaFX 11以上、Scene Builder、Eclipse を利用して


Scene Builder で作成したコンテナをそのコンテナの派生クラスとして利用する

投稿日時:

最終更新日時:

カテゴリー:

,

前の「JavaFX プロジェクトの作成、Scene Builder」では Scene Builder で BorderPane を親(root)コンテナとして作成したが、そのコンテナを処理するコントローラーは BorderPane の派生クラスではなかった。 今回は、VBox を親(root)コンテナとして作成し、それを処理するコントローラーを VBox の派生クラスとする。こうすることの利点は、この親(root)コンテナが再利用可能になること、コントローラーにコンストラクタの引数を通じてコントローラーの処理に必要となる情報を渡すこと、である。

作成するのは LaunchAnotherStage で、stage(ウィンドウ)内に自分に関する情報、「私は (何番目)/(総数) です。」を表示する。「起動」ボタンを押すことにより、高々1個だけ、同様な stage(ウィンドウ)である自分の子を起動できる。ウィンドウ右上にある閉じるボタン「X」を押すと、自分と(あれば、子ウィンドウ)を終了させる。下図は2個起動した状態、更に3個、合計5個を起動した状態、4個目の閉じるボタン「X」を押して、4個目と5個目を終了させた状況である。注意点は「私は (何番目)/(総数) です。」の(何番目)は変化しないが、(総数)が変化することである。

ウィンドウを2個、更に3個起動し、その後、4個目のウィンドウの閉じるボタン「X」を押したところ
他のウィンドウを起動1 他のウィンドウを起動2 他のウィンドウを起動3

Scene Builder で VBox を親(root)コンテナとして利用し下図のように作成する。(LaunchAnotherStage の Eclipse のプロジェクトsrcのzipファイルはこちら。)

VBox 内の階層構造と変数名
SceneBuilder1

今は共に1が入っているが、後で変更する必要がある「1 / 1」の左のラベルを labelMe、右を labelTotal と名づける。「起動」ボタンを buttonLaunch と名づける。

Scene Builder で作成される fxml ファイルを LaunchAntherStageMainVBox.fxml とする。このファイルを VBox を派生したクラスファイル(LaucheAnotherStageMainVBox.java とする)で利用できるように、下図のように、Controller class に何も記入せずに、Use fx:root construct をチェックする。

Use fx:root construct をチェックする
SceneBuilder2

以上で、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 で処理をする必要があるのは

     
  1. 「私は (何番目)/(総数) です。」の(何番目)と(総数)を適切に求め、ラベルに設定する。
  2. 「起動」ボタンが押されたら、自分の子として、クラス LaunchAnotherStage の新たなインスタンスを作成し anotherStage に代入し、適切に表示し、「起動」ボタンを利用不可能にする。また、anotherStage が閉じられる場合には anotherStage を null にし、「起動」ボタンを再度利用可能にする。
  3. ある 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ファイルはこちら。実行ファイルはこちら