デスクトップアクセサリのような、タスクバーに表示されず、枠が無くて四角くないウィンドウを 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();
}
}
実行結果
説明
-
StageのinitStyle()でStageStyle.UTILITYを指定すると、タスクバーに表示されないウィンドウを作ることができる -
UTILITYとあるように、本来これは補助用のウィンドウ(ツールウィンドウとか)を表示するためのもの - しかし、これ以外に JavaFX でタスクバーを使用しないウィンドウを作る方法は、調べた範囲ではなさそう
枠の無い非矩形ウィンドウの表示
main.fxml
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();
}
}
実行結果
説明
- 四角くないウィンドウを表示するには、まずは
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 にやらせるという方法。
この方法の懸念点について、上述の 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
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);
}
}
}
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);
}
}
実行結果
ドラッグしている様子
システムトレイに入っている様子
説明
- 基本は、ここまでの話を複合して、システムトレイの利用とウィンドウのドラッグを追加実装しただけ
- 1つだけ注意なのが、
Platform.setImplicitExit(false);
@Override
public void start(Stage primaryStage) throws Exception {
Platform.setImplicitExit(false);
- これは、最後のウィンドウが非表示になったときに JavaFX アプリケーションを暗黙的に終了させるかどうかのフラグで、デフォルトは
trueになっている - システムトレイを使う場合、最後のウィンドウを非表示にしても、アプリケーションは終了しないようにしないといけない
- そのため、この設定を
falseに切り替えておく必要がある
JavaFX でシステムトレイを使う方法






