You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

JScrollPane缩放后基于鼠标定位视图的计算错误修正问询

问题分析与修正方案

错误原因

  1. 鼠标坐标转换错误:使用MouseInfo获取的屏幕坐标计算相对组件的位置时,未考虑当前的缩放状态,得到的xRel/yRel是组件的像素坐标,而非绘制时的原始世界坐标,后续偏移计算逻辑基于缩放后的坐标体系,两者不匹配。
  2. 缩放因子逻辑混乱:prevZoomFactor存储的是单次缩放增量(如1.05),而TestPane.zoomFactor是累计总缩放因子,两者的比值zoomDiv无法正确反映缩放比例的变化,导致偏移计算完全错误。
  3. AffineTransform累积缩放问题:每次调用zoomTransform.scale()会累积缩放倍数,没有结合偏移逻辑同步更新,导致绘制坐标与视图坐标的映射关系错位。
  4. 视图位置计算错误:直接使用-xOffset/-yOffset设置视图位置,未将偏移量转换为缩放后的视图坐标,无法保证鼠标指向的内容在缩放后保持在原位。

修正方案

核心逻辑:缩放时保证鼠标指向的原始世界坐标在缩放后仍处于鼠标屏幕位置。具体步骤:

  1. 将鼠标屏幕坐标转换为组件的未缩放坐标,再结合当前视图偏移计算出鼠标对应的原始世界坐标。
  2. 计算新的总缩放因子,基于世界坐标不变的原则,推导新的视图偏移量。
  3. 使用总缩放因子直接设置AffineTransform,避免累积缩放的误差。
  4. 正确转换偏移量为视图位置,更新JViewport的视图坐标。

修正后的代码

调整TestPane类(移除静态变量依赖,改用实例变量)

package testpane;
import java.awt.event.MouseAdapter;
import javax.swing.JComponent;
import javax.swing.JFrame;
import javax.swing.JScrollPane;
import javax.swing.WindowConstants;

public class TestPane 
{
    private Drawing d;
    private double zoomFactor = 1;
    
    public static void main(String[] args) 
    {
        JFrame f = new JFrame("Tree Diagram");
        f.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
        f.getContentPane().add(new TestPane().makeDiagram());
        f.setSize(1600, 800);
        f.setLocationRelativeTo(null);
        f.setVisible(true);
    }
    
    public JComponent makeDiagram()
    {
        d = new Drawing();

        MouseAdapter mouseAdapter = new TestPaneMouseListener(this);
        d.addMouseListener(mouseAdapter);
        d.addMouseMotionListener(mouseAdapter);
        d.addMouseWheelListener(mouseAdapter);

        return new JScrollPane(d);
    }
    
    public Drawing getDrawing() {
        return d;
    }
    
    public double getZoomFactor() {
        return zoomFactor;
    }
    
    public void setZoomFactor(double zoomFactor) {
        this.zoomFactor = zoomFactor;
    }
}

修正Drawing类的核心逻辑

package testpane;
import java.awt.Color;
import java.awt.Container;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.geom.AffineTransform;
import javax.swing.JPanel;
import javax.swing.JViewport;
import javax.swing.SwingUtilities;

public class Drawing extends JPanel
{
    private final AffineTransform zoomTransform = new AffineTransform();
    private final Rectangle rect = new Rectangle(1600, 800);
    // 当前视图的偏移量(对应原始世界坐标)
    double viewX = 0;
    double viewY = 0;
    
    public Drawing()
    {
        Font currentFont = getFont();
        Font newFont = currentFont.deriveFont(currentFont.getSize() * 5F);
        setFont(newFont);
        setBackground(Color.WHITE);
    }
    
    public void setZoomFactor(double newZoomFactor, Point mouseScreenPos) 
    {
        Container container = SwingUtilities.getUnwrappedParent(this);
        if (!(container instanceof JViewport)) return;
        
        JViewport viewport = (JViewport) container;
        
        // 1. 将鼠标屏幕坐标转换为组件的未缩放坐标
        Point mouseComponentPos = SwingUtilities.convertPointFromScreen(mouseScreenPos, this);
        
        // 2. 计算鼠标对应的原始世界坐标:组件坐标 + 当前视图偏移(因为视图偏移是世界坐标的起始点)
        double worldX = viewX + mouseComponentPos.getX() / zoomTransform.getScaleX();
        double worldY = viewY + mouseComponentPos.getY() / zoomTransform.getScaleY();
        
        // 3. 更新缩放变换
        zoomTransform.setToScale(newZoomFactor, newZoomFactor);
        
        // 4. 计算新的视图偏移:保证世界坐标在新缩放下,鼠标位置不变
        double newMouseComponentX = mouseComponentPos.getX();
        double newMouseComponentY = mouseComponentPos.getY();
        viewX = worldX - newMouseComponentX / newZoomFactor;
        viewY = worldY - newMouseComponentY / newZoomFactor;
        
        // 5. 设置视图位置:视图位置是偏移量的缩放后的值(因为视图坐标是组件的像素坐标)
        int viewPosX = (int) (viewX * newZoomFactor);
        int viewPosY = (int) (viewY * newZoomFactor);
        viewport.setViewPosition(new Point(viewPosX, viewPosY));
        
        revalidate();
        repaint();
    }
    
