简介:面向Java初学者与GUI编程练习者的桌面宠物示例项目,聚焦Swing图形界面、事件响应与逐帧动画的综合实践,可用于课程作业、自学练手或小型桌面应用原型参考。压缩包共80个文件,约122KB,以java与class源码/编译文件、jpg宠物素材为主,并含工程配置与Git相关记录,便于直接导入IDE运行和二次修改。已有2646人学习下载,说明其作为入门案例具有一定参考价值。读者可从中梳理JFrame与JPanel的组织方式、自定义组件重写paintComponent()的绘制流程,以及Graphics2D绘图、MouseListener与MouseMotionListener交互、javax.swing.Timer动画循环、多线程避免阻塞UI、ImageIcon资源加载等关键点;也可借鉴MVC分层与资源释放思路,理解桌面宠物从静态界面到可拖动、可动画的完整实现脉络。
1. 从一张透明窗口开始:桌面宠物 java 到底能跑多轻
桌面宠物 java 这个小例子,第一眼看上去像玩具:一个透明窗口、几张帧图、鼠标一拖就跑。真拆开看,它把桌面端最容易被忽略的几件事压进了一个很小的工作面——无边框透明窗口、置顶层级、帧动画调度、鼠标坐标换算、资源路径和托盘退出。适合两类人:一是想用 Java 快速做一个能跑在桌面上的课程演示或小工具入口的开发者;二是已经会 Swing,但没系统做过透明窗口、不规则点击区域和打包后资源加载的人。它不解决复杂物理碰撞,也不追求商业级素材管线,价值在于骨架清楚,改起来不费劲。A 同学拿它做某高校课程演示,替换掉素材和菜单就能交差,这个场景很典型。
2. 窗口骨架:用 Swing 把无边框、透明、置顶一次配齐
桌面宠物的第一道门槛不是动画,而是窗口。窗口一旦出现黑底、白底、拖拽漂移或者置顶失效,后面的帧动画做得再顺也会被一眼看穿。这个例子里最值得先吃透的是窗口骨架:JFrame去装饰、背景全透明、内容面板不绘制底色,再把鼠标事件接到面板上。Swing 在这类小体量桌宠上有优势,JDK 自带、依赖少、打包成可执行 jar 的路径短;JavaFX 的透明 Stage 也稳,但模块化打包和运行参数对新手不够友好。选型没有绝对高低,关键是先让窗口在目标系统上稳定透明,再谈动画。
2.1 资源包先拆成窗口层、动画层、交互层
拿到这份小例子,不要急着改图片。先看类之间的分工:窗口层负责JFrame的透明、置顶、位置;动画层负责帧列表、当前帧、状态切换;交互层负责拖拽、右键菜单、托盘退出。资源目录按状态拆开,idle、walk、drag各自一组帧,文件名用“状态_序号.png”的约定。这样后面加状态时不用改加载逻辑,只补图片和枚举项。常见做法是让PetPanel只负责画当前帧,DesktopPet只负责窗口生命周期,SpriteLoader只负责把资源读成BufferedImage列表。三层分开后,排查“图片找不到”和“窗口不透明”不会互相干扰。
| 层次 | 典型类 | 职责 | 排查入口 |
|---|---|---|---|
| 窗口层 | DesktopPet | 创建JFrame、设置透明与置顶、退出 | 窗口是否黑底、是否总在最前 |
| 动画层 | PetPanel、SpriteLoader | 加载帧、定时切帧、绘制 | 是否闪、是否只显示第一帧 |
| 交互层 | 鼠标监听、JPopupMenu、TrayIcon | 拖拽、菜单、托盘 | 拖拽是否漂、右键是否弹 |
提示:资源路径统一用
/assets/pet/...这种以斜杠开头的 classpath 路径,后面打包成 jar 时不容易因为相对路径翻车。
2.2 透明窗口的最小可运行代码
先写一个能跑起来的最小窗口,不接动画。透明窗口的关键顺序是:先setUndecorated(true),再setBackground(new Color(0,0,0,0)),最后setVisible(true)。内容面板要setOpaque(false),否则面板会用默认背景把透明层盖住。下面这段代码可以直接作为入口类,窗口尺寸先给 240×240,图片按窗口大小拉伸,后续再换成按帧尺寸绘制。
import javax.swing.*; import java.awt.*; public class DesktopPet { public static void main(String[] args) { SwingUtilities.invokeLater(() -> { JFrame frame = new JFrame("桌面宠物"); // 先去装饰,再去设置透明背景,顺序反了容易出现黑底 frame.setUndecorated(true); frame.setBackground(new Color(0, 0, 0, 0)); frame.setAlwaysOnTop(true); frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); PetPanel panel = new PetPanel(); panel.setOpaque(false); // 面板不画底色,透明才能透到桌面 frame.setContentPane(panel); frame.setSize(240, 240); frame.setLocationRelativeTo(null); frame.setVisible(true); }); } }逻辑说明:SwingUtilities.invokeLater把窗口创建放回事件调度线程,避免偶发的界面卡死;setUndecorated(true)去掉标题栏和边框;setBackground(new Color(0,0,0,0))把窗口背景设为全透明;setAlwaysOnTop(true)让宠物压在普通窗口上方。参数说明:窗口宽高 240×240 是保守值,帧图如果是 128×128 可以按比例居中;setLocationRelativeTo(null)只用于首次居中,多屏环境不要依赖它做定位。透明窗口在部分 Linux 桌面环境需要窗口管理器支持合成,遇到黑底先确认系统合成是否开启,再怀疑代码。
2.3 拖拽坐标:为什么直接加 e.getX() 会漂
拖拽是桌宠最容易被低估的细节。很多示例在mouseDragged里写frame.setLocation(frame.getX() + e.getX() - pressX, ...),刚开始看着能用,窗口一移动就开始漂,拖得越快偏得越明显。原因是e.getX()是相对当前窗口的坐标,窗口移动后同一个鼠标位置对应的相对坐标已经变了,继续叠加就会累积误差。稳妥做法是在按下时记录两个屏幕坐标:鼠标按下的屏幕点和窗口左上角的屏幕点;拖动时用当前鼠标屏幕点减去按下点,得到位移量,再加到初始窗口位置上。这样窗口移动多少次都不会累积漂移。
final Point pressScreen = new Point(); final Point winStart = new Point(); panel.addMouseListener(new java.awt.event.MouseAdapter() { @Override public void mousePressed(java.awt.event.MouseEvent e) { pressScreen.setLocation(e.getLocationOnScreen()); winStart.setLocation(frame.getLocation()); } }); panel.addMouseMotionListener(new java.awt.event.MouseMotionAdapter() { @Override public void mouseDragged(java.awt.event.MouseEvent e) { Point now = e.getLocationOnScreen(); int nextX = winStart.x + now.x - pressScreen.x; int nextY = winStart.y + now.y - pressScreen.y; frame.setLocation(nextX, nextY); } });逻辑说明:getLocationOnScreen()返回鼠标在屏幕坐标系里的点,不受窗口移动影响;winStart只在按下时记录一次,拖动过程中不变。参数说明:nextX、nextY直接传给setLocation,单位是像素;多屏环境下winStart.x可能是负数,不要用Math.abs修正,负坐标是合法屏幕位置。拖拽过程中如果同时触发动画状态切换,建议在mousePressed里把状态切到DRAG,在mouseReleased里切回IDLE,这样拖拽帧和窗口移动能对上。边界回弹不要放在拖动过程中做,等释放后再夹取位置,否则拖到屏幕边缘会有“粘住”的手感。
3. 动画状态机:把精灵图拆成 idle、walk、drag 三组帧
窗口能透明之后,第二个问题是动画不能乱。桌面宠物不需要复杂骨骼,但必须有状态:站着发呆、走动、被拖拽。每个状态对应一组帧,定时器按状态切换延迟,按下拖拽时立即换组,释放后回到发呆。这里最容易翻车的地方是“所有状态共用一个帧索引”,拖拽时突然跳到走路帧,或者帧率固定导致拖拽帧看起来迟钝。正确做法是状态枚举加帧列表映射,切状态时重置帧索引,按状态设置定时器延迟。帧率不用追求 60fps,桌面宠物 8~12fps 已经够用,CPU 也更容易压住。
3.1 资源命名与加载顺序
资源命名要先定死,再写加载代码。推荐idle_0.png、idle_1.png这种“状态_序号”格式,序号从 0 开始,连续不跳号。加载时按状态和帧数循环拼路径,读不到就抛异常并打印路径,不要返回null让绘制层静默失败。图片统一用带 alpha 的 PNG,避免 JPG 的白底。下面这段加载代码把三个状态映射到EnumMap,每个状态一个List<BufferedImage>,后面切状态只要换 key。
import javax.imageio.ImageIO; import java.awt.image.BufferedImage; import java.io.IOException; import java.net.URL; import java.util.*; public class SpriteLoader { public static Map<PetState, List<BufferedImage>> loadAll() throws IOException { Map<PetState, List<BufferedImage>> map = new EnumMap<>(PetState.class); map.put(PetState.IDLE, loadFrames("idle", 4)); map.put(PetState.WALK, loadFrames("walk", 6)); map.put(PetState.DRAG, loadFrames("drag", 2)); return map; } private static List<BufferedImage> loadFrames(String prefix, int count) throws IOException { List<BufferedImage> frames = new ArrayList<>(); for (int i = 0; i < count; i++) { String path = "/assets/pet/" + prefix + "_" + i + ".png"; URL url = SpriteLoader.class.getResource(path); if (url == null) { throw new IOException("资源缺失: " + path); } frames.add(ImageIO.read(url)); } return frames; } }逻辑说明:EnumMap的 key 是枚举,顺序稳定,查找也快;getResource(path)从 classpath 根开始找,路径必须以/开头。参数说明:count是每个状态的帧数,必须和资源目录里的文件数一致;prefix不要带扩展名。帧数不是越多越好,idle 4 帧、walk 6 帧、drag 2 帧足够表达动作,帧数翻倍会让内存和绘制压力上来。加载失败时直接抛异常比返回空列表好排查,至少日志里能看到具体缺哪一张。
3.2 Timer 驱动与状态切换
动画用javax.swing.Timer,回调在事件调度线程,和 Swing 绘制在同一线程,省掉同步。不要每帧 new 一个 Timer,也不要在一个 Timer 里同时改多个状态。做法是持有一个 Timer,状态切换时改延迟并restart(),帧索引归零。setState里先判断是否同状态,同状态直接返回,避免拖拽过程中反复重置帧导致画面卡在第一帧。
enum PetState { IDLE, WALK, DRAG } // PetPanel 内部字段 private PetState state = PetState.IDLE; private int frameIndex = 0; private Map<PetState, List<BufferedImage>> frames; private final javax.swing.Timer timer = new javax.swing.Timer(200, e -> { List<BufferedImage> list = frames.get(state); frameIndex = (frameIndex + 1) % list.size(); repaint(); }); void setState(PetState next) { if (state == next) return; state = next; frameIndex = 0; timer.setDelay(delayOf(next)); timer.restart(); repaint(); } private int delayOf(PetState s) { switch (s) { case WALK: return 100; case DRAG: return 80; default: return 200; } }逻辑说明:timer只创建一次,回调里按当前状态取帧列表,取模循环;setState负责重置索引和延迟。参数说明:idle 延迟 200ms 约 5fps,walk 100ms 约 10fps,drag 80ms 约 12fps;延迟越小越顺,但 CPU 也会上去。拖拽状态不需要太快,80ms 已经能跟手。定时器启动放在资源加载完成后,不要先启动再加载,否则第一帧可能空指针。释放拖拽时调用setState(PetState.IDLE),不要直接改state字段,否则定时器延迟不会跟着变。
3.3 帧率、缩放和内存
帧图尺寸和窗口尺寸不一致时,绘制阶段缩放最省事,但每帧都做高质量缩放会吃 CPU。桌面宠物窗口小,短时间看不出,挂机半小时风扇开始转。稳妥做法是加载后预缩放到目标尺寸,缓存成新的BufferedImage,绘制时直接drawImage不缩放。预缩放用TYPE_INT_ARGB保留透明通道,插值用双线性,够用且不慢。如果要做高 DPI 适配,按屏幕缩放比例算目标尺寸,不要写死 1 倍。
static BufferedImage scale(BufferedImage src, int w, int h) { BufferedImage dst = new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB); Graphics2D g2 = dst.createGraphics(); g2.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR); g2.drawImage(src, 0, 0, w, h, null); g2.dispose(); return dst; }逻辑说明:新建 ARGB 图像,把原图按目标宽高画进去,保留透明像素;dispose释放绘图上下文。参数说明:w、h是预缩放后的像素尺寸,建议和窗口内容区一致;TYPE_INT_ARGB不能换成TYPE_INT_RGB,否则透明背景会变黑。预缩放放在加载阶段做一次,不要放在paintComponent里。图片总量也要控制,三组帧加起来别超过几十张 512×512 的图,桌面宠物不是游戏资源库,挂机场景下内存和 GC 都会敏感。
4. 交互层:右键菜单、托盘退出和边界回弹怎么接
桌面宠物没有标题栏,用户找不到关闭按钮,交互层必须补上退出路径。右键菜单是最低成本的入口,系统托盘是第二道保险,边界回弹决定拖到屏幕边缘后的手感。这三件事都不难,但跨平台差异明显:右键触发在 Windows 和 macOS 上不完全是同一个鼠标事件,系统托盘在部分 Linux 桌面环境直接不支持,多屏坐标可能是负数。交互层写的时候要按“菜单能弹、托盘能退、边界能夹”三个目标验收,不要等打包后才发现没有退出入口。
4.1 右键菜单与事件分发
右键菜单用JPopupMenu,菜单项至少放“隐藏 10 秒”和“退出”。隐藏用frame.setVisible(false)加一个Timer延迟恢复,退出用frame.dispose()加System.exit(0)。不要只依赖setComponentPopupMenu,透明面板上不同 JDK 和系统的触发时机有差异,稳妥做法是监听mousePressed和mouseReleased,用e.isPopupTrigger()判断,两个事件都试一次。菜单弹出位置用面板坐标,不要用屏幕坐标直接塞给show。
JPopupMenu menu = new JPopupMenu(); JMenuItem hideItem = new JMenuItem("隐藏 10 秒"); JMenuItem exitItem = new JMenuItem("退出"); menu.add(hideItem); menu.add(exitItem); panel.addMouseListener(new java.awt.event.MouseAdapter() { @Override public void mousePressed(java.awt.event.MouseEvent e) { maybeShow(e); } @Override public void mouseReleased(java.awt.event.MouseEvent e) { maybeShow(e); } private void maybeShow(java.awt.event.MouseEvent e) { if (e.isPopupTrigger()) { menu.show(panel, e.getX(), e.getY()); } } }); exitItem.addActionListener(e -> { frame.dispose(); System.exit(0); });逻辑说明:isPopupTrigger由系统判断当前事件是不是右键菜单触发,Windows 和 macOS 的按下/释放时机不同,所以两个事件都接;menu.show(panel, x, y)用组件相对坐标,弹出位置才会贴着鼠标。参数说明:e.getX()、e.getY()是相对面板的坐标,不要换成getLocationOnScreen,否则菜单会飞到屏幕左上角。隐藏逻辑如果加延迟恢复,记得在退出时停掉恢复定时器,避免窗口 already disposed 后还被setVisible(true)拉起来。
4.2 系统托盘不是所有环境都支持
系统托盘用SystemTray.isSupported()先判断,支持再添加TrayIcon。托盘图标要准备一张小尺寸 PNG,setImageAutoSize(true)让系统缩放。托盘菜单里放“显示”和“退出”,退出时先tray.remove(trayIcon)再System.exit(0),否则图标可能残留到下一次启动。部分 Linux 桌面环境没有托盘协议,isSupported()返回 false,这时不要强行添加,直接在右键菜单里保留退出入口即可。
if (SystemTray.isSupported()) { SystemTray tray = SystemTray.getSystemTray(); Image icon = Toolkit.getDefaultToolkit() .getImage(DesktopPet.class.getResource("/assets/pet/tray.png")); PopupMenu popup = new PopupMenu(); MenuItem showItem = new MenuItem("显示"); MenuItem quitItem = new MenuItem("退出"); popup.add(showItem); popup.add(quitItem); TrayIcon trayIcon = new TrayIcon(icon, "桌面宠物", popup); trayIcon.setImageAutoSize(true); showItem.addActionListener(e -> { frame.setVisible(true); frame.toFront(); }); quitItem.addActionListener(e -> { tray.remove(trayIcon); System.exit(0); }); try { tray.add(trayIcon); } catch (AWTException ex) { throw new IllegalStateException("托盘图标添加失败", ex); } }逻辑说明:托盘图标和右键菜单是两套入口,托盘用于窗口隐藏后恢复,右键菜单用于窗口可见时操作;tray.remove放在退出前,减少残留。参数说明:托盘图标建议 16×16 或 32×32,太大在某些系统上会被裁切;setImageAutoSize(true)只影响显示,不改变原图。AWTException要捕获并给出明确信息,不要静默吞掉,否则用户以为程序没启动。
4.3 边界回弹与多屏坐标
拖拽释放后把窗口夹回可视区域,避免拖到屏幕外找不回来。简单做法是用当前窗口的GraphicsConfiguration拿屏幕边界和ScreenInsets,再对 x、y 做Math.max/min。多屏环境下更稳的是先找鼠标所在屏幕,用那个屏幕的 bounds 和 insets 计算。负坐标不要修正,副屏在主屏左侧时 x 就是负数。夹取时留一点边距,让宠物贴边但不完全贴死。
static Rectangle screenBoundsAt(Point p) { for (GraphicsDevice device : GraphicsEnvironment.getLocalGraphicsEnvironment().getScreenDevices()) { Rectangle bounds = device.getDefaultConfiguration().getBounds(); if (bounds.contains(p)) { return bounds; } } return GraphicsEnvironment.getLocalGraphicsEnvironment() .getDefaultScreenDevice().getDefaultConfiguration().getBounds(); } void keepInside(JFrame frame) { Point mouse = MouseInfo.getPointerInfo().getLocation(); Rectangle screen = screenBoundsAt(mouse); Insets insets = Toolkit.getDefaultToolkit() .getScreenInsets(frame.getGraphicsConfiguration()); int minX = screen.x + insets.left; int minY = screen.y + insets.top; int maxX = screen.x + screen.width - insets.right - frame.getWidth(); int maxY = screen.y + screen.height - insets.bottom - frame.getHeight(); int x = Math.max(minX, Math.min(frame.getX(), maxX)); int y = Math.max(minY, Math.min(frame.getY(), maxY)); frame.setLocation(x, y); }逻辑说明:先按鼠标位置找屏幕,避免窗口跨屏时用错屏幕边界;ScreenInsets扣掉任务栏和系统栏,防止宠物被任务栏盖住。参数说明:minX、minY是可视区域左上角,maxX、maxY是允许的窗口左上角最大值;如果窗口比屏幕还宽,maxX会小于minX,这时要单独处理,至少保证左上角可见。夹取放在mouseReleased里调用,不要放在mouseDragged里,拖动过程中夹取会让手感发涩。
5. 避坑与排查:透明窗口黑底、拖拽偏移、打包丢图
这份小例子最容易让人后悔的地方,不是代码写不出来,而是“本机跑得好好的,换台机器就黑底”“IDE 里图片正常,打成 jar 就找不到”“拖拽刚开始准,拖两下就飘”。下面几条都是桌面宠物场景里高频出现的翻车点,按现象、原因、解决拆开写。排查时先复现最小路径:只留窗口和一张图,确认透明和资源没问题,再把动画和交互加回来。不要一上来就怀疑 JDK 有 bug,多数问题出在设置顺序、坐标系统和资源路径。
5.1 现象:窗口整块黑底或白底
现象:窗口启动后宠物图片周围是一块黑色或白色矩形,桌面透不过去。原因通常是透明设置顺序不对:先setVisible(true)再setBackground,或者内容面板setOpaque(true)把底色画上了;也有可能是系统窗口管理器没有开启合成,透明背景退化成黑色。解决:把setUndecorated(true)、setBackground(new Color(0,0,0,0))放到setVisible(true)之前,面板统一setOpaque(false),绘制时不要fillRect整个背景。Linux 环境先确认合成器可用,再考虑换窗口管理器或改用setShape裁形。
JFrame frame = new JFrame(); frame.setUndecorated(true); // 1. 去装饰 frame.setBackground(new Color(0, 0, 0, 0)); // 2. 透明背景 PetPanel panel = new PetPanel(); panel.setOpaque(false); // 3. 面板不画底色 frame.setContentPane(panel); frame.setSize(240, 240); frame.setVisible(true); // 4. 最后显示逻辑说明:四步顺序不要换,透明背景要在窗口显示前生效;面板不透明会直接盖掉窗口透明层。参数说明:new Color(0,0,0,0)的第四个参数是 alpha,0 表示全透明;不要用new Color(0,0,0),那是黑色不透明。如果图片本身带白底,透明窗口也救不了,先用图片工具确认 alpha 通道。
5.2 现象:拖拽时窗口越拖越偏
现象:鼠标按在宠物身上拖动,窗口移动方向和鼠标不一致,拖得越快偏得越多,松手后位置对不上。原因是用e.getX()、e.getY()这种相对窗口的坐标做累加,窗口一移动,相对坐标的参考系就变了。解决:按下时记录鼠标屏幕点和窗口屏幕点,拖动时用getLocationOnScreen()算位移,再设置窗口位置;不要用frame.getX() + e.getX()这种写法。拖拽状态切换放在按下和释放事件里,不要放在拖动事件里反复切。
5.3 现象:IDE 里能显示,打成 jar 后图片找不到
现象:在 IDE 里运行正常,打包后启动只看到空窗口,日志里getResource返回null。原因多是资源路径写成了相对文件路径,或者大小写和 jar 内目录不一致;getResourceAsStream和getResource的路径规则也不同。解决:统一用 classpath 绝对路径/assets/pet/idle_0.png,资源目录参与打包,文件名大小写和代码完全一致。排查时先打印 URL,看它是不是jar:file:...开头,如果是null,说明资源根本没进包。
URL url = DesktopPet.class.getResource("/assets/pet/idle_0.png"); System.out.println("resource = " + url); if (url == null) { throw new IllegalStateException("图片没有打进 classpath,检查 resources 目录"); }逻辑说明:getResource返回 URL,能直接看出资源来自文件系统还是 jar;返回null就不要继续往下走,否则ImageIO.read(null)会抛更难懂的异常。参数说明:路径开头的/表示从 classpath 根开始,不要写成assets/pet/...再依赖当前类包路径。资源目录名不要用中文和空格,打包工具对大小写敏感的环境里更稳。
5.4 现象:多屏或系统缩放后位置不对
现象:把宠物拖到副屏,窗口突然跳回主屏,或者在高 DPI 屏幕上图片模糊、窗口尺寸和鼠标位置对不上。原因是setLocationRelativeTo(null)只按主屏居中,getX()在多屏下可能是负数,系统缩放又会让逻辑像素和物理像素不一致。解决:首次定位用鼠标所在屏幕的 bounds,不要依赖默认屏幕;边界夹取用GraphicsDevice找屏;高 DPI 下按GraphicsConfiguration.getDefaultTransform()算缩放,或者至少保证图片按整数倍缩放。负坐标是合法值,不要用Math.abs强行修正。
| 检查项 | 常见错误 | 正确做法 |
|---|---|---|
| 首次位置 | 只用setLocationRelativeTo(null) | 取鼠标所在屏幕 bounds 后居中 |
| 副屏坐标 | 把负数当异常 | 允许负坐标,按屏幕 bounds 夹取 |
| 高 DPI | 固定 1 倍尺寸 | 读取缩放比例后预缩放帧图 |
| 任务栏遮挡 | 只用屏幕宽高 | 扣掉ScreenInsets |
5.5 现象:挂机一段时间 CPU 升高、动画发卡
现象:刚启动很顺,挂机十几分钟后风扇转起来,拖拽也变得一顿一顿。原因常见三类:每帧在paintComponent里做高质量缩放;定时器延迟设得太小,比如 16ms 追 60fps;图片资源太大,GC 频繁。解决:加载阶段预缩放并缓存,绘制阶段只drawImage;定时器延迟按状态设 80~200ms,不要全局 16ms;帧图尺寸控制在窗口大小的 2 倍以内。用jvisualvm或简单的帧计数日志看每秒重绘次数,重绘次数远高于设定帧率,说明有其他地方在反复repaint。
6. 进阶验证:用三个硬指标把桌面宠物调稳
调桌面宠物不要靠“看着还行”,要看三个硬指标:透明是否真透明、拖拽是否零漂移、打包后资源是否完整。透明验证用一张四角带透明像素的测试图,启动后看四角能不能透出桌面;拖拽验证在副屏负坐标区域拖一个来回,松手后窗口左上角和鼠标相对位置不变;资源验证直接运行打包后的 jar,不依赖 IDE 的工作目录。再加一项 CPU 观察,挂机十分钟看进程占用是否稳定。下面这张表可以当验收清单,每项都过一遍再换素材。
| 验收项 | 操作方法 | 通过标准 |
|---|---|---|
| 透明窗口 | 四角透明测试图启动 | 四角透出桌面,无黑底白底 |
| 拖拽坐标 | 主屏拖到副屏再拖回 | 窗口不跳、不漂、松手位置准 |
| 资源加载 | 直接运行打包后的 jar | 每个状态帧都能显示 |
| CPU 占用 | 挂机 10 分钟观察 | 占用平稳,无明显爬升 |
| 退出路径 | 右键菜单和托盘各退出一次 | 进程结束,托盘无残留 |
// 简单的帧计数日志:每 5 秒打印一次实际重绘次数 private int paintCount = 0; private long lastReport = System.currentTimeMillis(); @Override protected void paintComponent(Graphics g) { super.paintComponent(g); paintCount++; long now = System.currentTimeMillis(); if (now - lastReport >= 5000) { System.out.println("5s repaint = " + paintCount); paintCount = 0; lastReport = now; } // 正常绘制当前帧 }逻辑说明:重绘计数能看出定时器和界面其他逻辑有没有互相打架;如果设定 10fps,5 秒理论重绘约 50 次,实际远高于这个数就要查重复repaint。参数说明:统计窗口按 5 秒一次,不要每次都打印,否则日志本身会拖慢界面。验证完把日志删掉或降级为调试开关,别留在正式包里。从那以后我每次新建透明窗口项目,都会先把透明、置顶、拖拽、资源加载四条链路各跑一遍,再谈动画和素材。希望帮到你。
本文还有配套的精品资源,点击获取