Bootstrap

Java最新图形化界面开发技术——JavaFx教程(含UI控件用法介绍、属性绑定、事件监听、FXML)

JavaFx是Java图形化界面技术AWT、Swing技术的新替代品,如果想系统学习JavaFx,推荐一个B站UP主:https://space.bilibili.com/5096022/channel/collectiondetail?sid=210809
如果是想快速入门JavaFx,推荐视频:
https://www.bilibili.com/video/BV1H14y1g7ji/
https://www.bilibili.com/video/BV1Qf4y1F7Zv/

如果你因毕设或项目中用到图形化界面技术JavaFx,赶时间都不想看,请看下面的文字版,通过多个案例,直接带你快速入门JavaFx,轻松上手看懂并学会使用!


一、JavaFx介绍

1、JavaFx简介

JavaFX是Java的下一代图形用户界面工具包。JavaFX是一组图形和媒体API,我们可以用它们来创建和部署富客户端应用程序。
JavaFX允许开发人员快速构建丰富的跨平台应用程序。JavaFX通过硬件加速图形支持现代GPU。
JavaFX允许开发人员在单个编程接口中组合图形,动画和UI控件。
图表编程语言可用于开发互联网应用程序(RIA)。JavaFX技术主要应用于创建Rich internet applications(RIAs)。当前的JavaFX包括JavaFX脚本和JavaFX Mobile (一种运营于行动装置的操作系统),今后JavaFX将包括更多的产品。JavaFX Script编程语言 (以下称为JavaFX) 是一种声明性的、静态类型脚本语言。
JavaFX技术有着良好的前景,包括可以直接调用Java API的能力。因为JavaFX Script是静态类型,它同样具有结构化代码、重用性和封装性,如包、类、继承和单独编译和发布单元,这些特性使得使用JavaFX技术创建和管理大型程序变为可能。

JavaFx官网:https://openjfx.io/
image.png
下面是基于JavaFx实现的一些第三方开发案例和应用场景:
image.png

2、可用性

JavaFX APIJava SE运行时环境(JRE)和Java开发工具包(JDK)绑在一起。
JDK可用于所有主要的桌面平台,WindowsMac OS XLinux
JavaFX应用程序可以在所有主要的桌面平台上编译和运行。
JDK for ARM平台包括JavaFx的基础,图形和控件组件。

3、主要特征

JavaFX中包含以下功能-

  • JavaFX是用Java编写的,JavaFX应用程序代码可以从任何Java库引用API。
  • JavaFX应用程序的外观和感觉可以定制。因此可以使用级联样式表(CSS)来对JavaFX应用程序进行风格化。平面设计师可以通过CSS自定义外观和样式。
  • 还可以在FXML脚本语言中描述UI的表示方面,并使用Java对应用程序逻辑进行编码。
  • 通过使用JavaFX Scene Builder,可以通过拖放来设计Ul。Scene Builder将创建可以移植到集成开发环境(IDE)的FXML标记,以便开发人员可以添加业务逻辑。
  • JavaFX有一个称为WebView的控件,可以呈现复杂的网页。WebView支持JavaScript,我们可以从Java API在Web页面中调用Javascript。 WebView不支持额外的HTML5功能,包括Web套接字,Web Workers和Web字体,还可以从WebView打印网页。
  • Swing互操作性。现有的Swing应用程序可以使用JavaFX类,例如图表和WebView。还可以使用SwingNode类将Swing内容嵌入到我们应用程序中。
  • 3D图形功能。JavaFX支持Shape,如Box,Cylinder,MeshView和Sphere子类,SubScene,Material,PickResult,AmbientLight和PointLight。
  • Canvas API。 使用Canvas API,可以在JavaFX场景上绘制。
  • 打印APl。 javafx.print包提供了JavaFX Printing API的类。
  • 富文本支持。JavaFX支持增强的文本,包括双向文本和复杂的文本脚本,例如泰语和印度教的控件,以及多行,多种风格的文本。
  • 多点触控支持,JavaFX提供对多点触摸操作的支持。
  • JavaFX支持Hi-DPI显示。

4、UI控件

以下列出了JavaFX API中提供的一些内置JavaFX UI控件:

  • Label
  • Button
  • Radio Button
  • Toggle Button
  • Checkbox
  • ChoiceBox
  • Text Field
  • Password Field
  • Scroll Bar
  • Scroll Pane
  • List View
  • Table View
  • Tree View
  • Tree Table View
  • Combo Box
  • Separator
  • Slider
  • Progress Bar
  • Progress Indicator
  • Hyperlink
  • Tooltip
  • HTML Editor
  • Titled Pane
  • AccordionMenu
  • Color Picker
  • Date Picker
  • Pagination Control
  • File Chooser

JavaFX允许UI控制节点和形状节点在场景图上共存。
我们可以像任何其他JavaFX节点一样处理任何UI控件,例如可以缩放,旋转,样式和添加效果。


二、JavaFx概述

1、JavaFx结构图

一般来说,JavaFX应用程序包含一个或多个对应于窗口的阶段。每个阶段都有一个场景。每个场景都可以有一个控件、布局等附加到它的对象图,称为场景图。这些概念都将在后面更详细地解释。下面是JavaFX应用程序的般结构的图示:
image.png
image.png

2、JavaFx组件

(1)舞台

舞台是JavaFX应用程序的外部框架。舞台通常对应于一个窗口。在 JavaFX 可以在浏览器中运行的早期阶段,舞台还可以指网页内 JavaFX 可用于绘制自身的区域。
由于 Java 浏览器插件的弃用,JavaFX 主要用于桌面应用程序。在这里,JavaFX 取代了 Swing 作为推荐的桌面GUI 框架。而且JavaFX 看起来比 Swing 更加一致且功能丰富。
在桌面环境中使用时,JavaFX 应用程序可以打开多个窗口。每个窗口都有自己的舞台。
每个阶段都由StageJavaFX 应用程序中的一个对象表示。StageJavaFX 应用程序有一个由JavaFX 运行时为您创建的主对象。如果 JavaFX 应用程序Stage需要打开其他窗口,它可以创建其他对象。例如,对于对话框、向导等。

