Adding JScrollbars to a Zoomable JPanel in Swing

115 views Asked by At

I am able to get the scrollbars to work if I decide to zoom in and out in the same way Microsoft Paint does it -- by locking the content to coordinates zero, zero and resizing to the right and down depending on if we are zooming in or out.

But I don't want to do that...

What I want is for the mouse cursor to determine what we are zooming in on, or zooming out from.

That part of the code shown below works perfectly. My problem is that when we zoom in, the red rectangle gets clipped without any scrollbars available.

I even tried doing this with ChatGPT but it complains it is too complex to do using the current JScrollBar API because it does not consider the red rectangle when it decides to show or hide the scrollbars -- it depends on the size of the component itself. In addition, the way I am zooming adds an additional level of complexity (focusing on mouse cursor to zoom). The AI suggests using a 3rd party tool to get the desired zooming functionality that natively supports such a thing.

Here is the code I have so far

import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.RenderingHints;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseWheelEvent;
import java.awt.geom.AffineTransform;
import java.awt.geom.Rectangle2D;

import javax.swing.JFrame;
import javax.swing.JPanel;

public class ZoomDemo {

    public static void main(String[] args) {
        JFrame frame = new JFrame("Zoomable Canvas");
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.setSize(1024, 768);

        ZoomPanel zoomPanel = new ZoomPanel();
        frame.add(zoomPanel);

        frame.setVisible(true);
        frame.setLocationRelativeTo(null);
    }
}

class ZoomPanel extends JPanel {

    private static final long serialVersionUID = 1L;

    private static final double ZOOM_MULTIPLIER = 1.1;
    private static final double MIN_ZOOM = 0.1;
    private static final double MAX_ZOOM = 4.0;
    private static final double INITIAL_WIDTH = 3000;
    private static final double INITIAL_HEIGHT = 3000;

    private double _zoomFactor = 1.0;
    private Point _zoomCenter;
    private AffineTransform _currentTransform;
    private Rectangle2D.Double _redRectangle;

    public ZoomPanel() {
        _currentTransform = new AffineTransform();
        _redRectangle = new Rectangle2D.Double(0, 0, INITIAL_WIDTH, INITIAL_HEIGHT);
        setPreferredSize(new Dimension((int) INITIAL_WIDTH, (int) INITIAL_HEIGHT));

        addMouseWheelListener(new MouseAdapter() {
            @Override
            public void mouseWheelMoved(MouseWheelEvent e) {
                double adjustedZoomFactor = e.getWheelRotation() < 0 ? ZOOM_MULTIPLIER : 1 / ZOOM_MULTIPLIER;
                double newZoomFactor = _zoomFactor * adjustedZoomFactor;

                if (newZoomFactor >= MIN_ZOOM && newZoomFactor <= MAX_ZOOM) {
                    _zoomFactor = newZoomFactor;
                    _zoomCenter = e.getPoint();

                    AffineTransform at = new AffineTransform();
                    at.translate(_zoomCenter.getX(), _zoomCenter.getY());
                    at.scale(adjustedZoomFactor, adjustedZoomFactor);
                    at.translate(-_zoomCenter.getX(), -_zoomCenter.getY());

                    _currentTransform.preConcatenate(at);

                    int newWidth = (int) (_redRectangle.width * _zoomFactor);
                    int newHeight = (int) (_redRectangle.height * _zoomFactor);
                    setPreferredSize(new Dimension(newWidth, newHeight));
                    revalidate();
                    
                    repaint();
                }            
            }

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

        Graphics2D g2d = (Graphics2D) g;
        g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);

        if (_zoomCenter != null) {
            g2d.transform(_currentTransform);
        }

        g2d.setColor(Color.red);
        g2d.fill(_redRectangle);
    }
}

The resizing code used to update the preferred size "sort of" works, but is imprecise:

private void updatePreferredSize() {
    int width = (int) (redRectangle.width * zoomFactor);
    int height = (int) (redRectangle.height * zoomFactor);
    setPreferredSize(new Dimension(width, height));
    revalidate();
}   

Here is the full example with scrollbars showing clipping ...

package zoom;

import javax.swing.*;
import java.awt.*;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.MouseWheelEvent;
import java.awt.geom.AffineTransform;
import java.awt.geom.Rectangle2D;

public class StackOverflow {

