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();
        }});
    }

}