You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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() {}
}

关键说明

  1. 数据与视图分离:所有状态保存在共享模型中,两个窗口仅负责渲染,避免了重复逻辑和不同步问题。
  2. 坐标转换:180度旋转后,棋盘的行和列都需要反转(7 - originalRow),才能正确映射原始位置。
  3. 模型监听:利用JavaFX的ObservableMap,模型变化时自动触发UI更新,无需手动同步。

内容的提问来源于stack exchange,提问作者Khanjali

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.05 17:30:20