4

I'm trying to create a graphical component that allows me to draw one rectangle on a selected image (the rectangle must be drawn using drag-and-drop operations): the purpose of this component is to get the coordinates and the size of the rectangle drawn; the second goal is to provide a component which can be easily integrated in a graphical user interface.

The authors of this example created a subclass of JLabel in order to draw the image, then they added a MouseInputAdapter to the instance of this subclass in order to deal with the drawing of the rectangle.

I was inspired by that example, with the difference that I have created a subclass of JPanel class: I named it FigurePanel class. Then I made some changes in order to provide the following features:

  • if the image is larger than the instance of FigurePanel, then the scrollers must appear;
  • if the image is smaller than the instance of FigurePanel, then this image must be placed in the center of the panel;
  • while the user draws the rectangle, it should not extend beyond the limits of the image.

Here is the source code of FigurePanel class.

package imageselectionproject;

import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Image;
import java.awt.Rectangle;
import java.awt.event.MouseEvent;
import javax.swing.JPanel;
import javax.swing.event.MouseInputAdapter;


public class FigurePanel extends JPanel
{
    private Image backgroundImage = null;
    private Rectangle imageLimits = new Rectangle(0, 0, getWidth(), getHeight());

    private Rectangle currentRect = null;
    private Rectangle rectToDraw = null;
    private final Rectangle previousRectDrawn = new Rectangle();


    public FigurePanel()
    {
        setOpaque(true);

        SelectionListener listener = new SelectionListener();
        addMouseListener(listener);
        addMouseMotionListener(listener);
    }

    @Override
    public Dimension getPreferredSize()
    {
        return backgroundImage == null ? super.getPreferredSize() : new Dimension(backgroundImage.getWidth(this), backgroundImage.getHeight(this));
    }

    @Override
    protected void paintComponent(Graphics g)
    {
        super.paintComponent(g); //paints the background and image

        if (backgroundImage != null)
        {
            g.drawImage(backgroundImage, imageLimits.x, imageLimits.y, this);
        }

        // If currentRect exists, paint a box on top.
        if (currentRect != null)
        {
            // Draw a rectangle on top of the image.
            g.setXORMode(Color.white); // Color of line varies
                                       // depending on image colors
            g.drawRect(rectToDraw.x, rectToDraw.y, 
                       rectToDraw.width - 1, rectToDraw.height - 1);

            System.out.println(rectToDraw);
        }
    }


    public void setImage(Image image)
    {
        int x = 0;
        int y = 0;

        if (image != null)
        {
            backgroundImage = image;

            // The following instructions are used to center the image on the panel.
            /*x = (getSize().width - image.getWidth(this)) / 2;
            y = (getSize().height - image.getHeight(this)) / 2;

            if (x < 0) x = 0;
            if (y < 0) y = 0;*/
        }
        else
        {
            backgroundImage = null;
        }

        currentRect = null;

        setSize(getPreferredSize());
        imageLimits.setBounds(x, y, getWidth(), getHeight());
        System.out.println("imageLimits = " + imageLimits);

        repaint();
    }

    private void updateDrawableRect()
    {
        int x = currentRect.x;
        int y = currentRect.y;
        int width = currentRect.width;
        int height = currentRect.height;

        // Make the width and height positive, if necessary.
        if (width < 0)
        {
            width = 0 - width;
            x = x - width + 1;
            if (x < 0)
            {
                width += x;
                x = 0;
            }
        }
        if (height < 0)
        {
            height = 0 - height;
            y = y - height + 1;
            if (y < 0)
            {
                height += y;
                y = 0;
            }
        }

        // The rectangle should not extend beyond the boundaries of the image.
        if (x < imageLimits.x)
        {
            width -= (imageLimits.x - x);
            x = imageLimits.x;
        }
        else if ((x + width) > imageLimits.x + imageLimits.width)
        {
            width = imageLimits.x + imageLimits.width - x;
        }
        if (y < imageLimits.y)
        {
            height -= (imageLimits.y - y);
            y = imageLimits.y;
        }
        if ((y + height) > imageLimits.y + imageLimits.height)
        {
            height = imageLimits.y + imageLimits.height - y;
        }

        // Update rectToDraw after saving old value.
        if (rectToDraw != null)
        {
            previousRectDrawn.setBounds(rectToDraw.x, rectToDraw.y, 
                                        rectToDraw.width, rectToDraw.height);
            rectToDraw.setBounds(x, y, width, height);
        }
        else
        {
            rectToDraw = new Rectangle(x, y, width, height);
        }
    }

