目次
- 【講義】JavaFX で作成する GUI アプリケーション
- 【実習】GUI アプリケーションの実装
- 【講義】GUI アプリケーションの解説
- 【実習】単一のアプリケーションとしてビルドする
- 【実習】アニメーションを表示するアプリケーションの実装
- 【講義】アニメーションを表示するアプリケーションの解説
- 【実習】起動引数で動きを変更する
【講義】JavaFX で作成する GUI アプリケーション
この回では、 Scala を利用して単一のアプリケーションを作成できるようにする。
アプリケーションと言っても様々であるが、ここでは GUI (Graphical User Interface) のアプリケーションを作ってみる。
Scala が動いている JVM には様々な GUI 作成のためのライブラリが入っている。
その中でも比較的新しい JavaFX という GUI ライブラリを利用したアプリケーションを作成してみる。
元々この JavaFX は、サーバーなどにアクセスするリッチクライアントを開発するために設計されたライブラリとなっている。
リッチクライアント とは、サーバーとの通信をする表現力や操作性の髙いクライアント・アプリケーションのこと。
最近ではこのリッチクライアントを Web ブラウザと JavaScript で実装することもある。
ただし、大量のデータや大量の描画を扱う必要性がある場合には、 JavaScript よりも Java で実装された JavaFX の方が軽快に動くというメリットがある。
このように軽快に動くというメリットを活かして、 Java を利用してネットワークゲームなどを開発することもある。
有名な Minecraft というゲームの PC 版は、その特性を活かして Java で開発されている。
この Java の特性から軽快に動くのはもちろんのこと、 Windows だけではなく、 Mac やその他の OS でも利用できるようになっている。
【実習】JRE のインストール
インストール済みのためスキップ。
【実習】GUI アプリケーションの実装
早速実装していこう。
まずHomeディレクトリの workspace
フォルダに scala-gui
というフォルダを作成して、フォルダ構成を作っていく。
cd ~
mkdir -p workspace/scala-gui/src/main/scala
cd workspace/scala-gui
以上のコマンドを実行して、フォルダ構成を作成した後に、プロジェクトフォルダである、 scala-gui
フォルダに移動しよう。
今作成した、 src/main/scala
は、 Scala のソースコードを置くためのフォルダ。
sbt では、デフォルトでプロジェクトディレクトリ直下に Scala のソースコードを記述しても良いのだが、 Java のソースコードとの共存やテストコードとの共存をさせた理する可能性がある場合には、このような src/main/scala
等フォルダを利用する。
なお、 Java のコードと共存させる場合に src/main/java
というフォルダをテストコードと共存させる場合には、 src/test/scala
というフォルダを利用する決まりになっている。
次に、 src/main/scala/Main.scala
というファイルを作成して以下のように実装しよう。
import javafx.application.Application
import javafx.event.{ActionEvent, EventHandler}
import javafx.scene.Scene
import javafx.scene.control.Button
import javafx.scene.layout.StackPane
import javafx.stage.Stage
object Main extends App {
Application.launch(classOf[Main], args: _*)
}
class Main extends Application {
override def start(primaryStage: Stage): Unit = {
val btn = new Button()
btn.setText("押してね")
btn.setOnAction(new EventHandler[ActionEvent] {
override def handle(event: ActionEvent): Unit = {
println("こんにちは")
}
})
val root = new StackPane()
root.getChildren.add(btn)
val scene = new Scene(root, 300, 250)
primaryStage.setTitle("コンソールにこんにちはを出力")
primaryStage.setScene(scene)
primaryStage.show()
}
}
その後、 scala-gui フォルダの直下に build.sbt
というファイルを作成して、
scalaVersion := "2.12.7"
scalacOptions ++= Seq("-deprecation", "-feature", "-unchecked", "-Xlint")
val osName: SettingKey[String] = SettingKey[String]("osName")
osName := (System.getProperty("os.name") match {
case name if name.startsWith("Linux") => "linux"
case name if name.startsWith("Mac") => "mac"
case name if name.startsWith("Windows") => "win"
case _ => throw new Exception("Unknown platform!")
})
libraryDependencies += "org.openjfx" % "javafx-base" % "11-ea+25" classifier osName.value
libraryDependencies += "org.openjfx" % "javafx-controls" % "11-ea+25" classifier osName.value
libraryDependencies += "org.openjfx" % "javafx-fxml" % "11-ea+25" classifier osName.value
libraryDependencies += "org.openjfx" % "javafx-graphics" % "11-ea+25" classifier osName.value
libraryDependencies += "org.openjfx" % "javafx-web" % "11-ea+25" classifier osName.value
その後、 sbt を起動し、 run
コマンドを実行しよう。
依存ファイルのダウンロードとコンパイルに数分かかるので、待とう。
無事コンパイルが終わると、タイトルが「コンソールにこんにちはを出力」と書かれたダイアログが表示されたのではないだろうか。
また「押してね」と書いてあるボタンをクリックすることでコンソールに「こんにちは」と出力されるのではないだろうか。
[info] running Main
こんにちは
これで GUI のアプリケーションを作ることができた。
そしてこのアプリケーションを終了するときには、メニューの Java のところから、 Quit Main を選択するか、 Mac の場合は左上の赤いボタンをクリックして終了させよう。
【講義】GUI アプリケーションの解説
先ほど起動したアプリケーションのコードを解説する。
import javafx.application.Application
import javafx.event.{ActionEvent, EventHandler}
import javafx.scene.Scene
import javafx.scene.control.Button
import javafx.scene.layout.StackPane
import javafx.stage.Stage
以上は JavaFX で GUI を作るのに必要な「クラス」のインポート。
「クラス」については後にもっと詳しく説明するが、ここでは、オブジェクトを作るための雛形となる定義として知っておこう。
またそれぞれのクラスが持っているメソッドについては、 JavaFX の API ドキュメント を読むことでより詳細な使い方を見ることができる。
object Main extends App {
Application.launch(classOf[Main], args: _*)
}
これは、このアプリケーションが実行された際に呼び出される部分。
Application.launch 関数は、 JavaFX のアプリケーションを起動するための関数。
その第一引数には GUI を定義してあるクラスを、第二引数いこうには、可変長引数でコマンド実行時の引数の文字列の配列を渡してある。
classOf[Main]
はこの下に定義している Main クラスのクラス自身を、 args: _*
は App トレイトが定義している args という文字列の配列を、 : _*
という指定をつけることで、可変長引数で渡している。
可変長引数とは、引数のリストをいくらでも渡すことができる関数のインターフェースだが、ここでは、配列に置き換えてしまっている。
class Main extends Application {
override def start(primaryStage: Stage): Unit = {
val btn = new Button()
btn.setText("押してね")
btn.setOnAction(new EventHandler[ActionEvent] {
override def handle(event: ActionEvent): Unit = {
println("こんにちは")
}
})
val root = new StackPane()
root.getChildren.add(btn)
val scene = new Scene(root, 300, 250)
primaryStage.setTitle("コンソールにこんにちはを出力")
primaryStage.setScene(scene)
primaryStage.show()
}
}
以上は、 Main というクラスの定義。
Scala ではオブジェクトと同様の名前のクラスを作成できる。
このクラスの使い方などは、今後「クラス」や「継承」という概念を別の回でじっくりやるので、そこで詳しく解説する。
val btn = new Button()
btn.setText("押してね")
このコードではボタンを作成し、そこに「押してね」というラベルを設定している。
btn.setOnAction(new EventHandler[ActionEvent] {
override def handle(event: ActionEvent): Unit = {
println("こんにちは")
}
})
以上のコードではボタンが押された時のイベントを処理して、コンソールに「こんにちは」と出力している。
val root = new StackPane()
root.getChildren.add(btn)
val scene = new Scene(root, 300, 250)
primaryStage.setTitle("コンソールにこんにちはを出力")
primaryStage.setScene(scene)
primaryStage.show()
ここでは、 JavaFX のアプリケーションに載せるための画面を作成して、その大きさを定義したり、タイトルを「コンソールにこんにちはを出力」を設定したりした後、すべての GUI を表示させる処理をしている。
次に build.sbt
の説明。
build.sbt
は、 sbt がビルドを行う際の設定ファイルとなる。
Scala を拡張した文法で設定を書くことが可能。
scalaVersion := "2.13.6"
ここでは、 Scala のバージョンを指定している。
このようにビルドの設定で、好きな Scala のバージョンを指定できる。
scalacOptions ++= Seq("-deprecation", "-feature", "-unchecked", "-Xlint")
以上は、 Scala のコンパイル時のオプション。
それぞれのオプションは、
-
-deprecation
は、今後廃止の予定の API を利用している時の警告が出力される -
-feature
は、実験的な API 利用時の警告が出力される -
-unchecked
は、型を利用したパターンマッチという機能が正しく動かないときに警告が出力される -
-Xlint
は、望ましい書き方をされていない場合に警告が出力される
というものとなっている。
val osName: SettingKey[String] = SettingKey[String]("osName")
osName := (System.getProperty("os.name") match {
case name if name.startsWith("Linux") => "linux"
case name if name.startsWith("Mac") => "mac"
case name if name.startsWith("Windows") => "win"
case _ => throw new Exception("Unknown platform!")
})
libraryDependencies += "org.openjfx" % "javafx-base" % "11-ea+25" classifier osName.value
libraryDependencies += "org.openjfx" % "javafx-controls" % "11-ea+25" classifier osName.value
libraryDependencies += "org.openjfx" % "javafx-fxml" % "11-ea+25" classifier osName.value
libraryDependencies += "org.openjfx" % "javafx-graphics" % "11-ea+25" classifier osName.value
libraryDependencies += "org.openjfx" % "javafx-web" % "11-ea+25" classifier osName.value
プログラミング入門コースにおいて、【 Node.js の package.json にライブラリ依存関係を記述しさえすれば、自動的に npm install で必要なライブラリがダウンロードされ、ローカルにインストールされる】という仕組みを学んだjが、 sbt でも同様のことが可能。
このような実装で、 GUI のアプリケーションを作ることができる。
【実習】単一のアプリケーションとしてビルドする
さらに、先ほど作った GUI のアプリケーションを、単一のファイルとして配布できる形にビルドしてみよう。
project/assembly.sbt
というファイルを作成して、
addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "0.14.8")
以上のように記述しよう。
次に、 build.sbt
に次のような追記をする。
assemblyMergeStrategy in assembly := {
case PathList("module-info.class") => MergeStrategy.first
case x => (assemblyMergeStrategy in assembly).value(x)
}
その後、 sbt を起動して、
> assembly
assembly
コマンドを実行してみよう。
初めてのビルドだと、必要なモジュールのダウンロードに数分が必要となる。
なお先ほど追加した assembly.sbt
は、 sbt-assembly という sbt のプラグインの設定ファイル。
sbt-assembly は、コンパイルをすることで、 Scala を含めた依存関係のあるライブラリをすべて単一の .jar という拡張子のファイルにまとめてビルドしてくれる。
なお、このコンパイルが終了すると、 target/scala-2.13/scala-gui-assembly-0.1.0-SNAPSHOT.jar
というファイルが作成される。
この .jar で終わるファイルは、 Java の実行ファイルとなる。
コマンドラインからも実行できる他、 Windows のエクスプローラー、 Mac のファインダーなどで表示させてダブルクリックすることで実行することもできる。
次に、 target/scala-2.13/
フォルダを開いて、 scala-gui-assembly-0.1.0-SNAPSHOT.jar
をダブルクリックしてみよう。
ダブルクリックしてアプリケーションを起動できただろうか。
ただ、コンソールから起動しているわけではないので、ボタンを押しても何も表示されることはない。
scala-gui-assembly-0.1.0-SNAPSHOT.jar
を別なフォルダにコピーしたり、名前を変更しても同様に起動できる。
このような方法で Scala のアプリケーションを単一のファイルとしてビルドできる。
【実習】アニメーションを表示するアプリケーションの実装
今度は、アニメーションを行うアプリケーションを作成しよう。
まずホームディレクトリの workspace
フォルダに scala-animation
というフォルダを作成して、フォルダ構成を作っていく。
cd ~
mkdir -p workspace/scala-animation/src/main/scala
cd workspace/scala-animation
mkdir project
以上のコマンドを実行して、フォルダ構成を作成した後に、プロジェクトフォルダである、 scala-animation
フォルダに移動しよう。
次に以下の3つのファイルを作成する。
なお build.sbt
と project/assembly.sbt
の2つのファイルに関しては、 scala-gui
と同じ内容であるため、そちらからコピーしてきても OK 。
$ cp ../scala-gui/build.sbt build.sbt`
$ cp ../scala-gui/project/assembly.sbt project/assembly.sbt
import java.util.function.Consumer
import javafx.animation.{KeyFrame, KeyValue, Timeline}
import javafx.application.Application
import javafx.scene.{Group, Node, Scene}
import javafx.scene.paint.Color
import javafx.scene.shape.{Circle, StrokeType}
import javafx.stage.Stage
import javafx.util.Duration
import java.lang.Math.random
object Main extends App {
Application.launch(classOf[Main], args: _*)
}
class Main extends Application {
override def start(primaryStage: Stage): Unit = {
val root = new Group()
val scene = new Scene(root, 800, 600, Color.BLACK)
primaryStage.setScene(scene)
val circles = new Group()
for (i <- 1 to 30) {
val circle = new Circle(150, Color.web("white", 0.05))
circle.setStrokeType(StrokeType.OUTSIDE)
circle.setStroke(Color.web("white", 0.16))
circle.setStrokeWidth(4)
circles.getChildren().add(circle)
}
root.getChildren().add(circles)
val timeline = new Timeline()
circles.getChildren().forEach(new Consumer[Node] {
override def accept(circle: Node): Unit = {
timeline.getKeyFrames().addAll(
new KeyFrame(Duration.ZERO,
new KeyValue(circle.translateXProperty(), random() * 800: Number),
new KeyValue(circle.translateYProperty(), random() * 600: Number)
),
new KeyFrame(new Duration(40000),
new KeyValue(circle.translateXProperty(), random() * 800: Number),
new KeyValue(circle.translateYProperty(), random() * 600: Number)
)
)
}
})
timeline.play()
primaryStage.show()
}
}
無事以上のファイルの作成が終わったら、 sbt run
を実行してコンパイルと実行がうまくいけば成功。
おそらく 30 個の白い円が重なりながら移動するアニメーションが表示されたのではないだろうか。
【講義】アニメーションを表示するアプリケーションの解説
先ほどのアニメーションのコードを説明する。
build.sbt
と project/assembly.sbt
は同じなので説明を省いて、 src/main/scala/Main.scala
を説明する。
先ほどと大きく変わっているのは、
class Main extends Application {
override def start(primaryStage: Stage): Unit = {
val root = new Group()
val scene = new Scene(root, 800, 600, Color.BLACK)
primaryStage.setScene(scene)
val circles = new Group()
for (i <- 1 to 30) {
val circle = new Circle(150, Color.web("white", 0.05))
circle.setStrokeType(StrokeType.OUTSIDE)
circle.setStroke(Color.web("white", 0.16))
circle.setStrokeWidth(4)
circles.getChildren().add(circle)
}
root.getChildren().add(circles)
val timeline = new Timeline()
circles.getChildren().forEach(new Consumer[Node] {
override def accept(circle: Node): Unit = {
timeline.getKeyFrames().addAll(
new KeyFrame(Duration.ZERO,
new KeyValue(circle.translateXProperty(), random() * 800: Number),
new KeyValue(circle.translateYProperty(), random() * 600: Number)
),
new KeyFrame(new Duration(40000),
new KeyValue(circle.translateXProperty(), random() * 800: Number),
new KeyValue(circle.translateYProperty(), random() * 600: Number)
)
)
}
})
timeline.play()
primaryStage.show()
}
}
以上の Main クラスの定義。
val root = new Group()
val scene = new Scene(root, 800, 600, Color.BLACK)
primaryStage.setScene(scene)
ここでは背景が黒の幅 800 ピクセル、高さ 600 ピクセルのエリアを作成している。
val circles = new Group()
for (i <- 1 to 30) {
val circle = new Circle(150, Color.web("white", 0.05))
circle.setStrokeType(StrokeType.OUTSIDE)
circle.setStroke(Color.web("white", 0.16))
circle.setStrokeWidth(4)
circles.getChildren().add(circle)
}
root.getChildren().add(circles)
この実装では、円を管理する circles という Group クラスのオブジェクトを作り、そこに縁と中身の色が白でそれぞれ透明度が設定された円のオブジェクトを作成して、 circles に追加し、それを更に、 root という一番基底となるグループに追加している。
val timeline = new Timeline()
circles.getChildren().forEach(new Consumer[Node] {
override def accept(circle: Node): Unit = {
timeline.getKeyFrames().addAll(
new KeyFrame(Duration.ZERO,
new KeyValue(circle.translateXProperty(), random() * 800: Number),
new KeyValue(circle.translateYProperty(), random() * 600: Number)
),
new KeyFrame(new Duration(40000),
new KeyValue(circle.translateXProperty(), random() * 800: Number),
new KeyValue(circle.translateYProperty(), random() * 600: Number)
)
)
}
})
timeline.play()
以上がアニメーションの実装。
実装に使われている技術は今の知識では理解できないので、簡単に説明する。
ここでは Timeline というクラスでアニメーションを定義し、各円すべてに対してアニメーション開始 0 秒時点でランダムな位置にポジショニングした後、アニメーションが開始した後 40 秒経過した時点で、別なランダムな位置に移動完了するという設定をしている。
なおここでは Consumer クラスを継承して匿名内部クラスを作るという方法が利用されているが、このような「匿名内部クラス」についてはまた次回以降、詳しく説明していく。
簡単にいうとクラスを拡張して、その場で挙動の違うオブジェクトが作成できる機能。
まだこの回では完全に理解する必要はない。
【実習】起動引数で動きを変更する
Scala ではビルドして jar ファイルなどでプログラムを配布してしまった場合は簡単にその挙動を変えられない。
そこで挙動を変えたい場合には、その部分を設定ファイルとしたり、起動引数で柔軟に変えられるようにしたりして実装する。
ここでは、先ほど作成した scala-animation
で表示できる円の数を、起動引数で変更できるようにしてみよう。
以下のように変更してみよう。
primaryStage.setScene(scene)
val circles = new Group()
val circleNum = getParameters.getNamed.getOrDefault("num", "30").toInt
for (i <- 1 to circleNum) {
val circle = new Circle(150, Color.web("white", 0.05))
circle.setStrokeType(StrokeType.OUTSIDE)
circle.setStroke(Color.web("white", 0.16))
無事実装できたら、 sbt を起動して、 assembly コマンドで jar ファイルをビルドしてみよう。
コンパイルが成功したら sbt を終了し、ターミナルで
/Library/Internet\ Plug-Ins/JavaAppletPlugin.plugin/Contents/Home/bin/java -jar target/scala-2.13/scala-animation-assembly-0.1.0-SNAPSHOT.jar --num=100
表示される円の数を 100 に増やして実行してみよう。
無事円の数が増えていれば成功。
円の数を 10 や 1000 に変えて実験してみよう。
なお、
val circleNum = getParameters.getNamed.getOrDefault("num", "30").toInt
以上の実装は、コマンドライン引数から --num=100
のような形式で引数を受け取り、もし引数がなかった場合には "30" という文字列で受け取って、それを整数に変換して circleNum という名前の定数に定義する、という実装となる。
このように起動引数は、ソフトウェアで少し柔軟に変えたい部分を作成するのに非常に便利な機能となっている。
まとめ
- JavaFX を利用すると Scala で GUI のアプリケーションを開発できる
- sbt-assembly という sbt のプラグインを利用して、単一のファイルにビルドされたアプリケーションを利用できる
- 一度ビルドされてしまったアプリケーションは変更しづらいため、柔軟性が必要な部分には設定ファイルやコマンドライン引数を利用する
挑戦
初級
for (i <- 1 to circleNum) {
val circleColor = getParameters.getNamed.getOrDefault("color", "white")
val circle = new Circle(150, Color.web(circleColor, 0.05))
中級
object Main extends App {
println(args.map(_.toInt).sum)
}
以上のように実装し、 sbt assembly
の後に、ターミナルで
$ /Library/Internet\ Plug-Ins/JavaAppletPlugin.plugin/Contents/Home/bin/java -jar target/scala-2.13/scala-sum-assembly-0.1.0-SNAPSHOT.jar 25 25 50
100
以上のようなコマンドライン引数で確認できる。
上級
import javafx.application.Application
import javafx.scene.Scene
import javafx.scene.web.HTMLEditor
import javafx.stage.Stage
object Main extends App {
Application.launch(classOf[Main], args: _*)
}
class Main extends Application {
override def start(primaryStage: Stage): Unit = {
primaryStage.setTitle("HTMLEditor Sample")
primaryStage.setWidth(650)
primaryStage.setHeight(300)
val htmlEditor = new HTMLEditor()
htmlEditor.setPrefHeight(245)
val scene = new Scene(htmlEditor)
primaryStage.setScene(scene)
primaryStage.show()
}
}
scalaVersion := "2.12.7"
scalacOptions ++= Seq("-deprecation", "-feature", "-unchecked", "-Xlint")
val osName: SettingKey[String] = SettingKey[String]("osName")
osName := (System.getProperty("os.name") match {
case name if name.startsWith("Linux") => "linux"
case name if name.startsWith("Mac") => "mac"
case name if name.startsWith("Windows") => "win"
case _ => throw new Exception("Unknown platform!")
})
libraryDependencies += "org.openjfx" % "javafx-base" % "11-ea+25" classifier osName.value
libraryDependencies += "org.openjfx" % "javafx-controls" % "11-ea+25" classifier osName.value
libraryDependencies += "org.openjfx" % "javafx-fxml" % "11-ea+25" classifier osName.value
libraryDependencies += "org.openjfx" % "javafx-graphics" % "11-ea+25" classifier osName.value
libraryDependencies += "org.openjfx" % "javafx-web" % "11-ea+25" classifier osName.value
libraryDependencies += "org.openjfx" % "javafx-media" % "11-ea+25" classifier osName.value
assemblyMergeStrategy in assembly := {
case PathList("module-info.class") => MergeStrategy.first
case x => (assemblyMergeStrategy in assembly).value(x)
}
以上のように実装することで、 HTML エディタを実装できる。