torutkのブログ

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

JavaFXアプリケーションでユーザーが編集可能なCSSファイルを置くには

問題

JavaFXアプリケーションを作成する際に、画面のレイアウトをFXMLで記述、色やフォントなどの見栄えをCSSファイルに記述し、FXMLのルートノードにCSSファイルを指定しました。

模式的なソースファイル構成は次のようになります。

myapp
  +-- src
        +-- mypackage
              +-- MyApp.java
              +-- MyApp.fxml
              +-- MyApp.css

MyApp.javaの中でFXMLファイルを読み込みます。

Parent root = FXMLLoader.load(getClass().getResource("MyApp.fxml"));
Scene scene = new Scene(root);

MyApp.fxml の中でCSSファイルを指定します。

<HBox stylesheets="@MyApp.css" xmlns=...>

このアプリケーションをビルドすると、CSSファイルは他のクラスファイル等と一緒にJARファイルまたはjlinkでモジュールファイルに含まれます。

このアプリケーションを実行するマシンに配布したあとに、画面の見栄えをカスタマイズしようとCSSファイルを取り出して編集、再組み込みすることは簡単ではありません。
JARファイルならzipアーカイブツールでCSSファイルを取り出し編集後再度JARファイルに戻すことは可能です(かなり面倒です)。
jlinkのモジュールファイルの場合、zipではないので取り出し編集は困難です。

そこで、ビルド時にJARファイルまたはモジュールファイルに組み込まれるデフォルトの設定を記述したCSSファイルとは別に、独立したCSSファイルをアプリケーション実行時に読み込み反映する方法を探ってみました。

sceneに別CSSファイルを設定するも反映されず

カレントディレクトリにMyCustom.cssファイルが存在すればそれをSceneのスタイルシートとして設定するコードを追加しました。

Path path = Paths.get("MyCustom.css");
if (Files.exists(path)) {
    scene.getStylesheets().add(path.toUri().toString());
}

しかし、これではMyCustom.cssに記述したカスタム設定が反映されませんでした。

このコードでは、SceneとそのrootノードのHBoxにそれぞれ別のスタイルシートが設定されます。

Scene         ---> MyCustom.css
  +-- HBox    ---> MyApp.css

試行錯誤と資料調査をしたところ、JavaFXのドキュメント「JavaFX CSS Reference Guide」に次の記述を見つけました。

Style sheets from a Parent instance are considered to be more specific than those styles from Scene style sheets.

つまり、Sceneのスタイルシートよりもその子ノード(rootノード)のスタイルシートの方が優先されてしまいます

sceneのrootノードにスタイルシートを設定する

そこで、Sceneのrootノードに対してスタイルシートを設定するようにしました。

Path path = Paths.get("MyCustom.css");
if (Files.exists(path)) {
    scene.getRoot().getStylesheets().add(path.toUri().toString());
}

これで、アプリケーションを実行するときのカレントディレクトリにMyCustom.cssファイルが存在すれば、その内容が反映されるという仕組みができました。

gitのリリースタグをアプリケーションのバージョン表記に使う

はじめに

JavaJavaFXで小さなスタンドアロン・アプリケーション(ユーティリティ)を作成、使用するマシンに配置し、使い勝手を見ながら改善しています。

使用しているJavaの種類は、JavaFXを内蔵しているAzul Systems社のOpenJDKであるZuluFX 11で、モジュール(Java Platform Module System)として作成し、jlinkコマンドで実行イメージを作成してJavaのランタイム込みで配布しています。

アプリケーションの利用者から今使っているバージョンを認識し、フィードバックを得るようにするため、バージョン番号表記をどのように組み込もうかと試行錯誤してみました。

世の中よく見かけるバージョン表記は、アプリケーションのメニューバーに「ヘルプ」を置き、メニュー項目に「このアプリケーションについて」等を入れ、それをクリックするとアプリケーションの名前、バージョン、その他情報をポップアップ表示するといったものです。

