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

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

# 環境
## OS
Windows 10

## Java
1.8.0_162

# タスクバーなしのウィンドウ表示
```java
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](https://qiita-image-store.s3.amazonaws.com/0/28302/851bb896-b30f-9978-c9bd-c3d8d3631cd8.jpeg)

**説明**

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

# 枠の無い非矩形ウィンドウの表示
**main.fxml**

![javafx.jpg](https://qiita-image-store.s3.amazonaws.com/0/28302/53cfaa91-4f40-d38f-810b-deb360265baf.jpeg)

```java
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](https://qiita-image-store.s3.amazonaws.com/0/28302/bc8dd4f2-3f19-1a3d-ca05-29f8f9448cb9.jpeg)

**説明**

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

# ２つを組み合わせる
|              |`UTILITY`                        |`TRANSPARENT`                  |
|--------------|---------------------------------|-------------------------------|
|タスクバー非表示|<font color="blue">**OK**</font>|<font color="red">**NG**</font> |
|非矩形ウィンドウ|<font color="red">**NG**</font> |<font color="blue">**OK**</font>|


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

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

[[JDK-8091566] Taskbar-less Undecorated Transparent Window - Java Bug System](https://bugs.openjdk.java.net/browse/JDK-8091566)

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

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

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

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

![javafx.jpg](https://qiita-image-store.s3.amazonaws.com/0/28302/4484e542-a6b1-784e-756a-3690800d0f5f.jpeg)

この方法の懸念点について、上述の 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](https://qiita-image-store.s3.amazonaws.com/0/28302/a4a7466a-1934-c864-6aa5-429ee96cf23b.jpeg)


```java: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);
        }
    }
}
```

```java: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](https://qiita-image-store.s3.amazonaws.com/0/28302/23f0e7d2-4c2f-9f4a-b594-9fabf6c6e182.gif)

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

![javafx.png](https://qiita-image-store.s3.amazonaws.com/0/28302/91e5d10e-43e1-7fd5-7577-3e197279971f.png)

**説明**

- 基本は、ここまでの話を複合して、システムトレイの利用とウィンドウのドラッグを追加実装しただけ
- １つだけ注意なのが、 `Platform.setImplicitExit(false);`

```java:Main.java
    @Override
    public void start(Stage primaryStage) throws Exception {
        Platform.setImplicitExit(false);
```

- これは、最後のウィンドウが非表示になったときに JavaFX アプリケーションを暗黙的に終了させるかどうかのフラグで、デフォルトは `true` になっている
- システムトレイを使う場合、最後のウィンドウを非表示にしても、アプリケーションは終了しないようにしないといけない
- そのため、この設定を `false` に切り替えておく必要がある


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

- [Rab-Duck Software blog: JavaFX で Windows のシステムトレイを使う](http://rabduck.blogspot.jp/2016/07/javafx-windows.html)