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は、JavaFXScalaから扱うためのライブラリである。 Scalaから直接JavaFXを扱うことも可能だが、ScalaFXを用いることでより自然な形でコードを書くことができる。

Scalaのインストール

まず、Scalaをインストールする。

ダウンロードページからWindowsMSIインストーラをダウンロードし、実行する。 ここで、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をインストールする。

ダウンロードページからWindowsMSIインストーラをダウンロードし、実行する。 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」を押して実行し、適当にファイルをドラッグアンドドロップした後のスクリーンショットを次に示す。

f:id:inaz2:20160429033620p:plain

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で作った場合よりファイルサイズは大きくなる。

関連リンク

「Self Introduction & The Story that I Tried to Make Sayonara ROP Chain in Linux」というタイトルで発表した

Lightning TalkでLow Layer経験に関する自己紹介とSayonara ROP ChainLinuxで作ろうとした話について発表した。

内容はAVTOKYO 2014での発表と重複するが、改めて自分のやろうとしたことと結果を整理した。 デモの準備もしていたのだけど、忘れてしまったのが残念。 別に準備していたネタもあったのだけど、そちらは直前で差し替えることに決め、お蔵入りになった。

質疑応答の内容も参考になった。 ありがとうございました。

JavaFX+FXMLでGUIアプリケーションを作ってみる

「JavaFXでGUIアプリケーションを作ってみる」では、JavaFXによるGUIアプリケーションを作った。 JavaFXではコードから直接GUI画面を構築するほかに、FXMLと呼ばれるXMLファイルを使って構築することもでき、より見通しのよいコードを書くことができる。 ここでは、JavaFX+FXMLを使い、同様のGUIアプリケーションを作ってみる。

環境

Windows 8.1 Pro 64 bit版、JDK 8、Eclipse Mars 2 + e(fx)clipse

>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

Scene Builderのインストール

FXMLでGUI画面を構築する際、おおまかなレイアウトに関してはGUIツールScene Builderを使って編集すると楽である。 ここでは、e(fx)clipseとも連携できるexe版のScene Builderをインストールする。

まず、プロジェクトページから「Windows Installer (x64)」をダウンロードする。 インストーラを実行すると、プログラム一式は自動的にC:\Users\%USERNAME%\AppData\Local\SceneBuilderにインストールされる。

インストールが完了したら、続けてEclipseと連携できるように設定を行う。 メニューの「Window」→「Preference」を開き、「JavaFX」を選択して「SceneBuilder executable」にSceneBuilder.exeのパスを指定する。

JavaFX+FXMLでGUIアプリケーションを作ってみる

「Java+SwingでGUIアプリケーションを作ってみる」と同様に、ドラッグアンドドロップされたファイルのSHA-1ハッシュ値をテーブルに表示するアプリケーションを書いてみる。

まず、ツールバーから「New」→「Project」を開き、「JavaFX」→「JavaFX Project」を選択する。 プロジェクト名を「SHA1CalculatorFXML」としてNextを2回を押すとDeclative UIを選択できる画面が表示されるので、Languageを「FXML」に変更し、ファイル名、コントローラ名をそれぞれ「MainWindow」「MainWindowController」に指定してFinishを押す。

src/applicaion/MainWindow.fxmlを右クリックして「Open with SceneBuilder」を開くとScene Builderが起動するので、BorderPaneの中央にTableViewを配置し、カラム名をそれぞれ編集する。 さらにTableViewのプロパティを開いてeditableにチェックを入れ、ドラッグアンドドロップを有効にするためにfx:idにTableViewの変数名、onDragOver、onDragDroppedに適当な関数名を入力しておく。 このときのスクリーンショットを次に示す。

f:id:inaz2:20160427024712p:plain

編集が終わったら、保存して終了する。

これに他のコードの編集も交えながら手作業での修正を加えると、最終的には次のようなコードとなる。

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

<?import javafx.collections.FXCollections?>
<?import javafx.scene.control.TableColumn?>
<?import javafx.scene.control.TableView?>
<?import javafx.scene.control.cell.PropertyValueFactory?>
<?import javafx.scene.layout.BorderPane?>

