torutkのブログ

ソフトウェア・エンジニアのブログ

JavaFX 2でアプリケーション作成(その12)

JavaFX 2でアプリケーション作成(その11) - torutkのブログの続きです。

JavaFX 2アプリケーションのGUIをFXML(XMLファイル)で定義します。
今回は、FXMLで画面レイアウトを実現するところまで記述し、ボタンを押した時のコントロールは次回のネタとします。

FXMLをロードする処理を記述、画面配置はJavaプログラムから削除

まず、今までJavaプログラムで記述した画面配置はFXMLに記述するので、プログラムではFXMLファイルのロード処理に変えます。

import java.io.IOException;
import java.util.ResourceBundle;
import java.util.concurrent.Semaphore;
import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.stage.Stage;

public class SemaphoreView extends Application {
    private Semaphore semaphore = new Semaphore(1);
    private AcquireService acquireService;

    @Override
    public void start(Stage primaryStage) {
	primaryStage.setTitle("Semaphoreの動きを理解する");

	Parent root = createRootFromFxml();
	Scene scene = new Scene(root, 360 , 250);
	String style = SemaphoreView.class.getResource("stylesheet.css").toExternalForm();
	scene.getStylesheets().add(style);
	primaryStage.setScene(scene);
	primaryStage.show();
    }

    private Parent createRootFromFxml() {
	Parent root = null;
	try {
	    root = FXMLLoader.load(getClass().getResource("semaphore.fxml"));
	} catch (IOException ex) {
	    // エラーダイアログを出したい
	}
	return root;
    }


    public static void main(String[] args) {
	launch(args);
    }
}

FXMLLoaderクラスのloadメソッドでFXMLファイルを指定してロードさせています。

また、FXMLのロードで例外発生時にエラー処理をさぼっています。
そういえば、CSSファイルのロードでもエラー処理が必要なのかもしれませんが、こちらはチェック例外ではないので、実行時例外(CSSファイルを削除して実行したら)が発生しました。

FXMLファイルの記述

実行時にクラスファイル SemaphoreView.class と同じディレクトリに置きます。

  • semaphore.fxml
<?xml version="1.0" encoding="UTF-8"?>

<?import javafx.scene.control.Button?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.control.ProgressBar?>
<?import javafx.scene.control.TextField?>
<?import javafx.scene.layout.BorderPane?>
<?import javafx.scene.layout.GridPane?>
<?import javafx.scene.text.Text?>
<?import javafx.geometry.Insets?>

<BorderPane>
  <top>
    <Label text="Semaphoreへの操作とその結果"
	   id="topLabel" BorderPane.alignment="CENTER">
      <BorderPane.margin>
	<Insets top="6" right="6" bottom="6" left="6"/>
      </BorderPane.margin>
    </Label>
  </top>
  
  <center>
    <GridPane styleClass="grid" hgap="10" vgap="16">
      <BorderPane.margin>
	<Insets top="4" right="12" bottom="4" left="12"/>
      </BorderPane.margin>

      <children>
	<Label text="利用可能な許可数:"
	       GridPane.columnIndex="0" GridPane.rowIndex="0"/>
	<TextField GridPane.columnIndex="1" GridPane.rowIndex="0">
	  <GridPane.margin>
	    <Insets top="12" right="6" bottom="12" left="6"/>
	  </GridPane.margin>
	</TextField>

	<Button text="Try Acquire" maxWidth="Infinity"
		GridPane.columnIndex="0" GridPane.rowIndex="1"/>

	<Text text="result"
		GridPane.columnIndex="1" GridPane.rowIndex="1">
	  <GridPane.margin>
	    <Insets top="12" right="6" bottom="12" left="6"/>
	  </GridPane.margin>
	</Text>

	<Button text="Release" maxWidth="Infinity"
		GridPane.columnIndex="0" GridPane.rowIndex="2"/>

	<Button text="Acquire" maxWidth="Infinity"
		GridPane.columnIndex="0" GridPane.rowIndex="3"/>

	<ProgressBar maxWidth="Infinity"
		     GridPane.columnIndex="1" GridPane.rowIndex="3">
	  <GridPane.margin>
	    <Insets top="12" right="6" bottom="12" left="6"/>
	  </GridPane.margin>
	</ProgressBar>

      </children>
    </GridPane>
  </center>
</BorderPane>
import

まず、importが必要です。
使用するタグはimportしたものである必要があります。

<?import javafx.scene.control.Button?>

のように、XMLではなく独自構文となります。このimport文を書くと、

ルート要素

FXMLにはスキーマがないので、ルート要素は何でも書けます。ここではAPI版で最上位としたコンテナBorderPaneをルート要素としました。

XML名前空間の定義は今回は省いてますが、コントロールの導入(次回)で必要になるので次回追加予定です。

BorderPaneのTOPに貼るLabel

今回FXMLのレイアウトで苦労した一つがInsetsの設定です。ドキュメントには説明や例が見当たらず、試行錯誤の連続でした。今回示したFXMLの記述が最適かどうかも分かってはいませんが、とりあえず見た目にAPI版とほぼ同じ配置になったので、一応よしとしました。

BorderPaneのTOPに貼るLabelのレイアウトについて、APIで書いていた次のコード

	Label topLabel = new Label("Semaphoreへの操作とその結果");
	topLabel.setId("topLabel");
        BorderPane.setAlignment(topLabel, Pos.CENTER);
	BorderPane.setMargin(topLabel, new Insets(6));

は、FXMLでは次の記述となります。

    <Label text="Semaphoreへの操作とその結果"
	   id="topLabel" BorderPane.alignment="CENTER">
      <BorderPane.margin>
	<Insets top="6" right="6" bottom="6" left="6"/>
      </BorderPane.margin>
    </Label>

