Java2D 相对于鼠标位置缩放和滚动

Java2D zoom and scroll relative to mouse position

我正在尝试使用 swing 和 java2D 创建一个简单的绘图应用程序。 目的是实现平滑缩放,始终相对于鼠标光标点。 该应用程序包含两个 classes:CanvasPane 和 Canvas.

当矩形小于visibleRect时,canvas大小等于visibleRect,我调用AffineTransform.translate跟随鼠标 (感谢 this question

当矩形变大然后 canvas 时,canvas 大小也会变大并变得可滚动。然后我在上面调用 scrollRectToVisible 来跟随鼠标。

问题是: 如何一起使用 translate 和 scrollRectToVisible,以平滑缩放而不会出现图形跳跃。可能有一些已知的决定?

我想要的在YED Graph Editor中已经完美实现了,但是它的代码是封闭的。 我试过很多例子,但只有缩放或滚动,没有复杂的用法。

完整代码如下。

Class Canvas窗格:

import javax.swing.*;
import java.awt.*;


public class CanvasPane extends JPanel {

    private Canvas canvas;

    public CanvasPane(boolean isDoubleBuffered) {
        super(isDoubleBuffered);
        setLayout(new BorderLayout());
        canvas = new Canvas(1.0);
        JScrollPane pane = new JScrollPane(canvas);
        pane.getViewport().setBackground(Color.DARK_GRAY);
        add(pane, BorderLayout.CENTER);
    }

    public static void main(String[] args) {
        JFrame frame = new JFrame("Test Graphics");
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.setLayout(new BorderLayout());
        frame.add(new CanvasPane(true), BorderLayout.CENTER);
        frame.setSize(new Dimension(1000, 800));
        frame.setVisible(true);
    }
}

Class Canvas:

import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
import java.awt.geom.AffineTransform;
import java.awt.geom.NoninvertibleTransformException;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;


public class Canvas extends JComponent implements MouseWheelListener, MouseMotionListener, MouseListener {
    private double zoom = 1.0;
    public static final double SCALE_STEP = 0.1d;
    private Dimension initialSize;
    private Point origin;
    private double previousZoom = zoom;
    AffineTransform tx = new AffineTransform();
    private double scrollX = 0d;
    private double scrollY = 0d;
    private Rectangle2D rect = new Rectangle2D.Double(0,0, 800, 600);

    public Canvas(double zoom) {
        this.zoom = zoom;
        addMouseWheelListener(this);
        addMouseMotionListener(this);
        addMouseListener(this);
        setAutoscrolls(true);
    }

    public Dimension getInitialSize() {
        return initialSize;
    }

    @Override
    public void paintComponent(Graphics g) {
        super.paintComponent(g);
        Graphics2D g2 = (Graphics2D) g;
        Graphics2D g2d = (Graphics2D) g.create();
        g2d.clearRect(0, 0, getWidth(), getHeight());
        g2d.transform(tx);
        g2d.setColor(Color.DARK_GRAY);
        g2d.fill(rect);
        g2d.setColor(Color.GRAY);
        g2d.setStroke(new BasicStroke(5.0f));
        g2d.draw(rect);
        g2d.dispose();
    }

    @Override
    public void setSize(Dimension size) {
        super.setSize(size);
        if (initialSize == null) {
            this.initialSize = size;
        }
    }

    @Override
    public void setPreferredSize(Dimension preferredSize) {
        super.setPreferredSize(preferredSize);
        if (initialSize == null) {
            this.initialSize = preferredSize;
        }
    }

    public void mouseWheelMoved(MouseWheelEvent e) {
        double zoomFactor = - SCALE_STEP*e.getPreciseWheelRotation()*zoom;
        zoom = Math.abs(zoom + zoomFactor);
        //Here we calculate new size of canvas relative to zoom.
        Rectangle realView = getVisibleRect();
        Dimension d = new Dimension(
                (int)(initialSize.width*zoom),
                (int)(initialSize.height*zoom));
//        if (d.getWidth() >= realView.getWidth() && d.getHeight() >= realView.getHeight()) {
            setPreferredSize(d);
            setSize(d);
            validate();
            followMouseOrCenter(e);
//        }

        //Here we calculate transform for the canvas graphics to scale relative to mouse
            translate(e);
            repaint();
        previousZoom = zoom;
    }

    private void translate(MouseWheelEvent e) {
        Rectangle realView = getVisibleRect();
        Point2D p1 = e.getPoint();
        Point2D p2 = null;
        try {
            p2 = tx.inverseTransform(p1, null);
        } catch (NoninvertibleTransformException ex) {
            ex.printStackTrace();
            return;
        }
        Dimension d = getSize();
        if (d.getWidth() <= realView.getWidth() && d.getHeight() <= realView.getHeight()) {
            //Zooming and translating relative to the mouse position
            tx.setToIdentity();
            tx.translate(p1.getX(), p1.getY());
            tx.scale(zoom, zoom);
            tx.translate(-p2.getX(), -p2.getY());
        } else {
            //Only zooming, translate is not needed because scrollRectToVisible works;
            tx.setToIdentity();
            tx.scale(zoom, zoom);
        }
//        What to do next?
//        The only translation works when rect is smaller then canvas size.
//        Rect bigger then canvas must be scrollable, but relative to mouse position as before.
        // But when the rect gets bigger than canvas, there is a terrible jump of a graphics.
        //So there must be some combination of translation ans scroll to achieve a smooth scale.
        //... brain explosion(((
    }


    public void followMouseOrCenter(MouseWheelEvent e) {
        Point2D point = e.getPoint();
        Rectangle visibleRect = getVisibleRect();

        scrollX = point.getX()/previousZoom*zoom - (point.getX()-visibleRect.getX());
        scrollY = point.getY()/previousZoom*zoom - (point.getY()-visibleRect.getY());

        visibleRect.setRect(scrollX, scrollY, visibleRect.getWidth(), visibleRect.getHeight());
        scrollRectToVisible(visibleRect);
    }

    public void mouseDragged(MouseEvent e) {
        if (origin != null) {
            int deltaX = origin.x - e.getX();
            int deltaY = origin.y - e.getY();
            Rectangle view = getVisibleRect();
            Dimension size = getSize();
            view.x += deltaX;
            view.y += deltaY;
            scrollRectToVisible(view);
        }
    }

    public void mouseMoved(MouseEvent e) {
    }

    public void mouseClicked(MouseEvent e) {
    }

    public void mousePressed(MouseEvent e) {
        origin = new Point(e.getPoint());
    }

    public void mouseReleased(MouseEvent e) {

    }

    public void mouseEntered(MouseEvent e) {

    }

    public void mouseExited(MouseEvent e) {

    }

}

我终于开悟了=)

