【发布时间】:2020-08-12 09:09:53
【问题描述】:
我正在开发一个简单的图像编辑功能,作为大型 JavaFX 应用程序的一部分,但我在解决撤消/缩放和绘制需求时遇到了一些麻烦。
我的要求如下:
用户应该能够:
- 在图像上徒手绘制
- 放大和缩小图像
- 撤消更改
- 如果画布比窗口大,它应该有滚动条。
我是如何实现这些要求的:
绘制是这样完成的
缩放通过将画布缩放到更高或更低的值来工作。
Undo 方法在按下鼠标时(在进行任何更改之前)拍摄画布当前状态的快照,并将其推送到图像堆栈。当我需要撤消某些更改时,我会弹出堆栈的最后一张图像并将其绘制在画布上,用最后一张替换当前图像。
要拥有滚动条,我只需将 Canvas 放在 Group 和 ScrollPane 中。
一切正常,除非我尝试在缩放的画布上绘图。由于我实现撤消功能的方式,我必须将其缩放回 1,拍摄节点的快照,然后将其缩放回之前的大小。当发生这种情况并且用户正在拖动鼠标时,图像位置会在鼠标指针下方发生变化,从而导致它绘制一条不应该存在的线。
正常(未缩放的画布):
错误(缩放的画布)
我尝试了以下方法来解决问题:
不要重新缩放以拍摄快照 - 不会导致不需要的线条,但我最终会在堆栈中得到不同的图像大小,如果在拍摄快照时它更小(缩小)我现在图像的分辨率较低,我无法在不损失质量的情况下放大。
调整逻辑并将 pushUndo 调用放到 mouseReleased 事件中 - 几乎可以正常工作,但是当用户滚动到某个位置并在那里绘制时,重新缩放会导致图像滚动回顶部 -左;
试图搜索“克隆”或序列化画布并将对象状态存储在堆栈中的方法 - 没有找到我能够适应的任何内容,并且 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);
}
}
}
【问题讨论】:
-
这种情况是您创建应用程序的逻辑模型并从逻辑模型创建视图的原因。逻辑模型由一个或多个普通 Java 类组成,这些类包含执行应用程序功能所需的值。 en.wikipedia.org/wiki/Model%E2%80%93view%E2%80%93controller
-
确实我正在向后开发这个应用程序。虽然我知道这个理论,但软件开发的时间限制有时会迫使我们忽视它,但它总是有代价的。这也是一个教训,谢谢。