javajavafxmouseeventtransparentstage

How to make MouseTransparent Stage in JavaFX


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:

run config

--add-opens javafx.graphics/javafx.stage=com.example.demo --add-opens javafx.graphics/com.sun.javafx.tk.quantum=com.example.demo

Solution

  • 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.

    Solution for Windows 11

    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>
    

    Execution commands

    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.

    Running from IntelliJ Idea

    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:

    Alternate solution

    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.

    Additional info

    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.

    FAQ

    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.

    Linking via jlink

    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>