(2)场景

要在JavaFX 应用程序的舞台上显示何内容,您需要一个场景。一个舞台一次只能显示一个场景,但可以在运行时交换场景。就像剧院中的舞台可以重新安排以在戏剧期间显示多个场景一样,JavaFX 中的舞台对象可以在JavaFX 应用程序的生命周期内显示多个场景 (一次一个) 。
您可能想知道为什么 JavaFX 应用程序的每个阶段会有多个场景。想象一个电脑游戏。一个游戏可能有多个“屏幕”向用户显示。例如,初始菜单屏幕、主游戏屏幕(玩游戏的地方)、游戏结束屏幕和高分屏幕。这些屏幕中的每一个都可以由不同的场景来表示。当游戏需要从一屏切换到下一屏时,它只需将相应的场景附加到StageJavaFX 应用程序的对象上即可。
场景由SceneJavaFX 应用程序中的对象表示。JavaFX 应用程序必须创建Scene它需要的所有对象。

① 场景图

所有视觉组件(控件、布局等)都必须附加到要显示的场景,并且该场景必须附加到舞台才能使整个场景可见。附加到场景的所有控件、布局等的总对象图称为场景图。

② 节点

附加到场景图的所有组件都称为节点。所有节点都是JavaFX 类的子类,称为javafx.scene.Node。
有两种类型的节点:分支节点和叶节点。分支节点是可以包合其他节点(子节点)的节点。分支节点也称为父节点,因为它们可以包含子节点。叶节点是不能包含其他节点的节点。

(3)控件

JavaFX 控件是 JavaFX 组件,它们在JavaFX 应用程序中提供某种控制功能。例如,按钮、单选按钮、表格、树
为了使控件可见,它必须附加到某个Scene对象的场景图中。
控件通常嵌套在一些 JavaFX 布局组件中,这些组件管理控件相对于彼此的布局。
JavaFX 包含以下控件:

  • 手风琴
  • 按钮
  • 复选框
  • 选择框
  • 选色器
  • 组合框
  • 日期选择器
  • 标签
  • 列表显示
  • 菜单
  • 菜单栏
  • 密码字段
  • 进度条
  • 单选按钮
  • 滑块
  • 微调器
  • 拆分菜单按钮
  • 拆分窗格
  • 表视图
  • 选项卡窗格
  • 文本区域
  • 文本域
  • 标题窗格
  • 切换按钮
  • 工具栏
  • 树表视图
  • 树视图

这些控件中的每一个都将在单独的文本中进行解释。

(4)布局

JavaFX 布局是其中包含其他组件的组件。布局组件管理嵌套在其中的组件的布局。JavaFX 布局组件有时也称为父组件,因为它们包含子组件,而且布局组件是JavaFX 类的子类javafx.scene.Parent。
布局组件必须附加到某个Scene对象的场景图才能可见。
JavaFX 包含以下布局组件:

  • 团体
  • 地区
  • 窗格
  • HBox
  • 盒子
  • 流窗格
  • 边框窗格
  • 边框窗格
  • 堆栈窗格
  • 瓷砖窗格
  • 网格窗格
  • 锚点窗格
  • 文本流

这些布局组件中的每一个都将在单独的文本中进行介绍。

(5)图表

JavaFX 带有一组内置的即用型图表组件,因此您不必每次需要基本图表时都从头开始编写图表代码。
JavaFX 包含以下图表组件:

  • 面积图
  • 条形图
  • 气泡图
  • 折线图
  • 饼形图
  • 散点图
  • 堆积面积图
  • 堆积条形图

(6)2D图形

JavaFX 包含可以轻松在屏幕上绘制2D 图形的功能。

(7)3D图形

JavaFX 包含可以轻松在屏幕上绘制 3D 图形的功能。

(8)声音

JavaFX 包含使在 JavaFX 应用程序中播放音频变得容易的功能。这通常在游戏或教育应用中很有用。

(9)视频

JavaFX 包合使在 JavaFX 应用程序中播放视频变得容易的功能。这通常在流媒体应用程序、游戏或教育应用程序中很有用。

JavaFX 包含一个WebView能够显示网页 (HTML5、CSS 等)的组件。JavaFXWebView组件基于 WebKit-Chrome 和 Safari 中也使用的网页染引擎。
该WebView组件使得将桌面应用程序与 Web 应用程序混合成为可能。有时这很有用。例如,如果您已经有一个不错的 Web 应用程序,但需要一些只有桌面应用程序才能提供的功能一一例如磁盘访问、与 HTTP 以外的其他网络协议 (例如 UDP、IAP 等) 的通信。


三、JavaFx快速入门

JavaFx中文官方网站:https://openjfx.cn/

1、IDEA配置JavaFx环境

JDK8集成了JavaFx,而JDK11后就不自带JavaFx了,需要自己下载jar包进行导入。
如果使用的是Java8,请直接跳到下面第二步。

2、编码与运行

image.png
创建以下包目录结构、主入口Main类:
image.png
编写一个JavaFx基本结构代码:

import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.layout.BorderPane;
import javafx.stage.Stage;

// 继承Application抽象类,重新start方法
public class Main extends Application {
    public static void main(String[] args) {
        // 入口函数里调用Application的静态方法launch,之后会自动调用start方法
        Application.launch(args);
    }

    /**
     * @param primaryStage 主窗口
     */
    @Override
    public void start(Stage primaryStage) throws Exception {
        // 设置一个场景,场景里添加一个树形组件图,先创建一个标签
        Label label = new Label("Hello JavaFx!");
        // 创建布局,将标签放入布局里,BorderPane布局把场景划分为上下左右中,默认加入的控件在中间位置
        BorderPane pane = new BorderPane(label);
        // 创建场景,将布局放入场景里,设置宽度和高度
        Scene scene = new Scene(pane, 300, 300);
        // 将场景设置到窗口里
        primaryStage.setScene(scene);
        // 设置标题
        primaryStage.setTitle("我是窗口");
        primaryStage.show();
    }
}

