首页 / 资讯中心 / 文章详情

Java桌面宠物开发实战:透明窗口、帧动画与系统托盘

Java桌面宠物开发实战:透明窗口、帧动画与系统托盘 ★ FEATURED ARTICLE
简介利用Java开发桌面宠物的小例子面向初学Java GUI的开发者适合想了解Swing窗口、Graphics2D绘图与事件处理的读者。项目演示了使用JFrame和JPanel搭建主窗口、扩展自定义组件、重写paintComponent绘制宠物形象以及实现鼠标拖拽、定时动画与多线程刷新的完整流程并提供了资源加载和MVC结构的可参考思路。压缩包内共80个文件约122KB包含2个Java源文件、多个class编译产物、13张宠物图片jpg、Eclipse工程配置与Git元数据目录划分为源码、编译输出和图片等模块便于对照学习。已有2643人学习下载。通过阅读源码与图片素材可以掌握paintComponent自定义绘制、MouseListener鼠标交互、javax.swing.Timer动画循环以及ImageIcon加载图片等核心技能适合作为入门Java桌面动画与交互编程的练手项目。1. 桌面宠物这种小例子为什么值得用 Java 写想在桌面养一只宠物看着它睡觉、打哈欠、被鼠标拎起来乱晃这个需求听上去挺没用的但真把它拆开你会发现它几乎把 Java 桌面开发要碰的东西全占了无边框透明窗口、置顶、鼠标事件、帧动画、多线程调度、系统托盘。我见过不少 java 学习路线里把桌面宠物当作业也有人拿它当面向对象编程的练手项目。这篇就给你一个能直接跑起来的小例子从最朴素的 Swing 窗口开始一点一点加动画、加交互、加打包顺便把我踩过的坑都标出来。适合刚啃完 java 基础、想找个小而完整的项目练手的人也适合那种想在摸鱼时有个东西陪自己的。2. 先跑通最小窗口无边框、透明、置顶和鼠标拖动桌面宠物本质上不是普通应用窗口而是一张“贴在屏幕上的贴纸”。很多第一次写的人会直接用 JFrame然后一路跟标题栏、任务栏、焦点做斗争。我一般先用 JWindow 起手它没有标题栏也没有任务栏按钮天生适合做这种装饰性窗口。先跑通一个能显示图片、能拖动、能记住位置的骨架后面所有动画和交互都在这上面加。2.1 用 JWindow/JFrame 搭出宠物骨架不用布局管理器反而省事先给一个最少代码的透明窗口。这段代码也是后面所有版本的基座窗口完全透明里面放一个 JLabel图片填满标签标签填满窗口。如果你手头没有 GIF随便放一张 png 也能跑后面再补动画。import javax.swing.*; import java.awt.*; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; public class DesktopPet { private JWindow window; public DesktopPet() { window new JWindow(); window.setType(Window.Type.UTILITY); // 不抢任务栏焦点不出现在 AltTab window.setBackground(new Color(0, 0, 0, 0)); // 完全透明背景 window.setAlwaysOnTop(true); // 保持在桌面最前 window.setSize(120, 140); window.setLocation(200, 300); JLabel label new JLabel(new ImageIcon(pet_idle.gif)); window.add(label); window.setVisible(true); } public static void main(String[] args) { SwingUtilities.invokeLater(DesktopPet::new); } }代码里 setType(Window.Type.UTILITY) 是很多 Java 桌面教程不会提但桌面宠物必须加的一行它告诉窗口系统这是一个工具窗口在 Windows 上不会出现在 AltTab 切换列表里也不会抢任务栏空间。setBackground(new Color(0, 0, 0, 0)) 是真正的“透明背景”注意是 RGB 为 0、alpha 为 0 的颜色不要写成 new Color(0, 0, 0)那个是纯黑不透明。setAlwaysOnTop(true) 在 Windows 和 macOS 上基本可靠但在部分 Linux 的 Wayland 会话下可能失效后面避坑章会专门说。setSize(120, 140) 不一定要和图片像素完全一致但我建议先量好 GIF 的实际尺寸再设窗口否则 JLabel 会把图标拉伸变形。Swing 的默认布局是 BorderLayoutJLabel 放进去会占满整个内容区图片会跟着 JLabel 拉伸如果不想要拉伸可以给 JLabel 设一个固定尺寸 setPreferredSize然后窗口 pack()。不过最简单的是直接把窗口大小设成图片像素这个例子就这么做。这里还有一个更稳妥的做法用 JFrame 加 setUndecorated(true) 也能得到类似的窗口而且 JFrame 自带的可访问支持更完整。但 JWindow 省掉了关闭按钮、系统菜单等无关要素桌面宠物本来就不应该让用户去点最小化这也是为什么很多开源项目用 JWindow。如果你后续要在宠物窗口上放 JavaFX 组件那就另说纯 Java 2D 图形和 Swing 组件这个例子用 JWindow 足够。先验证这个骨架能不能跑编译后运行桌面应该出现一张透明背景的图片它不会被其他窗口挡住。如果出现黑色或白色长方形先检查 setBackground 有没有在 setVisible 之前调用或者你所在的系统不支持每像素透明度具体排查在第 5 章。2.2 鼠标拖动和坐标记忆让宠物不会跑出屏幕透明窗口能显示只是第一步不能拖的东西只能叫贴纸不叫宠物。拖动的核心是让窗口坐标跟随鼠标但注意不要直接 window.setLocation(e.getXOnScreen(), e.getYOnScreen())那样老鼠会“瞬移”到鼠标左上角因为鼠标点在图片内部的位置没有被扣除。正确的做法是记录鼠标按下时相对于窗户左上角的偏移拖动时用鼠标的屏幕坐标减去这个偏移。import javax.swing.*; import java.awt.*; import java.awt.event.*; import java.io.*; import java.util.Properties; public class DesktopPet { private JWindow window; private Point pressPoint; // 鼠标按下时相对于窗口左上角的偏移 public DesktopPet() { // ... 窗口初始化代码同上 ... JLabel label new JLabel(new ImageIcon(pet_idle.gif)); window.add(label); // 记录按下点 label.addMouseListener(new MouseAdapter() { Override public void mousePressed(MouseEvent e) { pressPoint e.getPoint(); } Override public void mouseReleased(MouseEvent e) { savePosition(); // 松手时保存位置 } }); // 拖动窗口 label.addMouseMotionListener(new MouseMotionAdapter() { Override public void mouseDragged(MouseEvent e) { if (pressPoint null) return; Point now MouseInfo.getPointerInfo().getLocation(); int x now.x - pressPoint.x; int y now.y - pressPoint.y; // 取当前窗口所在显示器的边界多屏下不会跑丢 Rectangle screen window.getGraphicsConfiguration().getBounds(); x Math.max(screen.x, Math.min(x, screen.x screen.width - window.getWidth())); y Math.max(screen.y, Math.min(y, screen.y screen.height - window.getHeight())); window.setLocation(x, y); window.repaint(); // 部分系统需要手动刷新避免残影 } }); window.setVisible(true); loadPosition(); // 启动时恢复上次位置 } private void savePosition() { try { Properties props new Properties(); props.setProperty(pet.x, String.valueOf(window.getX())); props.setProperty(pet.y, String.valueOf(window.getY())); File file new File(System.getProperty(user.home), .pet.properties); try (OutputStream out new FileOutputStream(file)) { props.store(out, Desktop Pet Position); } } catch (IOException ignored) { } } private void loadPosition() { File file new File(System.getProperty(user.home), .pet.properties); if (!file.exists()) return; try (InputStream in new FileInputStream(file)) { Properties props new Properties(); props.load(in); int x Integer.parseInt(props.getProperty(pet.x, 200)); int y Integer.parseInt(props.getProperty(pet.y, 300)); window.setLocation(x, y); } catch (IOException | NumberFormatException ignored) { } } }这段代码的 pressPoint 就是按下时的偏移点mouseDragged 里用 MouseInfo 实时取鼠标屏幕坐标这样即使鼠标移出窗口也还能继续拖不会丢事件。边界的两行用一个 min/max 把窗口限制在屏幕边界以内防止宠物被拖得只剩一条边在外面。注意这里用的是 window.getGraphicsConfiguration().getBounds() 而不是 Toolkit.getDefaultToolkit().getScreenSize()前者在多显示器环境下能取到当前窗口所在那个显示器的矩形后者只会返回主屏尺寸多屏用户拖到副屏时就翻车了。savePosition 和 loadPosition 用 Properties 把坐标存进用户目录。不要把配置写进当前工作目录很多教程图省事写 new File(pet.properties)换工作目录启动就丢了用 System.getProperty(user.home) 才靠谱。Properties.load/store 对桌面宠物这种小型配置足够不需要引入 JSON 依赖。文件格式就是普通的 keyvalue用户如果想改初始位置可以直接编辑。如果你想把“别跑出屏幕”做得更细腻一点可以在 mouseDragged 里同时限制 x 不小于屏幕左边界y 不小于屏幕上边界并考虑任务栏高度。但那是优化第 5 章会展开讲这个边界问题。先跑通拖动和记忆你已经完成桌面宠物 60% 的工作了。3. 让宠物动起来把静态图标变成帧动画能拖动的贴纸开始像个宠物了但它一动不动的话还是缺点灵魂。这里给你两条路一条是直接用 GIF一条是拆帧序列自己用 Timer 驱动。两条路各有各的适用场景我在实际做的时候会按项目需求选。GIF 适合快速验证帧序列适合要精细控制动作和状态切换的场景。3.1 两种动画方案GIF 懒人法和 Timer 帧序列法GIF 是最省事的JLabel 的 setIcon(new ImageIcon(pet.gif)) 就能直接播放Swing 的 ImageIcon 内置了 GIF 解码和帧循环不需要你写任何线程代码。但它的缺点也很明显帧率是 GIF 文件自己定的你没法在程序里让动作变快变慢而且很多从网上下载的 GIF 尺寸小得可怜放到桌面上全是马赛克。还有一个隐蔽问题部分 GIF 只循环一次就停了看起来像是死掉了。所以我更推荐用帧序列。去任何一个免费像素素材站下载动作序列或者用工具把 GIF 拆成一张张 PNG然后用 JPanel 自己画。下面这个类就是一个循环播放的帧动画面板传入图片前缀和数量它会按固定间隔切换帧。import javax.swing.*; import java.awt.*; public class PetPanel extends JPanel { private Image[] frames; private int current; private Timer timer; /** * param prefix 图片前缀如 walk_ * param count 帧数量如 4即 walk_0.png ~ walk_3.png * param interval 每帧间隔毫秒建议 120 ~ 200 */ public PetPanel(String prefix, int count, int interval) { frames new Image[count]; for (int i 0; i count; i) { frames[i] new ImageIcon(prefix i .png).getImage(); } timer new Timer(interval, e - { current (current 1) % count; repaint(); }); timer.start(); } Override protected void paintComponent(Graphics g) { super.paintComponent(g); g.drawImage(frames[current], 0, 0, getWidth(), getHeight(), null); } }这里我特意用 Image[] 而不是 ImageIcon[]paintComponent 里的 drawImage 可以直接缩放绘制而 ImageIcon 的 paintIcon 在某些 JDK 版本上会多一层封装绘制效率略低。Timer 的回调在 EDT 上执行所以 current 变量不需要额外加锁但如果你后面把动画线程切成 ScheduledExecutorService就必须用 SwingUtilities.invokeLater 包装否则会在非 EDT 线程刷新界面轻则闪烁重则抛出 NotOnEventThreadException。间隔时间这个参数很关键我一般把 interval 设在 120 到 200 毫秒小于 80 毫秒基本看不出细节反而把 CPU 占用拉高大于 250 毫秒就会觉得宠物在演 PPT。帧数量也要控制一个走路动作 4 到 8 帧就够了12 帧以上肉眼不会觉得更细腻内存倒是实打实多占几倍。如果你的是高清 PNG例如 512x512建议用 ImageIO.read 而不是 ImageIcon 的构造方法两者都要加载磁盘文件但 ImageIO 能更明确地让你做缓冲和缩放。3.2 用 ScheduledExecutorService 做定时随机动作宠物不能只会走路它还要会发呆、睡觉、伸懒腰。用一个定时任务线程池去驱动“随机切换状态”比在 Swing Timer 里写复杂逻辑要干净。我一般这样设计定义一个枚举 PetAction里面放 IDLE、WALK、SLEEP然后在一个公共的 setAction 方法里切换显示的帧序列。调度线程每隔几秒随机选一个动作通过 SwingUtilities.invokeLater 改 UI。public enum PetAction { IDLE, WALK, SLEEP } // 核心动作切换方法 public void setAction(PetAction action) { switch (action) { case IDLE: changePanel(idle_, 2, 300); // 发呆帧少间隔可以慢 break; case WALK: changePanel(walk_, 4, 150); // 走路帧多间隔要快 break; case SLEEP: changePanel(sleep_, 2, 500); // 睡觉动作基本是呼吸起伏 break; } } private void changePanel(String prefix, int count, int interval) { // 移除旧面板并新建 PetPanel或者复用面板并更新内部帧数组 // 若复用面板记得旧 Timer 要 stop()否则动画会叠着跑 } // 调度线程 ScheduledExecutorService executor Executors.newSingleThreadScheduledExecutor(); Random random new Random(); executor.scheduleAtFixedRate(() - { PetAction[] actions PetAction.values(); PetAction next actions[random.nextInt(actions.length)]; SwingUtilities.invokeLater(() - setAction(next)); }, 3, 10, TimeUnit.SECONDS);这段代码里有两个点要特别说明。第一个是 changePanel 里必须把旧 PetPanel 的 Timer 停掉。很多人第一次写时直接往容器里 add 新面板旧面板的动画线程还在跑结果 CPU 一路飙升窗口上叠着一堆残影。第二个是随机动作不能这么粗暴因为真实宠物的行为是有“惯性”的它不可能从走路瞬间切到睡觉中间应该有个“坐下”的过渡动画。你可以定义一个简单的状态机例如从 IDLE 到 WALK 需要先播放过渡帧到 SLEEP 前要先趴下这些在 setAction 里做分支处理就行。ScheduledExecutorService 的 random 动作目前是每 10 秒触发一次测试时可以改成 3 秒方便看效果。正式使用时建议把随机种子去掉让每次启动动作序列不一样。另外executor 这个线程池记得在窗口关闭时 shutdown否则 Java 进程不会退出——这个问题我拦过不少同事的代码他们关掉宠物窗口后任务管理器里 java.exe 还挂了一晚上。4. 交互与托盘右键菜单、随机状态和系统托盘动画跑起来后宠物已经有了“活”的感觉。但一个桌面宠物如果连右键菜单都没有用户想让它换动作只能干瞪眼。这章加两个最实用的交互右键菜单切换动作以及系统托盘常驻。这两种交互都是 Java Swing/Java AWT 的标准功能不需要额外引入依赖。4.1 右键菜单切换动作比按钮更符合桌面宠物习惯桌面宠物不需要按钮它需要像一个桌面小程序一样右键点一下弹出菜单。最简洁的做法是给 JLabel 设置 setComponentPopupMenu让 Swing 自己处理鼠标事件。import javax.swing.*; JPopupMenu menu new JPopupMenu(); JMenuItem idleItem new JMenuItem(发呆); idleItem.addActionListener(e - setAction(PetAction.IDLE)); menu.add(idleItem); JMenuItem walkItem new JMenuItem(走路); walkItem.addActionListener(e - setAction(PetAction.WALK)); menu.add(walkItem); JMenuItem sleepItem new JMenuItem(睡觉); sleepItem.addActionListener(e - setAction(PetAction.SLEEP)); menu.add(sleepItem); menu.addSeparator(); JMenuItem exitItem new JMenuItem(退出); exitItem.addActionListener(e - { if (SystemTray.isSupported()) SystemTray.getSystemTray().remove(trayIcon); executor.shutdown(); System.exit(0); }); menu.add(exitItem); label.setComponentPopupMenu(menu);setComponentPopupMenu 会自动判断鼠标左键还是右键常规菜单是右键弹出但在 mac 上 Command点击也会触发。如果你希望双击左键也能换动作可以再加 mouseClicked 监听器里判断 e.getClickCount() 2。这里有一个小细节JPopupMenu 在透明窗口上弹出时菜单本身会强制拉一个不透明的原生窗口所以不用担心菜单变透明看不见。菜单项命名尽量用动作描述而不是代码里的英文枚举用户看到“切换走路”一定比“PetAction.WALK”友好。如果你想做“随机动作”菜单可以直接往里加一个 JMenuItem触发时执行调度线程里的随机逻辑这样用户也能手动体验随机状态切换。注意菜单里的所有 UI 操作都在 EDT 上不要在 actionPerformed 里 sleep 或加载大图否则整个菜单会卡住。4.2 加入系统托盘避免宠物被误杀桌面宠物最烦人的事之一就是不小心退出就再也没了。正确做法是加入系统托盘用户可以“隐藏”宠物到托盘右键再选“显示”找回来。Java 的 SystemTray 是 AWT 的 API和 Swing 混用时要小心事件线程托盘菜单用的是 AWT 的 PopupMenu 和 MenuItem不要直接拿 JPopupMenu 去给 TrayIcon 用那是不能跨 API 的。import java.awt.*; import java.awt.event.*; import javax.swing.*; if (SystemTray.isSupported()) { TrayIcon trayIcon new TrayIcon( new ImageIcon(pet_icon_32.png).getImage(), 桌面宠物 - 右键菜单 ); PopupMenu trayMenu new PopupMenu(); MenuItem showItem new MenuItem(显示宠物); showItem.addActionListener(e - SwingUtilities.invokeLater(() - { window.setVisible(true); window.setAlwaysOnTop(true); })); trayMenu.add(showItem); MenuItem exitItem new MenuItem(退出); exitItem.addActionListener(e - { SystemTray.getSystemTray().remove(trayIcon); executor.shutdown(); window.dispose(); System.exit(0); }); trayMenu.add(exitItem); trayIcon.setPopupMenu(trayMenu); trayIcon.setImageAutoSize(true); try { SystemTray.getSystemTray().add(trayIcon); } catch (AWTException ex) { System.err.println(托盘添加失败 ex.getMessage()); } }托盘菜单里最好不要放“显示/隐藏”两个选项只放一个“显示宠物”就够因为用户隐藏宠物后托盘图标就是唯一的入口。trayIcon.setImageAutoSize(true) 可以让你用任意尺寸的图标系统会自动缩放但 16x16 和 32x32 是 Windows 托盘图标的黄金尺寸建议直接做两个尺寸不要依赖缩放。如果你同时设置了 JWindow 的关闭事件请注意桌面宠物没有标题栏正常关闭只能通过右键菜单的“退出”但会出现另一个问题——当你调用 window.dispose() 后再次 window.setVisible(true) 时会得到一个已经销毁的窗口导致异常。所以在“显示宠物”里不要用 dispose 后的窗口对象而是始终保证同一个 JWindow 实例只是在隐藏时 setVisible(false)在退出时才 dispose。这是新手常犯的错我有一个同事把窗口 dispose 又 setVisible搞出两次 Java 异常才意识到问题。5. 避开这些坑透明窗口、多屏、DPI 和 CPU 占用写到这儿一个能跑、能动、能交互的桌面宠物已经在桌面上了。但真正从“能用”到“好用”中间隔着的是几台不同机器上的实测。这一章把我踩过的和帮别人排查过的问题整理一下每条都是真实见过的现象不是抄文档。5.1 透明窗口变黑/白底或者鼠标“穿透”到后面现象运行后窗口不是透明背景而是一个黑色矩形或白色矩形另一种情况是鼠标点宠物的透明区域直接点到桌面上的图标。原因第一种多半是 setBackground(new Color(0,0,0,0)) 没有被系统正确识别或者被放在了 setVisible 之后。第二种是窗口本身没有启用“点击穿透”机制透明区域仍属于窗口命中区。解决先严格保证“先 setBackground再 setVisible”的顺序且 setBackground 要用带 alpha 的颜色。如果用的是 JFrame配合 setUndecorated(true) 和 setOpacity(1.0f) 一起试。有些 JDK 在 Windows 上对 JWindow 的透明支持更好所以我建议从 JWindow 起步。点击穿透这个需求本身和“拖动窗口”冲突因为一旦穿透鼠标事件就留给下面的窗口宠物也拖不动了。如果你的宠物有大量透明镂空区域又想让它能拖动可以把鼠标事件绑定在 JLabel 的图片上而不是 JPanel 上或者给透明区域画一个不可见但可点击的像素块这不完美但够用。5.2 多显示器坐标越界与任务栏遮挡现象把宠物拖到副屏上重启后宠物不见了或者拖到屏幕底部被任务栏挡住一半点不到右键菜单。原因保存的坐标是绝对值但显示器配置变了副屏可能没插电边界判断只用了 Toolkit.getScreenSize()它只返回主屏尺寸副屏上的边界检查全部失效。解决保存位置时建议记录当前的显示器索引通过 Window.getGraphicsConfiguration().getDevice()启动时遍历所有 GraphicsEnvironment.getLocalGraphicsEnvironment().getScreenDevices()如果保存的坐标落在某个显示器的 bounds 内就用它否则放到主屏中央。任务栏的问题需要获取 InsetsToolkit.getDefaultToolkit().getScreenInsets(window.getGraphicsConfiguration())然后用 insets.bottom 减去屏幕高度限制最大 y。这样在 Windows 上宠物永远不会被任务栏吞进去。// 获取屏幕可用区域去掉任务栏 Rectangle screen window.getGraphicsConfiguration().getBounds(); Insets insets Toolkit.getDefaultToolkit().getScreenInsets(window.getGraphicsConfiguration()); int maxY screen.y screen.height - insets.bottom - window.getHeight(); int maxX screen.x screen.width - insets.right - window.getWidth(); window.setLocation(Math.min(Math.max(x, screen.x), maxX), Math.min(Math.max(y, screen.y), maxY));5.3 GIF 循环、闪烁、内存泄漏与 CPU 飙升现象GIF 播一遍就停住动画切换时界面闪烁运行几小时后 CPU 占用常年 20% 以上。原因GIF 文件自带循环次数有的只循环一次JLabel 换 Icon 时Swing 默认会先用背景色清一次区域造成闪烁Timer 的回调里反复 new ImageIcon 加载磁盘文件或者动画面板的旧 Timer 没 stop导致多个动画线程同时跑。解决GIF 循环问题要么找循环次数为 0 的素材要么用 ImageIO 读入后自己拆帧我最常用的是把 GIF 转成 PNG 序列再用第 3 章的 PetPanel 播放这样循环、帧率和缩放都掌握在自己手里。闪烁问题用重写 paintComponent 加双缓冲解决Swing 的 JPanel 默认双缓冲但如果你在 paintComponent 里直接 drawImage 一张一张画仍然可能出现撕裂这时可以在构造里 setDoubleBuffered(true)或者在 paint 前自己创建 BufferedImage 当离屏缓冲。CPU 飙升先用 jvisualvm 或 jcmd 看线程栈重点搜 Timer 和 repaint确认是否有两个动画定时器在跑。5.4 setAlwaysOnTop 在某些 Linux 桌面失效现象同一个 jar 在 Windows 上置顶正常在 Ubuntu 上运行宠物总是被其他窗口盖住。原因Linux 的 X11/Wayland 窗口管理器对 always-on-top 提示的支持参差不齐特别是 Wayland 协议下普通客户端无法强制置顶。解决不要求程序上做完美兼容。常见的处理是先调用 window.toFront() 再 setAlwaysOnTop(true)能解决一部分 X11 问题Wayland 下只能建议用户改用 Windows 运行或者放弃置顶需求。这个限制对桌面宠物不算致命毕竟主要分发场景是 Windows。如果你确实需要跨平台可以改用 JavaFX 的 Stage.setAlwaysOnTop它在 Linux 上同样受制于窗口管理器别抱太大期望。5.5 多实例运行导致重叠和属性文件写入冲突现象用户多启动了几次程序桌面上出现好几个宠物叠在一起属性文件也被写乱。原因程序没有做单实例锁Properties 文件被多个进程同时读写后写进程覆盖前一个的位置记录。解决用 Java NIO 的文件锁在启动时占位。在用户目录创建一个 .pet.lock 文件用 FileChannel.tryLock() 尝试加锁拿不到锁就退出。顺便把启动位置改成随机偏移即使真的出现异常多开宠物也不会叠成一团。File lockFile new File(System.getProperty(user.home), .pet.lock); try (FileChannel channel FileChannel.open(lockFile.toPath(), StandardOpenOption.CREATE, StandardOpenOption.WRITE)) { FileLock lock channel.tryLock(); if (lock null) { System.err.println(已有桌面宠物实例在运行退出); return; } // 正常启动主逻辑 } catch (IOException e) { // 锁文件写入失败可以选择继续运行或退出 }这个锁有两个局限如果用户直接删掉了 .pet.lock锁会失效如果程序没有正常退出操作系统会在进程结束时自动释放锁所以不用担心僵尸锁。多实例问题解决后你的桌面宠物基本达到了可以发给朋友试玩的稳定性。6. 进阶验证用 jpackage 打包成 exe并在 100 台机器上测性能能跑通只是开始真正让别人用起来你需要打包和验证两个动作。这里说一个我常用的最小流程用 JDK 自带的 jpackage 生成免安装的 app-image然后用一段简单的帧率打点代码确认动画流畅。6.1 打包前检查的三件事打包之前先检查你的资源文件路径。桌面宠物的图片通常放在项目根目录的 resources 下如果你直接用相对路径 new ImageIcon(pet_idle.gif)在 IDE 里能跑但打包后 jar 中的资源路径会变成 jar:/... 的二进制流直接访问文件路径会失败。正确做法是统一走类加载器getClass().getResource(/images/pet_idle.gif)并把 resources 目录设为 jar 包的根。第二件事是确认 JDK 版本。jpackage 是从 JDK 14 开始提供的如果你还在用 JDK 8只能退回去用 exe4j 或者手动打 jar 包。我的常用命令是jpackage --name DesktopPet --input target --main-jar pet.jar --main-class DesktopPet --type app-image --dest dist这条命令会在 dist 目录生成一个完整的 app 目录里面有 exe 和运行时双击就能跑。注意 jpackage 不会自动把你的资源文件打进镜像你要么把 resources 里的图片复制到 input 目录要么在 jar 里用 getResource 加载。图片尺寸建议都控制在 512x512 以下否则打包出来的体积会占你 jar 包几十 MB启动还变慢。第三件事是验证打包后的目录结构。app-image 类型不会弹安装向导适合直接发给用户如果要 Windows 安装程序用 --type exe但需要系统装了 WiX 工具。我一般先出 app-image 自测确认资源、动画、托盘都没问题了再出安装包。6.2 性能验证帧率、CPU、内存的量化方法很多人调动画全凭肉眼觉得“看起来不卡”就完事。但桌面宠物要常驻后台量化指标很重要。我一般监控两个数字帧率和 CPU 占用。给 PetPanel 加一个最简单的帧率打点不用额外工具就在 paintComponent 里数调用次数private long lastFpsTime System.nanoTime(); private long frameCount 0; Override protected void paintComponent(Graphics g) { super.paintComponent(g); frameCount; long now System.nanoTime(); if (now - lastFpsTime 1_000_000_000L) { // 每 1 秒统计一次 System.out.println(fps: frameCount); frameCount 0; lastFpsTime now; } g.drawImage(frames[current], 0, 0, getWidth(), getHeight(), null); }这个打印输出对调试有帮助但不要留在最终版里频繁写控制台本身也会吃 CPU。如果是间隔 150ms 的 4 帧动画理想数字是每秒约 6 到 8 次刷新因为一帧画面要持续 4 个 tick 才轮一圈。如果你看到 fps 在 20 以上说明你的刷新间隔设置得太短或者有多个动画线程在同时跑。CPU 占用用 Windows 自带的任务管理器看 java.exe 进程就够了空闲状态应该低于 2%动画状态不要超过 8%如果超过优先检查有没有旧 Timer 没停这是最常见的翻车点。我自己在写这个例子时踩过最深的坑是透明窗口和坐标系第一次用 getScreenSize 结果副屏直接跑飞。后来把显示器遍历和边界检查做成工具方法才稳定下来。你如果照着这个流程做不用等到打包跑通动画那一步就能明显感觉到 Java 桌面的控制力比想象中要强。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?
咨询建站