Edited at

KotlinとJavaFXで流れるHello world

More than 1 year has passed since last update.


  • 実行可能JARファイル作成方法を追記(2018-03-11)


はじめに

Java読書会BOFでは2018年1月から「Kotlinイン・アクション」の読書会を開催しています。

せっかくKotlinを学ぶのであれば、何か作ってみようということで、以前作成したJavaFXでHello worldのメッセージが流れるGUIを、Kotlinで書き直すことにしました。

本記事は、上述のJavaコードをKotlinに置き換えることを目的としています。したがいまして、JavaFXの解説は上述ブログを参照ください。

作成したコードは次のリポジトリに格納(リポジトリビューアのURL)

http://www.torutk.com/projects/swe/repository/revisions/master/show/learn/kotlin/javafx/MessageBoard


環境の準備

環境構築の苦労を避け、またコード補完ほかの多大なコーディング支援を活用したいならば、Kotlinを開発しているJetBrains社の開発環境 IntelliJ IDEA か、IntelliJ IDEAをベースにしている Android Studio がほぼ一択です。

「JavaFXで流れるHello world」の記事ではJavaのコマンドライン環境で作成する方法を紹介しました。Javaの実行環境の知識は有用なので知っておくとよいという点があります。しかし、Kotlinの場合はJavaの環境に加えてKotlinの環境を整える部分での労が多く、またその知識はそれほど活用する機会もないので、今回は最初からIDEを使います。


IntelliJ IDEA Community Edition

IntelliJ IDEAは商用の開発環境製品ですが、Community Editionが無償で提供されています。Kotlinの開発環境はCommunity Editionに含まれているので今回はこれを使用します。

なお、Android Studio(こちらもGoogleから無償で提供)でもほぼ同じに使えると思いますが、検証はしていません。


IntelliJ IDEAの日本語化(お好みで)

Eclipseでおなじみの日本語化ツール Pleiades が IntelliJ IDEAの日本語化にも対応していました。次にちょっと導入方法を書いています。


日本語Windows環境でのIntelliJ IDEAのフォント(お好みで)

日本語Windows環境では、欧文フォントはきれいですが、日本語フォントはMSゴシックでギザギザです。

次にちょっとフォントをきれいにする設定を書いています。


Kotlinのプロジェクトを作成

IntelliJ IDEAで、流れるHello worldプログラムのプロジェクトを作成します。


  • IntelliJ IDEA の[ファイル]メニュー > [新規] > [プロジェクト]を選択

  • 「新規プロジェクト」画面の左側ペインで[Kotlin]を選択、右側ペインで[Kotlin/JVM]を選択し、[次へ]ボタンをクリック

  • [プロジェクト名]にMessageBoardを入れ、プロジェクトを作成


Kotlinソースファイルを作成


  • IntelliJ IDEAのプロジェクトペインで[src]を選択

  • [ファイル]メニュー > [新規] > [Kotlinファイル/クラス] を選択し、名前をMessageBoard、種類をClassとする


MessageBoard.kt

class MessageBoard {

}

ここから、JavaFXを呼び出して流れるHello worldを作成していきます。


KotlinとJavaFXで流れるHello worldのプログラミング

KotlinでJavaFXを呼び出し、流れるHello worldメッセージのGUIをプログラミングしていきます。


JavaFXのApplicationクラスを継承する

JavaFXではApplicationクラスを継承し、JavaFXの処理の入り口となるクラスを用意します。


ちょっとKotlinかじったJavaプログラマーの知識で進めていくと‥‥

アプリケーションで作成するクラスに、JavaFXのApplicationクラスを継承させます。


MessageBoard.kt

class MessageBoard : Application {

}

Kotlinでは、継承はコロン記号で指定します。ここで、Applicationクラスはインポートが必要ですので、IntelliJ IDEAのコード上では次のようにポップアップメッセージが表示されます。

ポップアップのメッセージに従いAlt+Enterを押し、候補リストからjavafx.application.Applicationを選択します。

import文が生成されます。


MessageBoard.kt

import javafx.application.Application

class MessageBoard : Application {
}


しかし、まだ未完成のためコンパイルエラー状態です。次の画面にエラー内容がポップアップされた状況を示します。

