如果使用了javaFX的FXML开发方式,那么就会非常明确的感受到MVC模式的气息,在FX程序运行的时候,我们的FX线程是保持在主线程里的,但是当我们在处理事件的时候想更新界面中元素的状态时,经常会遇到错误提示:在非FX线程中更新了UI。这个时候该如何解决呢?
解决方法有两种:1、Platform.runLater(()->{........});方式 2、使用Task方式。
1、 Platform.runLater方式
我一开始是使用Platform.runLater方式解决的,但是很多时候我们更新的UI内容很可能是controller里面处理的变量,而Platform.runLater不允许使用非final的变量,所以问题就来了,你想在UI上表现出来你的处理进度的时候这种方式就不行。
@FXML
private Text progressText;//界面上用来显示处理进度的Text
@FXML
private Button startBtn;//用来启动任务处理的按钮
@FXML
private void initialize(){
startBtn.setOnAction(e->startHandler());//注册按钮处理事件
}
private void startHandler(){
for(int a=0;a<100;a++){
Platform.runLater(()->{
progressText.setText("处理进度("+(a+1)+"/100):"+a);//其实这里就已经报错了
});
Thread.sleep(300)
}
}
如果按照以上的方式运行,除非变量a是final的,否则编译无法通过,但是如果a是final的,我的循环该怎么执行?
或许你可以想到变一下,把Platform.runLater放在外面,for放在里面,如果你想尝试,可以自行尝试,我可以告诉你的是,你可以编译通过了,而且看起来也没啥问了,但是啃爹的就是你会看到界面上的变化是那个Text的内容直接变为for循环最后一次循环后的结果,而不会看到它随着循环的执行而动态的变!
2、使用Task方式(正确的姿势)
其实使用Task的方式才是解决这种问题的根本,至少我学到现在,对我来说是这样的。那么下面例子,看看Task方式如何解决。
FXMLDocumentController.java
public class FXMLDocumentController implements Initializable {
@FXML
private ProgressBar process1;
@FXML
private ProgressIndicator process2;
@FXML
private Text process3;
@FXML
private Button bt1;
//按钮事件处理函数
private void handleButtonAction(ActionEvent event) {
Service<String> service=new Service<String>() {
@Override
protected Task<String> createTask() {
return new Task<String>() {
@Override
protected String call() throws Exception {
for(int a=1;a<=100;a++){
//更新service的value属性
updateValue("process:"+a+"%");
//更新service的progress属性
updateProgress(a, 100d);
Thread.sleep(100);
}
return "success";
}
};
}
};
//绑定process1的progress属性为service的progress属性
process1.progressProperty().bind(service.progressProperty());
//同上
process2.progressProperty().bind(service.progressProperty());
//绑定process3的text属性为service的text属性
process3.textProperty().bind(service.valueProperty());
//任务完成时会调用
service.setOnSucceeded((WorkerStateEvent event) -> {
System.out.println("任务处理完成!");
});
//启动任务start()一定是最后才调用的
service.start();
}
@Override
public void initialize(URL url, ResourceBundle rb) {
bt1.setOnAction(e->handleButtonAction(e));//按钮注册事件处理函数
}
}
在循环中更新了progress属性以及value属性,所以所有绑定了这两个属性的元素也会被跟着更新掉。因为一开始声明的Service<String>,所以value值就是个String,可以直接给process3使用!
FXMLDocument.fxml
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.control.Button?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.control.ProgressBar?>
<?import javafx.scene.control.ProgressIndicator?>
<?import javafx.scene.layout.AnchorPane?>
<?import javafx.scene.text.Text?>
<AnchorPane id="AnchorPane" prefHeight="400.0" prefWidth="500.0" xmlns:fx="http://javafx.com/fxml/1" xmlns="http://javafx.com/javafx/8.0.65" fx:controller="proces.FXMLDocumentController">
<children>
<Label fx:id="label" layoutX="126" layoutY="120" minHeight="16" minWidth="69" />
<ProgressBar fx:id="process1" layoutX="37.0" layoutY="35.0" prefHeight="23.0" prefWidth="393.0" progress="0.0" />
<ProgressIndicator fx:id="process2" layoutX="37.0" layoutY="84.0" prefHeight="72.0" prefWidth="88.0" progress="0.0" />
<Text fx:id="process3" layoutX="37.0" layoutY="181.0" strokeType="OUTSIDE" strokeWidth="0.0" text="Text" wrappingWidth="401.6513671875" />
<Button fx:id="bt1" layoutX="226.0" layoutY="264.0" mnemonicParsing="false" text="Button" />
</children>
</AnchorPane>
Proces.java
package proces;
import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.stage.Stage;
/**
*
* @author Administrator
*/
public class Proces extends Application {
@Override
public void start(Stage stage) throws Exception {
Parent root = FXMLLoader.load(getClass().getResource("FXMLDocument.fxml"));
Scene scene = new Scene(root);
stage.setScene(scene);
stage.show();
}
/**
* @param args the command line arguments
*/
public static void main(String[] args) {
launch(args);
}
}
目录结构:

看到最后你发现了吧,想要在非FX线程中更新UI的话,建议使用Task()方式来做,不敢保证这就是最好的解决方案,但是至少是我目前学到的最好的解决方案,说不定日后会发现更多的解决方法!