studio programacion móviles interfaz grafica desarrollo curso aplicaciones java memory-management graphics awt bufferedimage

java - programacion - ¿Es una buena idea almacenar objetos gráficos?



jlist java netbeans (4)

Deberá intentar comprimir sus imágenes (usar PNG es un buen comienzo, tiene algunos buenos filtros junto con la compresión zlib que realmente ayudan). Creo que la mejor manera de hacerlo es

  • haga una copia de la imagen antes de modificarla
  • modificarlo
  • compare la copia con la nueva imagen modificada
  • por cada píxel que no haya cambiado, haga que ese píxel sea un píxel negro y transparente.

Eso debería comprimir muy, muy bien en PNG. Pruebe con blanco y negro y vea si hay alguna diferencia (no creo que lo haya, pero asegúrese de establecer los valores de rgb en la misma cosa, no solo en el valor alfa, para que se comprima mejor).

Es posible que obtengas un mejor rendimiento al recortar la imagen a la parte que se modificó, pero no estoy seguro de cuánto ganarás, teniendo en cuenta la compresión (y el hecho de que ahora tendrás que guardar y recordar el desplazamiento) .

Entonces, dado que tienes un canal alfa, si lo hacen, puedes volver a colocar la imagen de deshacer en la parte superior de la imagen actual y estás listo.

Actualmente estoy en el proceso de escribir un programa de pintura en Java, diseñado para tener funcionalidades flexibles y completas. Surgió de mi proyecto final, que escribí la noche anterior el día anterior. Debido a eso, tiene toneladas y toneladas de errores, que he estado abordando uno por uno (por ejemplo, solo puedo guardar archivos que estarán vacíos, mis rectángulos no dibujan correctamente pero mis círculos sí ...).

Esta vez, he intentado agregar funcionalidad deshacer / rehacer a mi programa. Sin embargo, no puedo "deshacer" algo que he hecho. Por lo tanto, tuve la idea de guardar copias de mi imagen BufferedImage cada vez que se disparaba un evento mouseReleased . Sin embargo, con algunas de las imágenes que van a una resolución de 1920x1080, pensé que esto no sería eficiente: almacenarlas probablemente tomaría gigabytes de memoria.

La razón por la que no puedo simplemente pintar lo mismo con el color de fondo para deshacer es porque tengo muchos pinceles diferentes, que pintan según Math.random() y porque hay muchas capas diferentes (en una sola capa) .

Luego, consideré clonar los objetos de Graphics que uso para pintar en BufferedImage . Me gusta esto:

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

No he hecho esto antes, así que tengo un par de preguntas:

  • ¿Todavía estaría desperdiciando memoria inútil haciendo esto, como clonar mis BufferedImages ?
  • ¿Existe necesariamente una forma diferente de hacer esto?

Idea # 1, almacenar los objetos Graphics simplemente no funcionaría. Los Graphics no deben considerarse como "retener" algo de la memoria de la pantalla, sino como un asa para acceder a un área de la memoria de la pantalla. En el caso de BufferedImage , cada objeto Graphics será siempre el identificador del mismo buffer de memoria de imagen dado, por lo que todos representarán la misma imagen. Aún más importante, no se puede hacer nada con los Graphics almacenados: como no almacenan nada, no hay manera de que puedan "almacenar" nada.

Idea # 2, clonar BufferedImage es una idea mucho mejor, pero de hecho perderás memoria y rápidamente te quedarás sin ella. Solo ayuda a almacenar aquellas partes de la imagen afectadas por el dibujo, por ejemplo usando áreas rectangulares, pero aún cuesta mucha memoria. Almacenar en búfer esas imágenes de deshacer en el disco podría ayudar, pero hará que su UI sea lenta y no responda, y eso es malo ; además, hace que su aplicación sea más compleja y propensa a errores .

Mi alternativa sería almacenar almacenar las modificaciones de imagen en una lista, representada de principio a fin en la parte superior de la imagen. Una operación de deshacer simplemente consiste en eliminar la modificación de la lista.

Esto requiere que "reificar" las modificaciones de la imagen , es decir, crear una clase que implemente una única modificación, proporcionando un método de void draw(Graphics gfx) que realice el dibujo real.

