
做JavaFX桌面端的同学应该都有同感功能写起来很顺手一到界面层就开始头疼。默认那套Button、TextField、TableView摆出来总有一种上个时代桌面软件的既视感。FontAwesomeFX和JFoenix这个组合算是JavaFX社区里最主流的换脸方案——一个负责图标资源一个负责Material Design风格的控件外观。这篇是这个系列的第十五篇专门把两个库的集成细节完整过一遍依赖怎么引、版本怎么配、图标为什么经常不显示、怎么和JFoenix控件组合出日常够用的界面。无论你是刚接触JavaFX的新手还是已经写过几个桌面项目想优化观感的开发者这篇都能直接对照着用。1. 先把场景说清楚你什么时候才需要FontAwesomeFX和JFoenix1.1 原生JavaFX的界面短板JavaFX自带的modena样式其实不算丑但也仅仅停留在不算丑。它最大的问题不是单个控件难看而是整套界面缺乏设计语言按钮就是一個灰底圆角矩形输入框就是一个白底边框窗口标题栏还是操作系统原生样式和内部的Material风格内容放在一起很割裂。更麻烦的是图标。原生JavaFX没有内置图标库你想在按钮上加一个搜索放大镜、在菜单项上加一个用户头像只能自己找图片资源。图标一旦多起来图片管理、尺寸适配、高DPI下的模糊问题全部冒出来。我见过不少项目因为懒得分辨率直接把小尺寸PNG拉大结果在2K屏上糊成一团。1.2 FontAwesomeFX和JFoenix的分工边界这两个库经常被放在一起提但它们的职责完全不同搞清楚这一点你才能在设计方案时不犯糊涂。FontAwesomeFX本质是一个字体工具库。它以字体文件为载体把FontAwesome这套经典图标做成了JavaFX可以直接使用的节点和样式。因为在JavaFX里字体本质是Text节点所以图标天然支持任意颜色、任意字号、任意缩放放大不糊换色很方便这比图片方案舒服太多。JFoenix则是把Google Material Design的设计规范移植到了JavaFX上。它提供了一整套替换控件JFXButton、JFXTextField、JFXPasswordField、JFXComboBox等等外观是扁平化的Material风格部分控件自带水波纹反馈。它还附带JFXDecorator这样的窗口装饰器可以把标题栏也做成应用内样式。简单说FontAwesomeFX解决图标从哪来JFoenix解决控件怎么长得像现代应用。1.3 这套组合适合和不适合的项目类型先说适合的。内部管理系统、后台操作台、数据看板、工具类软件这些场景对界面一致性要求高但又不会有人拿放大镜挑剔你每个动画是否符合Google规范。拿这俩库做出来的应用视觉完成度会明显高于原生控件而且开发成本很低。再说不太适合的。如果你的产品是面向大众的跨平台客户端且需要严格跟随Windows或macOS的原生交互习惯Material风格反而不适合——用户会觉得它像网页不像桌面软件。另外如果项目有比较重的无障碍需求比如屏幕阅读器适配JFoenix有些控件的辅助功能支持并不完善需要额外处理。遇到这种情况不如只引入图标库控件老老实实用原生的。2. 依赖安装与版本匹配这一步错了后面全是坑2.1 Maven坐标和Gradle写法两个库在中央仓库都能直接拉到。最经典的一组坐标是dependency groupIdde.jensd/groupId artifactIdfontawesomefx/artifactId version8.9.1/version /dependency dependency groupIdcom.jfoenix/groupId artifactIdjfoenix/artifactId version9.0.10/version /dependencyGradle项目对应写法implementation de.jensd:fontawesomefx:8.9.1 implementation com.jfoenix:jfoenix:9.0.10这里要提醒一点FontAwesomeFX后来把原来的大包拆分成了多个独立构件比如fontawesomefx-fontawesome、fontawesomefx-materialdesignfont、fontawesomefx-weathericons。如果你只需要FontAwesome这一套图标用拆分后的构件更干净如果懒得管理多个依赖老版本的fontawesomefx:8.9.1一个包包含多套字体也能用。我用的是拆分后的新构件class名和包路径和老版本一致都是de.jensd.fx.glyphs.fontawesome开头切换成本很低。2.2 JDK8 / JDK11 / JDK17的版本对照版本匹配是集成这两个库最容易踩的第一个坑。JFoenix和FontAwesomeFX的老版本都是针对Java 8编译的你在JDK 11以上的项目里直接引入老版本轻则警告重则因为JavaFX模块化之后不再打包在JDK里而直接跑不起来。我实际验证过的兼容组合整理如下你的JDKFontAwesomeFX推荐JFoenix推荐备注JDK 8 JavaFX 88.9.18.0.10最经典的组合网上教程大半是这套JDK 11 JavaFX 11fontawesomefx-fontawesome 4.7.0-9.1.29.0.10需要自行引入JavaFX依赖JDK 17 JavaFX 17fontawesomefx-fontawesome 4.7.0-9.1.29.0.10实测能用个别控件注意CSS兼容JFoenix的9.0.10版本比较特殊它名义上支持Java 9到10但社区大量项目在Java 11和17上跑都没问题。如果你在模块化项目里遇到IllegalAccessError或者module not found之类的问题通常是JFoenix访问了JavaFX内部类需要在编译和运行参数里补--add-opens我把这段写进构建脚本里就稳了--add-opensjavafx.graphics/com.sun.javafx.csscom.jfoenix不带module-info的非模块化项目一般遇不到这个可以先跳过。2.3 字体资源加载图标白屏的常见根因很多人引入依赖后代码写完了运行起来界面上的图标却是个空白方块第一反应以为是库坏了。其实FontAwesomeFX的图标本质是字体代码里如果没有提前加载字体文件图标就渲染不出来。FontAwesomeIconView在实例化时一般会自动加载自带字体但如果你是用CSS的方式给普通Label或Text设置图标字体就必须手动加载。稳妥起见我习惯在Application.start最前面统一注册一次字体Font.loadFont(FontAwesomeIconView.class.getResource(/fonts/fontawesome-webfont.ttf).toExternalForm(), 10);JFoenix这边对应的坑是漏加样式表。JFXButton、JFXTextField这些控件设计是依赖JFoenix自己的CSS的只引入jar包不加载样式表控件会退化成一堆看不出Material样式的普通组件。正确做法是在构建Scene后立即加载两份CSSscene.getStylesheets().add(JFXDecorator.class.getResource(/css/jfoenix-fonts.css).toExternalForm()); scene.getStylesheets().add(JFXDecorator.class.getResource(/css/jfoenix-design.css).toExternalForm());3. FontAwesomeFX实战把图标用出字体的灵活度3.1 FontAwesomeIconView基础用法FontAwesomeIconView是FontAwesomeFX最核心的类它继承自Text节点所以创建图标就像创建一个文本节点一样简单FontAwesomeIconView icon new FontAwesomeIconView(FontAwesomeIcon.SEARCH); icon.setSize(16); icon.setFill(Color.valueOf(#666666));FontAwesomeIcon是个枚举里面的常量名基本就是英文语义比如USER、LOCK、PLUS、TRASH写起来几乎不需要查文档。setSize可以直接传数字也可以传带单位的字符串如18px传字符串的好处是变量化之后方便做皮肤系统。因为图标本质是文本所以它天然继承字体的一系列特性清晰度不受缩放影响、颜色可以随时改、可以做透明度动画。这些能力用图片实现要么很麻烦要么效果很差这也是我坚持用字体图标的原因。3.2 动态切换图标、颜色、尺寸的三种手段第一种是最直接的方式代码里换枚举icon.setIcon(FontAwesomeIcon.SEARCH); // 运行到某个时机时换成编辑图标 icon.setIcon(FontAwesomeIcon.EDIT);第二种是走CSS。给图标节点添加styleClass然后在样式表里控制.toolbar-icon { -fx-fill: #455A64; -fx-font-size: 20px; } .toolbar-icon:hover { -fx-fill: #2196F3; }第三种适合做状态联动。比如数据加载中显示转圈图标、加载完成显示对勾这时候我给图标节点动态增删不同的styleClass配合CSS里定义的-fx-fill比在代码里写一堆if else判断颜色要整洁得多。实际开发里我通常三种混用动画状态用代码切换icon静态主题色用CSS控制这样改主题的时候不用动Java代码。3.3 图标塞进Button、TextField左侧、Menu的代码示范图标最常见的落地场景就是这几个控件。先看按钮JFXButton searchBtn new JFXButton(查询); FontAwesomeIconView searchIcon new FontAwesomeIconView(FontAwesomeIcon.SEARCH); searchIcon.setSize(16); searchBtn.setGraphic(searchIcon);按钮上带图标的意义在于降低用户的理解成本一个打印机图标比一段打印报表文字更直观。但注意icon尺寸不要超过按钮的行高我一般控制在16到18px之间太大的话整个按钮会显得臃肿。TextField左侧放图标是登录界面和搜索框最常见的需求。TextField没有原生的left graphic属性我用StackPane叠层的方式解决StackPane wrap new StackPane(); FontAwesomeIconView userIcon new FontAwesomeIconView(FontAwesomeIcon.USER); userIcon.setSize(16); userIcon.setFill(Color.valueOf(#9E9E9E)); userIcon.setMouseTransparent(true); JFXTextField userField new JFXTextField(); userField.setPromptText(用户名); userField.setPrefWidth(280); wrap.getChildren().addAll(userField, userIcon); StackPane.setAlignment(userIcon, Pos.CENTER_LEFT); StackPane.setMargin(userIcon, new Insets(0, 0, 0, 12));关键点在setMouseTransparent(true)这行。图标只是装饰不能挡住TextField的点击事件尤其是当用户点击到图标区域时焦点必须落到输入框里。忘了这一行实际点击体验会很糟糕。Menu菜单项加图标就更简单了MenuItem menuItem new MenuItem(复制); FontAwesomeIconView copyIcon new FontAwesomeIconView(FontAwesomeIcon.COPY); copyIcon.setSize(14); menuItem.setGraphic(copyIcon);3.4 其他字体集MaterialDesignIcon、WeatherIcon等FontAwesomeFX强大的地方在于它不只有FontAwesome一套字体。如果你需要更现代一点的图标风格官方拆分的构件里还有Material Design Icon、Material Icons、Weather Icons、Octicons等选择。引入fontawesomefx-materialdesignfont后用法几乎一模一样MaterialDesignIconView mdIcon new MaterialDesignIconView(MaterialDesignIcon.ACCOUNT); mdIcon.setSize(18);Material Design Icon的图标数量比FontAwesome多不少方向也更偏现代感。我的习惯是工具类界面用FontAwesome追求精致观感或者需要大量布尔类型、数据分析类图标时用MaterialDesignIcon。两套字体可以在同一个界面共存互不干扰视觉上也没有冲突感。4. JFoenix接入Material Design控件和原生控件的混用准则4.1 必备的CSS样式表与JFXDecoratorJFoenix接入的核心动作就两个加载样式表、替换控件。样式表前面已经写过这里重点说JFXDecorator。JFXDecorator是JFoenix最亮眼的功能之一它把窗口的标题栏从操作系统原生样式替换成应用内自定义样式并自带最小化、最大化、关闭三个按钮标题栏背景也可以自由设置颜色JFXDecorator decorator new JFXDecorator(stage, rootPane); Scene scene new Scene(decorator, 1000, 640);注意构造顺序JFXDecorator接收的第二个参数是界面的根节点Scene包裹的是decorator而不是rootPane。很多第一次用的同学会在这里写反结果发现自定义标题栏没生效整个界面被顶出了窗口显示区域。设置完标题栏后别忘了把标题栏区域的鼠标拖动事件接上。JFXDecorator默认支持拖拽移动窗口但如果你在标题栏位置放了自己的节点可能需要调整一下覆盖层级。关闭按钮的默认行为是关闭窗口并退出应用如果你想像IDE那样在关闭时弹确认框可以覆写decorator.setOnCloseButtonAction(() - { // 自定义关闭逻辑 });4.2 常用控件替换清单JFoenix不是把所有原生控件都替换掉就完事了它是按需替换的。我把实际项目里常用的替换关系整理成了一张表场景原生控件JFoenix替换控件普通按钮ButtonJFXButton文本输入TextFieldJFXTextField密码输入PasswordFieldJFXPasswordField复选框CheckBoxJFXCheckBox下拉框ComboBoxJFXComboBox列表ListViewJFXListView滑块SliderJFXSlider日期选择DatePickerJFXDatePicker进度条ProgressBarJFXProgressBar抽屉菜单无JFXDrawer替换不是机械的你要理解替换背后的原因。比如JFXListView的单元格支持Material风格的水波纹反馈点击时的视觉反馈比原生方案强很多适合做设置类页面JFXDrawer则是在无原生方案的情况下提供抽屉导航适合做侧边栏布局。这里有个实际操作原则能用JFoenix控件的地方尽量统一用不要一半原生一半JFoenix混着放。混用会导致视觉规格不一致比如原生的TextField高度和JFXTextField高度不一样并排摆放时对不齐修复起来比直接全换还要费时间。4.3 自定义主题色覆盖JFoenix默认的蓝色主题用久了很容易腻。它提供了CSS变量机制定义在.root选择器上你可以用一套主题色贯穿所有控件.root { -jfx-primary-color: #00796B; -jfx-secondary-color: #009688; -jfx-rippler-color: rgba(0, 150, 136, 0.2); }设置之后JFXButton的主色调、JFXTextField的聚焦下边框颜色、水波纹颜色都会跟着变。如果要让某个按钮呈现实心高亮效果在CSS里控制按钮类型和背景色.jfx-button { -jfx-button-type: RAISED; -fx-background-color: -jfx-primary-color; } .jfx-button .text { -fx-fill: white; }这里有个细节JFXButton的文本是Text节点默认颜色继承不是特别直观想让它变成白色需要显式设置.text的-fx-fill直接对.jfx-button设置-fx-text-fill经常不生效。这种小坑不做几次不会意识到。5. 完整示例FontAwesomeFX JFoenix 做一个带图标输入框的登录界面5.1 界面拆解与布局思路这部分给一个可以直接跑通的完整示例登录界面。它麻雀虽小但把前面讲到的关键点全部串了起来——JFXDecorator窗口装饰、FontAwesome图标叠层输入框、Material按钮、统一主题色。布局思路是外层VBox垂直排列中间放用户名输入框、密码输入框、登录按钮三个元素每个输入框用StackPane把图标叠在左侧按钮用RAISED类型撑出Material实心效果。最后整个VBox包进JFXDecorator让窗口标题栏也变成应用内的样式。5.2 完整代码import de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon; import de.jensd.fx.glyphs.fontawesome.FontAwesomeIconView; import com.jfoenix.controls.JFXButton; import com.jfoenix.controls.JFXDecorator; import com.jfoenix.controls.JFXPasswordField; import com.jfoenix.controls.JFXTextField; import javafx.application.Application; import javafx.geometry.Insets; import javafx.geometry.Pos; import javafx.scene.Scene; import javafx.scene.layout.StackPane; import javafx.scene.layout.VBox; import javafx.scene.paint.Color; import javafx.stage.Stage; public class LoginDemo extends Application { Override public void start(Stage primaryStage) { // 用户名输入框 用户图标 JFXTextField userField new JFXTextField(); userField.setPromptText(用户名); userField.setPrefWidth(280); FontAwesomeIconView userIcon new FontAwesomeIconView(FontAwesomeIcon.USER); userIcon.setSize(16); userIcon.setFill(Color.valueOf(#9E9E9E)); userIcon.setMouseTransparent(true); StackPane userBox new StackPane(userField, userIcon); StackPane.setAlignment(userIcon, Pos.CENTER_LEFT); StackPane.setMargin(userIcon, new Insets(0, 0, 0, 12)); // 密码输入框 锁图标 JFXPasswordField pwdField new JFXPasswordField(); pwdField.setPromptText(密码); pwdField.setPrefWidth(280); FontAwesomeIconView pwdIcon new FontAwesomeIconView(FontAwesomeIcon.LOCK); pwdIcon.setSize(16); pwdIcon.setFill(Color.valueOf(#9E9E9E)); pwdIcon.setMouseTransparent(true); StackPane pwdBox new StackPane(pwdField, pwdIcon); StackPane.setAlignment(pwdIcon, Pos.CENTER_LEFT); StackPane.setMargin(pwdIcon, new Insets(0, 0, 0, 12)); // 登录按钮 JFXButton loginBtn new JFXButton(登 录); loginBtn.setButtonType(JFXButton.ButtonType.RAISED); loginBtn.setPrefWidth(280); loginBtn.setPrefHeight(40); // 布局 VBox content new VBox(24, userBox, pwdBox, loginBtn); content.setAlignment(Pos.CENTER); content.setPadding(new Insets(48)); // 自定义窗口标题栏 JFXDecorator decorator new JFXDecorator(primaryStage, content); Scene scene new Scene(decorator, 420, 360); // 加载JFoenix样式 scene.getStylesheets().add( JFXDecorator.class.getResource(/css/jfoenix-design.css).toExternalForm()); scene.getStylesheets().add( JFXDecorator.class.getResource(/css/jfoenix-fonts.css).toExternalForm()); primaryStage.setScene(scene); primaryStage.setTitle(登录); primaryStage.show(); } public static void main(String[] args) { launch(args); } }这段代码里我特意用StackPane而不是HBox来做图标叠层因为StackPane允许图标浮在输入框上面视觉上图标在框内而实际不参与输入框的布局计算调整位置也更方便。你如果想统一控制图标与输入框的间距可以在StackPane.setMargin里改left的值一般在10到16px之间效果比较好。5.3 运行效果与可扩展方向运行起来你会看到窗口标题栏变成应用内的深灰色条三个控件垂直居中输入框左侧有灰色图标点击输入框时底框颜色会变成主题色登录按钮是实心蓝绿底白字。这个效果和原生JavaFX对比观感完全不在一个级别。这套界面可以继续扩展的方向很多在VBox里加一个记住我的JFXCheckBox把登录按钮换成带Loading状态的图标切换用JFXSnackbar做表单校验提示甚至可以直接替换成JFXDrawer 右侧内容区做完整的后台样板。得益于这两个库的配合扩展成本都不高。6. 实际项目里踩过的坑和排查套路6.1 图标不显示的排查链路图标不显示是这两个库加进来之后最高频的问题。我自己的排查顺序基本固定按这个顺序走完绝大多数问题都能定位。第一步看字体加载。检查代码里有没有Font.loadFont如果图标是出现在FontAwesomeIconView节点里确认jar包里的/fonts/fontawesome-webfont.ttf确实存在且路径正确。这一步可以临时在代码里System.out.println(FontAwesomeIconView.class.getResource(/fonts/fontawesome-webfont.ttf))直接打印路径验证。第二步看CSS覆盖。某个图标放在一堆CSS样式很重的容器里可能被全局的text-fill或者-fx-fill刷成了和背景相同的颜色。在Scene上临时加一条* { -fx-fill: red; }如果图标变红了说明是样式冲突再去定位具体覆盖来源。第三步看枚举名。老版本FontAwesome图标枚举里有些图标在新版本改名了编译期不会报错但运行时会渲染成空白或方块。遇到疑似改名问题直接把图标值临时换成FontAwesomeIcon.INFO这种稳定存在的图标能显示就是枚举名的问题。第四步看打包。本地开发图标正常打出jar包后图标消失90%是字体资源没被打包工具包含进去。用maven-shade或jpackage时检查过滤配置别把*.ttf文件排除掉了。6.2 CSS优先级和样式污染问题JFoenix的jfoenix-design.css是一个全量的样式表加载之后会对全局控件产生作用。它加载在自定义样式表之前所以你的自定义样式可以覆盖它但如果你把JFoenix样式表加在自定义样式之后就会发现很多设置改了无效因为JFoenix的规则优先级更高。我建议把加载顺序固定为先加载jfoenix-fonts.css再加载jfoenix-design.css最后加载自己的应用样式表。这样应用样式拥有最高的源码顺序优先级覆盖行为可预期。还有一个容易忽视的地方jfoenix-design.css对原生Button、TextField也定义了样式。有时候你只想用JFoenix的某几个控件界面里其他原生控件却被影响得出乎意料。如果确实要做局部控制不要整个Scene加载jfoenix-design.css改成只对JFoenix控件所在的容器节点加载或者给容器加独立styleClass在CSS里用后代选择器限定生效范围。6.3 打包后字体丢失的问题我早期用maven-shade-plugin打包遇到过两次图标消失的问题。排查后发现都是shade插件在合并资源时把META-INF和字体文件过滤掉了。现在我的做法是在shade配置的filters里显式排除掉不需要的签名文件和杂项同时保证resources包含fonts目录resources resource directorysrc/main/resources/directory includes include**/*.ttf/include include**/*.css/include /includes /resource /resources如果用的是jlink或jpackage要确认--add-resources把字体目录带上否则生成的自包含镜像里缺了字体文件界面看起来就是一堆空白的方块按钮。测打包环境有没有带全资源最简单粗暴的办法是用jar tf命令查看最终产物里有没有fonts/fontawesome-webfont.ttf比上真机验证快很多。最后再分享一个实用技巧。我习惯把项目里用到的所有图标封装成一个静态工具类返回创建好的FontAwesomeIconView统一管理尺寸和默认颜色。这样整个项目里不会到处都是magic number也不会在某个角落忘写setSize导致图标大小不统一。这套组合我用下来最大的体会是版本锁定比什么都重要确认JDK和JavaFX版本后就别再动依赖版本集成阶段最怕的就是一边排查问题一边顺手升级库。