mike-neckのブログ

Java or Groovy or Swift or Golang

KullaでJavaFXの操作を対話的にやってみる

Kullaネタ

JavaFXなどのGUIのプログラムをゴリゴリと書いている時に、ふとこのオブジェクトを少し動かしたらどうなるんだろうと気になることがあります。実際にやってみようとすると、コードを書いてアプリケーションを起動して実際の動きを見て、「ああ、なんか違うな」となって、コードを書きなおしてアプリケーションを起動してとなるかと思います。

まあ、FXMLなどで書かずにコードでGUIを作る場合は大抵そうなりますね。

そこで、次のようなKulla上で走るスクリプトを作ってみました。

import java.util.function.*;

import javafx.application.Application;
import javafx.application.Platform;
import javafx.scene.*;
import javafx.stage.Stage;

// alias for Objects#requireNonNull
<T> T nonNull(T o) {
  return Objects.requireNonNull(o);
}

// Runnable with Exception
interface ExRunnable {
  void run() throws Exception;
}

// RuntimeException by Handler
class HandlingException extends RuntimeException {
  private final Exception original;
  HandlingException(Exception e) {
    super(e);
    this.original = e;
  }
  public Exception getOriginal() {
    return original;
  }
}

// Handler for Exception
interface Handler {
  void handle(Exception e);
}

// Handler Receiver
interface OnError {
  void onError(Handler h);
  default void ignore() {
    onError(e -> {});
  }
  default void rethrow() throws Exception {
    try {
      onError(e -> {throw new HandlingException(e);});
    } catch (HandlingException he) {
      throw he.getOriginal();
    }
  }
}

// Run some task on JavaFX Thread
OnError run(ExRunnable er) {
  return h -> {
    nonNull(er);
    nonNull(h);
    Runnable r = () -> {
      try {
        er.run();
      } catch(Exception e) {
        h.handle(e);
      }
    };
    Platform.runLater(r);
  };
}

class FxComponent<T> {
  private final T item;
  private FxComponent(T item){
    this.item = nonNull(item);
  }
  public static <T> FxComponent<T> create(Supplier<? extends T> s)
      throws Exception {
    final BlockingQueue<T> q = new LinkedBlockingQueue<>();
    run(() -> q.put(s.get())).rethrow();
    return new FxComponent<>(q.take());
  }
  public static <T> FxComponent<T> direct(T item) {
    return new FxComponent<>(item);
  }
  public T get() {
    return item;
  }
  public OnError compute(Consumer<? super T> task) {
    nonNull(task);
    final ExRunnable er = () -> task.accept(item);
    return run(er);
  }
}

class FxApp extends Application {

  private static final ExecutorService EXE = Executors.newFixedThreadPool(1);

  private static Size SIZE;

  private static FxApp APP;

  private static boolean initialized = false;

  public static FxApp get() throws IllegalStateException {
    if(initialized) return APP;
    throw new IllegalStateException("Application is not initialized");
  }

  private FxComponent<Stage> stg;

  public FxComponent<Stage> getStage() {
    return stg;
  }

  private FxComponent<Scene> scn;

  public FxComponent<Scene> getScene() {
    return scn;
  }

  private FxComponent<Group> root;

  public FxComponent<Group> getRoot() {
    return root;
  }

  @Override
  public void start(Stage stage) {
    APP = this;
    // initialize group
    Group g = new Group();
    this.root = FxComponent.direct(g);
    // initialize scene
    Scene s = new Scene(g, SIZE.width, SIZE.height);
    this.scn = FxComponent.direct(s);
    // initialize stage
    this.stg = FxComponent.direct(stage);
    stage.setScene(s);
    stage.setTitle("Kulla Fx Sample");
    stage.show();
    stage.setOnCloseRequest(we -> EXE.shutdown());
    // initialization finished
    initialized = true;
  }

  public static ApplicationLauncher width(int w) {
    return h -> {
      FxApp.SIZE = new Size(w, h);
      EXE.submit(() -> Application.launch(FxApp.class));
    };
  }

  public static interface ApplicationLauncher {
    void height(int h);
  }

  private static class Size {
    final int width;
    final int height;
    Size(int width, int height) {
      this.width = width;
      this.height = height;
    }
  }
}

上記のスクリプトをロードした後に、アプリケーションを始めるには次のように書いていきます。

// アプリケーション開始
FxApp.width(500).height(450)
// アプリケーションのインスタンスを取得
FxApp app = FxApp.get()
// ルートのGroupを取得
FxComponent<Group> root = app.getRoot()
// ルートのSceneを取得
FxComponent<Scene> scene = app.getScene()
// ルートのStageを取得
FxComponent<Stage> stage = app.getStage()
// Boxオブジェクトを作る
import javafx.scene.shape.Box
FxComponent<Box> box = FxComponent.create(() -> new Box(10, 10, 10))
// BoxオブジェクトをルートのGroupに追加する
root.compute(r -> r.getChildren().add(box.get())).ignore()
// Sceneの背景を灰色にする
import javafx.scene.paint.Color
scene.compute(s -> s.setFill(Color.GRAY)).ignore()

実際にはこんな感じで対話的にJavaFXのオブジェクトをいじることができます。

キャプチャー

【2015/07/08 9:28 補足】

  • WindowsだとOpenJDK 9 eaにjlineがついてないので、Kullaが実行できないかもしれない
  • 型を省略したラムダ式では補完が効かないので、型情報を与えたラムダ式(Box b -> b.setRotate(20))にすれば補完が効くかもしれない