    private class SelectionListener extends MouseInputAdapter
    {
        @Override
        public void mousePressed(MouseEvent e)
        {
            int x = e.getX();
            int y = e.getY();
            currentRect = new Rectangle(x, y, 0, 0);
            updateDrawableRect();
            repaint();
        }

        @Override
        public void mouseDragged(MouseEvent e)
        {
            updateSize(e.getX(), e.getY());
        }

        @Override
        public void mouseReleased(MouseEvent e)
        {
            updateSize(e.getX(), e.getY());
        }

        /* 
         * Update the size of the current rectangle
         * and call repaint.  Because currentRect
         * always has the same origin, translate it
         * if the width or height is negative.
         * 
         * For efficiency (though
         * that isn't an issue for this program),
         * specify the painting region using arguments
         * to the repaint() call.
         * 
         */
        void updateSize(int x, int y)
        {
            currentRect.setSize(x - currentRect.x, y - currentRect.y);
            updateDrawableRect();

            Rectangle totalRepaint = rectToDraw.union(previousRectDrawn);
            repaint(totalRepaint.x, totalRepaint.y,
                    totalRepaint.width, totalRepaint.height);
        }
    }

}

The method setImage is used to set a new image, so it invokes the method repaint to redraw the graphical component. In the code shown above, I disabled (via comments) instructions to center the image: in this way, the component seems to work properly.

Instead, if I enable such instructions, the image is correctly positioned in the center of the panel when it is smaller than the panel itself, however, I encountered the following problem: suppose that it is currently displayed an image larger than the panel, if the new image that I decide to load is smaller than the currently displayed image, then the new image is not displayed; if I try to reload the new image, then it appears.

Why does this problem occur? How to solve it?

I also created the FigurePanelTest class in order to test the FigurePanel class.

package imageselectionproject;

import java.awt.Container;
import java.awt.Dimension;
import java.awt.FlowLayout;
import java.awt.Image;
import java.awt.Point;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.io.File;
import java.io.IOException;
import javax.imageio.ImageIO;
import javax.swing.JButton;
import javax.swing.JFileChooser;
import javax.swing.JFrame;
import javax.swing.JScrollPane;


public class FigurePanelTest extends JFrame
{
    public FigurePanelTest()
    {
        FigurePanel imagePanel = new FigurePanel();

        JScrollPane imageScrollPane = new JScrollPane();
        imageScrollPane.setPreferredSize(new Dimension(420, 250));
        imageScrollPane.setViewportView(imagePanel);

        JButton imageButton = new JButton("Load Image");
        imageButton.addActionListener(
                new ActionListener()
                {
                    @Override
                    public void actionPerformed(ActionEvent evt)
                    {
                        JFileChooser fc = new JFileChooser();
                        int returnValue = fc.showOpenDialog(null);
                        if (returnValue == JFileChooser.APPROVE_OPTION) {
                            File selectedFile = fc.getSelectedFile();
                            System.out.println(selectedFile.getName());

                            try
                            {
                                Image image = ImageIO.read(selectedFile.getAbsoluteFile());
                                imagePanel.setImage(image);

                                imageScrollPane.getViewport().setViewPosition(new Point(0, 0));
                            }
                            catch(IOException e)
                            {
                                e.printStackTrace();
                            }
                        }
                    }
                }
        );

        Container container = getContentPane();
        container.setLayout(new FlowLayout());
        container.add(imageScrollPane);
        container.add(imageButton);

        setSize(600, 400);
        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    }

}

Here is the main.

public static void main(String args[]) {
        /* Create and display the form */
        java.awt.EventQueue.invokeLater(new Runnable() {
            public void run() {
                new FigurePanelTest().setVisible(true);
            }
        });
    }

For Andrew, a single program:

import java.awt.Color;
import java.awt.Container;
import java.awt.Dimension;
import java.awt.FlowLayout;
import java.awt.Graphics;
import java.awt.Image;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.MouseEvent;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;

import javax.imageio.ImageIO;
import javax.swing.JButton;
import javax.swing.JFileChooser;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.event.MouseInputAdapter;

public class TestDrawPanel {
   public static void main(String args[]) {
      /* Create and display the form */
      java.awt.EventQueue.invokeLater(new Runnable() {
         public void run() {
            new FigurePanelTest().setVisible(true);
         }
      });
   }
}

