基于AffineTransform与JScrollPane的鼠标位置缩放问题排查
解决JScrollPane适配缩放/旋转时的图片变形问题
问题根源
直接调用g2.setTransform(cordTransform)会覆盖Swing为JScrollPane视口预设的坐标变换(包含滚动偏移信息),导致滚动时图片绘制的坐标系与视口实际位置脱节,出现变形。同时现有代码未处理旋转后的组件尺寸变化,JScrollPane无法正确计算滚动范围。
解决方案
1. 保留Swing默认变换,叠加自定义变换
在paintComponent中,不要直接替换Graphics2D的变换,而是通过transform()叠加自定义变换,既保留视口的滚动偏移,又能应用缩放和旋转效果。
2. 修复旋转后的首选尺寸计算
添加旋转次数变量,根据旋转角度调整组件的首选宽高,确保JScrollPane能正确适配旋转后的图片尺寸。
3. 清理冗余代码
移除init变量的无效逻辑,不要手动调用g2.dispose()(Graphics对象由Swing负责回收)。
修改后的完整代码
package org.example; import java.awt.*; import java.awt.event.KeyEvent; import java.awt.event.KeyListener; import java.awt.event.MouseWheelEvent; import java.awt.geom.AffineTransform; import java.awt.geom.NoninvertibleTransformException; import java.awt.geom.Point2D; import java.awt.image.BufferedImage; import java.io.File; import java.io.IOException; import java.util.logging.Level; import java.util.logging.Logger; import javax.imageio.ImageIO; import javax.swing.*; public class ZoomAndPan1 extends JPanel { BufferedImage img; private static final double ZOOM_MULTIPLICATION_FACTOR = 1.2; private final AffineTransform cordTransform = new AffineTransform(); private double scale = 1.0; private int rotateCount = 0; // 跟踪旋转次数:0=0°,1=90°,2=180°,3=270° public ZoomAndPan1() throws IOException { this.addMouseWheelListener(e -> { if (e.isControlDown()) { zoom(e); } }); } @Override protected void paintComponent(Graphics g) { super.paintComponent(g); Graphics2D g2 = (Graphics2D) g; // 保存原有变换,叠加自定义变换后恢复 AffineTransform originalTransform = g2.getTransform(); try { g2.transform(cordTransform); // 叠加自定义变换,不覆盖视口滚动偏移 g2.drawImage(img, 0, 0, this); } finally { g2.setTransform(originalTransform); // 恢复原有变换,避免影响其他绘制 } } private void rotate(int direction) { double centerX = img.getWidth() / 2.0; double centerY = img.getHeight() / 2.0; cordTransform.translate(centerX, centerY); if (direction > 0) { cordTransform.quadrantRotate(3); // 逆时针90° rotateCount = (rotateCount + 3) % 4; } else { cordTransform.quadrantRotate(1); // 顺时针90° rotateCount = (rotateCount + 1) % 4; } cordTransform.translate(-centerX, -centerY); revalidate(); // 通知布局管理器更新尺寸 repaint(); } private void zoom(MouseWheelEvent e) { try { int wheelRotation = e.getWheelRotation(); Point p = e.getPoint(); Point2D p1 = cordTransform.inverseTransform(p, null); // 简化变换点计算 double zoomFactor = wheelRotation > 0 ? 1 / ZOOM_MULTIPLICATION_FACTOR : ZOOM_MULTIPLICATION_FACTOR; scale *= zoomFactor; // 围绕鼠标位置缩放:平移到鼠标点 → 缩放 → 平移回原位置 cordTransform.translate(p.getX(), p.getY()); cordTransform.scale(zoomFactor, zoomFactor); cordTransform.translate(-p.getX(), -p.getY()); revalidate(); repaint(); } catch (NoninvertibleTransformException ex) { ex.printStackTrace(); } } @Override public Dimension getPreferredSize() { int imgWidth = img.getWidth(); int imgHeight = img.getHeight(); // 根据旋转次数交换宽高 int effectiveWidth = (rotateCount % 2 == 0) ? imgWidth : imgHeight; int effectiveHeight = (rotateCount % 2 == 0) ? imgHeight : imgWidth; return new Dimension((int) (scale * effectiveWidth), (int) (scale * effectiveHeight)); } public void setImg(BufferedImage img) { this.img = img; // 重置变换和状态 cordTransform.setToIdentity(); scale = 1.0; rotateCount = 0; revalidate(); repaint(); } public static void main(String[] args) { try { BufferedImage img = ImageIO.read(new File("src/main/resources/car.jpg")); JFrame frame = new JFrame("Zoom and Pan"); Dimension d = new Dimension(1920, 1080); frame.setSize(d); frame.setPreferredSize(d); frame.setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE); ZoomAndPan1 zoomAndPan1 = new ZoomAndPan1(); zoomAndPan1.setImg(img); JScrollPane scrollPane = new JScrollPane(zoomAndPan1); frame.add(scrollPane, BorderLayout.CENTER); frame.pack(); frame.setVisible(true); // 解决键盘事件焦点问题 zoomAndPan1.setFocusable(true); zoomAndPan1.addKeyListener(new KeyListener() { @Override public void keyTyped(KeyEvent e) {} @Override public void keyPressed(KeyEvent e) { if (e.getKeyCode() == KeyEvent.VK_F7) { zoomAndPan1.rotate(1); } if (e.getKeyCode() == KeyEvent.VK_F8) { zoomAndPan1.rotate(-1); } } @Override public void keyReleased(KeyEvent e) {} }); frame.requestFocusInWindow(); } catch (IOException ex) { Logger.getLogger(ZoomAndPan1.class.getName()).log(Level.SEVERE, null, ex); } } }
关键修改说明
- paintComponent:用
g2.transform(cordTransform)替代setTransform,保留视口的滚动偏移;添加finally块恢复原有变换,避免影响其他组件绘制。 - getPreferredSize:根据旋转次数动态调整组件宽高,确保JScrollPane能正确计算滚动范围。
- zoom方法:简化变换点计算逻辑,调整缩放的变换顺序,保证围绕鼠标位置缩放的准确性。
- 键盘事件:将监听绑定到ZoomAndPan1组件并设置可聚焦,解决原代码中键盘事件不响应的问题。
内容的提问来源于stack exchange,提问作者Displayname
相关产品推荐
相关产品推荐

