torutkのブログ

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

JavaFXでウィンドウが閉じるときの処理はsetOnCloseRequestよりshowingPropertyがよい(改題)

この日記は朝の時点では setOnCloseRequest でウィンドウが閉じるときの実装について書いていました。次のツイートで、もっとよいやり方を教えてもらいましたので、showingPropertyを使う方法を追加しました。

はじめに

id:torutk:20170605 で、JavaFXのガジェット風プログラムのサポートクラスについて書きました。
そこでは、ガジェット風プログラムの共通の振る舞いの一つであるウィンドウの位置と大きさを終了時に保存する実装を持たせています。

この「終了時」の処理はウィンドウが閉じるときのタイミングとしています。ウィンドウが閉じるときのタイミングは次の方法でハンドラを登録して取ることができます。

  • WindowクラスのsetOnCloseRequestメソッド
  • WindowクラスのshowingPropertyメソッド

setOnCloseRequestでウィンドウが閉じるときの処理を実装

最初に見出したウィンドウが閉じるタイミングの取り方は、StageクラスのsetOnCloseRequestで実行したい処理をハンドラとして登録する方法です。

        stage.setOnCloseRequest(event -> saveStatus());

ガジェット風プログラムでこのウィンドウが閉じるタイミングの処理をsetOnCloseRequestで実装すると、次の2つの問題が生じました。

  1. メニューから終了する場合に、Stageクラスのclose()を呼ぶかPlatform.exit()を呼ぶと、setOnCloseRequestで登録したハンドラが呼ばれない
  2. ガジェットのサポートクラス内でsetOnCloseRequestでハンドラを登録し、それとは別にアプリケーション側でもsetOnCloseRequestでハンドラを登録すると、ハンドラが上書きされるため、後から登録したハンドラだけが実行される
プログラム内からウィンドウを閉じる

ウィンドウの閉じるボタン(Windows OSならタイトルバー右上の[X]ボタン)を押してウィンドウを閉じると、StageのsetOnCloseRequestで登録したハンドラが呼ばれます。
一方、Stageのclose()を呼びウィンドウを閉じるときは、setOnCloseRequestで登録したハンドラが呼ばれません。

setOnCloseRequestのJavadocには次の記述があります。

このWindowを閉じる外部リクエストがあると呼び出されます。

プログラム内でStageのclose()を呼ぶと、外部リクエストとしては扱われないようです。

そこで、次のようにウィンドウを閉じる要求をイベントとして発行します。

    MenuItem closeItem = ...
    closeItem.setOnAction(event -> {
           stage.fireEvent(new WindowEvent(stage, WindowEvent.WINDOW_CLOSE_REQUEST));
    });

WindowEventクラスのJavadocには次の記述があります。

このイベントは、そのウィンドウを閉じる外部リクエストがある場合にウィンドウに配信されます。イベントが、インストールされているいずれのウィンドウ・イベント・ハンドラでも使用されていない場合は、このイベントのデフォルトのハンドラが対応するウィンドウを閉じます。

この内容は、次のWikiページに記載しています。

JavaFXとウィンドウ - ソフトウェアエンジニアリング - Torutk

setOnCloseRequestはハンドラを1つしか登録できない

javafx.stage.Windowクラスのソースコードにある、setOnCloseRequestメソッドのコードを確認してみました。(Java SE 8 Update 131)

    public final void setOnCloseRequest(EventHandler<WindowEvent> value) {
        onCloseRequestProperty().set(value);
    }

プロパティのsetでハンドラを設定しているので、複数回呼ばれた場合は最後のハンドラのみ有効となります。したがって、setOnCloseRequestを使う場合、複数の処理を登録するには

    EventHandler<WindowEvent> closeHandler = primaryStage.getOnCloseRequest();
    stage.setOnCloseRequest(e -> {
        executor.shutdownNow();
        if (closeHandler != null) {
            closeHandler.handle(e);
        }
    });

と、先に登録されたハンドラがあれば、それを取っておいて呼び出すようにします。

かなり面倒な実装となってしまいました。

showingPropertyでウィンドウが閉じるときの処理を実装

Windowクラス(Stageクラスのスーパークラス)には、showingPropertyメソッドがあります。Javadocには次の記述があります。

このStageが表示されているかどうか(つまり、ユーザーのシステムで開いているかどうか)。ステージは、別のウィンドウの背後でステージがレンダリング中であったり、ステージがモニター外に配置されているためにユーザーに表示できない場合であっても、表示中である可能性があります。

ここで「表示されているかどうか」は、ウィンドウを最小化(アイコン化)しても表示されている状態として扱われていました。したがって、ガジェットプログラムを起動してウィンドウが表示されるときに、showingPropertyがfalseからtrueに変化し、ガジェットプログラムを終了してウィンドウが閉じるときに、showingPropertyがtrueからfalseに変化します。

そこで、ウィンドウが閉じるとき処理を実行する場合は、showingPropertyにリスナーを登録し、プロパティがtrueからfalseに変化するときに処理を実行するようにします。次にコードを示します。

        stage.showingProperty().addListener((observable, oldValue, newValue) -> {
            if (oldValue == true && newValue == false) {
                saveStatus();
            }
        });

このshowingPropertyのリスナーでウィンドウが閉じるときの処理を実装すると、setOnCloseRequestを使った際の問題点が発生しません。

  • Stageクラスのclose()やPlatform.exit()を呼んだ場合でも、showingPropertyで登録したハンドラが呼ばれる
  • 複数のハンドラがshowingPropertyのリスナーとして登録可能