<BorderPane xmlns="http://javafx.com/javafx/8.0.65" xmlns:fx="http://javafx.com/fxml/1" fx:controller="application.MainWindowController">
    <center>
        <TableView fx:id="tableView" editable="true" onDragDropped="#handleDragDropped" onDragOver="#handleDragOver">
            <columnResizePolicy>
                <TableView fx:constant="CONSTRAINED_RESIZE_POLICY" />
            </columnResizePolicy>
            <columns>
                <TableColumn fx:id="filePathCol" text="File Path">
                    <cellValueFactory><PropertyValueFactory property="path" /></cellValueFactory>
                </TableColumn>
                <TableColumn fx:id="sha1sumCol" text="sha1sum">
                    <cellValueFactory><PropertyValueFactory property="sha1sum" /></cellValueFactory>
                </TableColumn>
            </columns>
            <items>
                <FXCollections fx:factory="observableArrayList" />
            </items>
        </TableView>
    </center>
</BorderPane>

新しいクラスとしてFileInfoを作成し、src/applicaion/FileInfoにテーブルの各行を表すクラスを記述する。

package application;

import javafx.beans.property.SimpleStringProperty;

public class FileInfo {
    private final SimpleStringProperty path = new SimpleStringProperty();
    private final SimpleStringProperty sha1sum = new SimpleStringProperty();

    public FileInfo(String path, String sha1sum) {
        setPath(path);
        setSha1sum(sha1sum);
    }

    public String getPath() {
        return path.get();
    }

    public void setPath(String newPath) {
        path.set(newPath);
    }

    public String getSha1sum() {
        return sha1sum.get();
    }

    public void setSha1sum(String newSha1sum) {
        sha1sum.set(newSha1sum);
    }

    public SimpleStringProperty sha1sumProperty() {
        return sha1sum;
    }
}

FXMLファイルに対応するコントローラとなるsrc/applicaion/MainWindowController.javaを書くと、次のようになる。

package application;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;

import javafx.collections.ObservableList;
import javafx.concurrent.Task;
import javafx.fxml.FXML;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.control.cell.TextFieldTableCell;
import javafx.scene.input.DragEvent;
import javafx.scene.input.Dragboard;
import javafx.scene.input.TransferMode;

public class MainWindowController {
    @FXML private TableView<FileInfo> tableView;
    @FXML private TableColumn<FileInfo, String> filePathCol;
    @FXML private TableColumn<FileInfo, String> sha1sumCol;

    @FXML private void initialize() {
        filePathCol.setCellFactory(TextFieldTableCell.forTableColumn());
        sha1sumCol.setCellFactory(TextFieldTableCell.forTableColumn());
    }

    public void handleDragOver(DragEvent event) {
        Dragboard db = event.getDragboard();
        if (db.hasFiles()) {
            event.acceptTransferModes(TransferMode.COPY);
        } else {
            event.consume();
        }
    }

    public void handleDragDropped(DragEvent event) {
        ObservableList<FileInfo> data = tableView.getItems();
        Dragboard db = event.getDragboard();
        boolean success = false;
        if (db.hasFiles()) {
            success = true;
            for (File file : db.getFiles()) {
                FileInfo fileinfo = new FileInfo(file.getAbsolutePath(), "");
                data.add(fileinfo);
                SHA1CalculationTask task = new SHA1CalculationTask(fileinfo);
                fileinfo.sha1sumProperty().bind(task.messageProperty());
                new Thread(task).start();
            }
        }
        event.setDropCompleted(success);
        event.consume();
    }

    class SHA1CalculationTask extends Task<Void> {
        private final FileInfo fileinfo;

        public SHA1CalculationTask(FileInfo fileinfo) {
            this.fileinfo = fileinfo;
        }

        @Override
        protected Void call() throws Exception {
            MessageDigest md = null;
            try {
                md = MessageDigest.getInstance("SHA-1");
            } catch (NoSuchAlgorithmException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }

            // update digest with each 8MB chunk
            String path = fileinfo.getPath();
            long totalBytes = (new File(path)).length();
            long currentBytes = 0;
            try (FileInputStream istream = new FileInputStream(path)) {
                byte[] buf = new byte[8*1024*1024];
                int n;
                while ((n = istream.read(buf, 0, buf.length)) != -1) {
                    md.update(Arrays.copyOf(buf, n));
                    currentBytes += n;
                    String message = String.format("calculating %d%%...", 100 * currentBytes / totalBytes);
                    updateMessage(message);
                }
            } catch (IOException e) {
                e.printStackTrace();
            }

            byte[] digest = md.digest();
            StringBuilder sb = new StringBuilder();
            for (byte b : digest) {
                sb.append(String.format("%02x", b));
            }
            updateMessage(sb.toString());
            return null;
        }
    }
}