startメソッドを定義する必要があります。[コード]メニュー > [メソッドの実装] で、Applicationクラスの抽象メソッドstartを生成します。

生成されたコードは次です。


MessageBoard.kt

import javafx.application.Application

import javafx.stage.Stage

class MessageBoard : Application {
override fun start(primaryStage: Stage?) {
TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
}
}


ですが、まだエラーが残っています。Application の下に赤い波線が引かれています。カーソルをApplicationのところに持っていくと、次のようにエラー内容がポップアップされます。

"This type has a constructor, and thus must be initialized here"というメッセージです。これは何でしょうか?

詳しくは次項に記述します。


コンストラクタを持たないクラスで継承するときは

「Kotlinイン・アクション」 4.2.1項のp.106に


open class Button ← 引数のないデフォルトコンストラクタが生成される

Buttonクラスを継承し、なおかつコンストラクタを持たない場合は、superクラスのコンストラクタが引数を持たなかったとしても、そのスーパークラスのコンストラクタを明示的に呼び出す必要があります。

class RadioButton : Button()


ということで、今回はMessageBoardクラスにコンストラクタを定義しないので、継承のスーパークラスに丸括弧を付けるのが正解となります。


MessageBoard.kt

import javafx.application.Application

import javafx.stage.Stage

class MessageBoard : Application() {
override fun start(primaryStage: Stage?) {
TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
}
}


これでコンパイルエラーが取れたコードとなりました。


  • TODOは、実行すると例外(kotlin.NotImplementedError)をスローします


mainメソッドの定義場所

しかし、まだ実行ができません。プログラムのエントリポイントとなるmainメソッドを定義します。Kotlinでは、Javaのようにクラス内にstaticメソッドでmainメソッドを定義するのではなく、トップレベル関数として定義します。


MessageBoard.kt

class MessageBoard : Application() {...}

fun main(args: Array<String>) {
// ここにApplicationクラスのlaunchメソッドを呼ぶコードを記述
}


Kotlinには静的メソッド・静的フィールドはありませんが、Javaのクラスで定義されている静的メソッド・静的フィールドにアクセスすることは可能で、その呼び出し式はJavaのと同じです。

JavaFXのApplicationクラスのlaunchメソッドは、Applicationクラスのサブクラス内から呼び出すときと、そのサブクラス外から呼び出すときで引数が異なります。今回は、サブクラス外からとなるので、Javaでは次の形式のlaunchメソッドを呼びます。

public static void launch(

Class<? extends Application> appClass,
String... args
)

ここで、次の2つの課題があります。


  • Class型のインスタンスの指定(Javaでは MessageBoard.class を指定するところ)

  • 可変長引数の指定

1つ目は、 クラスに対して::classでクラス参照(KClass型)を取り出したあとに、KClass型の拡張プロパティjavaを呼び出し取得します。

2つ目は、スプレッド演算子を使って配列を展開します。

fun main(args: Array<String>) {

Application.launch(MessageBoard::class.java, *args)
}

スプレッド演算子で可変長引数を指定するところは、「Kotlinイン・アクション」 3.4.2項(p.76)に


Kotlinは配列の中身を明示的に取り出す必要があり、そうすることで配列の全要素が呼び出される関数への独立した引数となります。技術的には、この機能はスプレッド演算子(spread operator)を使用して呼び出されますが、実際には、対応する引数の前に*を置くという単純なものです。

fun main(args: Array<String>) {

val list = listOf("args: ", *args) スプレッド演算子は配列の中身を取り出す
print(list)
}

とあります。

これで最小限の実装ができました。実行してみます(まだウィンドウも表示さっれませんが)。


実行のための設定

ファイルのトップレベルにmain関数を定義すると、その行の左脇の箇所に次のアイコンが付与されます。

このアイコンをクリックすると、実行・デバッグ・実行カバレッジの選択肢がポップアップします。[実行]を選択します。

するとコードがビルドされ実行されます。

初回、このアイコンから実行すると、以降は[実行]メニューから[実行]が有効となり、また、[実行]メニュー > [起動構成の実行] でリストにmain関数を定義したファイルのクラス名(ファイル名にKtを付けた名前のクラス)が追加され選択できるようになります。

実行すると、startメソッドのテンプレート実装コードTODOが呼ばれて例外を表示します。