    public static void main(String[] args) {
        JFrame frame = new JFrame("Zoomable Canvas");
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.setSize(800, 600);

        ZoomPanel2 zoomPanel = new ZoomPanel2();
        JScrollPane scrollPane = new JScrollPane(zoomPanel);
        frame.add(scrollPane);
        frame.setVisible(true);
        frame.setLocationRelativeTo(null);
    }
}

class ZoomPanel2 extends JPanel {

    private static final long serialVersionUID = 1L;
    private double zoomFactor = 1.0;
    private static final double ZOOM_MULTIPLIER = 1.1;
    private static final double MIN_ZOOM = 0.1; // Minimum zoom level
    private static final double MAX_ZOOM = 4.0; // Maximum zoom level

    private Point zoomCenter;
    private AffineTransform currentTransform;
    private Point lastMousePosition;
    private Rectangle2D.Double square;
    private Point dragOffset;
    private Rectangle2D.Double redRectangle;

    public ZoomPanel2() {
        currentTransform = new AffineTransform();
        square = new Rectangle2D.Double(100, 100, 200, 200);
        redRectangle = new Rectangle2D.Double(0, 0, 1000, 1000);

        addMouseWheelListener(new MouseAdapter() {
            @Override
            public void mouseWheelMoved(MouseWheelEvent e) {
                double adjustedZoomFactor = e.getWheelRotation() < 0 ? ZOOM_MULTIPLIER : 1 / ZOOM_MULTIPLIER;
                double newZoomFactor = zoomFactor * adjustedZoomFactor;

                // Check if the new zoom factor is within the valid range
                if (newZoomFactor >= MIN_ZOOM && newZoomFactor <= MAX_ZOOM) {
                    zoomFactor = newZoomFactor;
                    zoomCenter = e.getPoint();

                    AffineTransform at = new AffineTransform();
                    at.translate(zoomCenter.getX(), zoomCenter.getY());
                    at.scale(adjustedZoomFactor, adjustedZoomFactor);
                    at.translate(-zoomCenter.getX(), -zoomCenter.getY());

                    currentTransform.preConcatenate(at);
                    updatePreferredSize(); // Add this line
                    repaint();
                }
            
            }
        });

        addMouseListener(new MouseAdapter() {
            @Override
            public void mousePressed(MouseEvent e) {
                Point transformedPoint = transformPoint(e.getPoint(), currentTransform);
                if (square.contains(transformedPoint)) {
                    lastMousePosition = e.getPoint();
                    dragOffset = new Point(transformedPoint.x - (int) square.getX(), transformedPoint.y - (int) square.getY());
                } else {
                    lastMousePosition = null;
                }
            }
        });

        addMouseMotionListener(new MouseAdapter() {
            @Override
            public void mouseDragged(MouseEvent e) {
                if (lastMousePosition != null) {
                    Point transformedPoint = transformPoint(e.getPoint(), currentTransform);
                    int newX = transformedPoint.x - dragOffset.x;
                    int newY = transformedPoint.y - dragOffset.y;

                    // Check if the new position is within the red rectangle
                    if (redRectangle.contains(newX, newY, square.getWidth(), square.getHeight())) {
                        square.setRect(newX, newY, square.getWidth(), square.getHeight());
                        lastMousePosition = e.getPoint();

                        repaint();
                    }
                }
            }
          });
      }
    
    
    private Point transformPoint(Point point, AffineTransform transform) {
        try {
            AffineTransform inverse = transform.createInverse();
            Point transformedPoint = new Point();
            inverse.transform(point, transformedPoint);
            return transformedPoint;
        } catch (Exception ex) {
            ex.printStackTrace();
        }
        return null;
    }

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

        Graphics2D g2d = (Graphics2D) g;
        g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);

        // Apply zoom factor
        if (zoomCenter != null) {
            g2d.transform(currentTransform);
        }

        // Draw red rectangle
        g2d.setColor(Color.red);
        g2d.fill(redRectangle);

        // Draw blue square
        g2d.setColor(Color.BLUE);
        g2d.fill(square);
    }

    private void updatePreferredSize() {
        int width = (int) (redRectangle.width * zoomFactor);
        int height = (int) (redRectangle.height * zoomFactor);
        setPreferredSize(new Dimension(width, height));
        revalidate();
    }    
} 
3