@FXMLのついたメンバ宣言は、FXMLでfx:idを指定した要素と対応する。 また、CellFactoryについてはTextFieldTableCell.forTableColumn()の結果を用いるため、initializeメソッドを定義してコードから指定を行っている。

src/applicaion/Main.javaを編集し、ウィンドウタイトルの指定を追加しておく。

package application;

import javafx.application.Application;
import javafx.stage.Stage;
import javafx.scene.Scene;
import javafx.scene.layout.BorderPane;
import javafx.fxml.FXMLLoader;


public class Main extends Application {
    @Override
    public void start(Stage primaryStage) {
        try {
            primaryStage.setTitle("SHA-1 Calculator FXML");

            BorderPane root = (BorderPane)FXMLLoader.load(getClass().getResource("MainWindow.fxml"));
            Scene scene = new Scene(root,600,400);  // expanded default size
            scene.getStylesheets().add(getClass().getResource("application.css").toExternalForm());
            primaryStage.setScene(scene);
            primaryStage.show();
        } catch(Exception e) {
            e.printStackTrace();
        }
    }

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

ここで、それぞれのファイルの役割を整理すると、次のようになる。

  • Main.java: FXMLのロード
  • MainWindow.fxml: GUI画面の定義
  • MainWindowController.java: GUI画面でのイベントを処理
  • FileInfo.java: データモデルの定義

コードを保存した後「Run」を押して実行し、適当にファイルをドラッグアンドドロップした後のスクリーンショットを次に示す。

f:id:inaz2:20160426084128p:plain

コードから直接GUI画面を構築した場合と同じユーザインタフェースになっていることが確認できる。

実行可能なJARファイルを作ってみる

build.fxbuildファイルを開き、表示されるフォームに対して次の例のように必須項目を入力する。

  • Vendor name: inaz2
  • Application title: SHA-1 Calculator FXML
  • Application version: 1.0.0
  • Application class: application.Main

そして、右側にある「Generate ant build.xml and run」をクリックするとビルドが行われ、完了するとbuild/distにJARファイルが生成される。

生成されたJARファイルを実行すると、上と同様のウィンドウが表示されることが確認できる。

関連リンク

JavaFXでGUIアプリケーションを作ってみる

「Java+SwingでGUIアプリケーションを作ってみる」では、JDKEclipseをインストールし、Java+SwingによるGUIアプリケーションを作った。 SwingはJava 1.2から存在する標準のGUIライブラリであるが、Java 8からは新たな標準としてJavaFXへの置き換えが進められている。 ここでは、JavaFXを使い、同様のGUIアプリケーションを作ってみる。

環境

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

e(fx)clipseのインストール

EclipseJavaFXプロジェクトを扱えるようにするために、JavaFX用のプラグインであるe(fx)clipseをインストールする。

まず、メニューの「Help」→「Install New Software」を選択する。 ウィザードが表示されたら、リポジトリとしてhttp://download.eclipse.org/efxclipse/updates-released/2.3.0/siteを追加し、「e(fx)clipse -IDE」にチェックを入れてNextを押す。 ユーザライセンスへの同意を求められるので、同意にチェックを入れてFinishを押すとインストールが行われる。

JavaFXGUIアプリケーションを作ってみる

「Java+SwingでGUIアプリケーションを作ってみる」と同様に、ドラッグアンドドロップされたファイルのSHA-1ハッシュ値をテーブルに表示するアプリケーションを書いてみる。

まず、ツールバーから「New」→「Project」を開き、「JavaFX」→「JavaFX Project」を選択して「SHA1CalculatorFX」プロジェクトを作成する。 src/application/Main.javaにファイルの雛形が作成されるので、続けて「Run」を押し、空のウィンドウが表示されることを確認する。

JavaFXでは、メインとなるクラスはプロジェクト名に関係なくMainクラスとなる。 雛形を編集し、実際にコードを書いてみると次のようになる。

package application;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;

import javafx.application.Application;
import javafx.beans.property.SimpleStringProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.concurrent.Task;
import javafx.event.EventHandler;
import javafx.scene.Scene;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.control.cell.PropertyValueFactory;
import javafx.scene.control.cell.TextFieldTableCell;
import javafx.scene.input.DragEvent;
import javafx.scene.input.Dragboard;
import javafx.scene.input.TransferMode;
import javafx.scene.layout.BorderPane;
import javafx.stage.Stage;


public class Main extends Application {
    private final TableView<FileInfo> table = new TableView<>();