运行main方法,测试结果:
image.png


四、组件功能介绍

1、Application

  1. Application是JavaFx程序的入口,任何JavaFx应用程序程序都要继承该类并重写start()方法。
public class Main extends javafx.application.Application {
    public static void main(String[] args) {
        Application.launch(args);
    }

    @Override
    public void start(Stage primaryStage) throws Exception {
    }
}
  1. 通过main()执行Application的launch(String… args)方法,当然该方法不传入任何值也是可以执行的。launch()方法会默认执行本类下的init()、start()、stop()方法。执行下面的main()方法后显示顺序为:
import javafx.application.Application;
import javafx.stage.Stage;

public class Main extends Application {
    public static void main(String[] args) {
        launch(args);
        System.out.println("这是main方法");
    }

    @Override
    public void init() {
        System.out.println("这是初始化方法");
    }

    @Override
    public void start(Stage primaryStage) throws Exception {
        primaryStage.show();
        System.out.println("这是start()方法");
    }

    @Override
    public void stop() throws Exception {
        System.out.println("这个是stop()方法");
    }
}

可以看出只有当stop()方法被调用时(即Application应用程序被正常关闭,也可以是图形界面右上角的❌被点击时),launch()方法才执行完毕。(通过Platform.exit()退出界面也是属于正常退出)
通过IDE控制台终止应用程序属于非正常终止,它是通过关闭JVM从而关闭应用程序的。

  1. 也可以在其他类中的main()方法中调用launch()方法,但要传入相应的参数,第一个参数指定一个Application的子类,从而调用该类下的init()、start()、stop()方法,第二个参数制定一个String类型的值。

该方法launch(Class<? extends Application> appClass, String… args)也可以不指定String类型的值。

  1. Application类下的getHostServices()方法,该方法返回一个HostServices实例,该实例的showDocument()方法可以指定一个网站地址或者URI,从而以系统默认的浏览器打开该URI。getDocumentBase()返回一个String类型的当前文档所在的路径。
import javafx.application.Application;
import javafx.application.HostServices;
import javafx.stage.Stage;

public class Main extends Application {
    public static void main(String[] args) {
        launch(args);
    }

    @Override
    public void start(Stage primaryStage) throws Exception {
        HostServices hostServices = getHostServices();
        hostServices.showDocument("www.baidu.com");
        System.out.println(hostServices.getDocumentBase());
    }
}

image.png

也可以写一个按钮,点击触发事件后弹出默认浏览器进入网站:

import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.layout.BorderPane;
import javafx.stage.Stage;

public class Main extends Application {
    public static void main(String[] args) {
        launch(args);
    }

    @Override
    public void start(Stage primaryStage) throws Exception {
        Button button = new Button("点击进入百度");
        BorderPane pane = new BorderPane(button);
        button.setOnAction(e -> {
            getHostServices().showDocument("www.baidu.com");
        });
        Scene scene = new Scene(pane, 400, 400);
        primaryStage.setScene(scene);
        primaryStage.setTitle("JavaFx App");
        primaryStage.show();
    }
}

image.png

另外线程方面,执行main()方法时是主线程,当调用init()方法时是JavaFX-Launcher线程,当调用start()方法时是JavaFX Application Thread线程,当调用stop()方法时依然是JavaFX Application Thread线程。
因此,我们可以在init()方法中做一些连接数据库等初始化工作,可与start()方法的不同线程同步进行,提高运行效率。


2、Stage舞台(窗口)

JavaFx Stage:javafx.stage.Stage表示JavaFx桌面应用程序中的窗口。在JavaFx中,窗口Stage可以插入一个场景Scene,它表示在窗口内显示的内容。
当JavaFx应用程序启动时,它会创建一个根Stage对象,该对象将传递给 start(Stage primaryStage) JavaFx应用程序的根类的方法。此Stage对象代表JavaFx应用程序的主窗口。您可以在应用程序生命周期的后期创建新Stage对象,以防应用程序需要打开更多窗口。

(1)创建舞台

像创建任何其他Java对象一样创建Stage对象:

Stage stage = new Stage();

(2)展示舞台

简单地创建Stage对象不会显示它,为了使Stage可见,必须调用它的show()或showAndWait()方法:

Stage stage = new Stage();
stage.show();

(3)在舞台上设置场景

JavaFX在Stage中显示任何场景内容,必须在Stage显示前将Scene创建,并设置在Stage中:

VBox VBox = new VBox(new Label("A JavaFx Label"));
Scene scene = new Scene(vBox);

Stage stage = new Stage();
stage.setScene(scene);
// ...
// stage.show();

(4)舞台标题

通过setTitle()方法设置Stage标题,将显示在Stage窗口的标题栏中:

stage.setTitle("JavaFx stage window Title");

(5)舞台位置

通过其setX()和setY()方法设置窗口左上角的位置(X, Y),即启动后窗口在屏幕中的坐标:

stage.setX(100);
stage.setY(100);

注意,如果设置X和Y位置,可能还需要设置宽度和高度,否则舞台窗口可能会变得非常小。

(6)舞台宽度和高度

通过其setWidth()和setHeight()方法,设置JavaFx的宽度和高度:

stage.setWidth(600);
stage.setHeight(300);

一般我们不需要单独设置舞台宽高,只需通过场景的宽高设置来影响舞台的宽高。

(7)舞台风格

通过其initStyle()方法,参数为枚举类StageStyle设置JavaFx的样式,可以选择一组不同的样式:

  • DECORATED:默认装饰Stage是带有操作系统装饰(标题栏和最小化/最大化/关闭按钮)和白色背景的标准窗口。
  • UNDECORATED:未装饰Stage是没有操作系统装饰的标准窗口,但仍具有白色背景。
  • TRANSPARENT:透明Stage是具有透明背景的未装饰窗口。
  • UNIFIED:统一Stage就像一个装饰的舞台,只是装饰区域和主要内容区域之间没有边界。
  • UTILITY:实用程序Stage是装饰过的窗口,但装饰最少。
