为什么JavaFX中的概念validate()
通常无效,加上解决线程问题
要解释您原始问题的一些问题以及为什么这个答案似乎不能直接回答它:
- 您通常不必
validate()
(即强制子组件的布局)JavaFX 中的组件,因为 JavaFX 会在每个脉冲时自动为您执行此操作(研究链接的文档以更充分地理解这一点)。
- 有时您可能想要生成一个布局传递(我猜,这在概念上有点类似于显式调用来验证 JFrame)。但是,这通常只是因为你想测量某物的尺寸,一旦它应用了 css 并且已经完全布局(这不是你想要做的)。
- 您不应该尝试在 JavaFX 应用程序线程之外更改场景图中的内容,因为这会导致异常和竞争条件(即
setVisible
,您在创建的线程中对进度指示器的调用是错误的)。
- 对于一些并发任务,最好使用内置的 JavaFX 并发机制,例如向登录服务提交登录并根据任务进度和结果与 UI 交互。
该怎么做
我认为您正在尝试做的是:
- 创建登录提示,其中登录逻辑发生在另一个线程中
- 当登录在另一个线程中进行时,有一个不确定的进度指示器旋转。
- 进度指示器仅在登录过程中显示,并且在登录尝试完成后不显示(无论成功还是失败)。
示例应用
登录服务处理异步登录过程。登录窗格中的进度指示器指示登录正在进行中。登录完成后,登录窗格将替换为登录用户的应用程序窗格。
以下行确保仅在登录服务执行时显示进度指示器:
progressIndicator.visibleProperty().bind(loginService.runningProperty());
完整代码:
import javafx.application.Application;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.concurrent.Service;
import javafx.concurrent.Task;
import javafx.geometry.Insets;
import javafx.scene.Scene;
import javafx.scene.control.*;
import javafx.scene.layout.*;
import javafx.stage.Stage;
import java.io.IOException;
public class LoginServiceApp extends Application {
private LoginService loginService = new LoginService();
@Override
public void start(Stage stage) throws IOException {
Pane loginPane = createLoginPane();
loginService.setOnSucceeded(event ->
stage.getScene().setRoot(createAppPane(stage))
);
stage.setScene(new Scene(new StackPane(loginPane)));
stage.show();
}
private Pane createLoginPane() {
GridPane credentialsGrid = new GridPane();
credentialsGrid.setHgap(10);
credentialsGrid.setVgap(10);
TextField usernameField = new TextField("frobozz");
PasswordField passwordField = new PasswordField();
credentialsGrid.addRow(0, new Label("Username"), usernameField);
credentialsGrid.addRow(1, new Label("Password"), passwordField);
Button loginButton = new Button("Login");
loginButton.setOnAction(event -> {
loginService.setUsername(usernameField.getText());
loginService.setPassword(passwordField.getText());
loginService.restart();
});
loginButton.disableProperty().bind(loginService.runningProperty());
ProgressIndicator progressIndicator = new ProgressIndicator();
progressIndicator.visibleProperty().bind(loginService.runningProperty());
progressIndicator.setPrefSize(20, 20);
HBox loginControl = new HBox(10, loginButton, progressIndicator);
VBox loginPane = new VBox(10, credentialsGrid, loginControl);
loginPane.setPadding(new Insets(10));
return loginPane;
}
private Pane createAppPane(Stage stage) {
Button logoutButton = new Button("Logout");
logoutButton.setOnAction(event -> stage.getScene().setRoot(createLoginPane()));
HBox appPane = new HBox(logoutButton);
appPane.setPadding(new Insets(10));
return appPane;
}
public static void main(String[] args) {
launch(args);
}
private static class LoginService extends Service<Void> {
private StringProperty username = new SimpleStringProperty(this, "username");
public final void setUsername(String value) { username.set(value); }
public final String getUsername() { return username.get(); }
public final StringProperty usernameProperty() { return username; }
private StringProperty password = new SimpleStringProperty(this, "password");
public final void setPassword(String value) { password.set(value); }
public final String getPassword() { return password.get(); }
public final StringProperty passwordProperty() { return password; }
@Override
protected Task<Void> createTask() {
final String _username = getUsername();
final String _password = getPassword();
return new Task<Void>() {
@Override
protected Void call() throws Exception {
// Simulate a long call to a login service,
// using the username and password we saved when the task was created.
// If login fails, an exception can be raised to report it and the
// caller starting the service can monitor setOnException to handle it.
// Or the Task could return a result value instead of void and the caller
// could monitor the value property of the task in addition to the exception handler.
Thread.sleep(1_000);
return null;
}
};
}
}
}