JavaFX - отменить рисование на масштабированном холсте - PullRequest
0 голосов
/ 28 апреля 2020

Я разрабатываю простую функцию редактирования изображений как часть более крупного приложения JavaFX, но у меня возникли некоторые проблемы, связанные с отменой / масштабированием и рисованием требований.

My требования следующие:

Пользователь должен иметь возможность:

  • Рисовать от руки на изображении
  • Увеличение и уменьшение изображения
  • Отменить изменения
  • Если холст больше окна, у него должны быть полосы прокрутки.

Как я реализовал эти требования:

  • Рисование выполняется путем запуска линии при нажатии мыши на холсте, поглаживания по ней при перетаскивании и закрытия пути при отпускании кнопки.

  • Zoom работает путем масштабирования холста до более высокого или более низкого значения.

  • Метод Undo делает снимок текущего состояния холста при нажатии мыши (до внесения каких-либо изменений) и pu sh в стек I маги. Когда мне нужно отменить некоторые изменения, я вытаскиваю последнее изображение стека и рисую его на холсте, заменяя текущее изображение последним.

  • Чтобы иметь полосы прокрутки, я просто поместите холст внутри группы и ScrollPane.

Все работает хорошо, кроме случаев, когда я пытаюсь рисовать на масштабированном холсте. В связи с тем, как я реализовал функциональность Undo, мне нужно уменьшить его до 1, сделать снимок узла, а затем уменьшить его до размера, который был раньше. Когда это происходит, и пользователь перетаскивает мышь, положение изображения изменяется под указателем мыши, в результате чего оно рисует линию, которой там быть не должно.

Обычный (немасштабированный холст):

Normal Example

Ошибка (масштабированный холст)

Bug Example

Я пробовал следующие подходы для решения проблемы :

  • Не масштабировать, чтобы сделать снимок - не вызывает нежелательной строки, но я получаю изображения в стеке разных размеров, если оно меньше (уменьшено ) когда был сделан снимок, теперь у меня есть более низкое разрешение изображения, которое я не могу увеличить без потери качества.

  • Настройте логику c и вызовите pushUndo событие mouseReleased - это почти сработало, но когда пользователь прокручивал до места и рисовал там, при повторном масштабировании изображение прокручивалось обратно в верхний левый угол;

  • Пробовал искать способ «клонировать» или сериализовать холст и сохранять объект st в стеке - не нашел ничего, что я смог адаптировать, и 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);
        }
    }
}

Ответы [ 2 ]

2 голосов
/ 29 апреля 2020

Рассмотрим рисование 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 голосов
/ 29 апреля 2020

Я решил проблему, расширив компонент Canvas и добавив второй холст в расширенный класс, который будет действовать как копия основного холста.

Каждый раз, когда я вносил изменения в холст, я делаю одно и то же изменение в этом "углеродном" холсте. Когда мне нужно изменить масштаб холста, чтобы получить снимок (root моей проблемы), я просто масштабирую «углеродный» холст обратно до 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();
    }
}
...