ですが、小さなプログラムでメニューバーを設けていない場合は別な手段が必要になります。
今回は、ウィンドウのタイトルバーにバージョンを表記することとしました。

f:id:torutk:20190624185938p:plain

バージョン管理ツールとバージョン

gitを使ってソースコードのバージョン管理をしています。リリースの際にはタグでリリースバージョンを設けます。

D:\work> git tag v1.3.2

このタグをバージョン表記に使用することとし、ソースコードリポジトリにはバージョン番号を記載したファイルは設けないようにします。

ビルド時に、次のコマンドでバージョンを取得します。

D:\work> git describe --tag abbrev=0
v1.3.2

abbrev=0を付与しない場合、タグをつけたコミットからタグをつけないコミットを行った際、次の様にタグをつけてからのコミット数とハッシュが追加されて表示されます。

D:\work> git describe --tag
v1.3.2-2-g595ad04
gitのタグの種類

gitには、軽量タグ、注釈タグ、署名タグとタグの種類がいくつか存在します。
軽量タグを付与した場合、git describeでは--tagオプションを指定する必要があります。

注釈タグは、タグを付与するときにコミットログを記述します。注釈タグを付与した場合、git describeでは--tagオプションがなくても最新のタグを取り出すことができます。

バージョン番号の埋め込み方法

JavaFXでは、ウィンドウのタイトルへ表示する文字は次のようにStageクラスのsetTitleメソッドを呼んで設定します。

    @Override
    public void start(Stage primaryStage) throws IOException {
        :(略)
        primaryStage.setTitle("STC-110 Remote Viewer");
        :(略)
    }

