☰
JavaFX+Jfoenix桌面应用:系统托盘集成与中文乱码完整解决方案
2026/10/20 9:54:37 网站建设 项目流程

简介:面向已有JavaFX基础、正需为桌面应用补充系统托盘交互并处理中文乱码的开发者,这份源码源自JavaFX加Jfoenix学习笔记第十篇,适合作为进阶实战参考。压缩包共80个文件,以58个class编译类、3个java源文件、2个xml配置、2个css样式、6个png图标、1个properties资源文件为主,另有ico图标和Eclipse工程配置,整体仅262KB,目录结构清晰,其中xml与properties承担配置,css负责界面样式,png及ico用作窗体与托盘图标。资源已有1674人浏览学习,内容覆盖基于java.awt.SystemTray的托盘功能,从检查系统支持、创建TrayIcon、绑定监听事件到将窗口最小化至托盘的完整流程,并提及右键菜单扩展思路;中文乱码方面,整理出UTF-8编码、pom.xml配置、FXMLLoader字符集指定等解决方案。直接导入即可运行,还可对照学习笔记逐段拆解,或作为新项目的托盘交互模块复用模板,便于快速应用到实际桌面项目。

1. JavaFX+Jfoenix 学习笔记(十):系统托盘、中文乱码,这两件事为什么总是一起翻车

做 JavaFX 桌面应用的人,早晚会遇到两个一起出现的坎:把窗口最小化到系统托盘,以及界面上中文变豆腐块。标题里“系统托盘及解决中文乱码问题源码”把这两个问题放在一起,不是我故意折腾自己,是因为真实项目里它们几乎同时爆发——用 JFoenix 写界面,默认字体是 Roboto,到了 Windows 上中文要么回退成宋体,要么直接显示成方框;再把代码打包成 exe,控制台又给中文补一刀乱码。这篇笔记就是把一套能直接跑起来的 JavaFX+Jfoenix 托盘源码拆开讲,含 AWT 与 JavaFX 线程怎么协作、JFoenix 字体怎么接管、Maven 和打包时编码怎么统一。适合准备用 JavaFX 做公司内部工具、需要托盘常驻,并且被中文乱码折磨过的开发者。

2. 先搞清楚托盘架构:JavaFX 窗口和 AWT 托盘怎么协作

2.1 JavaFX 没有托盘 API,所以要和 AWT 的 SystemTray 联动

纯 JavaFX 只负责场景图上的渲染,官方没有提供系统托盘能力。想往系统托盘放图标,必须用 JDK 自带的java.awt.SystemTray和TrayIcon。这两个类从 JDK 1.6 起就没大改过,稳定跨平台,但代价是它们属于 AWT/Swing 的事件模型,而你的 JavaFX 界面跑在 FX Application Thread 上。两类线程没有默认的桥接通道,窗口的显示、隐藏、退出都必须通过Platform.runLater切回 FX 线程执行。

很多人在这里踩的第一个坑,是直接在Application.start()里创建TrayIcon。表面上看不报错,但到 Windows 上会出现奇怪现象:托盘图标偶尔不出现、右键菜单偶尔卡住、双击事件丢一两个。原因不是SystemTray本身有问题,而是你在 FX 线程里操作了 AWT 组件。AWT 组件要求大部分操作在 AWT Event Dispatch Thread(EDT) 上完成,这是它自己的线程规矩,和 JavaFX 要求 UI 操作必须在 FX 线程同样严格。

我一般会做一个独立的TrayManager类,把托盘初始化放进去,在主程序里用SwingUtilities.invokeLater调用。这样 AWT 的归 AWT,JavaFX 的归 JavaFX,中间只通过Platform.runLater传指令。线程归属做不到位,后面所有功能都建立在沙子上,尤其是双击恢复窗口这种依赖事件时序的功能。

线程划分可以按下面这张表记:

