【问题标题】:How to serialize Java 2D Shape objects as XML?如何将 Java 2D Shape 对象序列化为 XML?
【发布时间】:2014-10-27 00:52:18
【问题描述】:

Shape 接口由 Java 2D 对象(Arc2DAreaCubicCurve2DEllipse2DGeneralPath 等)实现。

一些具体的对象被标记为Serializable,并且可以使用对象序列化来存储和恢复,但是像Area这样的其他对象没有实现接口并抛出错误。

但由于我们经常被警告说,这种幼稚的序列化在 Java 实现或版本中不一定是稳定的,所以我更喜欢使用某种形式的序列化。

这导致我们使用 XMLEncoderXMLDecoder 从 XML 存储/恢复,但这能够处理更少的 Java 2D Shape 对象。

两者的一些结果可以在下面看到。我们从 6 个形状开始,并尝试通过对象序列化和标准 XML 序列化来存储/恢复它们。

我们如何通过 XML 正确存储所有 Shape 对象?

import java.awt.*;
import java.awt.geom.*;
import java.awt.image.BufferedImage;
import java.beans.*;
import java.io.*;
import java.util.ArrayList;
import javax.swing.*;
import javax.swing.border.TitledBorder;

public class Serialize2D {

    private JPanel ui;

    Serialize2D() {
        initUI();
    }

    public void initUI() {
        if (ui != null) {
            return;
        }
        ui = new JPanel(new GridLayout(0, 1));

        int[] xpoints = {205, 295, 205, 295};
        int[] ypoints = {5, 25, 25, 45};
        Polygon polygon = new Polygon(xpoints, ypoints, xpoints.length);

        ArrayList<Shape> shapes = new ArrayList<Shape>();
        int w = 45;
        shapes.add(new Rectangle2D.Double(5, 5, 90, 40));
        shapes.add(new Ellipse2D.Double(105, 5, 90, 40));
        shapes.add(polygon);
        shapes.add(new GeneralPath(new Rectangle2D.Double(5, 55, 90, 40)));
        shapes.add(new Path2D.Double(new Rectangle2D.Double(105, 55, 90, 40)));
        shapes.add(new Area(new Rectangle2D.Double(205, 55, 90, 40)));

        addTitledLabelToPanel(shapes, "Original Shapes");
        addTitledLabelToPanel(
                serializeToFromObject(shapes), "Serialize via Object");
        addTitledLabelToPanel(
                serializeToFromXML(shapes), "Serialize via XML");
    }

    public JComponent getUI() {
        return ui;
    }

    public ArrayList<Shape> serializeToFromObject(ArrayList<Shape> shapes) {
        ArrayList<Shape> shps = new ArrayList<Shape>();
        try {
            ObjectOutputStream oos = null;
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            oos = new ObjectOutputStream(baos);
            for (Shape shape : shapes) {
                try {
                    oos.writeObject(shape);
                } catch (Exception ex) {
                    System.err.println(ex.toString());
                }
            }
            oos.flush();
            oos.close();
            System.out.println("length Obj: " + baos.toByteArray().length);
            ByteArrayInputStream bais = new ByteArrayInputStream(
                    baos.toByteArray());
            ObjectInputStream ois = new ObjectInputStream(bais);

            Object o = null;
            try {
                o = ois.readObject();
            } catch (NotSerializableException ex) {
                System.err.println(ex.getMessage());
            } catch (ClassNotFoundException ex) {
                ex.printStackTrace();
            }
            while (o != null) {
                shps.add((Shape) o);
                try {
                    o = ois.readObject();
                } catch (NotSerializableException ex) {
                    System.err.println(ex.getMessage());
                } catch (ClassNotFoundException ex) {
                    ex.printStackTrace();
                }
            }
            return shps;
        } catch (IOException ex) {
            ex.printStackTrace();
        }
        return shps;
    }

    public ArrayList<Shape> serializeToFromXML(ArrayList<Shape> shapes) {
        ArrayList<Shape> shps = new ArrayList<Shape>();
        try {
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            XMLEncoder xmle = new XMLEncoder(baos);
            for (Shape shape : shapes) {
                xmle.writeObject(shape);
            }
            xmle.flush();
            xmle.close();

            System.out.println("length XML: " + baos.toByteArray().length);
            ByteArrayInputStream bais
                    = new ByteArrayInputStream(baos.toByteArray());
            XMLDecoder xmld = new XMLDecoder(bais);
            Shape shape = (Shape) xmld.readObject();
            while (shape != null) {
                shps.add(shape);
                try {
                    shape = (Shape) xmld.readObject();
                } catch (ArrayIndexOutOfBoundsException aioobe) {
                    // we've read last object
                    shape = null;
                }
            }
            xmld.close();
        } catch (Exception ex) {
            ex.printStackTrace();
        }
        return shps;
    }

