You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

JavaFX macOS透明标题栏窗口无法拖动问题求解

问题描述

我通过Java FFM操作macOS原生NSWindow对象控制JavaFX窗口,将titlebarAppearsTransparent设为true并添加NSWindowStyleMaskFullSizeContentView后,只能拖动窗口最上方边框实现移动。现在标题栏已经不可见,我希望能通过一个“虚拟”标题栏区域拖动窗口,请问该如何实现?

以下是我目前的实现代码:

1. 获取NSWindow句柄

private long getNSWindowHandle() throws Throwable {

    // Get class NSApplication
    try (Arena arena = Arena.ofConfined()) {

        long nsApplication = (long) objc_getClass.invoke(arena.allocateFrom("NSApplication"));
        System.out.println("nsApplication: " + Long.toHexString(nsApplication));


        // Get selector for sharedApplication
        long sharedAppSelector = (long) sel_registerName.invoke(arena.allocateFrom("sharedApplication"));
        System.out.println("sharedAppSelector: " + Long.toHexString(sharedAppSelector));

        // Get NSApplication shared instance
        long sharedApplication = (long) LINKER.downcallHandle(
                        symbolLookup.find("objc_msgSend").orElseThrow(),
                        FunctionDescriptor.of(ValueLayout.JAVA_LONG, ValueLayout.JAVA_LONG, ValueLayout.JAVA_LONG))
                                                .invoke(nsApplication, sharedAppSelector);
        System.out.println("sharedApplication: " + Long.toHexString(sharedApplication));

        // Get selector for windows array
        long windowsSelector = (long) sel_registerName.invoke(arena.allocateFrom("windows"));
        System.out.println("windowsSelector: " + Long.toHexString(windowsSelector));

        // Get array of windows (NSArray of NSWindow objects)
        long windowsArray = (long) LINKER.downcallHandle(
                        symbolLookup.find("objc_msgSend").orElseThrow(),
                        FunctionDescriptor.of(ValueLayout.JAVA_LONG, ValueLayout.JAVA_LONG, ValueLayout.JAVA_LONG))
                                           .invoke(sharedApplication, windowsSelector);
        System.out.println("windowsArray: " + Long.toHexString(windowsArray));

        // Get selector for objectAtIndex:
        long objectAtIndexSelector = (long) sel_registerName.invoke(arena.allocateFrom("objectAtIndex:"));
        System.out.println("objectAtIndexSelector: " + Long.toHexString(objectAtIndexSelector));

        // Get first window (NSWindow) in the array (index 0)
        long nsWindow = (long) LINKER.downcallHandle(
                        symbolLookup.find("objc_msgSend").orElseThrow(),
                        FunctionDescriptor.of(ValueLayout.JAVA_LONG, ValueLayout.JAVA_LONG, ValueLayout.JAVA_LONG, ValueLayout.JAVA_LONG))
                                       .invoke(windowsArray, objectAtIndexSelector, (long) 0);
        System.out.println("nsWindow: " + Long.toHexString(nsWindow));
        
        return nsWindow;
    }
}

2. 修改NSWindow样式