    @Override
    @SuppressWarnings("unchecked")
    public void start(Stage primaryStage) {
        try {
            BorderPane root = new BorderPane();
            Scene scene = new Scene(root,600,400);  // expanded default size
            scene.getStylesheets().add(getClass().getResource("application.css").toExternalForm());
            primaryStage.setScene(scene);
            primaryStage.setTitle("SHA-1 Calculator FX");

            // TableView settings
            table.setEditable(true);
            table.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY);

            TableColumn<FileInfo, String> filePathCol = new TableColumn<>("File Path");
            filePathCol.setCellValueFactory(new PropertyValueFactory<>("path"));
            filePathCol.setCellFactory(TextFieldTableCell.forTableColumn());

            TableColumn<FileInfo, String> sha1sumCol = new TableColumn<>("sha1sum");
            sha1sumCol.setCellValueFactory(new PropertyValueFactory<>("sha1sum"));
            sha1sumCol.setCellFactory(TextFieldTableCell.forTableColumn());

            table.getColumns().addAll(filePathCol, sha1sumCol);
            ObservableList<FileInfo> data = FXCollections.observableArrayList();
            table.setItems(data);

            root.setCenter(table);

            // enable file drop
            scene.setOnDragOver(new DragOverHandler());
            scene.setOnDragDropped(new DragDroppedHandler());

            primaryStage.show();
        } catch(Exception e) {
            e.printStackTrace();
        }
    }

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

    private class DragOverHandler implements EventHandler<DragEvent> {
        @Override
        public void handle(DragEvent event) {
            Dragboard db = event.getDragboard();
            if (db.hasFiles()) {
                event.acceptTransferModes(TransferMode.COPY);
            } else {
                event.consume();
            }
        }
    }

    private class DragDroppedHandler implements EventHandler<DragEvent> {
        @Override
        public void handle(DragEvent event) {
            Dragboard db = event.getDragboard();
            boolean success = false;
            if (db.hasFiles()) {
                success = true;
                ObservableList<FileInfo> data = table.getItems();
                for (File file : db.getFiles()) {
                    FileInfo fileinfo = new FileInfo(file.getAbsolutePath(), "");
                    data.add(fileinfo);
                    SHA1CalculationTask task = new SHA1CalculationTask(fileinfo);
                    fileinfo.sha1sumProperty().bind(task.messageProperty());
                    new Thread(task).start();
                }
            }
            event.setDropCompleted(success);
            event.consume();
        }
    }

    class SHA1CalculationTask extends Task<Void> {
        private final FileInfo fileinfo;

        public SHA1CalculationTask(FileInfo fileinfo) {
            this.fileinfo = fileinfo;
        }

        @Override
        protected Void call() throws Exception {
            MessageDigest md = null;
            try {
                md = MessageDigest.getInstance("SHA-1");
            } catch (NoSuchAlgorithmException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }

            // update digest with each 8MB chunk
            String path = fileinfo.getPath();
            long totalBytes = (new File(path)).length();
            long currentBytes = 0;
            try (FileInputStream istream = new FileInputStream(path)) {
                byte[] buf = new byte[8*1024*1024];
                int n;
                while ((n = istream.read(buf, 0, buf.length)) != -1) {
                    md.update(Arrays.copyOf(buf, n));
                    currentBytes += n;
                    String message = String.format("calculating %d%%...", 100 * currentBytes / totalBytes);
                    updateMessage(message);
                }
            } catch (IOException e) {
                e.printStackTrace();
            }

            byte[] digest = md.digest();
            StringBuilder sb = new StringBuilder();
            for (byte b : digest) {
                sb.append(String.format("%02x", b));
            }
            updateMessage(sb.toString());
            return null;
        }
    }


    public static class FileInfo {
        private final SimpleStringProperty path = new SimpleStringProperty();
        private final SimpleStringProperty sha1sum = new SimpleStringProperty();

        private FileInfo(String path, String sha1sum) {
            setPath(path);
            setSha1sum(sha1sum);
        }

        public String getPath() {
            return path.get();
        }

        public void setPath(String newPath) {
            path.set(newPath);
        }

        public String getSha1sum() {
            return sha1sum.get();
        }

        public void setSha1sum(String newSha1sum) {
            sha1sum.set(newSha1sum);
        }

        public SimpleStringProperty sha1sumProperty() {
            return sha1sum;
        }
    }
}