我们可以简单地使 canvas 的尺寸比绘图对象的尺寸大得多,这样就不用计算任何难以理解的变换了。

我最初使 canvas 比绘图矩形大 100 倍。 然后我在绘画时缩放 Graphics2D 并将缩放后的图形平移到 canvas 的中心。接下来,我计算一个新的 visibleRect 以跟随鼠标点并滚动到它。

当canvas变得不可滚动时,跟随鼠标是不合理的,因为绘图对象太小(比初始大小小100倍),所以我只将它居中以使其始终可见。 它完全符合我的需要。

所以我们有一个工作示例,缩放跟随鼠标并通过鼠标拖动。 代码如下。

Class Canvas窗格:

import javax.swing.*;
import java.awt.*;


public class CanvasPane extends JPanel {

    private static Canvas canvas;

    public CanvasPane(boolean isDoubleBuffered) {
        super(isDoubleBuffered);
        setLayout(new BorderLayout());
        canvas = new Canvas(1.0);
        JScrollPane pane = new JScrollPane(canvas);
        pane.getViewport().setBackground(Color.DARK_GRAY);
        add(pane, BorderLayout.CENTER);
    }

    public static void main(String[] args) {
        JFrame frame = new JFrame("Test Graphics");
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.setLayout(new BorderLayout());
        frame.add(new CanvasPane(true), BorderLayout.CENTER);
        frame.setSize(new Dimension(1000, 800));
        frame.pack();
        frame.setVisible(true);

        //Initial scrolling of the canvas to its center
        Rectangle rect = canvas.getBounds();
        Rectangle visibleRect = canvas.getVisibleRect();
        double tx = (rect.getWidth() - visibleRect.getWidth())/2;
        double ty = (rect.getHeight() - visibleRect.getHeight())/2;
        visibleRect.setBounds((int)tx, (int)ty, visibleRect.width, visibleRect.height);
        canvas.scrollRectToVisible(visibleRect);
    }
}

