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

JavaFX绘图程序Undo/Redo按钮实现技术咨询

JavaFX绘图程序实现撤销/重做功能

针对撤销按钮的实现需求,这里用**双栈(撤销栈+重做栈)**的方式落地,配合ObservableList的自动监听,就能完成基础的撤销和重做功能。

1. 修改shapeModel类

在模型中添加撤销栈、重做栈,重写添加形状的逻辑并实现undo/redo方法:

import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.scene.paint.Color;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import java.util.ArrayDeque;
import java.util.Deque;

public class shapeModel {

    // 撤销栈:存储已添加的形状,用于撤销操作
    private final Deque<Shape> undoStack = new ArrayDeque<>();
    // 重做栈:存储被撤销的形状,用于重做操作
    private final Deque<Shape> redoStack = new ArrayDeque<>();

    private final ObservableList<Shape> shapeList;
    private final ObjectProperty<Color> colorPickerSelect;
    private ShapeType currentShape;

    public shapeModel() {
        this.colorPickerSelect = new SimpleObjectProperty<>(Color.GREEN);
        this.shapeList = FXCollections.observableArrayList(shape -> new Observable[]{
                shape.colorProperty()
        });
    }

    public ShapeType getCurrentShape() {
        return currentShape;
    }

    public void setCurrentShape(ShapeType currentShape) {
        this.currentShape = currentShape;
    }

    public void addShapes(Shape shape) {
        if (shape != null) {
            this.shapeList.add(shape);
            undoStack.push(shape);
            // 添加新形状后清空重做栈,之前的重做操作失效
            redoStack.clear();
        }
    }

    public Color getColorPickerSelect() {
        return colorPickerSelect.get();
    }

    public ObjectProperty<Color> colorPickerSelectProperty() {
        return colorPickerSelect;
    }

    public ObservableList<Shape> getShapeObservableList() {
        return shapeList;
    }

    // 撤销操作:移除最后添加的形状,放入重做栈
    public void undo() {
        if (!undoStack.isEmpty()) {
            Shape lastShape = undoStack.pop();
            shapeList.remove(lastShape);
            redoStack.push(lastShape);
        }
    }

    // 重做操作:恢复最后被撤销的形状,放入撤销栈
    public void redo() {
        if (!redoStack.isEmpty()) {
            Shape lastUndoneShape = redoStack.pop();
            shapeList.add(lastUndoneShape);
            undoStack.push(lastUndoneShape);
        }
    }
}

2. 修改HelloController类

完善撤销按钮的点击事件,同时修复画布重绘逻辑(避免图形叠加残留):

import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.scene.canvas.Canvas;
import javafx.scene.canvas.GraphicsContext;
import javafx.scene.control.Button;
import javafx.scene.control.ColorPicker;
import javafx.scene.control.ChoiceBox;
import javafx.scene.layout.BorderPane;
import javafx.scene.input.MouseEvent;
import javafx.application.Platform;
import javafx.stage.FileChooser;
import java.io.File;

public class HelloController {

    @FXML
    public Button eraserButton;
    @FXML
    public Button undoButton;
    // 若需要重做功能,可添加对应的按钮
    // @FXML public Button redoButton;
    @FXML
    public BorderPane scenePane;
    @FXML
    public ColorPicker myColorPicker;
    @FXML
    public ChoiceBox<String> myChoiceBox;
    public GraphicsContext context;
    @FXML
    public Canvas canvas;
    public shapeModel model;

    public void initialize() {
        context = canvas.getGraphicsContext2D();
        model.getShapeObservableList().addListener((ListChangeListener<Shape>) e -> listChanged());   
        myColorPicker.valueProperty().bindBidirectional(model.colorPickerSelectProperty());
    }

    public HelloController() {
        this.model = new shapeModel();
    }

    public void canvasClicked(MouseEvent mouseEvent) {
        Shape shape = Shape.creatingShapes(model.getCurrentShape(),mouseEvent.getX(),mouseEvent.getY(),50,50,myColorPicker.getValue());
        model.addShapes(shape);
    }

    // 修复重绘逻辑:先清空画布再重新绘制所有形状
    public void listChanged() {
        var context = canvas.getGraphicsContext2D();
        context.clearRect(0, 0, canvas.getWidth(), canvas.getHeight());
        model.getShapeObservableList().forEach(s -> s.draw(context));
    }

    // 撤销按钮点击事件
    public void undoClicked() {
        model.undo();
    }

    // 若有重做按钮,添加对应点击事件
    // public void redoClicked() {
    //     model.redo();
    // }

    public void handleClick() {
        FileChooser chooser = new FileChooser();
        File file = chooser.showSaveDialog(scenePane.getScene().getWindow());
    }

    public void onCircleClicked(ActionEvent e) {
        model.setCurrentShape(ShapeType.CIRCLE);
    }

    public void onRectangleClicked(ActionEvent e) {
        model.setCurrentShape(ShapeType.RECTANGLE);
    }

    public void exitAction(ActionEvent actionEvent) {
        Platform.exit();
    }

    public void eraser() {
        context.setFill(Color.WHITE);
        context.fillRect(0,0,canvas.getWidth(), canvas.getHeight());
    }
}

3. 关键逻辑说明

  • 撤销栈:每次添加新形状时,将形状推入撤销栈,同时清空重做栈(新操作会打断之前的重做历史)
  • 撤销操作:从撤销栈弹出最后一个形状,从显示列表移除后推入重做栈
  • 重做操作:从重做栈弹出最后一个被撤销的形状,重新添加到显示列表后推入撤销栈
  • 重绘逻辑:每次列表变化时先清空画布,再重新绘制所有形状,避免图形叠加残留

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.14 16:25:32