「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