ClassCanvas:

import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;


public class Canvas extends JComponent implements MouseWheelListener, MouseMotionListener, MouseListener {
    private double zoom = 1.0;
    public static final double SCALE_STEP = 0.1d;
    private Dimension initialSize;
    private Point origin;
    private double previousZoom = zoom;
    private double scrollX = 0d;
    private double scrollY = 0d;
    private Rectangle2D rect = new Rectangle2D.Double(0,0, 800, 600);
    private float hexSize = 3f;

    public Canvas(double zoom) {
        this.zoom = zoom;
        addMouseWheelListener(this);
        addMouseMotionListener(this);
        addMouseListener(this);
        setAutoscrolls(true);

        //Set preferred size to be 100x bigger then drawing object
        //So the canvas will be scrollable until our drawing object gets 100x smaller then its natural size.
        //When the drawing object became so small, it is unnecessary to follow mouse on it,
        //and we only center it on the canvas

        setPreferredSize(new Dimension((int)(rect.getWidth()*100), (int)(rect.getHeight()*100)));
    }

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

        //Obtain a copy of graphics object without any transforms
        Graphics2D g2d = (Graphics2D) g.create();
        g2d.clearRect(0, 0, getWidth(), getHeight());

        //Zoom graphics
        g2d.scale(zoom, zoom);

        //translate graphics to be always in center of the canvas
        Rectangle size = getBounds();
        double tx = ((size.getWidth() - rect.getWidth() * zoom) / 2) / zoom;
        double ty = ((size.getHeight() - rect.getHeight() * zoom) / 2) / zoom;
        g2d.translate(tx, ty);

        //Draw
        g2d.setColor(Color.LIGHT_GRAY);
        g2d.fill(rect);
        g2d.setColor(Color.DARK_GRAY);
        g2d.setStroke(new BasicStroke(5.0f));
        g2d.draw(rect);

        //Forget all transforms
        g2d.dispose();
    }

    @Override
    public void setSize(Dimension size) {
        super.setSize(size);
        if (initialSize == null) {
            this.initialSize = size;
        }
    }

    @Override
    public void setPreferredSize(Dimension preferredSize) {
        super.setPreferredSize(preferredSize);
        if (initialSize == null) {
            this.initialSize = preferredSize;
        }
    }

    public void mouseWheelMoved(MouseWheelEvent e) {
        double zoomFactor = - SCALE_STEP*e.getPreciseWheelRotation()*zoom;
        zoom = Math.abs(zoom + zoomFactor);
        //Here we calculate new size of canvas relative to zoom.
        Dimension d = new Dimension(
                (int)(initialSize.width*zoom),
                (int)(initialSize.height*zoom));
            setPreferredSize(d);
            setSize(d);
            validate();
        followMouseOrCenter(e.getPoint());
        previousZoom = zoom;
    }

    public void followMouseOrCenter(Point2D point) {
        Rectangle size = getBounds();
        Rectangle visibleRect = getVisibleRect();
        scrollX = size.getCenterX();
        scrollY = size.getCenterY();
        if (point != null) {
            scrollX = point.getX()/previousZoom*zoom - (point.getX()-visibleRect.getX());
            scrollY = point.getY()/previousZoom*zoom - (point.getY()-visibleRect.getY());
        }

        visibleRect.setRect(scrollX, scrollY, visibleRect.getWidth(), visibleRect.getHeight());
        scrollRectToVisible(visibleRect);
    }

    public void mouseDragged(MouseEvent e) {
        if (origin != null) {
            int deltaX = origin.x - e.getX();
            int deltaY = origin.y - e.getY();
            Rectangle view = getVisibleRect();
            view.x += deltaX;
            view.y += deltaY;
            scrollRectToVisible(view);
        }
    }

    public void mouseMoved(MouseEvent e) {
    }

    public void mouseClicked(MouseEvent e) {
    }

    public void mousePressed(MouseEvent e) {
        origin = new Point(e.getPoint());
    }

    public void mouseReleased(MouseEvent e) {
    }

    public void mouseEntered(MouseEvent e) {
    }

    public void mouseExited(MouseEvent e) {
    }

}