Stocker des objets graphiques est-il une bonne idée?

Je suis actuellement en train d'écrire un programme paint en java, conçu pour avoir des fonctionnalités flexibles et complètes. Il découle de mon projet final, que j'ai écrit la veille. Pour cette raison, il y a des tonnes et des tonnes de bugs, que j'ai abordés un par un (par exemple, Je ne peux enregistrer que des fichiers qui seront vides, mes rectangles ne dessinent pas correctement mais mes cercles le font...).

Cette fois, j'ai essayé d'ajouter une fonctionnalité Annuler/rétablir à mon programme. Cependant, je ne peux pas "annuler" quelque chose que j'ai fait. Par conséquent, j'ai eu l'idée de sauvegarder des copies de mon BufferedImage chaque fois qu'un événement mouseReleased était déclenché. Cependant, avec certaines des images allant à la résolution 1920x1080, j'ai pensé que cela ne serait pas efficace: les stocker prendrait probablement des gigaoctets de mémoire.

La raison pour laquelle je ne peux pas simplement peindre la même chose avec la couleur d'arrière-plan à défaire est parce que j'ai beaucoup de pinceaux différents, qui peignent en fonction de Math.random(), et parce qu'il y a beaucoup de couches différentes (en une seule couche).

Ensuite, j'ai envisagé de cloner les objets Graphics que j'utilise pour peindre sur le BufferedImage. Comme ceci:

ArrayList<Graphics> revisions = new ArrayList<Graphics>();

@Override
public void mouseReleased(MouseEvent event) {
    Graphics g = image.createGraphics();
    revisions.add(g);
}

Je n'ai pas fait cela avant, donc j'ai quelques questions:

  • est-ce que je gaspillerais encore de la mémoire inutile en faisant cela, comme cloner mon BufferedImages?
  • y a-t-il nécessairement une autre façon de faire?
44
demandé sur Zizouz212 2015-07-12 20:12:18

4 réponses

Non, stocker un objet Graphics est généralement une mauvaise idée. :-)

Voici pourquoi: normalement, les instances Graphics sont de courte durée et sont utilisées pour peindre ou dessiner sur une sorte de surface (typiquement un (J)Component ou un BufferedImage). Il détient l'état de ces opérations de dessin, comme les couleurs, course, échelle, rotation etc. Cependant, il ne contient pas le Résultat des opérations de dessin ou des pixels.

Pour cette raison, cela ne vous aidera pas à obtenir une fonctionnalité d'annulation. Les pixels appartiennent au composant ou de l'image. Ainsi, revenir à un objet Graphics" précédent " ne modifiera pas les pixels à l'état précédent.

Voici quelques approches que je connais fonctionne:

  • L'Utilisation d'une "chaîne" de commandes (modèle de commande) pour modifier l'image. Le modèle de commande fonctionne très bien avec undo / redo (et est implémenté dans Swing / AWT dans Action). Rendre toutes les commandes dans l'ordre, à partir de l'original. Pro: l'état dans chaque commande n'est généralement pas si grand, ce qui vous permet d'en avoir beaucoup étapes de défaire-tampon en mémoire. Con: Après beaucoup d'opérations, il devient lente...

  • Pour chaque opération, stockez l'intégralité de BufferedImage (comme vous l'avez fait à l'origine). Pro: Facile à mettre en œuvre. Con: vous allez manquer de mémoire rapidement. Astuce: vous pouvez sérialiser les images, ce qui rend undo / redo prenant moins de mémoire, au prix de plus de temps de traitement.

  • Une combinaison de ce qui précède, en utilisant command pattern / chain idea, mais en optimisant le rendu avec "snapshots" (comme BufferedImages) lorsque raisonnable. Ce qui signifie que vous n'aurez pas besoin de tout rendre depuis le début pour chaque nouvelle opération (plus rapide). Videz / sérialisez également ces instantanés sur le disque, pour éviter de manquer de mémoire (mais gardez-les en mémoire si vous le pouvez, pour la vitesse). Vous pouvez également sérialiser les commandes sur le disque, pour une annulation pratiquement illimitée. Pro: fonctionne très bien lorsqu'il est bien fait. Con: va prendre un certain temps pour obtenir le droit.

PS: pour tout ce qui précède, vous devez utiliser un thread d'arrière-plan (comme SwingWorker ou similaire) pour mettez à jour l'image affichée, stockez les commandes/images sur le disque, etc. en arrière-plan, pour garder une interface utilisateur réactive.

Bonne chance! :-)

46
répondu haraldK 2015-07-14 20:10:23

Idée # 1, stocker les objets Graphics ne fonctionnerait tout simplement pas. Le Graphics ne doit pas être considéré comme "contenant" de la mémoire d'affichage, mais plutôt comme un handle pour accéder à une zone de mémoire d'affichage. Dans le cas de BufferedImage, chaque objet Graphics sera toujours le handle du même tampon de mémoire d'image donné, donc ils représenteront tous la même image. Plus important encore, Vous ne pouvez rien faire avec le stocké Graphics: Comme ils ne stockent rien, il n'y a aucun moyen qu'ils pourrait "re-stocker" n'importe quoi.

