torutkのブログ

ソフトウェア・エンジニアのブログ

JavaFXでガジェット風プログラムのサポートクラス作成 #jjug_ccc #ccc_m6

はじめに

先月5月20日(土)に、JJUG CCC 2017 Springで「JavaFXでデスクトップガジェット風プログラムを作る」のセッションを行いました。

セッションのスライドは次です。
https://www.slideshare.net/torutk/jjugccc2017spring-m6-javafx

このセッションでは、デスクトップガジェット風プログラムに実装するガジェットに共通する次の振る舞いを紹介しました。

  • ウィンドウ枠の非表示、背景の非表示(透明ウィンドウ)
  • ウィンドウの位置をマウスドラッグで移動
  • ウィンドウの大きさをマウスホイール(+Ctrlキー)で変更
  • ポップアップメニューから終了
  • ウィンドウの位置と大きさを終了時に保存し、次回起動時にその値に復元

これらの振る舞いを、JavaFXアプリケーションに持たせるには、それなりのコード量を記述する必要があります。

ガジェット風の振る舞いの実装

ウィンドウ枠の非表示、背景の非表示(透明ウィンドウ)
        // ウィンドウ枠の非表示と背景透明化
        scene.setFill(Color.TRANSPARENT);
        stage.initStyle(StageStyle.TRANSPARENT);
ウィンドウの位置をマウスドラッグで移動
public class TinyGadgetApp extends Application {

    // ドラッグ&ドロップでウィンドウの移動
    private double dragStartX;
    private double dragStartY;