    private final static String getType(Object o) {
        String s = o.getClass().getName();
        String[] parts = s.split("\\.");
        s = parts[parts.length - 1].split("\\$")[0];
        return s;
    }

    public static void drawShapesToImage(
            ArrayList<Shape> shapes, BufferedImage image) {
        Graphics2D g = image.createGraphics();
        g.setRenderingHint(
                RenderingHints.KEY_ANTIALIASING,
                RenderingHints.VALUE_ANTIALIAS_ON);

        g.setColor(Color.WHITE);
        g.fillRect(0, 0, image.getWidth(), image.getHeight());
        for (Shape shape : shapes) {
            String s = getType(shape);
            g.setColor(Color.GREEN);
            g.fill(shape);
            g.setColor(Color.BLACK);
            g.draw(shape);
            Rectangle r = shape.getBounds();
            int x = r.x + 5;
            int y = r.y + 16;
            if (r.width * r.height != 0) {
                g.drawString(s, x, y);
            }
        }

        g.dispose();
    }

    private void addTitledLabelToPanel(ArrayList<Shape> shapes, String title) {
        int w = 300;
        int h = 100;
        BufferedImage bi = new BufferedImage(w, h, BufferedImage.TYPE_INT_RGB);
        drawShapesToImage(shapes, bi);
        JLabel l = new JLabel(new ImageIcon(bi));
        l.setBorder(new TitledBorder(title));
        ui.add(l);
    }

    public static void main(String[] args) {
        Runnable r = new Runnable() {

            @Override
            public void run() {
                Serialize2D ss = new Serialize2D();
                JOptionPane.showMessageDialog(null, ss.getUI());
            }
        };
        SwingUtilities.invokeLater(r);
    }
}

