JavaFXでWindowsアプリを作るまでに学んだこと

# GitHubリポジトリ

[customizable-clock](https://github.com/b150005/customizable-clock)

# JavaFXとは

Javaの**GUI開発フレームワーク**の1つで、`AWT`・`Swing`に比べ比較的新しく、`.fxml`ファイルで**GUIコンポーネントの配置を記述**できるという特徴がある。
環境構築や大まかな基本知識は[【超初心者向け】JavaFX超入門](https://qiita.com/tarosa0001/items/05ac653a091b7d1290f9)を参照。

# GUIアプリケーションの実行スレッド

[JavaFX 1.2 非同期処理の概要](https://shuji-w6e.hatenadiary.org/entry/20090801/1249125921)
[JavaFXにおけるスレッドの扱い](http://krr.blog.shinobi.jp/javafx/javafx%E3%81%AB%E3%81%8A%E3%81%91%E3%82%8B%E3%82%B9%E3%83%AC%E3%83%83%E3%83%89%E3%81%AE%E6%89%B1%E3%81%84)

GUIフレームワークは基本的に、**GUIコンポーネントの描画スレッドはシングルスレッド**であり、JavaFXも単一の`JavaFX Application Thread(JavaFXアプリケーションスレッド)`で処理される。
そのため、GUIコンポーネントの描画処理に時間がかかる場合は、描画に直接関係しない演算処理は`非同期処理`で実装するのが望ましい。

JavaFXにおける主なスレッドは以下の通り。

|スレッド|スレッド名|概要|
|---|---|---|
|メインスレッド|main|`main`関数を実行するスレッド|
|JavaFXランチャースレッド|JavaFX-Launcher|`init`関数を実行するスレッド|
|JavaFXアプリケーションスレッド|JavaFX Application Thread|イベント処理 / GUIコンポーネントの描画を行うスレッド|
|バックグラウンドスレッド|Task / Service / ScheduledService|`Thread::sleep`関数 / 重い処理を行うスレッド|

## JavaFXのバックグラウンドスレッドAPI

[JavaFXとマルチスレッド](https://www.torutk.com/projects/swe/wiki/JavaFX%E3%81%A8%E3%83%9E%E3%83%AB%E3%83%81%E3%82%B9%E3%83%AC%E3%83%83%E3%83%89)

|クラス|パッケージ||
|---|---|---|
|[Task<V>](https://openjfx.io/javadoc/17/javafx.graphics/javafx/concurrent/Task.html)|javafx.concurrent|再利用できないWorkerスレッド|
|[Service<V>](https://openjfx.io/javadoc/17/javafx.graphics/javafx/concurrent/Service.html)|javafx.concurrent|再利用可能なWorkerスレッド|
|[ScheduledService<V>](https://openjfx.io/javadoc/17/javafx.graphics/javafx/concurrent/ScheduledService.html)|javafx.concurrent|処理完了後に自動で再起動するService|

# アプリケーションを構成するApplication, Stage, Scene, Pane

`JavaFX`では基本的に、`.fxml`ファイルで定義したコンポーネントを[FXMLLoader](https://openjfx.io/javadoc/17/javafx.fxml/javafx/fxml/FXMLLoader.html)を通じて[Scene](https://openjfx.io/javadoc/17/javafx.graphics/javafx/scene/Scene.html)に紐づけて、Javaプログラムから制御する。

|クラス|パッケージ|概要|
|---|---|---|
|Application|javafx.application|アプリケーション|
|Stage|javafx.stage|トップレベルコンテナ(ウィンドウ)|
|Scene|javafx.scene|単一の`Pane`を持つコンテナ(`.fxml`ファイルのコンポーネント化)|
|Pane|javafx.scene.layout|各GUIコンポーネントのレイアウトを構成するコンテナ|

![JavaFX Scene Graph](https://fxdocs.github.io/docs/html5/images/scenegraph/scene_graph.jpg)

各ファイルのディレクトリ構成は以下の通りとする。

- src/
    - main/java/application/
        - SampleApp.java(アプリケーションのMainクラス)
        - SampleController.java(.fxmlで定義したコンポーネントを制御するクラス)
        - window.fxml(コンポーネントを定義するファイル)

```java
public class SampleApp extends Application {
  public static void main(String[] args) {
    // アプリケーションの起動
    // -> start(Stage)関数が呼び出される
    launch();
  }

  // アプリケーション起動時に呼び出される処理
  @Overrride
  public void start(Stage stage) {
    try {
      // .fxmlファイルの読み込み
      URL url = SampleApp.class.getResource("window.fxml");

      // FXMLLoaderを通じたSceneへの紐づけ
      FXMLLoader loader = new FXMLLoader(url);
      Scene scene = new Scene(loader.load());

      // Stage(ウィンドウ)へのSceneの紐付け
      stage.setScene(scene);
    }
    catch (IOException e) {
      e.printStackTrace();
    }

    // ウィンドウバーに表示するタイトル
    stage.setTitle("Sample Window");

    // ウィンドウの表示
    stage.show();
  }
}
```

もちろん、`.fxml`ファイルを用いることなくGUIコンポーネントを表示することもできる。
以下のサンプルコードは、背景が透明なウィンドウを表示するコードである。

```java
// Stage(ウィンドウ)の種類を指定
Stage stage = new Stage(StageStyle.TRANSPARENT);

// GUIコンポーネント群のRootNodeであるPaneとスタイルの指定
StackPane stackPane = new StackPane();
// 角を丸くし、背景色を透明にする
// -> transparentはfxmlで標準定義された「透明」な色
stackPane.setStyle(
  "-fx-background-radius: 10; -fx-background-color: transparent"
);

// Scene(コンポーネントをJavaプログラムで制御できるようにするクラス)にPaneをセット
// -> WindowサイズはSceneのサイズに依存するため、ここでウィンドウの幅と高さを指定
Scene scene = new Scene(stackPane, 400, 200);

// Sceneの背景色を設定しないようにする
scene.setFill(null);

// StageにSceneをセット
stage.setScene(scene);

// Stageを常に最前面に表示するようにする
stage.setAlwaysOnTop(true);

// Stageのタイトル
stage.setTitle("Transparent Window");

// Stageの表示
stage.show();

// Paneに入れるコンポーネント
Button button = new Button;

// Paneへのコンポーネントの追加
stackPane.getChildren().add(button);
```

## Paneの種類

![JavaFX-Pane.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/673880/856563e3-1bdf-931e-7a11-5b1f5d8b5e9a.png)

# .fxmlファイルのidとfx:id

|属性|概要|
|---|---|
|`id`|**CSS**で用いるID|
|`fx:id`|**JavaFX**で用いるID|

`UI部品`をコードで操作する場合は、UI部品を特定する際に`.fxml`ファイルの`fx:id`属性を用いる。
**Scene Builder**で`fx:id`属性を定義する場合は、

> ドキュメント > 階層の表示 > fx:id

から**階層**グループで入力するか、

> インスペクタ > Code > fx:id

に入力する。

```xml
<!--> window.fxml <-->
<ChoiceBox fx:id="sampleChoiceBox">...</ChoiceBox>
```

# イベント処理の記述

[JavaFX 8 Event Handling Examples](https://code.makery.ch/blog/javafx-8-event-handling-examples/)

`UI部品`をイベントリスナとして設定しイベント処理を実行する場合は、`.fxml`ファイルで`onAction`属性にコールバックメソッドを記述する。
また、コントローラでコールバック処理を記述する際は`@FXML`アノテーションを付与する。

なお、`ComboBox`や`ChoiceBox`は**Scene Builder**で`onAction`属性を設定できないため、`.fxml`ファイルに直接記述する。

```xml
<!--> window.fxml <-->
<CheckBox fx:id="sampleCheckButton" onAction="#onCheckBoxClick">
  ...
</CheckBox>
```

```java
// SampleController.java
public class SampleController implements Initializable {
  @FXML private CheckBox sampleCheckButton;
  @FXML protected void onCheckBoxClick(ActionEvent event) {
    switch (((CheckBox)event.getSource()).getId()) {
    case "sampleCheckButton":
      // イベント処理をここに記述
      break;
    default:
    }
  }
}
```

また、`DatePicker`・`Slider`・`Spinner`など`onAction`属性がないものについては、
**Application**の`start(Stage:)`や、`Initializable`インタフェースを実装した**Controller**の`initialize(URL:ResourceBundle:)`でイベントリスナとしてセットする必要がある。

ただし、`Slider`の`onAction`はドラッグと連動して実行されるため、
ドラッグ後にイベント処理を実行する場合は`onMouseReleased`属性を利用する。

```java
// SampleController.java
public class SampleController implements Initializable {
  @FXML private DatePicker datePicker;

  @Override
  public void initialize(URL url, ResourceBundle resourceBundle) {
    this.datePicker.valueProperty().addListener((observable, oldValue, newValue) -> {
      // イベント処理
    });
  }
}

/*
  Applicationでイベントリスナ定義をする場合は下記参照
*/
// SampleApp.java
public class SampleApp extends Application {
  @Override
  public void start(Stage stage) throws Exception {
    DatePicker datePicker = new DatePicker();
    datePicker.valueProperty().addListener((observable, oldValue, newValue) -> {
      // イベント処理
    });
  }
}
```

## Spinner

[SpinnerValueFactory<T>](https://openjfx.io/javadoc/17/javafx.controls/javafx/scene/control/SpinnerValueFactory.html)

`Spinner`の入力値・型を指定する場合は、`valueFactory`タグの内部に`SpinnerValueFactory`タグを記述する。

```xml
<!--> settings-view.fxml <-->
<?import javafx.util.* ?>
<?import javafx.scene.*?>
<?import javafx.scene.control.* ?>
<?import javafx.scene.layout.* ?>

<Spinner fx:id="spinner" editable="true">
  <valueFactory>
    <SpinnerValueFactory.DoubleSpinnerValueFactory min="0" max="1" initialValue="0.5" amountToStepBy="0.1"/>
  </valueFactory>
</Spinner>
```

# Windowsアプリケーションのプロファイルデータの保存(永続化)方法

[Javaで永続化](https://teachingprogramming.net/archives/701)

|カテゴリ|拡張子|概要|
|---|---|---|
|テキスト|`.ini`|Windows初期から利用される**Key-Value**形式|
|^|`.xml`|多くの言語の標準ライブラリでサポートされる**Key-Value**形式|
|^|`.json`|多くの言語の標準ライブラリでサポートされる**Key-Value**形式|
|^|`.yaml`|コメントが記述できる**Key-Value**形式|
|バイナリ|`.bin`|ユーザによる変更を防止する**2進数**形式|
|データベース|レジストリ|OSのシステム情報を格納する**Key-Value**形式|
|^|RDB|2次元の表でデータを管理するDB|
|^|OODB|オブジェクト指向でデータを管理するDB|

## データベースの利点

データの保存先として**データベース**を採用するメリットは、主に以下の3つ

> 1. アルゴリズムを駆使した高速処理が可能
> 2. 複数のプログラムからの同時アクセスが可能
> 3. 処理の中断・エラー時のロールバックが可能

## 組み込みDBMSの選定

[いまさら聞けない 組み込みデータベース入門](https://monoist.itmedia.co.jp/mn/articles/0710/17/news140.html)

|方式|概要|DB|
|---|---|---|
|インプロセス(ライブラリ)型|データベースをアプリケーションに組み込む方式|`SQLite`(RDB), `H2 Database`(RDB), `Redis`(NoSQL)|
|クライアント・サーバ型|DBプロセスがアプリケーションプロセスから独立する方式|`SQL Server`(RDB), `PostgreSQL`(RDB), `MongoDB`(NoSQL)|

### RDBMS vs. NoSQL

|分類|メリット|デメリット|
|---|---|---|
|RDBMS|①データ処理の一貫性が担保<br>②複雑なデータ処理が可能|スケールアウトが困難|
|NoSQL|①データ処理が高速<br>②拡張性に優れる|データの一貫性が担保されない|

## Redis(Lettuce)への接続

[Redis on Windows](https://github.com/MicrosoftArchive/redis/releases)
[Lettuce](https://github.com/lettuce-io/lettuce-core)

プログラムをRedisクライアントとして動作させるためRedisをPCにインストールし、
Redisを利用するためのライブラリの依存関係をPOMファイルに記述する。

```xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
  ...
  <dependencies>
    <dependency>
      <groupId>io.lettuce</groupId>
      <artifactId>lettuce-core</artifactId>
      <version>6.1.5.RELEASE</version>
    </dependency>
  </dependencies>
</project>
```

```java
// Redisクライアントの作成
// 事前にRedisのインストールが必要(https://github.com/MicrosoftArchive/redis/releases)
RedisClient redisClient = RedisClient.create("redis://localhost:6379");
try (
  // Redis Standaloneサーバへの接続
  // → <String, String>型でデータを管理
  StatefulRedisConnection<String, String> connection = redisClient.connect();
) {
  // 同期実行のコマンドAPIの取得
  RedisCommands<String, String> syncCommands = connection.sync();
  
  // 保存されたデータが存在しない場合は初期値をセット
  if (syncCommands.get("key")  == null) {
    syncCommands.set("key", "value");
  }
} catch (Exception e) {
  e.printStackTrace();
} finally {
  // Redisクライアントの終了
  redisClient.shutdown();
}
```

### データベースを利用する場合のJDBCドライバ

|DBMS|ドライバクラスのFQCN|JDBC URL|
|---|---|---|
|SQL Server|com.microsoft.sqlserver.jdbc.SQLServerDriver|jdbc:sqlserver:<DBファイル名>|
|MySQL|com.mysql.cj.jdbc.Driver|jdbc:mysql:<DBファイル名>|
|PostgreSQL|org.postgresql.Driver|jdbc:postgresql:<DBファイル名>|
|SQLite|org.sqlite.JDBC|jdbc:sqlite:<DBファイル名>|
|H2 Database|org.h2.Driver|jdbc:h2:<DBファイル名>|