class FigurePanelTest extends JFrame {
   public FigurePanelTest() {
      final FigurePanel imagePanel = new FigurePanel();

      final JScrollPane imageScrollPane = new JScrollPane();
      imageScrollPane.setPreferredSize(new Dimension(420, 250));
      imageScrollPane.setViewportView(imagePanel);

      JButton imageButton = new JButton("Load Image");
      imageButton.addActionListener(new ActionListener() {
         @Override
         public void actionPerformed(ActionEvent evt) {
            JFileChooser fc = new JFileChooser();
            int returnValue = fc.showOpenDialog(null);
            if (returnValue == JFileChooser.APPROVE_OPTION) {
               File selectedFile = fc.getSelectedFile();
               System.out.println(selectedFile.getName());

               try {
                  Image image = ImageIO.read(selectedFile.getAbsoluteFile());
                  imagePanel.setImage(image);

                  imageScrollPane.getViewport()
                        .setViewPosition(new Point(0, 0));
               } catch (IOException e) {
                  e.printStackTrace();
               }
            }
         }
      });

      Container container = getContentPane();
      container.setLayout(new FlowLayout());
      container.add(imageScrollPane);
      container.add(imageButton);

      setSize(600, 400);
      setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
   }

}

class FigurePanel extends JPanel {
   private Image backgroundImage = null;
   private Rectangle imageLimits = new Rectangle(0, 0, getWidth(), getHeight());

   private Rectangle currentRect = null;
   private Rectangle rectToDraw = null;
   private final Rectangle previousRectDrawn = new Rectangle();

   public FigurePanel() {
      setOpaque(true);

      SelectionListener listener = new SelectionListener();
      addMouseListener(listener);
      addMouseMotionListener(listener);
   }

   @Override
   public Dimension getPreferredSize() {
      return backgroundImage == null ? super.getPreferredSize()
            : new Dimension(backgroundImage.getWidth(this),
                  backgroundImage.getHeight(this));
   }

   @Override
   protected void paintComponent(Graphics g) {
      super.paintComponent(g); // paints the background and image

      if (backgroundImage != null) {
         g.drawImage(backgroundImage, imageLimits.x, imageLimits.y, this);
      }

      // If currentRect exists, paint a box on top.
      if (currentRect != null) {
         // Draw a rectangle on top of the image.
         g.setXORMode(Color.white); // Color of line varies
                                    // depending on image colors
         g.drawRect(rectToDraw.x, rectToDraw.y, rectToDraw.width - 1,
               rectToDraw.height - 1);

         System.out.println(rectToDraw);
      }
   }

   public void setImage(Image image) {
      int x = 0;
      int y = 0;

      if (image != null) {
         backgroundImage = image;

         // The following instructions are used to center the image on the
         // panel.
         /*
          * x = (getSize().width - image.getWidth(this)) / 2; y =
          * (getSize().height - image.getHeight(this)) / 2;
          * 
          * if (x < 0) x = 0; if (y < 0) y = 0;
          */
      } else {
         backgroundImage = null;
      }

      currentRect = null;

      setSize(getPreferredSize());
      imageLimits.setBounds(x, y, getWidth(), getHeight());
      System.out.println("imageLimits = " + imageLimits);

      repaint();
   }

   private void updateDrawableRect() {
      int x = currentRect.x;
      int y = currentRect.y;
      int width = currentRect.width;
      int height = currentRect.height;

      // Make the width and height positive, if necessary.
      if (width < 0) {
         width = 0 - width;
         x = x - width + 1;
         if (x < 0) {
            width += x;
            x = 0;
         }
      }
      if (height < 0) {
         height = 0 - height;
         y = y - height + 1;
         if (y < 0) {
            height += y;
            y = 0;
         }
      }

      // The rectangle should not extend beyond the boundaries of the image.
      if (x < imageLimits.x) {
         width -= (imageLimits.x - x);
         x = imageLimits.x;
      } else if ((x + width) > imageLimits.x + imageLimits.width) {
         width = imageLimits.x + imageLimits.width - x;
      }
      if (y < imageLimits.y) {
         height -= (imageLimits.y - y);
         y = imageLimits.y;
      }
      if ((y + height) > imageLimits.y + imageLimits.height) {
         height = imageLimits.y + imageLimits.height - y;
      }

      // Update rectToDraw after saving old value.
      if (rectToDraw != null) {
         previousRectDrawn.setBounds(rectToDraw.x, rectToDraw.y,
               rectToDraw.width, rectToDraw.height);
         rectToDraw.setBounds(x, y, width, height);
      } else {
         rectToDraw = new Rectangle(x, y, width, height);
      }
   }