    @Override
    public void start(Stage stage) throws Exception {

        // ドラッグ操作でウィンドウを移動
        scene.setOnMousePressed(event -> {
            dragStartX = event.getSceneX();
            dragStartY = event.getSceneY();
        });
        scene.setOnMouseDragged(event -> {
            stage.setX(event.getScreenX() - dragStartX);
            stage.setY(event.getScreenY() - dragStartY);
        });
ウィンドウの大きさをマウスホイール(+Ctrlキー)で変更
    @Override
    public void start(Stage stage) throws Exception {
        :
        // マウスホイール操作でウィンドウの大きさを変更
        scene.setOnScroll(event -> {
            if (event.isControlDown()) {
                zoom(event.getDeltaY() > 0 ? 1.1 : 0.9, stage);
            }
        });
        :
    }

    private void zoom(double factor, Stage stage) {
        stage.setWidth(stage.getWidth() * factor);
        stage.setHeight(stage.getHeight() * factor);
    }

タッチパネルのピンチ操作でのズーム

    @Override
    public void start(Stage stage) throws Exception {
        :
        // タッチパネルのズーム(ピンチ)操作でウィンドウサイズを変更
        scene.setOnZoom(event -> {
            zoom(event.getZoomFactor(), stage);
        });
        :
    }
ポップアップメニューから終了
    @Override
    public void start(Stage stage) throws Exception {
        :   
        // マウス右クリックでポップアップメニューを表示
        ContextMenu popup = createContextMenu(stage);
        root.setOnContextMenuRequested(event -> {
            popup.show(stage, event.getScreenX(), event.getScreenY());
        });
        :
    }

    private ContextMenu createContextMenu(Stage stage) {
        MenuItem exitItem = new MenuItem("終了");
        exitItem.setStyle("-fx-font-size: 2em");
        exitItem.setOnAction(event -> {
            stage.fireEvent(new WindowEvent(stage, WindowEvent.WINDOW_CLOSE_REQUEST));
        });
        ContextMenu popup = new ContextMenu(exitItem);
        return popup;
    }
    
ウィンドウの位置と大きさを終了時に保存し、次回起動時にその値に復元
public class TinyGadgetApp extends Application {
    // 設定の保存で使用するプリファレンスとキー
    private Preferences prefs;
    private static final String KEY_STAGE_X = "stageX";
    private static final String KEY_STAGE_Y = "stageY";
    private static final String KEY_STAGE_WIDTH = "stageWidth";
    private static final String KEY_STAGE_HEIGHT = "stageHeight";

    @Override
    public void start(Stage stage) throws Exception {
        :       
        // ウィンドウが終了するときに状態を保存
        stage.setOnCloseRequest(event -> {
            saveStatus(stage);
        });
        
        // 保存した状態があれば復元
        loadStatus(stage);
        :
    }

    /**
     * 状態を永続領域に保存する。
     */
    private void saveStatus(Stage stage) {
        prefs.putInt(KEY_STAGE_X, (int) stage.getX());
        prefs.putInt(KEY_STAGE_Y, (int) stage.getY());
        prefs.putInt(KEY_STAGE_WIDTH, (int) stage.getWidth());
        prefs.putInt(KEY_STAGE_HEIGHT, (int) stage.getHeight());
    }
    
    /**
     * 永続領域に保存された状態を復元する。
     */
    private void loadStatus(Stage stage) {
        stage.setX(prefs.getInt(KEY_STAGE_X, 0));
        stage.setY(prefs.getInt(KEY_STAGE_Y, 0));
        stage.setWidth(prefs.getInt(KEY_STAGE_WIDTH, 320));
        stage.setHeight(prefs.getInt(KEY_STAGE_HEIGHT, 200));
    }
 

ライブラリとして切り出す

これらの振る舞いをライブラリとして切り出してみます。とりあえずクラス名はTinyGadgetSupportとします。

利用イメージ

なるべく簡単に利用できるよう、APIとしては、JavaFXアプリケーション側に1行だけ記述すればよいようにします。

public class TinyGadgetApp extends Application {

    @Override
    public void start(Stage stage) throws Exception {
        new TinyGadgetSupport(stage, Preferences.userNodeForPackage(this.getClass()));
        :
    }
}

ガジェット風の振る舞いの実装には、stage、scene、preferences のインスタンスが必要です。
そこで、TinyGadgetSupportのコンストラクタにstageとpreferencesを渡すようにしました。
sceneは、stageから取得することにします。

TinyGadgetSupportの実装

TinyGadgetSupport の実装は、次で公開しています。

https://github.com/torutk/javafx-gadgetsupport

以下に、実装上の工夫点などをピックアップします。

stageからsceneの取得

stage.getScene() でsceneを取得できますが、初期化中でまだstage.setScene()を呼び出す前はnullが返ってきます。そこで、stageのscenePropertyにリスナーを設定し、sceneがセットされたときに必要な処理を呼ぶようにしました。

        if (scene != null) {
            setup();
        }
        stage.sceneProperty().addListener((obs, ov, nv) -> {
            if (ov != nv && nv != null) {
                scene = nv;
                setup();
            }
        });
前回保存した位置が画面の外側だった場合の処理

画面構成を変更(マルチディスプレイをシングルディスプレイに変更、解像度を小さくした、など)した場合、前回保存した表示位置・大きさでは次回起動時には画面の外側になって見えなくなってしまうことがあります。

そこで、起動時に前回保存した画面の位置と大きさが表示領域の中に含まれるかを判定し、含まれない場合はデフォルト位置に表示するようにしています。

    private void loadStatus() {
        double x = prefs.getInt(KEY_STAGE_X, 0);
        double y = prefs.getInt(KEY_STAGE_Y, 0);
        double width = prefs.getInt(KEY_STAGE_WIDTH, 320);
        double height = prefs.getInt(KEY_STAGE_HEIGHT, 200);
        if (Screen.getScreensForRectangle(x, y, width, height).isEmpty()) {
            x = 0;
            y = 0;
            width = 320;
            height = 200;
        }
        :
    }

ScreenクラスのgetScreensForRectangleメソッドは、引数で指定した領域と重なるScreenのリストを返却します。そこで、前回保存した位置と大きさを引数にgetScreensForRectangleを呼び、戻り値が空か否かで画面に表示されるか否かを判断しています。