jiichan.com

PROGRAMMING

Java
javascript
CSS
PHP
SQL

IntelliJ と Gradle で JavaFX アプリケーション(非モジュール版)を作る

apache netbeans 12.0で Java8 版の JavaFX アプリを、Java11 以降の JDK で書き換えようとしたが上手くいかない。 apache netbeansのサイトの情報は古いものばかりで、とても役に立たない。
そこで、IDEを思い切ってIntelliJに替えてみた。これが非常に良い。それにネット上の情報が多い。
そこで、初めてのGradleでJavaFXアプリケーションを作ってみた。
好きなもので72歳を過ぎても、まだこんなことをやっている。(2021.02.21)

今回作ってみたアプリ...外部モジュール(QRコード用)をjarに含めている

ボタンクリック後


アプリケーション作成の主な流れ 1.新規プロジェクト作成 2.パッケージを作成 3.パッケージに対応したディレクトリをresourcesに作成する 4.パッケージにjavaクラスファイルを追加 5.resourcesにfxmlファイルcssファイルを追加 6.build.gradleをjavaFX用に編集 7.ソースファイルをいろいろ作る 8.Task build で実行可能jarを作る 9.Jlinkでカスタム JRE を作る 10.jpackageで配布用パッケージ(インストーラー)を作る 11.exewrapで実行可能jarをexe化する 12.NSISでインストーラーを作る 13.追加:もしアプリをモジュール版にするとしたら(2021.02.24)

1.新規プロジェクト作成

[ウェルカム画面]若しくは、メニューバーの[ファイル] [新規] [プロジェクト] [Gradle] [Java] [次へ]
任意のプロジェクト名[fxSample]を入れて完了。 [build.gradle]ファイルが自動で生成される。

自動生成されたbuild.gradle
plugins {
    id 'java'
}

group 'org.example'
version '1.0-SNAPSHOT'

repositories {
    mavenCentral()
}

dependencies {
    testImplementation 'org.junit.jupiter:junit-jupiter-api:5.6.0'
    testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine'
}

test {
    useJUnitPlatform()
}

2.パッケージを作成

プロジェクトペインで[src] [main] [java]で右クリック、[新規] [パッケージ]でパッケージ名[com.jiichan]を入力Enter

3.パッケージに対応したディレクトリをresourcesに作成する

・[src] [main] [resources]で右クリック、
・[新規] [ディレクトリ]でディレクトリ名[com]を入力Enter、
・[src] [main] [resources] [com]で右クリック、
・[新規] [ディレクトリ]でディレクトリ名[jiichan]を入力Enter

プロジェクトペインではフォルダ名が[com.jiichan]となっているが、実際は[com] [jiichan]の二つのフォルダが出来ている。


4.パッケージにjavaクラスファイルを追加

[com.jiichan]で右クリック[新規] [javaクラス]でクラス名[Launcher]を入力しEnter
[com.jiichan]で右クリック[新規] [javaクラス]でクラス名[MyApplication]を入力しEnter

5.resourcesにfxmlファイルcssファイルを追加

ディレクトリ[com.jiichan]で右クリック[新規] [ファイル]でファイル名を拡張子付き[scene.fxml]で入力しEnter
ディレクトリ[com.jiichan]で右クリック[新規] [ファイル]でファイル名を拡張子付き[style.css]で入力しEnter

6.build.gradleをjavaFX用に編集

既定で作られたbuild.gradleを次のように編集。
これらの編集が無いと、
「JavaFXランタイムが構成されていません。JavaFXが組み込まれているJDKを使用するか、JavaFXライブラリをクラスパスに追加」 と警告が表示される

編集後のbuild.gradle
plugins {
    // gradleでアプリケーションを実行できる(javaプラグインも使える)
    id 'application'
    // gradleでjavaFXが使えるように
    id 'org.openjfx.javafxplugin' version '0.0.9'
    // Fatjar方法1: shadowを使って実行可能jarに依存モジュール等を含める
//    id 'com.github.johnrengelman.shadow' version '6.1.0'
}

application {
    // plugins{application}のメインクラス
    mainClassName = 'com.jiichan.Launcher'
}

// plugins{org.openjfx.javafxplugin}で使用するバージョンと依存モジュール
javafx {
    version = '15.0.1'
    modules = [ 'javafx.controls', 'javafx.fxml' ]
}

