JavaFX - 撤消在缩放画布上的画作

问题描述 投票:0回答:1

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

我的要求如下。

用户应该能够:

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

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

  • 绘图是通过在画布上按下鼠标时开始画一条线,拖动时抚摸它,释放按钮时关闭路径来完成的。

  • 缩放的工作原理是将画布缩放到一个较高或较低的值。

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

  • 要有滚动条,我只需要把Canvas放在一个Group和一个ScrollPane里面。

一切都很好,除了当我尝试在一个缩放的画布上绘制时。由于我实现Undo功能的方式,我必须将其缩放为1,并对Node进行快照,然后将其缩放为之前的大小。当这种情况发生时,用户拖动鼠标时,图像的位置会在鼠标指针下方发生变化,导致它画出一条不应该出现的线。

正常(未缩放画布)。

Normal Example

错误(缩放画布)

Bug Example

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

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

  • 调整逻辑,把pushUndo调用放到mouseReleased事件上--这几乎是成功的,但是当用户滚动到一个地方,并且它在那里画画时,重新缩放会导致图像滚动回左上方。

  • 尝试搜索一种方法来 "克隆 "或序列化画布,并将对象状态存储在Stack中--没有找到任何我能够适应的方法,而且JavaFX不支持其对象的序列化。

我认为这个问题可以通过重做undo功能来解决,因为它不需要重新缩放画布来复制它的状态,或者改变缩放画布的方式而不缩放画布,但我对如何实现这两种方案都没有想法。

下面是重现这个问题的功能代码示例。

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 javafx canvas undo
1个回答
2
投票

考虑绘制 Shape 对象,在这里 路径 对象,并对它们进行缩放。

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

0
投票

我解决这个问题的方法是扩展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();
    }
}
© www.soinside.com 2019 - 2024. All rights reserved.