"C:\Program Files\Java\jdk-9\bin\java" "-javaagent:C:\Program Files\JetBrains\IntelliJ IDEA Community Edition 2017.3.1\lib\idea_rt.jar=52509:C:\Program Files\JetBrains\IntelliJ IDEA Community Edition 2017.3.1\bin" -Dfile.encoding=UTF-8 -classpath "D:\work\java\repos\swe.primus\learn\kotlin\javafx\MessageBoard\out\production\MessageBoard;C:\Program Files\JetBrains\IntelliJ IDEA Community Edition 2017.3.1\plugins\Kotlin\kotlinc\lib\kotlin-stdlib.jar;C:\Program Files\JetBrains\IntelliJ IDEA Community Edition 2017.3.1\plugins\Kotlin\kotlinc\lib\kotlin-reflect.jar;C:\Program Files\JetBrains\IntelliJ IDEA Community Edition 2017.3.1\plugins\Kotlin\kotlinc\lib\kotlin-test.jar;C:\Program Files\JetBrains\IntelliJ IDEA Community Edition 2017.3.1\plugins\Kotlin\kotlinc\lib\kotlin-stdlib-jdk7.jar;C:\Program Files\JetBrains\IntelliJ IDEA Community Edition 2017.3.1\plugins\Kotlin\kotlinc\lib\kotlin-stdlib-jdk8.jar" MessageBoardKt

Exception in Application start method
Exception in thread "main" Exception in thread "Thread-2" java.lang.RuntimeException: Exception in Application start method
at javafx.graphics/com.sun.javafx.application.LauncherImpl.launchApplication1(LauncherImpl.java:973)
at javafx.graphics/com.sun.javafx.application.LauncherImpl.lambda$launchApplication$2(LauncherImpl.java:198)
at java.base/java.lang.Thread.run(Thread.java:844)
Caused by: kotlin.NotImplementedError: An operation is not implemented: not implemented
at MessageBoard.start(MessageBoard.kt:6)
at javafx.graphics/com.sun.javafx.application.LauncherImpl.lambda$launchApplication1$9(LauncherImpl.java:919)
at javafx.graphics/com.sun.javafx.application.PlatformImpl.lambda$runAndWait$11(PlatformImpl.java:449)
at javafx.graphics/com.sun.javafx.application.PlatformImpl.lambda$runLater$9(PlatformImpl.java:418)
at java.base/java.security.AccessController.doPrivileged(Native Method)
at javafx.graphics/com.sun.javafx.application.PlatformImpl.lambda$runLater$10(PlatformImpl.java:417)
at javafx.graphics/com.sun.glass.ui.InvokeLaterDispatcher$Future.run(InvokeLaterDispatcher.java:96)
at javafx.graphics/com.sun.glass.ui.win.WinApplication._runLoop(Native Method)
at javafx.graphics/com.sun.glass.ui.win.WinApplication.lambda$runLoop$3(WinApplication.java:175)
... 1 more
java.lang.IncompatibleClassChangeError


トップレベルウィンドウの表示

いよいよ実装に入ります。

まずは、startメソッドでstageの可視化を記述します。Javaプログラマーとしては次のようにコーディングしてしまいますが、これはコンパイルエラーです。

引数の型がStage?と末尾に?が付いているのがポイントです。


MessageBoard.kt

    override fun start(primaryStage: Stage?) {

primaryStage.show()
}

コンパイルエラーは次のポップアップに表示されています。

startメソッドの引数の型はStage?型とnull許容型です。したがってメソッド呼び出し前にnull検査をするか、null対応のメソッド呼び出しをする必要があります。

ここでは、引数primaryStageがnullとなった場合、何もしないので安全呼び出し演算子(?.)でshowメソッドを呼び出します。


MessageBoard.kt

    override fun start(primaryStage: Stage?) {

primaryStage?.show()
}

「Kotlinイン・アクション」 6.1.3項(p.181)に


安全呼び出し演算子(safe-call operator)の?.です。これはnullチェックとメソッド呼び出しを1つの演算子に結合したものです。(中略)

つまり、メソッドを呼び出そうとしている値がnullでない場合、メソッド呼び出しは通常どおりに実行され、値がnullの場合、その呼び出しはスキップされ、その代わりに値としてnullが使用されます。


