2023 IDEA 开发桌面图形界面程序 JavaFX 并导出 Jar 支持多系统
2023-08-17
阅读 {{counts.readCount}}
评论 {{counts.commentCount}}
## 前言
首先申明,我也是最近才研究,很多方法只是我跑通了分享出来,并不一定是最优解,如果以后有新的理解再回来更新。
先解释下 `JavaFX` 分为2个分支
1. 早期的是 `Oracle JDK 8` 自带的 `JavaFX` (恶心的地方是,OpenJDK 8无此功能 需要额外引入依赖,Oracle JDK8 已停止开源)
2. 后期的是 `OpenJDK 11` 及以上,引入开源的 `openjfx` (推荐)
本文基于的是后者,具体环境如下
系统: `Linux Deepin 20.9` / `Windows 11`
JDK: `AdoptOpenJDK 17.0.7+7`
IDE: `IntelliJ IDEA 2023.1.2 (Ultimate Edition)`
(补上JDK下载地址: [https://adoptium.net/zh-CN/temurin/releases/](https://adoptium.net/zh-CN/temurin/releases/) )
Maven依赖:
```xml
<dependency>
<groupId>org.openjfx</groupId>
<artifactId>javafx-controls</artifactId>
<version>17.0.6</version>
</dependency>
<dependency>
<groupId>org.openjfx</groupId>
<artifactId>javafx-fxml</artifactId>
<version>17.0.6</version>
</dependency>
```
<br>
**本文最终目标如下:**
简单实现一个 `JavaFX` 桌面图形界面程序
原生支持 `Windows` `Mac` `Linux` 三系统
导出为可执行程序 `jar` 文件
并附上完整JDK依赖文件夹
编写 `shell/bat` 脚本
即:无需安装JDK和配置系统环境变量,双击start.sh即可运行
<br>
## 折腾
<br>
**新建项目**
左边栏选择 `JavaFX`
JDK选择 `AdoptOpenJDK 17.0.7+7`
其余默认即可
![New Project](/api/file/getImage?fileId=64dc4314da74050014006af4)
<br>
**配置并运行项目**
如果IDEA自带的Maven速度很慢,建议配置为自己本地的Maven,并配置阿里云镜像 (具体方法略过)
我个人还会删除一些不需要的依赖和文件
等待依赖加载完成并简单配置过后
执行 `src/main/java/HelloApplication` 下的 `main` 方法
![Run Project](/api/file/getImage?fileId=64dc44a3da74050014006af5)
如果一切正常就会看到这个 `Hello World` 小程序执行出来的窗口
![Window](/api/file/getImage?fileId=64dc44e0da74050014006af6)
<br>
**简单调整FXML组件样式**
(这一步可跳过 不影响后续导出操作)
这里只演示基本的FXML组件,未实现具体功能 (那是另外的价钱 Doge)
完整代码开源地址: [https://gitee.com/zzzmhcn/JavaFX-Demo](https://gitee.com/zzzmhcn/JavaFX-Demo)
<br>
修改3个文件:
`hello-view.fxml`
```fxml
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>
<BorderPane xmlns="http://javafx.com/javafx/17.0.2-ea" xmlns:fx="http://javafx.com/fxml/1"
fx:controller="com.zzzmh.javafxdemo.HelloController">
<top>
<ToolBar>
<MenuBar>
<Menu text="文件">
<MenuItem text="设置"/>
<MenuItem text="退出"/>
</Menu>
<Menu text="编辑">
<MenuItem text="查找"/>
<MenuItem text="替换"/>
</Menu>
<Menu text="关于">
<MenuItem text="帮助"/>
<MenuItem text="关于"/>
</Menu>
</MenuBar>
</ToolBar>
</top>
<left>
<TreeView prefWidth="200">
<TreeItem expanded="true" value="菜单选项">
<children>
<TreeItem expanded="true" value="用户配置">
<children>
<TreeItem value="创建用户"/>
<TreeItem value="用户列表"/>
<TreeItem value="权限配置"/>
</children>
</TreeItem>
<TreeItem expanded="true" value="统计报表">
<children>
<TreeItem value="数据管理"/>
<TreeItem value="导出报表"/>
</children>
</TreeItem>
<TreeItem expanded="true" value="系统管理">
<children>
<TreeItem value="查看日志"/>
<TreeItem value="运行状态"/>
</children>
</TreeItem>
</children>
</TreeItem>
</TreeView>
</left>
<center>
<TabPane prefHeight="200.0" prefWidth="200.0" tabClosingPolicy="UNAVAILABLE" BorderPane.alignment="CENTER">
<Tab text="用户管理">
<TableView>
<columns>
<TableColumn text="ID"/>
<TableColumn text="用户名"/>
<TableColumn text="邮箱"/>
<TableColumn text="权限"/>
<TableColumn text="状态"/>
<TableColumn text="创建时间"/>
</columns>
</TableView>
</Tab>
<Tab text="查看日志">
<TextArea/>
</Tab>
</TabPane>
</center>
</BorderPane>
```
`HelloController.java`
```java
package com.zzzmh.javafxdemo;
// 控制层 由于只写界面 懒得写控制层 这里清空代码
public class HelloController {}
```
`Application.java`
```java
public void start(Stage stage) throws IOException {
FXMLLoader fxmlLoader = new FXMLLoader(HelloApplication.class.getResource("hello-view.fxml"));
// 只改了这个窗口大小 800x600
Scene scene = new Scene(fxmlLoader.load(), 800, 600);
stage.setTitle("后台管理");
stage.setScene(scene);
stage.show();
}
```
<br>
顺便插播一下图标如何配置
(这里的图标是指 标题栏+任务栏 不含桌面图标)
关键代码就一行
```java
stage.getIcons().add(new Image(this.getClass().getResourceAsStream("logo.png")));
```
![icon](/api/file/getImage?fileId=64dc6eccda74050014006b30)
<br>
样式写完发现已经累了,功能实现就懒得写了,反之不影响正片,重点是导出jar
![累了](/api/file/getImage?fileId=64dd9b09da74050014006c6d)
<br>
执行看下大致效果
![System](/api/file/getImage?fileId=64dc4d64da74050014006afc)
<br>
<br>
**重点来了!**
**正片开始!**
**导出jar文件**
关键是第一步
新建一个Java文件,main方法里调用 `Application.main(args)`
`Launcher.java`
```java
public class Launcher {
public static void main(String[] args) {
HelloApplication.main(args);
}
}
```
`File` -> `Project Strucuture`
![](/api/file/getImage?fileId=64dc767ada74050014006b33)
`Artifacts` -> `加号` -> `From modules with dependencies...`
![](/api/file/getImage?fileId=64dc772eda74050014006b35)
`Main Class:` -> `Launcher`
![](/api/file/getImage?fileId=64dc7897da74050014006b37)
最后在菜单里 `Bulid` -> `Bulid Artifacts` -> `Rebulid`
操作完成后,在项目目录下就有一个out文件夹
切换到终端
cd到out目录下
先 `java -version` 看下是否是 `openjdk 17`
(如果不是openjdk17 则需要你自行配置一下系统环境变量 略过)
```shell
$ java -version
openjdk version "17.0.7" 2023-04-18
OpenJDK Runtime Environment Temurin-17.0.7+7 (build 17.0.7+7)
```
然后直接执行 `java -jar`
```shell
java -jar JavaFX_Demo_jar/JavaFX-Demo.jar
```
**如果程序窗口正常启动说明第一部分大功告成**
![window](/api/file/getImage?fileId=64dc7974da74050014006b38)
<br>
如果你只是自己随便用用,到这一步就差不多了,环境变量保持在 `openjdk 17`,把jar文件复制出来,需要用的时候 `java -jar xxx.jar`
如果是像我一样还需要考虑发给别人用,或者自己有多系统,没jdk环境或者不能一直持续用jdk17等复杂情况
需要尽量免依赖直接执行的,就需要下列步骤
<br>
**注意:文末已更新最新版的打包方式,下文中的Linux/Windows打包方式不是最优版!最优方案翻到末尾更新!**
<br><br>
**Linux系统**
![linux](/api/file/getImage?fileId=64dd9b92da74050014006c6e)
新建文件夹 `JavaFXDemo`
分别新建4个目录
`jar` (放xxx.jar文件)
`icon` (放icon文件 linux建议用svg/png格式)
`jdk` (放jdk)
`log` (暂时空着 后续会放log)
`JavaFXDemo`根目录新建一个shell文件,内容如下
`linux_start.sh`
```shell
#!/bin/bash
cd /home/zzzmh/Desktop/JavaFXDemo
nohup jdk/bin/java -jar jar/JavaFX-Demo.jar > log/java.log 2>&1 &
```
<br>
最终大致效果如下
```treeview
JavaFXDemo/
|-- jar/
| |-- JavaFX-Demo.jar
|-- icon/
| |-- logo.png
|-- jdk/
| |-- 省略...
|-- log/
| |-- java.log
`-- linux_start.sh
```
<br>
```shell
# 给shell文件赋权
sudo chmod 755 linux_start.sh
# 执行看下效果
bash linux_start.sh
```
<br>
到这一步只要执行shell脚本,就可以实现,无需改变系统环境变量,就可用指定的自带的jdk来执行jar文件
最后一步是桌面新建一个快捷方式,指向这个shell脚本
<br>
桌面快捷方式(linux deepin为例)
在桌面新建一个文件(路径替换你自己的)
`javafx-demo.desktop`
```desktop
[Desktop Entry]
Name=JavaFX-Demo
Comment=JavaFX
Exec=/home/zzzmh/Desktop/JavaFXDemo/linux_start.sh %u
Icon=/home/zzzmh/Desktop/JavaFXDemo/icon/logo.png
Terminal=false
Categories=net
Type=Application
```
桌面就出现了快捷方式图标,双击即可运行jar(shell指定jdk)
![desktop](/api/file/getImage?fileId=64dd9838da74050014006c6c)
PS:Java桌面图形项目启动需要2秒左右是正常现象,我暂时也没找到解决方法。
<br><br>
**Windows系统**
![windows11](/api/file/getImage?fileId=64ddd9bcda74050014006cd0)
这里遇到点波折,我记得以前 `OracleJDK1.8` 自带的 `JavaFX` 是可以一次打包,多系统运行的(也可能是记错了)
但是这次用 `AdoptOpenJDK 17` 居然不行
`Linux` 下打包的比 `Windows` 下小,且拿到Win下运行会报错,看字面意思是缺少 `dll`
所以最后妥协,选择在 `Windows` 下重新编译一次Jar文件
过程就略过了,反正也是 `Idea` 拉代码,等 `Maven` 补依赖,添加 `Artifacts` ,最后 `build`
得到一份 `Windows` 版本的Jar,估计是自带的 `Win` 系统需要的dll
还是按照Linux那个目录去新建和摆文件
```treeview
JavaFXDemo/
|-- jar/
| |-- JavaFX-Demo.jar
|-- icon/
| |-- logo.png
|-- jdk/
| |-- 省略...
|-- log/
| |-- java.log
`-- windows_start.bat
```
有几个不一样
jar必须是刚才在Windows下打包出来的jar
icon必须是ico格式的,需要自己找地方png转ico
jdk必须是Windows版本的 `AdoptOpenJDK 17`
`windows_start.bat`
```bat
@echo off
start jdk\bin\javaw -jar jar\JavaFX-Demo.jar
```
仅测试双击执行Bat正常启动程序
![](/api/file/getImage?fileId=64ddd367da74050014006cc2)
最后补上桌面快捷方式的方法
对 `windows_start.bat` 文件右击 -> 发送桌面快捷发送
桌面得到这样一个快捷方式
![](/api/file/getImage?fileId=64ddd3efda74050014006cc3)
右击重命名可以修改名字
右击属性-> 更换图标 -> 选择 `JavaFX-Demo\icon` 目录下的 `logo.ico` 即可更换图标
![](/api/file/getImage?fileId=64ddd46ada74050014006cc4)
最终效果如下
![](/api/file/getImage?fileId=64ddd479da74050014006cc5)
## END
本文代码的开源地址
[https://gitee.com/zzzmhcn/JavaFX-Demo](https://gitee.com/zzzmhcn/JavaFX-Demo)
暂时先折腾到这里,还留下几个尾巴
1. IDEA有一个 `Artifacts` 是专门导出 `JavaFX` 文件的,目测比我导出jar更科学,但目前测试下,不支持 `AdoptOpenJDK` 以后我再用其他 `OpenJDK` 再试试
2. 目前只测试了 `Linux` 下打包的jar `Windows` 系统不能正常启动,反过来还没测过,目前希望得到一种最优解,即一个jar包3系统都能正常执行
3. jdk包都有290MB+的体积,导致了程序打包后文件过大,目测jre只有30~40MB,之后会再测测看jre作为运行环境是否更科学
总之这次只是探路性质的尝试,后续还是先花时间把JavaFX学好,等再有时间深入研究导出jar的事,折腾出更完美的方案,再回来新开博客推翻这一篇的内容。
![](/api/file/getImage?fileId=64c9c860da74050014005b69)
<br><br>
## 更新 2023.08.17
![](/api/file/getImage?fileId=64dde717da74050014006ce5)
搞了个半吊子我还是浑身难受
google半天,顺便用翻译插件生啃了一会英文文档
[https://openjfx.io/openjfx-docs/#modular](https://openjfx.io/openjfx-docs/#modular)
发现原来官方早就提供了一种最简单的方式
`maven` -> `Plugins` -> `javafx`
`javafx:run` 相当于是 JavaFX Maven 插件创建自定义运行
`javafx:jlink` 相当于是 编译一个含jre运行环境的最小的包并导出压缩包
所以前文中的配置 `Artifacts` 的方案可以废弃了
写完代码以后,直接运行 `maven` 中的 `clean` `javafx:jlink`
![](/api/file/getImage?fileId=64dde825da74050014006ce9)
正常情况下,会在项目下多出一个 `target` 文件夹
里面有4个文件夹 1个压缩包
![](/api/file/getImage?fileId=64dde876da74050014006cea)
4个文件夹就是打包后的完整程序 已含所有依赖
`app.zip` 压缩包就是上述4个文件夹压缩后的最终程序
我这里测试下来压缩包大约只有29MB
把压缩包放到合适位置解压,得到4个文件夹
进入 `app` 下的 `bin` 目录
用终端执行
```shell
./app
```
就执行成功了
![](/api/file/getImage?fileId=64dde92cda74050014006ceb)
经测试,同样无法直接跨系统,也就是说, `win/mac/linux` 需要分别在对应系统下用maven打包,才可以获得对应系统的程序包,这个问题目前我还没找到解决方案
先折腾到这里 有后续再回来更新