Why does the JTable header not appear in the image?
Asked Answered
A

6

27

I was offering advice on capturing an image of tabular data on Java API or Tool to convert tabular data into PNG image file - when the OP requested a code sample. Turns out to be harder than I thought! The JTable header vanishes from the PNG that the code writes.

PNG

PNG

Screen shot

enter image description here

import javax.swing.*;
import java.awt.Graphics;
import java.awt.BorderLayout;
import java.awt.image.BufferedImage;

import javax.imageio.ImageIO;
import java.io.File;

class TableImage {

    public static void main(String[] args) throws Exception {
        Object[][] data = {
            {"Hari", new Integer(23), new Double(78.23), new Boolean(true)},
            {"James", new Integer(23), new Double(47.64), new Boolean(false)},
            {"Sally", new Integer(22), new Double(84.81), new Boolean(true)}
        };

        String[] columns = {"Name", "Age", "GPA", "Pass"};

        JTable table = new JTable(data, columns);
        JScrollPane scroll = new JScrollPane(table);
        JPanel p = new JPanel(new BorderLayout());
        p.add(scroll,BorderLayout.CENTER);

        JOptionPane.showMessageDialog(null, p);

        BufferedImage bi = new BufferedImage(
            (int)p.getSize().getWidth(),
            (int)p.getSize().getHeight(),
            BufferedImage.TYPE_INT_RGB
            );

        Graphics g = bi.createGraphics();
        p.paint(g);

        JOptionPane.showMessageDialog(null, new JLabel(new ImageIcon(bi)));
        ImageIO.write(bi,"png",new File("table.png"));
    }
}

Note: I checked over camickr's Screen Image class and included a call to the doLayout(Component) method. The method is useful for if a Component has never been realized on screen, but has no effect on this code (which pops the panel containing the table in an option pane before trying to render it).

What is needed in order to get the table header to render?

Update 1

Changing the line..

        p.paint(g);

..to (with an appropriate import)..

        p.paint(g);
        JTableHeader h = table.getTableHeader();
        h.paint(g);

..produces..

2nd try

I'll keep tweaking it.

Update 2

kleopatra (strategy 1) & camickr (strategy 2) have provided an answer each, both of which work, & neither of which requires adding the JTable to a dummy component (which is an huge hack IMO).

While strategy 2 will crop (or expand) to 'just the table', the 1st strategy will capture the panel containing the table. This becomes problematic if the table contains many entries, showing an image of a truncated table with a scroll bar.

While strategy 1 might be further tweaked to get around that, I really like the neat simplicity of strategy 2, so it gets the tick.

As pointed out by kleopatra, there was no 'tweak' needed. So I'll try again..

Update 3

This is the image produced by the methods put forward by both camickr and kleopatra. I'd have put it twice, but to my eye, they are identical (though I have not done a pixel by pixel comparison).

JTable in Nimbus PLAF

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

import javax.imageio.ImageIO;
import java.io.File;

class TableImage {

    String[] columns = {"Name", "Age", "GPA", "Pass"};
    /** Any resemblance to persons living or dead is purely incidental. */
    Object[][] data = {
        {"André", new Integer(23), new Double(47.64), new Boolean(false)},
        {"Jeanie", new Integer(23), new Double(84.81), new Boolean(true)},
        {"Roberto", new Integer(22), new Double(78.23), new Boolean(true)}
    };

    TableImage() {
    }

    public JTable getTable() {
        JTable table = new JTable(data, columns);
        table.setGridColor(new Color(115,52,158));
        table.setRowMargin(5);
        table.setShowGrid(true);

        return table;
    }

    /** Method courtesy of camickr.
    https://mcmap.net/q/508609/-why-does-the-jtable-header-not-appear-in-the-image/7375655#7375655
    Requires ScreenImage class available from..
    http://tips4java.wordpress.com/2008/10/13/screen-image/ */
    public BufferedImage getImage1(JTable table) {
        JScrollPane scroll = new JScrollPane(table);

        scroll.setColumnHeaderView(table.getTableHeader());
        table.setPreferredScrollableViewportSize(table.getPreferredSize());

        JPanel p = new JPanel(new BorderLayout());
        p.add(scroll, BorderLayout.CENTER);

        BufferedImage bi = ScreenImage.createImage(p);
        return bi;
    }