JavaFXではデータバインディングの概念が取り入れられており、次のような特徴がある。

  • TableViewにitemsとしてobservableArrayListを登録することで、リストへの変更が直接テーブルに反映される
  • Tableの各セルはクラスのPropertyメンバで表現し、これをTaskクラスのPropertyにbindすることにより非同期での画面更新を行う

コードを保存した後「Run」を押して実行し、適当にファイルをドラッグアンドドロップした後のスクリーンショットを次に示す。

f:id:inaz2:20160426084024p:plain

Swingと比較して、より現代的なUIデザインになっていることが見て取れる。

実行可能なJARファイルを作ってみる

JavaFXプロジェクトで実行可能なJARファイルを作成する際は、プロジェクトのExportではなくビルドツールのAntを用いる。

まず、build.fxbuildファイルを開き、表示されるフォームに対して次の例のように必須項目を入力する。

  • Vendor name: inaz2
  • Application title: SHA-1 Calculator FX
  • Application version: 1.0.0
  • Application class: application.Main

そして、右側にある「Generate ant build.xml and run」をクリックするとビルドが行われ、完了するとbuild/distにJARファイルが生成される。

生成されたJARファイルを実行すると、上と同様のウィンドウが表示されることが確認できる。

関連リンク

Java+SwingでGUIアプリケーションを作ってみる

JavaとSwingを使い、JREJava Runtime Environment)をインストールしたコンピュータで実行可能なGUIアプリケーションを作ってみる。

環境

Windows 8.1 Pro 64 bit版

>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

JDK 8のインストール

まず、JDKのダウンロードページからWindows x64用のインストーラをダウンロードし、JDK 8をインストールする。 インストール後、PATH環境変数C:\Program Files\Java\jdk1.8.0_91\binを追加しておく。 コマンドプロンプトを起動し、次のコマンドが使えることを確認する。

>javac -version
javac 1.8.0_91

Eclipseのインストール

次に、EclipseのダウンロードページからWindows 64 bit用のzipファイルをダウンロードし、適当なディレクトリに展開する。 続けて、eclipse.exeを起動するとコード一式を置くパス(workspace)の設定を求められるので、適当なディレクトリを設定し、「Use this as default and do not ask again」にチェックを入れてOKを押す。 Tutorialsの「Create a Hello World application」を選択し、簡単にEclipseの使い方を確認しておくとよい。

SwingでGUIアプリケーションを作ってみる

ここでは、ドラッグアンドドロップされたファイルのSHA-1ハッシュ値をテーブルに表示するアプリケーションを書いてみる。

まず、ツールバーの「New」→「Java Project」から「SHA1Calculator」プロジェクトを作成し、続けて「Create new visual classes」から「Swing」→「JFrame」を選択する。 クラス名を「SHA1Calculator」にしてOKを押すとファイルの雛形が作成されるので、続けて「Run」を押し、空のウィンドウが表示されることを確認する。

雛形を編集し、実際にコードを書いてみると次のようになる。 javax.swing.JTableなど、使用するクラスを新たにimportする必要があることに注意。 なお、EclipseではCtrl+Spaceで補完リストの表示、Ctrl+Shift+Fでコードの自動整形、Ctrl+Shift+Oで不要なimport文の削除を行うことができる。

import java.awt.BorderLayout;
import java.awt.EventQueue;
import java.awt.datatransfer.DataFlavor;
import java.awt.datatransfer.Transferable;
import java.awt.datatransfer.UnsupportedFlavorException;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
import java.util.List;

import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTable;
import javax.swing.SwingWorker;
import javax.swing.TransferHandler;
import javax.swing.border.EmptyBorder;
import javax.swing.table.DefaultTableModel;

public class SHA1Calculator extends JFrame {

    private JPanel contentPane;
    private DefaultTableModel model;