接到归属线程说明
创建 TrayIcon、PopupMenu,添加/移除托盘图标AWT EDT用SwingUtilities.invokeLater放入
托盘图标点击、菜单点击事件回调AWT EDT回调里不要直接操作 Stage
显示/隐藏/关闭 JavaFX 窗口FX Application Thread通过Platform.runLater切入
修改 JFoenix 控件属性、创建新 StageFX Application Thread与上面相同

这条架构做对了,后面只需要专注界面对接,不用反复回头查灵异事件。

2.2 JFoenix 在架构里的位置:界面归它,托盘归系统,线程归事件

JFoenix 是一个 JavaFX 的 Material Design 控件库,负责按钮、输入框、对话框、Snackbar 这些视觉和交互组件。它和 Java 自带 AWT 的托盘图标没有任何依赖关系,也不该有。工程结构上我建议做三层:主界面层只管 JavaFX 控件,托盘管理层只持有外部传入的Stage引用,中间通过回调或事件通知串联。某团队模拟项目X 的代码里,除了主类之外单独拆出TrayManager.java,主界面里完全不出现SystemTray字样。

这样的好处是,当你要换托盘实现方案(比如从 AWT 换成第三方通知库)时,只改一个类。中文乱码修复时,改动也不会扩散到整个项目。

这里有一个必须先记住的设置:Platform.setImplicitExit(false)。JavaFX 默认行为是当最后一个窗口关闭时,FX 运行时自动退出。但托盘应用恰恰要“窗口关了、进程还在”,所以要在start()开头关掉这个默认行为。配合Stage.setOnCloseRequest里只执行stage.hide(),就能实现最小化到托盘而不是退出。

JFoenix 控件的主题样式通过 CSS 加载,它自带的样式表会去设置字体,比如JFXButton默认走 Roboto 体系。这一点是中文乱码的一个关键入口,下一章实现代码时我会专门处理字体覆盖。简单说:JFoenix 本身不会制造乱码,它只是把字体选择变得更集中、更容易被忽略。

3. 拉通源码:最小可运行的 JavaFX + Jfoenix 托盘程序

3.1 Maven 骨架和资源目录:先解决编译级联

开始写代码前,先把项目结构固定下来,中文乱码很多是编译环节带入的。我习惯用 Maven 管理,原因是project.build.sourceEncoding能从编译源头强制 UTF-8,避免 Eclipse 或旧工程里残留的 GBK 记忆。

模拟项目X 的目录结构:

tray-app/ ├─ pom.xml ├─ src/main/java/com/example/tray/ │ ├─ Launcher.java │ ├─ MainApp.java │ ├─ TrayManager.java │ └─ util/PropsReader.java └─ src/main/resources/ ├─ css/main.css └─ icon.png

Launcher.java和MainApp.java分开是有原因的:JavaFX Maven 插件在部分环境里要求主类不直接继承Application,否则运行时会报 JavaFX runtime components 缺失,或者出现第二次launch被拒绝的异常。常见做法是让一个普通类的main方法去调用Application.launch(MainApp.class, args)。

pom.xml 里关键的依赖和编译属性:

<properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <maven.compiler.encoding>UTF-8</maven.compiler.encoding> <maven.compiler.release>17</maven.compiler.release> <javafx.version>17.0.13</javafx.version> </properties> <dependencies> <dependency> <groupId>org.openjfx</groupId> <artifactId>javafx-controls</artifactId> <version>${javafx.version}</version> </dependency> <dependency> <groupId>com.jfoenix</groupId> <artifactId>jfoenix</artifactId> <version>9.0.10</version> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.openjfx</groupId> <artifactId>javafx-maven-plugin</artifactId> <configuration> <mainClass>com.example.tray.Launcher</mainClass> <options> <option>-Dfile.encoding=UTF-8</option> </options> </configuration> </plugin> </plugins> </build>

说明几个参数:project.build.sourceEncoding影响编译器读取源码的默认编码,这个不设,源码里所有中文字符串在 Windows 上会被按 GBK 读,界面直接出乱码。maven.compiler.release指定字节码级别为 17,它会约束可用 API,避免把 JDK 17 以上的行为误推到运行时。javafx.version按你本机实际验证过的版本来写,17.0.x 是长期维护分支,稳定性足够。