とあります。

実行すると、空のウィンドウが表示されます。


メッセージの配置

空のウィンドウに、テキストを表示させます。

まず、文字を表示するTextを生成します。Javaとの違いはnew演算子がないこと、変数の型はイミュータブルのvalで宣言しているところです。


MessageBoard.kt

val message = Text("Hello, world. This is JavaFX from Kotlin.")


JavaFXのノードをまとめるグループを生成します。インスタンスの生成と変数の型は先のTextと同様です。

次にJavaではsetLayoutY(50d)と呼び出していた箇所が、Kotlinではプロパティへの代入文になります。また、javaではDoubleの数値リテラルを示すD(またはd)がKotlinにはないので、小数点以下を.0と明示します。

ちなみに、50と整数(Int型)リテラルを記述すると型が違うとエラーになります。


MessageBoard.kt

val group = Group(message)

group.layoutY = 50.0

Sceneを生成します。サイズ(幅、高さ)は整数では型が違うとエラーになるので、小数点以下を明示しています。また、javaではprimaryStageへsetSceneメソッドでセットする所、Kotlinではプロパティへの代入となります。


MessageBoard.kt

        val scene = Scene(group, 320.0, 160.0)

primaryStage?.scene = scene

startメソッド全体は次になります。


MessageBoard.kt

    override fun start(primaryStage: Stage?) {

val message = Text("Hello, world. This is JavaFX from Kotlin.")
val group = Group(message)
group.layoutY = 50.0
val scene = Scene(group, 320.0, 160.0)
primaryStage?.scene = scene
primaryStage?.show()
}

実行します。


メッセージを流す

JavaFXのアニメーションを使ってテキストを右から左へ移動させていきます。

startメソッドに、アニメーション用のTranslateTransitionを定義します。


MessageBoard.kt

val messageTransition = TranslateTransition(Duration.seconds(8.0), message)

messageTransition.fromX = 320.0
messageTransition.toX = -320.0
messageTransition.interpolator = Interpolator.LINEAR
messageTransition.cycleCount = TranslateTransition.INDEFINITE

ここでも、new演算子が不要、setメソッドはプロパティへの代入とする、といった違いがありますが、ほとんどJavaのコードと一緒です。

また、最後にplay()を呼び出します。


MessageBoard.kt

messageTransition.play()


実行すると、流れるHello worldが表示されます。

次は表示された画面を10fpsでアニメーションGIFにしたものです(カクカクなのはアニメーションGIFにしたときのFPSの問題、JavaFXは最大60fpsで描画しています)。

先のコードで、TranslateTransitionの記述がKotlin的には冗長(ローカル変数へのプロパティ代入が繰り返し登場)なので、applyを使って記述量を減らします。減らした後のstartメソッド全体を次に示します。


MessageBoard.kt

    override fun start(primaryStage: Stage?) {

val message = Text("Hello, world. This is JavaFX from Kotlin.")

val group = Group(message)
group.layoutY = 80.0

val scene = Scene(group, 320.0, 160.0)

primaryStage?.scene = scene
primaryStage?.show()

TranslateTransition(Duration.seconds(8.0), message).apply {
fromX = 320.0
toX = -320.0
interpolator = Interpolator.LINEAR
cycleCount = TranslateTransition.INDEFINITE
}.play()

}



見栄えの調整、色とフォント

最後に、メッセージのテキストの色と大きさを設定します。


MessageBoard.kt

    override fun start(primaryStage: Stage?) {

val message = Text().apply {
text = "Hello, world. This is JavaFX from Kotlin."
fill = Color.DARKMAGENTA
font = Font.font("Serif", FontWeight.SEMI_BOLD, 32.0)
}
val messageWidth = message.layoutBounds.width
val messageHeight = message.layoutBounds.height

val group = Group(message)
group.layoutY = messageHeight * 2

val scene = Scene(group, messageWidth, messageHeight * 3)

primaryStage?.scene = scene
primaryStage?.show()

TranslateTransition(Duration.seconds(8.0), message).apply {
fromX = messageWidth
toX = -messageWidth
interpolator = Interpolator.LINEAR
cycleCount = TranslateTransition.INDEFINITE
}.play()
}


実行します。


流れるHello worldの配布