Idée # 2, cloner les BufferedImages est une bien meilleure idée, mais vous perdrez en effet de la mémoire, et vous en manquerez rapidement. Cela aide seulement à stocker les parties de l'image affectées par le tirage, par exemple en utilisant des zones rectangulaires, mais cela coûte toujours beaucoup de mémoire. La mise en mémoire tampon de ces images d'annulation sur le disque pourrait aider, mais cela rendra votre interface utilisateur lente et ne répond pas, et c'est mauvais; de plus, cela rend votre application plus complexe et sujet aux erreurs .

Mon alternative serait de stocker stocker les modifications d'image dans une liste, rendue du premier au dernier sur le dessus de l'image. Une opération d'annulation consiste alors simplement à supprimer la modification de la liste.

Cela vous oblige à "réifier" les modifications d'image , c'est-à-dire créer une classe qui implémente une seule modification, en fournissant une méthode void draw(Graphics gfx) qui effectue le dessin réel.

Comme vous l'avez dit, modifications aléatoires posent un problème supplémentaire. Cependant, le problème clé est votre utilisation de Math.random() pour créer des nombres aléatoires. Au lieu de cela, effectuez chaque modification aléatoire avec un Random Créé à partir d'une valeur de départ fixe, de sorte que les séquences de nombres aléatoires (pseudo)soient les mêmes sur chaque invocation de draw(), c'est-à-dire que chaque tirage a exactement les mêmes effets. (C'est pourquoi ils sont appelés "pseudo-aléatoires" -- les nombres générés semblent aléatoires, mais ils sont tout aussi déterministes, comme toute autre fonction.)

, contrairement à la technique de stockage d'images, qui a des problèmes de mémoire, le problème avec cette technique est que de nombreuses modifications peuvent rendre l'interface graphique lente, surtout si les modifications sont intensives en calcul. Pour éviter cela, le moyen le plus simple serait de fixer une taille maximale appropriée de la liste des modifications annulables . Si cette limite est dépassée en ajoutant une nouvelle modification, supprimez la modification la plus ancienne de la liste et appliquez - la au Support BufferedImage lui-même.

Ce qui suit simple application démo montre que (et comment) tout cela fonctionne ensemble. Il comprend également une belle fonctionnalité "refaire" pour refaire les actions annulées.

package stackoverflow;

import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.image.BufferedImage;
import java.util.LinkedList;
import java.util.Random;
import javax.swing.*;