textとidは、Labelのプロパティなので、FXMLでもLabelの属性で指定します。
alignmentは、Labelのプロパティではなく、BorderPaneのstaticな(?)定義なので、指定方法が分からず四苦八苦してましたが、BorderPane.alignment、BorderPane.marginという指定になるようです。なお、marginで指定するInsetsは単一項では指定できないので、Labelの属性ではなく子要素としてネストして指定しています。

BorderPaneのCENTERに貼るGridPane

BorderPaneのCENTERに貼るGridPaneのレイアウトについて、APIで書いていた次のコード

	GridPane semaphorePane = new GridPane();
	semaphorePane.getStyleClass().add("grid");
	BorderPane.setMargin(semaphorePane, new Insets(4, 12, 4, 12));
	borderPane.setCenter(semaphorePane);
	semaphorePane.setHgap(10);
	semaphorePane.setVgap(16);

は、FXMLでは次の記述となります。

  <center>
    <GridPane styleClass="grid" hgap="10" vgap="16">
      <BorderPane.margin>
	<Insets top="4" right="12" bottom="4" left="12"/>
      </BorderPane.margin>
      <children>
        :(中略)
      </children>
    </GridPane>
  </center>

styleClassの指定は属性にしてみました。適用するスタイルクラスが1つのときはこれでよいかと思いますが、2つ以上あるときは、別の書き方が必要と思います。子要素で複数列挙することができるかは未調査です。

BorderPaneのCENTERに貼るGridPaneの余白をBorderPane.marginで指定します。

GridPaneに貼るLabel

GridPaneに貼るLabelのレイアウトについて、APIで書いていた次のコード

	Label permitsLabel = new Label("利用可能な許可数:");
	semaphorePane.add(permitsLabel, 0, 0);

は、FXMLでは次の記述となります。

	<Label text="利用可能な許可数:"
	       GridPane.columnIndex="0" GridPane.rowIndex="0"/>
GridPaneに貼るTextField

GridPaneに貼るTextFieldのレイアウトについて、APIで書いていた次のコード

	permitsField = new TextField();
	semaphorePane.setMargin(permitsField, new Insets(12, 6, 12, 6));
	semaphorePane.add(permitsField, 1, 0);

は、FXMLでは次の記述となります。

	<TextField GridPane.columnIndex="1" GridPane.rowIndex="0">
	  <GridPane.margin>
	    <Insets top="12" right="6" bottom="12" left="6"/>
	  </GridPane.margin>
	</TextField>
GridPaneに貼るButton

GridPaneに貼るButtonのレイアウトについて、APIで書いていた次のコード

	Button tryButton = new Button("Try acquire");
	tryButton.setMaxWidth(Double.MAX_VALUE);
     :(中略)
	semaphorePane.add(tryButton, 0, 1);

は、FXMLでは次の記述となります。

	<Button text="Try Acquire" maxWidth="Infinity"
		GridPane.columnIndex="0" GridPane.rowIndex="1"/>

maxWidthの指定は、数値のときはそのまま数値を書きますが、Double.MAX_VALUEのときは、"Infinity"と記述しました。Double.MIN_VALUEは、"-Infinity"になるようです。

なお、ボタンを押した時のイベントハンドラの指定は今回は省略しています。

GridPaneに貼るText

GridPaneに貼るTextのレイアウトについて、APIで書いていた次のコード

	resultText = new Text();
	semaphorePane.setMargin(resultText, new Insets(12, 6, 12, 6));
	semaphorePane.add(resultText, 1, 1);

は、FXMLでは次の記述となります。

	<Text text="result"
		GridPane.columnIndex="1" GridPane.rowIndex="1">
	  <GridPane.margin>
	    <Insets top="12" right="6" bottom="12" left="6"/>
	  </GridPane.margin>
	</Text>
GridPaneに貼るProgressBar

GridPaneに貼るProgressBarのレイアウトについて、APIで書いていた次のコード

	progressBar = new ProgressBar(0);
          : (bindに関しては中略)
	progressBar.setMaxWidth(Double.MAX_VALUE);
	semaphorePane.setMargin(progressBar, new Insets(12, 6, 12, 6));
	semaphorePane.add(progressBar, 1, 3);

は、FXMLでは次の記述となります。

	<ProgressBar maxWidth="Infinity"
		     GridPane.columnIndex="1" GridPane.rowIndex="3">
	  <GridPane.margin>
	    <Insets top="12" right="6" bottom="12" left="6"/>
	  </GridPane.margin>
	</ProgressBar>

なお、bind設定は次回以降とします。

FXML雑感

APIで書いたInsetsの設定がFXMLではなかなか実現できず、SceneBuilderで吐かせたFXMLを参考に書きました。FXMLはスキーマもなく、ドキュメントにmarginまでの解説/サンプルが見当たらず、ここまで至るのに前回の日記から10日近くかかってしまいました。

APIで書くときは、呼び出せるメソッドはJavadoc APIで分かるし、IDEが間違いを書いているその場で指摘してくれるのですが、FXMLではJavadoc APIに相当する網羅的な情報がないのと記述時にエラーチェックがないため、試行錯誤が多く、大変苦労します。

SceneBuilderで丸ごと配置を作ろうともしましたが、BorderPaneがなかったので、生成されるFXMLを参考にする程度でした。

なお、SceneBuilderは、ビジュアルにGUIコントロールを配置しFXMLファイルを生成するツールで、以下からダウンロードできます(本日現在、プレビュー b37版)。