    @Override
    protected void paintComponent(Graphics g) 
    {
        super.paintComponent(g);
        
        Graphics2D g2d = (Graphics2D) g.create();
        // 先平移到视图偏移,再缩放
        AffineTransform transform = new AffineTransform();
        transform.translate(-viewX, -viewY);
        transform.concatenate(zoomTransform);
        g2d.setTransform(transform);

        g2d.drawString("Example", 300, 400);
            
        g2d.dispose();
    }
    
    @Override
    public Dimension getPreferredSize() 
    {
        Rectangle r = zoomTransform.createTransformedShape(rect).getBounds();
        return new Dimension(r.width, r.height);
    }
}

修正TestPaneMouseListener类(传递TestPane实例,传递鼠标屏幕坐标)

package testpane;
import java.awt.Component;
import java.awt.Container;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.MouseWheelEvent;
import javax.swing.JComponent;
import javax.swing.JViewport;
import javax.swing.SwingUtilities;

public class TestPaneMouseListener extends MouseAdapter
{ 
    private final Point origin = new Point();
    private final TestPane testPane;

    public TestPaneMouseListener(TestPane testPane) {
        this.testPane = testPane;
    }

    @Override
    public void mouseDragged(MouseEvent e) 
    {
        Component c = e.getComponent();
        Container p = SwingUtilities.getUnwrappedParent(c);
        if (p instanceof JViewport) 
        {
            JViewport viewport = (JViewport) p;
            Point cp = SwingUtilities.convertPoint(c, e.getPoint(), viewport);
            Point vp = viewport.getViewPosition();
            vp.translate(origin.x - cp.x, origin.y - cp.y);
            ((JComponent) c).scrollRectToVisible(new Rectangle(vp, viewport.getSize()));
            origin.setLocation(cp);
            
            // 更新视图偏移(对应原始世界坐标)
            Drawing drawing = testPane.getDrawing();
            double zoom = testPane.getZoomFactor();
            drawing.viewX = vp.getX() / zoom;
            drawing.viewY = vp.getY() / zoom;
        }
    }

   @Override
   public void mousePressed(MouseEvent e) 
   {
       Component c = e.getComponent();
       Container p = SwingUtilities.getUnwrappedParent(c);
       if(p instanceof JViewport) 
       {
           JViewport viewport = (JViewport) p;
           Point cp = SwingUtilities.convertPoint(c, e.getPoint(), viewport);
           origin.setLocation(cp);
       }
   }
   
   @Override
    public void mouseWheelMoved(MouseWheelEvent e) 
    {
        double currentZoom = testPane.getZoomFactor();
        double newZoom;
        if(e.getWheelRotation() < 0)
        {
            newZoom = currentZoom * 1.05;
        }
        else
        {
            newZoom = currentZoom / 1.05;
            // 限制最小缩放,避免过小
            newZoom = Math.max(newZoom, 0.1);
        }
        // 传递鼠标屏幕坐标
        testPane.getDrawing().setZoomFactor(newZoom, e.getLocationOnScreen());
        testPane.setZoomFactor(newZoom);
    }
}

关键修正点说明

  • 移除了静态变量依赖,改用实例变量传递数据,避免全局状态混乱。
  • 缩放时基于世界坐标不变的原则计算偏移,确保鼠标指向的内容始终在鼠标位置。
  • 正确转换鼠标坐标:屏幕坐标→组件坐标→世界坐标,再反向计算新的视图偏移。
  • 直接设置AffineTransform的缩放比例,避免累积缩放的误差。
  • 拖动视图时同步更新viewX/viewY,保证视图偏移和绘制逻辑一致。

内容的提问来源于stack exchange,提问作者Some Ace

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.06 23:55:16