ビルド時にgitのタグからバージョン番号を取り出してウィンドウタイトルに表示させるため、ここではリソースバンドルにバージョンをビルド時に埋め込むようにします。

    @Override
    public void start(Stage primaryStage) throws IOException {
        var bundle = ResourceBundle.getBundle("antenna.direction.AntennaDirectionView");
        :(略)
        primaryStage.setTitle("STC-110 Remote Viewer " + bundle.getString("antenna.direction.version"));
        :
Windowsバッチファイルで置換する場合

リソースバンドルには、ビルド時に文字列置換できるように置換キーワードを記述した状態でコミットしておきます。

antenna.direction.version = #VERSION_TO_BE_REPLACED#

そして、ビルド時に置換キーワードをgitのタグ情報に置き換えます。

for /f %%i in ('git describe --tag --abbrev^=0') do set VERSION=%%i

ren %PROPERTY_DIR%\%PROPERTY_FILE% %PROPERTY_FILE%_
setlocal enabledelayedexpansion
for /f "delims=" %%I in (%PROPERTY_DIR%\%PROPERTY_FILE%_) do (
    set line=%%I
    echo !line:#VERSION_TO_BE_REPLACED#=%VERSION%!>>%PROPERTY_DIR%\%PROPERTY_FILE%
)
endlocal

Java読書会BOF「Java 11 and 12 New Features」(洋書)を読む会を開催して

Java 11 and 12 New Features」(洋書)を読む会を開催

昨日6月15日(土)に、Java読書会BOFは書籍「Java 11 and 12 - New Features」を読む会(第1回)を開催しました。

あいにくの雨模様でしたが、10名を超える参加者が集まりました。Javaの最新技術、といっても2年前の2017年にリリースされたJava SE9からの技術を解説した日本語の書籍が皆無で、洋書の選択となりました。

Java読書会BOFが開催する読書会では、書籍を本文、脚注、ソースコードとすべてを朗読して進めています。今回は洋書なので、どう進めようか?と思いましたが、英語のままで朗読して進めてみることとしました。

書籍の入手に関して

Amazonのサイトには、紙の書籍とKindle電子書籍版があり、紙版も2日程度で入手可能となっています。また、洋書の出版社Packt Publishingのサイトには、ePubおよびPDFの電子書籍版があります。Kindle版は2840円ですが、ePubおよびPDF版は10ドル(約1100円)と半額以下です。

英語の朗読について

英語の朗読なので、英語の得意な人は問題なく把握できますが、そうでないと分からない単語が多く読み取ることがなかなかむずかしいというところがありました。今回の本は割とやさしい英文でしたが、それでもちょっと苦労はあったようです。

少し時間がかかりますが、段落ごとに内容を日本語で簡単にまとめるといったフェーズをおくのがよかったかもしれません。

内容について

型推論

Java 10で導入された型推論(var)について、3時間はじっくりとかけて読みました。
ローカル変数のみ型をvarとして宣言可能で、未初期化、null代入、配列初期化子、配列の括弧とは一緒に定義できないという制約があります。また、varは予約語(キーワード)ではないので、メソッド名やインスタンス変数、static変数の識別子にvarの名前を使用可能です。

議論はありましたが、ローカル変数の宣言行でvarによる型推論を使うことでその宣言行を読んだだけでは型が分からない場合(例を次に示す)はvarによる型推論を使わない、というのが必須と思います。

var i = getData();

これは、getDataのメソッド名の宣言行(シグニチャ)を調べないと、ローカル変数iの型が分かりません。varを導入したことによりコードの読み手に対する情報が欠落してしまっています。

左辺の変数名、あるいは右辺のメソッド名から型が容易に推測できる場合はvarによる型推論を使ってもよいという意見もあります。しかし、個人的には、ローカル変数の宣言行で、型名を2回記述しなければならないときに型指定をvarとしてもよい、それ以外の場合は、型を書くというのをベストプラクティスにしたいです。

CDS(クラス・データ・シェアリング)

CDSは実はJava SE 5.0 から搭載されていた機能でしたが、まったく気にしたことがなく、使い方の記述を日本語では殆ど見かけたことがありませんでした。今回読書会では、JDKの標準クラスが格納されたclasses.jsaと、アプリケーションの起動時に必要なクラスをシェアリング用ファイルとして生成し使用するAppCDSとについて、じっくり読んで知識を得られました。

自宅のPC(Windows OS 64bit)にインストールしているJDKの各ディストリビューションとバージョンについてclasses.jsaの有無を見てみると、

  • Oracle JDK 8/9/10 ⇒ あり
  • Oracle OpenJDK 11/12 ⇒ あり
  • Liberica JDK 11 ⇒ なし
  • ZuluFX 11 ⇒ なし

となっていました。classes.jsaがない場合は、次のコマンドで生成することができます。

java -Xshare:dump 

Java SE 11以降では、CDSの使用可否を指定するJVMオプション -Xshare=auto がデフォルトとなりました。
[JDK-8197967] Make -Xshare:auto the default for server VM - Java Bug System

なので、Oracle JDK または Oracle OpenJDK の11以降を使う場合は明示的に指定しなくてもCDSが有効となります。Oracle以外のOpenJDKでclasses.jsaファイルがない場合は、そのOpenJDKのインストール直後に java -Xshare:dump を一度実行しておくとよいでしょう。

AppCDS(アプリケーション・クラス・データ・シェアリング)

CDSJavaの標準クラスをシェアする機構に対して、AppCDSはアプリケーションのクラスを含んだクラスをシェアする機構です。

オプションの指定がJava SE 11以降とJava SE 10とで変わるので少々混乱してしまいます。
次のJVMオプションはJava SE 11以降では廃止となって指定する必要はありません。

-XX:+UseAppCDS 

Java SE 11以降では、次のステップでAppCDSのシェアドアーカイブファイルを適用します。

  • 次のJVMオプションを指定してアプリケーションを実行、起動時にロードするクラス一覧のリストファイルを生成
 -Xshare:off -XX:DumpLoadedClassList=<リストファイル名>
  • 次のJVMオプションを指定してアプリケーションを実行、アプリケーションのクラスを含む起動時にロードするシェアドアーカイブファイルを生成
-Xshare:dump -XX:SharedClassListFile=<リストファイル名> -XX:SharedArchiveFile=<シェアドアーカイブファイル名> 
  • 以後、アプリケーションを実行するときは次のJVMオプションを指定
-XX:SharedArchiveFile=<シェアドアーカイブファイル名>

最初のステップで-Xshare:offを指定するのは、リストを作成するときに標準CDSアーカイブファイルから読み込むことを抑制するためと思われます。

次回の範囲は

次回は、次の項目を読み進める予定です。

  • Parallel full GC for G1
  • Miscellaneous Improvements in JDK 10
  • Local Variable Syntax of Lambda Parameters
  • Epsilon GC
  • The HTTP Client API

JavaFXの入力部品でエラー時に色を変える

GUIでユーザーが入力した値が範囲外などのエラー時に、エラーとなった箇所が分かるよう色を変える方法を模索しました。今回はTextFieldを題材とします。

CSSの疑似クラスでエラーを定義

JavaFXで、CSSファイルを使って見栄えを定義している場合、CSSファイルにTextFieldの疑似クラスerrorを次のように定義します。

.text-field:error {
    -fx-text-box-border: red;
    -fx-focus-color: red;
}
  • "-fx-text-box-border"は、TextFieldにフォーカスが当たっていないときの枠の色を指定
  • "-fx-focus-color"は、TextFieldにフォーカスが当たっているときの枠の色を指定

Javaのコードで、テキストフィールドの個々のインスタンスに疑似クラスの状態を指定

    @FXML
    private TextField myTextField;
    private PseudoClass errorClass = PseudoClass.getPseudoClass("error");
    :
    void handleSubmit(ActionEvent event) {
        if (! validateInput()) {
            myTextField.pseudoClassStateChanged(errorClass, true);
        } else {
            myTextField.pseudoClassStateChanged(errorClass, false);
        }
        :
    }

疑似クラスの名前でPseudoClassインスタンスを取得し、対象となるTextFieldインスタンスのpseudoClassStateChangedメソッドで引数に疑似クラスのインスタンスと有効・無効のフラグを指定すると見栄えを変更することができます。

この実装では、submitボタンが押された段階でTextFieldの内容をチェックし疑似クラスerrorの適不適を切り替えています。
より本格的に、TextFieldに文字を入力する度にチェックし色を変えることもできますが、本日は割愛します。

Javaプログラムへの初期設定の与え方を悩む

Javaプログラムに初期設定を外部から渡す方法として、プログラム起動時のコマンドラインオプション、設定ファイル、システムプロパティ、リソースバンドル、プリファレンスAPIjava.util.pref)辺りが候補に挙がります。
今回、PCからシリアル通信でセンサー機器と通信し、センサー機器の状態をGUIで表示するスタンドアロンプログラムを作成しています。プログラムの構造は、GUIに纏わる部分、シリアル通信に纏わる部分とを疎結合しています。ここで、疎結合ゆえにプログラムの初期設定をどのように渡すかを悩むこととなりました。