stage.initStyle(StageStyle.DECORATED);	// 默认窗口样式
//stage.initStyle(StageStyle.UNDECORATED);	// 无装饰窗口样式,自定义窗口时常用
//stage.initStyle(StageStyle.TRANSPARENT);	// 透明窗口样式,与无装饰类似
//stage.initStyle(StageStyle.UNIFIED);	// 和默认窗口效果差不多
//stage.initStyle(StageStyle.UTILITY);	// 简单窗口样式,只有Title和关闭

(8)舞台全屏模式

通过其setFullScreen()方法将窗口设置为全屏模式:

stage.setFullScreen(true);

(9)阶段生命周期事件

Stage可以发出一些可以监听的事件,这些舞台事件是:

Close Request
Hiding
Hidden
Showing
Shown

关闭阶段事件监听器
可以在舞台上侦听关闭事件,当用户单击舞台窗口右上角的关闭按钮时,触发回调事件。
如需要在主Stage窗口关闭后清理一些资源、停止一些线程等。监听Stage关闭事件会很有用:

stage.setOnCloseRequest(event -> System.out.println("closing stage"));
  • 小案例:设置关闭窗口监听事件,当用户单击舞台窗口右上角的关闭按钮时,触发回调事件弹出确认框,点击确认后关闭窗口,点击取消不做动作。
import javafx.application.Application;
import javafx.application.Platform;
import javafx.scene.control.Alert;
import javafx.scene.control.ButtonType;
import javafx.stage.Stage;

import java.util.Optional;

public class Main extends Application {
    public static void main(String[] args) {
        Application.launch(args);
    }

    @Override
    public void start(Stage primaryStage) throws Exception {
        // 设置关闭窗口的监听事件,首先操作系统默认的关闭事件取消掉
        Platform.setImplicitExit(false);    // 取消操作系统默认退出事件,点击关闭后系统不结束运行
        primaryStage.setOnCloseRequest(event -> {	// 点击右上角关闭触发事件
            event.consume();    // 取消关闭窗口动作,点击关闭后不会关闭窗口
            Alert alert = new Alert(Alert.AlertType.CONFIRMATION);  // 弹出确认框
            alert.setTitle("退出程序");
            alert.setHeaderText(null);
            alert.setContentText("确认是否要退出程序?");
            Optional<ButtonType> result = alert.showAndWait();
            if (result.get() == ButtonType.OK) {    // 点击确定按钮
                //Platform.exit();    // 退出程序
                primaryStage.close(); // 仅关闭窗口,不退出程序
            }
        });
        primaryStage.setTitle("设置关闭窗口的监听事件Demo");
        primaryStage.show();
    }
}

(10)键盘或鼠标事件

可以在Stage上设置侦听键盘事件,可以捕获在舞台具有焦点时发生的所有键盘事件。

stage.addEventHandler(KeyEvent.KEY_PRESSED, e -> System.out.println("键盘按下的键:" + e.getCode().getName()));
stage.addEventHandler(MouseEvent.MOUSE_PRESSED, e -> System.out.println("鼠标按下的键:" + e.getButton()));
  • 小案例:WASD控制图片移动
import javafx.application.Application;
import javafx.scene.Group;
import javafx.scene.Scene;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.input.KeyEvent;
import javafx.stage.Stage;

import java.io.FileInputStream;

public class Main extends Application {
    public static void main(String[] args) {
        Application.launch(args);
    }

    @Override
    public void start(Stage stage) throws Exception {
        stage.setTitle("WASD控制图片移动");
        stage.getIcons().add(new Image("images/测试图片.png"));
        FileInputStream input = new FileInputStream("src/resources/images/测试图片.png");
        Image image = new Image(input);
        ImageView imageview = new ImageView(image);
        imageview.setFitWidth(100);
        imageview.setFitHeight(100);

        Group group = new Group(imageview);
        Scene scene = new Scene(group, 600, 600);
        stage.setScene(scene);

        stage.addEventHandler(KeyEvent.KEY_PRESSED, event -> {
            if ("W".equals(event.getCode().getName())) {
                imageview.setLayoutY(imageview.getLayoutY() - 10);
            }
            if ("S".equals(event.getCode().getName())) {
                imageview.setLayoutY(imageview.getLayoutY() + 10);
            }
            if ("A".equals(event.getCode().getName())) {
                imageview.setLayoutX(imageview.getLayoutX() - 10);
            }
            if ("D".equals(event.getCode().getName())) {
                imageview.setLayoutX(imageview.getLayoutX() + 10);
            }
        });

        stage.show();
    }
}

效果图:
image.png


3、Scene场景

场景对象是场景图的根。换句话说,场景包含其中的所有可视的GUI组件。场景由类表示javafx.scene.Scene。必须在Stage上设置Scene对象才能可见。
场景图是一种树状数据结构,用于排列(和分组)图形对象,以便于逻辑表示。它还允许图形引擎通过完全或部分跳过最终图像中看不到的对象,以最有效的方式渲染对象。下图显示了JavaFX场景图体系结构的一个示例:
image.png

(1)创建场景

Scene您可以通过其构造函数创建对象。需要传递根组件作为参数,下面是创建Scene对象的示例:

VBox vBox = new VBox();
Scene scene = new Scene(vBox);

(2)在舞台上设置场景

为了使Scene可见,必须在Stage上设置它:

VBox vBox = new VBox(new Label("A JavaFx Label"));
Scene scene = new Scene(vBox);

Stage stage = new Stage();
stage.setScene(scene);

注意:Stage一次只能显示一个Scene。

(3)场景鼠标光标