There are 3 answers

2
camickr On

As I stated you should be overriding getPreferredSize(). For example:

@Override
public Dimension getPreferredSize()
{
    int newWidth = (int) (_redRectangle.width * _zoomFactor);
    int newHeight = (int) (_redRectangle.height * _zoomFactor);

    return new Dimension(newWidth, newHeight);
}

Then get rid off all the setPreferredSize() statements. The getPreferredSize() method will be called when you invoke revalidate().

Next, if you want scrollbars to appear then you actually need to add the panel to a scroll pane and the scroll pane to the frame:

//frame.add(zoomPanel);
frame.add(new JScrollPane(zoomPanel));

Also, in my case I found that when checking the wheel rotation I was getting 3 values of 0, for every 1 or -1. So I changed my code to:

double adjustedZoomFactor = 1.0;

if (e.getWheelRotation() == 1)
    adjustedZoomFactor = ZOOM_MULTIPLIER;

if (e.getWheelRotation() == -1)
    adjustedZoomFactor = 1 / ZOOM_MULTIPLIER;

I also removed:

//at.translate(_zoomCenter.getX(), _zoomCenter.getY());
at.scale(adjustedZoomFactor, adjustedZoomFactor);
//at.translate(-_zoomCenter.getX(), -_zoomCenter.getY());

I'm not sure what the translate values should be, but with those statements removed I at least got the scroll bars working correctly.

1
Gilbert Le Blanc On

Introduction

I could not come up with a calculation that takes into account the cursor position. The best I could do was keep the image centered in the GUI. Maybe someone else can finish the calculation in the calculateScrollPaneOrigin method.

Here's the GUI and image at its maximum size.

Maximum

Here's the GUI and image at its minimum size.

Minimum

Explanation

Oracle has a helpful tutorial, Creating a GUI With Swing. Skip the Learning Swing with the NetBeans IDE section.

The ZoomPanel class was too confusing for me to understand. A drawing JPanel class should do one thing and one thing only. Draw. Period. Nothing else.

When I create a Swing GUI, I use the model-view-controller (MVC) pattern. This pattern allows me to separate my concerns and focus on one part of the application at a time.

Model

The first thing I did was create an application model using a plain Java getter/setter class. That way I could separate the model from the view and focus on one part of the application at a time.

I created the image at 100% to minimize any blurring that might occur when enlarging an image.

I used int values to calculate the various zoom levels. There's no need for floating point arithmetic, which is imprecise and will drift after an extended period of zooming.

I used other int values to hold the size of the image, the size of the viewing area, and the size of the checkerboard cells. These values are used in the view.

View

All Swing applications must start with a call to the SwingUtilities invokeLater method. This method ensures that the Swing components are created and executed on the Event Dispatch Thread.

I created a JFrame and a drawing JPanel. I enclosed the drawing JPanel in a JScrollPane so it would fit into a smaller viewing window. The drawing JPanel remains the size of the maximum image. The reduced image is centered in the drawing JPanel.

The image is reduced based on the scaling factor, expressed as a int percentage.

Controller

I separated the MouseAdapter into its own class. The mouseMoved method gets the coordinates of the mouse within the viewing area. The mouseWheelMoved method handles the zoom.

As I said earlier, the calculateScrollPaneOrigin method is the method that would calculate the position of the scroll bars. I could not come up with the math to perform this calculation reliability and accurately. Maybe someone else can.

Code

Here's the complete runnable code. I made all the additional classes inner classes so I could post the code as one block.

import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Image;
import java.awt.Point;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.MouseWheelEvent;
import java.awt.image.BufferedImage;

import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.JScrollBar;
import javax.swing.JScrollPane;
import javax.swing.SwingUtilities;

public class ZoomDemo implements Runnable {

    public static void main(String[] args) {
        SwingUtilities.invokeLater(new ZoomDemo());
    }

    private final ZoomModel zoomModel;

    private final ImagePanel imagePanel;

    private final JScrollPane scrollPane;

