java画图可以放大和缩小图片,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.

My requirements are the following:

The user should be able to:

Draw freehand on the image

Zoom in and out the image

Undo the changes

If the canvas is bigger than the window, it should have scroll-bars.

How I implemented these requirements:

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.

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

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):

0uScl.gif

Bug (scaled canvas)

yLxhp.gif

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.

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;

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

}

}

}

解决方案

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.

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 undoStack;

private final Stack 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();

}

}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值