人生、気合いと具合 - blog

カテゴリ:JavaFX

この記事は、JavaFX Advent Calendar 2016の21日目です。
昨日はHASUNUMA KenjiさんのJavaFX の TextField に入力補完を付けてみるでした 

去年のAdvent Calendarで、ListViewの扱いについて書きました。
ScalaFXの実装方針との兼ね合いで、ListCell#updateItemをオーバーライドするというCell描画の方式がうまくいかない、という内容です。

今年の記事を書くにあたって、このあたりについてもう一度調べてみたら、ScalaFXのGitHubに、issueがあがっていたので、今年は一年越しの続編として、これについて書いてみます。

まず、 JavaFXで、CellFactoryを設定するメソッドは以下となります。
public final void setCellFactory(Callback<TreeView<T>,TreeCell<T>> value)
ScalaFXでは以下。
def cellFactory_=(v: (TreeView[T]) ⇒ TreeCell[T]): Unit

どちらも、TreeViewを引数として受けて、描画処理を実装したTreeCellのインスタンスを返すコールバックを設定します。
しかし描画処理自体は、TreeCell#updateItem をオーバーライドして実装するので、ScalaFXのクラスだけでやろうとするとうまくいきません。上記のissueにも例がありますが、ちゃんとやろうとすると結局、ScalaFXのcellFactoryでも、JavaFXのTreeCellを使うことになってしまいます。
このへんは去年の記事で書いたとおりです。(私の記事はListCellを使って、継承したクラスを別に定義。issueの例ではTreeCellをメソッド内で直接newしてますが、JavaFXのクラスを使ってupdateItemメソッドをオーバーライドしなきゃならない、という意味で言いたいことは一緒です)

で、issueでは、cellFactoryメソッドのシグネチャを変えるのがいいんじゃね?という内容になってます。
def cellFactory_=(u: ((TreeCell[T], T, Boolean) => Unit))

というわけで、試しに実装してみました。
※FactoryなのにUnitなのがどうかなーと思ったので、「cellRenderer」にしてます
※issueではTreeCellですが、去年のコードを一部再利用している関係でListCellです

●ListViewの代わりに使うクラス
package com.bugworm.advent2016

import javafx.util.Callback
import javafx.scene.{control => jfxsc}

import scalafx.Includes._
import scalafx.scene.control.{ListCell, ListView}

class NewListView[T](override val delegate: jfxsc.ListView[T] = new jfxsc.ListView[T])
  extends ListView[T]{

  private var renderer_ : (ListCell[T], T, Boolean) => Unit = _
  def cellRenderer = renderer_
  def cellRenderer_=(r: (ListCell[T], T, Boolean) => Unit): Unit ={
    renderer_ = r;
    cellFactory() = new Callback[jfxsc.ListView[T], jfxsc.ListCell[T]] {
      override def call(param: jfxsc.ListView[T]): jfxsc.ListCell[T] = {
        new jfxsc.ListCell[T]{
          override def updateItem(item: T, empty: Boolean): Unit ={
            super.updateItem(item, empty)
            (item, empty) match {
              case (null, _) | (_, true) => {
                setText(null)
                setGraphic(null)
              }
              case (item, empty) => renderer_.apply(this, item, empty)
            }
          }
        }
      }
    }
  }
}

●使用例
package com.bugworm.advent2016

import scalafx.application.JFXApp
import scalafx.application.JFXApp.PrimaryStage
import scalafx.collections.ObservableBuffer
import scalafx.scene.Scene
import scalafx.scene.control.Label
import scalafx.scene.layout.VBox

object CellSample extends JFXApp{

  stage = new PrimaryStage{
    scene = new Scene(new VBox(
      Label("Cell Factory Test"),
      new NewListView[Book](){
        prefHeight = 100.0
        items = ObservableBuffer(
          new Book("ゴルゴ13", "さいとうたかを"),
          new Book("数学ガール", "結城浩"),
          new Book("三国志", "吉川英治"),
          new Book("ガラスの仮面", "美内すずえ"),
          new Book("哭きの竜", "能條純一"),
          new Book("傷だらけの天使たち", "喜国雅彦")
        )
        cellRenderer = (cell, item, empty) => {
          cell.text = item.title + " [" + item.author + "]"
        }
      }
    ))
  }
  stage.show()
}

ListViewの代わりのクラスはJavaFXとScalaFXが入り混じって結構面倒な実装になっていますが、使用例の方はJavaFXのクラスを使う必要が無いので、割といい感じなんじゃないかと思います。
 

このエントリは、JavaFX Advent Calendar 2015 の記事です。
昨日分は、aoeさんの、「javapackagerの紹介」でした。

JavaFXでは、ListViewやTreeViewの表示をカスタマイズできます。
ここでは例として、ListViewの場合を考えてみましょう。

表示方法を決めるために、ListCellのサブクラスを作成します。
以下、タイトルと著者を持つ、Bookクラスを表示するListViewのためのListCell<Book>です。

