How does JavaFX integrate the title bar and menu bar?

633 views Asked by At

Many modern applications combine title bar and menu bar (such as Chrome, VS Code, IntelliJ IDEA, MS Offer), how is this possible? Does the Windows system provide support for this effect or does the application implement all the title bar features entirely on its own?

I want to use JavaFX to achieve this effect, but it is not as simple as setting StageStyle.UNDECORATED and adding a mouse movement event.

For the Windows platform, double-clicking the title bar can switch the window between Maximize and Restore, press Alt+Space combination to open the title bar menu, there will be shadows or no shadows on the edge of the window according to the Windows system settings, and when dragging the window to move, only the window boundary is displayed instead of the window content according to the Windows system settings. In addition, Windows Aero Snap provides a switch between "Maximize", "Restore" and "Minimize" by pressing Win+↑ / Win+↓, and press Win+← / Win+→ to enter the snapped windows resize mode.

I found many JavaFX projects on the Internet for observation, so I found the FX-BorderlessScene library of the XR3Player project. It achieves a good effect, but it still does not achieve the full native effect. For example, after closing Aero Snap Window in the Windows system, it is still You can press Win+↑ to switch to maximize.

Is there a convenient and mature way to achieve a fully localized title bar? I would forego merging the title bar and menu bar if I had to do a huge amount of work to implement it myself.

IntelliJ IDEA developed in Java has almost 100% native title bar effect. How does it do it?

1

There are 1 answers

0
Xi Minghui On

Use JNA to call the native API of Mac, Windows or Linux to hide the title bar, and the following demo shows how to hide the title-bar on Windows and keep all Windows localized window decorations. There is no custom title-bar implementation provided here, you need to design it yourself.


The 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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>org.example</groupId>
    <artifactId>demo</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <maven.compiler.source>17</maven.compiler.source>
        <maven.compiler.target>17</maven.compiler.target>
        <maven.compiler.release>17</maven.compiler.release>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.26</version>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
            <version>3.12.0</version>
        </dependency>
        <dependency>
            <groupId>org.openjfx</groupId>
            <artifactId>javafx-controls</artifactId>
            <version>19.0.2.1</version>
        </dependency>
        <dependency>
            <groupId>net.java.dev.jna</groupId>
            <artifactId>jna-platform</artifactId>
            <version>5.13.0</version>
        </dependency>
        <!-- This dependency is required when using FX-BorderlessScene -->
        <dependency>
            <groupId>uk.co.bithatch</groupId>
            <artifactId>FX-BorderlessScene</artifactId>
            <version>5.0.0</version>
        </dependency>
    </dependencies>

</project>
package org.example;

import javafx.application.Application;

public class Main {

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

}
package org.example;

import com.goxr3plus.fxborderlessscene.borderless.BorderlessScene;
import javafx.application.Application;
import javafx.scene.*;
import javafx.scene.layout.*;
import javafx.scene.paint.*;
import javafx.stage.*;
import org.apache.commons.lang3.SystemUtils;

public class MyApp extends Application {

    private static final String TITLE = "Test GUI";

    private static final Color PRIMARY_COLOR = Color.rgb(60, 60, 60);

    private static final boolean USE_BORDERLESS_WRAPPER = true;

    private static Scene getScene(Stage stage, Parent root) {
        if (!USE_BORDERLESS_WRAPPER) return new Scene(root);
        BorderlessScene borderlessScene = new BorderlessScene(stage, StageStyle.DECORATED, root);
        // Pass your custom title-bar element
        borderlessScene.setMoveControl(root);
        return borderlessScene;
    }

    @Override
    public void start(Stage primaryStage) {
        // Init window
        primaryStage.setTitle(TITLE);
        primaryStage.setWidth(1024);
        primaryStage.setHeight(768);
        Pane pane = new Pane();
        pane.setBackground(Background.fill(PRIMARY_COLOR));
        primaryStage.setScene(getScene(primaryStage, pane));
        primaryStage.show();

        // Only for Windows system
        if (SystemUtils.IS_OS_WINDOWS) {
            // Remove caption
            WindowsUtils.removeCaption(null, TITLE);
            // Redraw window
            WindowsUtils.refreshWindow(null, TITLE);
        } else if (SystemUtils.IS_OS_MAC) {
            throw new RuntimeException("Please write a window custom implementation suitable for this platform.");
        } else if (SystemUtils.IS_OS_LINUX) {
            throw new RuntimeException("Please write a window custom implementation suitable for this platform.");
        } else {
            throw new RuntimeException("Please write a window custom implementation suitable for this platform.");
        }
    }

}
package org.example;

import com.sun.jna.Pointer;
import com.sun.jna.platform.win32.*;
import org.apache.commons.lang3.SystemUtils;

/** Windows OS utils */
public class WindowsUtils {

    public static void removeCaption(String className, String windowName) {
        requireWindowsOS();
        // Find window by class name and/or window name
        WinDef.HWND window = findWindow(className, windowName);
        // Get current style of window
        int presentStyle = User32.INSTANCE.GetWindowLongPtr(window, WinUser.GWL_STYLE).intValue();
        // Remove title-bar of window: current style minus caption
        int newStyle = presentStyle ^ WinUser.WS_CAPTION;
        // Update window style
        User32.INSTANCE.SetWindowLongPtr(window, WinUser.GWL_STYLE, new Pointer(newStyle));
    }

    public static void refreshWindow(String className, String windowName) {
        requireWindowsOS();
        // Find window by class name and/or window name
        WinDef.HWND window = findWindow(className, windowName);

        int uFlags =

                WinUser.SWP_FRAMECHANGED |
                        // 保留当前位置(忽略X和Y参数)
                        WinUser.SWP_NOMOVE |
                        // 保留当前大小(忽略cx和cy参数)
                        WinUser.SWP_NOSIZE |
                        // 不更改所有者窗口在 Z 顺序中的位置
                        WinUser.SWP_NOREPOSITION |
                        // 保留当前的 Z 顺序(忽略 hWndInsertAfter 参数)
                        WinUser.SWP_NOZORDER;

        // 更新SetWindowLong函数设置的样式
        User32.INSTANCE.SetWindowPos(window, null, 0, 0, 0, 0, uFlags);
    }

    public static WinDef.HWND findWindow(String className, String windowName) {
        requireWindowsOS();
        return User32.INSTANCE.FindWindow(className, windowName);
    }

    protected static void requireWindowsOS() {
        if (SystemUtils.IS_OS_WINDOWS) return;
        throw new IllegalStateException("unsupported operation");
    }

}

Demo: https://videos.ximinghui.org/230303_demo_running_on_win10.mp4

Note: The top 6px white bar question redirects Create window without titlebar, with resizable border and without bogus 6px white stripe