依赖里只放了javafx-controls,没有放javafx-fxml,因为示例里不用 FXML。FXML 有独立的编码坑,如果你本来就要用,在 XML 声明头必须写encoding="UTF-8",这我在第 4 章会再提。

3.2 TrayManager 源码:双击恢复窗口、右键菜单退出

托盘管理的完整代码,我按“可重复安装、线程安全、资源兜底”三个要求写。直接贴出来:

package com.example.tray; import java.awt.AWTException; import java.awt.Image; import java.awt.MenuItem; import java.awt.PopupMenu; import java.awt.SystemTray; import java.awt.TrayIcon; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; import java.awt.image.BufferedImage; import java.io.InputStream; import javax.imageio.ImageIO; import javafx.application.Platform; import javafx.stage.Stage; public class TrayManager { private static TrayIcon iconHandle; private TrayManager() { } public static synchronized void install(Stage stage) { if (!SystemTray.isSupported()) { System.err.println("当前系统不支持系统托盘,应用以普通窗口模式运行。"); return; } // 重复安装前先移除旧图标,避免托盘里出现多个相同图标 if (iconHandle != null) { SystemTray.getSystemTray().remove(iconHandle); iconHandle = null; } PopupMenu menu = new PopupMenu(); MenuItem openItem = new MenuItem("打开主界面"); openItem.addActionListener(e -> Platform.runLater(() -> showStage(stage))); MenuItem exitItem = new MenuItem("退出"); exitItem.addActionListener(e -> { Platform.runLater(() -> { stage.close(); Platform.exit(); }); }); menu.add(openItem); menu.addSeparator(); menu.add(exitItem); TrayIcon trayIcon = new TrayIcon(loadIconImage(), "模拟项目X", menu); trayIcon.setImageAutoSize(true); trayIcon.addMouseListener(new MouseAdapter() { @Override public void mouseClicked(MouseEvent e) { if (e.getButton() == MouseEvent.BUTTON1 && e.getClickCount() == 2) { Platform.runLater(() -> showStage(stage)); } } }); try { SystemTray.getSystemTray().add(trayIcon); iconHandle = trayIcon; } catch (AWTException e) { e.printStackTrace(); } } private static void showStage(Stage stage) { if (!Platform.isFxApplicationThread()) { Platform.runLater(() -> showStage(stage)); return; } if (stage.isIconified()) { stage.setIconified(false); } stage.show(); stage.toFront(); } private static Image loadIconImage() { try (InputStream in = TrayManager.class.getResourceAsStream("/icon.png")) { if (in != null) { return ImageIO.read(in); } } catch (Exception ignored) { // 资源缺失时降级为一张简单图形,避免整个托盘初始化失败 } return createFallbackIcon(); } private static Image createFallbackIcon() { BufferedImage image = new BufferedImage(32, 32, BufferedImage.TYPE_INT_ARGB); for (int x = 0; x < 32; x++) { for (int y = 0; y < 32; y++) { if ((x - 16) * (x - 16) + (y - 16) * (y - 16) <= 225) { image.setRGB(x, y, 0xFF42A5F5); } } } return image; } }

代码逻辑分三块。菜单动作里,所有窗口操作都包在Platform.runLater里,因为菜单回调发生在 AWT EDT 上。双击监听有两个硬性条件:左键BUTTON1、getClickCount() == 2,如果不判断按钮,在部分 Linux 桌面环境下鼠标中键也会触发窗口显示。showStage方法开头再做一次线程判断,是为了让这个方法在其他地方被误调用时依然安全。

退出菜单里我先把stage.close()执行,再Platform.exit()。这两步都在 FX 线程内执行,顺序不会错;Platform.exit()会结束 FX 事件循环,而系统托盘的图标会随 JVM 进程退出自动清理。注意我没有在退出线程里手动remove(iconHandle),如果你有强迫症,可以在Platform.exit()之前调用SwingUtilities.invokeLater(() -> tray.remove(...)),实际效果差别不大。

3.3 主界面的 JFoenix 控件集成:把托盘和 Material 控件串起来

主程序负责创建舞台、加载样式、把托盘装上去。示例里放一个JFXButton和JFXDialog,目的就是验证中文渲染链路:

package com.example.tray; import com.jfoenix.controls.JFXButton; import com.jfoenix.controls.JFXDialog; import com.jfoenix.controls.JFXDialogLayout; import javafx.application.Application; import javafx.application.Platform; import javafx.scene.Scene; import javafx.scene.control.Label; import javafx.scene.layout.StackPane; import javafx.scene.text.Text; import javafx.stage.Stage; import javax.swing.SwingUtilities; public class MainApp extends Application { private Stage primaryStage; @Override public void start(Stage primaryStage) { this.primaryStage = primaryStage; Platform.setImplicitExit(false); StackPane root = new StackPane(); JFXButton button = new JFXButton("弹出一个 JFoenix 对话框"); button.setOnAction(e -> showDialog(root)); root.getChildren().add(button); Scene scene = new Scene(root, 420, 280); scene.getStylesheets().add(getClass().getResource("/css/main.css").toExternalForm()); primaryStage.setScene(scene); primaryStage.setTitle("JavaFX + Jfoenix 系统托盘示例"); primaryStage.setOnCloseRequest(e -> primaryStage.hide()); // 托盘初始化交给 AWT 事件线程 SwingUtilities.invokeLater(() -> TrayManager.install(primaryStage)); primaryStage.show(); } private void showDialog(StackPane root) { JFXDialogLayout layout = new JFXDialogLayout(); layout.setHeading(new Text("中文乱码修复测试")); layout.setBody(new Label("到这里中文还正常,说明字体与编码链路没有问题。")); JFXButton okButton = new JFXButton("知道了"); JFXDialog dialog = new JFXDialog(root, layout, JFXDialog.DialogTransition.CENTER); okButton.setOnAction(e -> dialog.close()); layout.setActions(okButton); dialog.show(); } public static void main(String[] args) { launch(args); } }

SwingUtilities.invokeLater(() -> TrayManager.install(primaryStage))这行是最关键的一步:把托盘初始化的动作放到 AWT EDT 上执行。javax.swing.SwingUtilities不是只给 Swing 组件用,它是整个 AWT 工具包的通用线程入口。Platform.setImplicitExit(false)写了之后,主窗口关闭只触发hide(),JVM 不退出。

JFXDialog的构造需要传入一个父容器,用根StackPane最稳妥,因为它会覆盖阴影层实现模态效果。这个对话框里如果中文标题显示正常,基本可以排除字体回退问题,后面我会解释为什么会这样。JFoenix 的对话框不要直接加到Stage的 scene root 之外的节点,会导致层级错乱。

4. 中文乱码的完整修法:从编译期到界面字体,再到运行日志

4.1 三个容易忽略的乱码入口:源码、properties、控制台

桌面应用里的中文乱码,绝大多数不是“Java 不支持中文”,而是问题发生在三个固定入口之一。

第一个是源码编译阶段。IDE 默认编码和 Maven 的sourceEncoding不一致,字符串里的中文在编译时变成了错误字节。这种乱码的特点是:代码里看是正常中文,编译运行后标题、按钮文字全乱。第二个是配置文件读取阶段。Properties.load(InputStream)的默认编码是 ISO-8859-1,你把中文直接写进.properties文件,读出来必然乱码。第三个是控制台输出阶段。Windows 控制台默认代码页可能是 936 或 437,配上 JVM 启动参数里的file.encoding不一致,日志里的中文就变成一串问号或哥特字母。

乱码位置典型现象根因
窗口标题、按钮文字源码正确,运行后乱javac 编译编码不是 UTF-8
配置文件内容properties 中的中文读出来乱用了load(InputStream)而不是load(Reader)
System.out 输出控制台中文乱码,界面正常JVM 文件编码与控制台代码页不一致
JFoenix 控件文字中文变豆腐块Roboto 字体没有中文字形

JFoenix 的乱码会更隐蔽一点:它不是文件编码问题,而是字体问题。JFXButton的默认样式里有-fx-font-family: "Roboto Medium",Roboto 不含中文字形,JavaFX 会去系统字体里兜底,有些 Windows 简化版系统兜底失败,直接画空方框。这就是为什么标题里把“系统托盘”和“中文乱码”并列,看似无关,其实在 JFoenix 工程里它们要一起处理。

4.2 一份可以直接用的 UTF-8 工具代码:properties 和资源文件统一编码

中文配置文件推荐用UTF-8的.properties或.conf,读取时不要用load(InputStream),改用load(Reader)。工具类如下:

package com.example.tray.util; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.Reader; import java.nio.charset.StandardCharsets; import java.util.Properties; public final class PropsReader { private PropsReader() { } public static Properties loadUtf8(String resourcePath) throws IOException { Properties props = new Properties(); try (InputStream in = PropsReader.class.getResourceAsStream(resourcePath)) { if (in == null) { throw new IOException("未找到配置文件: " + resourcePath); } try (Reader reader = new InputStreamReader(in, StandardCharsets.UTF_8)) { props.load(reader); } } return props; } }

说明:Properties.load(Reader)会以 Reader 自身的字符集解析内容,和load(InputStream)的 ISO-8859-1 限制完全不同。这里把输入流包成InputStreamReader并明确指定StandardCharsets.UTF_8,后读出的中文键值都会保持原样。

如果你用 FXML 写界面,同样要保证两点:.fxml文件本身以 UTF-8 保存,第二行 XML 声明写完整:

<?xml version="1.0" encoding="UTF-8"?>

没有这个声明,FXMLLoader会按平台默认编码推断,中文文本就会踩到编译期同样的坑。

.properties还有一个额外注意点:如果你用 Maven 做资源过滤,要防止它把你的 UTF-8 文件重新编码。常规做法是在 pom 里让resources插件不做文件过滤,或者至少关掉nonFilteredFileExtension里对properties的过滤。

4.3 字体在 JFoenix 里的正确打开方式:覆盖 Roboto 的字体指针

JFoenix 控件的字体策略是:在它自己的 CSS 里用类选择器设置字体,比如.jfx-button。这带来一个优先级问题,你的main.css如果也被Scene.getStylesheets()加进去,只要挂在后端,同优先级后挂者胜。所以字体覆盖的要点是:把自定义样式表放在所有 JFoenix 样式之后。

在src/main/resources/css/main.css里写:

.root { -fx-font-family: "Microsoft YaHei", "PingFang SC", "Noto Sans CJK SC"; -fx-font-size: 14px; } .jfx-button, .jfx-text-field, .jfx-combo-box, .jfx-dialog-layout { -fx-font-family: "Microsoft YaHei"; }

这段 CSS 做的事情是:把根场景默认字体切成微软雅黑,再对 JFoenix 的常用控件类做二次覆盖。写.jfx-dialog-layout是因为JFXDialogLayout的标题区自有一套样式,不覆盖它的话,标题依然会走 Roboto 的路径。

如果目标机器上没有微软雅黑(某些精简版系统会砍掉它),第二优先级要放一个能给中文兜底的字体。Windows 上还可以用"Microsoft YaHei UI",macOS 用"PingFang SC"。尽量不要为了省事把字体写进代码里,CSS 里改一次,后续换字体方案成本低很多。

代码层面还有一条常用后路:如果你嵌入了一个开源中文字体包,可以在start()里先注册字体,再在 CSS 里引用注册名:

try (InputStream in = MainApp.class.getResourceAsStream("/fonts/NotoSansSC-Regular.ttf")) { if (in != null) { Font.loadFont(in, 14); } }

说明:Font.loadFont的第一个参数字体文件流,第二个参数是基准字号,加载后字体名称会进入 JavaFX 的字体注册表,CSS 里可以按名称引用。这个操作用在界面启动早期,因为后续控件创建时才来得及拿到形状。中文字体文件体积偏大,注意别让安装包因此膨胀太多。

4.4 运行和打包参数:Maven、IDE、jpackage 三条路径一起调

在开发期跑javafx:run,中文不乱码还不够,因为最终交付是原生安装包。两个路径的 JVM 参数不一样,需要分别配置。

运行期我在第 3 章的 pom 里已经写了-Dfile.encoding=UTF-8。IDE 里直接跑MainApp时,也要在启动配置的 VM options 里加上同样参数,否则 Eclipse 和某些版本 IDEA 会把file.encoding默认成 GBK。打包阶段用 jpackage 或另外封装的启动器,常见命令如下:

jpackage --input target/libs \ --main-class com.example.tray.Launcher \ --main-jar tray-app.jar \ --name MockProjectX \ --type exe \ --java-options "-Dfile.encoding=UTF-8" \ --dest dist

--java-options会写入生成启动器的 JVM 参数,效果等同进程启动时带了-Dfile.encoding=UTF-8。你别只改这里,Launcher.java对应的main方法里不能再做任何System.setProperty("file.encoding", "UTF-8")的尝试,因为 JVM 启动后这个属性已经固定,代码里设置基本无效,还会给阅读者造成误导。

控制台占用的另一条线是:如果你在 Windows 下用cmd直接跑java -jar,即使file.encoding正确,中文也可能还是乱码。这时在执行前设置chcp 65001切换代码页,然后重开进程。日志文件不受代码页影响,交给日志框架按 UTF-8 写就行。

5. 避坑与排查:托盘消失、双击失灵、中文变豆腐块的 5 个真实原因

5.1 托盘图标没出现在任务栏:多半是拿 FX 线程直接建了 AWT 控件

现象:程序启动,窗口正常,但托盘区域看不到图标;有时重启电脑后图标出现,有时运行十分钟后自己消失。

原因:在 FX 线程里直接new TrayIcon并SystemTray.add。AWT 组件接受了跨线程创建,但后续事件分发到 EDT 时对组件状态判断异常,导致图标刷新被系统忽略。另一个低频原因是icon.png路径写错,ImageIO.read返回空,Image传进TrayIcon后图标不可见。

解决:把托盘初始化整体放进SwingUtilities.invokeLater,确保TrayIcon、PopupMenu都在 EDT 上创建。资源文件用class.getResourceAsStream("/icon.png"),路径以 jar 根为基准。同时在TrayManager.install里做SystemTray.isSupported()判断,不支持的平台直接打印日志,不阻断主界面。

5.2 双击托盘图标窗口没反应:窗口状态没恢复,事件被吞了

现象:单击能弹右键菜单,但双击主窗口就是不显示;鼠标在任务栏点应用图标却又能弹出来。

原因:双击事件触发了,但窗口当前还处于ICONIFIED状态。只调用了stage.show()而没有先setIconified(false),JavaFX 会认为窗口仍是极小化状态,界面不还原。另一个常见原因是回调写在 FX 线程直接操作了Stage,而 AWT 事件线程还没把双击传递完,事件被后续的鼠标释放事件清掉。

解决:showStage里必须按顺序做三步:先判断线程,再setIconified(false),再show()和toFront()。前三步缺一不可,尤其toFront()在多显示器场景下能把窗口从另一块屏幕上拉回来。双击事件过滤用e.getButton() == MouseEvent.BUTTON1 && e.getClickCount() == 2,不要再加Platform.runLater之外的计算逻辑,否则会影响事件消费速度。

5.3 JFoenix 按钮中文变豆腐块:字体选择器写早了,或 CSS 挂载顺序不对

现象:普通Label中文正常,JFXButton上的中文变成一排方框;按钮旁边JFXTextField提示文字也乱,但对话框里自定义的 Text 没问题。

原因:JFoenix 样式表的.jfx-button {-fx-font-family: "Roboto Medium"}生效了,但你的覆盖样式先挂载,后者没接管到。Roboto 字体没有中文字形,系统回退又失败,JavaFX 把没找到的字形画成豆腐块。

解决:把你的 CSS 放到Scene.getStylesheets()的最后一条。如果场景里还有更多样式表,确保自定义样式最后 add;用JFXButton时在 CSS 里显式写.jfx-button { -fx-font-family: "Microsoft YaHei"; }。还想更保险,就在root.setStyle()里给根节点直接塞-fx-font-family,子节点未覆盖的话会继承根值。

5.4 控制台中文乱码但界面正常:编码被系统区域设置带偏了

现象:窗口标题、按钮都正常,只有System.out.println("中文")输出乱码,而且只在直接双击 jar 或 exe 时出现,IDE 里运行是好的。

原因:IDE 默认给 JVM 传了 UTF-8 参数,打包后的启动器不会自动携带;Windows 控制台使用GBK代码页,JVM 的sun.stdout.encoding按系统默认区域设置决定输出编码,两边对不上。

解决:jpackage 命令里加--java-options "-Dfile.encoding=UTF-8",并在控制台执行前chcp 65001。如果你的程序要长期跑在后台,建议不要用System.out,改成java.util.logging或 SLF4J,控制台编码怎么乱都不影响日志文件,配置一次比每次调 JVM 参数省心。

5.5 debug 正常、打包后乱码:源码编码被 IDE 的 GBK 记忆污染

现象:在 IDEA 或 Eclipse 里写代码中文预览正常,Maven 编译通过,但运行后全盘乱码;把源码用记事本打开,中文显示出“夋”“F”这类汉字拼接。

原因:代码文件实际是 GBK 编码,但 IDE 内存里按 UTF-8 解释显示,没报错。Maven 设置了sourceEncoding=UTF-8后,javac 按 UTF-8 读文件,编码不对,源码里的中文字符直接变成非法字节。

解决:检查源码文件编码,统一转成 UTF-8。VS Code 或支持批量转码的编辑器打开.java文件,把 GBK 转成 UTF-8 保存,再在 IDE 设置里把项目编码改成 UTF-8。git 仓库建议添加.gitattributes或.editorconfig,限制提交编码为 UTF-8,避免这个问题再次被带到下一个工程。

6. 把托盘应用做成成品的验证清单:启动隐藏、原生通知和最后检查

托盘程序做成产品,至少需要验证四个交互:启动即最小化、关闭到托盘、双击恢复、退出干净。还有一个可选增强是原生通知,TrayIcon.displayMessage在 Windows 上会弹系统气泡,适合做任务完成提醒。

启动即最小化,就是把主窗口先建好但不马上显示:

@Override public void start(Stage stage) { Platform.setImplicitExit(false); // 构建场景... SwingUtilities.invokeLater(() -> TrayManager.install(stage)); Platform.runLater(stage::hide); }

说明:stage.hide()要在show()之前调用,让窗口不闪出来。用户第一次双击托盘图标,TrayManager.showStage里会执行stage.show(),互相配合。

原生通知可以放在某个耗时任务结束时:

TrayManager.notify("模拟项目X", "任务处理完成", TrayIcon.MessageType.INFO);

封装方法在TrayManager内部调用iconHandle.displayMessage即可。要注意MessageType.WARNING和ERROR在 Windows 上会伴随提示音,别在频繁任务里误用。

离开 IDE 后最终验证,我建议按下面这个顺序过一遍:第一,用打包脚本生成 exe;第二,双击 exe,期间不打开任何 IDE,确认托盘图标出现;第三,点主窗口关闭按钮,确认窗口消失且进程还在;第四,双击托盘图标,确认窗口从所有显示器位置都能恢复;第五,在界面输入一段中文,确认无豆腐块;第六,打开 cmd 执行一次程序,确认控制台无中文乱码。每一步出问题,回到对应章节找根因。

这段代码折腾下来,我自己最大的收获是养成一个习惯:所有 JavaFX 控件与 AWT 界面功能之间,牢牢守住“AWT 事件回调只做事件分发,不做界面操作”这条线。中文乱码则更简单,先查编译编码,再查字体覆盖,再查运行参数,按顺序来基本一次定位。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询