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

基于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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.10 14:55:01