Save/Load 在 JavaFx 上绘制的窗格

Save/Load a Pane drawing on JavaFx

我创建了一个绘图应用程序,我可以在其中绘制形状并将它们作为节点添加到窗格中。类似于下面的代码:

currentShape = new Rectangle();
shapeList.add(currentShape);
pane.getChildren().addAll(shapelist);

我想保存绘图并能够加载保存的绘图。我不确定如何处理这个问题。我查看了使用 Snapshots/Writable 图像的示例,如下所示。然而,这给了我一个 Cannot Resolve SwingFXUtils 错误

save.setOnAction(event -> {
            FileChooser fileChooser = new FileChooser();

            //Set extension filter
            fileChooser.getExtensionFilters().add(new FileChooser.ExtensionFilter("png files (*.png)", "*.png"));

            //Prompt user to select a file
            File f = fileChooser.showSaveDialog(null);

            if(f != null){
                try {
                    //Pad the capture area
                    WritableImage writableImage = new WritableImage((int) pane.getWidth(), (int) pane.getHeight());
                    pane.snapshot(null, writableImage);
                    RenderedImage renderedImage = SwingFXUtils.fromFXImage(writableImage, null);
                    //Write the snapshot to the chosen file
                    ImageIO.write(renderedImage, "png", f);
                } catch (IOException ex)
                { ex.printStackTrace(); }

            }
        });

如果有人对如何在窗格上保存和加载绘图有更好的建议,我将不胜感激。

Slaw 是对的:目前最好的方法是创建您自己的模型 objects 来代表您的应用程序中显示的内容。

但是,如果您想尝试直接写入和读取 JavaFX Shapes,您可以选择:XML bean 序列化。

XML bean 序列化由 XMLEncoder and XMLDecoder classes 执行。

与常规 Java 序列化不同,它们不查看字段,只查看 bean 属性。 bean 属性 由 public read-method 定义,它是以 get 开头的 zero-argument 方法(或者,如果它 returns原始 boolean,它可以选择以 is 而不是 get).

开头

因此,getWidth() 方法的存在定义了一个名为 width.

的 属性

如果有一个相应的set方法(在上面的例子中,它将是setWidth),它只接受一个与get-method返回的相同类型的参数, 属性 被定义为可写的 属性.

(完整的规则比这复杂一点;我只描述了一般情况。完整的 JavaBeans 规范是 here。)

如果您看过 JavaFX 的 javadoc,您可能已经注意到 JavaFX classes 定义了很多属性。这意味着您可以像这样保存 pane children:

private static final java.nio.file.Path SAVE_FILE_LOCATION =
    Paths.get(System.getProperty("user.home"), "shapes.xml");

void save()
throws IOException {
    try (XMLEncoder encoder = new XMLEncoder(
        new BufferedOutputStream(
            Files.newOutputStream(SAVE_FILE_LOCATION)))) {

        encoder.setExceptionListener(e -> {
            throw new RuntimeException(e);
        });

        encoder.writeObject(pane.getChildren().toArray(new Node[0]));
    }
}

void load()
throws IOException {
    try (XMLDecoder decoder = new XMLDecoder(
        new BufferedInputStream(
            Files.newInputStream(SAVE_FILE_LOCATION)))) {

        decoder.setExceptionListener(e -> {
            throw new RuntimeException(e);
        });

        pane.getChildren().setAll((Node[]) decoder.readObject());
    }
}

然而,XML编码器对 class 的了解和直觉是有限的。例如,在上面的代码中,我使用的是 Node 的数组而不是原始的 ObservableList,因为 XMLEncoder 不知道如何序列化任何(不是 publicly记录)ObservableList 的具体实现。 XML但是,编码器确实具有 built-in 序列化数组的能力,只要它可以序列化数组元素本身。

一个更重要的问题是它不知道如何序列化某些属性,只会忽略它们。例如,Color 不是典型的 Java bean:它是 read-only,因此虽然 XMLEncoder 可以读取其数据,但没有 set-methods,因此编码器不会不知道未来 XML 解码器可以使用什么指令来创建等效的 Color object。

