torutkのブログ

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

JavaFXのグラフの凡例部分を操作可能にする

はじめに

先週書いた次のブログの続編です。 torutk.hatenablog.jp

一つのグラフ表示領域に、複数のグラフ(Series)を重畳表示した際に、簡単な操作でそれぞれのグラフを非表示にしたり表示したりとできると使い勝手が向上します。

グラフに凡例を表示した場合、それぞれの凡例をクリックするとその凡例に対応するグラフを表示する、非表示すると切り替えられるようにします。

課題

凡例全体のNodeは、Legendクラス(com.sun.javafx.chart.Legend)で実装されています。しかし、このcom.sun.javafx.chartパッケージは、アプリケーション側のモジュールには公開されていません。

ぐぐって検索した情報は、子ノードを辿ってLegendクラスかどうか判別してLegendを見つけ、Legendにキャストしてそのitemを取得して利用するもので、Java SE 9以降では使えません(--add-exportsオプション指定でコンパイルすれば可能)。

各凡例を表示するNodeを取得する方法

それぞれの凡例は、CSSセレクタ .chart-legend-item が指定されています。JavaFXAPIには、セレクタを指定してNodeを取得する機能(lookupおよびlookupAll)が提供されているので、これを使って凡例のNodeを取得します。

@FXML
LineChart<Float, Float> spectrumChart;
:
Set<Node> legendItems = spectrumChart.lookupAll(".chart-legend-item");

それぞれの凡例にマウスハンドラーを設定するには例えば次のようにします。

legendItems.forEach(item -> item.setOnMouseClicked(event -> System.out.println(event + " clicked")));

各凡例をクリックしたら対応するグラフを表示/非表示する

クリックされた凡例のNodeにマウスハンドラーを設定することはできましたが、次はそこから対応するグラフを取得して、その表示を切り替える方法を考えなくてはなりません。

それぞれの凡例はLabelクラスのインスタンスで、ラベルに表示されるテキストはグラフ(Series)のnameプロパティとなっています。

そこで、取得したそれぞれの凡例をLabel型にキャストしてラベルに表示されるテキストを取得し(getTextメソッド)、次にそのテキストを名前として持つSeriesをLineChartから探して取得します。

Labelにキャストしてテキストを取り出す

legendItems.forEach(item -> item.setOnMouseClicked(event -> {
    if (item instanceof Label) {
        String name = ((Label) item).getText();
    }
}));

Label型へのキャストが可能な場合に、Labelへキャストし、getTextメソッドでラベルに表示されるテキストを取得します。

チャート(LineChart)に登録されているSeriesからテキストと同じ名前のものを取り出す

legendItems.forEach(item -> item.setOnMouseClicked(event -> {
    if (item instanceof Label) {
        String name = ((Label) item).getText();
        Optional<XYChart.Series<Float, Float>> any = spectrumChart.getData().stream()
                .filter(series -> series.getName().equals(name))
                .findAny();
    }
}));

chartのdataプロパティには表示するグラフ(Series)がリストで登録されています。これを順次アクセスしSeriesのnameプロパティが凡例ラベルのテキストと一致するものを取り出します。

クリックした凡例に一致するグラフ(Series)の表示/非表示を切り替える

legendItems.forEach(item -> item.setOnMouseClicked(event -> {
    if (item instanceof Label) {
        String name = ((Label) item).getText();
        Optional<XYChart.Series<Float, Float>> any = spectrumChart.getData().stream()
                .filter(series -> series.getName().equals(name))
                .findAny();
        any.ifPresent(series -> {
            boolean currentVisible = series.getNode().isVisible();
            series.getNode().setVisible(!currentVisible);
        });
    }
}));

取り出したグラフ(Series)のgetNodeメソッドで画面に表示されるグラフのNodeを取得し、その表示状態をisVisibleで取得します。そして、その真偽を反転させてsetVisibleに指定し、表示/非表示を反転させます。

グラフが非表示のときは凡例の表示を薄くしたい

実現方法を悩みましたが、CSSの疑似クラスを使って非表示の際に薄く(透過度を変えて)表示させることにしました。

まず、CSSで、.labelに疑似クラス haze を記述します。