Como dijiste, las modificaciones aleatorias plantean un problema adicional. Sin embargo, el problema clave es su uso de Math.random() para crear números aleatorios. En su lugar, realice cada modificación aleatoria con un Random creado a partir de un valor de inicialización fijo, de modo que las secuencias de números (pseudo) al azar sean las mismas en cada invocación de draw() , es decir, cada draw tenga exactamente los mismos efectos. (Es por eso que se les llama "pseudoaleatorios"; los números generados parecen aleatorios, pero son tan deterministas como cualquier otra función).

En contraste con la técnica de almacenamiento de imágenes, que tiene problemas de memoria, el problema con esta técnica es que muchas modificaciones pueden hacer que la GUI sea lenta, especialmente si las modificaciones son computacionalmente intensas. Para evitar esto, la forma más simple sería fijar un tamaño máximo apropiado de la lista de modificaciones imposibles de hacer . Si este límite se excede agregando una nueva modificación, elimine la modificación más antigua de la lista y aplíquela al respaldo BufferedImage .

La siguiente aplicación de demostración simple muestra que (y cómo) todo esto funciona en conjunto. También incluye una buena función de "rehacer" para rehacer acciones desaprovechadas.

package ; 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 mayoría de los juegos (o programas) solo guardan las partes necesarias y eso es lo que debes hacer.

  • un rectángulo se puede representar por ancho, alto, color de fondo, trazo, contorno, etc. Así que puede guardar estos parámetros en lugar del rectángulo real. "color del rectángulo: ancho rojo: 100 alto 100"

  • para los aspectos aleatorios de su programa (color al azar en los pinceles) puede guardar la semilla o guardar el resultado. "semilla al azar: 1023920"

  • si el programa permite al usuario importar imágenes, entonces debe copiar y guardar las imágenes.

  • los rellenos y los efectos (zoom / transformación / brillo) pueden representarse mediante parámetros como las formas. p.ej. "escala de zoom: 2" "ángulo de rotación: 30"

  • por lo que guarda todos estos parámetros en una lista y, cuando necesita deshacer, puede marcar los parámetros como eliminados (pero no los elimine ya que también puede rehacerlos). Luego puede borrar todo el lienzo y volver a crear la imagen en función de los parámetros menos los que se marcaron como eliminados.

* para cosas como líneas, puede almacenar sus ubicaciones en una lista.


No, almacenar un objeto Graphics generalmente es una mala idea. :-)

Estos son los motivos: normalmente, las instancias de Graphics duran poco tiempo y se utilizan para pintar o dibujar sobre algún tipo de superficie (normalmente un (J)Component o una BufferedImage ). Mantiene el estado de estas operaciones de dibujo, como colores, trazo, escala, rotación, etc. Sin embargo, no retiene el resultado de las operaciones de dibujo o los píxeles.

Debido a esto, no lo ayudará a lograr la funcionalidad deshacer. Los píxeles pertenecen al componente o a la imagen. Por lo tanto, retroceder a un objeto Graphics "anterior" no modificará los píxeles al estado anterior.

Aquí hay algunos enfoques que sé que funcionan:

  • Use una "cadena" de comandos (patrón de comando) para modificar la imagen. El patrón de comando funciona muy bien con deshacer / rehacer (y se implementa en Swing / AWT en Action ). Renderice todos los comandos en secuencia, empezando por el original. Pro: el estado de cada comando generalmente no es tan grande, lo que le permite tener muchos pasos de deshacer en la memoria. Con: después de muchas operaciones, se vuelve lento ...

  • Para cada operación, almacene toda la BufferedImage (como originalmente lo hizo). Pro: fácil de implementar. Con: se quedará sin memoria rápidamente. Consejo: Puede serializar las imágenes, haciendo que deshacer / rehacer tome menos memoria, a costa de más tiempo de procesamiento.

  • Una combinación de lo anterior, que usa un patrón de comando / idea de cadena, pero que optimiza el renderizado con "instantáneas" (como imágenes BufferedImages ) cuando es razonable. Lo que significa que no necesitará renderizar todo desde el principio para cada nueva operación (más rápido). También enjuague / serialice estas instantáneas en el disco, para evitar que se quede sin memoria (pero guárdelas en la memoria si puede para obtener más velocidad). También puede serializar los comandos en el disco, para deshacer virtualmente ilimitado. Pro: Funciona muy bien cuando se hace bien. Con: tomará un tiempo para hacerlo bien.

PD: Para todo lo anterior, debe usar un hilo de fondo (como SwingWorker o similar) para actualizar la imagen mostrada, almacenar comandos / imágenes en el disco, etc. en el fondo, para mantener una IU receptiva.

¡Buena suerte! :-)