I've wanted to make JavaFX app that would display a crosshair in a middle of screen, but whenever I hover on the ImageView I can't do background tasks, like it blocks my mouse events.
I've tried using Node#setMouseTransparent
but it didn't really work, same for Scene.setFill(null)
This is the code I have now:
private void setStageProperties() {
Screen screen = Screen.getPrimary();
Rectangle2D bounds = screen.getBounds();
stage.setWidth(bounds.getWidth());
stage.setHeight(bounds.getHeight());
Scene scene = new Scene(this);
scene.setFill(null);
stage.setScene(scene);
stage.setAlwaysOnTop(true);
this.primary = new Stage();
primary.initStyle(StageStyle.UTILITY);
primary.setOpacity(0);
primary.setHeight(0);
primary.setWidth(0);
primary.show();
stage.initOwner(primary);
stage.initStyle(StageStyle.TRANSPARENT);
double centerX = bounds.getMinX() + bounds.getWidth() / 2;
double centerY = bounds.getMinY() + bounds.getHeight() / 2;
stage.setX(centerX - stage.getWidth() / 2);
stage.setY(centerY - stage.getHeight() / 2);
}
public CrosshairScene() {
this.stage = new Stage();
this.crosshairImage = new ImageView("crosshair.png");
this.crosshairImage.setPickOnBounds(false);
this.setMouseTransparent(true);
this.setCenter(crosshairImage);
this.setStageProperties();
this.setStyle("-fx-background-color: null;");
}
Run configuration:
--add-opens javafx.graphics/javafx.stage=com.example.demo --add-opens javafx.graphics/com.sun.javafx.tk.quantum=com.example.demo
The node mouseTransparent
property just makes the node mouse transparent in the context of the JavaFX application, not concerning the JavaFX application and the rest of the windowing system. To be able to do that, you need to change the window style in the native window system.
Here is a Windows only solution, based on the ideas from:
Run with VM args:
--add-opens javafx.graphics/javafx.stage=com.example.demo --add-opens javafx.graphics/com.sun.javafx.tk.quantum=com.example.demo
src/main/java/module-info.java
module com.example.demo {
requires javafx.controls;
requires com.sun.jna;
requires com.sun.jna.platform;
exports com.example.demo;
}
src/main/java/com/example/demo/TransparentApplication.java
package com.example.demo;
import com.sun.jna.Pointer;
import com.sun.jna.platform.win32.*;
import javafx.application.Application;
import javafx.scene.*;
import javafx.scene.layout.*;
import javafx.scene.paint.Color;
import javafx.scene.shape.SVGPath;
import javafx.stage.*;
import java.lang.reflect.Method;
public class TransparentApplication extends Application {
private static final String CROSSHAIR_SVG_PATH =
"""
M 14,8 A 6,6 0 0 1 8,14 6,6 0 0 1 2,8 6,6 0 0 1 8,2 6,6 0 0 1 14,8 Z M 8 0 L 8 6.5 M 0 8 L 6.5 8 M 8 9.5 L 8 16 M 9.5 8 L 16 8
""";
@Override
public void start(Stage stage) {
StackPane layout = new StackPane(
new Group(
createCrosshair()
)
);
layout.setBackground(Background.fill(Color.TRANSPARENT));
layout.setMouseTransparent(true);
Scene scene = new Scene(layout, Color.TRANSPARENT);
stage.initStyle(StageStyle.TRANSPARENT);
stage.setAlwaysOnTop(true);
stage.setScene(scene);
stage.show();
makeMouseTransparent(stage);
}
private static Node createCrosshair() {
SVGPath path = new SVGPath();
path.setContent(CROSSHAIR_SVG_PATH);
path.setFill(Color.TRANSPARENT);
path.setStroke(
Color.BLUEVIOLET.deriveColor(
0, 1, 1, .6
)
);
path.setScaleX(10);
path.setScaleY(10);
return path;
}
private static void makeMouseTransparent(Stage stage) {
WinDef.HWND hwnd = getNativeHandleForStage(stage);
int wl = User32.INSTANCE.GetWindowLong(hwnd, WinUser.GWL_EXSTYLE);
wl = wl | WinUser.WS_EX_LAYERED | WinUser.WS_EX_TRANSPARENT;
User32.INSTANCE.SetWindowLong(hwnd, WinUser.GWL_EXSTYLE, wl);
}
private static WinDef.HWND getNativeHandleForStage(Stage stage) {
try {
final Method getPeer = Window.class.getDeclaredMethod("getPeer", (Class<?>[]) null);
getPeer.setAccessible(true);
final Object tkStage = getPeer.invoke(stage);
final Method getRawHandle = tkStage.getClass().getMethod("getRawHandle");
getRawHandle.setAccessible(true);
final Pointer pointer = new Pointer((Long) getRawHandle.invoke(tkStage));
return new WinDef.HWND(pointer);
} catch (Exception ex) {
System.err.println("Unable to determine native handle for window");
ex.printStackTrace();
return null;
}
}
public static void main(String[] args) {
launch();
}
}
pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example.demo</groupId>
<artifactId>TransparentApp</artifactId>
<version>1.0-SNAPSHOT</version>
<name>TransparentApp</name>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>net.java.dev.jna</groupId>
<artifactId>jna-platform</artifactId>
<version>5.14.0</version>
</dependency>
<dependency>
<groupId>org.openjfx</groupId>
<artifactId>javafx-controls</artifactId>
<version>21.0.1</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.11.0</version>
<configuration>
<source>21</source>
<target>21</target>
</configuration>
</plugin>
</plugins>
</build>
</project>
Example commands to run from the command line from the project root directory. Provided for Windows 11 with JDK 21, and assuming you have already run mvn clean install
to build the application.
set JAVA_HOME=%userprofile%\.jdks\openjdk-21.0.1
set PATH=%PATH%;%JAVA_HOME%\bin
set M2=%userprofile%\.m2\repository
java --add-opens javafx.graphics/javafx.stage=com.example.demo --add-opens javafx.graphics/com.sun.javafx.tk.quantum=com.example.demo -p %M2%\net\java\dev\jna\jna\5.14.0\jna-5.14.0.jar;%M2%\org\openjfx\javafx-base\21.0.1\javafx-base-21.0.1-win.jar;%M2%\net\java\dev\jna\jna-platform\5.14.0\jna-platform-5.14.0.jar;%M2%\org\openjfx\javafx-graphics\21.0.1\javafx-graphics-21.0.1-win.jar;%M2%\org\openjfx\javafx-controls\21.0.1\javafx-controls-21.0.1-win.jar;target\classes -m com.example.demo/com.example.demo.TransparentApplication
These commands are just provided for testing purposes. Usually you can configure VM arguments in your IDE run configuration. Or, the application would be linked and packaged using jlink
or jpackage
(probably via a build tool plugin for Maven or Gradle), with VM arguments specified in the startup script included in the application packaging, then the installed application could be run by double click.
Supply virtual machine (VM) arguments not program arguments. VM arguments need to be provided before the class to run. Choose Modify options | Add VM options
, then add the VM arguments in the box that says "VM options", as shown in this answer:
This proposal does not break modularity and rely on JNA access from your application code (I did not try it though).
Unless you change the window settings, similar to as defined above for Windows, the window displaying the JavaFX content will intercept the mouse actions. Maybe you could do something tricky like capture the mouse input, then hide the stage momentarily, and trigger a mouse action using the Robot, perhaps in conjunction with some Platform.runLater
calls, but that is a bit of a hack.
As noted in comments by Slaw:
Because I swear making the stage transparent/undecorated, making the scene transparent, and having all nodes under the mouse either mouse-transparent or having no background/fill (even partially, e.g., an image with transparent pixels) allowed you to interact with whatever was behind the stage in the past. At least, I believe it worked with Windows 10 (though not on other platforms, such as macOS).
I checked with Windows 11 Pro and JavaFX 21.0.1. It worked mostly as Slaw remembered. If you clicked on the transparent area of the stage, the mouse interacted with the window under the stage.
However, if you clicked on non-transparent areas of the stage, then the mouse actions did not register in the window under the stage, unless the JNA code provided in this answer was used.
The error:
module javafx.graphics does not "opens javafx.stage" to module com.example.demo
means that your VM arguments are wrong.
This VM argument was not picked up when you ran your application:
--add-opens javafx.graphics/javafx.stage=com.example.demo
com.example.demo
is the module name in this case, not the package name. Unless your module is also named com.example.demo
, it will not work. See the java man page for the --add-opens
switch for more information.
The jna team currently provide two versions of the jna artifacts. One is designed to the work with the Java Platform Module System (JPMS). If you wish to jlink your application, you need to use the JPMS version, which has a -jpms
extension.
<dependency>
<groupId>net.java.dev.jna</groupId>
<artifactId>jna-platform-jpms</artifactId>
<version>5.14.0</version>
</dependency>