.label:haze {
    -fx-opacity: 0.2;
}

次に、凡例ラベルのマウスハンドラー内でグラフ(Series)の表示/非表示切り替えに合わせて疑似クラスの有効・無効を凡例ラベルに設定します。

private static final PseudoClass HAZE_PSEUDO_CLASS = PseudoClass.getPseudoClass("haze");
    :
legendItems.forEach(item -> item.setOnMouseClicked(event -> {
    if (item instanceof Label) {
        String name = ((Label) item).getText();
        Optional<XYChart.Series<Float, Float>> any = spectrumChart.getData().stream()
                .filter(series -> series.getName().equals(name))
                .findAny();
        any.ifPresent(series -> {
            boolean toBeInvisible = series.getNode().isVisible();
            series.getNode().setVisible(!toBeInvisible);
            item.pseudoClassStateChanged(HAZE_PSEUDO_CLASS, toBeInvisible);
        });
    }
}));

JavaFXで折れ線グラフを出そうとしてみた

はじめに(お題)

電気信号を時間領域から周波数領域に変換し、周波数に対するエネルギー量を解析できる測定器にスペクトラム・アナライザがあります。今回、このスペクトラム・アナライザで解析した結果をファイルに保存し、別な場所にあるPC上でその結果を表示、分析したいとします。

計測器にCSVファイルで結果を保存する機能があればこれをExcelで読み込ませてグラフ化することは容易ですが、今回の計測器はバイナリデータのファイルに保存するものでした。そこで、JavaFXで保存したバイナリデータのファイルを読み込み折れ線グラフ(LineChart)で表示するプログラムを作成してみます。

LineChart

JavaFXのプロジェクトを作成し、折れ線グラフ(LineChart)を表示する画面をScene Builderで作成しました。 ファイル構成は次です。

ファイル名 内容
SimpleSpectrumApp.java Application派生クラス、プログラムのエントリ(main)
SimpleSpectrumView.fxml 画面(ビュー)のレイアウトを定義するFXMLファイル
SimpleSpectrumView.css 画面(ビュー)の見栄えを定義するCSSファイル
SimpleSpectrumViewController.java 画面(ビュー)に対応するコントローラークラス
SimpleSpectrumViewModel.java GUI内のモデルクラス

デフォルトのLineChart

Scene BuilderでLineChartを画面にポトペタすると次の表示となります。 f:id:torutk:20190706211210p:plain

縦軸は数値メモリとなっていますが、横軸は表示されていません。Scene Builderの左下ペインを見ると、LineChartの子ノードとしてNumberAxisとCategoryAxisが1つずつ置かれています。縦軸がNumberAxisで、横軸がCategoryAxisです。NumberAxisは名前の通り数値の軸を表し、CategoryAxisは何らかの分類(県名、年別、チーム別、など)の軸を表します。

Scene Builder上では、LineChartの子ノードの軸を変更する方法が見つからなかったので、FXMLファイルを直接エディタで開き修正しました。

      <LineChart BorderPane.alignment="CENTER">
        <xAxis>
-         <CategoryAxis side="BOTTOM" />
+         <NumberAxis side="BOTTOM" />
        </xAxis>
        <yAxis>
          <NumberAxis side="LEFT" />
        </yAxis>
      </LineChart>

横軸がNumberAxisに変更されました。 f:id:torutk:20190706212808p:plain

最初のコード

SimpleSpectrumApp.java
package sample;

import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.stage.Stage;

