Swing中为鼠标中心缩放的JPanel添加JScrollbars问题
解决鼠标中心缩放时JScrollPane滚动条失效与内容裁剪问题
当实现以鼠标光标为中心的缩放功能时,仅通过缩放因子 × 原始尺寸设置组件首选尺寸的方式无法适配JScrollPane的滚动逻辑——因为鼠标中心缩放的AffineTransform包含平移操作,导致实际内容的显示范围和组件尺寸不匹配,最终出现内容被裁剪、滚动条无法正常工作的问题。
核心解决思路
- 准确计算变换后内容的边界:将原始内容的矩形通过当前的
AffineTransform进行变换,得到缩放平移后的实际边界矩形,以此作为组件首选尺寸的依据,确保JScrollPane能识别完整的内容范围。 - 同步调整视口位置:缩放完成后,调整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(); } }
关键修改说明
- 内容边界计算:通过
currentTransform.createTransformedShape(redRectangle).getBounds2D()获取原始内容经过缩放平移后的实际边界,确保组件首选尺寸能覆盖全部内容。 - 视口位置调整:根据鼠标在视口中的相对位置,计算缩放后的视口偏移量,让鼠标指向的内容区域始终保持在视口内,同步更新滚动条位置。
内容的提问来源于stack exchange,提问作者Newbie Guest
相关产品推荐
相关产品推荐