今回は、実行可能JARにまとめることを目標とします。


IntelliJ IDEAで実行可能JARを生成する

NetBeans IDEでは、Javaのプロジェクトを作成すると、特に追加設定をしなくても実行可能JARファイルを作成してくれました。

IntelliJ IDEAでもできるはずと調べてみると、次の手順で実行可能JARファイルを生成できるようになります。


  • [ファイル]メニュー > [プロジェクト構造] で、「プロジェクト構造」画面の左側ペインの [成果物]を選択、中側ペインの[+]をクリックし、[JAR] > [依存関係を持つモジュールから]をクリックします(次に画面を示します)。


Kotlinではメインクラスはどれ?

メイン・クラスを指定する必要があります。

Kotlinでは、main関数はファイルのトップレベルに記述します。Javaバイトコードにコンパイルした時点では、ファイル名にちなむクラス(MessageBoard.ktファイルのトップレベルにmain関数を定義した場合は、クラスMessageBoardKtに含まれる)になります。

IntelliJ IDEAでビルド・実行したときに、プロジェクトディレクトリ下のout\production\MessageBoardの下に次のクラスファイルが生成されます。


  • MessageBoard.class

  • MessageBoardKt.class

JDKのjavapコマンドでそれぞれクラスファイルに定義されるメソッドを調べます。

D:~> javap MessageBoard

Compiled from "MessageBoard.kt"
public final class MessageBoard extends javafx.application.Application {
public void start(javafx.stage.Stage);
public MessageBoard();
}

D:~>javap MessageBoardKt
Compiled from "MessageBoard.kt"
public final class MessageBoardKt {
public static final void main(java.lang.String[]);
}

mainメソッドを持つクラスは、MessageBoardKt.classとなります。

そこで、先ほどの続きで、次の画面でメイン・クラスを指定します。メイン・クラス欄右端の[…]をクリックします。

すると、「メイン・クラスを選択」画面が表示されます。[名前で検索]タブで、最初は空表示なのでメイン・クラス名の一部(ここでは"M")を入力します。するとその名前に該当するクラスが一覧表示されるので、ここでは[MessageBoardKt]を選択します。

ライブラリーからのJARファイル欄は、デフォルトでは[ターゲットJRAに抽出する]となっています。これは、Java SE標準以外のライブラリを使用しているとき、実行可能JARファイルの中にライブラリのクラスファイル群を一緒に含めてしまいます。

[マニフェスト経由で出力ディレクトリーとリンクにコピーする]は、実行可能JARファイルとは別にライブラリファイルを用意し、実行JARファイル内のマニフェストにライブラリファイルへの参照を記述するかの違いです。

今回は、1つのJARファイルだけ配布すれば実行可能となるようにデフォルトのままとします。[OK]をクリックすると、次のように構成内容が表示されます。

[プロジェクト・ビルドに含める]にチェックを付けると、ビルドを実行するときに一緒にJARファイル生成をします。時間がかかるのを嫌うなら、チェックは外したままとし、[ビルド]メニュー > [成果物のビルド]を選択してJARファイルの生成をします。先ほど作成した構成の名前のBuild/Rebuildを選択します。

JARファイルは、プロジェクトディレクトリの out\artifacts\MessageBoard_jar\MessageBoard.jar に生成されました。容量は3.3MBと大きいです。Kotlinの実行ライブラリを一緒に含んでいるためです。

このJARファイルを単独で配布すれば、JREが入っている環境でKotlinで作成したプログラムを実行可能です。


応用編


分解宣言をJavaのクラスに適用

次のコードが冗長と感じるようになってしまったら、

val messageWidth = message.layoutBounds.width

val messageHeight = message.layoutBounds.height

分解宣言が適用したくなって

val (messageWidth, messageHeight) = message.layoutBounds

と書きたくなってきます。しかし、Javaのクラスには分解宣言を適用するためのメソッド component1(), component2(), ... が定義されていません。

そこで、拡張メソッドで定義します。

operator fun Bounds.component1(): Double = width

operator fun Bounds.component2(): Double = height

こうすると、Boundsクラスを分解宣言で使うことができるようになります。

しかし、これはやり過ぎ感が大きいです。「過猶不及」(過ぎたるは猶及ばざるが如し)です。