    /** Method courtesy of kleopatra.
    https://mcmap.net/q/508609/-why-does-the-jtable-header-not-appear-in-the-image/7372045#7372045 */
    public BufferedImage getImage2(JTable table) {
        JScrollPane scroll = new JScrollPane(table);

        table.setPreferredScrollableViewportSize(table.getPreferredSize());

        JPanel p = new JPanel(new BorderLayout());
        p.add(scroll, BorderLayout.CENTER);

        // without having been shown, fake a all-ready
        p.addNotify();

        // manually size to pref
        p.setSize(p.getPreferredSize());

        // validate to force recursive doLayout of children
        p.validate();

        BufferedImage bi = new BufferedImage(p.getWidth(), p.getHeight(), BufferedImage.TYPE_INT_RGB);

        Graphics g = bi.createGraphics();
        p.paint(g);
        g.dispose();

        return bi;
    }

    public void writeImage(BufferedImage image, String name) throws Exception {
        ImageIO.write(image,"png",new File(name + ".png"));
    }

    public static void main(String[] args) throws Exception {
        UIManager.setLookAndFeel("com.sun.java.swing.plaf.nimbus.NimbusLookAndFeel");
        TableImage ti = new TableImage();
        JTable table;
        BufferedImage bi;

        table = ti.getTable();
        bi = ti.getImage1(table);
        ti.writeImage(bi, "1");
        JOptionPane.showMessageDialog(null, new JLabel(new ImageIcon(bi)));

        table = ti.getTable();
        bi = ti.getImage2(table);
        ti.writeImage(bi, "2");
        JOptionPane.showMessageDialog(null, new JLabel(new ImageIcon(bi)));
    }
}

Both achieve the goal. Using camickr's method you leverage the further power of the ScreenImage API. Using kleopatra's method - about a dozen lines (less the comments and white space) of pure J2SE.

While ScreenImage is a class I will use and recommend in future, the other approach using core J2SE is what I'd probably use for this exact circumstance.

So while the 'tick' will stay with camickr, the bounty is going to kleopatra.

Amygdala answered 10/9, 2011 at 5:17 Comment(13)
Questions like this make me afraid to ever use Swing.Marijane
@Kublai This is a weird corner case, and you're a scaredy cat. ;) BTW - try doing it in AWT.Amygdala
"Dude, where's my header?" - Dude, I don't know! Where did you last leave it? :-)Claudio
@Stephen its a pun from the movie "Dude! where's my car": imdb.com/title/tt0242423Aircool
@Suraj Maybe that was a dig from Stephen to the effect that I should not have been making puns. Glad I did not use the variant that was considered "Dude, where's my head at?". ;)Amygdala
such a nice title (self?) censored, what a pity ;-) This printing to image task indeed is more tricky than it should be. Nothing to add in terms of a solution/understanding (actually I dont understand at all why the header doesnt show up at all), just a couple of links to related threads: #7027322 and #6593999Culottes
OK - I had my fun with the title and it is now something that is way less funny, but somewhat more descriptive.Amygdala
@Andrew Thompson one step forward :-)Minivet
@Culottes because TableHeader && JTable returns only JTable#HEIGHT, that's your area, in link that I sticked there is something more about that, please see my undeleted post (this my post will be deleted again)Minivet
@Minivet - thanks for the idea, but that's not the problem, see my answer :-)Culottes
@Andrew Thompson I miss there question(s) for bounty, can you please update your ... for reason(s)Minivet
@Minivet Will explain in update (later).Amygdala
cool, 150 more for voting down nonsense :-) Thanks for the clarification.Culottes
S
11

It was not a requirement of this thread to render the table without first displaying it, but that was the ultimate goal

ScreenImage handles this.

You must manually add the header to the scrollpane.

import javax.swing.*;
import java.awt.Graphics;
import java.awt.BorderLayout;
import java.awt.image.BufferedImage;

import javax.imageio.ImageIO;
import java.io.File;

class TableImage {