public final class UndoableDrawDemo
        implements Runnable
{
    public static void main(String[] args) {
        EventQueue.invokeLater(new UndoableDrawDemo()); // execute on EDT
    }

    // holds the list of drawn modifications, rendered back to front
    private final LinkedList<ImageModification> undoable = new LinkedList<>();
    // holds the list of undone modifications for redo, last undone at end
    private final LinkedList<ImageModification> undone = new LinkedList<>();

    // maximum # of undoable modifications
    private static final int MAX_UNDO_COUNT = 4;

    private BufferedImage image;

    public UndoableDrawDemo() {
        image = new BufferedImage(600, 600, BufferedImage.TYPE_INT_RGB);
    }

    public void run() {
        // create display area
        final JPanel drawPanel = new JPanel() {
            @Override
            public void paintComponent(Graphics gfx) {
                super.paintComponent(gfx);

                // display backing image
                gfx.drawImage(image, 0, 0, null);

                // and render all undoable modification
                for (ImageModification action: undoable) {
                    action.draw(gfx, image.getWidth(), image.getHeight());
                }
            }

            @Override
            public Dimension getPreferredSize() {
                return new Dimension(image.getWidth(), image.getHeight());
            }
        };

        // create buttons for drawing new stuff, undoing and redoing it
        JButton drawButton = new JButton("Draw");
        JButton undoButton = new JButton("Undo");
        JButton redoButton = new JButton("Redo");

        drawButton.addActionListener(new ActionListener() {
            public void actionPerformed(ActionEvent e) {
                // maximum number of undo's reached?
                if (undoable.size() == MAX_UNDO_COUNT) {
                    // remove oldest undoable action and apply it to backing image
                    ImageModification first = undoable.removeFirst();

                    Graphics imageGfx = image.getGraphics();
                    first.draw(imageGfx, image.getWidth(), image.getHeight());
                    imageGfx.dispose();
                }

                // add new modification
                undoable.addLast(new ExampleRandomModification());

                // we shouldn't "redo" the undone actions
                undone.clear();

                drawPanel.repaint();
            }
        });

        undoButton.addActionListener(new ActionListener() {
            public void actionPerformed(ActionEvent e) {
                if (!undoable.isEmpty()) {
                    // remove last drawn modification, and append it to undone list
                    ImageModification lastDrawn = undoable.removeLast();
                    undone.addLast(lastDrawn);

                    drawPanel.repaint();
                }
            }
        });

        redoButton.addActionListener(new ActionListener() {
            public void actionPerformed(ActionEvent e) {
                if (!undone.isEmpty()) {
                    // remove last undone modification, and append it to drawn list again
                    ImageModification lastUndone = undone.removeLast();
                    undoable.addLast(lastUndone);

                    drawPanel.repaint();
                }
            }
        });

        JPanel buttonPanel = new JPanel(new FlowLayout());
        buttonPanel.add(drawButton);
        buttonPanel.add(undoButton);
        buttonPanel.add(redoButton);

        // create frame, add all content, and open it
        JFrame frame = new JFrame("Undoable Draw Demo");
        frame.getContentPane().add(drawPanel);
        frame.getContentPane().add(buttonPanel, BorderLayout.NORTH);
        frame.pack();
        frame.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
        frame.setLocationRelativeTo(null);
        frame.setVisible(true);
    }

    //--- draw actions ---

    // provides the seeds for the random modifications -- not for drawing itself
    private static final Random SEEDS = new Random();

    // interface for draw modifications
    private interface ImageModification
    {
        void draw(Graphics gfx, int width, int height);
    }

    // example random modification, draws bunch of random lines in random color
    private static class ExampleRandomModification implements ImageModification
    {
        private final long seed;

        public ExampleRandomModification() {
            // create some random seed for this modification
            this.seed = SEEDS.nextLong();
        }

        @Override
        public void draw(Graphics gfx, int width, int height) {
            // create a new pseudo-random number generator with our seed...
            Random random = new Random(seed);

            // so that the random numbers generated are the same each time.
            gfx.setColor(new Color(
                    random.nextInt(256), random.nextInt(256), random.nextInt(256)));

            for (int i = 0; i < 16; i++) {
                gfx.drawLine(
                        random.nextInt(width), random.nextInt(height),
                        random.nextInt(width), random.nextInt(height));
            }
        }
    }
}
9
répondu Franz D. 2015-07-16 23:59:53

La plupart des jeux (ou des programmes) enregistre uniquement les parties nécessaires et c'est ce que vous devez faire.

  • Un rectangle peut être représenté par la largeur, la hauteur, la couleur d'arrière-plan, le contour, etc. Vous pouvez donc simplement enregistrer ces paramètres au lieu du rectangle réel. "rectangle couleur:rouge largeur: 100 hauteur 100"

  • Pour les aspects aléatoires de votre programme (couleur aléatoire sur les pinceaux), vous pouvez enregistrer la graine ou enregistrer le résultat. "graine aléatoire: 1023920"

  • Si le programme permet à l'utilisateur d'importer des images, alors vous devez copier et enregistrer les images.

  • Les remplisseurs et les effets (zoom / transformation / lueur) peuvent tous être représentés par des paramètres tout comme les formes. par exemple. "zoom échelle: 2 ""Rotation angle: 30"

  • Donc, vous Enregistrez tous ces paramètres dans une liste et quand vous avez besoin d'Annuler, vous pouvez marquer les paramètres comme supprimés (mais ne les supprimez pas réellement puisque vous voulez pouvoir refaire aussi). Ensuite, vous pouvez effacer toute la toile et recréer l'image en fonction des paramètres moins ceux qui ont été marqués comme supprimés.

*pour des choses comme les lignes, vous pouvez simplement stocker leurs emplacements dans une liste.

7
répondu Renzhentaxi Baerde 2016-01-22 23:23:40

Vous allez vouloir essayer de compresser vos images (utiliser PNG est un bon début, il a de bons filtres Avec la compression zlib qui aide vraiment). Je pense que la meilleure façon de le faire est de

  • Faites une copie de l'image avant de la modifier
  • le modifier
  • comparer la copie avec la nouvelle image modifiée
  • pour chaque pixel que vous n'avez pas changé, faites de ce pixel un pixel noir et transparent.

Cela devrait vraiment compresser, vraiment bien en PNG. Essayer noir et blanc et voir s'il y a une différence (Je ne pense pas qu'il y en aura, mais assurez-vous de définir les valeurs RVB à la même chose, pas seulement la valeur alpha, donc il va mieux compresser).

Vous pourriez obtenir des performances encore meilleures en recadrant l'image sur la partie qui a été modifiée, mais je ne suis pas sûr de ce que vous gagnez, compte tenu de la compression (et du fait que vous devrez maintenant enregistrer et mémoriser le décalage).

Alors, puisque vous avez un canal alpha, si ils annuler, vous pouvez juste mettre l'image annuler en haut de l'image actuelle et vous êtes fixés.

4
répondu Guy Schalnat 2015-07-16 09:24:41