JScrollPane缩放后基于鼠标定位视图的计算错误修正问询
问题分析与修正方案
错误原因
- 鼠标坐标转换错误:使用
MouseInfo获取的屏幕坐标计算相对组件的位置时,未考虑当前的缩放状态,得到的xRel/yRel是组件的像素坐标,而非绘制时的原始世界坐标,后续偏移计算逻辑基于缩放后的坐标体系,两者不匹配。 - 缩放因子逻辑混乱:
prevZoomFactor存储的是单次缩放增量(如1.05),而TestPane.zoomFactor是累计总缩放因子,两者的比值zoomDiv无法正确反映缩放比例的变化,导致偏移计算完全错误。 - AffineTransform累积缩放问题:每次调用
zoomTransform.scale()会累积缩放倍数,没有结合偏移逻辑同步更新,导致绘制坐标与视图坐标的映射关系错位。 - 视图位置计算错误:直接使用
-xOffset/-yOffset设置视图位置,未将偏移量转换为缩放后的视图坐标,无法保证鼠标指向的内容在缩放后保持在原位。
修正方案
核心逻辑:缩放时保证鼠标指向的原始世界坐标在缩放后仍处于鼠标屏幕位置。具体步骤:
- 将鼠标屏幕坐标转换为组件的未缩放坐标,再结合当前视图偏移计算出鼠标对应的原始世界坐标。
- 计算新的总缩放因子,基于世界坐标不变的原则,推导新的视图偏移量。
- 使用总缩放因子直接设置
AffineTransform,避免累积缩放的误差。 - 正确转换偏移量为视图位置,更新
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
相关产品推荐
相关产品推荐