package samplej;

public class BookListCell extends ListCell<Book>{

    private final Label titleLabel = new Label();
    private final Label authorLabel = new Label();
    private final HBox container = new HBox(8.0, titleLabel, authorLabel);

    @Override
    protected void updateItem(Book book, boolean empty){
        super.updateItem(book, empty);
        if(!empty){
            titleLabel.setText(book.titlePropertey().get());
            authorLabel.setText(book.authorProperty().get());
            setGraphic(container);
        }
    }
}
※インポートなどは省略しています。以下同様。

そして、上記クラスをインスタンス化する、CellFactoryをListViewに設定します。

ListView<Book> list = new ListView<>();
list.setCellFactory(listView -> new BookListCell());

Callbackクラスなので、ラムダで書けますね。
こうすることで、ListViewを描画するときに、BookListCellクラスのupdateItemメソッドが実行されるわけです。

さて本題です。
これ、そのままScalaFXでやるとどうなるでしょうか?

package samples

class BookListCell extends ListCell[Book]{

  val titleLabel = new Label()
  val authorLabel = new Label()
  val container = new HBox(8.0, titleLabel, authorLabel)

  override def updateItem(book : Book, empty : Boolean) : Unit = {
  //この時点でコンパイルエラー
  }
}

はい、updateItemメソッドをoverrideで書くとコンパイルエラーになります。
おかしいなー、と思ってソースを追ってみると、Cell.scalaにこんなコメントがあります。(一番最後の方)
// TODO: implement updateItem(T item, boolean empty)
// might be difficult since updateItem is a protected method which needs to be defined in the delegate's class

まあ、確かにそうなんだよ...
ScalaFXの基本方針は、JavaFXの各クラスのラッパー的なクラスを用意して、各メソッドの実行を内部で持つJavaFXのインスタンスに委譲する感じになっています。
でも、CellクラスのupdateItemメソッドはprotectedなのでそういう方針では実装できませんよね。

とはいえ、そもそもupdateItemメソッドは外部から実行されるわけじゃないし(protectedなんだから)、やろうと思えば、Javaのソースと合わせて以下のようにできます。
ScalaFX側ではBookListCellを作成せず、

val list = new ListView[Book]()
list.cellFactory = (listView : ListView[Book]) => new samplej.BookListCell()

とします。
ListViewはScalaFXのクラスですが、samplej.BookListCellは先程作成したJavaのクラスの方です。
ScalaFXのListCellでオーバーライドはできませんが、ListCellの委譲先のインスタンスに、必要な処理をオーバーライドしたJavaFXのインスタンスを持たせる感じですね。
なお上記のソースは、ScalaFXで用意された良いように計らってくれるimplicit conversionを使った記述なので、ちゃんと書くと次のような感じになります。

list.cellFactory = (listView : ListView[Book]) => new ListCell[Book](new samplej.BookListCell())

JavaFXのサンプルで作った方のBookListCellで必要な処理をオーバーライドしているので、ScalaFXの方では委譲先を指定するコンストラクタを使用して、ListCellを作成しています。
※ちなみに、通常(引数のないコンストラクタ)は、内部でJavaFXのインスタンスが生成されています

JavaFXとScalaFX、両方のクラスが同じソースに出てくるといろいろ面倒というかややこしくなるのであまり好ましくないと思うのですが、やっぱり細かいところで必要になってしまいますね...


明日は、opengl-8080さんの、「Spring BootとJavaFXを組み合わせてみる」です。

割と早く申し込みがいっぱいになってしまっていたので、酔った勢いで懇親会LT枠に登録。

当日はちょっと遅れた上にLT資料を作り続けていたので、本編のメモが全然取れませんでした。
ハイライトはJava Updateでしょうか。

で、何とかLTの資料は完成したのですが、開始前に確認したら、HDMIが繋がらないことが発覚。結局ノートPCを貸していただき、資料をコピーして発表しました。(いろいろドタバタしてしまいスタッフの方々すみません)
そのせいか、最後のデモで例外が出てしまい残念でしたが、手元のタブレットで見てもらったりしたのでよいでしょう。
30億のデバイスで走るのに発表のデモは失敗するJava。ありがち。

ちなみに今回も資料はScalaFXで作ってます。表示用にノートPCで動かして、操作をタブレットでできるようにしてみました。
操作に対応する処理の実行部分を切り替え可能にしてあって、タブレット側は画面のタッチ等のイベントを拾って、処理すると同時にWebSocketに処理の内容を送る。
ノートPC側はWebSocketのメッセージ受信を契機に処理を行う、という感じです。
WebSocket通信は、herokuでGlassFish4.1(Embedded)を動かして実現してます。
LTでは操作側と表示側、1対1で実行しましたが、理論上、表示側は複数接続可能にしてあります。せっかくだから会場で試してみればよかったかな。