private void setCustomWindow(long nsWindow) throws Throwable {

    try (Arena arena = Arena.ofConfined()) {

        // TRANSPARENT TITLE BAR
        var titleBarTransparentSelector = (long) sel_registerName.invoke(arena.allocateFrom("setTitlebarAppearsTransparent:"));
        LINKER.downcallHandle(
                        symbolLookup.find("objc_msgSend").orElseThrow(),
                        FunctionDescriptor.of(ValueLayout.JAVA_LONG, ValueLayout.JAVA_LONG, ValueLayout.JAVA_LONG, ValueLayout.JAVA_BOOLEAN))
                .invoke(nsWindow, titleBarTransparentSelector, true);


        // UPDATE STYLE MASK
        long styleMaskSelector = (long) sel_registerName.invoke(arena.allocateFrom("styleMask"));
        long setStyleMaskSelector = (long) sel_registerName.invoke(arena.allocateFrom("setStyleMask:"));
        long styleMask = (long) LINKER.downcallHandle(
                        symbolLookup.find("objc_msgSend").orElseThrow(),
                        FunctionDescriptor.of(ValueLayout.JAVA_LONG, ValueLayout.JAVA_LONG, ValueLayout.JAVA_LONG))
                                        .invoke(nsWindow, styleMaskSelector);

        styleMask &= ~NSWindowStyleMaskUnifiedTitleAndToolbar;
        styleMask |= NSWindowStyleMaskFullSizeContentView;
        styleMask |= NSWindowStyleMaskTitled;
        LINKER.downcallHandle(symbolLookup.find("objc_msgSend")
                                      .orElseThrow(), FunctionDescriptor.of(ValueLayout.JAVA_LONG, ValueLayout.JAVA_LONG, ValueLayout.JAVA_LONG, ValueLayout.JAVA_LONG))
                .invoke(nsWindow, setStyleMaskSelector, styleMask);

        // TITLE SEPARATOR
        int titleSeparator = 1;
        long setTitlebarSeparator = (long) sel_registerName.invoke(arena.allocateFrom("setTitlebarSeparatorStyle:"));
        LINKER.downcallHandle(symbolLookup.find("objc_msgSend")
                                      .orElseThrow(), FunctionDescriptor.of(ValueLayout.JAVA_LONG, ValueLayout.JAVA_LONG, ValueLayout.JAVA_LONG, ValueLayout.JAVA_INT))
                .invoke(nsWindow, setTitlebarSeparator, titleSeparator);

        // TITLE VISIBILITY (0 = visible, 1 = hidden)
        int titleVisible = 0;
        long setTitleVisibility = (long) sel_registerName.invoke(arena.allocateFrom("setTitleVisibility:"));
        LINKER.downcallHandle(symbolLookup.find("objc_msgSend")
                                      .orElseThrow(), FunctionDescriptor.of(ValueLayout.JAVA_LONG, ValueLayout.JAVA_LONG, ValueLayout.JAVA_LONG, ValueLayout.JAVA_INT))
                .invoke(nsWindow, setTitleVisibility, titleVisible);


        // MOVABLE
        var setMovableSelector = (long) sel_registerName.invoke(arena.allocateFrom("setMovable:"));
        LINKER.downcallHandle(symbolLookup.find("objc_msgSend")
                                      .orElseThrow(), FunctionDescriptor.of(ValueLayout.JAVA_LONG, ValueLayout.JAVA_LONG, ValueLayout.JAVA_LONG, ValueLayout.JAVA_BOOLEAN))
                .invoke(nsWindow, setMovableSelector, true);

        // MOVABLE BY BACKGROUND
        var isMovableByBackgroundSelector = (long) sel_registerName.invoke(arena.allocateFrom("setMovableByWindowBackground:"));
        LINKER.downcallHandle(symbolLookup.find("objc_msgSend")
                                      .orElseThrow(), FunctionDescriptor.of(ValueLayout.JAVA_LONG, ValueLayout.JAVA_LONG, ValueLayout.JAVA_LONG, ValueLayout.JAVA_BOOLEAN))
                .invoke(nsWindow, isMovableByBackgroundSelector, true);

    }
}
解决方案

要让透明标题栏区域支持拖动,核心是告诉NSWindow哪些区域属于“标题栏拖动区”。macOS提供了setContentBorderThickness:forEdge:方法,可以给窗口顶部设置一个不可见的边框厚度,这个区域会被系统识别为可拖动的标题栏区域。

另外,需要将titleVisibility设置为NSWindowTitleHidden(对应值1),让标题完全隐藏,但保留标题栏的交互区域。

修改setCustomWindow方法,添加以下逻辑:

// 设置顶部内容边框厚度,模拟虚拟标题栏高度(28pt和原生标题栏高度一致,可按需调整)
long setContentBorderThicknessSelector = (long) sel_registerName.invoke(arena.allocateFrom("setContentBorderThickness:forEdge:"));
// NSMaxYEdge对应窗口顶部边缘,值为2
int maxYEdge = 2;
float borderThickness = 28.0f;
LINKER.downcallHandle(
                symbolLookup.find("objc_msgSend").orElseThrow(),
                FunctionDescriptor.of(ValueLayout.JAVA_LONG, ValueLayout.JAVA_LONG, ValueLayout.JAVA_LONG, ValueLayout.JAVA_FLOAT, ValueLayout.JAVA_INT))
        .invoke(nsWindow, setContentBorderThicknessSelector, borderThickness, maxYEdge);

// 修正标题可见性为隐藏
int titleVisible = 1;
long setTitleVisibility = (long) sel_registerName.invoke(arena.allocateFrom("setTitleVisibility:"));
LINKER.downcallHandle(symbolLookup.find("objc_msgSend")
                              .orElseThrow(), FunctionDescriptor.of(ValueLayout.JAVA_LONG, ValueLayout.JAVA_LONG, ValueLayout.JAVA_LONG, ValueLayout.JAVA_INT))
        .invoke(nsWindow, setTitleVisibility, titleVisible);

同时,在JavaFX布局中给顶部留出对应高度的空白区域,避免内容被拖动区遮挡:

// 顶部添加28px的透明Pane作为虚拟标题栏占位
Pane virtualTitleBar = new Pane();
virtualTitleBar.setPrefHeight(28);
virtualTitleBar.setBackground(new Background(new BackgroundFill(Color.TRANSPARENT, CornerRadii.EMPTY, Insets.EMPTY)));

VBox root = new VBox(virtualTitleBar, yourContent);
scene.setRoot(root);

修改后,窗口顶部对应厚度的区域会被系统识别为可拖动的标题栏区域,即使标题栏透明且隐藏。


内容的提问来源于stack exchange,提问作者user27408155

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.17 12:45:04