// 実行可能jarのファイル名に付加されるバージョン
version '1.0'

// ダウンロードするモジュール等の所在地
repositories {
    mavenCentral()
}

// 依存するモジュール等(distributionsフォルダの配布用Zipのlibフォルダにも入る)
// javaFXはpluginで使えるようになっているので記載しない
dependencies {
    // QRコード生成(1次元・2次元コード画像処理ライブラリ)
    implementation 'com.google.zxing:core:3.4.1'
    implementation 'com.google.zxing:javase:3.4.1'
}

// 実行可能jarを作成する
jar {
    // Jar内に作成されるMANIFEST.MFのMainクラスを指定(これで実行可能jarとなる)
    manifest {
        attributes 'Main-Class': 'com.jiichan.Launcher'
    }
    // FatJar方法2:jarに含める依存ライブラリ(dependenciesで指定したモジュール等)
    from {
        configurations.runtimeClasspath.collect { it.isDirectory() ? it : zipTree(it) }
    }
}


実行可能jarに依存モジュール等を含める方法は、どうやら2通りあるらしい。

方法1:plugin(shadow)を使う方法
plugins {
	 ..........
	 ..........
	 id 'com.github.johnrengelman.shadow' version '6.1.0'
}

このplugin(shadow)を使う方法では、build/distributionsフォルダやbuild/libsフォルダには2組のjarができる。 allやshadowなどの名称がついたもので、どうもスッキリしない。

方法2:jarタスクを使う方法
jar {
	 ..........
	 ..........
	 from {
    	 configurations.runtimeClasspath.collect {it.isDirectory() ? it : zipTree(it)}
	 }
}

この二つ目の方法ではruntimeClasspathを使うべきなのか、compileClasspathを使うべきなのかよく分からない。 色々なサイトを見るとどちらも使われていて、それぞれの意味が分からない。
runtimeClasspathの方が多いようなのでこちらを使うことした。

7.ソースファイルをいろいろ作る

アプリの出発点がLauncherクラスで、このLauncherクラスのmainメソッドが、 FXアプリであるMyApplicationクラスのmainメソッドを呼んでいる。

Launcher.java
package com.jiichan;

public class Launcher {
    public static void main(String[] args){
        MyApplication.main(args);
    }
}

MyApplication.java
package com.jiichan;

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

import java.io.IOException;

public class MyApplication extends Application {