    public ZoomDemo() {
        this.zoomModel = new ZoomModel();
        this.imagePanel = new ImagePanel(this, zoomModel);
        this.scrollPane = new JScrollPane(imagePanel);
        setScrollPaneValues();
    }

    private void setScrollPaneValues() {
        JScrollBar scrollBar = scrollPane.getHorizontalScrollBar();
        scrollBar.setMinimum(0);
        scrollBar.setMaximum(zoomModel.getInitialWidth());
        scrollBar.setUnitIncrement(zoomModel.getCellWidth());

        scrollBar = scrollPane.getVerticalScrollBar();
        scrollBar.setMinimum(0);
        scrollBar.setMaximum(zoomModel.getInitialHeight());
        scrollBar.setUnitIncrement(zoomModel.getCellHeight());

        int x = (zoomModel.getInitialWidth() - zoomModel.getDisplayWidth()) / 2;
        int y = (zoomModel.getInitialHeight() - zoomModel.getDisplayHeight())
                / 2;
        setScrollPaneOrigin(x, y);

        scrollPane.setPreferredSize(new Dimension(zoomModel.getDisplayWidth(),
                zoomModel.getDisplayHeight()));
    }

    @Override
    public void run() {
        JFrame frame = new JFrame("Zoomable Canvas");
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

        frame.add(scrollPane);

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

    public void setScrollPaneOrigin(int x, int y) {
        JScrollBar scrollBar = scrollPane.getHorizontalScrollBar();
        scrollBar.setValue(x);
        scrollBar = scrollPane.getVerticalScrollBar();
        scrollBar.setValue(y);
        scrollPane.validate();
    }

    public Point getScrollPaneOrigin() {
        JScrollBar scrollBar = scrollPane.getHorizontalScrollBar();
        int x = scrollBar.getValue();
        scrollBar = scrollPane.getVerticalScrollBar();
        int y = scrollBar.getValue();

        return new Point(x, y);
    }

    public void repaint() {
        imagePanel.repaint();
    }

    public class ImagePanel extends JPanel {

        private static final long serialVersionUID = 1L;

        private ZoomModel zoomModel;

        public ImagePanel(ZoomDemo view, ZoomModel zoomModel) {
            this.zoomModel = zoomModel;
            this.setPreferredSize(new Dimension(zoomModel.getInitialWidth(),
                    zoomModel.getInitialHeight()));
            ZoomListener listener = new ZoomListener(view, zoomModel);
//          this.addMouseListener(listener);
            this.addMouseMotionListener(listener);
            this.addMouseWheelListener(listener);
        }

        @Override
        protected void paintComponent(Graphics g) {
            super.paintComponent(g);
            int currentZoom = zoomModel.getCurrentZoom();
            Dimension m = zoomModel.getImageMargin(currentZoom);
            g.drawImage(zoomModel.getScaledImage(), m.width, m.height, this);
        }

    }

    public class ZoomListener extends MouseAdapter {

        private final ZoomDemo view;

        private final ZoomModel zoomModel;

        private Point mouseLocation;

        public ZoomListener(ZoomDemo view, ZoomModel zoomModel) {
            this.view = view;
            this.zoomModel = zoomModel;
        }

        @Override
        public void mouseMoved(MouseEvent event) {
            this.mouseLocation = event.getPoint();
        }

        @Override
        public void mouseWheelMoved(MouseWheelEvent event) {
            int adjustedZoomFactor = event.getWheelRotation()
                    * zoomModel.getIncrementZoom();
            int currentZoom = zoomModel.getCurrentZoom();
            int newZoom = currentZoom + adjustedZoomFactor;
            newZoom = Math.max(newZoom, zoomModel.getMinimumZoom());
            newZoom = Math.min(newZoom, zoomModel.getMaximumZoom());
            zoomModel.setCurrentZoom(newZoom);

            Point p = calculateScrollPaneOrigin(currentZoom, newZoom);
            view.setScrollPaneOrigin(p.x, p.y);
            view.repaint();
        }

        private Point calculateScrollPaneOrigin(int oldZoom, int newZoom) {
            int displayWidth = zoomModel.getDisplayWidth();
            int displayHeight = zoomModel.getDisplayHeight();
            int initialWidth = zoomModel.getInitialWidth();
            int initialHeight = zoomModel.getInitialHeight();
            int x = (initialWidth - displayWidth) / 2;
            int y = (initialHeight - displayHeight) / 2;

            return new Point(x, y);
        }

    }

    public class ZoomModel {

        private final int incrementZoom, minimumZoom, maximumZoom;
        private int currentZoom;

        private final int cellWidth, cellHeight;
        private final int displayWidth, displayHeight;
        private final int initialWidth, initialHeight;

        private final BufferedImage image;

        public ZoomModel() {
            this.incrementZoom = 10;
            this.minimumZoom = 10;
            this.maximumZoom = 100;
            this.currentZoom = 100;
            this.cellWidth = 100;
            this.cellHeight = 100;
            this.displayWidth = 800;
            this.displayHeight = 640;
            this.initialWidth = 3000;
            this.initialHeight = 3000;
            this.image = createCheckerboardImage();
        }

        private BufferedImage createCheckerboardImage() {
            BufferedImage image = new BufferedImage(initialWidth, initialHeight,
                    BufferedImage.TYPE_INT_RGB);

            Graphics2D g2d = (Graphics2D) image.getGraphics();
            boolean isRed = true;
            for (int x = 0; x < initialWidth; x += cellWidth) {
                for (int y = 0; y < initialHeight; y += cellHeight) {
                    if (isRed) {
                        g2d.setColor(Color.RED);
                    } else {
                        g2d.setColor(Color.BLACK);
                    }
                    g2d.fillRect(x, y, cellWidth, cellHeight);
                    isRed = !isRed;
                }
                isRed = !isRed;
            }
            g2d.dispose();

            return image;
        }

        public Image getScaledImage() {
            Dimension d = getImageSize(currentZoom);
            return image.getScaledInstance(d.width, d.height,
                    Image.SCALE_SMOOTH);
        }

        public Dimension getImageMargin(int currentZoom) {
            Dimension d = getImageSize(currentZoom);
            int marginW = (initialWidth - d.width) / 2;
            int marginH = (initialHeight - d.height) / 2;
            return new Dimension(marginW, marginH);
        }

        public Dimension getImageSize(int currentZoom) {
            int width = (initialWidth * currentZoom) / 100;
            int height = (initialHeight * currentZoom) / 100;
            return new Dimension(width, height);
        }

        public int getCurrentZoom() {
            return currentZoom;
        }

        public void setCurrentZoom(int currentZoom) {
            this.currentZoom = currentZoom;
        }

        public int getIncrementZoom() {
            return incrementZoom;
        }

        public int getMinimumZoom() {
            return minimumZoom;
        }

        public int getMaximumZoom() {
            return maximumZoom;
        }

        public int getCellWidth() {
            return cellWidth;
        }

        public int getCellHeight() {
            return cellHeight;
        }

        public int getDisplayWidth() {
            return displayWidth;
        }

        public int getDisplayHeight() {
            return displayHeight;
        }

        public int getInitialWidth() {
            return initialWidth;
        }

        public int getInitialHeight() {
            return initialHeight;
        }

        public BufferedImage getImage() {
            return image;
        }

    }
}
0
Newbie Guest On

After spending several hours on this to no avail, I have decided to scrap the use of scrollbars altogether and instead use a ZOOM and PAN solution for my workspace. The various widgets in my workspace can still be dragged around and zoomed however the issue is will be finding the rectangles if I pan too far ...

Anyways: Here's the PAN and ZOOM code that works ...

import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.RenderingHints;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.MouseMotionListener;
import java.awt.event.MouseWheelEvent;
import java.awt.geom.AffineTransform;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.util.ArrayList;
import java.util.List;

import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.SwingUtilities;

public class RectanglePainter extends JFrame {

