【问题标题】:JavaFX - Undo drawing on a scaled CanvasJavaFX - 在缩放的画布上撤消绘图
【发布时间】:2020-08-12 09:09:53
【问题描述】:

我正在开发一个简单的图像编辑功能,作为大型 JavaFX 应用程序的一部分,但我在解决撤消/缩放和绘制需求时遇到了一些麻烦。

我的要求如下:

用户应该能够:

  • 在图像上徒手绘制
  • 放大和缩小图像
  • 撤消更改
  • 如果画布比窗口大,它应该有滚动条。

我是如何实现这些要求的:

  • 绘制是这样完成的

  • 缩放通过将画布缩放到更高或更低的值来工作。

  • Undo 方法在按下鼠标时(在进行任何更改之前)拍摄画布当前状态的快照,并将其推送到图像堆栈。当我需要撤消某些更改时,我会弹出堆栈的最后一张图像并将其绘制在画布上,用最后一张替换当前图像。

  • 要拥有滚动条,我只需将 Canvas 放在 Group 和 ScrollPane 中。

一切正常,除非我尝试在缩放的画布上绘图。由于我实现撤消功能的方式,我必须将其缩放回 1,拍摄节点的快照,然后将其缩放回之前的大小。当发生这种情况并且用户正在拖动鼠标时,图像位置会在鼠标指针下方发生变化,从而导致它绘制一条不应该存在的线。

正常(未缩放的画布):

错误(缩放的画布)

我尝试了以下方法来解决问题:

  • 不要重新缩放以拍摄快照 - 不会导致不需要的线条,但我最终会在堆栈中得到不同的图像大小,如果在拍摄快照时它更小(缩小)我现在图像的分辨率较低,我无法在不损失质量的情况下放大。

  • 调整逻辑并将 pushUndo 调用放到 mouseReleased 事件中 - 几乎可以正常工作,但是当用户滚动到某个位置并在那里绘制时,重新缩放会导致图像滚动回顶部 -左;

  • 试图搜索“克隆”或序列化画布并将对象状态存储在堆栈中的方法 - 没有找到我能够适应的任何内容,并且 JavaFX 不支持其对象的序列化。

我认为这个问题可以通过重新设计撤消功能来解决,因为它不需要重新缩放画布来复制其状态,或者通过改变我缩放画布而不缩放它的方式,但我已经出局了关于如何实施其中任何一个选项的想法。

下面是重现问题的功能代码示例:

import javafx.application.Application;
import javafx.scene.Group;
import javafx.scene.Scene;
import javafx.scene.canvas.Canvas;
import javafx.scene.canvas.GraphicsContext;
import javafx.scene.control.Button;
import javafx.scene.control.ScrollPane;
import javafx.scene.image.Image;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.paint.Color;
import javafx.stage.Stage;

import java.util.Stack;

public class Main extends Application {
    Stack<Image> undoStack;
    Canvas canvas;
    double canvasScale;

    public static void main(String[] args) {
        launch(args);
    }

    @Override
    public void start(Stage stage) {
        canvasScale = 1.0;
        undoStack = new Stack<>();

        BorderPane borderPane = new BorderPane();
        HBox hbox = new HBox(4);
        Button btnUndo = new Button("Undo");
        btnUndo.setOnAction(actionEvent -> undo());

        Button btnIncreaseZoom = new Button("Increase Zoom");
        btnIncreaseZoom.setOnAction(actionEvent -> increaseZoom());

        Button btnDecreaseZoom = new Button("Decrease Zoom");
        btnDecreaseZoom.setOnAction(actionEvent -> decreaseZoom());

        hbox.getChildren().addAll(btnUndo, btnIncreaseZoom, btnDecreaseZoom);

        ScrollPane scrollPane = new ScrollPane();
        Group group = new Group();

        canvas = new Canvas();
        canvas.setWidth(400);
        canvas.setHeight(300);
        group.getChildren().add(canvas);
        scrollPane.setContent(group);

        GraphicsContext gc = canvas.getGraphicsContext2D();
        gc.setLineWidth(2.0);
        gc.setStroke(Color.RED);

        canvas.setOnMousePressed(mouseEvent -> {
            pushUndo();
            gc.beginPath();
            gc.lineTo(mouseEvent.getX(), mouseEvent.getY());
        });

        canvas.setOnMouseDragged(mouseEvent -> {
            gc.lineTo(mouseEvent.getX(), mouseEvent.getY());
            gc.stroke();
        });

        canvas.setOnMouseReleased(mouseEvent -> {
            gc.lineTo(mouseEvent.getX(), mouseEvent.getY());
            gc.stroke();
            gc.closePath();
        });

        borderPane.setTop(hbox);
        borderPane.setCenter(scrollPane);
        Scene scene = new Scene(borderPane, 800, 600);
        stage.setScene(scene);
        stage.show();
    }

    private void increaseZoom() {
        canvasScale += 0.1;
        canvas.setScaleX(canvasScale);
        canvas.setScaleY(canvasScale);
    }