候補となる方法

コマンドライン引数

GUIJavaFXで作成しています。JavaFXコマンドラインを解析し、JavaFXのParametersクラスにキー・バリュー形式で保持する仕組みを持っています。しかし、このParametersクラスをシリアル通信部分に渡してしまうと、シリアル通信部分がJavaFXのライブラリへ依存してしまい、結合を疎にする観点では後退してしまいます。

設定ファイル

プログラムの起動時に所定のパスにあるProperties形式の設定ファイルを読み込み、これを参照する方法であれば、Java SEの標準APIjava.util.Properties)への依存で済むので、疎結合をそれなりには保てます。しかし、設定ファイルから生成したPropertiesインスタンスGUIの各所とシリアル通信の各所に持ち回す必要が生じます。
JavaFXでは、通常上位のApplication派生クラスからコントローラクラスへは直接参照を持たないので、プロパティを渡すためにコントローラーの参照を取得するコードをApplication派生クラスに追加し、またコントローラーにはプロパティを渡すメソッドを追加し、コントローラーからさらに各クラスに渡すといった持ちまわしが生じます。
シリアル通信においても、そのプロパティを各クラスに渡す持ちまわしが生じます。
この持ちまわしを避けるとしたら、プロパティをシングルトンで実装するといったプログラム全体のグローバル参照が発生します。
今回は小さなプログラムなので、1つのプロパティでもそう煩雑にはなりませんが、大きなプログラムになると、1つのプロパティに数百個以上のキー・バリュー定義が格納されることになり、あまり望ましい姿ではありません。