    public static void main(String[] args) throws Exception {
        Object[][] data = {
            {"Hari", new Integer(23), new Double(78.23), new Boolean(true)},
            {"James", new Integer(23), new Double(47.64), new Boolean(false)},
            {"Sally", new Integer(22), new Double(84.81), new Boolean(true)}
        };

        String[] columns = {"Name", "Age", "GPA", "Pass"};

        JTable table = new JTable(data, columns);
        JScrollPane scroll = new JScrollPane(table);

        scroll.setColumnHeaderView(table.getTableHeader());
        table.setPreferredScrollableViewportSize(table.getPreferredSize());

        JPanel p = new JPanel(new BorderLayout());
        p.add(scroll, BorderLayout.CENTER);

        BufferedImage bi = ScreenImage.createImage(p);

        JOptionPane.showMessageDialog(null, new JLabel(new ImageIcon(bi)));
        ImageIO.write(bi,"png",new File("table.png"));
    }
}

Note: Kleopatra's suggeston to use the addNotify() on the panel will not work with ScreenImage. The addNotify() method makes the component displayable and the ScreenImage code will only lay out the components for non-displayable components. I might look into making this more general.

Smokestack answered 11/9, 2011 at 0:58 Comment(2)
@MarkJeronimus, Still works for me using JDK7_60 on Windows 7. Maybe this is an OS issue? If so, try wrapping all the code in a SwingUtilties.invokeLater(...) to make sure the code executes on the EDT, which is the proper way to create a GUI.Smokestack
Inside ScreenImage.java at line component.paint( g2d );: NullPointerException in getVolatileOffscreenBuffer() because internally it gets the top-level ancestor window/dialog which doesn't exist. (I looked into the RepaintManager.java source for that)Phail
C
15

that was a tough one

Was puzzled that that header didn't show up on the image at all: it wasn't clipped or something, it wasn't painted at all. The reason for that is .. that at the time of painting the panel to the image, the header is no longer part of the hierarchy. When closing the optionPane, table.removeNotify removes the header. For adding it again, call addNotify, as in this snippet:

    JScrollPane scroll = new JScrollPane(table);
    JPanel p = new JPanel(new BorderLayout());
    p.add(scroll, BorderLayout.CENTER);
    JOptionPane.showMessageDialog(null, p);
    table.addNotify();
    p.doLayout();
    BufferedImage bi = new BufferedImage(p.getWidth() + 100,
            p.getHeight() + 100, BufferedImage.TYPE_INT_RGB);

    Graphics g = bi.createGraphics();
    p.paint(g);
    g.dispose();

[solved in edit] What I still don't understand why the panel shows up empty without first having been shown in the optionPane - typically some combination of ..

   p.doLayout();
   p.setSize(p.getPreferredSize()) 

... will do

Edit

last confusion solved: to force a recursive re-layout of the pane, all container along the line must be driven into believing they have a peer - validate is optimized to do nothing if not. Doing the addNotify on the panel (instead of on the table) plus validate does the trick

    //        JOptionPane.showMessageDialog(null, p);
    // without having been shown, fake a all-ready
    p.addNotify();
    // manually size to pref
    p.setSize(p.getPreferredSize());
    // validate to force recursive doLayout of children
    p.validate();
    BufferedImage bi = new BufferedImage(p.getWidth() + 100,
            p.getHeight() + 100, BufferedImage.TYPE_INT_RGB);

    Graphics g = bi.createGraphics();
    p.paint(g);
    g.dispose();

    JOptionPane.showMessageDialog(null, new JLabel(new ImageIcon(bi)));

Not tested for side-effects.

Edit 2

Just grumbling a bit about the..

strategy 1 might be further tweaked to get around that [truncated table column]

Actually there is no tweak needed (or the the same tweak needed as for ScreenImage, depends on what you consider a tweak :) - both require to make the JScrollPane not do its job by setting the table's prefViewportSize, the exact same line in both:

    // strategy 1
    table.setPreferredScrollableViewportSize(table.getPreferredSize());
    p.addNotify();

    // strategy 2
    scroll.setColumnHeaderView(table.getTableHeader());
    table.setPreferredScrollableViewportSize(table.getPreferredSize());
