Newbie Guest
Newbie Guest

Reputation: 13

Java mouse events do not translate coordinates properly when zooming with mouse wheel

I need some assistance figuring out how to translate coordinates from mouse events during zoom ... it works when zoom factor is 1.0 but not sure of algorithm when it changes...

I can drag the rectangle around the screen when if I comment out the zoom code but once zoom, the mouse coordinates screw up once the zoom is applied

I just cannot figure out the coordinates translation code

package ca.stackoverflow.main;

import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.MouseInfo;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.MouseMotionListener;
import java.awt.event.MouseWheelEvent;
import java.awt.event.MouseWheelListener;
import java.awt.geom.AffineTransform;

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

public class Demo extends JFrame {

    private static final long serialVersionUID = 1L;

    public Demo() {
        setPreferredSize(new Dimension(640, 480));
        setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);

        JScrollPane scroll = new JScrollPane(new Panel());
        add(scroll, BorderLayout.CENTER);

        pack();
        setLocationRelativeTo(null);
    }

    public static void main(String[] args) {
        SwingUtilities.invokeLater(new Runnable() {
            public void run() {
                try {
                    Demo demo = new Demo();
                    demo.setVisible(true);
                }
                catch(Throwable e) {
                    throw new RuntimeException(e);
                }
            }
        });
    }
}

class Panel extends JPanel implements MouseWheelListener, MouseListener, MouseMotionListener {

    private static final long serialVersionUID = 1L;
    private static final double MIN_ZOOM_FACTOR = 0.1;
    private static final double MAX_ZOOM_FACTOR = 5.0;

    private Color _fillColor;
    private Point _startPoint;
    private double xOffset;
    private double yOffset;

    private int xdragOffset;
    private int ydragOffset;    
    
    private double zoomFactor = 1;
    private double prevZoomFactor = 1;
    private boolean zoomer;
        
    private Rectangle _rectangle;

    public Panel() {
        setPreferredSize(new Dimension(1000, 1000));
        addMouseWheelListener(this);
        addMouseMotionListener(this);
        addMouseListener(this);
        _fillColor = Color.WHITE;
        _rectangle = new Rectangle(50, 50, 100, 200);
    }

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

        Graphics2D g2 = (Graphics2D) g;
        AffineTransform at = new AffineTransform();

        if (zoomer) {

            double xRel = MouseInfo.getPointerInfo().getLocation().getX() - getLocationOnScreen().getX();
            double yRel = MouseInfo.getPointerInfo().getLocation().getY() - getLocationOnScreen().getY();
            double zoomDiv = zoomFactor / prevZoomFactor;
            xOffset = (zoomDiv) * (xOffset) + (1 - zoomDiv) * xRel;
            yOffset = (zoomDiv) * (yOffset) + (1 - zoomDiv) * yRel;
            prevZoomFactor = zoomFactor;
            zoomer = false;
        }
        
        at.translate(xOffset, yOffset);
        at.scale(zoomFactor, zoomFactor);
        g2.transform(at);
        
        
        g.setColor(_fillColor);
        g.fillRect(_rectangle.x, _rectangle.y, _rectangle.width, _rectangle.height);
        g.setColor(Color.BLACK);
        g.drawRect(_rectangle.x, _rectangle.y, _rectangle.width, _rectangle.height);      
    }

    @Override public void mouseMoved(MouseEvent e) {
        _fillColor = Color.WHITE;
        if(_rectangle.contains(e.getPoint())) {
            _fillColor = Color.GREEN;
        }
        repaint();
    }

    @Override public void mousePressed(MouseEvent e) {
        _startPoint = null;
        _fillColor = Color.WHITE;

        if(_rectangle.contains(e.getPoint())) {
            _startPoint = e.getPoint();
            _fillColor = Color.GREEN;
            xdragOffset = _startPoint.x - _rectangle.x;
            ydragOffset = _startPoint.y - _rectangle.y;
        }
        repaint();
    }
    
    @Override public void mouseDragged(MouseEvent e) {
        if(_startPoint != null) {
            int diffX = e.getX() - _startPoint.x;
            int diffY = e.getY() - _startPoint.y;
            
            _rectangle.x = _startPoint.x + diffX - xdragOffset;  
            _rectangle.y = _startPoint.y + diffY - ydragOffset;  
        }
        else {
            _fillColor = Color.WHITE;
            if(_rectangle.contains(e.getPoint())) {
                _fillColor = Color.GREEN;
            }
        }
        repaint();
    }
    
    @Override public void mouseWheelMoved(MouseWheelEvent e) {
        zoomer = true;
        if (e.getWheelRotation() < 0) {
            zoomFactor = Math.max(zoomFactor / 1.1, MIN_ZOOM_FACTOR);
        }
        if (e.getWheelRotation() > 0) {
            zoomFactor = Math.min(zoomFactor * 1.1, MAX_ZOOM_FACTOR);
        }
        repaint();
    }

    @Override public void mouseClicked(MouseEvent e) {}
    @Override public void mouseReleased(MouseEvent e) {} 
    @Override public void mouseEntered(MouseEvent e) {}
    @Override public void mouseExited(MouseEvent e) {}
} 