システムプロパティ

Java起動時のコマンドラインJVMオプション)でシステムプロパティを定義することができます。
システムプロパティは、Java SE標準のAPIjava.lang.SystemクラスのgetPropertyメソッド)で取得できるので、疎結合の点で問題はなく、持ちまわしも不要ですが、多数の項目があるとJava起動時のコマンドラインが膨大となってしまいます。

リソースバンドル

Javaには、主に国際化対応の仕組みとしてロケールに応じて切り替える文字列等のリソースをプロパティファイルまたはクラスに切り出し、実行時にロケールに応じたファイルを読み込むリソースバンドルの仕組みがあります。このファイルに初期設定を書く方法がありますが、リソースファイルは通常JARファイルの中に収められるので、インストール後に設定を変更する場合、JARから取り出し修正したファイルを再度JARに戻すという手順が必要になり、これも好ましくありません。また、リソースバンドルは主にGUIの表示で使うので、シリアル通信などGUIから切り離した内容を記述するのは不適切と思われます。

プリファレンスAPI

Javaには、プリファレンスAPIjava.util.prefs)があり、ユーザー毎に異なる設定を保存・読み出して使用することと、ユーザー共通の設定を保存・読み出して使用することができます。
これはJava SE標準APIなのでシリアル通信からJavaFXへの依存はなくて済みます。
プリファレンスAPIを通して設定する内容は、Windows OSの場合はレジストリに格納され、UNIX系OSの場合はユーザー毎の設定はユーザーのホームディレクトリ下のファイルに、ユーザー共通の設定はシステムで一意の場所にファイルで置かれます。
レジストリやファイルは、プログラムをインストールして最初にプリファレンスを保存するまで生成されないので、インストール直後から初期設定を置くにはプリファレンスAPIを使って保存操作を最初にする必要があります。また、実行するユーザーの権限ではユーザー共通の設定を書き込むことができないことがあります(通常このケースが多いかと)。プリファレンス設定ツールが必要になるかもしれません。

結局どれにしよう?

用途によりトレードオフすることとなりそうです。

Java読書会BOFは6月から「Java 11 and 12 - New Features」を読み始めます

Java読書会BOFは、6月から新しい書籍「Java 11 and 12 - New Features」(洋書)を読み始めます。

Java SE 9が2017年9月にリリース後、この2019年5月末時点までに、Java SE 10(2018年3月)、Java SE 11(2018年9月)、Java SE 12(2019年3月)とリリースされ、Java SE 11については、長期間サポート版(LTS: Long Term Support)となっています。

ところが、この2年内のJavaのアップデートについてしっかり書かれた日本語書籍はほとんどなく、洋書で勉強するとなった次第です。

Java読書会BOFでは、過去洋書を扱ったときは事前に分担を決め日本語訳をして、それを当日配布して日本語訳を朗読するという方法を取ってきました。
これは、精読できるのですが事前の準備負担が大きいので対応し難くなっています。今回は、事前訳はせずにその場で粗い日本語で朗読を進めようかと思っています。

