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
相关产品推荐
相关产品推荐

