ScalaFXでGUIアプリケーションを作ってみる
「JavaFXでGUIアプリケーションを作ってみる」、「JavaFX+FXMLでGUIアプリケーションを作ってみる」では、JavaFXを使って簡単なGUIアプリケーションを作った。 ここでは、JVMベースの言語であるScalaとScalaFXライブラリを用いて、同様のアプリケーションを作ってみる。
環境
Windows 8.1 Pro 64 bit版、JDK 8、Eclipse Mars 2
>systeminfo
OS 名: Microsoft Windows 8.1 Pro
OS バージョン: 6.3.9600 N/A ビルド 9600
OS ビルドの種類: Multiprocessor Free
システムの種類: x64-based PC
プロセッサ: 1 プロセッサインストール済みです。
[01]: Intel64 Family 6 Model 69 Stepping 1 GenuineIntel ~1596 Mhz
ScalaおよびScalaFXの概要
Scalaは、Javaバイトコードにコンパイルするタイプのプログラミング言語である。 Scalaの特長として型推論、第一級関数およびlambda式のサポート、パターンマッチが挙げられ、Javaコードと同じ意味を持つコードをより簡潔に書くことができる。
// Java List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5); List<Integer> squares = numbers.stream().map(x -> x * x).collect(Collectors.toList());
// Scala val numbers = 1 to 5 val squares = numbers.map(x => x * x)
ScalaFXは、JavaFXをScalaから扱うためのライブラリである。 Scalaから直接JavaFXを扱うことも可能だが、ScalaFXを用いることでより自然な形でコードを書くことができる。
Scalaのインストール
まず、Scalaをインストールする。
ダウンロードページからWindows用MSIインストーラをダウンロードし、実行する。 ここで、PATH環境変数への追加はインストーラが行ってくれる。
REPLを起動し、簡単なコードを動かしてみる。
>scala
Welcome to Scala 2.11.8 (Java HotSpot(TM) 64-Bit Server VM, Java 1.8.0_91).
Type in expressions for evaluation. Or try :help.
scala> println("Hello, Scala!")
Hello, Scala!
scala> for (x <- 1 to 25 if x*x > 50) yield 2*x
res1: scala.collection.immutable.IndexedSeq[Int] = Vector(16, 18, 20, 22, 24, 26
, 28, 30, 32, 34, 36, 38, 40, 42, 44, 46, 48, 50)
二つ目のfor文は、Pythonにおけるリスト内包表記に相当するコードである。
sbtのインストール
次に、Scalaにおけるビルドツールsbtをインストールする。
ダウンロードページからWindows用MSIインストーラをダウンロードし、実行する。 Scala同様に、PATH環境変数への追加はインストーラが行ってくれる。
次のようなHelloWorld.scalaファイルを作り、ビルド実行を行ってみる。
なお、初回起動時は必要なライブラリのインストールが行われるため、しばらく時間がかかる。
object HelloWorld {
def main(args: Array[String]) = println("Hello, Scala!")
}
>sbt
Java HotSpot(TM) 64-Bit Server VM warning: ignoring option MaxPermSize=256m; sup
port was removed in 8.0
Getting org.fusesource.jansi jansi 1.11 ...
downloading https://repo1.maven.org/maven2/org/fusesource/jansi/jansi/1.11/jansi
-1.11.jar ...
[SUCCESSFUL ] org.fusesource.jansi#jansi;1.11!jansi.jar (1188ms)
:: retrieving :: org.scala-sbt#boot-jansi
confs: [default]
1 artifacts copied, 0 already retrieved (111kB/31ms)
Getting org.scala-sbt sbt 0.13.11 ...
(snip)
> run
[info] Updating {file:/C:/Users/user/tmp/}tmp...
[info] Resolving org.fusesource.jansi#jansi;1.4 ...
[info] Done updating.
[info] Compiling 1 Scala source to C:\Users\user\tmp\target\scala-2.10\classes..
.
[info] 'compiler-interface' not yet compiled for Scala 2.10.6. Compiling...
[info] Compilation completed in 12.718 s
[info] Running HelloWorld
Hello, Scala!
[success] Total time: 24 s, completed 2016/04/28 9:14:54
>[Ctrl+D]
ScalaFXのダウンロード
次に、ScalaFXをダウンロードする。
Quick-start guideを参照し、sbtを利用してJARファイルをダウンロードする。
具体的には、次のようなbuild.sbtファイルを用意し、上のHelloWorld.scalaをsbtから実行することで、依存ライブラリとしてJARファイルのダウンロードが行われる。
scalaVersion := "2.11.8" libraryDependencies ++= Seq( "org.scalafx" %% "scalafx" % "8.0.92-R10" )
>sbt
Java HotSpot(TM) 64-Bit Server VM warning: ignoring option MaxPermSize=256m; sup
port was removed in 8.0
[info] Set current project to tmp (in build file:/C:/Users/user/tmp/)
> run
[info] Updating {file:/C:/Users/user/tmp/}tmp...
[info] Resolving org.fusesource.jansi#jansi;1.4 ...
[info] downloading https://repo1.maven.org/maven2/org/scalafx/scalafx_2.10/8.0.9
2-R10/scalafx_2.10-8.0.92-R10.jar ...
[info] [SUCCESSFUL ] org.scalafx#scalafx_2.10;8.0.92-R10!scalafx_2.10.jar (9627
ms)
[info] Done updating.
[info] Running HelloWorld
Hello, Scala!
[success] Total time: 13 s, completed 2016/04/28 9:16:30
>[Ctrl+D]
今回の環境では、JARファイルはC:\Users\%USERNAME%\.ivy2\cache\org.scalafx\scalafx_2.11\jars\scalafx_2.11-8.0.92-R10.jarに保存された。
Eclipseプラグイン(Scala IDE for Eclipse)のインストール
Eclipseを用いてScalaコードを書くために、Scala用のEclipseプラグインをインストールする。
まず、Eclipseを起動し、「Help」→「Install new software」を開く。 ダウンロードページに書いてあるUpdate siteのURLを指定し、「Scala IDE for Eclipse」を選択、インストールする。 ダイアログの問いに合わせて再起動するとScala IDE向けに設定を変更するように問われるが、ここでは変更せずそのままの状態にした。
ScalaFXでGUIアプリケーションを作ってみる
「Java+SwingでGUIアプリケーションを作ってみる」と同様に、ドラッグアンドドロップされたファイルのSHA-1ハッシュ値をテーブルに表示するアプリケーションを書いてみる。
Eclipseを起動した後、ツールバーから「New」→「Other」を開き、「Scala Wizards」→「Scala Project」を選択する。 プロジェクト名を「SHA1CalculatorSFX」としてNextを押し、出てくる画面の「Libraries」→「Add External JARs」から上でダウンロードしたScalaFXのJARファイルを依存ライブラリに加えてFinishを押す。 ここで、Scala perspectiveへの切り替えを問われるので、Yesを選択する。
続けて、ツールバーの「New」→「Scala Class」から「main.scala.SHA1CalculatorSFX」クラスを作成する。 なお、パッケージ名を「main.scala」にするのは、後述するJARファイルの作成時にsbtがこれを標準で参照するためである。
SHA1CalculatorSFX.scalaを開き、実際にコードを書くと次のようになる。
package main.scala
// enables conversions by .asScala and .asJava
// http://docs.scala-lang.org/overviews/collections/conversions-between-java-and-scala-collections
import collection.JavaConverters._
// use JavaFX controls that aren't wrapped by ScalaFX
// http://www.scalafx.org/docs/faq_Using_unwrapped_JavaFX_components/
import scalafx.Includes._
import java.io.File
import java.io.FileInputStream
import java.io.IOException
import java.security.MessageDigest
import javafx.{concurrent => jfxc}
import scalafx.application.JFXApp
import scalafx.application.JFXApp.PrimaryStage
import scalafx.beans.property.StringProperty
import scalafx.collections.ObservableBuffer
import scalafx.concurrent.Task
import scalafx.scene.Scene
import scalafx.scene.control.TableColumn
import scalafx.scene.control.TableView
import scalafx.scene.control.cell.TextFieldTableCell
import scalafx.scene.input.DragEvent
import scalafx.scene.input.TransferMode
import scalafx.scene.layout.BorderPane
class FileInfo(path_ : String, sha1sum_ : String) {
val path = new StringProperty(this, "path", path_)
val sha1sum = new StringProperty(this, "sha1sum", sha1sum_)
}
object SHA1CalculatorSFX extends JFXApp {
private val pane = new BorderPane()
stage = new PrimaryStage {
scene = new Scene(600, 400) {
title = "SHA-1 Calculator SFX"
root = pane
}
}
configureTable(pane)
private def configureTable(root: BorderPane) = {
val data = ObservableBuffer[FileInfo]()
val table = createTableView(data)
table.columnResizePolicy = TableView.ConstrainedResizePolicy
root.center = table
}
private def createTableView(data: ObservableBuffer[FileInfo]) = {
new TableView[FileInfo] {
editable = true
columns ++= List(
new TableColumn[FileInfo, String] {
text = "File Path"
cellValueFactory = { _.value.path }
cellFactory = TextFieldTableCell.forTableColumn[FileInfo]()
}.delegate,
new TableColumn[FileInfo, String] {
text = "sha1sum"
cellValueFactory = { _.value.sha1sum }
cellFactory = TextFieldTableCell.forTableColumn[FileInfo]()
}.delegate
)
items = data
onDragOver = (event: DragEvent) => {
val db = event.getDragboard()
if (db.hasFiles()) {
event.acceptTransferModes(TransferMode.COPY)
} else {
event.consume()
}
}
onDragDropped = (event: DragEvent) => {
val db = event.getDragboard()
var success = false
if (db.hasFiles()) {
success = true
for (file <- db.getFiles().asScala) {
val fileinfo = new FileInfo(file.getAbsolutePath(), "")
data.add(fileinfo)
val task = new SHA1CalculationTask(fileinfo)
fileinfo.sha1sum <== task.message
new Thread(task).start()
}
}
event.setDropCompleted(success)
event.consume()
}
}
}
}
class SHA1CalculationTask(fileinfo: FileInfo) extends Task(new jfxc.Task[Unit] {
protected def call() = {
val md = MessageDigest.getInstance("SHA-1")
// update digest with each 8MB chunk
val path = fileinfo.path.value
val istream = new FileInputStream(path)
val totalBytes = (new File(path)).length()
var currentBytes = 0L
try {
val buf = new Array[Byte](8*1024*1024)
Iterator.continually(istream.read(buf, 0, buf.length))
.takeWhile(_ != -1)
.foreach { n =>
md.update(buf.slice(0, n))
currentBytes += n
val message = "calculating %d%%...".format(100 * currentBytes / totalBytes)
updateMessage(message)
}
} catch {
case e: IOException => e.printStackTrace()
} finally {
istream.close()
}
val hexdigest = md.digest().map("%02x".format(_)).mkString
updateMessage(hexdigest)
}
})
最初の二つのimport文は、JavaクラスとScalaクラスの変換を行う上で重要なため、常にimportしておくとよい。 Scalaにおいて、valは再代入できない変数(const変数)、varは再代入可能な変数の宣言を意味する。 また、Scalaでは行末のセミコロンを省略することができ、一般に書かないことが多い。
「JavaFXでGUIアプリケーションを作ってみる」のコードと比較すると、次のような箇所でコードがより簡潔に書けることがわかる。
// Java
byte[] digest = md.digest();
StringBuilder sb = new StringBuilder();
for (byte b : digest) {
sb.append(String.format("%02x", b));
}
// Scala
val hexdigest = md.digest().map("%02x".format(_)).mkString
また、ScalaFXを用いることで、JavaFXにおけるPropertyのバインディングをより直感的に書くことができる。
// Java fileinfo.sha1sumProperty().bind(task.messageProperty());
// Scala fileinfo.sha1sum <== task.message
なお、ScalaFXにおいてTaskクラスを使う際は、scalafx.concurrent.Taskに加えjavafx.concurrent.Taskもimportして使う必要がある。
コードを保存した後「Run」を押して実行し、適当にファイルをドラッグアンドドロップした後のスクリーンショットを次に示す。