Culottes answered 10/9, 2011 at 13:35 Comment(1)
+1 keep forgetting about addNotify. Also, now it looks like I will need to look at my ScreenImage class to see if the recursive doLayout() code can be replaced with a simple validate() :-)Smokestack
S
11

It was not a requirement of this thread to render the table without first displaying it, but that was the ultimate goal

ScreenImage handles this.

You must manually add the header to the scrollpane.

import javax.swing.*;
import java.awt.Graphics;
import java.awt.BorderLayout;
import java.awt.image.BufferedImage;

import javax.imageio.ImageIO;
import java.io.File;

class TableImage {

    public static void main(String[] args) throws Exception {
        Object[][] data = {
            {"Hari", new Integer(23), new Double(78.23), new Boolean(true)},
            {"James", new Integer(23), new Double(47.64), new Boolean(false)},
            {"Sally", new Integer(22), new Double(84.81), new Boolean(true)}
        };

        String[] columns = {"Name", "Age", "GPA", "Pass"};

        JTable table = new JTable(data, columns);
        JScrollPane scroll = new JScrollPane(table);

        scroll.setColumnHeaderView(table.getTableHeader());
        table.setPreferredScrollableViewportSize(table.getPreferredSize());

        JPanel p = new JPanel(new BorderLayout());
        p.add(scroll, BorderLayout.CENTER);

        BufferedImage bi = ScreenImage.createImage(p);

        JOptionPane.showMessageDialog(null, new JLabel(new ImageIcon(bi)));
        ImageIO.write(bi,"png",new File("table.png"));
    }
}

Note: Kleopatra's suggeston to use the addNotify() on the panel will not work with ScreenImage. The addNotify() method makes the component displayable and the ScreenImage code will only lay out the components for non-displayable components. I might look into making this more general.

Smokestack answered 11/9, 2011 at 0:58 Comment(2)
@MarkJeronimus, Still works for me using JDK7_60 on Windows 7. Maybe this is an OS issue? If so, try wrapping all the code in a SwingUtilties.invokeLater(...) to make sure the code executes on the EDT, which is the proper way to create a GUI.Smokestack
Inside ScreenImage.java at line component.paint( g2d );: NullPointerException in getVolatileOffscreenBuffer() because internally it gets the top-level ancestor window/dialog which doesn't exist. (I looked into the RepaintManager.java source for that)Phail
M
6

Manually adding your JPanel to a JFrame of your own creation, not just the one that JOptionPane uses, seems to resolve this.

Introducing the JFrame allows you to skip the initial dialog display, if you want.

// ...snip 

JOptionPane.showMessageDialog(null, p);

JFrame frame = new JFrame();
frame.setContentPane(p);
frame.pack();

BufferedImage bi = new BufferedImage(
    (int)p.getSize().getWidth(),
    (int)p.getSize().getHeight(),
    BufferedImage.TYPE_INT_RGB
);

Graphics g = bi.createGraphics();
p.paint(g);
g.dispose();
frame.dispose();

JOptionPane.showMessageDialog(null, new JLabel(new ImageIcon(bi)));
ImageIO.write(bi,"png",new File("table.png"));
Mignonmignonette answered 10/9, 2011 at 6:24 Comment(1)
Good point. It was not a requirement of this thread to render the table without first displaying it, but that was the ultimate goal.Amygdala
A
5

Table image

import javax.swing.*;
import javax.swing.table.JTableHeader;
import java.awt.Graphics;
import java.awt.BorderLayout;
import java.awt.image.BufferedImage;

import javax.imageio.ImageIO;
import java.io.File;

class TableImage {

