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?
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! :-)
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 BufferedImage
s 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));
}
}
}
}
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.
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.