资讯动态

JavaFX音乐播放器开发实战:MediaPlayer状态机与打包避坑指南

发布时间:2026/10/15 17:30:44 来源:尧图企业网站定制
简介这份压缩包是一份JavaFX制作的简易音乐播放器完整源码示例主要面向正在学习Java桌面应用开发的初学者也适合需要快速搭建播放器原型或复习JavaFX组件的开发者参考。包内共35个文件包括13个编译好的class文件、8个java源文件、2个fxml界面布局文件以及项目配置、prefs、构建脚本和备份文件等整体体积只有30KB结构紧凑没有冗余资源便于逐文件阅读和修改。资源发布以来已有1371人学习下载具备一定的参考热度。通过这份源码可以清晰看到JavaFX项目如何划分UI层与逻辑层FXML文件负责界面结构Java代码处理播放、暂停、切歌等交互同时还能参考其Maven或Eclipse工程配置方式了解从源码编译到运行的完整组织方法。对想要动手实践JavaFX媒体播放功能的读者来说是一份轻量便捷的练习素材。1. 简易音乐播放器比想象中简单也比想象中容易翻车用 JavaFX 做一个简易音乐播放器听起来像是某高校课程设计的老题目但真动手做过的开发者都知道把界面搭出来只要半天让播放器在切歌、拖进度、换音频格式时不崩、不卡、不出玄学问题才是真正耗时间的地方。这个项目本质上是JavaFX 桌面应用骨架 MediaPlayer 状态机 本地文件 IO三件事的组合解决的是你手里正好有一批本地音频、想在桌面端用一个轻量工具按列表播放又不想被重型播放器绑定的场景。适合刚学完 Java 语法、准备第一个图形界面项目的学生也适合需要内部工具快速播放音频文件的一线开发者。我是按先能出声再像播放器最后能交付的顺序来做这条路最稳。2. 选型与骨架为什么是 JavaFX 的 MediaPlayer而不是 Swing 或命令行2.1 技术选型JavaFX 正好有 Media 与 MediaPlayer但底层是平台编解码器做音乐播放器第一步不是写 UI而是选对能播放音频的 API。Swing 本身没有成熟的音频播放组件JLayer 这类第三方库只解 MP3WAV 又得自己读采样数据再送音频线代码量会失控命令行调用外部播放器则完全拿不到播放进度和状态回调。JavaFX 自带javafx.scene.media包里面有Media、MediaPlayer、MediaView三个核心类其中MediaView是给视频用的做纯音频播放器不需要它但前面的课程设计例子经常把它一起拉进来这是新手常见的第一个多余动作。Media负责描述放什么它接收一个 URL 字符串MediaPlayer负责怎么放包括播放、暂停、停止、跳转、音量、循环等操作。要特别明确一点JavaFX 的MediaPlayer并不是自己解码音频的播放引擎它通过 JavaFX Media 框架调用当前操作系统的底层多媒体编解码能力。这意味着能不能放这个文件取决于你的操作系统和 JavaFX 运行时带了多少原生编解码器而不是你写的 Java 代码。MP3、WAV、AAC 在主流平台上通常没问题FLAC 这类无损格式在部分平台的标准 JavaFX 构建里会直接抛MediaException这个前置认知能帮你省掉后面一整夜的排查时间。2.2 先搭一个能出声的最小工程模块描述、目录结构与启动类我一般建议先用 Maven 构架一个最小 JavaFX 工程目录固定为标准的src/main/java结构不接 FXML不接 Scene Builder先把Application子类跑起来再说。JavaFX 从 11 开始不再是 JDK 自带的一部分所以必须在pom.xml里显式声明依赖这里不写死版本号用你本机安装的 JavaFX 对应发行版即可。工程里第一个要写的是module-info.javaJavaFX 11 之后推荐用模块描述文件声明依赖module music.player { requires javafx.controls; requires javafx.media; exports musicplayer; }逻辑说明requires javafx.controls是因为要用Button、ListView、Slider这些控件requires javafx.media是本次项目的核心依赖提供Media和MediaPlayerexports musicplayer把主包导出给外部模块。如果没有这一步运行时大概率报ModuleNotFoundException或IllegalAccessError而且不一定第一眼看得懂。参数说明如果你的工程里后续要读取 FXML 并通过反射注入控制器还需要opens musicplayer to javafx.fxml。这个模块描述是 JavaFX 项目里最容易踩的隐性地雷少写requires javafx.media时项目能编译一运行就崩。2.3 加载本地音频并播放Media、MediaPlayer 与线程模型的关系最小可播放代码是理解这三个类关系的最短路径。下面这段代码不需要 UI直接在start方法里播放一首音频文件package musicplayer; import javafx.application.Application; import javafx.scene.media.Media; import javafx.scene.media.MediaPlayer; import javafx.stage.Stage; import java.nio.file.Path; public class Main extends Application { Override public void start(Stage primaryStage) { Media media new Media(Path.of(song.mp3).toUri().toString()); MediaPlayer mediaPlayer new MediaPlayer(media); mediaPlayer.setOnReady(() - { System.out.println(时长: mediaPlayer.getTotalDuration().toSeconds() 秒); mediaPlayer.play(); }); primaryStage.setTitle(最小播放器); primaryStage.show(); } public static void main(String[] args) { launch(args); } }逻辑说明Media的构造参数必须是 URL 格式。很多人会直接传song.mp3或file:///song.mp3前者会被当作相对 URL 解析失败后者在带空格或中文路径时大概率翻车。用Path.toUri().toString()是通用做法它会自动完成编码和绝对路径转换。setOnReady是播放器状态变为 ready 时的回调只有进入 ready 状态后getTotalDuration()才有效这时才能安全调用play()。参数说明MediaPlayer有setOnPlaying、setOnPaused、setOnStopped、setOnEndOfMedia、setOnError这一组状态回调后续的播放列表逻辑全靠它们驱动。在还没有进入 ready 状态之前调用play()不会报异常但音频设备可能没有正确初始化在错误状态下调用stop()则可能触发MediaException。这个最小工程能出声说明环境没问题接下来才值得搭 UI。3. UI 与状态联动从「能出声」到「像一个播放器」3.1 用 BorderPane 搭主界面中置播放列表、底置控制栏播放器的界面布局是有固定套路的左侧或中间放列表底部放控制栏顶部可以放标题和搜索框。JavaFX 的BorderPane正好适合这种结构它把窗口分成上、下、左、右、中五个区域。我一般把ListView放在中心区把控制栏放在bottom区这样窗口尺寸变化时列表能自动撑开控制栏保持固定高度。import javafx.application.Application; import javafx.geometry.Insets; import javafx.scene.Scene; import javafx.scene.control.Button; import javafx.scene.control.Label; import javafx.scene.control.ListView; import javafx.scene.control.Slider; import javafx.scene.layout.BorderPane; import javafx.scene.layout.HBox; import javafx.stage.Stage; import java.io.File; public class PlayerApp extends Application { private ListViewFile playlistView new ListView(); private Label timeLabel new Label(00:00 / 00:00); private Slider progressSlider new Slider(0, 100, 0); Override public void start(Stage primaryStage) { BorderPane root new BorderPane(); Button prevButton new Button(上一首); Button playButton new Button(播放); Button nextButton new Button(下一首); HBox controlBar new HBox(10, prevButton, playButton, nextButton, progressSlider, timeLabel); controlBar.setPadding(new Insets(10)); root.setCenter(playlistView); root.setBottom(controlBar); primaryStage.setScene(new Scene(root, 720, 480)); primaryStage.setTitle(JavaFX 简易音乐播放器); primaryStage.show(); } public static void main(String[] args) { launch(args); } }逻辑说明ListViewFile直接以文件对象作为列表项比存字符串更合理因为后续播放时需要的是文件的路径和 URI界面展示时再通过toString()取文件名。控制栏用HBox排列Slider放在按钮和标签之间保持上一首/播放/下一首/进度/时间的常见播放器顺序符合用户习惯。参数说明Slider(0, 100, 0)的三个参数是最小值、最大值、当前值。这里暂时用 0 到 100 的百分比值等播放器就绪后再根据音频总时长动态调整范围。HBox(10, children)的第一个参数是子节点间距单位是像素10 像素是比较紧凑又不拥挤的间距。3.2 播放列表 ListView 与当前歌曲高亮选择、双击播放与索引同步有了静态界面下一步给列表装行为。需要支持两种触发方式单击选中但不播放、双击播放、播放结束后自动播放下一条。当前播放的歌曲要能在列表中高亮出来方便用户知道耳朵里听到的是哪一首。playlistView.setCellFactory(param - new ListCell() { Override protected void updateItem(File file, boolean empty) { super.updateItem(file, empty); if (empty || file null) { setText(null); } else { setText(file.getName()); } } }); playlistView.getSelectionModel().selectedIndexProperty().addListener((obs, oldIndex, newIndex) - { if (newIndex.intValue() 0 playlistView.getItems().size() newIndex.intValue()) { currentIndex newIndex.intValue(); } }); playlistView.setOnMouseClicked(event - { if (event.getClickCount() 2) { int index playlistView.getSelectionModel().getSelectedIndex(); if (index 0) { playAt(index); } } });逻辑说明setCellFactory自定义了列表项的展示方式默认File.toString()会显示完整路径改成file.getName()后界面只显示文件名。selectedIndexProperty的监听器用来维护当前选中索引这样即使用户只单击不双击播放逻辑也能知道自己应该从哪个位置继续。双击判定用getClickCount() 2这是 JavaFX 里判断双击的标准写法注意双击事件会先触发一次单击事件所以不要在单击监听里做播放动作。参数说明高亮实际上不用手动设置颜色ListView默认选中项就有蓝色背景前提是样式表没有覆盖默认选中状态。如果后面引入自定义 CSS要小心把.list-cell:selected的样式改了之后看不到选中项这是换皮肤时最常见的隐形问题。3.3 进度条与时间显示currentTimeProperty 绑定与 seek 的一个注意点进度条是播放器里牵一发而动全身的地方。MediaPlayer有两个属性直接驱动进度currentTimeProperty()持续发出当前播放到的时间点totalDurationProperty()在就绪后发出总时长。把这两个属性绑到进度条上UI 就能自动跟着播放走。mediaPlayer.currentTimeProperty().addListener((obs, oldTime, newTime) - { Duration total mediaPlayer.getTotalDuration(); if (total ! null total.toMillis() 0) { double percentage newTime.toMillis() / total.toMillis() * 100.0; progressSlider.setValue(percentage); } timeLabel.setText(formatTime(newTime) / formatTime(total)); }); progressSlider.valueProperty().addListener((obs, oldVal, newVal) - { if (progressSlider.isValueChanging()) { Duration total mediaPlayer.getTotalDuration(); if (total ! null total.toMillis() 0) { mediaPlayer.seek(total.multiply(newVal.doubleValue() / 100.0)); } } });逻辑说明第一段监听器的职责是播放器动进度条跟着动第二段监听器的职责是用户拖进度条播放器跳到对应位置。这两个方向如果不做区分会互相触发进度条值一变播放器 seekseek 又触发 currentTime 变化又回头设置进度条形成一个肉眼可见的循环抖动。isValueChanging()的作用就是识别当前值变化是由用户拖动触发的而不是由程序赋值触发的这是进度条联动里最关键的一行。参数说明seek接收的是绝对时间点Duration不是相对偏移量。把百分比换算成Duration时用total.multiply(ratio)而不是Duration.seconds(total.toSeconds() * ratio)后者在精度上会有微小误差播放时间久了会明显对不上。formatTime建议写成mm:ss格式超过一小时的音频再用hh:mm:ss这个函数自己随手写就行。4. 播放控制与异常分支切歌、暂停、循环与格式兜底4.1 播放状态机idle/ready/playing/paused/ended 之间的转换播放器做到这个阶段靠播放按钮里直接调 play()已经不够用了。用户会在歌曲播放中时点击下一首、在暂停状态点击上一首、在刚开始加载时疯狂点击按钮这些操作交错出现会让参数状态互相打架。我一般会在项目里维护一个显式的播放状态枚举让每个按钮动作都先检查状态再做响应。enum PlayState { IDLE, READY, PLAYING, PAUSED, STOPPED, ENDED } private PlayState playState PlayState.IDLE; private void bindMediaPlayer(MediaPlayer player) { player.setOnPlaying(() - playState PlayState.PLAYING); player.setOnPaused(() - playState PlayState.PAUSED); player.setOnStopped(() - playState PlayState.STOPPED); player.setOnEndOfMedia(() - playState PlayState.ENDED); player.setOnReady(() - playState PlayState.READY); } private void playPause() { switch (playState) { case PLAYING - mediaPlayer.pause(); case PAUSED, READY, ENDED - mediaPlayer.play(); case STOPPED, IDLE - { /* 先加载再播放 */ } } }逻辑说明MediaPlayer内部本身有状态但它的状态转换会异步发生比如play()调用之后状态不会立刻变成 playing中间可能经过若干帧的缓冲。自己维护一份PlayState是为了让业务逻辑不直接依赖底层状态按钮点击时通过 switch 决定当前处境下应该做什么动作。setOnPlaying这些回调是状态机更新的唯一入口不要在图方便时自己直接赋值否则会出现界面显示播放中、实际声音已经停了的脱节现象。参数说明JavaFX 的MediaPlayer没有公开的setOnReady之外的状态查询方法getStatus()返回的也只是底层状态所以在业务层自己维护一份状态是常见做法。setOnEndOfMedia里记得先更新状态再处理切歌逻辑否则自动切歌会漏掉一次状态刷新。4.2 自动切歌与循环模式setOnEndOfMedia 的三种常见实现自动切歌是播放器区别于单曲播放工具的关键功能。三种常见做法里最简单的是顺序播放到尾停止其次是顺序循环最后是单曲循环。只需要一个枚举加一个switch就能完整覆盖。enum RepeatMode { NONE, ALL, ONE } private ListFile playlist new ArrayList(); private int currentIndex 0; private RepeatMode repeatMode RepeatMode.ALL; private void playAt(int index) { if (index 0 || index playlist.size()) return; currentIndex index; releaseMediaPlayer(); File file playlist.get(index); Media media new Media(file.toURI().toString()); mediaPlayer new MediaPlayer(media); bindMediaPlayer(mediaPlayer); mediaPlayer.setOnReady(() - { progressSlider.setMax(100); mediaPlayer.play(); }); mediaPlayer.setOnEndOfMedia(this::handleEndOfMedia); } private void handleEndOfMedia() { switch (repeatMode) { case ONE - mediaPlayer.seek(Duration.ZERO); case ALL - playAt((currentIndex 1) % playlist.size()); case NONE - { /* 停在 ENDED 状态等待用户操作 */ } } }逻辑说明handleEndOfMedia是整个自动切歌的核心。单曲循环用seek(Duration.ZERO)回到开头再继续播放效率比重新创建MediaPlayer高很多而且不会短暂黑屏无声。列表循环用取模运算绕回开头边界处(currentIndex 1) % playlist.size()在播最后一首时会正确回到 0。NONE模式其实什么都不用做只更新状态让播放器静默停在 ended。参数说明releaseMediaPlayer()是必须写的释放函数里面要做mediaPlayer.stop()、mediaPlayer.dispose()然后置空引用。切歌时如果直接 new 一个新的MediaPlayer而不释放旧的音频设备会被占满播到第三四首就开始卡顿。JavaFX 里dispose()会释放原生资源这个动作没有后悔药释放后不能再对旧实例调用任何方法。4.3 对不支持的本地音频做兜底读 MediaPlayer.error 而不是凭扩展名判断项目做到能播放 MP3马上就会遇到为什么这首歌播放不了的报告。最常见的两个原因文件扩展名是.mp3但实际编码是别的格式或者 FLAC 文件在目标机器上缺少解码头。别依赖扩展名判断能不能播正确姿势是统一尝试创建MediaPlayer然后监听 error 事件。mediaPlayer.setOnError(() - { MediaException error mediaPlayer.getError(); if (error ! null) { switch (error.getType()) { case MEDIA_UNAVAILABLE - showAlert(媒体不可用文件可能被移动或删除); case MEDIA_INCORRECT_CODE - showAlert(编码格式不支持缺少对应编解码器); default - showAlert(播放失败 error.getMessage()); } } playNextAfterError(); });逻辑说明MediaPlayer.getError()返回MediaException里面带一个MediaException.Type枚举常见的有MEDIA_UNAVAILABLE文件不存在或 URL 错误、MEDIA_INCORRECT_CODE解码器不支持、MEDIA_UNSUPPORTED格式整体不支持。根据不同类型给用户不同的提示文案比一概显示播放失败体验好得多。playNextAfterError()是业务层回退策略我一般会让它自动跳到下一首但只在repeatMode ALL时这样做否则用户会困惑于为什么老是自己跳歌。参数说明setOnError也同样适用于Media对象本身Media的错误可以通过Media.getError()读取。两条链路要分别处理有些错误在构建Media时就已经发生根本等不到MediaPlayer创建。一个隐藏点是MediaPlayer.getError()在成功播放过一次之后不会自动清空所以每次切换歌曲前先检查并广播一次错误状态是不少项目的默认做法。5. JavaFX 音乐播放器的避坑指南5 个让初学者原地翻车的细节5.1 现象双击歌曲没声音控制台也干干净净双击列表项界面没有异常没有报错堆栈音量也确认开启但就是不出声。原因最常见的是Media构造参数传了本地路径字符串D:/music/song.flac而不是 URL。Media要求的参数是符合 URL 规范的字符串本地文件必须用file:协议前缀并正确编码。更隐蔽的一种是文件路径里包含中文或空格时直接拼字符串生成的 URL 是非法字符解码失败静默发生监听器里如果没有挂setOnError就完全看不到原因。解决统一用Path.of(file).toUri().toString()或者file.toURI().toString()把编码交给标准库不要自己拼接。同时所有MediaPlayer创建后立刻挂setOnError哪怕只是打印一行日志也能避免排查时像无头苍蝇一样乱试。5.2 现象切歌时偶发 IllegalStateException快速连续点击下一首偶然会抛IllegalStateException崩溃点指向mediaPlayer.play()或mediaPlayer.stop()。原因MediaPlayer没有准备好的时候调用 seek 或 play或者旧MediaPlayer已经dispose()过了业务代码还在持有旧引用调用方法。由于切歌逻辑里创建新实例和释放旧实例是分开写的并发点击会让两条路径交错执行出现上一秒还在播的歌下一秒已经被释放的状态。解决把释放旧实例、创建新实例、绑定事件、调用 play放在同一个方法里顺序执行加一个loading标志位在setOnReady回调之前拒绝所有播放控制输入。我一般在 UI 层用一个boolean isSwitching状态切歌过程中按钮全部置灰从物理上消除竞态窗口。这个做法有点笨但胜在稳定播放器这类小工具不值得为并发体验引入复杂锁。5.3 现象拖动进度条后播放位置跳回开头用户把播放进度拖到 2 分 30 秒松手后播放器先跳过去然后立刻回到开头重新播。原因Slider的valueProperty监听器和currentTimeProperty监听器互相触发。用户拖动进度条 → 进度条值变化 →seek()→ 播放器 currentTime 变化 →setValue()重置进度条 → 又触发一次进度条监听 → 再次seek()到错误位置。因为没有对程序赋值变化和用户拖动变化做区分两次 seek 在播放器尚未稳定时直接打架。解决只在progressSlider.isValueChanging()为 true 时才执行 seek程序赋值时isValueChanging()为 false就不会触发跳转。另一个补充手段是记录一个isSeeking布尔值在 seek 前后手动加锁。注意isValueChanging()在 JavaFX 里是用户正在拖动滑块的滚动过程中的标记松手后它会变回 false所以逻辑上刚好贴合需求。5.4 现象界面卡顿进度条像幻灯片一样一跳一跳播放正常但拖动窗口时界面反应迟钝进度条刷新不平滑CPU 占用莫名飙升。原因currentTimeProperty的刷新频率由播放器驱动本身不高但如果在监听器里做了重量级操作比如文件 IO、字符串拼接大日志、或者调用了布局刷新每个 tick 都会放大开销。另一个常见来源是用了Timeline每 10 毫秒取一次播放位置来更新 UI这完全没必要再一个就是同时在非 JavaFX 线程里修改 UI 属性导致内部死锁或重排风暴。解决把currentTimeProperty的监听器尽量变瘦只做两件事更新进度条值、更新文本标签。文本标签用String.format而不是多次拼接。不要在监听器里读取目录、扫描文件列表。如果发现MediaPlayer时间回调不规律可以换用Timeline每 200 毫秒采样一次进度值这种降频方案能大幅减少布局失效次数已经够肉眼平滑了。5.5 现象开发环境一切正常打成安装包后双击闪退代码在 IDE 里运行没有问题但用 jpackage 或者 fat jar 方式交付后安装完打开就闪退毫无提示。原因JavaFX 的 media 模块是原生代码模块打包时必须把对应的原生库文件一起带出去。纯 jar 打包模式不会包含.dll/.so/.dylib文件jlink 时漏掉javafx.media模块则会在加载播放器时直接抛UnsatisfiedLinkError或NoClassDefFoundError。这类问题在开发环境不会出现因为 IDE 通常是使用完整的 JavaFX SDK 模块在运行。解决交付阶段统一用jlink生成自定义运行时把javafx.controls、javafx.media都放进add-modules列表再交给jpackage打包当前平台安装包。另外在入口方法最前面加一段启动日志输出到文件中这样用户双击闪退时能把日志文件发回来定位到底崩在哪一步。这个文件日志兜底的做法是很多桌面工具开发者交付前必加的一层保险。6. 再往前走一步CSS 皮肤、快捷键与用 jpackage 交付6.1 用 CSS 让默认控件改头换面JavaFX CSS 与网页 CSS 的区别JavaFX 控件支持 CSS 美化但它不是网页 CSS。选择器用.class和#id属性是-fx-background-color这种带前缀的写法。给控制栏加背景、给播放按钮突出配色只需要一份style.css然后在Scene上挂样式表#controlBar { -fx-background-color: #2b2b2b; -fx-padding: 12 16 12 16; -fx-spacing: 12; } #playButton { -fx-background-color: #4a90d9; -fx-text-fill: white; -fx-font-size: 14px; -fx-background-radius: 6; } .list-cell:filled:selected { -fx-background-color: #4a90d9; -fx-text-fill: white; }Scene scene new Scene(root, 720, 480); scene.getStylesheets().add(getClass().getResource(/style.css).toExternalForm()); primaryStage.setScene(scene);逻辑说明-fx-spacing对应 HBox 构造器里的间距参数CSS 里设置后构造器里的值会被覆盖。.list-cell:filled:selected这个伪类选择器必须这样写才能覆盖默认蓝色只写.list-cell拦不住默认样式。CSS 文件放在src/main/resources下通过getResource加载不要拼文件绝对路径否则打包后一定找不到样式。6.2 给播放器加全局快捷键KeyCombination 与 setOnKeyPressed鼠标点按钮不是播放器的完整形态桌面工具加上快捷键才有实用价值。JavaFX 里用KeyCombination描述组合键然后在场景上挂按键事件KeyCombination playPauseKey new KeyCodeCombination(KeyCode.SPACE); KeyCombination nextKey new KeyCodeCombination(KeyCode.RIGHT, KeyCombination.CONTROL_DOWN); scene.setOnKeyPressed(event - { if (playPauseKey.match(event)) { playPause(); } else if (nextKey.match(event)) { playAt((currentIndex 1) % playlist.size()); } });逻辑说明KeyCodeCombination构造器的第一个参数是主键后面的可变参数是修饰键。空格键单独做播放/暂停右方向键加 Ctrl 做下一首这两个组合是播放器标配。要特别注意如果焦点在Button上空格键会被按钮消费掉场景层的按键事件收不到。常见做法是根布局调用setFocusTraversable(true)并把焦点打到根节点或者干脆给控制栏设setFocusTraversable(false)让焦点永远停在舞台根上。6.3 用 jpackage 产出当前平台的安装包模块化与非模块化两种打包方式开发完成后交付是最后的关卡。现在 JavaFX 官方推荐的链路是jlink加jpackage前者产出精简运行时后者把运行时和应用一起封装成当前平台的安装包。两个命令分别执行mvn clean package jlink \ --module-path target/classes \ --add-modules javafx.controls,javafx.media \ --output runtime jpackage \ --input target \ --main-class musicplayer.Main \ --main-jar musicplayer.jar \ --module-path runtime \ --add-modules javafx.controls,javafx.media \ --name MusicPlayer \ --type dmg逻辑说明第一条mvn clean package产出应用 jar第二条jlink把占用的 JavaFX 模块单独抽出来形成一个精简运行时输出到runtime目录第三条jpackage读取这个运行时和应用 jar打包成当前系统的安装包。--type参数在 macOS 上是dmg在 Windows 上是msi或exe在 Linux 上是deb或rpm不同平台用各自对应的值。参数说明--main-class填的是Application子类的全限定名这个必须和代码里的一致--main-jar填的是mvn package产出的 jar 文件名。如果项目没有做模块化没有module-info.javajlink和jpackage的--module-path要指向 JavaFX 的 jmods 目录而不是target/classes两种方式的路径语义完全不同照着命令抄之前先确认自己的项目是不是模块化结构。这个播放器我从最早只有播放按钮的原型一路做到带列表、切歌、循环、CSS 皮肤、打包交付的完整桌面工具最大的教训是JavaFX 写界面真的不难难的是把异步状态、底层解码器边界和打包依赖这三维的暗坑同时填平。每次看到别人在群里问为什么我双击不出声都能猜到八成卡在了Media的 URL 上——这坑我也踩过所以值得写出来。做完这个项目你对 JavaFX 的事件回调、属性绑定和模块化打包的理解会直接上一个大台阶后续做任何桌面工具都有了可复用的骨架。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