    private static final long serialVersionUID = 1L;
    private static final int WINDOW_WIDTH = 1024;
    private static final int WINDOW_HEIGHT = 768;
    Model _model;

    public RectanglePainter(Model m) {
        _model = m;
        setTitle("Rectangle Painter");
        setSize(WINDOW_WIDTH, WINDOW_HEIGHT);
        Canvass canvass = new Canvass(m);
        getContentPane().add(canvass, BorderLayout.CENTER); 

        setDefaultCloseOperation(EXIT_ON_CLOSE);
        setVisible(true);
    }

    public static void main(String[] args) {
        SwingUtilities.invokeLater(new Runnable() {
            @Override
            public void run() {
                Model m = new Model();
                new RectanglePainter(m);
            }
        });
    }
}

class Canvass extends JPanel {
    private static final long serialVersionUID = 1L;
    private static final double ZOOM_MULTIPLIER = 1.1;
    private static final double MIN_ZOOM = 0.1;
    private static final double MAX_ZOOM = 4.0;

    Model _model;
    Rectangle2D selected;
    AffineTransform _currentTransform;
    private double _zoomFactor = 1.0;
    private Point _zoomCenter;
    private Point lastDragPoint;

    public Canvass(Model m) {
        super(new BorderLayout());
        _model = m;
        _currentTransform = new AffineTransform();      

        addMouseWheelListener(new MouseAdapter() {
            
            @Override
            public void mouseWheelMoved(MouseWheelEvent e) {
                double adjustedZoomFactor = e.getWheelRotation() < 0 ? ZOOM_MULTIPLIER : 1 / ZOOM_MULTIPLIER;
                double newZoomFactor = _zoomFactor * adjustedZoomFactor;

                if (newZoomFactor >= MIN_ZOOM && newZoomFactor <= MAX_ZOOM) {
                    Point cursorPoint = e.getPoint();
                    _zoomFactor = newZoomFactor;
                    _zoomCenter = cursorPoint;

                    AffineTransform at = new AffineTransform();
                    at.translate(_zoomCenter.getX(), _zoomCenter.getY());
                    at.scale(adjustedZoomFactor, adjustedZoomFactor);
                    at.translate(-_zoomCenter.getX(), -_zoomCenter.getY());
                    _currentTransform.preConcatenate(at);                   
                    repaint();
                }
            }
        });

        addMouseListener(new MouseAdapter() {
            @Override
            public void mouseReleased(MouseEvent e) {
                lastDragPoint = null; // Reset last drag point on mouse release
            }
        });
        
        addMouseMotionListener(new MouseMotionListener() {
             @Override
             public void mouseDragged(MouseEvent e) {
                 try {
                    if (selected == null) {
                        if (lastDragPoint != null) {
                            double dx = (e.getX() - lastDragPoint.x) / _zoomFactor;
                            double dy = (e.getY() - lastDragPoint.y) / _zoomFactor;
                            _currentTransform.translate(dx, dy);
                            repaint();
                        }
                    }
                    else {
                        if (lastDragPoint != null) {
                            double dx = (e.getX() - lastDragPoint.x) / _zoomFactor;
                            double dy = (e.getY() - lastDragPoint.y) / _zoomFactor;
                            
                             
                            selected.setRect(selected.getX() + dx, selected.getY() + dy, selected.getWidth(), selected.getHeight());
                            
                            repaint();
                        }
                        
                        
                    }
                    lastDragPoint = e.getPoint();
                 } 
                 catch (Throwable e1) {
                        e1.printStackTrace();
                    }
                }
            @Override
            public void mouseMoved(MouseEvent e) {                  
                try {
                    Point2D modelPoint = _currentTransform.inverseTransform(e.getPoint(), null);
                    selected = m.selected( modelPoint );
                    repaint();
                } 
                catch (Throwable  e1) {
                    throw new RuntimeException(e1);
                }
            }
        });             
    }

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

        Graphics2D g2d = (Graphics2D) g;
        g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
        g2d.transform(_currentTransform);


        for(Rectangle2D.Double rec : _model.recs) {
            g2d.draw(rec);  
        }

        if(selected != null) {
            g.setColor(Color.YELLOW);
            g2d.fill(selected);
            g.setColor(Color.BLACK);
            g2d.draw(selected);
        }
    }
}

class Model {
    List<Rectangle2D.Double> recs;

    public Model() {
        recs = new ArrayList<Rectangle2D.Double>();

        for(int x = 0; x < 5; x++) {
            recs.add(new Rectangle2D.Double(x * 100, x * 10, 50, 50));
        }
    }

    public Rectangle2D.Double selected(Point2D p) {
        for(Rectangle2D.Double rec : recs) {
            if(rec.contains(p)) {
                return rec;
            }
        }   
        return null;
    }   
}