I've looked over the forums but I can't find anything to help me. I've tried out some of the code but I still end up with a blank image.

What I'm trying to do is allow the user to draw an image in a JPanel and then I want to save it. It doesn't really matter what type of image. All I'm getting is a blank image of the JPanel. It will capture any background color I have in the JPanel but not anything the user has drawn.

I don't have any JFrame which from what I've read could be the problem. Is there any way to save the JPanel as an image without the use of a JFrame? The JPanel is on a LayeredPane along with a lot of other items.

Here's the code that just gives me the blank JPanel. What am I missing?

private void saveImage(JPanel p) {
        try {
            int w = p.getWidth(), h = p.getHeight();
            BufferedImage image = new BufferedImage(w, h, BufferedImage.TYPE_INT_RGB);
            Graphics g = image.createGraphics();
            p.setVisible(true);
            p.paint(g);
            ImageIO.write(image, "jpeg", new File("example.jpeg"));
        } 
        catch (IOException e) {
            System.err.println(e);
        }
    }

Thanks in advance.

Recommended Answers

All 31 Replies

You could try serializing the JPanel class and seeing if that works as wanted when you load it. I have no idea if that will work or not for an image the user drew on the panel though, but it might be worth your time to try it out since Serializing something doesn't take very long.

Thanks for the idea. I tried it and it doesn't work. Any other suggestions?

What is your mechanism for allowing the user to draw on the JPanel to begin with? You might have to save the way the user drew the image and duplicate it when the program is re-run.

Are you sure that your panel has width and height when you create the BufferedImage? How you are rendering what the user has drawn? What you have there should basically work.

edit: cross-post with BJSJC

Yes the JPanel definitely has width and height. The user has already drawn on it when I try to save it. Here's the code that I use to draw on the JPanel. That part is working fine.

Graphics G = drawingPanel.getGraphics();
            Graphics2D g = (Graphics2D)G;

            int currX = evt.getX();
            int currY = evt.getY();

            //Previous coordinates will only be 0,0 if this is a new line.
            //If this is the case, set previous coords equal to new coords
            //which will begin a new line.
            if (prevX == 0) { prevX = currX; }
            if (prevY == 0) { prevY = currY; }

            //If-else group to set the line color based on the global color value.
            Stroke stroke = new BasicStroke(4, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND );
            g.setStroke(stroke);
            g.setColor(Color.black);

            Line2D line = new Line2D.Float(prevX, prevY, currX, currY);
            g.draw(line);

            //Set previous coords equal to current coords to create a smooth line.
            prevX = currX;
            prevY = currY;

There is most likely your problem then: you should be rendering what the user has drawn by overriding the paintComponent() method on the JPanel.

Your save method is handing a graphics context from the image object you created to the panel to paint on. Unfortunately, that paint operation doesn't include what you've drawn onto the previous (and now invalid) graphics reference that you obtained earlier with getGraphics().

Ok great, at least I know where I'm going wrong. I'd still like you to clarify a couple of things for me though. What do I put into the overridden paintComponent() method? Do I call the overridden method instead of paint() in my save method?

Most of the drawing code itself needs to go into paintComponent(). paintComponent() will be called from the paint() method, so you can still use that with your image.

Update the rendering data in your UI methods and call repaint() to refresh the screen. Let paintComponent() handle the actual drawing of that data onto the graphics reference. You can see an example of that in this post: http://www.daniweb.com/forums/post1146173.html#post1146173 The data is being altered through a Timer, but the concept is still the same when using mouse events.

That solved the problem with the saving. It started a problem with the painting though. It's because of the MouseEvent. I can't pass the MouseEvent into the draw method like I used to because now it's the paintComponent() method. The line is only appearing as a dot in the current mouse dragged position. I've tried to get around this as shown in the code below but I can't. Is there another way to do this?

import java.awt.*;
import java.awt.event.*;
import java.awt.geom.Line2D;
import java.awt.image.BufferedImage;
import java.io.*;
import javax.imageio.ImageIO;
import javax.swing.*;

class SaveEx extends JPanel implements MouseMotionListener{
    int prevX =0;
    int prevY=0;
    int currX = 0;
    int currY = 0;
    MouseEvent evt;