我们可以通过 providing it with custom PersistenceDelegates. Conveniently, the DefaultPersistenceDelegate subclass 自定义 XML 编码器,允许将 bean 属性 名称传递给构造函数,它创建一个委托,它将告诉 XML解码器寻找一个构造函数,该构造函数采用与原始写入数据中的每个属性对应的参数。

由于 Color 具有 a four-argument constructor that takes the values of the red, green, blue, and opacity 属性,我们可以将 DefaultPersistenceDelegate 添加到 XMLEncoder,它指示未来的 XMLDecoder 在重构 Color [=] 时使用这些属性的值77=]:

encoder.setPersistenceDelegate(Color.class,
    new DefaultPersistenceDelegate(
        new String[] { "red", "green", "blue", "opacity" }));

上面的意思是:“当写一个Colorobject时,写指令让以后的解码器在Colorclass中寻找一个需要四次double的构造函数,然后写出实际值传递将来分别调用 Color object 的 getRed、getGreen、getBlue 和 getOpacity 方法。”

如果您希望您的形状包含 Text objects, you can add a persistence delegate for the Font class:

encoder.setPersistenceDelegate(Font.class,
    new DefaultPersistenceDelegate(
        new String[] { "name", "size" }));

您还可以为其他 Paint 实现添加持久性委托:

encoder.setPersistenceDelegate(LinearGradient.class,
    new DefaultPersistenceDelegate(new String[] {
        "startX", "startY", "endX", "endY",
        "proportional", "cycleMethod", "stops"
    }));
encoder.setPersistenceDelegate(RadialGradient.class,
    new DefaultPersistenceDelegate(new String[] {
        "focusAngle", "focusDistance", "centerX", "centerY",
        "radius", "proportional", "cycleMethod", "stops"
    }));

(我特意省略了 ImagePattern,因为虽然可以用 XML 表示一个图像,但它丑陋且效率低下。如果你打算存储图像,XML 不是一个好的存储格式.)

因此,加载和存储方法的更新版本如下所示:

private static void addPersistenceDelegatesTo(Encoder encoder) {
    encoder.setPersistenceDelegate(Font.class,
        new DefaultPersistenceDelegate(
            new String[] { "name", "size" }));
    encoder.setPersistenceDelegate(Color.class,
        new DefaultPersistenceDelegate(
            new String[] { "red", "green", "blue", "opacity" }));
    encoder.setPersistenceDelegate(LinearGradient.class,
        new DefaultPersistenceDelegate(new String[] {
            "startX", "startY", "endX", "endY",
            "proportional", "cycleMethod", "stops"
        }));
    encoder.setPersistenceDelegate(RadialGradient.class,
        new DefaultPersistenceDelegate(new String[] {
            "focusAngle", "focusDistance", "centerX", "centerY",
            "radius", "proportional", "cycleMethod", "stops"
        }));
}

private static final java.nio.file.Path SAVE_FILE_LOCATION =
    Paths.get(System.getProperty("user.home"), "shapes.xml");

void save()
throws IOException {
    try (XMLEncoder encoder = new XMLEncoder(
        new BufferedOutputStream(
            Files.newOutputStream(SAVE_FILE_LOCATION)))) {

        encoder.setExceptionListener(e -> {
            throw new RuntimeException(e);
        });

        addPersistenceDelegatesTo(encoder);

        encoder.writeObject(pane.getChildren().toArray(new Node[0]));
    }
}

void load()
throws IOException {
    try (XMLDecoder decoder = new XMLDecoder(
        new BufferedInputStream(
            Files.newInputStream(SAVE_FILE_LOCATION)))) {

        decoder.setExceptionListener(e -> {
            throw new RuntimeException(e);
        });

        pane.getChildren().setAll((Node[]) decoder.readObject());
    }
}