通过 FXML 在 JavaFX 中管理场景切换(性能问题)
Managing scene switching in JavaFX via FXML (Performance Question)
我正在使用 JavaFX 和 FXML 加载的各种场景。所以我有了写一个处理场景切换的管理器的想法。
到目前为止一切正常,但我不确定这是否是一个好的实现。
import java.io.IOException;
import javafx.fxml.FXMLLoader;
import javafx.scene.Scene;
import javafx.scene.layout.Pane;
import javafx.stage.Stage;
public class SceneManager {
private static final String[] fxmlFiles = {"../gui/MainWindow.fxml", "../gui/NewGameWindow.fxml"};
private static SceneManager instance = null;
private static Stage rootStage = null;
private FXMLLoader[] loadedFxml;
private Pane[] loadedPanes;
private Scene[] scenes;
public enum States {
MAIN_MENU, NEW_GAME;
}
private SceneManager() {
try {
this.loadedFxml = new FXMLLoader[States.values().length];
this.loadedPanes = new Pane[States.values().length];
this.scenes = new Scene[States.values().length];
for(int i = 0; i < fxmlFiles.length; i++) {
loadedFxml[i] = new FXMLLoader(getClass().getResource(fxmlFiles[i]));
loadedPanes[i] = loadedFxml[i].load();
scenes[i] = new Scene(loadedPanes[i]);
}
rootStage.setScene(scenes[0]);
rootStage.setResizable(false);
rootStage.show();
} catch(IOException e) {
e.printStackTrace();
}
}
public static SceneManager getInstance() {
if(instance == null) {
instance = new SceneManager();
}
return instance;
}
public static void setUp(Stage stage) {
SceneManager.rootStage = stage;
}
public void switchScene(States state) {
rootStage.setScene(scenes[state.ordinal()]);
}
}
所以我打算做的是,通过加载程序加载 FXML,将其分配给窗格,创建所有场景。
然后我将一个场景设置为它的起始场景,然后通过控制器中的 getInstance().switchScene() 方法完成剩下的工作。
效果很好,但我不确定这是否是一个好方法。
恕我直言,由于以下几个原因,实施非常糟糕:
未正确实现单例模式
单例模式用于通过 static
方法访问包含相关 data/functionality 的实例,但此实例应包含所有相关数据作为实例字段。
rootStage
和 setUp
虽然是 static
。
您将数据存储在不再需要的字段中
您永远不会在循环外读取 loadedFxml
或 loadedPanes
。您可以改为在循环体中创建局部变量。
一切都立即加载
对于几个小场景,这可能没有太大区别,但随着您添加越来越多的场景,这将增加启动时间。考虑延迟加载场景。
场景数据保存在不同的数据结构中
问题不大,但它使 class 更难维护。 enum
存储用于 create/access 场景的一部分数据 fxmlFiles
包含另一半。每次 add/remove 一个场景时,您都需要更新代码的两个部分。在这种情况下,最好将 url 数据存储在枚举本身中:
public enum States {
MAIN_MENU("../gui/MainWindow.fxml"), NEW_GAME("../gui/NewGameWindow.fxml");
private final url;
States(String url) {
this.url = url;
}
}
for(States state : States.values()) {
FXMLLoader loader = new FXMLLoader(getClass().getResource(state.url));
...
}
请注意,如果您将程序打包为 jar,您在 url 中使用的 ..
将停止工作。
但是首先使用 enum
是一个有问题的决定。这样,如果不重新编译,您将无法 add/remove 场景。
似乎完全没有必要使用 static
最好尽可能避免使用 static
。 (参见 Why are static variables considered evil?)。
如果我假设你只使用它加载的场景中的 SceneManager
class(以及显示初始场景)是正确的,那么不难通过 SceneManager
场景控制器的实例,以避免在那些 class 中使用 SceneManager.getInstance
的需要(参见 Passing Parameters JavaFX FXML):
控制器超级class
public class BaseController {
protected SceneManager sceneManager;
void setSceneManager(SceneManager sceneManager) { // if SceneManager and BaseController are in different packages, change visibility
this.sceneManager = sceneManager;
}
}
FXMLLoader loader = ...
Pane pane = loader.load();
BaseController controller = loader.getController();
controller.setSceneManager(this);
为了简单起见,使用 url 作为场景的标识符,您可以改进实现:
public class SceneManager {
private final Stage rootStage;
public SceneManager(Stage rootStage) {
if (rootStage == null) {
throw new IllegalArgumentException();
}
this.rootStage = rootStage;
}
private final Map<String, Scene> scenes = new HashMap<>();
public void switchScene(String url) {
Scene scene = scenes.computeIfAbsent(url, u -> {
FXMLLoader loader = new FXMLLoader(getClass().getResource(u));
try {
Pane p = loader.load();
BaseController controller = loader.getController();
controller.setSceneManager(this);
return new Scene(p);
} catch (IOException ex) {
throw new RuntimeException(ex);
}
});
rootStage.setScene(scene);
}
}
这让您可以
- 为不同的阶段创建不同的经理
- 首先在需要时加载场景
- 动态添加更多场景
- 防止调用
switchScene
但阶段为 null
的状态
- 将控制器 class 中的
SceneManager
的访问简化为 sceneManager.switchScene
SceneManager
可能在程序完成之前可用于垃圾回收,因为没有对它的静态引用。
我正在使用 JavaFX 和 FXML 加载的各种场景。所以我有了写一个处理场景切换的管理器的想法。
到目前为止一切正常,但我不确定这是否是一个好的实现。
import java.io.IOException;
import javafx.fxml.FXMLLoader;
import javafx.scene.Scene;
import javafx.scene.layout.Pane;
import javafx.stage.Stage;
public class SceneManager {
private static final String[] fxmlFiles = {"../gui/MainWindow.fxml", "../gui/NewGameWindow.fxml"};
private static SceneManager instance = null;
private static Stage rootStage = null;
private FXMLLoader[] loadedFxml;
private Pane[] loadedPanes;
private Scene[] scenes;
public enum States {
MAIN_MENU, NEW_GAME;
}
private SceneManager() {
try {
this.loadedFxml = new FXMLLoader[States.values().length];
this.loadedPanes = new Pane[States.values().length];
this.scenes = new Scene[States.values().length];
for(int i = 0; i < fxmlFiles.length; i++) {
loadedFxml[i] = new FXMLLoader(getClass().getResource(fxmlFiles[i]));
loadedPanes[i] = loadedFxml[i].load();
scenes[i] = new Scene(loadedPanes[i]);
}
rootStage.setScene(scenes[0]);
rootStage.setResizable(false);
rootStage.show();
} catch(IOException e) {
e.printStackTrace();
}
}
public static SceneManager getInstance() {
if(instance == null) {
instance = new SceneManager();
}
return instance;
}
public static void setUp(Stage stage) {
SceneManager.rootStage = stage;
}
public void switchScene(States state) {
rootStage.setScene(scenes[state.ordinal()]);
}
}
所以我打算做的是,通过加载程序加载 FXML,将其分配给窗格,创建所有场景。
然后我将一个场景设置为它的起始场景,然后通过控制器中的 getInstance().switchScene() 方法完成剩下的工作。
效果很好,但我不确定这是否是一个好方法。
恕我直言,由于以下几个原因,实施非常糟糕:
未正确实现单例模式
单例模式用于通过 static
方法访问包含相关 data/functionality 的实例,但此实例应包含所有相关数据作为实例字段。
rootStage
和 setUp
虽然是 static
。
您将数据存储在不再需要的字段中
您永远不会在循环外读取 loadedFxml
或 loadedPanes
。您可以改为在循环体中创建局部变量。
一切都立即加载
对于几个小场景,这可能没有太大区别,但随着您添加越来越多的场景,这将增加启动时间。考虑延迟加载场景。
场景数据保存在不同的数据结构中
问题不大,但它使 class 更难维护。 enum
存储用于 create/access 场景的一部分数据 fxmlFiles
包含另一半。每次 add/remove 一个场景时,您都需要更新代码的两个部分。在这种情况下,最好将 url 数据存储在枚举本身中:
public enum States {
MAIN_MENU("../gui/MainWindow.fxml"), NEW_GAME("../gui/NewGameWindow.fxml");
private final url;
States(String url) {
this.url = url;
}
}
for(States state : States.values()) {
FXMLLoader loader = new FXMLLoader(getClass().getResource(state.url));
...
}
请注意,如果您将程序打包为 jar,您在 url 中使用的 ..
将停止工作。
但是首先使用 enum
是一个有问题的决定。这样,如果不重新编译,您将无法 add/remove 场景。
似乎完全没有必要使用 static
最好尽可能避免使用 static
。 (参见 Why are static variables considered evil?)。
如果我假设你只使用它加载的场景中的 SceneManager
class(以及显示初始场景)是正确的,那么不难通过 SceneManager
场景控制器的实例,以避免在那些 class 中使用 SceneManager.getInstance
的需要(参见 Passing Parameters JavaFX FXML):
控制器超级class
public class BaseController {
protected SceneManager sceneManager;
void setSceneManager(SceneManager sceneManager) { // if SceneManager and BaseController are in different packages, change visibility
this.sceneManager = sceneManager;
}
}
FXMLLoader loader = ...
Pane pane = loader.load();
BaseController controller = loader.getController();
controller.setSceneManager(this);
为了简单起见,使用 url 作为场景的标识符,您可以改进实现:
public class SceneManager {
private final Stage rootStage;
public SceneManager(Stage rootStage) {
if (rootStage == null) {
throw new IllegalArgumentException();
}
this.rootStage = rootStage;
}
private final Map<String, Scene> scenes = new HashMap<>();
public void switchScene(String url) {
Scene scene = scenes.computeIfAbsent(url, u -> {
FXMLLoader loader = new FXMLLoader(getClass().getResource(u));
try {
Pane p = loader.load();
BaseController controller = loader.getController();
controller.setSceneManager(this);
return new Scene(p);
} catch (IOException ex) {
throw new RuntimeException(ex);
}
});
rootStage.setScene(scene);
}
}
这让您可以
- 为不同的阶段创建不同的经理
- 首先在需要时加载场景
- 动态添加更多场景
- 防止调用
switchScene
但阶段为null
的状态
- 将控制器 class 中的
SceneManager
的访问简化为sceneManager.switchScene
SceneManager
可能在程序完成之前可用于垃圾回收,因为没有对它的静态引用。