Mostrando entradas con la etiqueta swing. Mostrar todas las entradas
Mostrando entradas con la etiqueta swing. Mostrar todas las entradas

martes, 31 de mayo de 2016

Java Swing: Event Dispatch Thread (EDT) - Parte 3

En este artículo consiste en un pequeño programa que permite comprobar algunas de las cosas que he comentado sobre el Event Dispatch Thread de Java Swing en artículos anteriores.

Haciendo pruebas con el Event Dispatch Thread


El pequeño programa EventDispatchThreadTest (ver código fuente completo aquí) ilustrar fácilmente algunos de los efectos que se producen al bloquear el EDT. Se compone de 2 ventanas (JFrame) llamadas One y Two:
  • En la ventana One hay un botón llamado Long-running task que ejecuta una tarea larga que bloquea el EDT.
  • En la ventana Two se deberían mostrar los efectos de la pulsación de dicho botón.
El botón Long-running task simplemente imprime por consola una línea cada cierto número de iteraciones del bucle. De este modo es una tarea medianamente intensiva en operaciones de entrada/salida y no sólo en consumo de procesador (no conviene hacer pruebas de bloqueo con un simple bucle vacío porque se consumiría el procesador sin facilitar la conmutación entre varios hilos de ejecución).

Al comienzo del oyente del botón se establece el texto del JLabel de la ventana Two a "Left button pressed. Task running...". Sin embargo, jamás se llega a ver ese texto porque hasta que no termina el oyente del botón, no se repinta y al final del evento, se ejecuta el método labelAutoText() para pintar el número de veces que se ha realizado al tarea en ese mismo JLabel.

Además, durante todo ese tiempo, el botón parece pulsado, aunque se haya soltado el botón del ratón y se esté haciendo otras cosas con él.

Otra cosa que se puede comprobar es que el resto de componentes de la GUI no responde mientras la tarea está en ejecución. Por ejemplo, no se puede pulsar el botón Nothing.

Sí se puede mover, maximizar/minimizar y cambiar el tamaño de ambas ventanas. Si se agranda cualquiera de ellas, se verá como el nuevo área queda sin pintar hasta que finaliza la tarea. En este caso, el tamaño, posición y estado (minimizado/maximizado) de las ventanas es controlado por el gestor de ventanas del sistema operativo, pero el repintado no se puede realizar hasta que termine el oyente del botón.

Otra de las cosas que permite comprobar el programa es que mientras la tarea se está ejecutando, no se reciben eventos (y por tanto no se podrán disparar otros oyentes). En especial, si se minimiza y se vuelve a maximizar una de las ventanas mientras se está ejecutando la tarea, los mensajes respectivos se verán todos juntos al final, pero no se recibirán en el momento en el que ocurren.

El programa es muy sencillo y se puede retocar muy fácilmente.

Enlaces


Los enlaces de interés relacionados con esta serie de artículos sobre el EDT están aquí, en el primero de los artículos sobre este tema.


(Actualizado 31/05/2016)

Source code: EventDispatchThreadTest

Código fuente del programa EventDispatchThreadTest

Ver la entrada correspondiente en el siguiente enlace:


Java Swing: Event Dispatch Thread (EDT) - Parte 3

import java.awt.Color;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.MouseMotionListener;
import java.awt.event.WindowEvent;
import java.awt.event.WindowListener;
import java.awt.event.WindowStateListener;
import java.util.Date;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.SwingUtilities;


public class EventDispatchThreadTest {

    private JFrame myOne;
    private JFrame myTwo;
    private JButton myButtonTask;
    private JButton myButtonReset;
    private JButton myButtonNothing;
    private JLabel myLabel;
    private int myClickCount;
    private int myResetCount;