   private class SelectionListener extends MouseInputAdapter {
      @Override
      public void mousePressed(MouseEvent e) {
         int x = e.getX();
         int y = e.getY();
         currentRect = new Rectangle(x, y, 0, 0);
         updateDrawableRect();
         repaint();
      }

      @Override
      public void mouseDragged(MouseEvent e) {
         updateSize(e.getX(), e.getY());
      }

      @Override
      public void mouseReleased(MouseEvent e) {
         updateSize(e.getX(), e.getY());
      }

      /*
       * Update the size of the current rectangle and call repaint. Because
       * currentRect always has the same origin, translate it if the width or
       * height is negative.
       * 
       * For efficiency (though that isn't an issue for this program), specify
       * the painting region using arguments to the repaint() call.
       */
      void updateSize(int x, int y) {
         currentRect.setSize(x - currentRect.x, y - currentRect.y);
         updateDrawableRect();

         Rectangle totalRepaint = rectToDraw.union(previousRectDrawn);
         repaint(totalRepaint.x, totalRepaint.y, totalRepaint.width,
               totalRepaint.height);
      }
   }

}
Hovercraft Full Of Eels
  • 283,665
  • 25
  • 256
  • 373
enzom83
  • 8,080
  • 10
  • 68
  • 114
  • 1
    *"Here is the main."* For better help sooner, post **one** [MCVE](http://stackoverflow.com/help/mcve) (Minimal Complete Verifiable Example) or [SSCCE](http://www.sscce.org/) (Short, Self Contained, Correct Example) as opposed to multiple code snippets. – Andrew Thompson Jul 19 '15 at 11:33
  • Not sure where your problem is, but I'd do something along [these lines](http://pastie.org/10300781) and I'd revalidate the jscrollpane (or possibly its viewport) when the image changes. Caveat: code not tested. – Hovercraft Full Of Eels Jul 19 '15 at 11:35
  • 2
    The cause is that the small image's offsets are outside the component's bounds. A simpler approach would be [centering the component](http://stackoverflow.com/questions/10486171/swing-creating-a-jscrollpane-that-displays-its-component-centered) itself inside the scroll pane. – kiheru Jul 19 '15 at 11:38
  • @AndrewThompson: I edited the OP's question posting in my version of a single file program using his code. I unfortunately do not have the time to go through all his code to find his bug. – Hovercraft Full Of Eels Jul 19 '15 at 11:38
  • @kiheru: The approach of centering the component itself inside the scrollpane is very simple: in essence, the `JPanel` is put into the `JScrollPane`, the layout of `JPanel` is set to `GridBagLayout`, a `JLabel` is added to the `JPanel` with no constraints. In this way, the `JLabel` is automatically centered, but in this case how could I deal with the drawing of the rectangle? I wish that, every time you draw a new rectangle, the custom panel should launch an event containing the location and size of the rectangle. – enzom83 Jul 19 '15 at 13:54
  • 1
    It is possible to extend `JLabel` instead of `JPanel`; `super.paintComponent(g)` then draws whatever the label would draw, and you can paint the rectangle atop that. Or in case you want to do all the drawing, simply draw the image always at (0, 0). If you wish to be able to draw outside the bounds of the image, [JLayer](http://docs.oracle.com/javase/tutorial/uiswing/misc/jlayer.html) can do that. – kiheru Jul 19 '15 at 14:03

1 Answers1

4

The problem is calculating the x and y in the setImage().It's not calculating the center of panel correctly.I'm saying this by checking the values of x and y after loading small and large images many times

However I tried putting the centering the image inside paintComponent and it worked perfectly

 if (backgroundImage != null) {
        imageLimits.x = (this.getWidth() - backgroundImage.getWidth(this)) / 2;
        imageLimits.y = (this.getHeight() - backgroundImage.getHeight(this)) / 2;
        g.drawImage(backgroundImage, imageLimits.x, imageLimits.y, this);
    }
Andrew Thompson
  • 168,117
  • 40
  • 217
  • 433
Madhan
  • 5,750
  • 4
  • 28
  • 61
  • The comments written by _kiheru_ gave me to know an alternative way to create the graphic component, while the response written by _Madhan_ has solved the problem that I described in this question, so I accepted his answer. Unfortunately, I noticed another issue: if the panel is resizable, when I resize the window, then the rectangle drawn does not stay in the same position relative to the image. For this issue, I'll write another question. – enzom83 Jul 19 '15 at 14:16