ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

FlatLaf 原生库研发原型:使用 JNA 验证 Windows/Linux 原生窗口 API 的实践指南

FlatLaf 原生库研发原型:使用 JNA 验证 Windows/Linux 原生窗口 API 的实践指南
  • 桌面应用
  • UI组件

【免费下载链接】FlatLaf

FlatLaf - Swing Look and Feel (with Darcula/IntelliJ themes support)

项目地址:https://gitcode.com/gh_mirrors/fl/FlatLaf
点击查看免费下载

FlatLaf 是一个为 Swing 提供扁平化外观的 Look and Feel 库,其部分高级特性(如 Windows 原生窗口边框、Linux 窗口移动与系统菜单)依赖操作系统原生 API。为了在 Java 中先行验证这些 API 的调用方式,FlatLaf 仓库中保留了一个专门的研发子项目——flatlaf-natives/flatlaf-natives-jna,它通过 JNA(Java Native Access)直接访问 Windows 与 Linux 的系统级接口。阅读本文,你将理解这个"研发原型"子项目的定位、核心源码实现、它与最终 C++ 原生库之间的演进关系,以及如何在开发环境中构建与使用它来验证原生 API 行为。

子项目定位:为什么 FlatLaf 需要一个"JNA 原型库"

在 flatlaf-natives-jna/README.md 中,项目作者对子项目做了三点明确说明:

  1. 该子项目包含使用 JNA 访问原生操作系统 API的源代码;
  2. 关键约束:这里的代码并未被 FlatLaf 正式库使用(Code in this sub-project is not used in FlatLaf libraries);
  3. 它的实际用途是研发与测试——在 Java 中借助 JNA 验证某些原生操作系统 API 的用法,验证通过后再将其转换为 C++ 实现,进入正式的flatlaf-natives-*原生库。

也就是说,flatlaf-natives-jna是 FlatLaf 原生能力研发流程中的"探路者":先用 JNA 快速验证 API 参数、结构体布局与消息处理逻辑,再以性能与部署更优的 C++/JNI 方式落地。理解这一点,是阅读本子项目全部代码的前提——它的价值在于研究参考,而不是直接依赖。

从仓库结构看,settings.gradle.kts 将四个原生子项目并列纳入构建:flatlaf-natives-windows、flatlaf-natives-macos、flatlaf-natives-linux与flatlaf-natives-jna;而 flatlaf-natives/README.md 明确将flatlaf-natives-jna标注为 "for development only"(仅用于开发),进一步印证了其研发原型的定位。

项目结构与构建配置

子项目目录结构非常精简,仅包含两个平台下的核心原型类:

flatlaf-natives/flatlaf-natives-jna/ ├── README.md ├── build.gradle.kts └── src/main/java/com/formdev/flatlaf/natives/jna/ ├── linux/X11WmUtils.java └── windows/FlatWindowsNativeWindowBorder.java

其构建脚本 build.gradle.kts 声明了两个依赖:

  • implementation( project( ":flatlaf-core" ) ):依赖 FlatLaf 核心模块,以便复用SystemInfo、UIScale等工具类以及FlatNativeWindowBorder.Provider接口;
  • implementation( libs.jna )与implementation( libs.jna.platform ):JNA 及其平台封装库。版本统一由 gradle/libs.versions.toml 管理,当前为JNA 5.19.1。

其中jna-platform提供了User32、GDI32、Shell32、X11、Advapi32Util等现成的 Win32/X11 声明,大幅降低了手写原生函数签名的成本——这正是原型验证阶段选择 JNA 的核心原因之一。

Windows 原型:FlatWindowsNativeWindowBorder 的窗口过程接管

Windows 侧的原型类是 FlatWindowsNativeWindowBorder.java,它实现了FlatNativeWindowBorder.Provider接口(该接口定义于 FlatNativeWindowBorder.java)。其目标是:在应用使用自定义装饰时,隐藏 Windows 10 标题栏(含最小化/最大化/关闭按钮),但保留缩放边框与投影,且不影响系统的窗口对齐(Snap)功能。

Provider 接口与生命周期管理

FlatWindowsNativeWindowBorder以单例方式提供(getInstance()),内部用IdentityHashMap<Window, WndProc>维护"窗口 → 自定义窗口过程"的映射,核心生命周期如下:

  • setHasCustomDecoration(window, true)触发install(window):仅当运行于 Windows 10 及以上、窗口是JFrame/JDialog且不是 undecorated时才安装;
  • setHasCustomDecoration(window, false)触发uninstall(window):移除自定义窗口过程并恢复原生标题栏;
  • updateTitleBarInfo(...):由 Swing 侧告知标题栏高度、应用图标与三个窗口按钮(最小化/最大化/关闭)的点击区域矩形,供命中测试使用;
  • showWindow(window, cmd):包装User32.INSTANCE.ShowWindow,命令常量(SW_MAXIMIZE=3、SW_MINIMIZE=6、SW_RESTORE=9)与 Win32 API 保持一致。

