Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Create an image from a non-visible AWT Component?

I'm trying to create an image (screen-shot) of a non-visible AWT component. I can't use the Robot classes' screen capture functionality because the component is not visible on the screen. Trying to use the following code:

BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
Graphics2D g = image.createGraphics();
component.paintAll(g);

Works sometimes, but does not work if the component contains things such as a text box or button, or some sort of OpenGL / 3D component (these things are left out of the image!). How can I take a proper screenshot of the whole thing?

like image 209
Isaac Waller Avatar asked Oct 26 '10 23:10

Isaac Waller


2 Answers

(disclamer: woops.. this doesn't seem to work for AWT )-:

I can't believe no one has suggested SwingUtilities.paintComponent or CellRendererPane.paintComponent which are made for this very purpose. From the documentation of the former:

Paints a component to the specified Graphics. This method is primarily useful to render Components that don't exist as part of the visible containment hierarchy, but are used for rendering.


Here is an example method that paints a non-visible component onto an image:

import java.awt.*;
import java.awt.image.BufferedImage;

import javax.swing.*;

public class ComponentPainter {

    public static BufferedImage paintComponent(Component c) {

        // Set it to it's preferred size. (optional)
        c.setSize(c.getPreferredSize());
        layoutComponent(c);

        BufferedImage img = new BufferedImage(c.getWidth(), c.getHeight(),
                BufferedImage.TYPE_INT_RGB);

        CellRendererPane crp = new CellRendererPane();
        crp.add(c);
        crp.paintComponent(img.createGraphics(), c, crp, c.getBounds());    
        return img;
    }

    // from the example of user489041
    public static void layoutComponent(Component c) {
        synchronized (c.getTreeLock()) {
            c.doLayout();
            if (c instanceof Container)
                for (Component child : ((Container) c).getComponents())
                    layoutComponent(child);
        }
    }
}

Here is a snippet of code that tests the above class:

JPanel p = new JPanel();
p.add(new JButton("Button 1"));
p.add(new JButton("Button 2"));
p.add(new JCheckBox("A checkbox"));

JPanel inner = new JPanel();
inner.setBorder(BorderFactory.createTitledBorder("A border"));
inner.add(new JLabel("Some label"));
p.add(inner);

BufferedImage img = ComponentPainter.paintComponent(p);

ImageIO.write(img, "png", new File("test.png"));

And here is the resulting image:

                      enter image description here

like image 180
aioobe Avatar answered Oct 09 '22 17:10

aioobe


Component has a method paintAll(Graphics) (as you already have found). That method will paint itself on the passed graphics. But we have to pre-configure the graphics before we call the paint method. That's what I found about the AWT Component rendering at java.sun.com:

When AWT invokes this method, the Graphics object parameter is pre-configured with the appropriate state for drawing on this particular component:

  • The Graphics object's color is set to the component's foreground property.
  • The Graphics object's font is set to the component's font property.
  • The Graphics object's translation is set such that the coordinate (0,0) represents the upper left corner of the component.
  • The Graphics object's clip rectangle is set to the area of the component that is in need of repainting.

So, this is our resulting method:

public static BufferedImage componentToImage(Component component, Rectangle region)
{
    BufferedImage img = new BufferedImage(component.getWidth(), component.getHeight(), BufferedImage.TYPE_INT_ARGB_PRE);
    Graphics g = img.getGraphics();
    g.setColor(component.getForeground());
    g.setFont(component.getFont());
    component.paintAll(g);
    g.dispose();
    if (region == null)
    {
        return img;
    }
    return img.getSubimage(region.x, region.y, region.width, region.height);
}

This is also the better way instead of using Robot for the visible components.


EDIT:

A long time ago I used the code I posted here above, and it worked, but now not. So I searched further. I have a tested, working way. It is dirty, but works. The Idea of it is making a JDialog, put it somewhere out of the Screen bounds, set it visible, and then draw it on the graphics.

Here is the code:

public static BufferedImage componentToImageWithSwing(Component component, Rectangle region) {
    BufferedImage img = new BufferedImage(component.getWidth(), component.getHeight(), BufferedImage.TYPE_INT_RGB);
    Graphics g = img.createGraphics();

    // Real render
    if (component.getPreferredSize().height == 0 && component.getPreferredSize().width == 0)
    {
        component.setPreferredSize(component.getSize());
    }

    JDialog f = new JDialog();
    JPanel p = new JPanel();
    p.add(component);
    f.add(p);
    f.pack();
    f.setLocation(-f.getWidth() - 10, -f.getHeight() -10);
    f.setVisible(true);
    p.paintAll(g);
    f.dispose();
    // ---

    g.dispose();
    if (region == null) {
        return img;
    }
    return img.getSubimage(region.x, region.y, region.width, region.height);
}

So, this will work also on Windows and Mac. The other answer was to draw it on a virtual screen. But this doesn't need it.

like image 25
Martijn Courteaux Avatar answered Oct 09 '22 17:10

Martijn Courteaux