Search code examples
javaswingcanvasprintingjpanel

JPanel print attached canvases shows up as blank


I am trying to help my son do math, so I made a small program. I have a JPanel with 12 Canvases and a giant print button on the bottom.

Window I want to print

This is the listener I have attached on my print button.

    print.addActionListener(new ActionListener()
    {
        @Override
        public void actionPerformed(ActionEvent e)
        {
            panel.remove(print);
            window.pack();
            printComponent(panel, false);
            panel.add(print);
            window.pack();
        }
    });

I have followed example from here and sadly it doesn't work. When I pass JFrame as the component then it prints the whole window! Window printed! When I pass JPanel as the component then it only prints the print button! Print button only printed!

If it is of any relevance I attach everything to the JFrame and JPanel like so:

panel.setBackground(Color.WHITE);
panel.setLayout(/*layout stuff*/);
window.setContentPane(panel);
for (int i = 0; i < 12; i++)
{
    panel.add(/*canvas class which renders one single question*/);
}
panel.add(print, /*layout constraints*/);

I have even tried following examples from here and here and nothing works. I can't seem to just print jpanel with the canvas showing up, and I sure as heck do not want to print the whole window. Any advice on how to fix this?

Here is small example code with same problem.

package carefree.school;

import javax.imageio.ImageIO;
import javax.swing.*;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.awt.print.PageFormat;
import java.awt.print.Paper;
import java.awt.print.Printable;
import java.io.File;
import java.io.IOException;

public class TestWindow
{
    public static void main(String[] args)
    {
        JFrame frame = new JFrame();
        JPanel panel = new JPanel();
        Canvas canvas = new Canvas()
        {
            @Override
            public void paint(Graphics g)
            {
                super.paint(g);
                g.drawOval(3, 3, 94, 94);
            }
        };
        canvas.setSize(100, 100);
        JButton print = new JButton("Print");
        frame.setContentPane(panel);
        panel.add(canvas);
        panel.add(print);
        frame.setVisible(true);
        frame.pack();
        print.addActionListener(e -> printComponentToFile(panel, false));
    }

    public static void printComponentToFile(Component comp, boolean fill)
    {
        Paper paper = new Paper();
        paper.setSize(8.3 * 72, 11.7 * 72);
        paper.setImageableArea(18, 18, 559, 783);

        PageFormat pf = new PageFormat();
        pf.setPaper(paper);
        pf.setOrientation(PageFormat.PORTRAIT);

        BufferedImage img = new BufferedImage((int) Math.round(pf.getWidth()), (int) Math.round(pf.getHeight()),
                                              BufferedImage.TYPE_INT_RGB);

        Graphics2D g2d = img.createGraphics();
        g2d.setColor(Color.WHITE);
        g2d.fill(new Rectangle(0, 0, img.getWidth(), img.getHeight()));
        ComponentPrinter cp = new ComponentPrinter(comp, fill);
        try
        {
            cp.print(g2d, pf, 0);
        }
        finally
        {
            g2d.dispose();
        }

        try
        {
            ImageIO.write(img, "png", new File("Page-" + (fill ? "Filled" : "") + ".png"));
        }
        catch (IOException ex)
        {
            ex.printStackTrace();
        }
    }

    public static class ComponentPrinter
            implements Printable
    {
        private Component comp;
        private boolean   fill;

        public ComponentPrinter(Component comp, boolean fill)
        {
            this.comp = comp;
            this.fill = fill;
        }

        @Override
        public int print(Graphics g, PageFormat format, int page_index)
        {
            if (page_index > 0)
            {
                return Printable.NO_SUCH_PAGE;
            }

            Graphics2D g2 = (Graphics2D) g;
            g2.translate(format.getImageableX(), format.getImageableY());

            double width  = (int) Math.floor(format.getImageableWidth());
            double height = (int) Math.floor(format.getImageableHeight());

            if (!fill)
            {

                width = Math.min(width, comp.getPreferredSize().width);
                height = Math.min(height, comp.getPreferredSize().height);

            }

            comp.setBounds(0, 0, (int) Math.floor(width), (int) Math.floor(height));
            if (comp.getParent() == null)
            {
                comp.addNotify();
            }
            comp.validate();
            comp.doLayout();
            comp.printAll(g2);
            if (comp.getParent() != null)
            {
                comp.removeNotify();
            }

            return Printable.PAGE_EXISTS;
        }
    }
}

Solution

  • First, Don't use Canvas for this, I'm not sure it's "printable", or at the very least, it didn't work for me.

    Instead, extend from JPanel instead...

    JPanel canvas = new JPanel() {
        @Override
        protected void paintComponent(Graphics g) {
            super.paintComponent(g);
            g.drawOval(3, 3, 94, 94);
        }
    
        @Override
        public Dimension getPreferredSize() {
            return new Dimension(100, 100);
        }
    };
    

    You should also avoid call setBounds and prefer to provide sizing hints instead.

    Second, choose the component you want to print carefully, for example, in your runnable code, you're passing panel, which will print both the button and the circle, instead, you should really only need to pass canvas

    print.addActionListener(e -> printComponentToFile(canvas, false));
    

    With these couple of fixes, it now outputs...

    enter image description here

    I also thing that if (comp.getParent() != null) { is a mistake and instead should be if (comp.getParent() == null) {

    You may also need to call

    comp.revalidate();
    comp.repaint();
    

    after you've printed the component, but this is one of the many issues with trying to print a "live" component.

    Personally, I create a concept of a "renderable question", which could be passed a Graphics context onto which it should be rendered.

    You could then create a "renderable question sheet" which would paint multiple "questions".

    This could then be used by both a custom component and Printable independently, but that's me.