Upvotes: 0

Views: 414

Answers (2)

Newbie Guest
Newbie Guest

Reputation: 13

After some digging on the internet, I found another way to do this...

All the complicated logic remains inside the mouse wheel event...

@Override public void mouseWheelMoved(MouseWheelEvent e) {
    try {
        if (e.getScrollType() == MouseWheelEvent.WHEEL_UNIT_SCROLL) {

            double scaleValue = scale - (0.1 * e.getWheelRotation());
            scaleValue = scaleValue < 0.1 ? 0.1 : scaleValue;
            scaleValue = scaleValue > 3 ? 3 : scaleValue;
            scale = scaleValue;
 
            Point2D p1 = e.getPoint();
            Point2D p2 = null;
            p2 = tx.inverseTransform(p1, null);
            tx.setToIdentity();
            tx.translate(p1.getX(), p1.getY());
            tx.scale(scale, scale);
            tx.translate(-p2.getX(), -p2.getY());

            repaint();
        }
    } 
    catch (Throwable ex) {
        throw new RuntimeException(ex);
    }
}

In the paintComponent, the call is quite simple:

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

    Graphics2D g2 = (Graphics2D) g.create();
    g2.setTransform(tx);

    g2.setColor(Color.BLACK);       
    g2.draw(getBounds());

    g2.setColor(_color);
    g2.fill(rect1);
    g2.setColor(Color.BLACK);       
    g2.draw(rect1);

    g2.setColor(Color.BLUE);
    g2.draw(rect2);
    g2.dispose();
}

All mouse events are handled the same way. I created a routine to convert the original mouse event's coordinates ...

private final Point transformMouseEventPoint(MouseEvent event) {
    try {
        Point2D p = tx.inverseTransform(
                event.getPoint(), null);
        return new Point((int)p.getX(), (int)p.getY());
    }
    catch(Throwable e) {
        throw new RuntimeException(e);
    }
}

This is how it is used

@Override public void mousePressed(MouseEvent e) {
    try {
        _color = Color.WHITE;
        _startPoint = null;

        Point p = transformMouseEventPoint(e);

        if(rect1.contains(p)) {
            _color = Color.RED;
            _startPoint = p;
            xOffset = _startPoint.x - (int)rect1.getX();
            yOffset = _startPoint.y - (int)rect1.getY();
        }
        repaint();
    } 
    catch (Throwable e1) {
        throw new RuntimeException(e1);
    }
}

Next challenge is to add scrollbars to the scaled workspace

Upvotes: 0

MadProgrammer
MadProgrammer

Reputation: 347194

Okay, so, that was a little more involved than I first thought.

The "basic" concept is, you need to apply the same AffineTransformation you used to paint the component to you _rectangle

So, I started by creating an instance property to keep track of the current transformation, as this is going to get re-used a bit

private AffineTransform transformation = new AffineTransform();

Then I added a getter

protected AffineTransform getTransformation() {
    return transformation;
}

Then I "optimised" your paintComponent, seriously...

double xRel = MouseInfo.getPointerInfo().getLocation().getX() - getLocationOnScreen().getX();
double yRel = MouseInfo.getPointerInfo().getLocation().getY() - getLocationOnScreen().getY();

