Le stockage des objects graphiques est-il une bonne idée?

Je suis en train d’écrire un programme de peinture en Java, conçu pour avoir des fonctionnalités flexibles et complètes. Cela découle de mon projet final, que j’ai écrit du jour au lendemain. A cause de cela, il ya des tonnes et des tonnes de bugs, dont je me suis attaqué un par un (par exemple, je ne peux enregistrer que des fichiers vides, mes rectangles ne dessinent pas correctement mais mes cercles le font …).

Cette fois, j’ai essayé d’append des fonctionnalités d’annulation / restauration à mon programme. Cependant, je ne peux pas “défaire” quelque chose que j’ai fait. Par conséquent, j’ai eu une idée pour enregistrer des copies de mon BufferedImage chaque fois qu’un événement mouseReleased était déclenché. Cependant, avec certaines images allant jusqu’à une résolution de 1920×1080, je pensais 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 pour annuler est parce que j’ai beaucoup de pinceaux différents, qui peignent basés sur Math.random() , et parce qu’il y a beaucoup de calques différents (dans un seul calque) .

Ensuite, j’ai envisagé de cloner les objects Graphics que j’utilise pour peindre sur BufferedImage . Comme ça:

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

Je ne l’ai pas fait avant, alors j’ai quelques questions:

  • Est-ce que je perdrais encore de la mémoire inutile en clonant mes BufferedImages ?
  • Y a-t-il nécessairement une autre façon de le faire?

Non, stocker un object Graphics est généralement une mauvaise idée. 🙂

Voici pourquoi: Normalement, les instances Graphics ont une durée de vie limitée et sont utilisées pour peindre ou dessiner sur un type de surface (généralement un (J)Component ou une image BufferedImage ). Il contient l’état de ces opérations de dessin, telles que les couleurs, les traits, l’échelle, la rotation, etc. Cependant, il ne tient pas compte du résultat des opérations de dessin ou des pixels.

De ce fait, cela ne vous aidera pas à obtenir une fonctionnalité non fonctionnelle. Les pixels appartiennent au composant ou à l’image. Donc, revenir à un “précédent” object Graphics ne modifiera pas les pixels à l’état précédent.

Voici quelques approches que je connais:

  • Utilisez 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 en Action ). Rendu toutes les commandes en séquence, en commençant par l’original. Pro: l’état de chaque commande n’est généralement pas très élevé, ce qui vous permet d’avoir plusieurs étapes de défaire la mémoire. Con: Après beaucoup d’opérations, il devient lent …

  • 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 la fonction Annuler / Refaire moins de mémoire, au prix d’un temps de traitement plus long.

  • Une combinaison de ce qui précède, en utilisant un modèle de commande / une idée de chaîne, mais en optimisant le rendu avec “snapshots” (en tant que BufferedImages ) lorsque cela est 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 plus de rapidité). Vous pouvez également sérialiser les commandes sur le disque, pour une annulation pratiquement illimitée. Pro: Fonctionne bien une fois terminé. Con: va prendre un peu de temps pour bien faire.

PS: Pour tout ce qui précède, vous devez utiliser un thread d’arrière-plan (comme SwingWorker ou similaire) pour mettre à jour l’image affichée, stocker des commandes / images sur le disque, etc. en arrière-plan, pour conserver une interface réactive.

Bonne chance! 🙂

Idée n ° 1, stocker les objects 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 object Graphics sera toujours le handle de la même mémoire tampon d’image donnée, ils représenteront donc tous la même image. Plus important encore, vous ne pouvez rien faire avec les Graphics stockés: comme ils ne stockent rien, il n’ya aucun moyen de «re-stocker» quoi que ce soit.

Idée n ° 2, le clonage de BufferedImage est une idée bien meilleure, mais vous gaspillerez en effet votre mémoire, et vous en manquerez rapidement. Cela aide seulement à stocker les parties de l’image affectées par le dessin, par exemple en utilisant des zones rectangulars, 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 rendrait votre interface lente et insensible, et c’est mauvais ; De plus, cela rend votre application plus complexe et sujette à des erreurs .

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

Cela vous oblige à “réifier” les modifications de l’image , c’est-à-dire à créer une classe qui implémente une seule modification, en fournissant une méthode de dessin void draw(Graphics gfx) qui exécute le dessin réel.

Comme vous l’avez dit, les modifications aléatoires posent un problème supplémentaire. Cependant, le problème majeur 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 graine fixe, de sorte que les séquences de nombres (pseudo-) aléatoires soient les mêmes à chaque appel de draw() , c.-à-d. (C’est pourquoi ils sont appelés “pseudo-aléatoires” – les nombres générés semblent aléatoires, mais ils sont aussi déterministes que toute autre fonction.)

Contrairement à la technique de stockage d’images, qui présente des problèmes de mémoire, le problème avec cette technique est que de nombreuses modifications peuvent ralentir l’interface graphique, en particulier si les modifications nécessitent beaucoup de calculs. Pour éviter cela, le plus simple serait de fixer une taille maximale appropriée de la liste des modifications à annuler . Si cette limite est dépassée en ajoutant une nouvelle modification, supprimez la modification la plus ancienne de la liste et appliquez-la à la sauvegarde BufferedImage elle-même.

L’ application de démonstration simple suivante montre que (et comment) tout cela fonctionne ensemble. Il comprend également une fonctionnalité de “refaire” permettant de rétablir 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(Ssortingng[] args) { EventQueue.invokeLater(new UndoableDrawDemo()); // execute on EDT } // holds the list of drawn modifications, rendered back to front private final LinkedList undoable = new LinkedList<>(); // holds the list of undone modifications for redo, last undone at end private final LinkedList 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)); } } } } 

La plupart des jeux (ou programmes) n’enregistrent que 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 parameters 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 soit enregistrer la graine, soit enregistrer le résultat. “graine aléatoire: 1023920”

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

  • les fillters et les effets (zoom / transformation / glow) peuvent tous être représentés par des parameters comme des formes. par exemple. “échelle de zoom: 2” “angle de rotation: 30”

  • Ainsi, vous enregistrez tous ces parameters dans une liste et lorsque vous devez les annuler, vous pouvez les supprimer comme supprimés (mais ne les supprimez pas car vous souhaitez pouvoir les refaire). Vous pouvez ensuite effacer l’intégralité du canevas et recréer l’image en fonction des parameters moins ceux marqués comme supprimés.

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

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

  • faire 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 modifié, faites de ce pixel un pixel noir et transparent.

Cela devrait vraiment compresser très bien en PNG. Essayez le noir et blanc et voyez s’il y a une différence (je ne pense pas qu’il y en aura, mais assurez-vous de définir les valeurs de RVB sur la même chose, pas seulement la valeur alpha, donc cela compressera mieux).

Vous pourriez obtenir des performances encore meilleures en recadrant l’image sur la partie qui a été modifiée, mais je ne suis pas certain de ce que vous en retirez, compte tenu de la compression (et du fait qu’il vous faudra maintenant enregistrer et mémoriser le décalage) .

Puis, comme vous avez un canal alpha, s’ils se défont, vous pouvez simplement remettre l’image d’annulation au-dessus de l’image actuelle et vous êtes défini.