    private void decreaseZoom () {
        canvasScale -= 0.1;
        canvas.setScaleX(canvasScale);
        canvas.setScaleY(canvasScale);
    }

    private void pushUndo() {
        // Restore the canvas scale to 1 so I can get the original scale image
        canvas.setScaleX(1);
        canvas.setScaleY(1);

        // Get the image with the snapshot method and store it on the undo stack
        Image snapshot = canvas.snapshot(null, null);
        undoStack.push(snapshot);

        // Set the canvas scale to the value it was before the method
        canvas.setScaleX(canvasScale);
        canvas.setScaleY(canvasScale);
    }

    private void undo() {
        if (!undoStack.empty()) {
            Image undoImage = undoStack.pop();
            canvas.getGraphicsContext2D().drawImage(undoImage, 0, 0);
        }
    }
}

【问题讨论】:

  • 这种情况是您创建应用程序的逻辑模型并从逻辑模型创建视图的原因。逻辑模型由一个或多个普通 Java 类组成,这些类包含执行应用程序功能所需的值。 en.wikipedia.org/wiki/Model%E2%80%93view%E2%80%93controller
  • 确实我正在向后开发这个应用程序。虽然我知道这个理论,但软件开发的时间限制有时会迫使我们忽视它,但它总是有代价的。这也是一个教训,谢谢。

标签: java javafx canvas undo


【解决方案1】:

考虑绘制Shape 对象,在本例中为Path 对象,并对它们应用缩放:

import java.util.Stack;
import javafx.application.Application;
import javafx.scene.Group;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Pane;
import javafx.scene.paint.Color;
import javafx.scene.shape.LineTo;
import javafx.scene.shape.MoveTo;
import javafx.scene.shape.Path;
import javafx.stage.Stage;

public class Main extends Application {

    private Path path;
    private Stack<Path> undoStack;
    private Group group;
    private  double scale = 1;

    public static void main(String[] args) {
        launch(args);
    }

    @Override
    public void start(Stage primaryStage) {

        undoStack = new Stack<>();

        Button btnUndo = new Button("Undo");
        btnUndo.setOnAction(actionEvent -> undo());

        Button btnIncreaseZoom = new Button("Increase Zoom");
        btnIncreaseZoom.setOnAction(actionEvent -> increaseZoom());

        Button btnDecreaseZoom = new Button("Decrease Zoom");
        btnDecreaseZoom.setOnAction(actionEvent -> decreaseZoom());
        HBox hbox = new HBox(4, btnUndo, btnIncreaseZoom, btnDecreaseZoom);

        group = new Group();
        BorderPane root = new BorderPane(new Pane(group), hbox, null,null, null);
        Scene scene = new Scene(root, 300, 400);

        root.setOnMousePressed(mouseEvent -> newPath(mouseEvent.getX(), mouseEvent.getY()));
        root.setOnMouseDragged(mouseEvent -> addToPath(mouseEvent.getX(), mouseEvent.getY()));

        primaryStage.setScene(scene);
        primaryStage.show();
    }

    private void newPath(double x, double y) {

        path = new Path();
        path.setStrokeWidth(1);
        path.setStroke(Color.BLACK);
        path.getElements().add(new MoveTo(x,y));
        group.getChildren().add(path);
        undoStack.add(path);
    }

    private void addToPath(double x, double y) {
        path.getElements().add(new LineTo(x, y));
    }

    private void increaseZoom() {
        scale += 0.1;
        reScale();
    }

    private void decreaseZoom () {
        scale -= 0.1;
        reScale();
    }

    private void reScale(){
        for(Path path : undoStack){
            path.setScaleX(scale);
            path.setScaleY(scale);
        }
    }

    private void undo() {
        if(! undoStack.isEmpty()){
            Node node = undoStack.pop();
            group.getChildren().remove(node);
        }
    }
}

【讨论】:

  • 感谢您的回答,我测试了代码,几乎可以正常工作。唯一的问题是当您放大/缩小绘图对象时会移动。我认为通过一些更改可以解决它,但我找到了我在下面发布的解决方案。再次感谢您花时间帮助我。
【解决方案2】:

我通过扩展 Canvas 组件并在扩展类中添加第二个画布作为主画布的副本解决了这个问题。

每次我对画布进行更改时,我都会在这个“碳”画布上进行相同的更改。当我需要重新缩放画布以获取快照(我的问题的根源)时,我只需将“碳”画布重新缩放回 1 并从中获取我的快照。这不会导致鼠标在主画布中的拖动,因为它在此过程中保持缩放。可能这不是最佳解决方案,但它确实有效。

以下是供将来可能遇到类似问题的任何人参考的代码。

ExtendedCanvas.java

import javafx.scene.canvas.Canvas;
import javafx.scene.canvas.GraphicsContext;
import javafx.scene.image.Image;

import java.util.Stack;

public class ExtendedCanvas extends Canvas {
    private final double ZOOM_SCALE = 0.1;
    private final double MAX_ZOOM_SCALE = 3.0;
    private final double MIN_ZOOM_SCALE = 0.2;