public class SimpleSpectrumApp extends Application {
    @Override
    public void start(Stage primaryStage) throws Exception {
        Parent root = FXMLLoader.load(getClass().getResource("SimpleSpectrumView.fxml"));
        primaryStage.setTitle("Simple Spectrum");
        primaryStage.setScene(new Scene(root));
        primaryStage.show();
    }

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

JavaFXの典型的なApplication派生クラスの実装です。mainメソッドでプロセスのエントリを用意し、startメソッドでFXMLファイルを読み込みシーングラフを作成、Stageを表示させています。

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

<?import javafx.scene.chart.LineChart?>
<?import javafx.scene.chart.NumberAxis?>
<?import javafx.scene.layout.BorderPane?>

<BorderPane maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" prefHeight="400.0" prefWidth="600.0" xmlns="http://javafx.com/javafx/11.0.1" xmlns:fx="http://javafx.com/fxml/1" fx:controller="sample.SimpleSpectrumViewController">
   <center>
      <LineChart fx:id="chart" BorderPane.alignment="CENTER">
        <xAxis>
          <NumberAxis side="BOTTOM" fx:id="xAxis" />
        </xAxis>
        <yAxis>
          <NumberAxis fx:id="yAxis" side="LEFT" />
        </yAxis>
      </LineChart>
   </center>
</BorderPane>

Scene Builderでポトペタ作成したFXMLファイルで、ビュー(画面)を定義しています。 コントローラークラスを指定しているので、このFXMLがロードされると自動でコントローラークラスのインスタンスが生成されます。

SimpleSpectrumViewController.java
package sample;

import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.chart.LineChart;
import javafx.scene.chart.NumberAxis;

import java.net.URL;
import java.util.ResourceBundle;

public class SimpleSpectrumViewController implements Initializable {
    @FXML
    LineChart chart;
    @FXML
    NumberAxis xAxis;
    @FXML
    NumberAxis yAxis;

    SimpleSpectrumViewModel model = SimpleSpectrumViewModel.ofRandom();

    @Override
    public void initialize(URL location, ResourceBundle resources) {
        chart.getData().setAll(model.getSeriesList());
    }
}

FXMLに対応するコントローラークラスです。 コントローラーから操作するビュー部品(コントロール)は、@FXMLアノテーションを指定し、FXML側で指定したfx:idの識別子と同じ名前の変数名とすることで、FXMLロード時にインスタンスがインジェクションされます。

データについては、今回はデータファイルの読み込み部分が未実装なので、ランダムな値を表示するモデルを使います。

SimpleSpectrumViewModel.java
package sample;

import javafx.scene.chart.XYChart;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.ThreadLocalRandom;

public class SimpleSpectrumViewModel {

    private List<XYChart.Series<Double, Double>> seriesList = new ArrayList<>();

    public List<XYChart.Series<Double, Double>> getSeriesList() {
        return Collections.unmodifiableList(seriesList);
    }

    public static SimpleSpectrumViewModel ofRandom() {
        var model = new SimpleSpectrumViewModel();
        model.seriesList.add(createRandomSeries("Random"));
        return model;
    }

    private static XYChart.Series<Double, Double> createRandomSeries(String name) {
        XYChart.Series<Double, Double> series = new XYChart.Series<>();
        series.setName(name);
        for (double x = 950d; x <= 1450d; x++) {
            series.getData().add(new XYChart.Data<Double, Double>(
                    x, ThreadLocalRandom.current().nextGaussian() * 5.0 - 60.0));
        }
        return series;
    }
    
}

JavaFXではモデルクラスについては特にフレームワークが用意されていませんが、コントローラーを軽くするために、ちょっとしたロジックであってもモデルクラスを設けるようにしています。

表示確認のために、データファイルを読み込む前にランダムなスペクトルを表示するモデルを作りました。

JavaFXの折れ線グラフ(LineChart)に表示する1本の連続する線は、データ系列(XYChart.Series)で定義し、複数のデータ系列を表示することができます。 データ系列は、複数の点データ(XYChart.Data)のリストとして作られています。 今回は、1つのデータ系列を用意しています。 用意するデータは、周波数軸(X軸)の950~1450までの間を1刻みで作成し、縦軸の電力は正規分布乱数で平均-60.0、分散5.0としています。

表示

プログラムを実行すると、次の画面が表示されました。

f:id:torutk:20190707043824p:plain

いろいろ改善したい点があります。

  • 縦軸、横軸に名称・単位を表示したい
  • 〇の表示(シンボル)は不要としたい
  • 折れ線を細くしたい
  • 縦軸、横軸の表示範囲を指定したい

LineChartの設定

縦軸、横軸に名称を表示

これは、Scene Builderで、軸のLabelプロパティに文字列を設定すれば表示できます。 f:id:torutk:20190707050535p:plain

縦軸、横軸の表示範囲を指定

縦軸は、-30.0~-80.0の範囲を表示し、横軸は950~1450の範囲を表示します。 表示すべきデータを読み込んだ際にデータに応じた値を設定するので、コードで制御します。

前回からのコード変更部分を次に記載します。