is a really bad idea, generally, but especially from within a paint pass

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

    Graphics2D g2 = (Graphics2D) g.create();

    g2.setTransform(getTransformation());

    g2.setColor(_fillColor);
    g2.fillRect(_rectangle.x, _rectangle.y, _rectangle.width, _rectangle.height);
    g2.setColor(Color.BLACK);
    g2.drawRect(_rectangle.x, _rectangle.y, _rectangle.width, _rectangle.height);

    g2.dispose();
}

I then modified the mouseWheelMoved to calculate the new AffineTransformation each time it changed

@Override
public void mouseWheelMoved(MouseWheelEvent e) {

    Point2D zoomAnchor = e.getPoint();

    prevZoomFactor = zoomFactor;
    if (e.getWheelRotation() < 0) {
        zoomFactor = Math.max(zoomFactor / 1.1, MIN_ZOOM_FACTOR);
    }
    if (e.getWheelRotation() > 0) {
        zoomFactor = Math.min(zoomFactor * 1.1, MAX_ZOOM_FACTOR);
    }

    transformation = new AffineTransform();
    double zoomDiv = zoomFactor / prevZoomFactor;
    xOffset = (zoomDiv) * (xOffset) + (1 - zoomDiv) * zoomAnchor.getX();
    yOffset = (zoomDiv) * (yOffset) + (1 - zoomDiv) * zoomAnchor.getX();

    transformation.translate(xOffset, yOffset);
    transformation.scale(zoomFactor, zoomFactor);

    repaint();
}

Now, each time mouseMoved is called, you need to apply the AffineTransformation to the _rectangle and check it for any collisions...

@Override
public void mouseMoved(MouseEvent e) {
    AffineTransform at = getTransformation();
    PathIterator pathIterator = _rectangle.getPathIterator(at);
    GeneralPath shape = new GeneralPath();
    shape.append(pathIterator, true);
    
    _fillColor = Color.WHITE;
    if (shape.contains(e.getPoint())) {
        _fillColor = Color.GREEN;
    }
    repaint();
}

Now, I would be tempted to create a "shadow" Shape each time mouseWheelMoved is called, as you're going to want it for the other mouse events

Runnable example...

I've not updated your mousePressed or mouseDragged methods, I'll leave you to do that. I've also left in some debug code which will draw the "transformed shape" on each paint pass. This is draw BEFORE the Graphics context is transformed, so it should give you a bit of a guide

import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.Shape;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.MouseMotionListener;
import java.awt.event.MouseWheelEvent;
import java.awt.event.MouseWheelListener;
import java.awt.geom.AffineTransform;
import java.awt.geom.GeneralPath;
import java.awt.geom.PathIterator;
import java.awt.geom.Point2D;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.SwingUtilities;

public class Main extends JFrame {

    private static final long serialVersionUID = 1L;

    public Main() {
        setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);

        JScrollPane scroll = new JScrollPane(new TestPane());
        add(scroll, BorderLayout.CENTER);

