JavaFx的那些坑--学习JavaFX过程中遇到的坑
最近在看JavaFX,为了早日进入状态,没有从基础看起,而是直接从网上的例子教程开始编写代码。期间遇到不少的坑,对界面开发也有不少心得,因此在这篇文章中记录下来,持续更新。
我的开发环境是
- IntelliJ 2017
- JDKD1.8.0_152
0.JavaFX最佳的学习资源
其实Oracle已经把最佳的学习资源提供给大家了,就是jdk自带的sample中的demo,尤其是Ensemble8这个demo,几乎包罗万象。最新的JDK8的下载地址在这里(我用的是8u152):
页面中除了JDK以外,还有一个可以下载的包就是“ Java SE Development Kit 8u152 Demos and Samples Downloads”,下载下来后,jdk1.8.0_152\demo\javafx_samples\Ensemble8.jar就是一个可以执行的jar包。其源代码就在jdk1.8.0_152\demo\javafx_samples\src\Ensemble8中。
设置好JDK,直接双击jar包,运行起来是这个样子的:

所有控件的使用方法,以及对应的源代码都可以在程序中直接查看,还支持app内搜索,以及直接作为浏览器连接Oracle网站查看相关文档。作为一个demo,它已经做到了最好。
Ensemble8的源代码有两种管理方式,一种是Gradle,一种是NetBean的project。因为我使用的是IDEA,因此直接new一个新的project,选择Gradle模式,导入源代码包即可运行。需要任何控件或者特性的使用方法,直接在这个demo中搜索关键字(例如tree),然后运行相应的demo就可以,还能查看源代码、相关文档和相关的示例,简直是JavaFX的良心之作。
1.JDK8必须是最新版才能支持所有的功能
例如Alert对话框必须在JDK8的比较新的版本中才支持,较低的版本你会发现没有这个类,例子如下:
Alert alert = new Alert(Alert.AlertType.INFORMATION);
alert.setTitle("信息");
alert.setHeaderText("header");
alert.setContentText("message");
alert.show();
刚开始我的JDK是1.8.0_20,就没有Alert类。
2.对于MVC模式的理解
M就是各种POJO类,也可以认为是bean类,这些类的属性可以bind另外的属性,例如控件中的属性,这样就可以和控件上的显示联动;
V就是fxml文档,可以使用JavaFX Scene Builder 2.0工具来实现可视化的编写,这个工具要单独下载,然后和IDEA关联起来使用;
C就是Controller类,这个类中的方法都是fxml文件中定义的控件方法的实现。
3.JavaFX Scene Builder 2.0的下载、设置和使用
Oracle把这个工具的下载地址藏得很好,我也不知道为什么,它在这里:
JavaFX Scene Builder 1.x Archive
下载后直接在系统中安装,然后在IntelliJ 中设置:
Settings-->Languages & frameworks-->JavaFx-->Path to SceneBuilder
设置为JavaFX Scene Builder 2.0的exe工具地址。
在IntelliJ工程中,选中fxml文件,右键选中open in SceneBuilder就可以打开使用了。确实非常好用,强烈推荐。
4.在Application类中获取Controller
Controller类用来处理界面的各种控件属性,各种响应函数,因此有些Controller中的代码希望在Application类中显式执行,因此必须在Application类中获取Controller类的实例。在Application类中获取Controller的方法,一般来说start函数是这样的:
Parent root = FXMLLoader.load(getClass().getResource("MySecene.fxml"));
primaryStage.setTitle("My Application");
primaryStage.setScene(new Scene(root, 600, 400));
primaryStage.show();
这样来获取Controller:
//FXMLLoader必须使用参数初始化,否则getController会失败
FXMLLoader loader = new FXMLLoader(getClass().getResource("MySecene.fxml"));
Parent root = loader.load();
//这个方法必须在load方法之后调用
MySeceneController controller = loader.getController();
primaryStage.setTitle("My Application");
primaryStage.setScene(new Scene(root, 600, 400));
primaryStage.show();
5.Controller的initialize方法
可以在Controller类中添加一个initialize方法,这个方法则会自动在构造函数之后调用,例如:
public void initialize() {
System.out.println("MySeceneController initialize");
}
public MySeceneController() {
System.out.println("constructor");
}
这样在程序启动时,会先调用构造函数,然后调用initialize方法。
6.bind属性
将一个TextField的文本属性bind到Stage的Title属性,代码如下(从不同的类中聚合到此处):
TextField txtBind;
SimpleStringProperty bindProperty;
//初始化控件等。。。
bindProperty.bind(txtBind.textProperty());
stage.titleProperty().bind(bindProperty);
然后在TextField填写文本时,Stage标题会随之改变。
7.在非UI线程中更新界面的问题(例如进度条完成后弹出对话框)
一个经典场景,进度条完成后,弹出一个对话框。那么,首先需要绑定一个SimpleDoubleProperty到进度条的progressProperty()属性上,然后给进度条增加一个事件监听ChangeListener。当进度条完成时,希望弹出一个对话框。
如果直接在ChangeListener的实现函数中这样写:
progressBar.progressProperty().addListener(new ChangeListener<Number>() {
@Override
public void changed(ObservableValue<? extends Number> observable, Number oldValue, Number newValue) {
System.out.println(newValue);
if (newValue.intValue() >= 1.0) {
Alert alert = new Alert(Alert.AlertType.INFORMATION);
alert.setTitle("完成");
alert.setHeaderText("header");
alert.setContentText("进度完成");
alert.show();
}
}
});
那么就是这样报错:
Exception in thread "Thread-5" java.lang.IllegalStateException: Not on FX application thread; currentThread = Thread-5
原因就是在非UI线程中调用了界面相关方法。
解决方法就是利用Platform.runLater方法:
progressBar.progressProperty().addListener(new ChangeListener<Number>() {
@Override
public void changed(ObservableValue<? extends Number> observable, Number oldValue, Number newValue) {
System.out.println(newValue);
if (newValue.intValue() >= 1.0) {
runLaterProgressOver();
}
}
});
private void runLaterProgressOver() {
Platform.runLater(new Runnable() {
@Override
public void run() {
//更新JavaFX的主线程的代码放在此处
Alert alert = new Alert(Alert.AlertType.INFORMATION);
alert.setTitle("完成");
alert.setHeaderText("header");
alert.setContentText("进度完成");
alert.show();
}
});
}
事实上各种界面语言都是这样的,不过解决方法各有不同而已。例如在Android中就是用这些方法:
为了解决这个问题,Android提供了一些方法,从其他线程访问UI线程:
总之,我们也要与Android遵循同样的守则
1.不要阻塞UI线程。
2.不要在UI线程之外访问 UI 相关代码。
8.定制DatePicker
要让DatePicker返回选择的日期值,办法是用一个SimpleStringProperty来bind它的getEditor().textProperty()的值(直接在DatePicker中找相关属性没有找到)。
定制DatePicker的单元格DateCell,方法是设置setDayCellFactory方法,其参数是一个Callback<DatePicker, DateCell>的实例。
具体代码如下:
public class DatePickerMain extends Application {
private SimpleStringProperty dateString;
public static void main(String[] args) {
launch(args);
}
@Override
public void start(Stage stage) {
VBox vbox = new VBox(20);
Scene scene = new Scene(vbox, 400, 400);
stage.setScene(scene);
final DatePicker startDatePicker = new DatePicker();
DatePicker endDatePicker = new DatePicker();
startDatePicker.setValue(LocalDate.now());
//为DateCell添加功能
final Callback<DatePicker, DateCell> dayCellFactory = new Callback<DatePicker, DateCell>() {
@Override
public DateCell call(final DatePicker datePicker) {
return new DateCell() {
@Override
public void updateItem(LocalDate item, boolean empty) {
super.updateItem(item, empty);
long p = ChronoUnit.DAYS.between(startDatePicker.getValue(), item);
setTooltip(new Tooltip("You're about to stay for " + p + " days"));
}
};
}
};
endDatePicker.setDayCellFactory(dayCellFactory);
endDatePicker.setValue(startDatePicker.getValue().plusDays(1));
//将endDatePicker的Editor的textProperty属性bind到dateString
dateString = new SimpleStringProperty();
dateString.bindBidirectional(endDatePicker.getEditor().textProperty());
endDatePicker.getEditor().textProperty().addListener(new ChangeListener<String>() {
@Override
public void changed(ObservableValue<? extends String> observable, String oldValue, String newValue) {
//在UI线程中弹出对话框
runLaterDate(dateString);
}
});
vbox.getChildren().add