    @Override
    public void initialize(URL location, ResourceBundle resources) {
+       xAxis.setLowerBound(950d);
+       xAxis.setUpperBound(1450d);
+       yAxis.setLowerBound(-80d);
+       yAxis.setUpperBound(-30d);
        chart.getData().setAll(model.getSeriesList());
    }

NumberAxisには、表示する軸の値の上限値・下限値を指定するlowerBound、upperBoundがあります。 ここに表示する軸の数値範囲を指定します。この上下限値設定は、LineChartにデータ(XYChart.Series)を登録する前に実行する必要があります。あとから実行したら表示には反映されません。

ただし、これだけでは意図したとおりの動作をしません。NumberAxisのAutoRangingプロパティが有効だと、登録したデータの値に応じた軸の数値範囲に自動調整が働いてしまいます。今回はScene Builder上でAutoRangingプロパティを無効にします(次の図参照)。

f:id:torutk:20190707101033p:plain

プログラムを実行すると、次の様に意図した数値範囲でグラフが表示されました。

f:id:torutk:20190707101215p:plain

  • グラフの罫線間隔については、NumberAxisのTickUnitプロパティで制御可能です。
〇の表示(シンボル)をなくす

デフォルトでは、折れ線グラフの点データに丸いシンボルが表示されます。 データ数が少ないときは、データとデータを補完する線とを明確に表現するこの表示が適していますが、データ数が多いときは上図のようにシンボルだらけで肝心の折れ線が見えなくなってしまいます。

シンボルをなくすのであれば、LineChartのCreateSymbolプロパティを無効にします(次の図参照)。

f:id:torutk:20190707103825p:plain

プログラムを実行すると、次のとおり丸いシンボルが表示されなくなりました。

