JavaFX如何在两个窗口复用同一场景实例实现镜像效果?
解决方案:双窗口同步国际象棋棋盘(含180度旋转镜像)
问题根源
你当前的实现中,两次加载game_board.fxml会生成完全独立的视图和控制器实例,各自拥有独立的GridPane和game_logic对象,因此第一个窗口的棋子移动无法同步到第二个窗口。同时JavaFX不允许同一个Node/Scene实例被添加到多个窗口(每个节点只能有一个父节点),所以复用同一场景的思路不可行。
核心解决思路
采用数据驱动视图的模式:将棋盘状态(棋子位置)抽离为独立的共享数据模型,两个窗口的视图都监听该模型的变化,模型更新时自动同步两个窗口的UI渲染;同时针对第二个窗口的180度旋转,做坐标映射转换。
具体实现步骤
1. 创建共享棋盘数据模型
该模型保存所有棋子的位置信息,并提供可监听的集合,让视图能感知状态变化:
import javafx.collections.FXCollections; import javafx.collections.ObservableMap; public class ChessBoardModel { // 键为坐标字符串(如"0,0"),值为棋子类型(如"WHITE_PAWN") private final ObservableMap<String, String> piecePositions = FXCollections.observableHashMap(); public ObservableMap<String, String> getPiecePositions() { return piecePositions; } // 移动棋子的核心方法,由游戏逻辑调用 public void movePiece(String fromCoords, String toCoords) { String piece = piecePositions.remove(fromCoords); if (piece != null) { piecePositions.put(toCoords, piece); } } // 初始化初始棋子位置 public void initializePieces() { // 白方 pawn for (int col = 0; col < 8; col++) { piecePositions.put("1," + col, "WHITE_PAWN"); } // 黑方 pawn for (int col = 0; col < 8; col++) { piecePositions.put("6," + col, "BLACK_PAWN"); } // 其他棋子(车、马、象、后、王)初始化逻辑补充... } }
2. 重构游戏逻辑,依赖数据模型
让游戏逻辑只负责修改模型状态,不直接操作UI:
public class game_logic { private final ChessBoardModel model; public game_logic(ChessBoardModel model) { this.model = model; model.initializePieces(); } // 验证移动合法性后,更新模型 public boolean handleMove(String fromCoords, String toCoords) { if (isValidMove(fromCoords, toCoords)) { model.movePiece(fromCoords, toCoords); return true; } return false; } // 实现国际象棋棋子移动规则判断 private boolean isValidMove(String fromCoords, String toCoords) { // 补充具体规则逻辑,比如棋子类型对应的移动范围、是否越子等 return true; } }
3. 修改主菜单启动逻辑,传递共享模型
创建一个全局共享的模型实例,传递给两个窗口的控制器:
@FXML public void play_game(ActionEvent event) throws IOException { // 创建唯一的共享棋盘模型 ChessBoardModel sharedModel = new ChessBoardModel(); game_logic gameLogic = new game_logic(sharedModel); // 加载第一个窗口 FXMLLoader firstLoader = new FXMLLoader(getClass().getResource("game_board.fxml")); Parent root = firstLoader.load(); board_controller firstController = firstLoader.getController(); firstController.init(sharedModel, gameLogic, false); // false表示无需坐标转换 // 加载第二个窗口 FXMLLoader secondLoader = new FXMLLoader(getClass().getResource("game_board.fxml")); Parent secondScreen = secondLoader.load(); board_controller secondController = secondLoader.getController(); secondController.init(sharedModel, gameLogic, true); // true表示需要坐标转换 // 显示第一个窗口 Stage mainStage = (Stage)((Node)event.getSource()).getScene().getWindow(); mainStage.setScene(new Scene(root)); mainStage.show(); // 显示第二个窗口并旋转180度 Stage secondStage = new Stage(); secondStage.setScene(new Scene(secondScreen)); secondScreen.rotateProperty().set(180); secondStage.show(); }
4. 修改控制器,监听模型变化同步UI
让控制器监听模型的位置变化,自动更新棋盘,同时处理旋转窗口的坐标转换:
import javafx.fxml.FXML; import javafx.scene.image.Image; import javafx.scene.image.ImageView; import javafx.scene.layout.GridPane; import javafx.scene.Node; public class board_controller { @FXML GridPane chess_board; private ChessBoardModel model; private game_logic gameLogic; private boolean isRotated; private String fromCoords; // 记录点击的起始坐标 public void init(ChessBoardModel model, game_logic gameLogic, boolean isRotated) { this.model = model; this.gameLogic = gameLogic; this.isRotated = isRotated; // 初始化棋盘UI resetBoard(); // 监听模型变化,自动同步UI model.getPiecePositions().addListener((change) -> updateBoard()); } // 重置棋盘格子(可选,添加背景色) private void resetBoard() { chess_board.getChildren().clear(); for (int row = 0; row < 8; row++) { for (int col = 0; col < 8; col++) { Node cell = new javafx.scene.layout.Region(); cell.setStyle((row + col) % 2 == 0 ? "-fx-background-color: #f0d9b5;" : "-fx-background-color: #b58863;"); cell.setOnMouseClicked(this::onCellClicked); GridPane.setRowIndex(cell, row); GridPane.setColumnIndex(cell, col); chess_board.getChildren().add(cell); } } } // 根据模型状态更新棋子显示 private void updateBoard() { // 移除所有棋子视图 chess_board.getChildren().removeIf(node -> node instanceof ImageView); model.getPiecePositions().forEach((coords, pieceType) -> { String[] parts = coords.split(","); int originalRow = Integer.parseInt(parts[0]); int originalCol = Integer.parseInt(parts[1]); // 旋转窗口坐标转换:原(0,0)对应旋转后的(7,7) int displayRow = isRotated ? 7 - originalRow : originalRow; int displayCol = isRotated ? 7 - originalCol : originalCol; // 加载棋子图片(需自行准备对应图片资源) Image pieceImg = new Image(getClass().getResourceAsStream("/images/" + pieceType + ".png")); ImageView pieceView = new ImageView(pieceImg); pieceView.setFitWidth(60); pieceView.setFitHeight(60); GridPane.setRowIndex(pieceView, displayRow); GridPane.setColumnIndex(pieceView, displayCol); chess_board.getChildren().add(pieceView); }); } // 处理棋盘格子点击事件,实现棋子移动 private void onCellClicked(javafx.scene.input.MouseEvent event) { Node cell = (Node)event.getSource(); int displayRow = GridPane.getRowIndex(cell); int displayCol = GridPane.getColumnIndex(cell); // 转换为原始坐标 int originalRow = isRotated ? 7 - displayRow : displayRow; int originalCol = isRotated ? 7 - displayCol : displayCol; String coords = originalRow + "," + originalCol; if (fromCoords == null) { fromCoords = coords; // 第一次点击记录起始位置 } else { gameLogic.handleMove(fromCoords, coords); // 第二次点击触发移动 fromCoords = null; } } @FXML public void initialize() {} }
关键说明
- 数据与视图分离:所有状态保存在共享模型中,两个窗口仅负责渲染,避免了重复逻辑和不同步问题。
- 坐标转换:180度旋转后,棋盘的行和列都需要反转(
7 - originalRow),才能正确映射原始位置。 - 模型监听:利用JavaFX的
ObservableMap,模型变化时自动触发UI更新,无需手动同步。
内容的提问来源于stack exchange,提问作者Khanjali
相关产品推荐
相关产品推荐