【问题讨论】:

    标签: java xml swing awt java-2d


    【解决方案1】:

    不幸的是,使用XMLEncoder/DecoderShape 简单地编码/解码为XML 通常会破坏Shape 的所有重要信息!

    为此,仍然使用上述类,我们序列化并恢复正确构造的 bean,这些 bean 代表从 PathIterator 获得的形状部分。这些 bean 是: p>

    • PathBean 存储 PathSegment 对象的集合,这些对象形成 Java-2D Shape 的形状。
    • PathSegment 存储路径特定部分的详细信息(段类型、缠绕规则和坐标)。

    SerializeShapes图形界面

    用于演示存储和恢复形状的 GUI。

    • 单击 椭圆 (Ellipse2D)、矩形 (Rectangle2D) 或 人脸 (Area) 按钮次。
    • 退出 GUI。形状将被序列化到磁盘。
    • 重新启动 GUI。上次随机绘制的形状将从磁盘中恢复并重新出现在 GUI 中。

    选中的形状将填充为绿色,其他形状为红色。

    package serialize2d;
    
    import java.awt.*;
    import java.awt.event.*;
    import java.awt.font.FontRenderContext;
    import java.awt.geom.AffineTransform;
    import java.awt.geom.*;
    import java.awt.image.BufferedImage;
    import java.io.FileNotFoundException;
    import java.util.ArrayList;
    import java.util.Enumeration;
    import java.util.Random;
    import java.util.Vector;
    import javax.swing.*;
    import javax.swing.border.EmptyBorder;
    import javax.swing.event.*;
    
    
    /** A GUI to make it easy to add/remove shapes from a canvas. 
     It should persist the shapes between runs.  */
    public class SerializeShapes {
    
        JPanel ui;
        JPanel shapePanel;
        Random rand;
        JPanel shapeCanvas;
        DefaultListModel<Shape> allShapesModel;
        ListSelectionModel shapeSelectionModel;
        RenderingHints renderingHints;
    
        SerializeShapes() {
            initUI();
        }
    
        public void initUI() {
            if (ui != null) {
                return;
            }
            renderingHints = new RenderingHints(RenderingHints.KEY_DITHERING,
                    RenderingHints.VALUE_DITHER_ENABLE);
            renderingHints.put(RenderingHints.KEY_ANTIALIASING,
                    RenderingHints.VALUE_ANTIALIAS_ON);
            renderingHints.put(RenderingHints.KEY_ALPHA_INTERPOLATION,
                    RenderingHints.VALUE_ALPHA_INTERPOLATION_QUALITY);
            renderingHints.put(RenderingHints.KEY_COLOR_RENDERING,
                    RenderingHints.VALUE_COLOR_RENDER_QUALITY);
            renderingHints.put(RenderingHints.KEY_RENDERING,
                    RenderingHints.VALUE_RENDER_QUALITY);
            renderingHints.put(RenderingHints.KEY_STROKE_CONTROL,
                    RenderingHints.VALUE_STROKE_NORMALIZE);
            ui = new JPanel(new BorderLayout(4, 4));
            ui.setBorder(new EmptyBorder(4, 4, 4, 4));
    
            JPanel controls = new JPanel(new FlowLayout(FlowLayout.CENTER, 4, 4));
            ui.add(controls, BorderLayout.PAGE_START);
            shapeCanvas = new ShapeCanvas();
            ui.add(shapeCanvas);
            rand = new Random();
    
            allShapesModel = new DefaultListModel<Shape>();
            JList<Shape> allShapes = new JList<Shape>(allShapesModel);
            allShapes.setCellRenderer(new ShapeListCellRenderer());
            shapeSelectionModel = allShapes.getSelectionModel();
            shapeSelectionModel.setSelectionMode(
                    ListSelectionModel.SINGLE_SELECTION);
            ListSelectionListener shapesSelectionListener
                    = new ListSelectionListener() {
    
                        @Override
                        public void valueChanged(ListSelectionEvent e) {
                            shapeCanvas.repaint();
                        }
                    };
            allShapes.addListSelectionListener(shapesSelectionListener);
    
            JScrollPane shapesScroll = new JScrollPane(
                    allShapes,
                    JScrollPane.VERTICAL_SCROLLBAR_ALWAYS,
                    JScrollPane.HORIZONTAL_SCROLLBAR_NEVER
            );
            // TODO fix this hack..
            shapesScroll.getViewport().setPreferredSize(new Dimension(60, 200));
            ui.add(shapesScroll, BorderLayout.LINE_START);
    
            Action addEllipse = new AbstractAction("Ellipse") {
    
                @Override
                public void actionPerformed(ActionEvent e) {
                    int w = rand.nextInt(100) + 10;
                    int h = rand.nextInt(100) + 10;
                    int x = rand.nextInt(shapeCanvas.getWidth() - w);
                    int y = rand.nextInt(shapeCanvas.getHeight() - h);
                    Ellipse2D ellipse = new Ellipse2D.Double(x, y, w, h);
                    addShape(ellipse);
                }
            };
            addEllipse.putValue(Action.MNEMONIC_KEY, KeyEvent.VK_E);
    
            Action addRectangle = new AbstractAction("Rectangle") {
    
                @Override
                public void actionPerformed(ActionEvent e) {
                    int w = rand.nextInt(100) + 10;
                    int h = rand.nextInt(100) + 10;
                    int x = rand.nextInt(shapeCanvas.getWidth() - w);
                    int y = rand.nextInt(shapeCanvas.getHeight() - h);
                    Rectangle2D rectangle = new Rectangle2D.Double(x, y, w, h);
                    addShape(rectangle);
                }
            };
            addRectangle.putValue(Action.MNEMONIC_KEY, KeyEvent.VK_R);
    
            final int faceStart = 128513;
            final int faceEnd = 128528;
            final int diff = faceEnd - faceStart;
            StringBuilder sb = new StringBuilder();
            for (int count = faceStart; count <= faceEnd; count++) {
                sb.append(Character.toChars(count));
            }
            final String s = sb.toString();
            Vector<Font> compatibleFontList = new Vector<Font>();
            GraphicsEnvironment ge
                    = GraphicsEnvironment.getLocalGraphicsEnvironment();
            Font[] fonts = ge.getAllFonts();
            for (Font font : fonts) {
                if (font.canDisplayUpTo(s) < 0) {
                    compatibleFontList.add(font);
                }
            }
            JComboBox fontChooser = new JComboBox(compatibleFontList);
            ListCellRenderer fontRenderer = new DefaultListCellRenderer() {
    
                @Override
                public Component getListCellRendererComponent(
                        JList list, Object value, int index,
                        boolean isSelected, boolean cellHasFocus) {
                    Component c = super.getListCellRendererComponent(
                            list, value, index,
                            isSelected, cellHasFocus);
                    JLabel l = (JLabel) c;
                    Font font = (Font) value;
                    l.setText(font.getName());
                    return l;
                }
            };
            fontChooser.setRenderer(fontRenderer);
            final ComboBoxModel<Font> fontModel = fontChooser.getModel();
    
            BufferedImage bi = new BufferedImage(1, 1, BufferedImage.TYPE_INT_RGB);
            Graphics2D g = bi.createGraphics();
            final FontRenderContext fontRenderContext = g.getFontRenderContext();
    
            Action addFace = new AbstractAction("Face") {
    
                @Override
                public void actionPerformed(ActionEvent e) {
                    int codepoint = faceStart + rand.nextInt(diff);
                    String text = new String(Character.toChars(codepoint));
    
                    Font font = (Font) fontModel.getSelectedItem();
                    Area area = new Area(
                            font.deriveFont(80f).
                            createGlyphVector(fontRenderContext, text).
                            getOutline());
                    Rectangle bounds = area.getBounds();
                    float x = rand.nextInt(
                            shapeCanvas.getWidth() - bounds.width) - bounds.x;
                    float y = rand.nextInt(
                            shapeCanvas.getHeight() - bounds.height) - bounds.y;
                    AffineTransform move = AffineTransform.
                            getTranslateInstance(x, y);
                    area.transform(move);
                    addShape(area);
                }
            };
            addFace.putValue(Action.MNEMONIC_KEY, KeyEvent.VK_F);
    
            Action delete = new AbstractAction("Delete") {
    
                @Override
                public void actionPerformed(ActionEvent e) {
                    int idx = shapeSelectionModel.getMinSelectionIndex();
                    if (idx < 0) {
                        JOptionPane.showMessageDialog(
                                ui,
                                "Select a shape to delete",
                                "Select a Shape",
                                JOptionPane.ERROR_MESSAGE);
                    } else {
                        allShapesModel.removeElementAt(idx);
                        shapeCanvas.repaint();
                    }
                }
            };
            delete.putValue(Action.MNEMONIC_KEY, KeyEvent.VK_D);
    
            controls.add(new JButton(addEllipse));
            controls.add(new JButton(addRectangle));
            controls.add(new JButton(addFace));
            controls.add(fontChooser);
            controls.add(new JButton(delete));
    
            try {
                ArrayList<Shape> shapes = deserializeShapes();
                for (Shape shape : shapes) {
                    allShapesModel.addElement(shape);
                }
            } catch (Exception ex) {
                System.err.println("If first launch, this is as expected!");
                ex.printStackTrace();
            }
        }
    
        private void addShape(Shape shape) {
            allShapesModel.addElement(shape);
            int size = allShapesModel.getSize() - 1;
            shapeSelectionModel.addSelectionInterval(size, size);
        }
    
        class ShapeCanvas extends JPanel {
    
            ShapeCanvas() {
                setBackground(Color.WHITE);
            }
    
            @Override
            public void paintComponent(Graphics g) {
                super.paintComponent(g);
                Graphics2D g2 = (Graphics2D) g;
                g2.setRenderingHints(renderingHints);
                Stroke stroke = new BasicStroke(1.5f);
                g2.setStroke(stroke);
                int idx = shapeSelectionModel.getMinSelectionIndex();
                Shape selectedShape = null;
                if (idx > -1) {
                    selectedShape = allShapesModel.get(idx);
                }
                Enumeration en = allShapesModel.elements();
                while (en.hasMoreElements()) {
                    Shape shape = (Shape) en.nextElement();
                    if (shape.equals(selectedShape)) {
                        g2.setColor(new Color(0, 255, 0, 191));
                    } else {
                        g2.setColor(new Color(255, 0, 0, 191));
                    }
                    g2.fill(shape);
                    g2.setColor(new Color(0, 0, 0, 224));
                    g2.draw(shape);
                }
            }
    
            @Override
            public Dimension getPreferredSize() {
                return new Dimension(500, 300);
            }
        }
    
        public JComponent getUI() {
            return ui;
        }
    
        public static void main(String[] args) {
            Runnable r = new Runnable() {
                @Override
                public void run() {
                    SerializeShapes se = new SerializeShapes();
    
                    JFrame f = new JFrame("Serialize Shapes");
                    f.addWindowListener(new SerializeWindowListener(se));
                    f.setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE);
                    f.setContentPane(se.getUI());
                    f.setResizable(false);
                    f.pack();
                    f.setLocationByPlatform(true);
                    f.setVisible(true);
                }
            };
            SwingUtilities.invokeLater(r);
        }
    
        public void serializeShapes() throws FileNotFoundException {
            ArrayList<Shape> shapes
                    = new ArrayList<Shape>();
            Enumeration en = allShapesModel.elements();
            while (en.hasMoreElements()) {
                Shape shape = (Shape) en.nextElement();
                shapes.add(shape);
            }
            ShapeIO.serializeShapes(shapes, this.getClass());
            try {
                Desktop.getDesktop().open(
                        ShapeIO.getSerializeFile(this.getClass()));
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    
        public ArrayList<Shape> deserializeShapes() throws FileNotFoundException {
            return ShapeIO.deserializeShapes(this.getClass());
        }
    
        class ShapeListCellRenderer extends DefaultListCellRenderer {
    
            @Override
            public Component getListCellRendererComponent(
                    JList<? extends Object> list, Object value,
                    int index, boolean isSelected, boolean cellHasFocus) {
                Component c = super.getListCellRendererComponent(list, value, index,
                        isSelected, cellHasFocus);
                JLabel l = (JLabel) c;
                Shape shape = (Shape) value;
                ShapeIcon icon = new ShapeIcon(shape, 40);
                l.setIcon(icon);
                l.setText("");
    
                return l;
            }
        }
    
        class ShapeIcon implements Icon {
    
            Shape shape;
            int size;
    
            ShapeIcon(Shape shape, int size) {
                this.shape = shape;
                this.size = size;
            }
    
            @Override
            public void paintIcon(Component c, Graphics g, int x, int y) {
                Graphics2D g2 = (Graphics2D) g;
                g2.setRenderingHints(renderingHints);
                Rectangle bounds = shape.getBounds();
                int xOff = -bounds.x;
                int yOff = -bounds.y;
                double xRatio = (double) bounds.width / (double) size;
                double yRatio = (double) bounds.height / (double) size;
                double ratio = xRatio > yRatio ? xRatio : yRatio;
                AffineTransform scale = AffineTransform.getScaleInstance(1 / ratio, 1 / ratio);
                AffineTransform shift = AffineTransform.getTranslateInstance(xOff, yOff);
    
                AffineTransform totalTransform = new AffineTransform();
    
                totalTransform.concatenate(scale);
                totalTransform.concatenate(shift);
    
                Area b = new Area(shape).createTransformedArea(totalTransform);
                bounds = b.getBounds();
    
                g2.setColor(Color.BLACK);
                g2.fill(b);
            }
    
            @Override
            public int getIconWidth() {
                return size;
            }
    
            @Override
            public int getIconHeight() {
                return size;
            }
        }
    }
    
    class SerializeWindowListener extends WindowAdapter {
    
        SerializeShapes serializeShapes;
    
        SerializeWindowListener(SerializeShapes serializeShapes) {
            this.serializeShapes = serializeShapes;
        }
    
        @Override
        public void windowClosing(WindowEvent e) {
            try {
                serializeShapes.serializeShapes();
            } catch (FileNotFoundException ex) {
                ex.printStackTrace();
                System.exit(1);
            }
            System.exit(0);
        }
    }
    

    ShapeIO

    执行与 XML 的 I/O。

    package serialize2d;
    
    import java.awt.Shape;
    import java.beans.*;
    import java.io.*;
    import java.util.ArrayList;
    
    public class ShapeIO {
    
        /** Save the list of shapes to the file system. */
        public static void serializeShapes(
                ArrayList<Shape> shapes, Class serializeClass) 
                throws FileNotFoundException {
            File f = getSerializeFile(serializeClass);
            XMLEncoder xmle = new XMLEncoder(new FileOutputStream(f));
    
            ArrayList<PathBean> pathSegmentsCollection = new ArrayList<>();
            for (Shape shape : shapes) {
                ArrayList<PathSegment> pathSegments = 
                        BeanConverter.getSegmentsFromShape(shape);
                PathBean as = new PathBean(pathSegments);
                pathSegmentsCollection.add(as);
            }
    
            xmle.writeObject(pathSegmentsCollection);
            xmle.flush();
            xmle.close();
        }
    
        /** Load the list of shapes from the file system. */
        public static ArrayList<Shape> deserializeShapes(Class serializeClass) 
                throws FileNotFoundException {
            File f = getSerializeFile(serializeClass);
            XMLDecoder xmld = new XMLDecoder(new FileInputStream(f));
            ArrayList<PathBean> pathSegmentsCollection
                    = (ArrayList<PathBean>) xmld.readObject();
            ArrayList<Shape> shapes = new ArrayList<Shape>();
            for (PathBean pathSegments : pathSegmentsCollection) {
                shapes.add(BeanConverter.getShapeFromSegments(pathSegments));
            }
    
            return shapes;
        }
    
        /** Provide an unique, reproducible & readable/writable path for a class. */
        public static File getSerializeFile(Class serializeClass) {
            File f = new File(System.getProperty("user.home"));
            String[] nameParts = serializeClass.getCanonicalName().split("\\.");
    
            f = new File(f, "java");
            for (String namePart : nameParts) {
                f = new File(f, namePart);
            }
            f.mkdirs();
            f = new File(f, nameParts[nameParts.length-1] + ".xml");
    
            return f;
        }
    }
    

    BeanConverter

    Shape 获取PathIterator 并将其转换为可序列化的bean。将 bean 转换回 GeneralPath

    package serialize2d;
    
    import java.awt.Shape;
    import java.awt.geom.*;
    import java.util.ArrayList;
    
    /** Utility class to convert bean to/from a Shape. */
    public class BeanConverter {
    
        /** Convert a shape to a serializable bean.  */
        public static ArrayList<PathSegment> getSegmentsFromShape(Shape shape) {
            ArrayList<PathSegment> shapeSegments = new ArrayList<PathSegment>();
            for (
                    PathIterator pi = shape.getPathIterator(null); 
                    !pi.isDone(); 
                    pi.next()) {
                double[] coords = new double[6];
                int pathSegmentType = pi.currentSegment(coords);
                int windingRule = pi.getWindingRule();
                PathSegment as = new PathSegment(
                        pathSegmentType, windingRule, coords);
                shapeSegments.add(as);
            }
            return shapeSegments;
        }
    
        /** Convert a serializable bean to a shape.  */
        public static Shape getShapeFromSegments(PathBean shapeSegments) {
            GeneralPath gp = new GeneralPath();
            for (PathSegment shapeSegment : shapeSegments.getPathSegments()) {
                double[] coords = shapeSegment.getCoords();
                int pathSegmentType = shapeSegment.getPathSegmentType();
                int windingRule = shapeSegment.getWindingRule();
                gp.setWindingRule(windingRule);
                if (pathSegmentType == PathIterator.SEG_MOVETO) {
                    gp.moveTo(coords[0], coords[1]);
                } else if (pathSegmentType == PathIterator.SEG_LINETO) {
                    gp.lineTo(coords[0], coords[1]);
                } else if (pathSegmentType == PathIterator.SEG_QUADTO) {
                    gp.quadTo(coords[0], coords[1], coords[2], coords[3]);
                } else if (pathSegmentType == PathIterator.SEG_CUBICTO) {
                    gp.curveTo(
                            coords[0], coords[1], coords[2], 
                            coords[3], coords[4], coords[5]);
                } else if (pathSegmentType == PathIterator.SEG_CLOSE) {
                    gp.closePath();
                } else {
                    System.err.println("Unexpected value! " + pathSegmentType);
                }
            }
            return gp;
        }
    }
    

    PathBean

    将路径段的集合存储在可序列化的 bean 中。

    package serialize2d;
    
    import java.awt.geom.*;
    import java.util.ArrayList;
    
    /** PathBean stores the collection of PathSegment objects
     that constitute the path of a Shape. */
    public class PathBean {
    
        public ArrayList<PathSegment> pathSegments;
    
        public PathBean() {}
    
        public PathBean(ArrayList<PathSegment> pathSegments) {
            this.pathSegments = pathSegments;
        }
    
        public ArrayList<PathSegment> getPathSegments() {
            return pathSegments;
        }
    
        public void setPathSegments(ArrayList<PathSegment> pathSegments) {
            this.pathSegments = pathSegments;
        }
    
        @Override
        public String toString() {
            StringBuilder sb = new StringBuilder("{");
            for (PathSegment pathSegment : pathSegments) {
                sb.append(" \n\t");
                sb.append(pathSegment.toString());
            }
            sb.append(" \n");
            sb.append("}");
            return "PathSegments: " + sb.toString();
        }
    }
    

    PathSegment

    存储整个路径的一部分的路径段。

    package serialize2d;
    
    import java.util.Arrays;
    
    /** PathSegment bean stores the detail on one segment of the path
     that constitutes a Shape. */
    public class PathSegment {
    
        public int pathSegmentType;
        public int windingRule;
        public double[] coords;
    
        public PathSegment() {}
    
        public PathSegment(int pathSegmentType, int windingRule, double[] coords) {
            this.pathSegmentType = pathSegmentType;
            this.windingRule = windingRule;
            this.coords = coords;
        }
    
        public int getPathSegmentType() {
            return pathSegmentType;
        }
    
        public void setPathSegmentType(int pathSegmentType) {
            this.pathSegmentType = pathSegmentType;
        }
    
        public int getWindingRule() {
            return windingRule;
        }
    
        public void setWindingRule(int windingRule) {
            this.windingRule = windingRule;
        }
    
        public double[] getCoords() {
            return coords;
        }
    
        public void setCoords(double[] coords) {
            this.coords = coords;
        }
    
        @Override
        public String toString() {
            String sC = (coords != null ? "" : Arrays.toString(coords));
            String s = String.format(
                    "PathSegment: Path Segment Type:- %d \t"
                    + "Winding Rule:- %d \tcoords:- %s",
                    getPathSegmentType(), getWindingRule(), sC);
            return s;
        }
    }
    

    注意事项

    这旨在作为概念证明,而不是一种完善的方法。

    • XML 序列化数据变大真的很快,通常会被压缩。 Zip 压缩可能会减少 30-40% 的序列化对象或类文件的字节大小,但减少 80-95% 的 XML。无论如何,zip 也适用于下一点。
    • 对于我们希望序列化和恢复形状的项目类型,我们可能希望包含更多详细信息形状(例如填充颜色或纹理以及绘制颜色或描边等)以及其他数据,如图像或字体。这也是 Zip 派上用场的地方,因为我们可以将它们全部放在同一个存档中,每个存档都具有最佳压缩级别(例如 XML 的标准和图像的无标准)。

    可以从我的云盘下载zip archive of the source files in this answer

    【讨论】:

    • 谢谢 OP.. 顺便说一句,出于好奇.. 你是在使用 JAVA 6 还是有什么理由避免使用钻石运算符和 try-with-recourse?
    • @Payam 我喜欢让我的示例使用尽可能低的 Java 版本。
    • 此 XML 序列化仅保留对象的 Shape。序列化对象的class全部转换为GeneralPath对象。只要只需要形状的内容,就可以了。但是如果用户希望序列化一个包含Area属性的自定义类,反序列化将尝试将GeneralPath存储在Area属性中,反序列化将失败。
    【解决方案2】:

    自定义PersistenceDelegate 可与XMLEncoder 一起使用,将Path2DGeneralPath 序列化为XML。

    考虑以下 XML:

    <?xml version="1.0" encoding="UTF-8"?>
    <java version="1.8.0_60" class="java.beans.XMLDecoder">
     <object class="java.awt.geom.Path2D$Float">
      <void property="windingRule">
       <int>0</int>
      </void>
      <void method="moveTo">
       <float>1.0</float>
       <float>1.0</float>
      </void>
      <void method="lineTo">
       <float>2.0</float>
       <float>0.0</float>
      </void>
      <void method="lineTo">
       <float>0.0</float>
       <float>3.0</float>
      </void>
      <void method="closePath"/>
     </object>
    </java>
    

    当被XMLEncoder 实例读取时,将执行以下命令...

    Path2D.Float object = new Path2D.Float();
    object.setWindingRule(0); // Note: 0 => Path2D.WIND_EVEN_ODD
    object.moveTo(1.0, 1.0);
    object.lineTo(2.0, 0.0);
    object.lineTo(0.0, 3.0);
    object.closePath();
    

    ...XMLDecoder.readObject() 将返回一个闭合三角形对象。

    基于此,我们可以得出结论,XMLDecoder 已经可以反序列化 Path2D 形状,如果它被正确编码的话。 XMLEncoder 现在能为我们做什么?

    Path2D.Float path = new Path2D.Float(GeneralPath.WIND_EVEN_ODD, 10);
    path.moveTo(1, 1);
    path.lineTo(2, 0);
    path.lineTo(0, 3);
    path.closePath();
    
    try (XMLEncoder xml = new XMLEncoder(System.out)) {
        xml.writeObject(path);
    }
    

    这会产生以下 XML:

    <?xml version="1.0" encoding="UTF-8"?>
    <java version="1.8.0_60" class="java.beans.XMLDecoder">
     <object class="java.awt.geom.Path2D$Float">
      <void property="windingRule">
       <int>0</int>
      </void>
     </object>
    </java>
    

    不是很好,但也不算太差。我们只是缺少路径数据。所以我们只需要扩展DefaultPersistenceDelegate 以将所需的路径命令添加到输出中。

    public class Path2DPersistenceDelegate extends DefaultPersistenceDelegate {
    
        @Override
        protected void initialize(Class<?> cls, Object oldInstance, Object newInstance, Encoder out) {
            super.initialize(cls, oldInstance, newInstance, out);
    
            Shape shape = (Shape) oldInstance;
    
            float coords[] = new float[6];
            Float pnt0[] = new Float[0];
            Float pnt1[] = new Float[2];
            Float pnt2[] = new Float[4];
            Float pnt3[] = new Float[6];
            Float pnts[];
    
            PathIterator iterator = shape.getPathIterator(null);
            while (!iterator.isDone()) {
                int type = iterator.currentSegment(coords);
                String cmd;
                switch (type) {
                case PathIterator.SEG_CLOSE:
                    cmd = "closePath";
                    pnts = pnt0;
                    break;
                case PathIterator.SEG_MOVETO:
                    cmd = "moveTo";
                    pnts = pnt1;
                    break;
                case PathIterator.SEG_LINETO:
                    cmd = "lineTo";
                    pnts = pnt1;
                    break;
                case PathIterator.SEG_QUADTO:
                    cmd = "quadTo";
                    pnts = pnt2;
                    break;
                case PathIterator.SEG_CUBICTO:
                    cmd = "curveTo";
                    pnts = pnt3;
                    break;
                default:
                    throw new IllegalStateException("Unexpected segment type: " + type);
                }
    
                for (int i = 0; i < pnts.length; i++) {
                    pnts[i] = coords[i];
                }
    
                out.writeStatement(new Statement(oldInstance, cmd, pnts));
                iterator.next();
            }
        }
    }
    

    然后,我们只需向XMLEncoder 注册这个持久性委托,它就会生成本文顶部所示的 XML。

    Path2DPersistenceDelegate path2d_delegate = new Path2DPersistenceDelegate();
    try (XMLEncoder xml = new XMLEncoder(System.out)) {
        xml.setPersistenceDelegate(Path2D.Float.class, path2d_delegate);
        xml.writeObject(path);
    }
    

    由于Path2D.FloatGeneralPath 的父类,GeneralPath 也将被正确编码。如果要正确编码 Path2D.Double 形状,则需要修改委托以使用 double 值和 Double 对象。


    更新:

    要使用正确的windingRule 属性构造Path2D.Float 对象,而不是事后设置该属性,请将以下构造函数添加到Path2DPersistenceDelegate

    public Path2DPersistenceDelegate() {
        super(new String[] { "windingRule" });
    }
    

    然后 XML 将读取:

    ...
     <object class="java.awt.geom.Path2D$Float">
      <int>0</int>
      <void method="moveTo">
      ...
    

    这会丢失 XML 中一些人类可读的上下文信息;人们需要阅读文档来确定使用 Path2D.Float(int) 构造函数时,int 参数是 windingRule 属性。


    更新 2:

    Polygon 持久性委托相当简单:

    public class PolygonPersistenceDelegate extends PersistenceDelegate {
    
        @Override
        protected Expression instantiate(Object oldInstance, Encoder out) {
            Polygon polygon = (Polygon) oldInstance;
    
            return new Expression(oldInstance, oldInstance.getClass(), "new",
                    new Object[] { polygon.xpoints, polygon.ypoints, polygon.npoints });
        }
    }
    

    由于Area Constructive Area Geometry 对象更复杂,它不能通过moveTolineTo 类型的方法创建,而只能通过添加、减去或异或Shape 对象来创建。但是构造函数接受Shape 对象,而Path2D.Double 可以从Area 对象构造,所以持久化委托实际上也可以很简单地编写:

    public class AreaPersistenceDelegate extends PersistenceDelegate {
    
        @Override
        protected Expression instantiate(Object oldInstance, Encoder out) {
            Area area = (Area) oldInstance;
            Path2D.Double p2d = new Path2D.Double(area);
    
            return new Expression(oldInstance, oldInstance.getClass(), "new",
                    new Object[] { p2d });
        }
    }
    

    由于我们在内部使用Path2D.Double,因此我们需要将两个持久委托都添加到XMLEncoder

    try (XMLEncoder encoder = new XMLEncoder(baos)) {
        encoder.setPersistenceDelegate(Area.class, new AreaPersistenceDelegate());
        encoder.setPersistenceDelegate(Path2D.Double.class, new Path2DPersistenceDelegate.Double());
        encoder.writeObject(area);
    }
    

    更新 3:

    已在 GitHub 上创建了一个带有 PersistenceDelegate 的项目,用于 AreaPath2DGeneralPath

    注意事项:

    • Polygon 的持久性委托已删除,因为 Java 1.7 似乎不需要它

    更新 4:

    对于Java 1.7,必须为每个new Statement()分配pnts数组;它不能分配一次并重复使用。因此,Path2D 代理必须按如下方式更改:

            float coords[] = new float[6];
            /* Removed: Float pnt0[] = new Float[0];
                        Float pnt1[] = new Float[0];
                        Float pnt2[] = new Float[4];
                        Float pnt3[] = new Float[6]; */
            Float pnts[];
    
            PathIterator iterator = shape.getPathIterator(null);
            while (!iterator.isDone()) {
                int type = iterator.currentSegment(coords);
                String cmd;
                switch (type) {
                case PathIterator.SEG_CLOSE:
                    cmd = "closePath";
                    pnts = new Float[0];  // Allocate for each segment
                    break;
                case PathIterator.SEG_MOVETO:
                    cmd = "moveTo";
                    pnts = new Float[2];  // Allocate for each segment
                    break;
                /* ... etc ...*/
    

    【讨论】:

    • 不错的答案! :)
    • @AndrewThompson 谢谢。真正找到一个我可以贡献的重要领域感觉很好。 (如果得到赏金奖励就更好了。非常感谢。)我正在网上整理一个包,它将所有代表的来源放在一个地方。完成后我会添加一个链接。
    • “我会在它完成后添加一个链接。” 很棒的东西。我很期待。 “能真正找到我可以贡献的重要领域的感觉真好。” 有一个领域的专家,在我简陋的问答中提交他们的智慧也感觉很好。
    猜你喜欢
    • 1970-01-01
    • 2011-12-03
    • 2019-03-04
    • 2018-10-11
    • 2017-08-20
    • 2011-01-07
    • 2017-07-16
    • 2011-03-09
    • 2011-05-06
    相关资源
    最近更新 更多