一応、スクリーンショットを並べたものをslideshareにあげておきました。


スタッフの方々お疲れ様でした&会場提供していただいたビズリーチさんありがとうございました。
参加無料で懇親会にピザとビールも提供していただけるとは太っ腹。ビズリーチさん素晴らしい!

この記事は、しょぼちむ Advent Calendar 2014 の20日目の記事になるはずでした。

19日目は @setoazusa さんの しょぼちむにテストファーストについて説明してみる でした。
21日目は @seri_k さんの 若者と転職 です。 

 そんなわけで、残念ながら間に合わなかった記事でございます。なぜかって?SIerだからさ!

しょぼちむといえばやはり目の前で見た、某雪だるま氏へのくんかくんかが最も記憶に残っています。ってことで(?)、ScalaFXを使ったミニゲームです。 

画面に出てくる雪だるまをタッチするとロックオン、画面下でうろうろしているレッドキングにタッチするとくんかくんかします。(タッチパネルじゃない場合はマウスクリックでも動きます)
複数まとめてロックオンしてからくんかくんかすると高得点。約1分間でどれだけくんかくんかできるかを競います?(なんのこっちゃ)

ダウンロード:http://homepage2.nifty.com/bugworm/webapp/syobo/AdventCalendar2014.jar
 
Java 8 + Scala2.11 + ScalaFXを使用。動作はJava 8だけあればOKだと思います。

kunka




























この記事は、JavaFX Advent Calendar 2014 の14日目の記事です。

昨日は @peko_kun さんの DataFXでFlow でした。
明日は @suzukij さんです。

Javaでは、Java SE 6から、スクリプト言語の実行を標準化した仕様が取り入れられています。
まずはこれで、JavaとJavaScriptを連携させてみましょう。

package sample;
import javax.script.ScriptEngine;
import javax.script.ScriptEngineManager;

public class JSCall {
    public static void main(String... args)throws Exception{
        ScriptEngineManager man = new ScriptEngineManager();
        ScriptEngine engine = man.getEngineByName("nashorn");
        engine.eval("Java.type('sample.JSCall').callback('実行した!');");
    }
    public static void callback(String message){
        System.out.println(message);
    }
}

ScriptEngineのevalメソッドで、JavaからJavaScriptを実行できます。
そして、JavaScriptでは、Java.typeを使ってクラス名を指定、そのままスタティックメソッドを実行できます。
そんなわけで、とりあえずJavaからJavaScript、JavaScriptからJava、両方実行できることはわかりました。

さてJavaFXにはjavafx.scene.web.WebEngineというクラスがあり、こちらでもJavaScriptを実行することができます。要はブラウザのJavaScriptを実行するわけですね。
ところが、WebEngineのexecuteScriptで先程と同じようにやってみると、「Java」なんて変数は無いよー、ということでエラーになってしまいます。
Java.typeが使えないというわけですが、ブラウザのJavaScript環境で任意のJavaクラスにアクセスできたら危険過ぎるから、ってことでしょうかね?

ではJavaScriptからJavaへ連携する方法がないのかというと、いくつかあるset~Handlerメソッドでコールバックを受けることができます。
たとえば、setPromptHandlerを使ってみましょう。
以下はJavaFXのコントローラークラスです。画面にはテキストフィールドとボタンがある感じですね。

package sample;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.control.TextField;
import javafx.scene.web.WebEngine;

import java.net.URL;
import java.util.ResourceBundle;

public class Controller implements Initializable{
    public TextField text;
    public WebEngine engine = new WebEngine();

    @Override
    public void initialize(URL location, ResourceBundle resources) {
        engine.setPromptHandler(data -> {
            System.out.println(data.getMessage());
            return "";
        });
    }

    public void buttonAction(ActionEvent actionEvent) {
        engine.executeScript("prompt('" + text.getText() + "');");
    }
}

ブラウザとして使うわけではないので、WebEngineはインスタンス化の際にnewしています。
初期化時に、WebEngineのsetPromptHandlerで、コールバック関数というかコールバックラムダ?を設定しています。(インタフェースとしては、Callback<PromptData,String>)
あとは、ボタンが押されたらexecuteScriptにprompt関数を実行するJavaScriptを渡してやります。すると、設定しておいたコールバックが実行されるという流れですね。

このしくみを使えば、JavaFXのアプリでブラウザのJavaScriptと(ある程度)やり取りができますね!


ちなみに、これを調べたきっかけは、WebSocketで遊んでた時です。
クライアント側をJavaFX作るとした場合、もちろんWebSocketのクライアント用JavaAPIがあるのでそれでもいいのですが、WebEngineのJavaScriptと連携できれば素のJava SEだけで行けるのでは?と思ったから。
というか本当はそこまで試したかったんだけど、時間切れなのでこの辺で。

俺、今のプロジェクトの炎上がおさまったらこの続編としてやってみるんだ...(フラグ!?)

このページのトップヘ