    public static void main(String[] args) throws Exception {
        Object[][] data = {
            {"Hari", new Integer(23), new Double(78.23), new Boolean(true)},
            {"James", new Integer(23), new Double(47.64), new Boolean(false)},
            {"Sally", new Integer(22), new Double(84.81), new Boolean(true)}
        };

        String[] columns = {"Name", "Age", "GPA", "Pass"};

        JTable table = new JTable(data, columns);
        JScrollPane scroll = new JScrollPane(table);
        JPanel p = new JPanel(new BorderLayout());
        p.add(scroll,BorderLayout.CENTER);

        JOptionPane.showMessageDialog(null, p);

        JTableHeader h = table.getTableHeader();
        int x = h.getWidth();
        int y = h.getHeight() + table.getHeight();

        BufferedImage bi = new BufferedImage(
            (int)x,
            (int)y,
            BufferedImage.TYPE_INT_RGB
            );

        Graphics g = bi.createGraphics();
        h.paint(g);
        g.translate(0,h.getHeight());
        table.paint(g);

        JOptionPane.showMessageDialog(null, new JLabel(new ImageIcon(bi)));
        ImageIO.write(bi,"png",new File("table.png"));
    }
}
Amygdala answered 10/9, 2011 at 5:42 Comment(3)
I deleted my post, please amend your self-answerMinivet
I'd prefer you undelete your answer. But are you sure it works? I was unable to get it working in my example - maybe I was just using it wrong.Amygdala
heavens Dude, be sure that works, :-) I'm sure that you somewhere lost :-), and I can't see diference between Custom Print and Custom Paint, get size from JTable plus TableHeader separatelly and pass that to the Graphics2D,Minivet
M
4

please, that isn't answer just comment and little bit .... :-)

@Andrew Thompson, if I correctly understood for example, just reference to the J/Components is inside/past Graphics2D g2 = (Graphics2D) g; and not accepted referenced/derived TableHeader .... probably (I lazy to check that) something in API

code snipped shows :-)

g2.scale(scale,scale);
tableView.paint(g2);
g2.scale(1/scale,1/scale);
g2.translate(0f,pageIndex*pageHeightForTable);
g2.translate(0f, -headerHeightOnPage);
g2.setClip(0, 0,
   (int) Math.ceil(tableWidthOnPage), 
   (int)Math.ceil(headerHeightOnPage));
g2.scale(scale,scale);
tableView.getTableHeader().paint(g2);
//paint header at top

enter image description here

import javax.swing.*;
import javax.swing.table.JTableHeader;
import java.awt.Graphics;
import java.awt.BorderLayout;
import java.awt.Graphics2D;
import java.awt.image.BufferedImage;
import javax.imageio.ImageIO;
import java.io.File;

class TableImage {

    public static void main(String[] args) throws Exception {
        Object[][] data = {
            {"Hari", new Integer(23), new Double(78.23), (true)},
            {"James", new Integer(23), new Double(47.64), (false)},
            {"Sally", new Integer(22), new Double(84.81), (true)}
        };
        String[] columns = {"Name", "Age", "GPA", "Pass"};
        JTable table = new JTable(data, columns);
        JScrollPane scroll = new JScrollPane(table);
        JPanel p = new JPanel(new BorderLayout());
        p.add(scroll, BorderLayout.CENTER);
        JOptionPane.showMessageDialog(null, p);
        JTableHeader h = table.getTableHeader();
        int x = h.getWidth();
        int y = h.getHeight() + table.getHeight();
        BufferedImage bi = new BufferedImage(
                x, y, BufferedImage.TYPE_INT_RGB);
        Graphics g = bi.createGraphics();
        //g.translate(0, table.getTableHeader().getHeight());
        Graphics2D g2 = (Graphics2D) g;
        table.paint(g2);
        //table.paint(g);
        table.getTableHeader().paint(g2);
        //h.paint(g);
        JOptionPane.showMessageDialog(null, new JLabel(new ImageIcon(bi)));
        ImageIO.write(bi, "png", new File("ctable.png"));
    }

    private TableImage() {
    }
}
Minivet answered 10/9, 2011 at 6:16 Comment(0)
S
3

Try disabling double buffering on the component (or globally) before printing it (c.setDoubleBuffered(false)). Apparently it has an effect :) (see http://www.java2s.com/Code/Java/2D-Graphics-GUI/PrintSwingcomponents.htm and http://www.apl.jhu.edu/~hall/java/Swing-Tutorial/Swing-Tutorial-Printing.html)

Sistrunk answered 10/9, 2011 at 5:41 Comment(1)
Calling c.setDoubleBuffered(false) had no visible effect. Maybe it is purely related to printing.Amygdala

© 2022 - 2024 — McMap. All rights reserved.