    public EventDispatchThreadTest() {
        super();

        JFrame one= new JFrame("One");
        one.setName( one.getTitle() );
        one.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        one.setSize(300, 400);
        one.setLocation(0, 0);
        JButton buttonTask= new JButton("Long-running task");
        JButton buttonReset= new JButton("Reset click count");
        JPanel onePanel= new JPanel();
        onePanel.setOpaque(true);
        onePanel.setBackground(Color.BLUE);
        onePanel.add(buttonTask);
        onePanel.add(buttonReset);
        one.setContentPane(onePanel);

        WindowListener auxWindowListener= EventDispatchThreadTest.createWindowListener();
        one.addWindowListener(auxWindowListener);

        WindowStateListener auxWindowStateListener= EventDispatchThreadTest.createWindowStateListener();
        one.addWindowStateListener(auxWindowStateListener);

        MouseListener auxMouseListener= EventDispatchThreadTest.createMouseListener();
        onePanel.addMouseListener(auxMouseListener);

        MouseMotionListener auxMouseMotionListener= EventDispatchThreadTest.createMouseMotionListener();
        onePanel.addMouseMotionListener(auxMouseMotionListener);

        JFrame two= new JFrame("Two");
        two.setName( two.getTitle() );
        two.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        two.setSize(300, 400);
        two.setLocation(400, 0);
        JLabel label= new JLabel("Never pressed");
        JButton buttonNothing= new JButton("Nothing");
        JPanel twoPanel= new JPanel();
        twoPanel.setOpaque(true);
        twoPanel.setBackground(Color.BLUE);
        twoPanel.add(label);
        twoPanel.add(buttonNothing);
        two.setContentPane(twoPanel);

        buttonTask.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                //This text is never shown.
                EventDispatchThreadTest.this.myLabel.setText("Left button pressed. Task running...");

                for (int i= 0; i < 99999; i++) {
                    String auxString= String.format("Working... %s %s%n", i, new Date(System.currentTimeMillis()));
                    if ((i % 2) == 0) {
                        System.out.print(auxString);
                    }
                }

                EventDispatchThreadTest.this.myClickCount++;
                EventDispatchThreadTest.this.labelAutoText();
                System.out.format("isEventDispatchThread=%s%n", SwingUtilities.isEventDispatchThread());
            }
        });

        buttonReset.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                EventDispatchThreadTest.this.myClickCount= 0;
                EventDispatchThreadTest.this.myResetCount++;

                EventDispatchThreadTest.this.labelAutoText();
            }
        });

        this.myOne= one;
        this.myTwo= two;
        this.myButtonTask= buttonTask;
        this.myButtonReset= buttonReset;
        this.myButtonNothing= buttonNothing;
        this.myLabel= label;

        this.myClickCount= 0;
        this.myResetCount= 0;
        this.myOne.setVisible(true);
        this.myTwo.setVisible(true);
    }

    private void labelAutoText() {
        this.myLabel.setText(String.format("Click count: %s. Reset count= %s"
            , this.myClickCount
            , this.myResetCount
        ));
    }

    private static WindowListener createWindowListener() {
        WindowListener auxWindowListener= new WindowListener() {
            @Override
            public void windowOpened(WindowEvent e) {
                System.out.format("WindowEvent (EDT? %s): %s%n", SwingUtilities.isEventDispatchThread(), e);
            }

            @Override
            public void windowClosing(WindowEvent e) {
                System.out.format("WindowEvent (EDT? %s): %s%n", SwingUtilities.isEventDispatchThread(), e);
            }

            @Override
            public void windowClosed(WindowEvent e) {
                System.out.format("WindowEvent (EDT? %s): %s%n", SwingUtilities.isEventDispatchThread(), e);
            }

            @Override
            public void windowIconified(WindowEvent e) {
                System.out.format("WindowEvent (EDT? %s): %s%n", SwingUtilities.isEventDispatchThread(), e);
            }

            @Override
            public void windowDeiconified(WindowEvent e) {
                System.out.format("WindowEvent (EDT? %s): %s%n", SwingUtilities.isEventDispatchThread(), e);
            }

            @Override
            public void windowActivated(WindowEvent e) {
                System.out.format("WindowEvent (EDT? %s): %s%n", SwingUtilities.isEventDispatchThread(), e);
            }

            @Override
            public void windowDeactivated(WindowEvent e) {
                System.out.format("WindowEvent (EDT? %s): %s%n", SwingUtilities.isEventDispatchThread(), e);
            }
        };

        return auxWindowListener;
    }

    private static WindowStateListener createWindowStateListener() {
        WindowStateListener auxWindowStateListener= new WindowStateListener() {
            @Override
            public void windowStateChanged(WindowEvent e) {
                System.out.format("Window[State]Event (EDT? %s): %s%n", SwingUtilities.isEventDispatchThread(), e);
            }
        };

        return auxWindowStateListener;
    }

    private static MouseListener createMouseListener() {
        MouseListener auxMouseListener= new MouseListener() {
            @Override
            public void mouseClicked(MouseEvent e) {
                System.out.format("MouseEvent (EDT? %s): %s%n", SwingUtilities.isEventDispatchThread(), e);
            }

            @Override
            public void mousePressed(MouseEvent e) {
                System.out.format("MouseEvent (EDT? %s): %s%n", SwingUtilities.isEventDispatchThread(), e);
            }

            @Override
            public void mouseReleased(MouseEvent e) {
                System.out.format("MouseEvent (EDT? %s): %s%n", SwingUtilities.isEventDispatchThread(), e);
            }

            @Override
            public void mouseEntered(MouseEvent e) {
                System.out.format("MouseEvent (EDT? %s): %s%n", SwingUtilities.isEventDispatchThread(), e);
            }

            @Override
            public void mouseExited(MouseEvent e) {
                System.out.format("MouseEvent (EDT? %s): %s%n", SwingUtilities.isEventDispatchThread(), e);
            }
        };

        return auxMouseListener;
    }

    private static MouseMotionListener createMouseMotionListener() {
        MouseMotionListener auxMouseMotionListener= new MouseMotionListener() {

            @Override
            public void mouseDragged(MouseEvent e) {
                System.out.format("MouseMotionEvent (EDT? %s): %s%n", SwingUtilities.isEventDispatchThread(), e);
            }

            @Override
            public void mouseMoved(MouseEvent e) {
                System.out.format("MouseMotionEvent (EDT? %s): %s%n", SwingUtilities.isEventDispatchThread(), e);
            }
        };

        return auxMouseMotionListener;
    }

    public static void main(String[] args) {
        System.out.format("isEventDispatchThread=%s%n", SwingUtilities.isEventDispatchThread());
        //GUI code should be executed in the EDT.
        SwingUtilities.invokeLater(new Runnable()  {
            @Override
            public void run() {
                System.out.format("isEventDispatchThread=%s%n", SwingUtilities.isEventDispatchThread());
                new EventDispatchThreadTest();
            }
        });
    }

}

