JavaFX-撤消缩放画布上的绘图

JavaFX-撤消缩放画布上的绘图,java,javafx,canvas,undo,Java,Javafx,Canvas,Undo,我正在开发一个简单的图像编辑功能,作为一个更大的JavaFX应用程序的一部分,但是我在一起解决撤销/缩放和绘图需求时遇到了一些麻烦 我的要求如下: 用户应能够: 在图像上徒手画 放大和缩小图像 撤消更改 如果画布比窗口大,它应该有滚动条 我是如何实现这些要求的: 通过在画布上按下鼠标时开始一条线,拖动鼠标时抚摸它,松开按钮时关闭路径,可以完成绘图 缩放的工作原理是将画布缩放到更高或更低的值 Undo方法在按下鼠标(在进行任何更改之前)时拍摄画布当前状态的快照,并将其推送到图像堆栈中。当我需

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

我的要求如下:

用户应能够:

  • 在图像上徒手画
  • 放大和缩小图像
  • 撤消更改
  • 如果画布比窗口大,它应该有滚动条
我是如何实现这些要求的:

  • 通过在画布上按下鼠标时开始一条线,拖动鼠标时抚摸它,松开按钮时关闭路径,可以完成绘图

  • 缩放的工作原理是将画布缩放到更高或更低的值

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

  • 要有滚动条,我只需将画布放在一个组和一个滚动窗格中

一切都很好,除了我尝试在缩放的画布上画画。由于我实现撤销功能的方式,我必须将其缩放回1,拍摄节点快照,然后将其缩放回以前的大小。发生这种情况时,当用户拖动鼠标时,鼠标指针下方的图像位置会发生变化,从而导致它画出一条不应该在那里的线

正常(未缩放画布):

Bug(缩放画布)

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

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);
        }
    }
}
  • 不重新缩放以拍摄快照-不会导致不需要的线条,但我最终会在堆栈中使用不同的图像大小,如果在拍摄快照时图像较小(缩小),我现在的图像分辨率较低,无法在不降低质量的情况下进行缩放

  • 调整逻辑并将pushUndo调用放到mousererelease事件中——它几乎成功了,但是当用户滚动到某个地方并在那里绘制时,重新缩放会导致图像滚动回左上角

  • 试图搜索一种“克隆”或序列化画布并将对象状态存储在堆栈中的方法-没有找到任何我能够适应的方法,并且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);
        }
    }
}
导入javafx.application.application;
导入javafx.scene.Group;
导入javafx.scene.scene;
导入javafx.scene.canvas.canvas;
导入javafx.scene.canvas.GraphicsContext;
导入javafx.scene.control.Button;
导入javafx.scene.control.ScrollPane;
导入javafx.scene.image.image;
导入javafx.scene.layout.BorderPane;
导入javafx.scene.layout.HBox;
导入javafx.scene.paint.Color;
导入javafx.stage.stage;
导入java.util.Stack;
公共类主扩展应用程序{
堆栈撤消堆栈;
帆布;
双拉票量表;
公共静态void main(字符串[]args){
发射(args);
}
@凌驾
公众假期开始(阶段){
拉票量表=1.0;
undoStack=新堆栈();
BorderPane BorderPane=新的BorderPane();
HBox HBox=新的HBox(4);
按钮btnUndo=新按钮(“撤消”);
setOnAction(actionEvent->undo());
按钮BTNinIncreaseZoom=新按钮(“增加缩放”);
btninIncreaseZoom.setOnAction(actionEvent->increaseZoom());
按钮BTN减少缩放=新按钮(“减少缩放”);
btnDecreseZoom.setOnAction(actionEvent->DecreseZoom());
hbox.getChildren().addAll(btnUndo、btninIncreaseZoom、btnDecreaseZoom);
ScrollPane ScrollPane=新的ScrollPane();
组=新组();
画布=新画布();
画布设置宽度(400);
画布设置高度(300);
group.getChildren().add(画布);
scrollPane.setContent(组);
GraphicsContext gc=canvas.getGraphicsContext2D();
gc.设定线宽(2.0);
gc.设定行程(颜色为红色);
canvas.setOnMousePressed(mouseEvent->{
pushUndo();
gc.beginPath();
gc.lineTo(mouseEvent.getX(),mouseEvent.getY());
});
canvas.setonmousedrable(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);
边框窗格。设置中心(滚动窗格);
场景=新场景(边框窗格,800600);
舞台场景;
stage.show();
}
私有void increaseZoom(){
拉票比例+=0.1;
canvas.setScaleX(canvasScale);
canvas.setScaleY(canvascale);
}
私有void递减缩放(){
拉票比例-=0.1;
canvas.setScaleX(canvasScale);
canvas.setScaleY(canvascale);
}
私有void pushUndo(){
//将画布比例恢复为1,以便获得原始比例图像
画布。设置刻度(1);
帆布。定格尺(1);
//使用快照方法获取图像并将其存储在撤消堆栈上
Image snapshot=canvas.snapshot(null,null);
撤消堆栈推送(快照);
//将画布比例设置为方法之前的值
canvas.setScaleX(canvasScale);
canvas.setScaleY(canvascale);
}
私有void undo(){
如果(!撤消堆栈)
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();
    }

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