可以在Scene上设置鼠标光标。鼠标光标是显示在鼠标光标(指针)位置的小图标。Scene您可以通过setCursor()方法设置鼠标的光标。

  • 小案例:设置两个场景,通过按钮绑定事件,实现场景和鼠标光标的互相转换:
public class Main extends Application {
    public static void main(String[] args) {
        Application.launch(args);
    }

    @Override
    public void start(Stage primaryStage) throws Exception {
        Button button0 = new Button("点击切换场景");
        button0.setLayoutX(200);
        button0.setLayoutY(200);
        AnchorPane root = new AnchorPane();
        root.getChildren().add(button0);
        Scene scene = new Scene(root, 500, 500);
        scene.setCursor(new ImageCursor(new Image("images/测试图片1.png")));

        Label label = new Label("你好,JavaFx");
        label.setLayoutX(200);
        label.setLayoutY(200);
        Button button1 = new Button("返回原场景");
        button1.setLayoutX(200);
        button1.setLayoutY(250);
        AnchorPane root1 = new AnchorPane();
        root1.getChildren().addAll(label, button1);
        Scene scene1 = new Scene(root1, 500, 500);
        scene1.setCursor(new ImageCursor(new Image("images/测试图片2.png")));

        button0.setOnAction(event -> {
            primaryStage.setScene(scene1);
        });

        button1.setOnAction(event -> {
            primaryStage.setScene(scene);
        });

        primaryStage.setScene(scene);
        primaryStage.show();
    }
}

效果图:鼠标光标图片也会切换
image.png

image.png


4、Node节点 UI控件的通用属性

Node类是所有控件的父类,下面通过其控件子类示例演示Node节点的UI控件的通用属性。

(1)控件节点坐标系

每个 JavaFX 节点都有自己的笛卡尔坐标系。与常规笛卡尔坐标系的唯一区别是Y轴是相反的。也就是说,坐标系的原点在坐标系的左上角。随着Y值的增加,该点从坐标系的顶部向下移动。Y轴的这种反转在2D图形坐标系中是正常的。

image.png

JavaFX 节点可能具有负X和Y坐标。
每个节点都有自己的坐标系。此坐标系用于在父节点内定位子节点实例,或者在 JavaFX 画布上绘图时。这意味着,作为另一个节点的子节点的节点都有自己的坐标系,以及在其父节点坐标系中的位置 (X,Y)。
以下是父节点坐标系的示例,其中子节点位于父节点坐标系中的(25,25)处。子节点也有它自己的坐标系,它有它的 (0,0),其中子节点位于父坐标系中-意思是在父节点坐标系中的(25,25)。
image.png

(2)设置控件坐标

Label label = new Label("你好,JavaFx");
label.setLayoutX(200);  // 设置控件X轴坐标
label.setLayoutY(200);  // 设置控件Y轴坐标

(3)设置控件风格

与css语法类似,只是前面加上-fx,设置红色背景,蓝色边框和边框宽度为3个像素:

label.setStyle("-fx-background-color: red; -fx-border-color: blue; -fx-border-width: 3px"); // 设置控件风格,与css语法类似,只是前面加上-fx

(4)设置控件首要背景宽度和高度

label.setPrefWidth(200);    // 设置控件背景宽度
label.setPrefHeight(50);    // 设置控件背景高度

(5)设置文本居中

label.setAlignment(Pos.CENTER); // 设置文本居中

(6)设置是否显示/隐藏该控件

true:默认显示控件,false:隐藏控件

label.setVisible(false);    // 是否显示该控件

(7)设置控件半透明值

label.setOpacity(0.5);  // 设置控件半透明值

(8)设置控件旋转角度

label.setRotate(70);    // 设置控件旋转角度

(9)设置控件X轴和Y轴平移量

label.setTranslateX(60);    // 设置X轴平移量
label.setTranslateY(100);   // 设置Y轴平移量
  • 案例演示:
public class Main extends Application {
    public static void main(String[] args) {
        Application.launch(args);
    }

    @Override
    public void start(Stage primaryStage) throws Exception {
        Label label = new Label("你好,JavaFx");
        label.setLayoutX(200);  // 设置控件X轴坐标
        label.setLayoutY(200);  // 设置控件Y轴坐标
        label.setStyle("-fx-background-color: red; -fx-border-color: blue; -fx-border-width: 3px"); // 设置控件风格,与css语法类似,只是前面加上-fx
        label.setPrefWidth(200);    // 设置控件背景宽度
        label.setPrefHeight(50);    // 设置控件背景高度
        label.setAlignment(Pos.CENTER); // 设置文本居中
        //label.setVisible(false);    // 是否显示该控件
        label.setOpacity(0.5);  // 设置控件半透明值
        label.setRotate(70);    // 设置控件旋转角度
        label.setTranslateX(60);    // 设置X轴平移量
        label.setTranslateY(100);   // 设置Y轴平移量
        AnchorPane root = new AnchorPane();
        root.getChildren().add(label);
        Scene scene = new Scene(root, 500, 500);
        primaryStage.setScene(scene);
        primaryStage.show();
    }
}

image.png


5、UI控件的属性绑定和属性监听

  • 小案例:在场景中画一个圆,利用属性单向绑定,使拖动边框大小时,圆始终保持在中心位置。并且设置X轴属性监听器,使得横向拖动边框时,控制台打印移动数据值,而Y轴不设置属性监听,纵向拖动时不打印数据值。
import javafx.application.Application;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.scene.Scene;
import javafx.scene.layout.AnchorPane;
import javafx.scene.paint.Color;
import javafx.scene.shape.Circle;
import javafx.stage.Stage;

public class Main extends Application {
    public static void main(String[] args) {
        Application.launch(args);
    }