sábado, 30 de abril de 2016

Java Swing: Event Dispatch Thread (EDT) - Parte 2

En este artículo continuo hablando sobre el Event Dispatch Thread de Java Swing del que comencé a hablar en el artículo anterior.

Aunque este artículo pueda dar una impresión muy negativa del uso de Swing, no es mi intención criticar negativamente dicho proyecto ni asustar a quienes estén usándolo, sino tan sólo comentar características que, a mi jucio, deben tenerse muy en cuenta al usarlo para no llevarse sorpresas desagradables y que en la mayoría de la documentación a penas se comentan.

Y en cualquier caso, hay que tener en cuenta que muchas de las reflexiones de este artículos son extrapolables a otros entornos, como por ejemplo, el UI Thread de Android.

Restricción impuesta por el Event Dispatch Thread


Una cuestión clave sobre el funcionamiento de Swing es que todo el código que maneje/acceda a componentes debe ejecutarse dentro el EDT (ver Swing's Threading Policy), lo cual no es ninguna tontería. Los componentes no son Thread safe, es decir, no incluyen código de sincronización para que sean accesibles de forma consistente desde varios hilos.

Eso se debe, fundamentalmente, a razones de eficiencia y complejidad de desarrollo. Por una parte, habría muchísimo overhead (coste extra) computacional provocado por el código de sincronización, que influiría negativamente en la fluidez de la GUI. Y por otra parte, conseguir que la GUI fuese accesible desde varios hilos de forma completamente consistente sería una tarea extremadamente compleja (en este artículo uno de sus creadores reflexiona sobre el tema Multithreaded toolkits: A failed dream?).

Complicaciones


A priori la restricción no parece incómoda, de hecho, en muchas ocasiones los programadores ni nos planteamos el uso de varios hilos. Sin embargo, hay que ser conscientes de que en cuanto se utiliza algún componente para la GUI se usan, de forma implícita, al menos 2 hilos: el principal y el EDT.

En el artículo anterior expliqué lo fácil que es bloquear la GUI realizando alguna tarea costosa (acceso a disco, acceso a red, cálculos muy complejos, etc) dentro del EDT. Para evitar que la GUI se bloquee habrá que realizar las tareas pesadas fuera del EDT, pero sin acceder a los componentes de la GUI desde fuera de él. En otras palabras, gestionar asíncronamente los eventos.

Por ejemplo, al pulsar un botón se podría realizar una consulta sobre una base de datos y rellenar un JTable con los datos obtenidos:
  • El evento de pulsación del botón se recibirá dentro del EDT.
  • El JTable ya podría estar creado y mostrándose vacío.
  • La consulta a la base de datos habrá que hacerla fuera del EDT, desde otro hilo.
  • Para poder rellenar el JTable, habrá que esperar a tener los resultados.
  • Habrá que construir un TableModel con los resultados, ¿dentro o fuera del EDT?
  • Habrá que asignar el nuevo TableModel al JTable, dentro del EDT.

O sea, que habrá que andar "saltando" de un hilo a otro dependiendo de la tarea. ¿Y qué pasa con la GUI mientras se obtienen los datos de la consulta?
  • ¿se bloquea la GUI?
  • ¿se ignora la interacción del usuario (descartando clics de ratón y pulsaciones de teclado)?
  • ¿se muestra algún indicador de que la aplicación está ocupada (como el típico cursor de ratón con un reloj de arena)?
  • ¿se muestra un indicador del progreso de la tarea?
  • ¿la GUI sigue siendo operativa? ¿y si se vuelve a pulsar en el botón de cargar mientras hay una o varias cargas en proceso?

Obviamente, el código resultante ya no es tan sencillo como poner dentro del oyente del botón una tras otra las instrucciones:
Además, implementar alguna de las alternativas para la transición es un problema en sí mismo.

La opción de establecer un modelo nuevo completo no siempre es viable, ya que se pueden tener instalados oyentes interesados en saber cuando se añaden o quitan elementos. Estos oyentes deben ser notificados dentro el EDT. ¿Y si añadir o quitar elementos es una tarea pesada que deba realizarse fuera del EDT? ¿Y si hay sucesos que ocurren fuera del EDT que afecten a la GUI?

Por ejemplo, supóngase una aplicación que observa cierta tabla de una base de datos, mostrando su contenido mediante un JTable con su correspondiente TableModel. En el servidor se añade una nueva fila a la tabla y se notifica a la aplicación observadora. Esa notificación debería recibirse desde fuera del EDT. Habrá que actualizar el TableModel del JTable o asignarle uno nuevo dentro el EDT.

Más y más complicaciones


Al gestionar eventos de forma asíncrona se complica la gestión de eventos interrelacionados, como por ejemplo un MOUSE_PRESSED y el siguiente MOUSE_RELEASED. Al recibir la notificación del primer evento dentro del EDT y delegar su gestión a otro hilo, el EDT recibirá el siguiente evento, mientras es muy probable que aún se esté procesando el anterior: ¿y ahora, se delega el segundo evento a un tercer hilo, se descarta, se memoriza en algún sitio hasta que se pueda procesar?

También puede ocurrir que en medio de una tarea pesada sea necesario consultar al usuario, por ejemplo para que confirme si realizar o no una operación peligrosa, o para que introduzca algún dato necesario. Lo normal es que la tarea pesada se esté ejecutando fuera del EDT, por lo que habrá que pausarla para consultar al usuario dentro del EDT y luego continuarla fuera del EDT (leyendo la respuesta que se obtuvo dentro del EDT).

Aparte de la complicación de tener gestionar los eventos de forma asíncrona, al programar en varios hilos pueden aparecer los problemas intrínsecos de la programación concurrente: coordinar (sincronizar) los diversos hilos.

Otro problema añadido que aparece es el trocear el código del algoritmo creando objetos (típicamente anónimos) Runnable con el código a ejecutar dentro del EDT y con objetos SwingWorker (la opción típica) para ejecutar código fuera del EDT. En el caso de ser objetos de clases locales anónimas, además hay que tener en cuenta que no se puede acceder a variables locales del método envolvente a menos que dichas variables hayan sido declaradas como final.

Cuando se acopla fuertemente la vista con el modelo, se tiende a reducir código creándolos de manera conjunta. Por ejemplo, Swing lo hace con el constructor JTable(Object[][] rowData, Object[] columnNames), en el que se construye internamente un TableModel a partir de esos arrays. Inspirados en él, se podría tener la idea de crear una subclase con el constructor MyTable(Connection, Query) que realice una consulta sobre una base de datos y construya el JTable con las filas resultantes (o una función factoría JTable showQuery(Connection, Query) equivalente). El new MyTable se haría:
  • ¿dentro del EDT ejecutando una consulta a base de datos desde él, y por tanto, bloqueándolo?
  • ¿fuera del EDT violando la norma de manipular los objetos de GUI siempre dentro del EDT?
Ninguna de las 2 opciones es completamente buena.

Más vale prevenir


Resumiendo todo lo anterior, la gestión de los eventos se puede complicar mucho.

La cuestión es tener todo eso en cuenta, saber que todo eso está detrás para tener en cuenta que cuando algún evento pueda derivar en una tarea pesada (como entrada/salida) más vale entretenerse en orientarlo a un diseño asíncrono desde el principio, a tener que cambiarlo a posteriori.

Por experiencia sé que convertir código síncrono monohilo en código asíncrono multihilo con las restricciones comentadas en el artículo no es una tarea sencilla, y suele requerir modificar código en muchos más sitios de los que sería deseable.

Enlaces


Los enlaces de interés relacionados con esta serie de artículos sobre el EDT están aquí, en el primero de los artículos.


(Actualizado 30/04/2016)

jueves, 31 de marzo de 2016

Java Swing: Event Dispatch Thread (EDT) - Parte 1

Introducción


Si has programado Interfaces Gráficas de Usuario (GUI: Graphical User Interface) en Java Swing (JFC/Swing), deberías saber qué es el Event Dispatch Thread (de aquí en adelante EDT), pero tampoco me extrañaría que no hayas oído hablar de él.

Desarrollando una GUI en Java es típico preguntarse por qué durante el tratamiento de un evento (por ejemplo, la pulsación de un botón) pasan cosas como:
  1. que no aparece un cambio visual (texto, color...) que se trata de mostrar transitoriamente.
  2. que la interfaz se queda bloqueda/congelada/colgada (blocks/hungs/freezes).
La respuesta a ese tipo de preguntas se vuelve obvia cuando se conoce el modelo de hilos (hebras o threads) de ejecución que utiliza Swing.

El problema es que la mayoría de introducciones, tutoriales y guías rápidas de Swing no suelen hablar del EDT o no lo hacen entre los primeros puntos (en los tutoriales oficiales de JFC/Swing es el 4º y con el nombre que tiene, al menos a mí, no me dan ganas de leerlo para aprender a poner botones en pantalla), lo que suele provocar confusión en los programadores cuando empiezan a hacer cosas relativamente complejas.

Muchas de las cosas que comentaré sobre este tema se deben a mi experiencia usando Swing, y no a que yo tenga conocimientos de cómo funciona internamente.

¿Qué es y cómo funciona el Event Dispatch Thread?


El EDT es el hilo en el que la máquina virtual Java lleva a cabo todos las operaciones relacionadas con la GUI, como pintar componentes (objetos de la interfaz como botones, etiquetas, etc.) y atender los eventos de ratón y teclado (la exigua explicación del tutorial oficial aquí).

He subrayado pintar porque el proceso de pintar en pantalla también lo lleva a acabo el EDT, es decir, que en un instante determinado, o bien se está pintando en pantalla o bien se están atendiendo eventos, pero no ambas cosas a la vez.

Sabiendo eso, es muy fácil responder las preguntas anteriores:
  1. Al pulsar un botón no se puede mostrar el texto Cargando, realizar una carga lenta de datos y finalmente mostrar Cargado pensando que se llegará a ver el texto Cargando, porque el EDT no podrá repintar la pantalla hasta que finalice el evento completo, mostrando únicamente el resultado final Cargado pero no los posibles estados intermedios.
  2. Mientras el EDT esté atendiendo un evento, no podrá atender ningún otro ni refrescar la pantalla, por lo que si el evento tarda mucho tiempo el programa dará la sensación de estar colgado.
En esencia, el funcionamiento del EDT consiste en procesar, uno a uno y en orden de llegada, los eventos que han sido encolados en una cola (First In First Out) de eventos, considerando (re)pintar la pantalla también como un tipo especial de evento que se dispara cuando se alteran las propiedades gráficas (texto, colores, tamaño, etc.) de algún componente. Dicha cola especializada para gestionar los eventos de la GUI es la clase java.awt.EventQueue, que incluye el método isDispatchThread(), aunque es más común usar los métodos wrapper de la clase javax.swing.SwingUtilities, como isEventDispatchThread().

Pero es muy importante tener en cuenta que no es sólo el EDT quien puede encolar eventos, por lo que aunque el EDT esté bloqueado, pueden seguir llegando eventos que se procesaran posteriormente.

El procesamiento de cada evento incluye notificar a los oyentes correspondientes. Por tanto, cuando el programador registra un oyente de cierto tipo de evento, debe tener en cuenta que ese código se ejecutará en el EDT, y que si el tratamiento del evento es muy largo (por ejemplo, realiza alguna operación de entrada/salida), tendrá la GUI bloqueada hasta que el código del oyente termine, no pudiendo procesar nuevos eventos ni notificar a otros oyentes (lo que no impide que se sigan encolando eventos).

¿Por qué no se bloquea todo mientras el EDT está ocupado?


Aunque el EDT esté bloqueado, se pueden seguir haciendo algunas cosas con la GUI como cambiar el tamaño de las ventanas, minimizarlas y maximizarlas, mover el ratón y pulsar el teclado.

No hay que olvidar que la máquina virtual Java es una aplicación que se ejecuta sobre un sistema operativo real, y en el caso de necesitar interfaz gráfica, también sobre un gestor de ventanas (Windows, Gnome, KDE, etc).

Hay que tener en cuenta que hay eventos de bajo nivel, como el movimiento del ratón, las pulsaciones en el teclado y el manejo de las ventanas que son controlados por el gestor de ventanas y notificados a la máquina virtual Java como si ésta fuese un oyente. Luego Swing transformará algunos de esos eventos de bajo nivel en eventos de más alto nivel como que se ha hecho clic en un botón y los notificará a la aplicación.

Los eventos de bajo nivel se seguirán produciendo y aquello que sea controlado por el gestor de ventanas seguirá siendo posible, aunque la aplicación no recibirá todos esos eventos, pero en cualquier caso, los recibidos sólo se podrán procesar cuando el EDT se desbloquee, con lo que parecerá que se recibieron inmediatamente todos seguidos. Por ejemplo, no se recibirán todos los eventos de tipo MouseEvent que debería recibir un MouseMotionListener.

Aún así, probablemente pasarán efectos gráficos extraños con las ventanas, como que queden áreas sin pintar si se agranda.

Continuará


En futuros artículos continuaré hablando sobre el EDT y pondré un pequeño programa con el que ilustrar algunas de las cuestiones relacionadas con él que explique en los artículos.

Enlaces


Enlaces de interés relacionados con este artículo:

(Actualizado 06/04/2016)

domingo, 31 de agosto de 2014

Capturar un Component de Swing/AWT (renderizar a una imagen)

Introducción


A veces, programáticamente interesa guardar una imagen de la apariencia visual (captura) de un progrma Java. Una posibilidad muy sencilla es utilizar el método:

Pero... ¿y si lo que se desea capturar es solamente un componente?

¿Y si se desea capturar un componente que no cabe en pantalla? Por ejemplo, un componente que esté envuelto en un JScrollPane, todas las filas de una tabla o una ventana sobredimensionada.

¿Y si se desea capturar una componente que está parcialmente o totalmente fuera de la pantalla? Por ejemplo, una ventana que se ha desplazado parcialmente fuera de la pantalla.

Solución


La clase Component de Java tiene el método:

La clase BufferedImage tiene el método:

La clave está en combinar ambos métodos:
  1. Construir una imagen del tamaño del componente.
  2. Pintar el componente sobre el contexto gráfico de la imagen.
Con la imagen luego se pueden hacer procesos posteriores como guardarla en un fichero.

Este mecanismo se puede utilizar con cualquier Component, incluidas las ventanas y los diálogos, incluso aunque no estén total o parcialmente en la pantalla.

Ejemplo


En el siguiente enlace está la pequeña clase CaptureTest que permite guardar cualquier componente en un fichero PNG (Portable Network Graphics):

El método CaptureTest#capture(Component, Color) realiza la tarea descrita en este artículo, incluyendo algunos detalles técnicos como el color de fondo para componentes con partes transparentes o el ajuste del tamaño de ventanas descontando las decoraciones (la barrita con los botones de maximizar, minimizar, etc. que incluye el manejador de ventanas).

Al probarla, hay que tener en cuenta que los ficheros con las capturas se intentan crear en el directorio desde donde se invoca la máquina virtual Java.

Conclusión


Este artículo demuestra un uso muy sencillo y práctico de algunas funcionalidades disponibles en la la API de Swing/AWT y referencia la clase CaptureTest, cuyo código se puede utilizar para realizar la tarea descrita en él.

Enlaces


Enlaces de interés relacionados con este artículo:

(Actualizado 31/08/2014)

Source code: CaptureTest

Código fuente del programa CaptureTest

Ver la entrada correspondiente en el siguiente enlace:


Capturar un Component de Swing (renderizar a una imagen)



package pruebas;

import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Component;
import java.awt.Graphics;
import java.awt.Insets;
import java.awt.Window;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.image.BufferedImage;
import java.awt.image.RenderedImage;
import java.io.File;
import java.io.IOException;
import javax.imageio.ImageIO;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTextArea;
import javax.swing.SwingUtilities;

public class CaptureTest {

    public static RenderedImage capture(Component component, Color background) {
        //Se crea una imagen de las dimensiones del componente.
        int width= component.getWidth();
        int height= component.getHeight();

        //El tamaño de las ventanas incluye las decoraciones si las hay, que no se pintan en el método paint(Graphics).
        if (component instanceof Window) {
            Window window= (Window) component;
            Insets insets= window.getInsets();

            if (insets != null) {
                //Hay que restar el ancho y el alto del Insets (Border y decoración).
                width= width - (insets.left + insets.right);
                height= height - (insets.top + insets.bottom);
            }
        }

        int imageType= BufferedImage.TYPE_INT_ARGB;
        BufferedImage bufImage= new BufferedImage(width, height, imageType);

        //Se pinta el componente sobre la imagen creada.
        Graphics graphics= bufImage.getGraphics();

        //El tamaño de las ventanas incluye las decoraciones si las hay, que no se pintan en el método paint(Graphics).
        if (component instanceof Window) {
            Window window= (Window) component;
            Insets insets= window.getInsets();

            if (insets != null) {
                //Hay que mover el origen.
                graphics.translate(
                    0 - insets.left,
                    0 - insets.top
                );
            }
        }

        //Si el componente no es opaco y se ha indicado un color de fondo.
        if (component.isOpaque() == false) {
            if (background != null) {
                graphics.setColor(background);
                graphics.fillRect(0, 0, width, height);
            }
        }

        component.paint(graphics);

        graphics.dispose();

        return bufImage;
    }


    private static File doCapture(Component component, String fileName) {
        try {
            RenderedImage image= CaptureTest.capture(component, null);
            File file= new File(fileName);
            boolean wasOK= ImageIO.write(image, "PNG", file);

            return file;
        }
        catch (IOException ex) {
            ex.printStackTrace(System.err);
            System.exit(1);
            return null;//Necesario para que compile.
        }
    }


    public static void test() {
        final JTextArea ta= new JTextArea();
        ta.setText("This is a very looooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooog line.");
        ta.setCaretPosition(0);

        JScrollPane scroll= new JScrollPane(ta);

        JButton bCaptureTA= new JButton("Capture Text Area");

        JButton bCaptureWin= new JButton("Capture Window");

        JPanel panel= new JPanel( new BorderLayout() );
        panel.add(scroll, BorderLayout.CENTER);
        panel.add(bCaptureTA, BorderLayout.PAGE_START);
        panel.add(bCaptureWin, BorderLayout.PAGE_END);

        final JFrame frame= new JFrame();
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.setContentPane(panel);
        frame.setSize(300, 200);

        bCaptureTA.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                File file= CaptureTest.doCapture(ta, "ta.png");
                String msg= String.format("Saved to file <%s>.", file.getAbsolutePath());
                JOptionPane.showMessageDialog(frame, msg);
            }
        });

        bCaptureWin.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                //Se usa invokeLater para que el botón no se capture pulsado.
                SwingUtilities.invokeLater(new Runnable() { public void run() {
                    File file= CaptureTest.doCapture(frame, "win.png");
                    String msg= String.format("Saved to file <%s>.", file.getAbsolutePath());
                    JOptionPane.showMessageDialog(frame, msg);
                }});
            }
        });

        frame.setVisible(true);
    }


    public static void main(String[] args) {
        SwingUtilities.invokeLater(new Runnable() { public void run() {
            CaptureTest.test();
        }});
    }

}