WndProc:替换窗口过程

内部类WndProc implements WindowProc是原型的技术核心。它在构造时:

  1. 通过Native.getComponentPointer(window)取得窗口的HWND;
  2. 按架构区分调用SetWindowLongPtr(x86_64)或SetWindowLong(x86)替换窗口过程(GWLP_WNDPROC = -4),并保存原始过程defaultWndProc;
  3. 调用updateFrame()通过SetWindowPos(... SWP_FRAMECHANGED ...)触发WM_NCCALCSIZE,从而移除系统标题栏;
  4. 根据window.getBackground()用GDI32Ex.CreateSolidBrush创建背景画刷,避免窗口缩放/移动时背景闪烁。

原型类中定义了完整的 Windows 消息常量,包括:

WM_NCCALCSIZE = 0x0083 WM_NCHITTEST = 0x0084 WM_ENTERSIZEMOVE = 0x0231 WM_EXITSIZEMOVE = 0x0232 WM_DPICHANGED = 0x02E0 WM_DWMCOLORIZATIONCOLORCHANGED = 0x0320 HTCLIENT=1 HTCAPTION=2 HTSYSMENU=3 HTMINBUTTON=8 HTMAXBUTTON=9 HTTOP=12 HTCLOSE=20

WM_NCCALCSIZE:隐藏标题栏与修正客户区

WmNcCalcSize处理流程(与 Microsoft Terminal 的NonClientIslandWindow::_OnNcCalcSize()思路一致):

  1. 先保存默认窗口过程计算前的原始rgrc[0].top;
  2. 调用默认窗口过程应用系统默认边框;
  3. 把顶部恢复为原始值,即让客户区顶到窗口顶部,实现标题栏隐藏;
  4. 若窗口处于最大化且非全屏状态:将顶部加上缩放手柄高度(SM_CXPADDEDBORDER + SM_CYSIZEFRAME),并处理任务栏自动隐藏(ABM_GETAUTOHIDEBAREX)情况下的边距修正;
  5. 在 Windows 11 上,通过DwmGetWindowAttribute(hwnd, DWMWA_VISIBLE_FRAME_BORDER_THICKNESS=37, ...)获取可见边框厚度并加到顶部,避免窗口顶部出现黑线。

WM_NCHITTEST:命中测试与窗口按钮

WmNcHitTest决定鼠标所在位置属于哪个窗口区域(标题栏、缩放边框、应用图标、窗口按钮等):

  • 先调用默认窗口过程处理左右下三边;若结果不是HTCLIENT则直接返回;
  • 将屏幕坐标换算为窗口坐标,再按GraphicsConfiguration.getDefaultTransform()做降采样(scaleDown),与 AWT 在 HiDPI 下的坐标处理保持一致;
  • 顶部缩放边框被限制在UIScale.scale(4)像素内(现代 WinUI 应用的标准),并根据鼠标位置返回HTTOPLEFT/HTTOPRIGHT/HTTOP;
  • 依次命中测试应用图标(HTSYSMENU)、最小化(HTMINBUTTON)、最大化(HTMAXBUTTON)、关闭(HTCLOSE)按钮区域;
  • 标题栏区域通过captionHitTestCallback回调让 Swing 判断该点是否落在处理鼠标事件的组件上,从而返回HTCLIENT(Swing 可处理)或HTCAPTION(可拖动窗口)。

注释中还记录了平台差异细节:Windows 11 上鼠标悬停HTMAXBUTTON会弹出 Snap Layouts 菜单;系统菜单(HTSYSMENU)区域左键单击弹菜单、双击发送WM_CLOSE。

DWM 颜色化信息与系统菜单

原型还实现了两处系统集成:

  • 颜色化查询:从注册表SOFTWARE\Microsoft\Windows\DWM读取ColorPrevalence、ColorizationColor、ColorizationColorBalance,用于计算激活窗口顶边颜色(对应 flatlaf-core 中FlatNativeWindowBorder.WindowTopBorder的配色逻辑);收到WM_DWMCOLORIZATIONCOLORCHANGED时通过fireStateChangedLaterOnce()在 AWT 事件线程上延迟 300ms 合并触发一次ChangeListener;
  • 系统菜单:右键标题栏/图标时,用GetSystemMenu获取系统菜单,按窗口状态(最大化、有无缩放边框等)更新SC_RESTORE/SC_MOVE/SC_SIZE/SC_MINIMIZE/SC_MAXIMIZE/SC_CLOSE的启用状态,并将SC_CLOSE设为默认项,再以TrackPopupMenu(TPM_RETURNCMD)弹出,选中后PostMessage(WM_SYSCOMMAND)执行。

