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

Time:02-21

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) {}
} 

CodePudding user response:

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) {
        }
    }
}
  •  Tags:  
  • java
  • Related