ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

JavaFX整合FontAwesomeFX与JFoenix:字体冲突与CSS样式问题全解析

JavaFX整合FontAwesomeFX与JFoenix:字体冲突与CSS样式问题全解析 开始动手之前先聊个很有意思的现象做JavaFX桌面应用的人很少有没听过FontAwesomeFX和JFoenix这两个库的但真正能把这俩“无缝捏合”在一起的人却少得多。很多项目里装完这两个依赖要么图标显示不出来要么按钮字体被覆盖成方块要么一跑起来控制台全是警告最后只能在网上翻各种“玄学”解决办法。我自己的经验是问题多半不是出在“用没用对”而是出在“这两个库底层对字体和CSS样式的处理机制完全不一样”。这篇我就把集成过程、冲突根源、解决思路一次讲透从环境配置、Maven依赖到实测渲染效果尽量做到你照着做就能跑通。这篇文章适合刚接触JavaFX但想要提升界面质感的人也适合已经在项目里踩过坑、想彻底搞明白原理的朋友。1. 内容整体设计与思路拆解1.1 生态背景为什么偏偏是这两个库JavaFX本身提供的原生控件说实话功能上是完整的但视觉效果停留在“业务系统工具”那个年代。哪怕你费劲调了半天CSS默认的Button、TextField、TableView还是带着一股浓重的“演示Demo”味。所以很多项目会引入第三方组件库来做视觉增强。JFoenix是目前JavaFX生态里最接近Material Design风格的控件库提供了JFXButton、JFXTextField、JFXDialog等一系列带浮动提示、波纹动画、圆角效果的控件。而FontAwesomeFX解决的是另一个问题图标。它把FontAwesome字体里那上千个矢量图标封装成了一个个枚举常量在Java代码里通过FontAwesomeIcon.XXX就能直接引用再也不用为了一个小箭头去PS里切图。这两个库一个管控件形态一个管视觉元素理论上搭配起来非常自然。但实际集成时因为它们都重度依赖自定义字体和CSS样式表很容易互相干扰。比如JFoenix控件内部会加载Roboto字体而FontAwesomeFX会把FontAwesome字体注册进同一套JavaFX字体体系一旦初始化顺序不对某个字体就会被覆盖或丢失表现出来的就是控件文字消失、图标变成空心方块。1.2 集成方案的选型逻辑集成这两个库网上能搜到的大概有三条路线直接下载JAR包扔进项目classpath。优点是没有网络依赖缺点是版本管理混乱、JAR包容易丢换台电脑就傻眼。用Maven或Gradle引入依赖。这是目前最推荐的方式版本清晰、可复现也方便后续升级。手动源码编译。适用于需要魔改源码的场景但大多数人不值得这么做。我选的是Maven路线版本上也做了固定JavaFX 11.0.2配合JFoenix 9.0.10和FontAwesomeFX 8.2。这里特别注意一点FontAwesomeFX官方的最新版还停留在8.x它内部对JavaFX 9以上的模块化体系兼容性一般所以要用--add-exports或者--add-opens之类的手段来绕过模块权限限制。这些细节一会儿在实操部分我会专门讲到。1.3 核心风险点预先认知集成过程中最典型的三个坑先提前给你打个预防针JFoenix对JDK版本非常敏感JDK 9以上容易出现IllegalAccessError必须在VM参数里加一堆--add-opens。FontAwesomeFX默认字体尺寸是12像素在JavaFX的控件里显示出来会非常小不调整的话看起来像没加载成功。两个库都会向Application.setUserAgentStylesheet()注册默认样式如果加载顺序不对后加载的会把先加载的全局样式覆盖掉最终界面上所有控件都变回默认的Modena风格。只要对这三个风险点有意识后面就好办很多。2. 核心细节解析与实操要点2.1 环境准备与依赖引入细节先把基础环境列一下我用的组合是JDK 11不要用太高版本JDK 17之后JFoenix有一些兼容性大坑JavaFX 11.0.2Maven 3.6IDEA 2020.3以上低版本对JavaFX的支持不够友好在pom.xml里需要加以下依赖dependency groupIdorg.openjfx/groupId artifactIdjavafx-controls/artifactId version11.0.2/version /dependency dependency groupIdcom.jfoenix/groupId artifactIdjfoenix/artifactId version9.0.10/version /dependency dependency groupIdde.jensd/groupId artifactIdfontawesomefx/artifactId version8.2/version /dependency这里有个关键提醒JFoenix 9.0.10这个版本内部依赖了JavaFX的许多内部包如果你不配置--add-exports运行时大概率会抛Module java.base does not export com.sun.glass.ui to unnamed module这类错误。IDEA里要打开Run Configuration在VM options里加一长串JVM参数我用的是一组经过实测的配置后面会贴出来。2.2 FontAwesomeFX的初始化机制很多人的误区是把FontAwesomeFX的依赖加好然后直接在FXML里面写IconAwesome styleClass.../以为就能显示图标。但事实上FontAwesomeFX需要在程序启动时主动初始化字体否则图标文本对应的Unicode字符根本加载不出来。FontAwesomeFX的核心思路是字体文件TTF里定义了一堆Unicode字符每个字符对应一个图标。代码里你使用FontAwesomeIcon.ANDROID本质上就是拿到一个Glyph对象然后把它渲染成一个Text节点放进布局。一个最简洁的启动方式是这样import de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon; import de.jensd.fx.glyphs.fontawesome.FontAwesomeIconView; public class IconDemo extends Application { Override public void start(Stage stage) { VBox root new VBox(); FontAwesomeIconView icon new FontAwesomeIconView(FontAwesomeIcon.ANDROID); icon.setSize(40.0); root.getChildren().add(icon); Scene scene new Scene(root, 300, 200); stage.setScene(scene); stage.show(); } }这里有两个设置几乎必用setSize()控制图标大小setFill()控制颜色。这两个方法内部实际上是通过CSS变量去控制字体大小和文字颜色的所以如果你在后面用CSS覆盖了样式类图标大小很可能会被重置回默认值这点需要格外小心。2.3 JFoenix的字体依赖与样式结构JFoenix和FontAwesomeFX有一点本质不同JFoenix不是图标库它是整套控件库提供的是一整套Material Design风格控件和配套的CSS皮肤。它内部默认使用Roboto字体并且会把Roboto Medium等字体打包进JAR包里。启动JFoenix应用最标准的做法是在Application.start()里先加载它的样式表Override public void start(Stage stage) { // 加载JFoenix全局样式 Application.setUserAgentStylesheet(getClass().getResource(/css/jfoenix-design.css).toExternalForm()); // 或者使用 // new JFXDecorator(stage, root); ... }这里有一个非常容易忽略的点JFoenix控件的样式是基于它的jfoenix-design.css来的但如果你在这个样式表加载之前就创建了控件控件会先用默认的JavaFX样式渲染一遍然后等你加载样式表时再整体刷新一遍。这个过程会导致窗口打开时能看到界面“闪了一下”从默认样式切换到Material样式。为了避免这个问题最好的方式是先在start()方法里就加载样式然后再构建界面控件的层级最后才创建Scene并显示。2.4 字体继承与加载的底层逻辑为什么这两个库放在一起特别容易出字体问题这得从JavaFX的字体渲染机制讲起。在JavaFX中Font.loadFont()会从URL加载字体到本地的字体缓存中这个缓存是全局的后续所有使用该字体名称的Text或控件都会生效。但如果字体名称相同但来源不同后加载的会覆盖先加载的导致字形变化。FontAwesomeFX加载的是“FontAwesome”字体而JFoenix加载的是“Roboto”字体两者看起来井水不犯河水。但问题出在JFoenix的很多控件中比如JFXButton它的默认文本样式指定了-fx-font-family: Roboto而JFXTextField又指定了另一个字体。这些字体名称在JFoenix内部经常会以“Roboto Medium”、“Roboto Regular”的形式出现一旦系统里没有这些字体JavaFX就会退回默认字体视觉上跟普通JavaFX控件没什么区别。所以如果只集成JFoenix而不加载它JAR包里的字体文件路径通常是/font/roboto/Roboto-Regular.ttf等整个界面就会显得不够精致。在集成时我一般会手动加载一套字体Font.loadFont(getClass().getResourceAsStream(/font/roboto/Roboto-Regular.ttf), 14); Font.loadFont(getClass().getResourceAsStream(/font/roboto/Roboto-Medium.ttf), 14); Font.loadFont(getClass().getResourceAsStream(/font/roboto/Roboto-Bold.ttf), 14); Font.loadFont(getClass().getResourceAsStream(/font/roboto/Roboto-Light.ttf), 14); // 再加载FontAwesome Font.loadFont(getClass().getResourceAsStream(/font/fontawesome-webfont.ttf), 14);这样做的目的是提前把字体文件注册进JavaFX字体缓存避免控件在创建时找不到字体。这个操作一般不会报错但如果没有提前加载控件渲染出的字体就是你系统默认字体和Material风格差距很大。3. 实操过程与核心环节实现3.1 创建一个基础的JavaFX项目骨架我习惯用Maven来管理项目。创建一个标准的JavaFX项目需要确保module-info.java里面把这个模块需要的依赖都声明好。如果你用的是非模块化项目就没那么麻烦直接以普通应用方式运行即可。但既然到了第十几篇的集成步骤我更推荐一开始就养成模块化习惯。在module-info.java中我写了这样一段module com.example.javafx { requires javafx.controls; requires javafx.fxml; requires jfoenix; requires fontawesomefx; exports com.example.javafx; }有这么几个容易踩的坑JFoenix依赖了java.xml、java.desktop等模块如果module-info.java里不显式加requires java.desktop;运行时就会报ModuleNotFoundException。我当时查了很久才发现是这个原因所以在这里提前提醒一下。3.2 IDEA里的运行配置与VM参数如果你用的IDEA在配置Run Configuration时Main类选择你启动类然后在VM options里填上这样一段参数以JDK 11为例--add-exportsjavafx.graphics/com.sun.javafx.csscom.jfoenix --add-exportsjavafx.graphics/com.sun.javafx.scene.layoutcom.jfoenix --add-exportsjavafx.graphics/com.sun.javafx.scene.textcom.jfoenix --add-exportsjavafx.base/com.sun.javafx.eventcom.jfoenix --add-exportsjavafx.base/com.sun.javafx.loggingALL-UNNAMED --add-opensjavafx.graphics/com.sun.javafx.csscom.jfoenix --add-opensjavafx.graphics/com.sun.javafx.scene.textcom.jfoenix如果你不使用模块化项目这些参数确实不一定全部都需要但JFoenix在非模块化项目里同样会触发sun.*和com.sun.*的访问异常所以加上这些参数有备无患。这里有个值得注意的点--add-opens的参数目标是com.jfoenix意味着只有JFoenix模块才能反射访问这些内部包其他库想用这种方式来访问是不行的也就起到了模块隔离的作用。3.3 控件集成JFXButton与FontAwesome图标的组合集成示例场景做一个侧边栏菜单每个菜单项是一个JFXButton左边有一个FontAwesome图标右边是菜单名称。首先是Java代码层面import com.jfoenix.controls.JFXButton; import de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon; import de.jensd.fx.glyphs.fontawesome.FontAwesomeIconView; public void addNavItem(VBox menuBox, String title, FontAwesomeIcon iconEnum) { JFXButton btn new JFXButton(); btn.setText(title); btn.setPrefWidth(180.0); btn.setPrefHeight(40.0); FontAwesomeIconView iconView new FontAwesomeIconView(iconEnum); iconView.setSize(20.0); iconView.setFill(Color.WHITE); btn.setGraphic(iconView); menuBox.getChildren().add(btn); }这样写完之后如果直接运行你大概率会遇到一个问题按钮上的文字和图标大小比例不协调或者图标被按钮的文字挤到一边去。原因在于JFXButton默认的content-display是LEFT而且它的内部有个固定的graphic-text-gap需要通过CSS去调整。我通常会配套一个CSS类.nav-menu-btn { -fx-background-color: transparent; -fx-background-radius: 0; -fx-text-fill: WHITE; -fx-font-size: 14px; -fx-alignment: CENTER_LEFT; -fx-graphic-text-gap: 12; -fx-padding: 0 0 0 20; -fx-cursor: HAND; } .nav-menu-btn:hover { -fx-background-color: #3A3F5C; }关键是-fx-graphic-text-gap和-fx-padding这两个属性它们直接影响图标的间距和文字的缩进。如果这里不设置图标会贴紧控件左边框看起来很局促。3.4 样式冲突与级联顺序的解决接下来要处理最核心的冲突问题。当FontAwesomeFX的FontAwesomeIconView和JFoenix的JFXButton同时出现时按钮的CSS样式会作用于FontAwesomeIconView这个子节点吗答案是不一定。原因在于FontAwesomeIconView继承的是Text而Text在JavaFX CSS体系里继承的样式和Labeled系列控件不同。JFXButton本身会设置-fx-text-fill属性但这个属性只会传递给按钮自身的文本节点不会传递给Graphic节点。所以如果图标没有显式设置颜色它会继承场景的默认文本颜色而不会自动变成按钮的文本颜色。这就是为什么很多人在深色背景的按钮上用图标时图标是黑色的——因为默认文本颜色是黑色。解决办法也很简单在设置Graphic时给它显式指定颜色iconView.setFill(Color.WHITE);或者在CSS里为所有FontAwesomeIconView统一设置颜色.glyph-icon { -fx-fill: WHITE; }把这个样式类加到图标上iconView.getStyleClass().add(glyph-icon);这样无论按钮的文本颜色怎么变图标颜色始终跟你的设计稿保持一致。3.5 区分JFXButton的ButtonType与样式覆盖JFoenix的JFXButton有一个很特殊的机制就是它的ButtonType枚举控制了按钮的默认样式风格ButtonType.RAISED带阴影的浮动按钮ButtonType.FLAT扁平透明按钮ButtonType.TRANSITION带像素化过渡效果的按钮实际不常用不设置时默认是FLAT。这个细节很多人不知道结果明明设置了-fx-background-color却不起作用因为在FLAT模式下JFoenix默认样式会把背景设置成透明并且优先级比用户在CSS中设置的更高。要规避这个坑最简单的方式是在创建按钮时初始化ButtonTypeJFXButton btn new JFXButton(title); btn.setButtonType(JFXButton.ButtonType.RAISED);这样它才会走“带背景和阴影”的样式分支你自己写的背景色才能正常渲染。这句话我要重点强调很多网上教程里都没有提到ButtonType这个东西导致不少人拿着样式表反复调却一直没有效果最后误以为是类名写错了。3.6 完整的集成代码参考把上面这些组件组合到一起写一个最简单的Demo你可以直接拷贝运行public class DemoApplication extends Application { Override public void start(Stage primaryStage) { // 注意这里要提前加载字体避免字体缺失 loadFonts(); // 设置JFoenix全局样式 Application.setUserAgentStylesheet( getClass().getResource(/css/jfoenix-design.css).toExternalForm() ); JFXButton btnDashboard new JFXButton(仪表盘); btnDashboard.setButtonType(JFXButton.ButtonType.RAISED); btnDashboard.setPrefWidth(200); btnDashboard.setPrefHeight(45); FontAwesomeIconView icon new FontAwesomeIconView(FontAwesomeIcon.DASHBOARD); icon.setSize(18.0); icon.setFill(Color.WHITE); btnDashboard.setGraphic(icon); btnDashboard.setStyle(-fx-background-color: #3F51B5; -fx-text-fill: white;); StackPane root new StackPane(btnDashboard); Scene scene new Scene(root, 400, 300); primaryStage.setTitle(FontAwesomeFX JFoenix Demo); primaryStage.setScene(scene); primaryStage.show(); } private void loadFonts() { Font.loadFont(getClass().getResourceAsStream(/font/roboto/Roboto-Regular.ttf), 14); Font.loadFont(getClass().getResourceAsStream(/font/fontawesome-webfont.ttf), 14); } public static void main(String[] args) { launch(args); } }跑起来之后如果顺利你会看到一个蓝底白字、带图标且带阴影的Material风格按钮。如果没显示图标先排查刚才提到的字体加载和样式覆盖问题。4. 常见问题与排查技巧实录4.1 图标显示成方块或完全空白这个现象在我看来是集成时最典型的问题原因一般有三类FontAwesome字体没有加载到JavaFX字体缓存中。解决办法就是调用Font.loadFont()提前加载且要确认TTF文件路径正确。图标的Text节点大小设置不当。如果设置了setSize(10)在很多高清屏上可能小到看不清尤其是缩放比例大的Windows机器建议至少设置20。控件被JFoenix的样式覆盖了字体族。比如JFXButton的默认-fx-font-family是Roboto如果你把FontAwesome图标当作按钮的文本而不是Graphic那它自然显示的是Roboto字体而不是FontAwesome字体就会出现方块。解决办法是永远用setGraphic()而不是用文本节点传图标。4.2 控件没有Material风格很多人把JFoenix集成了之后跑出来的界面跟普通JavaFX没有任何区别按钮没有波纹动画输入框没有浮动提示。这种情况基本都是Application.setUserAgentStylesheet()没有生效。JFoenix的样式加载走的是JavaFX的UserAgentStylesheet机制这个机制是全局的而且是进程级的。如果你在一个Scene里面切换过多个皮肤或者在代码里重复调用setUserAgentStylesheet(null)就有可能导致样式失效。另一个典型案例是你把jfoenix-design.css放在resources目录下但忘记导入到最终构建产物中。Maven项目里默认会把resources下的文件打进去但如果你用了IDEA的某些构建方式资源过滤配置可能会漏掉CSS文件。检查一下target目录里有没有这个CSS文件是排查此类问题最快的方法。4.3 启动时报各种模块访问错误这是JFoenix的老毛病了。之前我提到过JDK 11以上的模块系统默认不允许反射访问内部接口。JFoenix在内部大量使用了CSS相关类和com.sun.javafx.scene.control.skin里面的类如果不在VM参数中开放这些包就会抛InaccessibleObjectException。这种问题最好的解决方式是使用Maven插件直接配置运行时参数比如javafx-maven-plugin里可以这样plugin groupIdorg.openjfx/groupId artifactIdjavafx-maven-plugin/artifactId version0.0.8/version configuration mainClasscom.example.javafx.DemoApplication/mainClass options option--add-opensjavafx.graphics/com.sun.javafx.csscom.jfoenix/option option--add-opensjavafx.graphics/com.sun.javafx.scene.textcom.jfoenix/option /options /configuration /plugin这样至少在命令行环境下跑mvn javafx:run时不会报错。那些在IDEA里直接运行时不带VM参数、只靠直观报错去搜解决方案的思路效率太低了。我建议你在项目创建之初就把常用参数固化到插件配置里。4.4 常见问题速查表现象可能原因处理办法图标不显示块状/空心字体没加载或字体族被覆盖Font.loadFont()先加载TTF用setGraphic()图标显示太小像“半像素”默认size是12DPI缩放适配差setSize(24)以上或用CSS的-fx-font-size按钮背景色不生效ButtonTypeFLAT默认样式优先级高调用setButtonType(RAISED)再设背景色控件全部回到JavaFX默认样式UserAgentStylesheet没加载或加载顺序被覆盖检查resources打包启动时先设置样式再构建控件运行报InaccessibleObjectException模块权限限制VM参数加--add-opens并确认目标模块正确JFXTextField无法输入中文某些版本在默认样式下输入法不兼容尝试升级到9.0.10并检查-fx-font-family是否保留中文字体4.5 我自己的调试习惯如果有一天你的界面又出了问题我的建议是不要急着改代码先打开Scene的userAgentStylesheet看看当前生效的是哪个。在调试模式下输入System.out.println(Scene.getUserAgentStylesheet());这样可以快速判断是JFoenix全局样式有没有加载成功。如果显示的是null那就是样式加载的问题如果显示的是JavaFX自带的modena.css说明你的样式表被覆盖了去检查调用顺序。再有一个习惯是“二次确认字体”。在图标不显示的时候打印一下Font.getFamilies()看输出列表里有没有FontAwesome。如果没有就说明字体根本没注册进来或者路径不对。这个惯用方法至少帮我节省了三分之一查Bug的时间。5. 从集成到工程化落地5.1 项目结构建议既然已经有十几个系列的经验积累我建议你不要把这两个库当成“临时粘合剂”来用而应该在项目里做一个清晰的分层├── fonts │ ├── fontawesome-webfont.ttf │ └── roboto/ ├── css │ ├── jfoenix-design.css │ └── app.css └── com/example ├── Main.java ├── controller/ └── component/这样做有好处在混合使用JFoenix自带控件和自定义组件时CSS的集中管理能减少很多重复代码。例如把菜单按钮统一提取成一个NavButton类内部封装JFXButton FontAwesomeIconView的组装逻辑这样在界面层只需要一行代码就能创建一个带图标的菜单按钮。5.2 自定义主题时的注意事项FontAwesomeFX和JFoenix都支持深色主题但你需要非常注意图标的颜色和控件的背景色对比度。比如在深色导航栏上JFXButton的默认-fx-text-fill是黑色不改成白色或者亮灰色就会看不清。我的经验是为所有导航栏按钮统一定义一个样式类在CSS里设置颜色而不要在Java代码里逐个setStyle()。因为setStyle()优先级高于外部CSS一旦你用代码写死了颜色后续想要切换主题时所有控件都得改一遍代码维护成本太高了。5.3 性能与体积的权衡FontAwesomeFX的JAR大约不到2MBJFoenix大约3MB左右对于JavaFX项目来说体积不算大。真正需要留意的是启动性能。因为在启动时要加载TTF字体和解析CSS首次启动会比纯JavaFX慢几百毫秒。如果对启动速度非常敏感可以考虑延迟加载一些不常用的图标但从实际项目体验来看这个差距用户基本感知不到。还有一个小地方值得提不要同时把所有FontAwesome图标都创建到界面上。虽然图标是矢量字体渲染性能很高但当界面上同时存在几十个图标节点时JavaFX的CSS样式计算依然会占用一定CPU资源。我的经验是对于重复出现的图标尽量复用同一个FontAwesomeIconView实例必要的时候用ImageView封装一下。5.4 后续扩展思路这两个库只是JavaFX视觉增强的开始。如果你愿意继续深入可以考虑用JFoenix的JFXDialog和JFXSnackbar来做全局的消息提示配合FontAwesome图标界面交互的质感能提升不少。把FontAwesome图标动态生成到一个Button里实现类似“图标按钮”的通用组件便于在多页面项目中复用。结合JFXListView做侧滑菜单每个列表项左边配一个FontAwesome图标右边配一个“”箭头图标视觉效果会很接近目前主流的桌面应用。这些扩展思路我都实际操作过尤其是在大型JavaFX应用中组件的复用程度直接决定了后期维护的幸福感。5.5 一点个人体会把这些库集成了不少项目之后我最大的体会是JavaFX并不缺功能缺的是审美上的工程化包装。FontAwesomeFX和JFoenix的组合本质上就是用字体代替图片、用控件库统一视觉规范让开发者不需要设计师介入也能做出像样的界面。但这些东西始终是“锦上添花”真正决定软件质量的还是底层架构。如果你在这个集成过程中踩了坑别灰心大概率是字体加载顺序或者CSS优先级的问题把这两块吃透后面的路会顺很多。最后再分享一个小技巧调UI的时候开一个独立的调试窗口用JFXButton、TextField、FontAwesomeIconView把常用控件一次性铺出来实时切换主题所见即所得。这个方法虽然土但每次都能帮我快速定位到底是代码的问题还是样式的问题。希望这篇文章能帮你省下那些我当年踩坑浪费的时间。
返回列表