Linux 原型:X11WmUtils 与窗口管理器协议消息

Linux 侧原型类是 X11WmUtils.java,目标是通过 X11 客户端消息(ClientMessage)与窗口管理器通信,实现两个功能:

  • xMoveOrResizeWindow(window, x, y, direction):发送_NET_WM_MOVERESIZE(freedesktop EWMH 规范定义的协议),direction取值 0~7 对应八个缩放方向、8 对应移动;
  • xShowWindowMenu(window, x, y):发送_GTK_SHOW_WINDOW_MENU(GTK 私有协议),请求窗口管理器在指定坐标弹出系统窗口菜单。

sendEvent的调用链展示了 JNA 与 X11 交互的完整套路:

  1. 用sun.awt.SunToolkit.awtLock()加 AWT 锁,保证与 AWT 的互斥;
  2. XOpenDisplay(null)打开默认 Display,XDefaultRootWindow获取根窗口;
  3. 用Native.getComponentPointer(window)取 AWT 窗口的 X11 Window ID;
  4. XUngrabPointer/XUngrabKeyboard解除指针与键盘捕获,让窗口管理器能够接管;
  5. 构造XClientMessageEvent:message_type = XInternAtom(display, atom_name, false),format = 32,data.l[0..4]填入参数(移动缩放时含按钮Button1与 source indication=1);
  6. XSendEvent发给根窗口(掩码SubstructureNotifyMask | SubstructureRedirectMask),随后XFlush与XCloseDisplay。

需要特别注意的是,源码注释中给出了明确的警告:

Warning: Although the implementation of this method is (nearly) identical to the C++ implementation, this one does not work correctly. DO NOT USE.