  • シンボルを消すのではなく、シンボルの形状を変更したいときは、CSSセレクターに.chart-line-symbolを指定し見栄えを記述します。

f:id:torutk:20190707104015p:plain

折れ線を細く

デフォルトでは、折れ線の幅がちょっと太いので、これを細くします。形状の変更はCSSで記述します。 セレクターに.chart-series-lineを指定し見栄えを記述します。

.chart-series-line {
    -fx-stroke-width: 0.75px;
}

CSSファイルは、アプリケーション派生クラスでSceneに設定するか、FXMLで最上位要素に指定するかの方法があります。今回は、FXML(Scene Builder)上で最上位要素のBorderPaneにCSSファイルを設定します。CSSファイルはFXMLファイルと同じ場所に置き、BorderPaneのStylesheetsプロパティに指定します(次の図参照)。

f:id:torutk:20190707105138p:plain

プログラムを実行すると、次のとおり折れ線が細くなりました。

f:id:torutk:20190707105538p:plain

凡例の記号を四角ではなく線にする

CSSファイルで凡例の記号を定義している@.chart-legend-item-symbol@の定義を変更します。

.chart-legend-item-symbol {
    -fx-background-radius: 0;
    -fx-background-insets: 0;
    -fx-shape: "M0,5 L0,7 L12,7 L12,5 Z";
    -fx-scale-shape: false;
}
.default-color0.chart-legend-item-symbol {
    -fx-background-color: CHART_COLOR_1;
}
.default-color1.chart-legend-item-symbol {
    -fx-background-color: CHART_COLOR_2;
}
.default-color2.chart-legend-item-symbol {
    -fx-background-color: CHART_COLOR_3;
}
.default-color3.chart-legend-item-symbol {
    -fx-background-color: CHART_COLOR_4;
}
.default-color4.chart-legend-item-symbol {
    -fx-background-color: CHART_COLOR_5;
}
.default-color5.chart-legend-item-symbol {
    -fx-background-color: CHART_COLOR_6;
}
.default-color6.chart-legend-item-symbol {
    -fx-background-color: CHART_COLOR_7;
}
.default-color7.chart-legend-item-symbol {
    -fx-background-color: CHART_COLOR_8;
}
  • デフォルトの凡例シンボルは枠が系列色、中が白色塗りつぶしの円です。@-fx-background-radius@と@-fx-background-insets@はデフォルトの定義を無効化するのに指定する模様です(細部よくわからない)
  • グラフはデフォルトで8つまで色分けできるようにCHART_COLOR_1からCHART_COLOR_8まで色が定義されています。svg-pathで定義した枠の中をグラフの対応する色で塗りつぶすために、@.default-colorX.chart-legend-item-symbol@を定義しています。

インターネットとつながらないソフトウェア開発環境で使うツール

前提

ソフトウェア開発環境がインターネットと隔離されていることは、昨今のセキュリティ事情からよくあることと思います*1

ソフトウェアエンジニアの場合、事務作業で使うPC環境とは別にソフトウェア開発用のPC環境があり、それは事務作業で使うネットワーク環境とは異なるソフトウェア開発のネットワーク環境につながっており、インターネットとはつながらない(あるいは強度な制約のもとインターネットにつながる)環境となります。

例えば、事務作業用のPCと開発用のPCが物理的に分かれていて、それぞれ別々なネットワークに接続されていることや、事務作業用PCの利用は仮想マシンを通して行い、開発用PCと仮想マシン間のデータ交換は不可となっていることなどです。

開発したソフトウェアを組み込む製品と開発環境をつないで、ソフトウェアのインストール・更新、ログの取得、デバッグ、各種設定をする際は、製品が開発用ネットワークとはつながっていないため、開発用PCを開発用ネットワークから切り離して使うこともあります。場合によっては開発用ネットワークなしに単独で最初から最後まで開発を行うこともあります。

このように開発環境がインターネットとは接続されていない場合は、開発に使うツールの選定時点からそのことを考慮に入れておくことが不可欠です。

開発環境で使うツールの準備

インターネットと隔離されているので、開発環境で使うツールは開発環境用のネットワークの中で展開できるようにする必要があります。

ここで困るのが、Webインストーラーのみ提供され、インターネットとつながっていないとインストールができないツールです*2

開発用PCをインターネットにつないでインストールしてから開発環境用ネットワークに持ち込めばいいのでは?との声があるかもしれませんが、開発データを抱えたPCをインターネットに接続することは原則不可でしょう。

インストールはできるもののライセンス認証にインターネット接続が必須となるツールも使うことができません。

ツール本体はオフラインでインストールできても、プラグインの追加がインターネット接続必須となると困ります。

インストールができたとしても、実行時に問題があるというケースも困ります*3

主要なJava開発ツール(統合開発環境)のNetBeansEclipseIntelliJ IDEAは、いずれもインストールイメージをオフラインで展開して使用できます。

ビルドツール

maven、gradleはJavaのビルドツールとして非常に流行っておりますが、インターネットに接続していることをほぼ前提としているツールなので、インターネットと隔離した開発環境での使用には不適です。

mavenは、自身の実行にもインターネットからモジュールのダウンロードが発生します。オフラインオプションもありますが、一度オンラインでダウンロードしたキャッシュを使うものなので、ゼロから使うには、mavenセントラルリポジトリのクローンをNexusArtifactoryなどで開発ネットワーク内に構築し、各開発PCからはそのクローンを参照する必要があります*4。ただし、開発環境から切り離しての作業には制限があります*5

gradleは、mavenに比べるとオフライン使用がまだ容易ですが、オフラインで使用できるようにするための手間はやはり発生します。以前、Android Studioで開発した際は、SDKのインストール場所、ライブラリの配置などを調整する必要がありました。その時の取り組みは次のブログに記載しています。

現時点では、引き続きAntを使うのがインターネット隔離環境では適すると言えます。

mavenセントラルリポジトリのライブラリ利用におけるセキュリティリスク

mavenリポジトリを利用する場合、利用するライブラリが依存するライブラリ群を逐一把握して揃えなくてもビルド時に自動で取得してくれる利便性があります。しかし、これは逆に意図しないライブラリが入り込むことになります。意図しないものに、知財的な問題からバックドア等のセキュリティ脆弱性問題までいろいろな問題が含まれます。

開発者が使用するPC個別に直接mavenリポジトリへアクセスしライブラリをダウンロードしてくるので、マルウェア等が入り込み別サイトに誘導されダウンロードしてしまうということも考えられます。

バージョン管理ツール

開発用PCが開発ネットワークに接続して使用、あるいは切り離して使用するので、リポジトリサーバーへの常時接続がなくても管理できる分散リポジトリ方式のGitやMercurialがよいでしょう。特に製品組み込み後にその場で修正をした場合は、開発ネットワークから切り離されている間にリポジトリへのコミットを重ねておき、もどってから中央リポジトリに反映することができるのは大きな利点です。

CIツール

計画的な(デイリーの)ビルド・成果物作成はJenkinsなどのサーバーを使いますが、製品と隣り合わせでビルド・インストールを繰り返すときには持ち出しできる(開発PCに一緒に乗せて使える)CIツールがあるといいですね。これは今後調査していきたいと思っています。

不具合管理・プロジェクト管理ツール

プロジェクトの不具合状況、進捗状況は、ソフトウェア開発メンバーだけでなく、組織の管理者層や品質保証部門からも参照したい情報です。ソフトウェア開発環境の中に閉じたツールを立てると、開発メンバーしか共有できません。
隔離したネットワークの双方から参照したい場合の実現方法は今後調査していきたいと思っています。

コミュニケーションツール

開発環境とは別に、事務作業用の環境で使うか、開発環境の中に独自にツールを用意するかの選択(あるいは併用)となります。
ビルド結果の通知、プロジェクト管理ツールからの通知などをコミュニケーションツールで受けるので開発環境の中に置きたいところですが、開発以外のメンバーとのコミュニケーションには事務作業用の環境で使えるツールが必要です。インターネット接続環境ではチャットツールの活用でコミュニケーションが良好になっています。隔離したネットワーク内およびその外側とでコミュニケーションをとる方法を今後調査したいところです。

現状と今後

前提で書いた開発環境は、かなり制約がつよい(利便性が犠牲になっている)環境となっています。プログラマー視点で普段目にする技術情報では、便利な技術の紹介がほとんどで、インターネット上のサービスをガンガン使うものばかりですが、所属する会社組織において要求されるセキュリティレベルによってはそうした便利技術が使えないということが私の経験では多いです。

前提のような環境を強いられなくてもセキュリティが保てるような開発環境をどうしたら構築できるかについて、少しずつ検討していきたいと考えています。

インターネット接続できない場所が意外と

先日出張の際、建物の中のある会議室では携帯キャリアによってつながる/つながらないが分かれました。
また、出張の合間に車で移動する際、キャリアすべてがつながらない場所が少なからずありました。

*1:開発用PCがインターネットとつながってメールやWebができると、開発環境にある情報が漏洩するリスクがあり、入口対策でファイアウォールやウィルス対策をしていても、標的型攻撃や開発ツール・ライブラリ等に仕込まれたサプライチェーン攻撃等による漏洩は防げません。開発環境に攻撃が及んでも社外に漏洩しないよう隔離をして対策を取るケースです。

*2:Visual Studio 2017以降はISOイメージが提供されなくなり、オフラインインストールが困難になりました。インターネットに接続したPCでいったんWebインストーラーを動かしオフラインイメージをダウンロード、それを検疫等必要な処置をして開発環境へ持ち込み、開発PCへインストールする手順は用意されていますが、コード署名の証明書が追加で必要なるとか、ダウンロードしたPCと構成が違うときにエラーになるといったトラブルが生じることがあります。また、インターネットに接続するPCでWebインストーラーを実行できるかは組織のセキュリティポリシーにより許されないこともあります。

*3:過去にあったのが、アセンブリ(DLLファイル)にコード署名が施されており、ツールを起動するたびにコード署名の検証をするがその際インターネット接続を試み、タイムアウトが発生してから起動するというもの。ツールの起動に数分待たされてしまいました。

*4:すごい大変

*5:キャッシュしたコマンドは実行可能だが、切り離してから始めて使うコマンドはキャッシュがないのでエラーになってしまう

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に文字を入力する度にチェックし色を変えることもできますが、本日は割愛します。