JavaFX-在缩放的画布上撤消绘图 [英] JavaFX - Undo drawing on a scaled Canvas

查看:140
本文介绍了JavaFX-在缩放的画布上撤消绘图的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

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

I'm developing a simple image editing functionality as a part of a larger JavaFX application, but I'm having some trouble to work out the undo/zoom and draw requirements together.

我的要求如下:

用户应该能够:

  • 徒手绘制图像
  • 放大和缩小图像
  • 撤消更改
  • 如果画布大于窗口,则应具有滚动条.

我如何实现这些要求:

  • 通过在画布上按下鼠标时开始绘制线条,在拖动时将其抚摸并在释放按钮时关闭路径来完成绘制.

  • The Drawing is done by starting a line when the mouse is pressed on the canvas, stroking it when it is dragged and closing the path when the button is released.

缩放功能通过将画布缩放到更高或更低的值来工作.

The Zoom works by scaling the canvas to a higher or lower value.

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

The Undo method takes a snapshot of the current state of the canvas when the mouse is pressed (before any change is made) and push it to a Stack of Images. When I need to undo some change I pop the last image of the Stack and draw it on the canvas, replacing the current image by the last one.

要具有滚动条,我只需将Canvas放置在Group和ScrollPane中.

To have scroll-bars I just place the Canvas inside a Group and a ScrollPane.

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

Everything works fine, except when I try to draw on a scaled canvas. Due to the way I implemented the Undo functionality, I have to scale it back to 1, take a snapshot of the Node then scale it back to the size it was before. When this happens and the user is dragging the mouse the image position changes below the mouse pointer, causing it to draw a line that shouldn't be there.

普通(无比例缩放的画布):

Normal (unscaled canvas):

错误(缩放画布)

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

I tried the following approaches to solve the problem:

  • 不要重新缩放以拍摄快照-不会引起不必要的线条,但是如果拍摄快照时图像尺寸较小(缩小),我最终会在堆栈中使用不同的图像尺寸现在图像的分辨率较低,我无法在不降低质量的情况下进行放大.

  • Don't re-scale to take the snapshot - Doesn't cause the unwanted line, but I end up with different image sizes in the stack, if it's smaller (zoomed out) when the snapshot was taken I now have a lower resolution of the image that I can't scale up without losing quality.

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

Tweak the logic and put the pushUndo call to the mouseReleased event - It almost worked, but when the user scrolled to a place and it's drawing there, the re-scaling causes the image to scroll back to the top-left;

试图寻找一种克隆"或序列化画布并将对象状态存储在堆栈中的方法-找不到我能适应的任何东西,并且JavaFX不支持其对象的序列化.

Tried to search an way to "clone" or serialize the canvas and store the object state in the Stack - Didn't found anything I was able to adapt, and JavaFX doesn't support serialization of its objects.

我认为可以通过重新设计撤消功能来解决此问题,因为它不需要重新缩放画布即可复制其状态,也可以通过更改不缩放画布的方式来缩放画布,但是我已经退出了如何实施这些选项的想法.

I think the problem can be solved either by reworking the undo functionality as it doesn't need to re-scale the canvas to copy its state or by changing the way I zoom the canvas without scaling it, but I'm out of ideas on how to implement either of those options.

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

Below is the functional code example to reproduce the problem:

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

推荐答案

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

I solved the problem by extending the Canvas component and adding a second canvas in the extended class to act as a copy of the main canvas.

每次我在画布上进行更改时,我都会在碳"画布上进行相同的更改.当我需要重新缩放画布以获取快照(问题的根源)时,我只需将"carbon"画布重新缩放至1并从中获取快照即可.这不会导致鼠标在主画布中拖动,因为在此过程中鼠标仍会缩放.可能这不是最佳解决方案,但它可以工作.

Every time I made a change in the canvas I do the same change in this "carbon" canvas. When I need to re-scale the canvas to get the snapshot (the root of my problem) I just re-scale the "carbon" canvas back to 1 and get my snapshot from it. This doesn't cause the drag of the mouse in the main canvas, as it remains scaled during this process. Probably this isn't the optimal solution, but it works.

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

Below is the code for reference, to anyone who may have a similar problem in the future.

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

这篇关于JavaFX-在缩放的画布上撤消绘图的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