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

基于JScrollPane实现鼠标定点缩放的问题求助

问题描述

我找到了几个与我遇到的问题类似的提问,但由于我使用的是JScrollPane,这些问题的解答都无法真正解决我的问题。

我找到了定点缩放所需的公式代码:

originX -= mouseX / zoom - mouseX;
originY -= mouseY / zoom - mouseY;

于是我尝试将其适配到JScrollPane中:

JScrollPane scrollPane = drawFrame.scrollPane;
JScrollBar vert = scrollPane.getVerticalScrollBar(),
      horiz = scrollPane.getHorizontalScrollBar();
horiz.setValue(
     (int)(horiz.getValue() - (float)pastMouseX / zoom + pastMouseX)
);
vert.setValue(
     (int)(vert.getValue() - (float)pastMouseY / zoom + pastMouseY)
);

但缩放时鼠标指向的定点位置完全混乱,我不清楚原因。以下是最小可复现示例代码:

import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.event.MouseEvent;
import java.awt.event.MouseMotionListener;
import java.awt.event.MouseWheelEvent;
import java.awt.event.MouseWheelListener;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import javax.imageio.ImageIO;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.JScrollBar;
import javax.swing.JScrollPane;
public class ResizeTest extends JFrame{
    JScrollPane scrollPane;
    BufferedImage image;
    int mouseX, mouseY;
    float zoom;
    
    public static void main(String[] args) {
        EventQueue.invokeLater(new Runnable(){
            @Override
            public void run(){
                new ResizeTest();
            }
        });
    }
    
    public ResizeTest(){
        zoom = 1;
        
        try{
            image = ImageIO.read(new File("a.jpg"));
        }catch(IOException e){
            e.printStackTrace();
        }
        
        JPanel panel = new JPanel(){
            @Override
            public void paintComponent(Graphics g){
                super.paintComponent(g);
                Graphics2D g2 = (Graphics2D)g;
                g2.scale(zoom, zoom);
                g2.drawImage(image, 0, 0, null);
                System.out.println(zoom);
            }
            
            @Override
            public Dimension getPreferredSize(){
                return new Dimension(image.getWidth(), image.getHeight());
            }
            
        };
        
        scrollPane = new JScrollPane(panel);
        
        panel.addMouseWheelListener(new MouseWheelListener(){
            @Override
            public void mouseWheelMoved(MouseWheelEvent e) {
                if(e.isControlDown()) {
                    if(e.getWheelRotation() > 0){
                        zoom /= 1.3333333333f;
                        if(zoom < 0.125)
                           zoom = 0.125f;
                    }
                    else if (e.getWheelRotation() < 0){
                        zoom *= 1.3333333333f;
                        if(zoom > 10)
                            zoom = 10;
                    }
                    if(zoom < 10){
                        JScrollBar vert = scrollPane.getVerticalScrollBar(),
                                horiz = scrollPane.getHorizontalScrollBar();
                        horiz.setValue(
                                (int)(horiz.getValue() - (float)mouseX / zoom + mouseX)
                        );
                        vert.setValue(
                                (int)(vert.getValue() - (float)mouseY / zoom + mouseY)
                        );
                    }

                } else {
                    scrollPane.dispatchEvent(e);
                }
                
                panel.repaint();
            }
        });
        
        panel.addMouseMotionListener(new MouseMotionListener(){
            @Override
            public void mouseDragged(MouseEvent e) {
                mouseX = e.getX();
                mouseY = e.getY();
            }

            @Override
            public void mouseMoved(MouseEvent e) {
                mouseX = e.getX();
                mouseY = e.getY();
            }
        });
        
        setSize(new Dimension(1200,700));
        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        add(scrollPane);
        setVisible(true);
    }
    
    @Override
    public Dimension getPreferredSize(){
        return new Dimension(1200, 700);
    }
}
问题原因与解决方法