    @Override
    public void start(Stage primaryStage) throws Exception {
        AnchorPane root = new AnchorPane();
        Scene scene = new Scene(root, 500, 500);

        Circle circle = new Circle();
        circle.setCenterX(250);
        circle.setCenterY(250);
        circle.setRadius(100);
        circle.setStroke(Color.BLACK);
        circle.setFill(Color.WHITE);
        // 设置属性单向绑定,使圆始终保持在中心位置
        circle.centerXProperty().bind(scene.widthProperty().divide(2));
        circle.centerYProperty().bind(scene.heightProperty().divide(2));

        // 设置X轴属性监听器
        circle.centerXProperty().addListener(new ChangeListener<Number>() {
            @Override
            public void changed(ObservableValue<? extends Number> observable, Number oldValue, Number newValue) {
                System.out.println("x轴中心点改变了,原来是:" + oldValue + ",现在是:" + newValue);
            }
        });

        root.getChildren().add(circle);
        primaryStage.setScene(scene);
        primaryStage.show();
    }
}

image.png

image.png


6、JavaFx应用里的事件驱动编程

image.png

  • 小案例:利用事件驱动,实现将一个任意文件拖拽到文本框中后,拖拽时显示移动,拖拽松开后文本框显示文件的绝对路径。
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.TextField;
import javafx.scene.input.Dragboard;
import javafx.scene.input.TransferMode;
import javafx.scene.layout.AnchorPane;
import javafx.stage.Stage;

public class Main extends Application {
    public static void main(String[] args) {
        Application.launch(args);
    }

    @Override
    public void start(Stage primaryStage) throws Exception {
        AnchorPane root = new AnchorPane();
        Scene scene = new Scene(root, 500, 500);
        TextField textField = new TextField();  // 文本框
        textField.setLayoutX(150);
        textField.setLayoutY(200);

        textField.setOnDragOver(event -> {
            event.acceptTransferModes(TransferMode.ANY);
        });

        textField.setOnDragDropped(event -> {
            Dragboard dragboard = event.getDragboard();
            if (dragboard.hasFiles()) {
                String path = dragboard.getFiles().get(0).getAbsolutePath();
                textField.setText(path);
            }
        });
        root.getChildren().add(textField);
        primaryStage.setScene(scene);
        primaryStage.show();
    }
}

效果图:
image.png

image.png


7、Color、Font、Image

本介绍直接采用案例演示法,具体参数方法请参阅JavaFx API文档。

(1)Color颜色

  • 案例演示:利用Color类设置圆的边框颜色和内部填充颜色
public class Main extends Application {
    public static void main(String[] args) {
        Application.launch(args);
    }

    @Override
    public void start(Stage primaryStage) throws Exception {
        AnchorPane root = new AnchorPane();
        Scene scene = new Scene(root, 500, 500);

        Circle circle = new Circle();
        circle.setCenterX(250); // 设置圆心X轴位置
        circle.setCenterY(250); // 设置圆心Y轴位置
        circle.setRadius(100);  // 设置半径
        //circle.setFill(Color.rgb(100, 50, 100));    // 设置内部填充颜色
        circle.setFill(Color.web("#f66a08"));   // 设置内部填充颜色,与CSS颜色语法一样
        circle.setStroke(Color.BLUE);  // 设置描边颜色
        circle.setStrokeWidth(10);    // 设置圆边框宽度

        root.getChildren().add(circle);
        primaryStage.setScene(scene);
        primaryStage.show();
    }
}

效果图:
image.png

(2)Font字体

在C:\Windows\Fonts下可查阅系统字体。
image.png

image.png

  • 案例演示:为Label标签设置Font字体风格
public class Main12 extends Application {
    public static void main(String[] args) {
        Application.launch(args);
    }

    @Override
    public void start(Stage primaryStage) throws Exception {
        AnchorPane root = new AnchorPane();
        Scene scene = new Scene(root, 500, 500);

        Label label = new Label("文本Font演示");
        label.setLayoutX(150);
        label.setLayoutY(150);
        //label.setFont(new Font(30));  // 设置字体
        label.setFont(Font.font("华文行楷", FontWeight.BOLD, 30));   // 参数:字体名称,字重(斜体,黑体,粗体等),字体大小


        root.getChildren().add(label);
        primaryStage.setScene(scene);
        primaryStage.show();
    }
}

效果图:
image.png

(3)Image图片

Image图片支持BMP/GIF/JPEG/PNG

  • 案例演示:通过Image和ImageView类显示图片
public class Main13 extends Application {
    public static void main(String[] args) {
        Application.launch(args);
    }

    @Override
    public void start(Stage primaryStage) throws Exception {
        AnchorPane root = new AnchorPane();
        Scene scene = new Scene(root, 500, 500);

        ImageView imageView = new ImageView();
        Image image = new Image("images/背景.png");
        imageView.setImage(image);

        root.getChildren().add(imageView);
        primaryStage.setScene(scene);
        primaryStage.show();
    }
}

效果图:
image.png


五、FXML

FXML是一种可编写的、基于XML的用于构造JavaFX场景图的标记语言。在FXML中,一个FXML标签代表以下类型之一:

  • 某个类的实例
  • 某个类实例的属性
  • 某个静态属性
  • 一个定义代码块
  • 一个脚本代码块

一个FXML属性表示以下类型之一:

  • 某个类实例的属性
  • 某个静态属性
  • 事件处理程序

image.png
Introduction to FXML:https://openjfx.cn/javadoc/18/javafx.fxml/javafx/fxml/doc-files/introduction_to_fxml.html


1、FXML布局文件的使用

  • 案例演示:将下面的JavaFx文件代码中的容器、组件和监听事件,改写为FXML文件的格式,简化和方便管理JavaFx类的编写。
public class Main extends Application {
    public static void main(String[] args) {
        Application.launch(args);
    }

