JavaFX 将重复视图同步到同一控制器(FXML 和 MVC)
JavaFX Sync Duplicate Views to the Same Controller (FXML & MVC)
下面是一个说明问题的小应用程序:
ButtonPanel.fxml
<ScrollPane fx:controller="ButtonPanelController">
<VBox>
<Button fx:id="myButton" text="Click Me" onAction="#buttonClickedAction" />
</VBox>
</ScrollPane>
ButtonPanelController.java
public class ButtonPanelController {
@FXML
Button myButton;
boolean isRed = false;
public void buttonClickedAction(ActionEvent event) {
if(isRed) {
myButton.setStyle("");
} else {
myButton.setStyle("-fx-background-color: red");
}
isRed = !isRed;
}
}
TestApp.java
public class TestApp extends Application {
ButtonPanelController buttonController;
@Override
public void start(Stage stage) throws Exception {
// 1st Stage
stage.setTitle("1st Stage");
stage.setWidth(200);
stage.setHeight(200);
stage.setResizable(false);
// Load FXML
FXMLLoader loader = new FXMLLoader(
ButtonPanelController.class.getResource("ButtonPanel.fxml"));
Parent root = (Parent) loader.load();
// Grab the instance of ButtonPanelController
buttonController = loader.getController();
// Show 1st Scene
Scene scene = new Scene(root);
stage.setScene(scene);
stage.show();
// 2nd Stage
Stage stage2 = new Stage();
stage2.setTitle("2nd Stage");
stage2.setWidth(200);
stage2.setHeight(200);
stage2.setResizable(false);
/* Override the ControllerFactory callback to use
* the stored instance of ButtonPanelController
* instead of creating a new one.
*/
Callback<Class<?>, Object> controllerFactory = type -> {
if(type == ButtonPanelController.class) {
return buttonController;
} else {
try {
return type.newInstance();
} catch(Exception e) {
throw new RuntimeException(e);
}
}
};
// Load FXML
FXMLLoader loader2 = new FXMLLoader(
ButtonPanelController.class.getResource("ButtonPanel.fxml"));
// Set the ControllerFactory before the load takes place
loader2.setControllerFactory(controllerFactory);
Parent root2 = (Parent) loader2.load();
// Show 2nd Scene
Scene scene2 = new Scene(root2);
stage2.setScene(scene2);
stage2.show();
}
public static void main(String[] args) {
launch(args);
}
}
基本上,我有一个 FXML 用于两个单独的场景,这两个场景可能同时在屏幕上处于活动状态,也可能不处于活动状态。一个实际的例子是将内容停靠到侧面板加上一个按钮,该按钮可以在单独的 window 中打开相同的内容,可以是 dragged/resized/etc.
我试图实现的目标是保持视图同步(更改其中一个视图会影响另一个视图)。
我可以通过回调将两个视图指向同一个控制器,但是我 运行 现在遇到的问题是 UI 更改仅反映在第二个场景中。两个视图都与控制器对话,但控制器只与第二个场景对话。我假设在通过 FXMLLoader 加载时,JavaFX 的 MVC 或 IOC 实现 link 将控制器以某种 1:1 关系连接到视图。
我很清楚尝试 link 对 1 个控制器的两个视图是错误的 MVC 实践,但是我想避免必须实现一个单独的 FXML 和控制器,它们实际上是相同的。
是否可以实现我上面列出的这种同步?
如果我需要创建一个单独的控制器,确保两个 UI 同步(甚至连边栏移动)的最佳方法是什么?
提前致谢!
-史蒂夫
您的代码不起作用的原因是 FXMLLoader
将对 FXML 中具有 fx:id
属性的元素的引用注入到具有匹配名称的控制器中的字段中。因此,当您第一次加载 FXML 文件时,FXMLLoader
将字段 myButton
设置为对它在加载 FXML 时创建的按钮的引用。由于您在第二次加载 FXML 时使用完全相同的控制器实例,因此 FXMLLoader
现在将相同的字段(在同一控制器实例中)设置为对再次加载 FXML 文件时创建的按钮的引用.换句话说,buttonController.myButton
现在指的是创建的第二个按钮,而不是第一个。因此,当您调用 myButton.setStyle(...)
时,它会更新第二个按钮的样式。
基本上,您总是希望每个视图实例有一个控制器实例。您需要的是两个控制器访问相同的共享状态。
创建一个存储数据的模型class。在 MVC 架构中,View 观察模型并在模型中的数据发生变化时进行更新。控制器对用户与视图的交互做出反应并更新模型。
(可以说,FXML 给你更多的是 MVP 架构,这是相似的。这也有变体,但一般情况下,演示者会观察模型并在模型中的数据发生变化时更新视图,以及更新模型以响应用户交互。)
因此您的模型可能如下所示:
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
public class Model {
private final BooleanProperty red = new SimpleBooleanProperty();
public final BooleanProperty redProperty() {
return this.red;
}
public final boolean isRed() {
return this.redProperty().get();
}
public final void setRed(final boolean red) {
this.redProperty().set(red);
}
public void toggleRed() {
setRed(! isRed() );
}
}
你的ButtonPanel.fxml没有改变:
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.control.ScrollPane?>
<?import javafx.scene.layout.VBox?>
<?import javafx.scene.control.Button?>
<ScrollPane xmlns:fx="http://javafx.com/fxml/1" fx:controller="ButtonPanelController">
<VBox >
<Button fx:id="myButton" text="Click Me" onAction="#buttonClickedAction" />
</VBox>
</ScrollPane>
您的控制器引用了该模型。它可以在模型属性上使用绑定或侦听器来更新 UI,并且处理程序方法只更新模型:
import javafx.beans.binding.Bindings;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.scene.control.Button;
public class ButtonPanelController {
@FXML
Button myButton;
boolean isRed = false;
private Model model ;
public ButtonPanelController(Model model) {
this.model = model ;
}
public void initialize() {
myButton.styleProperty().bind(Bindings.
when(model.redProperty()).
then("-fx-background-color: red;").
otherwise("")
);
}
public void buttonClickedAction(ActionEvent event) {
model.toggleRed();
}
}
最后,您保持所有内容同步,因为视图是同一模型的视图。换句话说,您只需创建一个模型并将其引用传递给两个控制器。由于我将模型作为控制器中的构造函数参数(这很好,因为您知道实例一创建就拥有一个模型),我们需要一个控制器工厂来创建控制器实例:
import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.stage.Stage;
import javafx.util.Callback;
public class TestApp extends Application {
@Override
public void start(Stage stage) throws Exception {
// 1st Stage
stage.setTitle("1st Stage");
stage.setWidth(200);
stage.setHeight(200);
stage.setResizable(false);
// The one and only model we will use for both views and controllers:
Model model = new Model();
/* Override the ControllerFactory callback to create
* the controller using the model:
*/
Callback<Class<?>, Object> controllerFactory = type -> {
if(type == ButtonPanelController.class) {
return new ButtonPanelController(model);
} else {
try {
return type.newInstance();
} catch(Exception e) {
throw new RuntimeException(e);
}
}
};
// Load FXML
FXMLLoader loader = new FXMLLoader(
ButtonPanelController.class.getResource("ButtonPanel.fxml"));
loader.setControllerFactory(controllerFactory);
Parent root = (Parent) loader.load();
// Show 1st Scene
Scene scene = new Scene(root);
stage.setScene(scene);
stage.show();
// 2nd Stage
Stage stage2 = new Stage();
stage2.setTitle("2nd Stage");
stage2.setWidth(200);
stage2.setHeight(200);
stage2.setResizable(false);
// Load FXML
FXMLLoader loader2 = new FXMLLoader(
ButtonPanelController.class.getResource("ButtonPanel.fxml"));
// Set the ControllerFactory before the load takes place
loader2.setControllerFactory(controllerFactory);
Parent root2 = (Parent) loader2.load();
// Show 2nd Scene
Scene scene2 = new Scene(root2);
stage2.setScene(scene2);
stage2.show();
}
public static void main(String[] args) {
launch(args);
}
}
下面是一个说明问题的小应用程序:
ButtonPanel.fxml
<ScrollPane fx:controller="ButtonPanelController">
<VBox>
<Button fx:id="myButton" text="Click Me" onAction="#buttonClickedAction" />
</VBox>
</ScrollPane>
ButtonPanelController.java
public class ButtonPanelController {
@FXML
Button myButton;
boolean isRed = false;
public void buttonClickedAction(ActionEvent event) {
if(isRed) {
myButton.setStyle("");
} else {
myButton.setStyle("-fx-background-color: red");
}
isRed = !isRed;
}
}
TestApp.java
public class TestApp extends Application {
ButtonPanelController buttonController;
@Override
public void start(Stage stage) throws Exception {
// 1st Stage
stage.setTitle("1st Stage");
stage.setWidth(200);
stage.setHeight(200);
stage.setResizable(false);
// Load FXML
FXMLLoader loader = new FXMLLoader(
ButtonPanelController.class.getResource("ButtonPanel.fxml"));
Parent root = (Parent) loader.load();
// Grab the instance of ButtonPanelController
buttonController = loader.getController();
// Show 1st Scene
Scene scene = new Scene(root);
stage.setScene(scene);
stage.show();
// 2nd Stage
Stage stage2 = new Stage();
stage2.setTitle("2nd Stage");
stage2.setWidth(200);
stage2.setHeight(200);
stage2.setResizable(false);
/* Override the ControllerFactory callback to use
* the stored instance of ButtonPanelController
* instead of creating a new one.
*/
Callback<Class<?>, Object> controllerFactory = type -> {
if(type == ButtonPanelController.class) {
return buttonController;
} else {
try {
return type.newInstance();
} catch(Exception e) {
throw new RuntimeException(e);
}
}
};
// Load FXML
FXMLLoader loader2 = new FXMLLoader(
ButtonPanelController.class.getResource("ButtonPanel.fxml"));
// Set the ControllerFactory before the load takes place
loader2.setControllerFactory(controllerFactory);
Parent root2 = (Parent) loader2.load();
// Show 2nd Scene
Scene scene2 = new Scene(root2);
stage2.setScene(scene2);
stage2.show();
}
public static void main(String[] args) {
launch(args);
}
}
基本上,我有一个 FXML 用于两个单独的场景,这两个场景可能同时在屏幕上处于活动状态,也可能不处于活动状态。一个实际的例子是将内容停靠到侧面板加上一个按钮,该按钮可以在单独的 window 中打开相同的内容,可以是 dragged/resized/etc.
我试图实现的目标是保持视图同步(更改其中一个视图会影响另一个视图)。
我可以通过回调将两个视图指向同一个控制器,但是我 运行 现在遇到的问题是 UI 更改仅反映在第二个场景中。两个视图都与控制器对话,但控制器只与第二个场景对话。我假设在通过 FXMLLoader 加载时,JavaFX 的 MVC 或 IOC 实现 link 将控制器以某种 1:1 关系连接到视图。
我很清楚尝试 link 对 1 个控制器的两个视图是错误的 MVC 实践,但是我想避免必须实现一个单独的 FXML 和控制器,它们实际上是相同的。
是否可以实现我上面列出的这种同步?
如果我需要创建一个单独的控制器,确保两个 UI 同步(甚至连边栏移动)的最佳方法是什么?
提前致谢!
-史蒂夫
您的代码不起作用的原因是 FXMLLoader
将对 FXML 中具有 fx:id
属性的元素的引用注入到具有匹配名称的控制器中的字段中。因此,当您第一次加载 FXML 文件时,FXMLLoader
将字段 myButton
设置为对它在加载 FXML 时创建的按钮的引用。由于您在第二次加载 FXML 时使用完全相同的控制器实例,因此 FXMLLoader
现在将相同的字段(在同一控制器实例中)设置为对再次加载 FXML 文件时创建的按钮的引用.换句话说,buttonController.myButton
现在指的是创建的第二个按钮,而不是第一个。因此,当您调用 myButton.setStyle(...)
时,它会更新第二个按钮的样式。
基本上,您总是希望每个视图实例有一个控制器实例。您需要的是两个控制器访问相同的共享状态。
创建一个存储数据的模型class。在 MVC 架构中,View 观察模型并在模型中的数据发生变化时进行更新。控制器对用户与视图的交互做出反应并更新模型。
(可以说,FXML 给你更多的是 MVP 架构,这是相似的。这也有变体,但一般情况下,演示者会观察模型并在模型中的数据发生变化时更新视图,以及更新模型以响应用户交互。)
因此您的模型可能如下所示:
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
public class Model {
private final BooleanProperty red = new SimpleBooleanProperty();
public final BooleanProperty redProperty() {
return this.red;
}
public final boolean isRed() {
return this.redProperty().get();
}
public final void setRed(final boolean red) {
this.redProperty().set(red);
}
public void toggleRed() {
setRed(! isRed() );
}
}
你的ButtonPanel.fxml没有改变:
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.control.ScrollPane?>
<?import javafx.scene.layout.VBox?>
<?import javafx.scene.control.Button?>
<ScrollPane xmlns:fx="http://javafx.com/fxml/1" fx:controller="ButtonPanelController">
<VBox >
<Button fx:id="myButton" text="Click Me" onAction="#buttonClickedAction" />
</VBox>
</ScrollPane>
您的控制器引用了该模型。它可以在模型属性上使用绑定或侦听器来更新 UI,并且处理程序方法只更新模型:
import javafx.beans.binding.Bindings;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.scene.control.Button;
public class ButtonPanelController {
@FXML
Button myButton;
boolean isRed = false;
private Model model ;
public ButtonPanelController(Model model) {
this.model = model ;
}
public void initialize() {
myButton.styleProperty().bind(Bindings.
when(model.redProperty()).
then("-fx-background-color: red;").
otherwise("")
);
}
public void buttonClickedAction(ActionEvent event) {
model.toggleRed();
}
}
最后,您保持所有内容同步,因为视图是同一模型的视图。换句话说,您只需创建一个模型并将其引用传递给两个控制器。由于我将模型作为控制器中的构造函数参数(这很好,因为您知道实例一创建就拥有一个模型),我们需要一个控制器工厂来创建控制器实例:
import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.stage.Stage;
import javafx.util.Callback;
public class TestApp extends Application {
@Override
public void start(Stage stage) throws Exception {
// 1st Stage
stage.setTitle("1st Stage");
stage.setWidth(200);
stage.setHeight(200);
stage.setResizable(false);
// The one and only model we will use for both views and controllers:
Model model = new Model();
/* Override the ControllerFactory callback to create
* the controller using the model:
*/
Callback<Class<?>, Object> controllerFactory = type -> {
if(type == ButtonPanelController.class) {
return new ButtonPanelController(model);
} else {
try {
return type.newInstance();
} catch(Exception e) {
throw new RuntimeException(e);
}
}
};
// Load FXML
FXMLLoader loader = new FXMLLoader(
ButtonPanelController.class.getResource("ButtonPanel.fxml"));
loader.setControllerFactory(controllerFactory);
Parent root = (Parent) loader.load();
// Show 1st Scene
Scene scene = new Scene(root);
stage.setScene(scene);
stage.show();
// 2nd Stage
Stage stage2 = new Stage();
stage2.setTitle("2nd Stage");
stage2.setWidth(200);
stage2.setHeight(200);
stage2.setResizable(false);
// Load FXML
FXMLLoader loader2 = new FXMLLoader(
ButtonPanelController.class.getResource("ButtonPanel.fxml"));
// Set the ControllerFactory before the load takes place
loader2.setControllerFactory(controllerFactory);
Parent root2 = (Parent) loader2.load();
// Show 2nd Scene
Scene scene2 = new Scene(root2);
stage2.setScene(scene2);
stage2.show();
}
public static void main(String[] args) {
launch(args);
}
}