IntelliJ IDEAとGradleでのJavaFXアプリケーション開発 〜環境構築からサンプルコードまで〜

#0.はじめに
いま、筆者は絶賛GUIツールを使ったソフトウェアを作る必要に迫られています。
~~ずっと放置していて~~最近着手したのですが、思いの外環境構築に手間取ったので備忘録を兼ねてここに書いておきます。

#1.JavaFXって何ぞ？
「JavaでGUIと言ったらAWT！Swing！」と思っている方はいませんか？
実は最近、JavaFXというフレームワークが出てきてかなりGUIツール開発のハードルが下がったと聞いています(聞いているだけ)。

筆者はたまたま、「あー、GUIかー、どうやって作るんだろうなー」とネットを彷徨っていたら見つけました。
今日はそんな便利~~そう~~なJavaFXの入り口を少しだけご紹介します。

#2.筆者の環境
- MacBook Pro (Early 2015)
- macOS Mojave (10.14.6)
- IntelliJ IDEA Community Edition (2018.3.1)
- OpenJDK (13.0.1)

#3.環境構築
##3.1.OpenJDKのインストール
###3.1.1.ダウンロード
[OpenJDKのサイト](http://jdk.java.net/)から自分が使いたいバージョンのJDKをダウンロードします。
ただし、JavaFX側がJDK11以上を必要とするので注意。

■上記のサイト
<img width="500" alt="191121-0001.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/156346/0aa9e8ce-48fd-c179-80d4-f4e8cd5267db.png">
この画像の真ん中あたり、`macOS/x64`の右にある`tar.gz`のリンクからダウンロードします。
私は13.0.1を使いました。


###3.1.2.インストール
ダウンロードしたtar.gzファイルを展開し、以下のディレクトリに移動します。
`/Library/Java/JavaVirtualMachines`

```
/
└── Library
    └── Java
        └── JavaVirtualMachines
            └── jdk-13.0.1.jdk
                └── Contents
                    └── ...
```
確認として以下のコマンドを実行します。

```
$ /usr/libexec/java_home -V
Matching Java Virtual Machines (2):
    13.0.1, x86_64:	"OpenJDK 13.0.1"	/Library/Java/JavaVirtualMachines/jdk-13.0.1.jdk/Contents/Home
    11.0.2, x86_64:	"OpenJDK 11.0.2"	/Library/Java/JavaVirtualMachines/jdk-11.0.2.jdk/Contents/Home

/Library/Java/JavaVirtualMachines/jdk-13.0.1.jdk/Contents/Home
```
実行結果が全く同じでなくても構いません。
今インストールしたバージョンが認識されているか確認してください。

##3.2.プロジェクトのセットアップ
###3.2.1.プロジェクトの作成
IntelliJ IDEAを立ち上げ、Create New Projectから新しいプロジェクトを作ります。
今回はGradleを使ってJavaFXを利用するので、もちろんGradleのプロジェクトを作りましょう。

■プロジェクト作成画面
<img width="650" alt="191121-0002.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/156346/7de35d42-1d8a-8b70-07a1-2d052c12c530.png">
Project SDKは13になるように設定をしておいてください。

次の画面でGroupId, ArtifactIdを指定します。

さらに次の画面はプロジェクトの設定をする場所ですが、私はここは基本的にいじりません。
Gradle JVMが13になっていることを確認するだけでいいと思います。

その次の画面で使用するフォルダを指定したらプロジェクトが作成されます。
諸々の処理が終わるまで少し待ちましょう。

###3.2.2.build.gradleの設定
※以下、私のプロジェクトでのフォルダ構成で書きます。
フォルダ構成によって記述が変わる箇所は下の方にまとめて書きます。

build.gradleに以下の部分を**追記**します。
書いていないところは何も編集していません。

```gradle:build.gradle
plugins {
    id 'application'
    id 'org.openjfx.javafxplugin' version '0.0.8'
}

dependencies {
    //このruntimeOnly群はクロスプラットフォーム対応のjarを作るためのものです。
    //不必要なものは消すとjarファイルのサイズを小さくできます。
    runtimeOnly "org.openjfx:javafx-base:$javafx.version:win"
    runtimeOnly "org.openjfx:javafx-base:$javafx.version:linux"
    runtimeOnly "org.openjfx:javafx-base:$javafx.version:mac"
    runtimeOnly "org.openjfx:javafx-controls:$javafx.version:win"
    runtimeOnly "org.openjfx:javafx-controls:$javafx.version:linux"
    runtimeOnly "org.openjfx:javafx-controls:$javafx.version:mac"
    runtimeOnly "org.openjfx:javafx-fxml:$javafx.version:win"
    runtimeOnly "org.openjfx:javafx-fxml:$javafx.version:linux"
    runtimeOnly "org.openjfx:javafx-fxml:$javafx.version:mac"
    runtimeOnly "org.openjfx:javafx-graphics:$javafx.version:win"
    runtimeOnly "org.openjfx:javafx-graphics:$javafx.version:linux"
    runtimeOnly "org.openjfx:javafx-graphics:$javafx.version:mac"
}

javafx {
    version = "13"
    modules = [ 'javafx.controls', 'javafx.fxml' ]
}

mainClassName = 'maru.test.Launcher'
jar {
    manifest {
        attributes 'Main-Class': 'maru.test.Launcher'
    }
    from {
        configurations.runtimeClasspath.collect { it.isDirectory() ? it : zipTree(it) }
    }
}
```

■今こんな感じ(下の方ちょっと切れてます)
<img width="750" alt="191122-0003.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/156346/f79b4f08-3089-f1e3-f5a4-37701e32a6d4.png">

#4.サンプルのプロジェクトを動かす
##4.1.サンプルコードの実装
サンプルコードは[OpenJFXの公式さん](https://openjfx.io/index.html)からお借りしました。

```java:src/main/java/maru/test/Launcher.java
package maru.test;

import javafx.application.Application;

public class Launcher {
    public static void main(String... args){
        Application.launch(MainApp.class);
    }
}
```

```java:src/main/java/maru/test/MainApp.java
package maru.test;

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


public class MainApp extends Application {
    
    @Override
    public void start(Stage stage) throws Exception {
        Parent root = FXMLLoader.load(getClass().getResource("scene.fxml"));
        
        Scene scene = new Scene(root);
        scene.getStylesheets().add(getClass().getResource("styles.css").toExternalForm());
        
        stage.setTitle("JavaFX and Gradle");
        stage.setScene(scene);
        stage.show();
    }
}
```

```java:src/main/java/maru/test/FXMLController.java
package maru.test;

import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.control.Label;

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

public class FXMLController implements Initializable {
    
    @FXML
    private Label label;
    
    @Override
    public void initialize(URL url, ResourceBundle rb) {
        String javaVersion = System.getProperty("java.version");
        String javafxVersion = System.getProperty("javafx.version");
        label.setText("Hello, JavaFX " + javafxVersion + "\nRunning on Java " + javaVersion + ".");
    }
}
```

```xml:src/main/resources/maru/test/scene.fxml
<?xml version="1.0" encoding="UTF-8"?>

<?import javafx.scene.control.Label?>
<?import javafx.scene.layout.StackPane?>


<StackPane maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" prefHeight="400.0" prefWidth="600.0" xmlns="http://javafx.com/javafx/8.0.171" xmlns:fx="http://javafx.com/fxml/1" fx:controller="maru.test.FXMLController">
    <children>
        <Label fx:id="label" text="Label" />
    </children>
</StackPane>
```

```css:src/main/resources/maru/test/styles.css
.label {
    -fx-text-fill: blue;
}
```

■こんな感じ
<img width="750" alt="191122-0004.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/156346/e2d47dda-7c3d-0555-3756-ea46486a4495.png">

##4.2.コンパイル、実行
右上の方にあるGradleをクリック、Tasks>build>buildを実行します。
それが終わったら少し上のTasks>application>runを実行します。

■これです
<img width="750" alt="191122-0005.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/156346/6f4c298f-cb43-42d9-db18-0b629a64e024.png">
こんな感じのが出てきたら成功です！

■「こんな感じの」
<img width="500" alt="191121-0006.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/156346/5bd361ed-d2b4-9198-cc24-d4a35430946e.png">
`build/libs/`にjarファイルができていると思います。
~~このjarファイルはfatjar(ライブラリとかも含んでいるjarファイル)なので、誰のPCでも動くはずです。~~
**ごめんなさい多分嘘つきました。fatjarが欲しかったらshadowを入れましょう。**

##4.3.フォルダ構成によって記述が変わる場所
`<>`で囲まれている場所をよしなに変えてください。
フォルダ構成に左右される箇所だけ抜き出しています。

| ファイル名      |      |
|:--------------|:-----|
| build.gradle  | 2箇所 |
| Launcher.java | 1箇所 |
| MainApp.java  | 2箇所 |
| scene.fxml    | 1箇所 |

```gradle:build.gradle
mainClassName = '<Applicationクラスを継承していないランチャー用のクラス>'
jar {
    manifest {
        attributes 'Main-Class': '<上のmainClassNameと同じ>'
    }
}
```

```java:Launcher.java
public static void main(String... args){
    Application.launch(<Applicationクラスを継承しているクラス>.class);
}
```

```java:MainApp.java
@Override
public void start(Stage stage) throws Exception {
    Parent root = FXMLLoader.load(getClass().getResource("<fxmlファイル>"));
    scene.getStylesheets().add(getClass().getResource("<cssファイル>").toExternalForm());
}
```

```xml:scene.fxml
<StackPane (中略) fx:controller="<Controllerクラス>">
```
おそらく以上です。見落としがあったらごめんなさい。。。

#5.最後に
どうでしたか？思ったより簡単だったのではないでしょうか？

ちなみに、画面の構築には[SceneBuilder](https://gluonhq.com/products/scene-builder/)が便利です。
IntelliJ IDEAの設定をすればこんなこともできちゃいます。

■こんなこと
<img width="750" alt="191122-0001.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/156346/f59528e3-b979-2450-95c1-a16d2eb36dd0.png">
これでGUIアプリケーション開発のハードルがグッと下がったのではないでしょうか。
皆さんもよいGUIアプリケーションライフをお楽しみください！

#6.参考文献
- [Getting Started with JavaFX 13](https://openjfx.io/openjfx-docs/)