    @Override
    public void start(Stage primaryStage) throws Exception {
        AnchorPane root = new AnchorPane();
        Scene scene = new Scene(root, 500, 500);

        Label label = new Label("按键盘↓向下移动");
        label.setLayoutX(100);
        label.setLayoutY(150);
        label.setFont(new Font(30));
        Button button = new Button("点击按钮向上移动");
        button.setLayoutX(350);
        button.setLayoutY(200);

        button.setOnAction(new EventHandler<ActionEvent>() {
            @Override
            public void handle(ActionEvent event) {
                label.setLayoutY(label.getLayoutY() - 5);
            }
        });

        scene.setOnKeyReleased(new EventHandler<KeyEvent>() {
            @Override
            public void handle(KeyEvent event) {
                KeyCode keyCode = event.getCode();
                if (keyCode.equals(KeyCode.DOWN)) {
                    label.setLayoutY(label.getLayoutY() + 5);
                }
            }
        });

        root.getChildren().addAll(label, button);
        primaryStage.setScene(scene);
        primaryStage.show();
    }
}

改写为FXML:

public class Demo extends Application {
    public static void main(String[] args) {
        Application.launch(args);
    }

    @Override
    public void start(Stage primaryStage) throws Exception {
        // 使用FXMLLoader类的load方法来加载FXML文件,并将其与Controller类进行关联。
        //Pane root = FXMLLoader.load(getClass().getClassLoader().getResource("com/aizen/javafx/fxml/demo.fxml"));
        Pane root = FXMLLoader.load(getClass().getResource("demo.fxml"));
        Scene scene = new Scene(root, 500, 500);
        primaryStage.setScene(scene);
        primaryStage.show();
    }
}

public class DemoController {
    @FXML
    Label la;

    @FXML
    Button bu;


    public void handleButtonAction() {
        la.setLayoutY(la.getLayoutY() - 5);
    }

    public void handleKeyReleased(KeyEvent event) {
        KeyCode keyCode = event.getCode();
        if (keyCode.equals(KeyCode.DOWN)) {
            la.setLayoutY(la.getLayoutY() + 5);
        }
    }
}
<?xml version="1.0" encoding="UTF-8"?>

<?import java.lang.*?>
<?import java.util.*?>
<?import javafx.scene.*?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>

<?import javafx.scene.text.Font?>
<AnchorPane xmlns="http://javafx.com/javafx"
            xmlns:fx="http://javafx.com/fxml"
            fx:controller="com.aizen.javafx.fxml.DemoController"
            onKeyReleased="#handleKeyReleased"
            prefHeight="400.0" prefWidth="600.0">
    <children>
        <Label fx:id="la" text="按键盘↓向下移动" layoutX="100" layoutY="150">
            <font>
                <Font size="30"/>
            </font>
        </Label>

        <Button fx:id="bu" text="点击按钮向上移动" layoutX="350" layoutY="200" onAction="#handleButtonAction"/>
    </children>
</AnchorPane>

2、Scene Builder里构建fxml布局文件

官方下载链接:https://openjfx.cn/scene-builder/
image.png

点击下一步,完成安装
image.png

选择空项目
image.png

布局和组件直接拖拽进场景即可,保存后可生成对应的fxml文件。
image.png


3、Controller里的initialize方法

有时我们是无法在fxml文件里填充数据的,并且有些内容需要初始化时就填充(如表格),而不是触发事件后填充,此时就可以使用initialize方法,做一些初始化的工作。

initialize()方法需要自定义,定义完之后会自动调用,该方法调用的时机是加载好fxml文件,并绑定好控件id之后,才会自动调用一次。

需要注意的是在initialize()方法中是无法访问Scene场景的。

  • 演示案例:使用initialize()方法初始化时填充完TableView的数据。
public class Main extends Application {
    public static void main(String[] args) {
        Application.launch(args);
    }

    @Override
    public void start(Stage primaryStage) throws Exception {
        Pane root = FXMLLoader.load(getClass().getResource("hello.fxml"));
        primaryStage.setScene(new Scene(root));
        primaryStage.show();
    }
}

public class Person {
    private String name;
    private int age;

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }
}


public class Controller {

    @FXML
    private TableView<Person> tableView;

    @FXML
    private TableColumn<Person, String> name;

    @FXML
    private TableColumn<Person, Integer> age;
    
    public void initialize() {
        ObservableList<Person> cellDate = FXCollections.observableArrayList();
        name.setCellValueFactory(new PropertyValueFactory<Person, String>("name"));
        age.setCellValueFactory(new PropertyValueFactory<Person, Integer>("age"));
        cellDate.add(new Person("张三", 18));
        cellDate.add(new Person("李四", 19));
        cellDate.add(new Person("王五", 23));
        cellDate.add(new Person("赵六", 15));
        tableView.setItems(cellDate);
    }
}

<?xml version="1.0" encoding="UTF-8"?>

<?import javafx.scene.control.TableColumn?>
<?import javafx.scene.control.TableView?>
<?import javafx.scene.layout.AnchorPane?>


<AnchorPane fx:controller="com.aizen.javafx.fxml_02.Controller" maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" prefHeight="400.0" prefWidth="600.0" xmlns="http://javafx.com/javafx/8.0.171" xmlns:fx="http://javafx.com/fxml/1">
   <children>
      <TableView fx:id="tableView" prefHeight="400.0" prefWidth="600.0">
        <columns>
          <TableColumn fx:id="name" prefWidth="75.0" text="name" />
          <TableColumn fx:id="age" prefWidth="75.0" text="age" />
        </columns>
      </TableView>
   </children>
</AnchorPane>

效果图:
image.png


4、在Application里操作Controller

  • 案例演示:和之前圆的案例一样,要求圆的中心点自适应边框大小,使用fxml实现。
public class JavaFxApplication extends Application {
    public static void main(String[] args) {
        launch(args);
    }

    @Override
    public void start(Stage primaryStage) throws IOException {
        FXMLLoader fxmlLoader = new FXMLLoader();   // 使用FXMLLoader获取布局里面的Controller的引用
        fxmlLoader.setLocation(getClass().getResource("hello.fxml"));
        Parent root = fxmlLoader.load();
        Scene scene = new Scene(root);
        // 在Application中操作Controller进行属性绑定
        Controller controller = fxmlLoader.getController();
        controller.circleLocationBind(scene);

        primaryStage.setScene(scene);
        primaryStage.show();
    }
}

