1
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

JavaFX でタスクバーを使わない枠なしの非矩形ウィンドウを作る

1
Posted at

デスクトップアクセサリのような、タスクバーに表示されず、枠が無くて四角くないウィンドウを JavaFX アプリケーションを作ろうとしたときにした試行錯誤のメモ。

環境

OS

Windows 10

Java

1.8.0_162

タスクバーなしのウィンドウ表示

package sample.javafx;

import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.stage.Stage;
import javafx.stage.StageStyle;

public class Main extends Application {
    
    public static void main(String[] args) {
        Application.launch(args);
    }

    @Override
    public void start(Stage primaryStage) throws Exception {
        primaryStage.initStyle(StageStyle.UTILITY);
        Parent root = FXMLLoader.load(this.getClass().getResource("/main.fxml"));
        primaryStage.setScene(new Scene(root));
        primaryStage.show();
    }
}

実行結果

javafx.jpg

説明

  • Stage の initStyle() で StageStyle.UTILITY を指定すると、タスクバーに表示されないウィンドウを作ることができる
  • UTILITY とあるように、本来これは補助用のウィンドウ(ツールウィンドウとか)を表示するためのもの
  • しかし、これ以外に JavaFX でタスクバーを使用しないウィンドウを作る方法は、調べた範囲ではなさそう

枠の無い非矩形ウィンドウの表示

main.fxml

javafx.jpg

package sample.javafx;

import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.scene.paint.Color;
import javafx.stage.Stage;
import javafx.stage.StageStyle;

public class Main extends Application {
    
    public static void main(String[] args) {
        Application.launch(args);
    }

    @Override
    public void start(Stage primaryStage) throws Exception {
        primaryStage.initStyle(StageStyle.TRANSPARENT);

        Parent root = FXMLLoader.load(this.getClass().getResource("/main.fxml"));
        Scene scene = new Scene(root);
        scene.setFill(Color.TRANSPARENT);
        primaryStage.setScene(scene);

        primaryStage.show();
    }
}

実行結果

javafx.jpg

説明

  • 四角くないウィンドウを表示するには、まずは Stage の initStyle() で StageStyle.TRANSPARENT を指定する
    • これを指定すると、ウィンドウ枠が無くなり Stage の背景が透明になる
    • ただし、この場合タスクバーは表示される
  • Stage に載せる Scene の setFill() で Color.TRANSPARENT を指定して、 Scene の背景も透明にする
  • これで Scene に載せているルートノードが、そのウィンドウの形を決めるようになるので、あとは -fx-background-radius などで枠の形を変更すれば、ウィンドウの形を任意に変更できるようになる

2つを組み合わせる

UTILITY TRANSPARENT
タスクバー非表示 OK NG
非矩形ウィンドウ NG OK

こんな感じで、それぞれが欲しい機能と不要な機能を持っているせいで、あちらを立てればこちらが立たず状態になっている。
この2つを綺麗に組み合わせる方法は、調べた感じ、2018年4月現在はなさそう。

OpenJDK の issue に、この問題があげられているが、最終更新日が 2017年の5月になっていて、その後進展はなさそうな気配。

[JDK-8091566] Taskbar-less Undecorated Transparent Window - Java Bug System

無理やり組み合わせる

上述の Issue にも書かれているが、一応無理やり2つを組み合わせることはできる。

具体的には、次のようにする。

  • プライマリステージを StageStyle.UTILITY にして、画面外の見えないところに移動させる
  • もう1つ Stage を作り、
    • StageStyle.TRANSPARENT を設定する
    • プライマリステージを owner に設定する

つまり、プライマリステージはタスクバーを表示させない UTILITY の機能を使うためだけに使用し画面には見せないようにし、実際に目に見えるウィンドウは TRANSPARENT を設定した別の Stage にやらせるという方法。

javafx.jpg

この方法の懸念点について、上述の Issue では次のように説明されている。

This is a dangerous hack because that UTILITY window could get placed back on the main screen if the computer screen resizes like from a remote desktop session connect and the OS decides to place all windows back on the visible bounds. Tests have shown this doesn't happen, but it feels like it could in certain cases.

(訳)
これは危険なハックです。なぜなら、もしリモートデスクトップセッションの接続などによってコンピュータのスクリーンがリサイズされ、 OS が全てのウィンドウの位置を可視領域に戻すことを決定した場合に、 UTILITY ウィンドウがメインウィンドウに戻ってくるかもしれません。テストではこれは起こらないとなっていますが、特定のケースでは起こりそうな気がします。

一応テストでは起こらないことが確認されているそうだが、まぁ綺麗な解決方法でない感じはする。
もし何かの間違いでプライマリステージが見えてしまうと、かなり格好悪いことになるのは想像に難くない。