    public static void main(String[] args) {
        JFrame f = new JFrame("SaveEx");
        Container cp = f.getContentPane();
        final SaveEx p = new SaveEx();
        p.setSize(new Dimension(120,120));

        p.setBackground(Color.yellow);
        final Component comp = cp.add(p);
        final JPanel south = new JPanel();

        cp.add(south, BorderLayout.SOUTH);


        JButton save = (JButton) south.add(new JButton("save"));
        save.addActionListener(new ActionListener(){
            public void actionPerformed(ActionEvent evt) {
                try {
                    int w = comp.getWidth(), h = comp.getHeight();
                    BufferedImage image = new BufferedImage(w, h,
                        BufferedImage.TYPE_INT_RGB);
                    Graphics2D g2 = image.createGraphics();
                    comp.paint(g2);
                    g2.dispose();
                    ImageIO.write(image, "jpeg", new File("example.jpeg"));
                } catch (IOException e) {
                    System.err.println(e);
                }
            }
        });

        //JButton draw = (JButton) south.add(new JButton("draw"));
        p.addMouseMotionListener(new MouseMotionListener(){
            public void mouseDragged(MouseEvent evt) {
                
                Graphics g = comp.getGraphics();
                p.currX = evt.getX();
                p.currY = evt.getY();
                 
                //p.paintComponent(g);
                p.repaint();
            }

            public void mouseMoved(MouseEvent e) {
            }
        });
        f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        f.setSize(400,400);
        f.show();
    }

    @Override
    public void paintComponent(Graphics g)
    {
        super.paintComponent(g);
        Graphics2D g2 = (Graphics2D)g;

        //Previous coordinates will only be 0,0 if this is a new line.
        //If this is the case, set previous coords equal to new coords
        //which will begin a new line.
        if (prevX == 0) { prevX = currX; }
        if (prevY == 0) { prevY = currY; }

        //If-else group to set the line color based on the global color value.
        Stroke stroke = new BasicStroke(4, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND );
        g2.setStroke(stroke);
        g2.setColor(Color.black);

        Line2D line = new Line2D.Float(prevX, prevY, currX, currY);
        g2.draw(line);

        //Set previous coords equal to current coords to create a smooth line.
        prevX = currX;
        prevY = currY;
    }

    public void mouseDragged(MouseEvent e) {
        throw new UnsupportedOperationException("Not supported yet.");
    }

    public void mouseMoved(MouseEvent e) {
        throw new UnsupportedOperationException("Not supported yet.");
    }
}

At line 87, you're setting prev coords equal to the current coords each repaint. You don't want to do that. Leave the prev values where they are at.

It does draw a line if I remove those 2 lines. However it only draws one. Any time I try to draw another one it gets rid of the old one and uses the same starting point. I can't figure this out!

You'll need to save the line coordinates if you want to draw more.

It's only drawing a straight line. I want to be able to have a full drawing ability like I did before I had to change it to override paintComponent(). Do I have to save every single coordindate that the user wants to draw?

Yes, otherwise how does it know how to redraw it on repaint? You can use a GeneralPath for that.

Your other option is to use an offscreen buffer image to draw on and then render that in paintComponent(). That adds a couple of additional issues with resizing and clearing, but it's another option if you don't want to save shapes.

I've gotten it to draw and save using a GeneralPath. The only problem is that there's quite a lag. I have to move the mouse very slowly for it to register each set of coordinates as now it has to store and redraw each time the mouse moves. I don't suppose there's any way to speed this up?

Just adding a point to a path and repainting shouldn't really be causing a noticeable lag.

shape.lineTo(e.getX(), e.getY());
repaint();

If you're still having a lag problem, post your revised code and I'll take a look.

import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Stroke;
import java.awt.event.MouseEvent;
import java.awt.geom.GeneralPath;
import java.util.Vector;
import javax.swing.JPanel;

public class DrawingPanel extends JPanel{

    public int currX = 0;
    public int currY = 0;
    
    public Vector generalPath = new Vector();
    public GeneralPath path;
   
   public void mouseDragged(MouseEvent e) {
        currX = e.getX();
        currY = e.getY();
        path = new GeneralPath();
        path.moveTo(currX, currY);
        generalPath.addElement(path);

        repaint();
    }

    public void mouseMoved(MouseEvent e) { }

    @Override
    public void paintComponent(Graphics g)
    {
        super.paintComponent(g);
        Graphics2D g2 = (Graphics2D)g;

        for (int i = 0; i < generalPath.size(); i++)
        {
            g2.setPaint(Color.black);
            Stroke stroke = new BasicStroke(4, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND );
            g2.setStroke(stroke);
            g2.setColor(Color.black);
            g2.draw((GeneralPath) generalPath.elementAt(i));
        }
    }
}

Thanks. Also, is there any way to check if a point is in a GeneralPath and delete it if it is? I'm trying to rewrite my erase function so that it works with the new way of doing things.