public class Controller {
    @FXML
    private Circle ci;

    public void circleLocationBind(Scene scene) {
        // 获得X和Y中心点的可绑定对象,设置中心点自适应边框大小
        ci.centerXProperty().bind(scene.widthProperty().divide(2));
        ci.centerYProperty().bind(scene.heightProperty().divide(2));
    }
}

<?xml version="1.0" encoding="UTF-8"?>

<?import javafx.scene.layout.AnchorPane?>
<?import javafx.scene.shape.Circle?>


<AnchorPane fx:controller="com.aizen.javafx.fxml_03.Controller" maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" prefHeight="488.0" prefWidth="496.0" xmlns="http://javafx.com/javafx/8.0.171" xmlns:fx="http://javafx.com/fxml/1">
   <children>
      <Circle fx:id="ci" centerX="250.0" centerY="250.0" fill="DODGERBLUE" radius="100.0" stroke="BLACK" strokeType="INSIDE" />
   </children>
</AnchorPane>

效果图:
image.png

image.png


六、JavaFx多线程操作Platform.runLater

当我们执行一些耗时操作时,如加载资源。我们为了防止这些耗时操作占用JavaFx的主线程资源,下面的代码无法执行造成软件界面加载卡顿,我们通常使用JavaFx支持的多线程操作来解决这个问题。

  • 案例演示1:使用多线程技术实现点击Button按钮获取姓名数据(模拟数据库获取数据)显示在Label文字布局上。
public class Main extends Application {
    public static void main(String[] args) {
        launch(args);
    }

    @Override
    public void start(Stage primaryStage) throws Exception {
        Label label = new Label("姓名是?");
        label.setLayoutX(200);
        label.setLayoutY(350);
        Button button = new Button("点击获取姓名");
        button.setLayoutX(200);
        button.setLayoutY(400);
        button.setOnAction(event -> {
            new Thread(() -> {
                String name = "Aizen";	// 模拟数据库获取值的操作,这里直接定义
                label.setText(name);	// 更新UI控件的操作
            }).start();
        });
        AnchorPane pane = new AnchorPane();
        pane.getChildren().addAll(label, button);
        Scene scene = new Scene(pane, 500, 500);
        primaryStage.setScene(scene);
        primaryStage.show();
    }
}

如果直接这样运行,点击后会报出非法状态异常,不在JavaFx的Application线程中,更新UI控件必须在Fx Application主线程中。
image.png

想要避免这个问题,需要使用到JavaFx提供的静态方法Platform.runLater(Runnable runnable),其中参数需要一个Runnber对象,runLater方法将Runnable放在任务队列里面,在Application线程空闲的时候,会执行队列里的任务。

public class Main0 extends Application {
    public static void main(String[] args) {
        launch(args);
    }

    @Override
    public void start(Stage primaryStage) throws Exception {
        Label label = new Label("姓名是?");
        label.setLayoutX(200);
        label.setLayoutY(350);
        Button button = new Button("点击获取姓名");
        button.setLayoutX(200);
        button.setLayoutY(400);
        button.setOnAction(event -> {
            // 报错案例:
            /*new Thread(() -> {
                String name = "Aizen";  // 模拟数据库获取值的操作,这里直接定义
                label.setText(name);    // 更新UI控件的操作
            }).start();*/
            
            // 正确案例:
            // runLater方法将Runnable放在任务队列里面,在Application线程空闲的时候,会执行队列里的任务
            new Thread(() -> {
                String name = "Aizen";  // 模拟数据库获取值的操作,这里直接定义
                Platform.runLater(() -> {
                    label.setText(name);    // 更新UI控件的操作
                });
            }).start();
        });
        AnchorPane pane = new AnchorPane();
        pane.getChildren().addAll(label, button);
        Scene scene = new Scene(pane, 500, 500);
        primaryStage.setScene(scene);
        primaryStage.show();
    }
}

效果图:
image.png

image.png

其实在JavaFx很多控件的事件里面都内部调用了runLater()方法,比如下面这个案例:

  • 案例演示2:使用多线程技术实现点击Button按钮通过url加载图片,并显示图片和加载进度(模拟耗时操作)。
public class Main1 extends Application {
    public static void main(String[] args) {
        launch(args);
    }

    @Override
    public void start(Stage primaryStage) throws Exception {
        ImageView imageView = new ImageView();
        imageView.setLayoutX(100);
        imageView.setLayoutY(20);

        Label label = new Label();
        label.setLayoutX(400);
        label.setLayoutY(350);

        Button button = new Button("点击获取图片");
        button.setLayoutX(400);
        button.setLayoutY(410);

        button.setOnAction(event -> {
            // 通过url加载图片,这种操作可能会比较耗时通常会另开一个线程去执行,而不占用主线程
            Thread thread = new Thread(() -> {
                Image image = new Image("https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fsafe-img.xhscdn.com%2Fbw1%2F0a0caee4-4afd-4fbf-bcf8-8269315c7915%3FimageView2%2F2%2Fw%2F1080%2Fformat%2Fjpg&refer=http%3A%2F%2Fsafe-img.xhscdn.com&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=auto?sec=1715148006&t=f254957770daea43b9b1f4cdb6c2c987", true);
                // 该监听器的changed方法在底层已经封装了Platform.runLater()方法
                image.progressProperty().addListener(new ChangeListener<Number>() {
                    @Override
                    public void changed(ObservableValue<? extends Number> observable, Number oldValue, Number newValue) {
                        int process = (int) (newValue.doubleValue() * 100);
                        // 显示图片联网加载进度
                        label.setText("图片加载了:" + process + "%");
                    }
                });
                imageView.setImage(image);
            });

            thread.setName("线程1");
            thread.start();
        });
        AnchorPane pane = new AnchorPane();
        pane.getChildren().addAll(imageView, label, button);
        Scene scene = new Scene(pane, 950, 500);
        primaryStage.setScene(scene);
        primaryStage.show();
    }
}

效果图:
image.png

image.png


;