        pack();
        setLocationRelativeTo(null);
    }

    public static void main(String[] args) {
        SwingUtilities.invokeLater(new Runnable() {
            public void run() {
                try {
                    Main demo = new Main();
                    demo.setVisible(true);
                } catch (Throwable e) {
                    throw new RuntimeException(e);
                }
            }
        });
    }

    class TestPane extends JPanel implements MouseWheelListener, MouseListener, MouseMotionListener {

        private static final long serialVersionUID = 1L;
        private static final double MIN_ZOOM_FACTOR = 0.1;
        private static final double MAX_ZOOM_FACTOR = 5.0;

        private Color _fillColor;
        private Point _startPoint;
        private double xOffset;
        private double yOffset;

        private int xdragOffset;
        private int ydragOffset;

        private double zoomFactor = 1;
        private double prevZoomFactor = 1;
        private boolean zoomer;
        private Point zoomAnchor;

        private Rectangle _rectangle;

        private AffineTransform transformation = new AffineTransform();

        public TestPane() {
            addMouseWheelListener(this);
            addMouseMotionListener(this);
            addMouseListener(this);
            _fillColor = Color.WHITE;
            _rectangle = new Rectangle(50, 50, 100, 200);
        }

        @Override
        public Dimension getPreferredSize() {
            return new Dimension(640, 480);
        }

        // This would be better pre-calculated when the 
        // zoom actually changes
        protected AffineTransform getTransformation() {
            return transformation;
        }

        private Shape testPath;

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

            Graphics2D g2 = (Graphics2D) g.create();

            if (testPath != null) {
                g2.setColor(Color.RED);
                g2.draw(testPath);
            }

            g2.setTransform(getTransformation());

            g2.setColor(_fillColor);
            g2.fillRect(_rectangle.x, _rectangle.y, _rectangle.width, _rectangle.height);
            g2.setColor(Color.BLACK);
            g2.drawRect(_rectangle.x, _rectangle.y, _rectangle.width, _rectangle.height);

            g2.dispose();
        }

        // This is purly for test purposes and you could simply
        // create based on needs at the time
        // Alternativly, you could apply a simular logic to it as the
        // AffineTransformation and each time the zoom is changed,
        // you could create a new, reusable, instance
        protected Shape createTransformedShape() {
            AffineTransform at = getTransformation();
            PathIterator pathIterator = _rectangle.getPathIterator(at);
            GeneralPath path = new GeneralPath();
            path.append(pathIterator, true);

            testPath = path;
            return path;
        }

        @Override
        public void mouseMoved(MouseEvent e) {
            Shape shape = createTransformedShape();

            _fillColor = Color.WHITE;
            if (shape.contains(e.getPoint())) {
                _fillColor = Color.GREEN;
            }
            repaint();
        }

        @Override
        public void mousePressed(MouseEvent e) {
//            _startPoint = null;
//            _fillColor = Color.WHITE;
//
//            AffineTransform at = getTransformation();
//            Point2D zoomPoint = zoomedPointFrom(e.getPoint());
//            PathIterator pathIterator = _rectangle.getPathIterator(at);
//            GeneralPath path = new GeneralPath();
//            path.append(pathIterator, true);
//
//            if (path.contains(zoomPoint)) {
//                _startPoint = e.getPoint();
//                _fillColor = Color.GREEN;
//                xdragOffset = _startPoint.x - _rectangle.x;
//                ydragOffset = _startPoint.y - _rectangle.y;
//            }
//            repaint();
        }

        @Override
        public void mouseDragged(MouseEvent e) {
//            if (_startPoint != null) {
//                int diffX = e.getX() - _startPoint.x;
//                int diffY = e.getY() - _startPoint.y;
//
//                _rectangle.x = _startPoint.x + diffX - xdragOffset;
//                _rectangle.y = _startPoint.y + diffY - ydragOffset;
//            } else {
//                _fillColor = Color.WHITE;
//                if (_rectangle.contains(e.getPoint())) {
//                    _fillColor = Color.GREEN;
//                }
//            }
//            repaint();
        }

        @Override
        public void mouseWheelMoved(MouseWheelEvent e) {

            Point2D zoomAnchor = e.getPoint();

            prevZoomFactor = zoomFactor;
            if (e.getWheelRotation() < 0) {
                zoomFactor = Math.max(zoomFactor / 1.1, MIN_ZOOM_FACTOR);
            }
            if (e.getWheelRotation() > 0) {
                zoomFactor = Math.min(zoomFactor * 1.1, MAX_ZOOM_FACTOR);
            }

            transformation = new AffineTransform();
            double zoomDiv = zoomFactor / prevZoomFactor;
            xOffset = (zoomDiv) * (xOffset) + (1 - zoomDiv) * zoomAnchor.getX();
            yOffset = (zoomDiv) * (yOffset) + (1 - zoomDiv) * zoomAnchor.getX();

            transformation.translate(xOffset, yOffset);
            transformation.scale(zoomFactor, zoomFactor);

            createTransformedShape();
            repaint();
        }

        @Override
        public void mouseClicked(MouseEvent e) {
        }

        @Override
        public void mouseReleased(MouseEvent e) {
        }

        @Override
        public void mouseEntered(MouseEvent e) {
        }

        @Override
        public void mouseExited(MouseEvent e) {
        }
    }
}

Upvotes: 1

Related Questions