即该 JNA 版本虽然与最终 C++ 实现逻辑几乎一致,但实际不能正常工作(原型中保留了大量System.out.println调试输出与// TODO标记可佐证其"未完成/仅供研究"状态),这正是它没有进入正式库的原因之一。

从 JNA 原型到 C++ 实现的演进路径

对比 JNA 原型与其对应的正式 C++ 实现,可以清晰还原"验证 → 落地"的演进过程。

Linux 侧:正式实现位于 flatlaf-natives-linux/src/main/cpp/X11WmUtils.cpp,导出Java_com_formdev_flatlaf_ui_FlatNativeLinuxLibrary_xMoveOrResizeWindow与xShowWindowMenu两个 JNI 方法。与 JNA 版相比,C++ 版补齐了原型缺失的关键环节:

  • 通过JAWT_GetAWT获取 AWT 接口、getWindowHandle取得Display*与窗口句柄,并用awt.Lock/awt.Unlock同步;
  • 新增isWMHintSupported():读取根窗口的_NET_SUPPORTED属性,先检查窗口管理器是否支持该协议,不支持则直接返回 false——这是 JNA 原型没有的健壮性处理;
  • 事件结构体XClientMessageEvent直接栈上初始化,无 JNA 结构体往返开销。

Java 侧调用方是 FlatNativeLinuxLibrary.java,其中moveOrResizeWindow/showWindowMenu的实现体里保留了一段被注释掉的 JNA 反射调用代码(对应原型类X11WmUtils),实际执行的则是private static native声明的 JNI 方法——这是"原型验证后转为 C++"的最直接证据。

Windows 侧:正式实现位于 flatlaf-natives-windows/src/main/cpp/FlatWndProc.cpp。FlatWndProc::WindowProc的WM_NCCALCSIZE/WM_NCHITTEST处理与 JNA 原型逻辑一致,但采用 C++ 原生实现 + JNI 回调的方式:原生代码通过onNcHitTestMID(GetMethodID(cls, "onNcHitTest", "(IIZ)I"))回调 Java 侧的命中测试逻辑,将"原生窗口消息处理"与"Swing 坐标/组件判断"分离。Windows 正式类 FlatWindowsNativeWindowBorder.java 的getInstance()会检查FlatNativeWindowsLibrary.isLoaded()(即原生库是否成功加载),而不是直接使用 JNA 类。

集成边界:为什么正式库不使用 JNA

除了 README 的明文说明,源码中还能找到多处"JNA 已被 C++ 取代"的痕迹:

  1. FlatNativeWindowBorder.java 的initialize()中,反射加载 JNA 类com.formdev.flatlaf.natives.jna.windows.FlatWindowsNativeWindowBorder的代码整体被注释,实际改为调用 flatlaf-core 包内的FlatWindowsNativeWindowBorder.getInstance()(JNI 版本);
  2. FlatNativeLinuxLibrary.java 中同样保留了被注释的 JNA 反射分支,正式路径走native方法;
  3. flatlaf-demo/build.gradle.kts 与 flatlaf-testing/build.gradle.kts 中对:flatlaf-natives-jna的依赖均处于注释状态。

从工程角度,这种演进是合理的:正式库需要通过 FlatNativeLibrary.java 按 OS/架构(windows-x86_64、linux-arm64等 classifier)加载.dll/.so/.dylib,并从java.library.path、jar 旁目录或 jar 内临时解压三种途径获取原生库,还会校验getApiVersion()的 API 版本号——这套部署与加载机制更适合静态编译的 JNI 库,而非运行时绑定 JNA。

在开发环境中构建与使用该子项目

虽然正式应用不应依赖flatlaf-natives-jna,但作为研究工具,开发者可以按以下方式在本地构建:

# 在仓库根目录执行,编译该子项目 ./gradlew :flatlaf-natives-jna:compileJava # 查看其依赖(确认 JNA 5.19.1 已解析) ./gradlew :flatlaf-natives-jna:dependencies

该子项目依赖flatlaf-core(复用SystemInfo/UIScale等工具)以及 JNA/JNA-Platform 5.19.1(见 gradle/libs.versions.toml)。若要临时启用它做实验,可参照仓库中已被注释的用法,在 flatlaf-demo/build.gradle.kts 中放开implementation( project( ":flatlaf-natives-jna" ) )并加入对应调用。

需要强调的注意事项:

  • 不要在正式应用中使用该子项目:README 明确声明其代码不用于 FlatLaf 库;Linux 的X11WmUtils源码更直接标注 "DO NOT USE";
  • 平台前提:Windows 侧代码依赖 Windows 10+ 的窗口消息体系与 DWM 注册表键,Linux 侧依赖 X11(XToolkit)与支持 EWMH 协议的窗口管理器(GNOME Mutter、KDE KWin 等),且部分行为随窗口管理器实现而异;
  • 研究价值:该子项目保留了完整的消息常量、结构体布局(如NCCALCSIZE_PARAMS、MENUITEMINFO的@FieldOrder声明)与注释中的参考链接指向,是学习"如何用 JNA 与 Win32/X11 交互"的高质量示例——包括坐标换算、HiDPI 缩放、注册表读取、系统菜单操作等细节。

效果验证与进一步探索

FlatLaf 仓库的测试模块提供了与该原生能力直接相关的验证入口:

  • FlatNativeWindowBorderTest.java:创建JFrame与模态JDialog,用于验证原生窗口边框行为;
  • FlatWindowDecorationsTest.java:提供 "FlatLaf window decorations" 复选框,可交互式开关FlatLaf.setUseNativeWindowDecorations(...),直观对比系统装饰与 FlatLaf 装饰;
  • 仓库中的实拍截图 Windows 10 decorations.png 与 Windows 11 decorations.png,展示了该特性在两种系统上的实际渲染效果。

![Windows 10 下 FlatLaf 自定义窗口装饰的实拍效果](https://raw.gitcode.com/gh_mirrors/fl/FlatLaf/raw/dc2afcaa443027d40a0c15938d785ba01b2ac49c/flatlaf-testing/Windows 10 decorations.png?utm_source=gitcode_repo_files)

![Windows 11 下 FlatLaf 自定义窗口装饰的实拍效果(含 Snap Layouts 等平台特性)](https://raw.gitcode.com/gh_mirrors/fl/FlatLaf/raw/dc2afcaa443027d40a0c15938d785ba01b2ac49c/flatlaf-testing/Windows 11 decorations.png?utm_source=gitcode_repo_files)

总而言之,flatlaf-natives-jna是 FlatLaf 原生窗口能力研发链路中承上启下的一环:它用 JNA 在 Java 侧快速验证了 Windows 窗口过程接管、命中测试、DWM 颜色化与 Linux 窗口管理器协议消息等复杂原生交互,随后这些验证成果被移植为高性能的 C++/JNI 实现并集成进正式库。对研究 Swing 与操作系统原生层互操作的开发者而言,它是一份不可多得的、带有完整调试轨迹的活教材。

  • 桌面应用
  • UI组件

【免费下载链接】FlatLaf

FlatLaf - Swing Look and Feel (with Darcula/IntelliJ themes support)

项目地址:https://gitcode.com/gh_mirrors/fl/FlatLaf
点击查看免费下载

相关推荐

上一篇:Sphinx 解析器(Parser)API 详解:自定义源解析器与 source_suffix 注册机制
下一篇:Echo Music架构深剖:读懂这款开源音乐播放器的15个模块与设计哲学

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进