Okay, this may work a little better for you. It keep a list of the shapes, so erase should be pretty easy for you to implement. If you need to check a shape for a point, there is a contains() method on Path2D.Float.

class ScratchPanel extends JPanel implements MouseMotionListener, MouseListener{

    List<Shape> shapeList = new ArrayList<Shape>();
    Path2D.Float currentShape=null;

    public ScratchPanel(){
        addMouseListener(this);
        addMouseMotionListener(this);
    }

    @Override
    protected void paintComponent(Graphics g) {
        super.paintComponent(g);
        Graphics2D g2 = (Graphics2D)g;
        for(Shape s : shapeList){
            g2.draw(s);
        }
    }

    public void mouseDragged(MouseEvent e) {
        currentShape.lineTo(e.getX(), e.getY());
        repaint();
    }

    public void mouseMoved(MouseEvent e) {}

    public void mouseClicked(MouseEvent e) {}

    public void mousePressed(MouseEvent e) {
        currentShape = new Path2D.Float();
        currentShape.moveTo(e.getX(), e.getY());
        shapeList.add(currentShape);
    }

    public void mouseReleased(MouseEvent e) {}
    public void mouseEntered(MouseEvent e) {}
    public void mouseExited(MouseEvent e) {}
}

That's great, works perfectly, no lag. Thanks!

Could you please explain why its better than my way? Also, I don't understand Path2d.Float in line 4.

You were creating a new GeneralPath with each point in mouseDragged and adding it to your vector. You really only need one for each "shape" since the path is just a series of connected points.

Path2D.Float is just the more recent incarnation of GeneralPath. GeneralPath is an older legacy class.

I've been trying and I can't understand how I can access a Path2D.Float in shapeList to check for a point. How should it be accessed?

I also can't find a way to remove it from the shape. Would I be best off having another shapeList with my erased points and make them white?

Ah, I thought you wanted to erase the whole shape. Determining if a path contains a point is very easy, but like you mentioned, I don't see any direct way of removing a point from that path. You may be right that drawing a new shape with the background color might be the easiest way to accomplish an "eraser" mode.

This version uses an offscreen buffer image to draw on. Erasing is trivial with this one, but you don't have the easy shape management like the other one. Hold down 'shift' to erase

class ScratchPanel2 extends JPanel implements MouseMotionListener, MouseListener{

    BufferedImage image;

    public ScratchPanel2(){
        addMouseListener(this);
        addMouseMotionListener(this);
        addComponentListener(new java.awt.event.ComponentAdapter() {
            public void componentResized(java.awt.event.ComponentEvent evt) {
                panelResized(evt);
            }
        });
    }

    @Override
    protected void paintComponent(Graphics g) {
        super.paintComponent(g);
        Graphics2D g2 = (Graphics2D)g;
        g2.drawImage(image, 0, 0, this);
    }

    private void panelResized(ComponentEvent evt) {
        if (getWidth()>0 && getHeight()>0){
            // create new temp image
            BufferedImage tempImage = (BufferedImage)createImage(getWidth(),getHeight());
            // draw old image to the new one
            Graphics2D g2 = (Graphics2D)tempImage.getGraphics();
            g2.drawImage(image, 0, 0, this);
            // swap
            image = tempImage;
        }
    }

    public void mouseDragged(MouseEvent e) {
        Graphics2D g2 = (Graphics2D)image.getGraphics();
        g2.setColor(e.isShiftDown() ? getBackground() : Color.BLUE);
        g2.fillRect(e.getX(), e.getY(), 2, 2);
        g2.dispose();
        repaint();
    }

    public void mouseMoved(MouseEvent e) {}

    public void mouseClicked(MouseEvent e) {}

    public void mousePressed(MouseEvent e) {
        mouseDragged(e);
    }

    public void mouseReleased(MouseEvent e) {}
    public void mouseEntered(MouseEvent e) {}
    public void mouseExited(MouseEvent e) {}
}

Thanks for all of your help. I haven't had a chance to implement an eraser function yet but at least my original problem is fixed.

I've been looking back over this code again and don't understand the panelResized method. I know it doesn't work without it though. I understand the code, but not why it's needed. Could someone please explain this to me?

Thanks

When the panel is resized, you have to create a new "image" with the new dimensions. That code is creating that new image, copying the current image onto it, and then replacing the current "image" reference with the new one. Essentially it's just re-sizing your buffer image.

Be a part of the DaniWeb community

We're a friendly, industry-focused community of developers, IT pros, digital marketers, and technology enthusiasts meeting, networking, learning, and sharing knowledge.