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

Swing中为鼠标中心缩放的JPanel添加JScrollbars问题

解决鼠标中心缩放时JScrollPane滚动条失效与内容裁剪问题

当实现以鼠标光标为中心的缩放功能时,仅通过缩放因子 × 原始尺寸设置组件首选尺寸的方式无法适配JScrollPane的滚动逻辑——因为鼠标中心缩放的AffineTransform包含平移操作,导致实际内容的显示范围和组件尺寸不匹配,最终出现内容被裁剪、滚动条无法正常工作的问题。

核心解决思路

  1. 准确计算变换后内容的边界:将原始内容的矩形通过当前的AffineTransform进行变换,得到缩放平移后的实际边界矩形,以此作为组件首选尺寸的依据,确保JScrollPane能识别完整的内容范围。
  2. 同步调整视口位置:缩放完成后,调整JScrollPane的视口偏移量,保证鼠标指向的内容区域在缩放前后保持在视口的同一相对位置,让滚动条位置与内容偏移匹配。

修改后的完整代码

package zoom;

import javax.swing.*;
import java.awt.*;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.MouseWheelEvent;
import java.awt.geom.AffineTransform;
import java.awt.geom.Rectangle2D;

public class StackOverflow {

    public static void main(String[] args) {
        JFrame frame = new JFrame("Zoomable Canvas");
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.setSize(800, 600);

        ZoomPanel2 zoomPanel = new ZoomPanel2();
        JScrollPane scrollPane = new JScrollPane(zoomPanel);
        frame.add(scrollPane);
        frame.setVisible(true);
        frame.setLocationRelativeTo(null);
    }
}

class ZoomPanel2 extends JPanel {

    private static final long serialVersionUID = 1L;
    private double zoomFactor = 1.0;
    private static final double ZOOM_MULTIPLIER = 1.1;
    private static final double MIN_ZOOM = 0.1;
    private static final double MAX_ZOOM = 4.0;

    private AffineTransform currentTransform;
    private Point lastMousePosition;
    private Rectangle2D.Double square;
    private Point dragOffset;
    private Rectangle2D.Double redRectangle;

    public ZoomPanel2() {
        currentTransform = new AffineTransform();
        square = new Rectangle2D.Double(100, 100, 200, 200);
        redRectangle = new Rectangle2D.Double(0, 0, 1000, 1000);

        addMouseWheelListener(new MouseAdapter() {
            @Override
            public void mouseWheelMoved(MouseWheelEvent e) {
                double adjustedZoomFactor = e.getWheelRotation() < 0 ? ZOOM_MULTIPLIER : 1 / ZOOM_MULTIPLIER;
                double newZoomFactor = zoomFactor * adjustedZoomFactor;

                if (newZoomFactor >= MIN_ZOOM && newZoomFactor <= MAX_ZOOM) {
                    zoomFactor = newZoomFactor;
                    Point zoomCenter = e.getPoint();

                    AffineTransform at = new AffineTransform();
                    at.translate(zoomCenter.getX(), zoomCenter.getY());
                    at.scale(adjustedZoomFactor, adjustedZoomFactor);
                    at.translate(-zoomCenter.getX(), -zoomCenter.getY());

                    currentTransform.preConcatenate(at);
                    updatePreferredSize(e);
                    repaint();
                }
            }
        });

        addMouseListener(new MouseAdapter() {
            @Override
            public void mousePressed(MouseEvent e) {
                Point transformedPoint = transformPoint(e.getPoint(), currentTransform);
                if (square.contains(transformedPoint)) {
                    lastMousePosition = e.getPoint();
                    dragOffset = new Point(transformedPoint.x - (int) square.getX(), transformedPoint.y - (int) square.getY());
                } else {
                    lastMousePosition = null;
                }
            }
        });

        addMouseMotionListener(new MouseAdapter() {
            @Override
            public void mouseDragged(MouseEvent e) {
                if (lastMousePosition != null) {
                    Point transformedPoint = transformPoint(e.getPoint(), currentTransform);
                    int newX = transformedPoint.x - dragOffset.x;
                    int newY = transformedPoint.y - dragOffset.y;

                    if (redRectangle.contains(newX, newY, square.getWidth(), square.getHeight())) {
                        square.setRect(newX, newY, square.getWidth(), square.getHeight());
                        lastMousePosition = e.getPoint();
                        repaint();
                    }
                }
            }
          });
      }
    
    
    private Point transformPoint(Point point, AffineTransform transform) {
        try {
            AffineTransform inverse = transform.createInverse();
            Point transformedPoint = new Point();
            inverse.transform(point, transformedPoint);
            return transformedPoint;
        } catch (Exception ex) {
            ex.printStackTrace();
        }
        return null;
    }

    @Override
    protected void paintComponent(Graphics g) {
        super.paintComponent(g);

        Graphics2D g2d = (Graphics2D) g;
        g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
        g2d.transform(currentTransform);

        g2d.setColor(Color.red);
        g2d.fill(redRectangle);

        g2d.setColor(Color.BLUE);
        g2d.fill(square);
    }

    private void updatePreferredSize(MouseWheelEvent e) {
        // 计算变换后内容的实际边界
        Rectangle2D transformedContentBounds = currentTransform.createTransformedShape(redRectangle).getBounds2D();
        
        // 设置组件首选尺寸为内容完整大小
        setPreferredSize(new Dimension(
            (int) Math.ceil(transformedContentBounds.getWidth()),
            (int) Math.ceil(transformedContentBounds.getHeight())
        ));
        
        // 调整视口位置,保证鼠标指向区域不偏移
        JScrollPane scrollPane = (JScrollPane) SwingUtilities.getAncestorOfClass(JScrollPane.class, this);
        if (scrollPane != null) {
            JViewport viewport = scrollPane.getViewport();
            Point currentViewPos = viewport.getViewPosition();
            
            double mouseRelativeX = e.getX();
            double mouseRelativeY = e.getY();
            double scaleDelta = e.getWheelRotation() < 0 ? ZOOM_MULTIPLIER : 1/ZOOM_MULTIPLIER;
            
            // 计算新的视口偏移量
            double newViewX = currentViewPos.x + mouseRelativeX * (1 - scaleDelta);
            double newViewY = currentViewPos.y + mouseRelativeY * (1 - scaleDelta);
            
            // 限制视口位置不超出内容边界
            newViewX = Math.max(0, Math.min(newViewX, transformedContentBounds.getWidth() - viewport.getWidth()));
            newViewY = Math.max(0, Math.min(newViewY, transformedContentBounds.getHeight() - viewport.getHeight()));
            
            viewport.setViewPosition(new Point((int) newViewX, (int) newViewY));
        }
        
        revalidate();
    }    
} 

关键修改说明

  1. 内容边界计算:通过currentTransform.createTransformedShape(redRectangle).getBounds2D()获取原始内容经过缩放平移后的实际边界,确保组件首选尺寸能覆盖全部内容。
  2. 视口位置调整:根据鼠标在视口中的相对位置,计算缩放后的视口偏移量,让鼠标指向的内容区域始终保持在视口内,同步更新滚动条位置。

内容的提问来源于stack exchange,提问作者Newbie Guest

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.20 04:47:27