views:

1083

answers:

4

In my program I frequently need to print various JComponents (generally JPanels) and I like them to be full-page. The way I do it now is by using the following code:

g2d.scale(pf.getImageableWidth()/componentToPrint.getWidth(), pf.getImageableHeight()/componentToPrint.getHeight());

but this often stretches or otherwise deforms whatever I am trying to print, and I would much prefer to do something that re-sized intelligently, perhaps a functional version of:

componentToPrint.setSize(pf.ImageableWidth(), pf.ImageableHeight);

or say adding the component into a new JFrame and then setting the frame size (problem is components can't exist in two place at once). I wouldn't care if the resizing would make the rest of the GUI look terrible, as long as it is something that can easily be reset.

Is there any way to do this?

A: 

I think you answered your own question. Each Component has a method :

setSize(int width, int height);
Savvas Dalkitsis
the component is under the control of a layout manager, and therefore does not have control over its own size. Calling that will do nothing.
Lesman
A: 

I think the problem you're facing has nothing to do with scale operation. It is your logic that is incorrect. In most cases your x and y scales will be different, since the scale of Imageable and and your component's scale will not be the same. This is the reason your component is distorted after the scale operation. You have to do something like:

double factorX = pf.getImageableWidth() / component.getWidth();
double factorY = pf.getImageableHeight() / component.getHeight();
double factor = Math.min( factorX, factorY );
g2.scale(factor,factor);

After that you can translate your image to the appropriate coordinates according to it's new size. Hope it helps...

eugener
Not quite: the problem isn't just the aspect ration. Most of my GUI relies heavily on GridBagLayout which intelligently re-sizes components to best fit them into the space. For example, maximizing will create larger gaps between objects, and shrinking may cause text to be cut short "sample te...". Also, any image based scaling will lead to lower quality, as opposed to intelligent component re-sizing.
Lesman
I do understand what you said about GridBagLayout, but don't agree with what you about scaling. In this case it is NOT AN IMAGE SCALING, it is a AffineTransform applied to graphical operations before they are executed. So there is no loss of quality at all.
eugener
That's a good point, I did not check how it scaled. Nevertheless, even though the image quality is consistent, any scaling of that kind makes the printouts inconsistent (depending on the aspect ration on the screen) and not necessarily optimally displayed in the printing space.
Lesman
I'm not sure why you insisting on your point. My panels (with a lot of components on them) are printing fine. The code I have finds optimal scale so the panel would fit the page and then centers it on the page.This works in production for months now.
eugener
A: 

I would refactor the drawing code out of the paintComponent() method of your implementation of JPanel to a public/package protected method in that panel which can draw to an arbitrary Graphics object, of any width/height (presumably that the drawing code is general enough).

This example has a frame, containing a panel, which has some drawing logic (a big X, the size of the panel). The main method of this example shows you a way to get an image and write it to a file, even if the image size is different than the panel's size.


import java.awt.Color;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.image.BufferedImage;
import java.io.File;

import javax.imageio.ImageIO;
import javax.swing.JFrame;
import javax.swing.JPanel;

public class MockFrame extends JFrame {

    // throws Exception, as just an example (not really advised to do this)
    public static void main(String[] args) throws Exception {
        MockFrame frame = new MockFrame();
        frame.setVisible(true);

        // different sizes from the frame
        int WIDTH = 500;
        int HEIGHT = 500;

        BufferedImage b = new BufferedImage(WIDTH, HEIGHT,
            BufferedImage.TYPE_INT_ARGB);

        Graphics2D g2d = (Graphics2D) b.getGraphics();

        // should set some background, as the panel's background
        // is dealt with by super.paintComponent()
        g2d.setBackground(Color.white);

        frame.getPanel().drawingLogic(b.getGraphics(), WIDTH, HEIGHT);

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

    }

    private MockPanel panel;

    public MockFrame() {
        this.setSize(200, 200);
        this.setDefaultCloseOperation(EXIT_ON_CLOSE);

        panel = new MockPanel();
        getContentPane().add(panel);
    }

    public MockPanel getPanel() {
        return panel;
    }

    private class MockPanel extends JPanel {

    @Override
    protected void paintComponent(Graphics g) {
        super.paintComponent(g);

        drawingLogic(g, getWidth(), getHeight());
    }

    public void drawingLogic(Graphics g, int width, int height) {
        g.setColor(Color.black);

        g.drawLine(0, 0, width, height);
        g.drawLine(0, height, width, 0);
    }
    }
}

This allows objects external to the GUI to hook into its drawing algorithm. One drawback I see is that you will create a dependency between any object wanting to print the panel to the actual implementation of the panel. But it's still better than resizing the panel on the fly (I have tried that and seems to have some issues - I think it takes some time for the setSize() change to propagate.

EDIT:

In response to the comments, I have provided a modified version of the code snippet above. It is probably not the best way to do it, and not very user friendly (so I wouldn't use it in an end-user application), but it does resize everything in the frame according to the rules of the layout manager.


/* This code snippet describes a way to resize a frame for printing at 
 * a custom size and then resize it back.
 * 
 * Copyright (C)   
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see .
 */

import java.awt.Container;
import java.awt.Graphics2D;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.Insets;
import java.awt.image.BufferedImage;
import java.io.File;

import javax.imageio.ImageIO;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JOptionPane;

public class MockFrame extends JFrame {

    // throws Exception, as just an example (not really advised to do this)
    public static void main(String[] args) throws Exception {
        final MockFrame frame = new MockFrame();
        frame.setVisible(true);

        // different sizes from the frame
        final int WIDTH = 500;
        final int HEIGHT = 700;

        final BufferedImage b = new BufferedImage(WIDTH, HEIGHT,
                BufferedImage.TYPE_INT_ARGB);

        final Graphics2D g2d = (Graphics2D) b.getGraphics();

        final int previousWidth = frame.getWidth();
        final int previousHeight = frame.getHeight();

        frame.setSize(WIDTH, HEIGHT);
        frame.repaint();

        JOptionPane.showMessageDialog(null,
                "Press OK when the window has finished resizing");

        frame.print(g2d);
        frame.setSize(previousWidth, previousHeight);
        ImageIO.write(b, "png", new File("test.png"));

    }

    public MockFrame() {
        this.setSize(200, 200);
        this.setDefaultCloseOperation(EXIT_ON_CLOSE);

        boolean shouldFill = true;
        boolean shouldWeightX = true;

        Container pane = getContentPane();

        // code from
        // http://java.sun.com/docs/books/tutorial/uiswing/layout/gridbag.html
        // just to add some components in the frame... :)
        // left out in here for brevity

    }

}

The code basically resizes the frame, shows the user a confirm message so the thread can be blocked until the repaint is done (could be done by Thread.sleep(), but it's more transparent using a message). Then it prints the frame and resizes it back to its original shape. A bit hacky, but it works.

-- Flaviu Cipcigan

Flaviu Cipcigan
That's what I'm looking for, but I'm not sure how I could make this work. All the paint logic is from the components inherited paint method because it's all Swing components; nothing is drawn by my code. All that code relies on the components actual size to paint, so without changing the components actual size, I can't change the way they paint.
Lesman
See my edited post above for an revised answer to your comment. Cheers!
Flaviu Cipcigan
That is what I was hoping to do, but a randomly selected Component is likely to be controlled by a Layout Manager, so the setSize() method does nothing.
Lesman
A: 

I think the solution you're looking for would be to construct a new JPanel that contains the desired contents and to print the copy instead. If you do so using the CellRendererPane you can get the exact resizing behaviour it sounds like you're looking for.

If your JComponents are reasonably well written then it shouldn't be a problem to new up a fresh one and to set its model to be the same as the original's.

CellRendererPane cellRendererPane = new CellRendererPane();
// It's important to add the cell renderer pane to something
// you can use the same one for all of your exporting if you like and just
// add it to your main frame's content pane - it won't show up anywhere.
add(cellRendererPane);

JPanel printPanel = createCopy(panel);
cellRendererPane.paintComponent(g, printPanel, null, 0, 0, exportDim.width, exportDim.height, true);

Here's a complete working example. The createPanel() method should create whatever component it is that you want rendered. A real example should be sure to use the same model rather than recreating a new model for a throwaway component.

public class SCCE {

 public static void main(String[] args) throws Exception {
  UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
  final JFrame f = new JFrame("SCCE");
  f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);


  f.getContentPane().setLayout(new BorderLayout());
  f.getContentPane().add(SCCE.createPanel());

  final CellRendererPane backgroundRenderer = new CellRendererPane();

  // Add the renderer somewhere where it won't be seen
  f.getContentPane().add(backgroundRenderer, BorderLayout.NORTH);
  f.getContentPane().add(createSaveButton(backgroundRenderer), BorderLayout.SOUTH);

  f.pack();
  f.setLocationRelativeTo(null);
  f.setVisible(true);
 }

 // Create your custom component from whatever model here..
 private static final Component createPanel() {
  DefaultListModel model = new DefaultListModel();
  for (int i = 0; i < 10; i++) {
   model.addElement("Item number " + i);
  }
  return new JList(model);
 }

 private static JButton createSaveButton(final CellRendererPane backgroundRenderer) {
  return new JButton(new AbstractAction("Save image to file") {
   @Override
   public void actionPerformed(ActionEvent e) {
    Dimension d = new Dimension(400, 300);

    BufferedImage img = new BufferedImage(d.width, d.height, BufferedImage.TYPE_INT_ARGB);
    Graphics2D g = img.createGraphics();
    backgroundRenderer.paintComponent(g, createPanel(), null, 0, 0, d.width, d.height, true);
    g.dispose();

    try {
     File output = new File("test.png");
     System.err.println("Saved to " + output.getAbsolutePath());
     ImageIO.write(img, "png", output);
    } catch (IOException ex) {
     ex.printStackTrace();
    }
   }
  });
 }

}
Nj