    @Override
    public void start(Stage stage) {
        try {
            Parent root = FXMLLoader.load(getClass().getResource("scene.fxml"));
            Scene scene = new Scene(root);
            stage.setTitle("JavaFX and Gradle");
            Image icon = new Image(getClass().getResourceAsStream("kobuta.png"));
            stage.getIcons().add(icon);
            stage.setScene(scene);
            stage.show();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

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

FXMLController.java
package com.jiichan;

import com.google.zxing.BarcodeFormat;
import com.google.zxing.EncodeHintType;
import com.google.zxing.WriterException;
import com.google.zxing.client.j2se.MatrixToImageWriter;
import com.google.zxing.common.BitMatrix;
import com.google.zxing.qrcode.QRCodeWriter;
import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel;
import javafx.fxml.FXML;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.TextField;
import javafx.scene.image.ImageView;
import javafx.scene.image.PixelWriter;
import javafx.scene.image.WritableImage;

import java.awt.image.BufferedImage;
import java.util.Hashtable;
import java.util.Objects;

public class FXMLController {

    @FXML
    private Label label;
    @FXML
    private Button qrButton;
    @FXML
    private ImageView imgView;
    @FXML
    private TextField urlStr;

    @FXML
    private void initialize() {
    }

    @FXML
    private void onButtonClick() {
        String contents = urlStr.getText();
        BarcodeFormat format = BarcodeFormat.QR_CODE;
        int width = 160;
        int height = 160;

        Hashtable hints = new Hashtable<>();
        hints.put(EncodeHintType.ERROR_CORRECTION, ErrorCorrectionLevel.M);

        QRCodeWriter writer = new QRCodeWriter();
        BitMatrix bitMatrix = null;
        try {
            bitMatrix = writer.encode(contents, format, width, height, hints);
        } catch (WriterException e) {
            e.printStackTrace();
        }
        BufferedImage bfimg = MatrixToImageWriter.toBufferedImage(Objects.requireNonNull(bitMatrix));
        // javaFX(imageView)で表示できるように変換
        WritableImage wrimg = new WritableImage(bfimg.getWidth(), bfimg.getHeight());
        PixelWriter pw = wrimg.getPixelWriter();
        for (int x = 0; x < bfimg.getWidth(); x++) {
            for (int y = 0; y < bfimg.getHeight(); y++) {
                pw.setArgb(x, y, bfimg.getRGB(x, y));
            }
        }
        imgView.setImage(wrimg);

        this.label.setText("QRコードが生成されました");
    }
}

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

<?import javafx.scene.control.*?>
<?import javafx.scene.image.*?>
<?import javafx.scene.layout.*?>
<?import javafx.scene.text.*?>

<AnchorPane prefHeight="400.0" prefWidth="600.0" 
            stylesheets="@styles.css" 
            xmlns="http://javafx.com/javafx/11.0.1" 
            xmlns:fx="http://javafx.com/fxml/1" 
            fx:controller="com.jiichan.FXMLController">
    <Label fx:id="label" layoutX="208.0" layoutY="25.0" text="QR code">
            <font><Font size="24.0" /></font></Label>
    <Button fx:id="qrButton" layoutX="208.0" layoutY="301.0" 
            mnemonicParsing="false" 
            onAction="#onButtonClick" 
            prefHeight="35.0" prefWidth="183.0" text="QRコード生成" />
    <TextField fx:id="urlStr" layoutX="163.0" layoutY="232.0" 
            prefHeight="26.0" prefWidth="272.0" 
            text="https://jiichan.com/">
            <font><Font size="17.0" /></font></TextField>
    <ImageView fx:id="imgView" fitHeight="150.0" fitWidth="150.0" 
            layoutX="199.0" layoutY="60.0" 
            pickOnBounds="true" preserveRatio="true" />
</AnchorPane>

styles.css
.label {
    -fx-text-fill: blue;
}

8.Task build で実行可能jarを作る

IntelliJ の右端、縦長のGraidleツールバーでウィンドウを開き、


[Tasks][build][build]をクリックすると、


暫くして左側のプロジェクトウィンドウにbuildフォルダが作られ、 その中のlibsフォルダに実行可能jarができる。


build->distributions フォルダ内の fxSample-1.0.zip の中身 binフォルダ ・fxSample(拡張子無しのファイル) ・fxSample.bat libフォルダ ・core-3.4.1.jar(外部依存ライブラリ) ・fxSample-1.0.jar (build->libs フォルダのものと同じ 10.3MB) ・jai-imageio-core-1.4.0.jar(外部依存ライブラリ) ・javafx-base-15.0.1.jar ・javafx-base-15.0.1-win.jar ・javafx-controls-15.0.1.jar ・javafx-controls-15.0.1-win.jar ・javafx-fxml-15.0.1-win.jar ・javafx-graphics-15.0.1.jar ・javafx-graphics-15.0.1-win.jar ・javase-3.4.1.jar(外部依存ライブラリ) ・jcommander-1.78.jar(外部依存ライブラリ)
bin フォルダには fxSample.bat があり、クリックするとアプリケーションが立ち上がる。
この fxSample.bat の中を覗いてみると JAVA_HOME の java.exe で起動しているようだ。

ということは、javaFXが含まれるこの zip だけを配布しても、 そのパソコンにJREがインストールされていないと立ち上がらないことになる。 カスタムJRE(javaFXはzipに含まれているので不要) を一緒に配布する必要がある。

どうやら、このzipは JREが入っているPCに配布するものらしい
念のためfxSample-1.0.jarの中を覗いたらjavaFX?らしきファイルがわんさか入っていた。

結論1:実行可能jarにはjavaFXのモジュールは 含めないようだ。
buid.gradleのdependencies{}で、javaFXを含めて実行可能jarを作っても、サイズが膨らむだけで 呼ばれないのでは無駄なことになる。ウ~ン分からない。

build時にはプラグイン org.openjfx.javafxplugin によってjavaFXが使えるようになっているらしい。
結論2:javaFXはJREにだけ含める。そもそもjavaFXはJRE(ランタイム)だということか。

9.Jlinkでカスタム JRE を作る

プラグイン org.beryx.jlink を使えば graidle 上でJlink を利用できるようだが、 どうもモジュールにしなければいけないらしい。
そこで、IntelliJ のターミナル上で Jlink を使いカスタムJREを作る。
確認の意味でjavaFXが含まれないjava.base・java.desktop だけのモジュールのカスタムJRE(jre-0)と、 javafx.controls,javafx.fxmlの入ったカスタムJRE(jre-15.0.1)を作ってみた。

javaFX無し
> jlink --compress=2 --module-path c:\Java\jdk-15.0.1\jmods --add-modules java.base,java.desktop --output jre-0
javaFX有り
> jlink --compress=2 --module-path c:\Java\jdk-15.0.1\jmods;c:\Java\javafx-jmods-15.0.1 --add-modules java.base,javafx.controls,javafx.fxml --output jre-15.0.1

jdk-15.0.1をシステムPATHから外し、このカスタムJREで順次実行してみた。

> jre-0\bin\java -jar buid\libs\fxSample-1.0.jar

やはりというか当然というか、カスタムJRE(jre-0)でfxSample-1.0.jarを実行してみるとエラー(Exception in Application start method )がでる。
良くは分からないがMyApplication.javaでscene.fxmlを読み込むところの行を示しているようだ。

次にjavaFXを含んだカスタムJRE(jre-15.0.1)でjarを実行してみた。

> jre-15.0.1\bin\java -jar build\libs\fxSample-1.0.jar

するとバッチリ立ち上がった。

10.jpackageで配布用パッケージ(インストーラー)を作る

openJDK14 から使えるパッケージツール jpackage は、そのサイトの説明によると何時また JDK から削除されるか分からないようだ。
javaアプリの起動をexeで起動できるようにラップする exewrap を使っているサイトもよく見かける。
jpackageが削除されたらそれはその時に考えるとして、取り敢えず本家で準備しているjpackageを使ってみることにした。

jpackageのオプション
オプション内容
–typeインストーラーの形式(exe、msiなど無指定はexe)
–nameアプリケーション名(無指定はjar名)
–input実行可能jarがあるパス
-destインストーラーの出力パス
–main-jar実行可能jarを指定(fxSample-1.0.jar)
–runtime-imagejlinkで作成した配布用JREがあるパス
–vendor配布元の開発者等
–win-shortcutデスクトップにショートカットを作成する
-win-menuシステム・メニューに追加
--iconiconファイルのパス"src\main\resources\com\jiichan\icon.ico"

コマンドプロンプトで実行してみる
> jpackage --type msi --name fxSample --input build\libs --dest build\installer --main-jar fxSample-1.0.jar --runtime-image jreAll --vendor jiichan --win-shortcut

作られている時間は多少長かったが、最初はすんなりとmsiが出来た。PCへのインストールも問題なかった。
ショートカットなどのオプションを増やしていくと、javaのioエラーがでて進まなくなってしまった。
オプションを減らして、成功したときの状態にしても全く作ることが出来なくなってしまった。
かなり時間をかけてネットを調べたがjpackage自体が余り検索に引っかからない。
やむを得ず断念し、 exewrap を使うことにした。

11.exewrapで実行可能jarをexe化する

初めからこの exewrap を使うべきだった。 使い方も実に簡単で、よく更新もされている。海外の物とは違い何か安心感もあった。
使い方は、実行可能jarと同じフォルダにexewrap.exeをコピーし、 コマンドプロンプトで次のようにjarを指定して実行すればよい。
次の例はjavaFX、つまりウィンドウアプリケーションなので-gオプション、 アイコンも指定するので-iオプションを付加している。

> exewrap -g -i kobuta.ico fxSample-1.0.jar

素晴らしい。

12.NSISでインストーラーを作る

exewrap が使えるのでインストーラーは必要ないかと思ったが もしもの時を考えて、インストーラーの作り方も勉強しておくことにした。
インストーラーは NSIS にした。

設定はスクリプトをnsiという拡張子のファイルに文字コードはShift-JISで書くらしい。

install.nsi
# Modern UIをインクルードする
!include MUI2.nsh

#基本データ
!define NAME "fxSample"
!define VERSION "1.0"
!define PACKAGE "${NAME}-${VERSION}"

# アプリケーション名
Name "${PACKAGE}"

# 作成するインストーラー名
OutFile "${PACKAGE}_Setup.exe"

# インストール先のディレクトリ
InstallDir "$PROGRAMFILES\${PACKAGE}"

# インストーラーページ
!insertmacro MUI_PAGE_WELCOME
!insertmacro MUI_PAGE_DIRECTORY
!insertmacro MUI_PAGE_INSTFILES
!insertmacro MUI_PAGE_FINISH

# アンインストーラ ページ
!insertmacro MUI_UNPAGE_WELCOME
!insertmacro MUI_UNPAGE_CONFIRM
!insertmacro MUI_UNPAGE_INSTFILES
!insertmacro MUI_UNPAGE_FINISH

# 日本語UI
!insertmacro MUI_languageUAGE "Japanese"

# インストールを中断するときに警告を出す
!define MUI_ABORTWARNING

# デフォルトセクション
Section
  # 出力先を指定
  SetOutPath "$INSTDIR"
  # インストーラーに組み込むファイル群
  File "C:\NSIS\${NAME}\${PACKAGE}.exe"
  File /r "C:\NSIS\${NAME}\jre-15.0.1"

  # アンインストーラを出力
  WriteUninstaller "$INSTDIR\Uninstall.exe"

  # スタートメニューにショートカットを登録
  CreateShortcut "$SMPROGRAMS\${PACKAGE}\${PACKAGE}.lnk" "$INSTDIR\${PACKAGE}.exe" ""
  # デスクトップにショートカットを作成
  CreateShortcut "$DESKTOP\${PACKAGE}.lnk" "$INSTDIR\${PACKAGE}.exe" ""
  # レジストリに登録
  WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${PACKAGE}" "DisplayName" "${PACKAGE}"
  WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${PACKAGE}" "UninstallString" '"$INSTDIR\Uninstall.exe"'
SectionEnd

# アンインストーラ
Section "Uninstall"
  # アンインストーラを削除
  Delete "$INSTDIR\Uninstall.exe"
  # ファイルを削除
  Delete "$INSTDIR\${PACKAGE}.exe"
  # ディレクトリを削除
  RMDir /r "$INSTDIR"
  # スタートメニューから削除
  Delete "$SMPROGRAMS\${PACKAGE}\${PACKAGE}.lnk"
  Delete "$DESKTOP\${PACKAGE}.lnk"
  RMDir "$SMPROGRAMS\${PACKAGE}"
  # レジストリ キーを削除
  DeleteRegKey HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${PACKAGE}"
SectionEnd

このnsiスクリプトをNSISアプリ[Compile NSI scripts]で読込ませるとインストーラー(exe)ができる。
これでインストーラーまで出来た。
長い道のりだった。しかし、世の中には頭の良い人がいっぱいいるものだ。凄い。

13.追加:もしアプリをモジュール版にするとしたら

そもそもモジュールにするというのは、アクセス権の強化のためらしい。
クラスがpublicでも、そのパッケージにはアクセスさせたくないとか。
爺ちゃんレベルだと利点が良く分からない。

ところで、もし、このアプリをモジュール版にするとしたら
まず、モジュール定義ファイルをsrc/main/java/に配置する。
これでjavaフォルダのパッケージ(com.jiichan)がモジュールになるらしい。

module-info.java
module com.jiichan {
    // 依存するモジュール
    requires 依存しているモジュール名

    // 他のモジュールからのアクセスを許可するパッケージ
    exports アクセスを許可するパッケージ名
    // モジュールを限定してアクセスを許可するパッケージ
    exports 許可するパッケージ名 to アクセスできるモジュール名, ...

    // 実行時にのみパッケージへのアクセスを許可するパッケージ
    open アクセスを許可するパッケージ名
    // 実行時にのみ特定のモジュールに対してパッケージへのアクセスを許可する
    opens アクセスを許可するパッケージ名 to アクセスできるモジュール名, ...
}

次に、build.gradleに一項目追加、アプリが複数モジュールの場合は更にmainModuleも追加のようだ。

build.gradle
java {                                  // 追加
    modularity.inferModulePath = true   // 追加
}                                       // 追加

application {
    mainModule = 'com.jiichan'          // 複数モジュールの場合追加
    // plugins{application}のメインクラス
    mainClassName = 'com.jiichan.Launcher'
}

ところでモジュールにする為に必要なことは少し分かったが、 依存するjarがモジュールかライブラリかはどうしたら分かるのだろう。
いちいちjarを展開してmodule-info.javaが存在するか確認するのだろうか。
その場合リポジトリからダウンロードするものだとどうなるのだろう。

分からないことだらけだ。