    /**
     * Launch the application.
     */
    public static void main(String[] args) {
        EventQueue.invokeLater(new Runnable() {
            public void run() {
                try {
                    SHA1Calculator frame = new SHA1Calculator();
                    frame.setVisible(true);
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        });
    }

    /**
     * Create the frame.
     */
    public SHA1Calculator() {
        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        setBounds(100, 100, 600, 300);  // expanded default frame size
        contentPane = new JPanel();
        contentPane.setBorder(new EmptyBorder(5, 5, 5, 5));
        contentPane.setLayout(new BorderLayout(0, 0));
        setContentPane(contentPane);

        setTitle("SHA-1 Calculator");

        // create scrollable JTable
        model = new DefaultTableModel(new String[]{"File Path", "sha1sum"}, 0);
        JTable table = new JTable(model);
        JScrollPane scrollPane = new JScrollPane(table);
        contentPane.add(scrollPane);

        // enable file drop
        contentPane.setTransferHandler(new DropFileHandler());
    }

    private class DropFileHandler extends TransferHandler {
        @Override
        public boolean canImport(TransferSupport support) {
            // Check if dropped data is files
            return (support.isDrop()
                    && support.isDataFlavorSupported(DataFlavor.javaFileListFlavor));
        }

        @Override
        @SuppressWarnings("unchecked")
        public boolean importData(TransferSupport support) {
            if (!canImport(support)) {
                return false;
            }

            Transferable t = support.getTransferable();
            List<File> files = null;
            try {
                files = (List<File>) t.getTransferData(DataFlavor.javaFileListFlavor);
            } catch (UnsupportedFlavorException | IOException e1) {
                // TODO Auto-generated catch block
                e1.printStackTrace();
            }

            for (File file : files) {
                String path = file.getAbsolutePath();
                model.addRow(new String[]{path, "calculating..."});
                ModelUpdater mu = new ModelUpdater(path, model.getRowCount()-1);
                mu.execute();
            }

            return true;
        }
    }

    private class ModelUpdater extends SwingWorker<String, Long> {
        private String path;
        private Integer rowindex;

        public ModelUpdater(String path, Integer rowindex) {
            this.path = path;
            this.rowindex = rowindex;
        }

        @Override
        public String doInBackground() throws IOException {
            MessageDigest md = null;
            try {
                md = MessageDigest.getInstance("SHA-1");
            } catch (NoSuchAlgorithmException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }

            // update digest with each 8MB chunk
            long totalBytes = (new File(path)).length();
            long currentBytes = 0;
            try (FileInputStream istream = new FileInputStream(path)) {
                byte[] buf = new byte[8*1024*1024];
                int n;
                while ((n = istream.read(buf, 0, buf.length)) != -1) {
                    md.update(Arrays.copyOf(buf, n));
                    currentBytes += n;
                    publish(100*currentBytes/totalBytes);
                }
            } catch (IOException e) {
                e.printStackTrace();
            }

            byte[] digest = md.digest();
            StringBuilder sb = new StringBuilder();
            for (byte b : digest) {
                sb.append(String.format("%02x", b));
            }
            return sb.toString();
        }

        @Override
        protected void process(List<Long> values) {
            // only last one is effective
            String message = String.format("calculating %d%%...", values.get(values.size()-1));
            model.setValueAt(message, rowindex, 1);
        }

        @Override
        protected void done() {
            try {
                model.setValueAt(get(), rowindex, 1);
            } catch (Exception ignore) {
            }
        }
    }

}

上のコードでは、SHA1Calculatorクラスでウィンドウとコンポーネントを生成し、そのサブクラスのDropFileHandlerクラスでドラッグアンドドロップに対する処理を記述している。 また、SwingWorkerクラスを継承したModelUpdaterクラスでSHA-1ハッシュ値の計算を行っている。 Swingでハッシュ値計算などの時間がかかる処理を実行する場合、メインスレッドで実行すると画面描画がブロックしてしまうため、SwingWorkerを使ってバックグラウンドで実行させる必要がある。

コードを保存した後「Run」を押して実行し、適当にファイルをドラッグアンドドロップした後のスクリーンショットを次に示す。

f:id:inaz2:20160425223859p:plain

計算されたハッシュ値を別の方法で調べたハッシュ値と照らし合わせることにより、正しく計算できていることが確認できる。

実行可能なJARファイルを作ってみる

上のコードをJREで実行可能なJARファイルにするには、Package Explorerのプロジェクト名(SHA1Calculator)を右クリックし、「Export」を選択する。 表示されるウィザードから「Java」→「Runnable JAR file」を選択し、configurationと出力先パスを指定してFinishを押すと、JARファイルが生成される。

生成されたJARファイルを実行すると、上と同様のウィンドウが表示されることが確認できる。

関連リンク

Raspberry PiにインストールしたブラウザをRDPから操作する

Raspberry PiでWebブラウザを全画面表示し、RDP(リモートデスクトップ接続)から操作できるようにしてみる。

環境

Raspberry Pi 3 (Raspbian Jessie Lite)

$ uname -a
Linux raspberrypi 4.1.19-v7+ #858 SMP Tue Mar 15 15:56:00 GMT 2016 armv7l GNU/Linux

$ cat /etc/os-release
PRETTY_NAME="Raspbian GNU/Linux 8 (jessie)"
NAME="Raspbian GNU/Linux"
VERSION_ID="8"
VERSION="8 (jessie)"
ID=raspbian
ID_LIKE=debian
HOME_URL="http://www.raspbian.org/"
SUPPORT_URL="http://www.raspbian.org/RaspbianForums"
BUG_REPORT_URL="http://www.raspbian.org/RaspbianBugs"

Xorgのインストール

ブラウザを表示するにはGUI環境が必要になるので、まずXorgをインストールする。 合わせて、fbturboドライバをインストールして画面表示を高速化する。

$ sudo apt-get install xorg xserver-xorg-video-fbturbo

GUIログインを行うために、ディスプレイマネージャLightDMをインストールする。

$ sudo apt-get install lightdm

ここで、/etc/lightdm/lightdm.confを開き、以下のような設定を加えておくとよい。

  • 一定時間操作がないとき画面が消える動作をオフにする
  • ログイン画面においてユーザをリストから選択できるようにする
[SeatDefaults]
xserver-command=X -s 0 -dpms
greeter-hide-users=false

raspi-configを起動し、「3.3 Desktop」を選択してグラフィカルログインを有効にする。

$ sudo raspi-config

合わせて、「9.3 Memory Split」からGPU用のメモリをデフォルトの64MBから256MBに増やしておくとよい。

x11vncのインストール

Raspberry Piに接続したディスプレイ画面をリモートで操作できるようにするため、x11vncをインストールする。 続けて、VNCパスワードを設定する。

$ sudo apt-get install x11vnc

$ x11vnc -storepasswd
Enter VNC password:
Verify password:
Write password to /home/pi/.vnc/passwd?  [y]/n
Password written to: /home/pi/.vnc/passwd

起動時に自動実行されるよう、/lib/systemd/system/x11vnc.serviceを新規作成し、次のように記述する。

[Unit]
Description=Start x11vnc at startup.
After=multi-user.target

[Service]
Type=simple
ExecStart=/usr/bin/x11vnc -auth guess -forever -loop -noxdamage -repeat -rfbauth /home/pi/.vnc/passwd -rfbport 5900 -shared

[Install]
WantedBy=multi-user.target

次のコマンドで、作成したサービスを有効化する。

$ sudo systemctl daemon-reload
$ sudo systemctl enable x11vnc.service

xrdpのインストール

x11vncだけでもVNCプロトコルで接続が可能だが、Windows標準の「リモートデスクトップ接続」から接続できるとより便利である。 そこで、xrdpをインストールし、RDP経由でVNCサーバに接続できるようにする。

xrdpパッケージをインストールするには次のようにする。

$ sudo apt-get install xrdp

接続時のダイアログにおいて、VNC経由のコンソール接続がデフォルトになるように、/etc/xrdp/xrdp.iniを開き、次の2つのセクションを入れ替えておく。

[xrdp1]
name=console
lib=libvnc.so
ip=127.0.0.1
port=5900
username=na
password=ask

[xrdp2]
name=sesman-Xvnc
lib=libvnc.so
username=ask
password=ask
ip=127.0.0.1
port=-1

再起動した後、LAN内のWindows端末からRDP接続を行い、Raspberry Piのデスクトップを操作できるか確認する。

$ sudo reboot

Epiphanyブラウザのインストール

Raspberry Piでは、GPUアクセラレーションが有効なブラウザとしてEpiphanyが用意されている。 ここでは、browserユーザでログインすることでEpiphanyブラウザが全画面表示されるようにしてみる。

まず、Epiphanyブラウザとこれが利用するgnome-keyring、全画面表示のために使うxdotoolをインストールする。

$ sudo apt-get install epiphany-browser gnome-keyring xdotool

次に、音声、映像、キーボード・マウス入力が有効なbrowserユーザを作成し、パスワードを設定する。

$ sudo useradd -G audio,video,input -m browser

$ sudo passwd browser
Enter new UNIX password:
Retype new UNIX password:
passwd: password updated successfully

browserユーザに切り替え、~/.bashrcをバックアップした後、全画面でEpiphanyブラウザを表示するように編集する。

$ sudo su - browser

$ mv .bashrc{,.bak}

$ vi .bashrc
xdotool search --sync --onlyvisible --class "epiphany-browser" windowsize 100% 100% &
exec epiphany-browser

RDPで接続してbrowserユーザでログインした後、適当なWebサイトを表示したときの写真を次に示す。

f:id:inaz2:20160422013334p:plain

課題

以上の設定である程度ブラウジングできるようになるが、以下のような課題も残されている。

  • Input Methodが入っていないため、日本語入力ができない
  • Flashが再生できない
  • クラッシュして見れないページがある(Netflixのログインページなど)

関連リンク

Special Thanks

Raspberry PiにKodiをインストールしてネットテレビを作る

Raspberry PiにメディアセンターKodi(旧XMBC)をインストールして、接続されたディスプレイでYouTubeなどを見れるようにしてみる。 また、AndroidのリモコンアプリKoreからKodiを操作できるようにしてみる。

環境

Raspberry Pi 3 (Raspbian Jessie Lite)

$ uname -a
Linux raspberrypi 4.1.19-v7+ #858 SMP Tue Mar 15 15:56:00 GMT 2016 armv7l GNU/Linux

$ cat /etc/os-release
PRETTY_NAME="Raspbian GNU/Linux 8 (jessie)"
NAME="Raspbian GNU/Linux"
VERSION_ID="8"
VERSION="8 (jessie)"
ID=raspbian
ID_LIKE=debian
HOME_URL="http://www.raspbian.org/"
SUPPORT_URL="http://www.raspbian.org/RaspbianForums"
BUG_REPORT_URL="http://www.raspbian.org/RaspbianBugs"

Raspbianの設定

まず、raspi-configで「9.3 Memory Split」を選択し、GPU用のメモリをデフォルトの64MBから256MBに増やしておく。

$ sudo raspi-config

合わせて、HDMIディスプレイとUSBキーボードを接続しておく。

Kodiのインストール

パッケージをインストールし、起動時に自動起動するように設定する。 また、自動起動時のユーザであるkodiinputグループに追加し、キーボード入力を扱えるようにする。

$ sudo apt-get update

$ sudo apt-get install kodi

$ sudo vi /etc/default/kodi
# Set this to 1 to enable startup
ENABLED=1

$ sudo usermod -a -G input kodi

$ sudo reboot

再起動すると、接続されたディスプレイにKodiのホーム画面が表示される。

日本語を表示できるようにする

標準の状態では日本語が文字化けしてしまうため、フォントの設定を変更する。

  • System→Appearance→Skin→Fontsで「Arial based」を選択

Androidから操作できるようにする

USBキーボードではなくAndroidから操作できるように、リモートコントロールの設定を行う。

  • System→Service→Remote controlで「Allow remote control by programs on other systems」をオン
  • System→Service→Web serverで「Allow remote control via HTTP」をオン
  • System→Service→Zeroconfで「Announce services to other systems」をオン
    • 標準でオンになっている

LANに接続したAndroidにリモコンアプリKoreをインストールし、実行する。 設定ウィザードにおいて自動でKodiが発見されるので、これに接続し、アプリの上下左右ボタンでKodiを操作できることを確認する。

Add-onのインストール

KodiはHDD内の動画や音楽の再生に加え、Add-onのインストールによりさまざまなWebサービスを利用できるようになる。 ここでは、YouTube用Add-onのインストールについて説明する。

まず、Videos→Add-ons→Get more...と移動し、公式Add-onの一覧を表示する。 次に、YouTubeを選択し、表示された詳細画面からInstallを選択してインストールする。

インストール後、Add-onsの画面に戻るとYouTubeが追加されているので、これを選択する。 セットアップウィザードを実行するか聞かれるので、Yesを選択する。 ここでは次のように設定するとよい。

  • View: Default: Thumbnail
  • View: Episodes: Media info
  • Language: English
  • Region: Japan

続けてメニューが表示されるので、Sign Inを選択する。 画面にアクティベートコードが表示されるので、適当な端末からhttps://youtube.com/activateにアクセスしアクティベートを行う。 なお、Sign Inしないと正しく動作しないので注意。

以上の設定が終わると、次の写真のようにYouTubeが見れるようになる。

f:id:inaz2:20160421015255p:plain

また、Androidからは次のようにコントロールできる。

f:id:inaz2:20160421015306p:plain

他にもUdacityKhan Academy、音楽についてはSHOUTcastSoundCloudMixcloudなどのAdd-onがあるので、必要に応じて有効にしておくとよい。

関連リンク