如何通过Linux原生代码在JavaFX中实现任务栏进度更新
我想通过项目实践掌握Java 22+的Foreign Function & Memory API,计划实现一个JavaFX程序,在Linux任务栏显示长耗时任务的进度。目前JavaFX无原生支持,现有类库如FXTaskbarProgressBar仅支持Windows且基于JNI。
参考Go语言的taskbar库,我用jextract生成Xlib的Java绑定,执行命令:
jextract --output target/generated-sources/jextract -t "taskbar_test.gen" --include-function "XOpenDisplay" --include-function "XChangeProperty" --include-function "XFlush" --include-function "XCloseDisplay" /usr/include/X11/Xlib.h
随后编写了模拟长任务并调用XChangeProperty更新任务栏进度的JavaFX代码,但程序运行后任务栏无进度更新,程序未崩溃。代码如下:
package taskbar_test; import com.sun.glass.ui.Window; import javafx.application.Application; import javafx.concurrent.Task; import javafx.scene.Scene; import javafx.scene.control.Button; import javafx.scene.layout.VBox; import javafx.stage.Stage; import taskbar_test.gen.Xlib_h; import java.lang.foreign.Arena; import java.lang.foreign.MemorySegment; import java.lang.foreign.ValueLayout; public class AppLinuxXlib extends Application { @Override public void start(Stage primaryStage) { Button startButton = new Button("Start Long Running Task"); startButton.setOnAction(event -> { final long rawHandle = Window.getWindows().getFirst().getRawHandle(); System.out.println(rawHandle); // Create a long-running task Task<Void> longTask = new Task<>() { @Override protected Void call() throws Exception { System.out.println("Started"); try (var arena = Arena.ofConfined()) { var NET_WM_XAPP_PROGRESS = arena.allocateFrom("NET_WM_XAPP_PROGRESS"); // var NET_WM_XAPP_PROGRESS_PULSE = arena.allocateFrom("NET_WM_XAPP_PROGRESS_PULSE"); MemorySegment x11Session = Xlib_h.XOpenDisplay(MemorySegment.NULL); System.out.println(x11Session); // Prepare the progress data MemorySegment initData = arena.allocateFrom(ValueLayout.JAVA_INT, 0); Xlib_h.XChangeProperty(x11Session, // display MemorySegment.ofAddress(rawHandle).address(), // window NET_WM_XAPP_PROGRESS.address(), // property 6, // type 32, // format 0, // mode PropModeReplace=0 initData, // data 1); // nelements Xlib_h.XFlush(x11Session); System.out.println("Countdown started"); // Set the taskbar progress for (int i = 0; i <= 100; i+=20) { // Simulate work Thread.sleep(500); System.out.println(i); MemorySegment progressData = arena.allocateFrom(ValueLayout.JAVA_INT, i); // Update taskbar progress // https://www.x.org/releases/X11R7.7/doc/libX11/libX11/libX11.html#XChangeProperty Xlib_h.XChangeProperty(x11Session, // display MemorySegment.ofAddress(rawHandle).address(), // window NET_WM_XAPP_PROGRESS.address(), // property 6, // type 32, // format 0, // mode PropModeReplace=0 progressData, // data 1); // nelements Xlib_h.XFlush(x11Session); } System.out.println("Finished"); Xlib_h.XCloseDisplay(x11Session); } catch (Throwable ex) { ex.printStackTrace(); } return null; } }; // Start the task in a new thread new Thread(longTask).start(); }); VBox vbox = new VBox(10, startButton); Scene scene = new Scene(vbox, 300, 200); primaryStage.setScene(scene); primaryStage.setTitle("Taskbar Progress Example Linux"); primaryStage.show(); } public static void main(String[] args) { launch(args); } }
请问我的代码存在什么问题,正确的实现方向是什么?
你的代码存在的核心问题
属性名传递错误
Xlib要求窗口属性名必须是原子值(Atom),不是字符串的内存指针。你直接把NET_WM_XAPP_PROGRESS字符串的内存地址传给XChangeProperty,桌面环境无法识别这个属性。必须先用XInternAtom将字符串转换为对应的Atom值。窗口ID参数传递错误
XChangeProperty的第二个参数是窗口ID(无符号长整型),但你传递的是MemorySegment.ofAddress(rawHandle).address()——这相当于把窗口ID的内存地址当成了窗口ID本身,完全不符合Xlib的参数要求,应该直接传递rawHandle对应的MemorySegment(或直接传数值,取决于jextract生成的绑定类型)。属性类型参数硬编码错误
你给type参数传了6,这是无意义的硬编码。对于NET_WM_XAPP_PROGRESS这类进度属性,类型应该是XA_CARDINAL(需要用XInternAtom获取对应的原子值)。缺少进度条显示开关属性
仅设置NET_WM_XAPP_PROGRESS进度值没用,必须同时设置NET_WM_XAPP_PROGRESS_VISIBLE为1,桌面环境才会渲染任务栏进度条;任务结束后还要把这个属性设为0,隐藏进度条。
正确的实现方向
补充Xlib函数绑定
修改jextract命令,添加XInternAtom函数的绑定,因为转换字符串到原子值必须用这个函数:jextract --output target/generated-sources/jextract -t "taskbar_test.gen" --include-function "XOpenDisplay" --include-function "XChangeProperty" --include-function "XFlush" --include-function "XCloseDisplay" --include-function "XInternAtom" /usr/include/X11/Xlib.h修正参数与属性处理逻辑
- 用
XInternAtom获取NET_WM_XAPP_PROGRESS、NET_WM_XAPP_PROGRESS_VISIBLE、XA_CARDINAL对应的原子值; - 正确传递窗口ID:用
MemorySegment.ofAddress(rawHandle)作为窗口参数(匹配Xlib的Window类型要求); - 将
type参数替换为XA_CARDINAL的原子值。
- 用
完善进度条生命周期管理
- 任务启动时,先设置
NET_WM_XAPP_PROGRESS_VISIBLE为1,再设置初始进度; - 任务执行过程中同步更新进度值;
- 任务结束后,设置
NET_WM_XAPP_PROGRESS_VISIBLE为0,清理进度条。
- 任务启动时,先设置
线程安全注意
X11连接通常是单线程上下文,确保所有Xlib调用都在同一个后台线程中执行,避免跨线程操作X11连接导致的异常。
修正后的核心代码片段
try (var arena = Arena.ofConfined()) { MemorySegment x11Session = Xlib_h.XOpenDisplay(MemorySegment.NULL); if (x11Session.address() == 0) { throw new RuntimeException("Failed to open X11 display"); } // 获取必要的原子值 var netWmXappProgress = Xlib_h.XInternAtom(x11Session, arena.allocateFrom("NET_WM_XAPP_PROGRESS"), false); var netWmXappProgressVisible = Xlib_h.XInternAtom(x11Session, arena.allocateFrom("NET_WM_XAPP_PROGRESS_VISIBLE"), false); var xaCardinal = Xlib_h.XInternAtom(x11Session, arena.allocateFrom("CARDINAL"), false); // 显示进度条 MemorySegment visibleData = arena.allocateFrom(ValueLayout.JAVA_INT, 1); Xlib_h.XChangeProperty(x11Session, MemorySegment.ofAddress(rawHandle), netWmXappProgressVisible, xaCardinal, 32, 0, visibleData, 1); Xlib_h.XFlush(x11Session); // 更新进度 for (int i = 0; i <= 100; i+=20) { Thread.sleep(500); MemorySegment progressData = arena.allocateFrom(ValueLayout.JAVA_INT, i); Xlib_h.XChangeProperty(x11Session, MemorySegment.ofAddress(rawHandle), netWmXappProgress, xaCardinal, 32, 0, progressData, 1); Xlib_h.XFlush(x11Session); } // 隐藏进度条 MemorySegment hiddenData = arena.allocateFrom(ValueLayout.JAVA_INT, 0); Xlib_h.XChangeProperty(x11Session, MemorySegment.ofAddress(rawHandle), netWmXappProgressVisible, xaCardinal, 32, 0, hiddenData, 1); Xlib_h.XFlush(x11Session); Xlib_h.XCloseDisplay(x11Session); } catch (Throwable ex) { ex.printStackTrace(); }
内容的提问来源于stack exchange,提问作者Petr Štechmüller