さて、今回の書籍の章題を列挙すると次になります。

  • 型推論(Type Inference)
  • アプリケーション・クラス・データ共有(Application Class-Data Sharing: AppCDC)
  • ガーベージコレクター最適化(Garbage Collector Optimizations)
  • JDK 10の細々な改善(Miscellaneous Improvements in JDK 10)
  • ラムダ式の引数のローカル変数文法(Local Variable Syntax for Lambda Parameters)
  • イプシロンGC(Epsilon GC
  • HTTPクライアントAPI(The HTTP Client API
  • ZGC
  • フライトレコーダーとミッションコントロール(Flight Recorder and Mission Control)
  • JDK 11の細々な改善(Miscellaneous Improvements in JDK 11)
  • switch式(Switch Expressions)
  • JDK 12の細々な改善(Miscellaneous Improvements in JDK 12)
  • プロジェクトAmberにおける列挙型の改善(Enhanced Enums in Project Amber)
  • データクラスとその使用(Data Classes and Their Usage)
  • 生文字列リテラル(Raw String Literals)
  • ラムダの食べ残し(Lambda Leftovers)
  • パターンマッチング(Pattern Matching)

JDKの新しい機能としてちらっと見聞きしたことのある、でも良くわからない機能が並んでいます。
今回の読書会を通じて、これらの新機能について一定の理解をしておくことができるのはなかなかに期待度が高いです。

今月のJava読書会は、6月15日(土)に川崎駅近くで開催します。

Java読書会6月からの本の投票

Java読書会BOFの書籍選定

Java読書会BOFでは、月1回の読書会を開催しており、5月で「Effective Java第3版」をほぼ読了し、6月からは新しい本の読書会を開始します。現在読書会のWebサイトでWeb投票実施中(5月26日〆切)です。

Javaの技術動向は変化が大きくなってきている

Javaは、2017年9月に Java SE 9がリリースされ、そこではModule Systemが導入されています。また、Java SE 9以降はこれまでの2~3年に1回のメジャーバージョンアップをリリースする形態から、半年毎にメジャーバージョンアップをリリースする形態に変更され、新機能が早い段階でリリースされ利用可能になっています。2018年3月にJava SE 10、2018年9月にJava SE 11、2019年3月にJava SE 12と半年毎にバージョンアップされています。

一方で安定的に利用したいユーザー向けには3年に1回、長期サポート版(LTS)を提供するようになりました。最初のLTS版は、2018年9月のJava SE 11です。これは少なくとも次のLTS版まで(則ち最低3年間)はセキュリティパッチを含む修正版が提供されます。LTS版以外は、次のバージョンがリリースされるまでの間(則ち半年間)だけは修正版が提供されます。

余談、有償化騒ぎについて

このLTS版については、Oracleが提供するOracle JDKについては商用利用(個人利用および商用利用でも開発・試験・デモは除く)が有償となっているので、すわJavaが有償化だと一部で騒ぎになっていました。しかし、Javaはずいぶん前からオープンソース開発(OpenJDK)となっており、特に最初のLTS版であるJava SE 11ではOracle JDKに搭載されていた商用機能(Flight Recorder等)がOpenJDKに搭載され、Oracle以外にもいくつかの組織からLTS版が無償提供されています。

読書会で読みたい本が・・・

このようにJavaの進化は短い時間で次々行われてきていますが、日本語での書籍でJava SE 9以降の新しい機能を詳しく解説しているものはほとんど出版されていません。

となると、Javaの新しい技術を読みたいとすると洋書を選択することになります。過去読書会でも幾度か洋書を読んできたので、それほど敷居が高いということはありませんが、洋書ウォッチャーが少ないのでこれぞという本を選ぶのが大変です。

Java Platform Module System(JPMS)の本

Java SE 9で導入され、なかなか骨のある(移行が大変、理解が大変)割にプログラミングの改善にはあまり寄与していない(APIの実装隠蔽や、ソフトウェアのライフサイクル上はとても重要な)技術がJPMSではないでしょうか。

JPMSを扱った書籍を調べてみると、次が見つかりました。

最初の本はまだ未完です。2番目以降はJava SE 9リリース時期(2年前)に出版されているので、ちょっと古いなというのと、ページ数がやや多いが難です。
(洋書の読書会の場合、事前に粗く訳してくるので、ページ数が多いとそれだけ大変)

ということで、JPMS本は今回は推薦せずにおこうかと思います。