核心问题

  1. 鼠标坐标未转换:mouseX、mouseY是面板上的像素坐标,和缩放后的图像实际坐标不匹配,直接代入公式会导致滚动条计算偏差。
  2. 滚动条逻辑错误:原始公式针对画布原点偏移,而JScrollPane滚动条值对应视图偏移量,需结合缩放比例变化重新推导,不能直接套用原始公式。
  3. 面板尺寸未适配缩放:getPreferredSize返回原始图像尺寸,滚动条范围不会随缩放变化,导致滚动逻辑失效。

修正实现

关键调整点

  • 先记录旧缩放比例,计算缩放变化量
  • 将面板鼠标坐标转换为原始图像坐标系下的位置
  • 重新推导滚动条计算公式,保证鼠标指向的图像位置固定
  • 更新面板首选尺寸,适配缩放后的图像大小

修正后的完整代码

import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.event.MouseEvent;
import java.awt.event.MouseMotionListener;
import java.awt.event.MouseWheelEvent;
import java.awt.event.MouseWheelListener;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import javax.imageio.ImageIO;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.JScrollBar;
import javax.swing.JScrollPane;

public class ResizeTest extends JFrame{
    JScrollPane scrollPane;
    BufferedImage image;
    int mouseX, mouseY;
    float zoom;
    
    public static void main(String[] args) {
        EventQueue.invokeLater(new Runnable(){
            @Override
            public void run(){
                new ResizeTest();
            }
        });
    }
    
    public ResizeTest(){
        zoom = 1;
        
        try{
            image = ImageIO.read(new File("a.jpg"));
        }catch(IOException e){
            e.printStackTrace();
        }
        
        JPanel panel = new JPanel(){
            @Override
            public void paintComponent(Graphics g){
                super.paintComponent(g);
                Graphics2D g2 = (Graphics2D)g;
                g2.scale(zoom, zoom);
                g2.drawImage(image, 0, 0, null);
            }
            
            @Override
            public Dimension getPreferredSize(){
                return new Dimension((int)(image.getWidth() * zoom), (int)(image.getHeight() * zoom));
            }
        };
        
        scrollPane = new JScrollPane(panel);
        
        panel.addMouseWheelListener(new MouseWheelListener(){
            @Override
            public void mouseWheelMoved(MouseWheelEvent e) {
                if(e.isControlDown()) {
                    float oldZoom = zoom;
                    // 调整缩放比例
                    if(e.getWheelRotation() > 0){
                        zoom /= 1.3333333333f;
                        zoom = Math.max(zoom, 0.125f);
                    }
                    else {
                        zoom *= 1.3333333333f;
                        zoom = Math.min(zoom, 10f);
                    }

                    // 转换鼠标坐标到原始图像坐标系
                    float imageMouseX = mouseX / oldZoom;
                    float imageMouseY = mouseY / oldZoom;
                    
                    JScrollBar vert = scrollPane.getVerticalScrollBar();
                    JScrollBar horiz = scrollPane.getHorizontalScrollBar();
                    
                    // 计算新滚动条位置,保证鼠标指向的图像点固定
                    int newHorizValue = (int)(imageMouseX * zoom - mouseX + (horiz.getValue() / oldZoom) * zoom);
                    int newVertValue = (int)(imageMouseY * zoom - mouseY + (vert.getValue() / oldZoom) * zoom);
                    
                    horiz.setValue(newHorizValue);
                    vert.setValue(newVertValue);
                    
                    // 更新滚动条范围
                    panel.revalidate();
                } else {
                    scrollPane.dispatchEvent(e);
                }
                
                panel.repaint();
            }
        });
        
        panel.addMouseMotionListener(new MouseMotionListener(){
            @Override
            public void mouseDragged(MouseEvent e) {
                mouseX = e.getX();
                mouseY = e.getY();
            }

            @Override
            public void mouseMoved(MouseEvent e) {
                mouseX = e.getX();
                mouseY = e.getY();
            }
        });
        
        setSize(new Dimension(1200,700));
        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        add(scrollPane);
        setVisible(true);
    }
    
    @Override
    public Dimension getPreferredSize(){
        return new Dimension(1200, 700);
    }
}

内容的提问来源于stack exchange,提问作者The Dog on the Log

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.03 21:18:10