JavaFXで書いた場合と同じように動作していることが確認できる。
実行可能なJARファイルを作ってみる
Scalaで実行可能なJARファイルを作成する際は、Eclipseからではなく、sbtとsbt-assemblyプラグインを用いて行う。
まずはじめに、JAVA_HOME環境変数をC:\Program Files\Java\jdk1.8.0_91として作成しておく。
次に、プロジェクトフォルダを開き、project\assembly.sbtを次のような内容で作成する。
これにより、sbt-assemblyプラグインが有効になる。
addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "0.14.3")
そして、ビルド設定ファイルbuild.sbtを次のような内容で作成する。
name := "SHA-1 Calculator SFX"
version := "1.0.0"
scalaVersion := "2.11.8"
unmanagedJars in Compile += Attributed.blank(file(System.getenv("JAVA_HOME") + "/jre/lib/ext/jfxrt.jar"))
libraryDependencies ++= Seq(
"org.scalafx" %% "scalafx" % "8.0.92-R10"
)
fork := true
JavaFXを利用するには、unmanagedJarsにjfxrt.jarを追加する必要があることに注意。
最後に、コマンドプロンプトから次のコマンドを実行してJARファイルを作成する。
>sbt assembly
Java HotSpot(TM) 64-Bit Server VM warning: ignoring option MaxPermSize=256m; sup
port was removed in 8.0
[info] Loading project definition from C:\Users\user\workspace\SHA1CalculatorSFX
\project
[info] Set current project to SHA-1 Calculator SFX (in build file:/C:/Users/user
/workspace/SHA1CalculatorSFX/)
[info] Updating {file:/C:/Users/user/workspace/SHA1CalculatorSFX/}sha1calculator
sfx...
[info] Resolving org.scala-sbt.ivy#ivy;2.3.0-sbt-2cc8d2761242b072cedb0a04cb39435
[info] Resolving org.fusesource.jansi#jansi;1.4 ...
[info] downloading https://repo.scala-sbt.org/scalasbt/sbt-plugin-releases/com.e
ed3si9n/sbt-assembly/scala_2.10/sbt_0.13/0.14.3/jars/sbt-assembly.jar ...
[info] [SUCCESSFUL ] com.eed3si9n#sbt-assembly;0.14.3!sbt-assembly.jar (5117ms)
(snip)
[info] Done updating.
[info] Compiling 1 Scala source to C:\Users\user\workspace\SHA1CalculatorSFX\tar
get\scala-2.11\classes...
[warn] there was one deprecation warning; re-run with -deprecation for details
[warn] one warning found
[info] Including: scalafx_2.11-8.0.92-R10.jar
[info] Including: scala-reflect-2.11.8.jar
[info] Including: scala-library-2.11.8.jar
[info] Including: jfxrt.jar
[info] Checking every *.class/*.jar file's SHA-1.
[info] Merging files...
[warn] Merging 'com\sun\webkit\network\about' with strategy 'rename'
[warn] Merging 'META-INF\INDEX.LIST' with strategy 'discard'
[warn] Merging 'META-INF\MANIFEST.MF' with strategy 'discard'
[warn] Strategy 'discard' was applied to 2 files
[warn] Strategy 'rename' was applied to a file
[info] SHA-1: a3f61d268f0ea843866218070f9a3e97d0ce749c
[info] Packaging C:\Users\user\workspace\SHA1CalculatorSFX\target\scala-2.11\SHA
-1 Calculator SFX-assembly-1.0.0.jar ...
[info] Done packaging.
[success] Total time: 90 s, completed 2016/04/29 3:27:46
生成されたJARファイルを実行すると、上と同様のウィンドウが表示されることが確認できる。 なお、Scala用のライブラリも必要とすることから、Javaで作った場合よりファイルサイズは大きくなる。
関連リンク
- Scala | mwSoft
- Custom cells - ScalaFX • simpler way to use JavaFX from Scala
- ProScalaFX/WorkerAndTaskExample.scala at master · scalafx/ProScalaFX
- Iterator.continually()を使おう - kmizuの日記
- How to deploy a Scala project from Eclipse? - Stack Overflow
- Build a JavaFX 8 app with sbt
- mog project: Scala: Tackling with sbt-assembly