    private double currentScale;
    private final Stack<Image> undoStack;
    private final Stack<Image> redoStack;
    private final Canvas carbonCanvas;

    private final GraphicsContext gc;
    private final GraphicsContext carbonGc;

    public ExtendedCanvas(double width, double height){
        super(width, height);

        carbonCanvas = new Canvas(width, height);
        undoStack = new Stack<>();
        redoStack = new Stack<>();
        currentScale = 1.0;

        gc = this.getGraphicsContext2D();
        carbonGc = carbonCanvas.getGraphicsContext2D();

        setEventHandlers();
    }

    private void setEventHandlers() {
        this.setOnMousePressed(mouseEvent -> {
            pushUndo();
            gc.beginPath();
            gc.lineTo(mouseEvent.getX(), mouseEvent.getY());

            carbonGc.beginPath();
            carbonGc.lineTo(mouseEvent.getX(), mouseEvent.getY());
        });

        this.setOnMouseDragged(mouseEvent -> {
            gc.lineTo(mouseEvent.getX(), mouseEvent.getY());
            gc.stroke();

            carbonGc.lineTo(mouseEvent.getX(), mouseEvent.getY());
            carbonGc.stroke();
        });

        this.setOnMouseReleased(mouseEvent -> {
            gc.lineTo(mouseEvent.getX(), mouseEvent.getY());
            gc.stroke();
            gc.closePath();

            carbonGc.lineTo(mouseEvent.getX(), mouseEvent.getY());
            carbonGc.stroke();
            carbonGc.closePath();
        });
    }

    public void zoomIn() {
        if (currentScale < MAX_ZOOM_SCALE ) {
            currentScale += ZOOM_SCALE;
            setScale(currentScale);
        }
    }

    public void zoomOut() {
        if (currentScale > MIN_ZOOM_SCALE) {
            currentScale -= ZOOM_SCALE;
            setScale(currentScale);
        }
    }

    public void zoomNormal() {
        currentScale = 1.0;
        setScale(currentScale);
    }

    private void setScale(double value) {
        this.setScaleX(value);
        this.setScaleY(value);
        carbonCanvas.setScaleX(value);
        carbonCanvas.setScaleY(value);
    }

    private void pushUndo() {
        redoStack.clear();
        undoStack.push(getSnapshot());
    }

    private Image getSnapshot(){
        carbonCanvas.setScaleX(1);
        carbonCanvas.setScaleY(1);
        Image snapshot = carbonCanvas.snapshot(null, null);
        carbonCanvas.setScaleX(currentScale);
        carbonCanvas.setScaleY(currentScale);
        return snapshot;
    }

    public void undo() {
        if (hasUndo()) {
            Image redo = getSnapshot();
            redoStack.push(redo);
            Image undoImage = undoStack.pop();
            gc.drawImage(undoImage, 0, 0);
            carbonGc.drawImage(undoImage, 0, 0);
        }
    }

    public void redo() {
        if (hasRedo()) {
            Image undo = getSnapshot();
            undoStack.push(undo);
            Image redoImage = redoStack.pop();
            gc.drawImage(redoImage, 0, 0);
            carbonGc.drawImage(redoImage, 0, 0);
        }
    }

    public boolean hasUndo() {
        return !undoStack.isEmpty();
    }

    public boolean hasRedo() {
        return !redoStack.isEmpty();
    }

}

Main.java

package com.felipepaschoal;

import javafx.application.Application;
import javafx.scene.Group;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.ScrollPane;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.stage.Stage;

public class Main extends Application {
    ExtendedCanvas extendedCanvas;

    public static void main(String[] args) {
        launch(args);
    }

    @Override
    public void start(Stage stage) {
        BorderPane borderPane = new BorderPane();
        HBox hbox = new HBox(4);

        Button btnUndo = new Button("Undo");
        btnUndo.setOnAction(actionEvent -> extendedCanvas.undo());

        Button btnRedo = new Button("Redo");
        btnRedo.setOnAction(actionEvent -> extendedCanvas.redo());

        Button btnDecreaseZoom = new Button("-");
        btnDecreaseZoom.setOnAction(actionEvent -> extendedCanvas.zoomOut());

        Button btnResetZoom = new Button("Reset");
        btnResetZoom.setOnAction(event -> extendedCanvas.zoomNormal());

        Button btnIncreaseZoom = new Button("+");
        btnIncreaseZoom.setOnAction(actionEvent -> extendedCanvas.zoomIn());

        hbox.getChildren().addAll(
                btnUndo,
                btnRedo,
                btnDecreaseZoom,
                btnResetZoom,
                btnIncreaseZoom
        );

        ScrollPane scrollPane = new ScrollPane();
        Group group = new Group();

        extendedCanvas = new ExtendedCanvas(300,200);

        group.getChildren().add(extendedCanvas);
        scrollPane.setContent(group);

        borderPane.setTop(hbox);
        borderPane.setCenter(scrollPane);

        Scene scene = new Scene(borderPane, 600, 400);
        stage.setScene(scene);
        stage.show();
    }
}

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-06-06
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多