次善策として、自分はさらにプライマリステージの opacity を 0 にしている。
opacity がサポートされている環境であれば、間違ってプライマリステージが可視領域に入ってきても、見えるようにはならない。
ただし、 opacity がサポートされていない環境では意味がないので、やはりスクリーン外に移動させておく対応は必要になると思う。

最終形態

上述の無理やりの組み合わせに加えて、システムトレイなどの実装も入れると、最終的に次のようになる。

main.fxml

javafx.jpg

Main.java
package sample.javafx;

import javafx.application.Application;
import javafx.application.Platform;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.scene.paint.Color;
import javafx.stage.Stage;
import javafx.stage.StageStyle;

import javax.imageio.ImageIO;
import java.awt.TrayIcon;
import java.awt.SystemTray;
import java.awt.AWTException;
import java.awt.image.BufferedImage;
import java.io.IOException;

public class Main extends Application {

    private TrayIcon trayIcon;
    
    public static void main(String[] args) {
        Application.launch(args);
    }

    @Override
    public void start(Stage primaryStage) throws Exception {
        Platform.setImplicitExit(false);
        
        this.initPrimaryStage(primaryStage);
        Stage mainStage = this.initMainStage(primaryStage);
        this.initSystemTray(mainStage);
        
        primaryStage.show();
        mainStage.show();
    }
    
    private void initPrimaryStage(Stage primaryStage) {
        primaryStage.initStyle(StageStyle.UTILITY);
        primaryStage.setOpacity(0.0);
        primaryStage.setX(-1000);
        primaryStage.setY(-1000);
    }
    
    private Stage initMainStage(Stage primaryStage) throws IOException {
        Stage mainStage = new Stage();
        mainStage.initOwner(primaryStage);
        mainStage.initStyle(StageStyle.TRANSPARENT);
        
        FXMLLoader fxmlLoader = new FXMLLoader(this.getClass().getResource("/main.fxml"));
        Parent root = fxmlLoader.load();
        Scene scene = new Scene(root);
        scene.setFill(Color.TRANSPARENT);
        mainStage.setScene(scene);

        MainController controller = fxmlLoader.getController();
        controller.setStage(mainStage);

        return mainStage;
    }
    
    private void initSystemTray(Stage mainStage) throws IOException, AWTException {
        if (!SystemTray.isSupported()) {
            return;
        }
        
        BufferedImage img = ImageIO.read(this.getClass().getResource("/img/open.png"));
        this.trayIcon = new TrayIcon(img);
        this.trayIcon.setImageAutoSize(true);
        this.trayIcon.addActionListener(e -> {
            Platform.runLater(() -> {
                if (mainStage.isShowing()) {
                    mainStage.hide();
                } else {
                    mainStage.show();
                }
            });
        });
        this.trayIcon.setToolTip("show/hide");
        SystemTray systemTray = SystemTray.getSystemTray();
        systemTray.add(trayIcon);
    }

    @Override
    public void stop() {
        if (SystemTray.isSupported() && this.trayIcon != null) {
            SystemTray.getSystemTray().remove(this.trayIcon);
        }
    }
}
MainController.java
package sample.javafx;

import javafx.application.Platform;
import javafx.fxml.FXML;
import javafx.scene.input.MouseEvent;
import javafx.stage.Stage;

public class MainController {
    private Stage stage;
    private double mouseOffsetX;
    private double mouseOffsetY;

    public void setStage(Stage stage) {
        this.stage = stage;
    }

    @FXML
    public void close() {
        Platform.exit();
    }

    @FXML
    public void onMousePressed(MouseEvent event) {
        this.mouseOffsetX = this.stage.getX() - event.getScreenX();
        this.mouseOffsetY = this.stage.getY() - event.getScreenY();
    }
    
    @FXML
    public void onMouseDragged(MouseEvent event) {
        this.stage.setX(event.getScreenX() + this.mouseOffsetX);
        this.stage.setY(event.getScreenY() + this.mouseOffsetY);
    }
}

実行結果

ドラッグしている様子

javafx.gif

システムトレイに入っている様子

javafx.png

説明

  • 基本は、ここまでの話を複合して、システムトレイの利用とウィンドウのドラッグを追加実装しただけ
  • 1つだけ注意なのが、 Platform.setImplicitExit(false);
Main.java
    @Override
    public void start(Stage primaryStage) throws Exception {
        Platform.setImplicitExit(false);
  • これは、最後のウィンドウが非表示になったときに JavaFX アプリケーションを暗黙的に終了させるかどうかのフラグで、デフォルトは true になっている
  • システムトレイを使う場合、最後のウィンドウを非表示にしても、アプリケーションは終了しないようにしないといけない
  • そのため、この設定を false に切り替えておく必要がある

JavaFX でシステムトレイを使う方法

1
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?