可缩放的 JScrollPane - setViewPosition 无法更新

Posted

技术标签:

【中文标题】可缩放的 JScrollPane - setViewPosition 无法更新【英文标题】:Zoomable JScrollPane - setViewPosition fails to update 【发布时间】:2014-05-04 04:25:51 【问题描述】:

我正在尝试在 JScrollPane 中编写可缩放图像

当图像完全缩小时,它应该水平和垂直居中。当两个滚动条都出现时,缩放应该总是相对于鼠标坐标发生,即在缩放事件之前和之后,图像的同一点应该在鼠标下方。

我几乎实现了我的目标。不幸的是,“scrollPane.getViewport().setViewPosition()”方法有时无法正确更新视图位置。在大多数情况下,调用该方法两次(hack!)解决了这个问题,但视图仍然闪烁。

我无法解释为什么会这样。不过我相信这不是数学问题。


下面是 MWE。要查看我的问题具体是什么,您可以执行以下操作:

放大直到出现一些滚动条(200% 左右) 点击滚动条滚动到右下角 将鼠标放在角落并放大两次。第二次您会看到滚动位置如何跳向中心。

如果有人能告诉我问题出在哪里,我将不胜感激。谢谢!

package com.vitco;

import javax.swing.*;
import java.awt.*;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseWheelEvent;
import java.awt.image.BufferedImage;
import java.util.Random;

/**
 * Zoom-able scroll panel test case
 */
public class ZoomScrollPanel 

    // the size of our image
    private final static int IMAGE_SIZE = 600;

    // create an image to display
    private BufferedImage getImage() 
        BufferedImage image = new BufferedImage(IMAGE_SIZE, IMAGE_SIZE, BufferedImage.TYPE_INT_RGB);
        Graphics g = image.getGraphics();
        // draw the small pixel first
        Random rand = new Random();
        for (int x = 0; x < IMAGE_SIZE; x += 10) 
            for (int y = 0; y < IMAGE_SIZE; y += 10) 
                g.setColor(new Color(rand.nextInt(255),rand.nextInt(255),rand.nextInt(255)));
                g.fillRect(x, y, 10, 10);
            
        
        // draw the larger transparent pixel second
        for (int x = 0; x < IMAGE_SIZE; x += 100) 
            for (int y = 0; y < IMAGE_SIZE; y += 100) 
                g.setColor(new Color(rand.nextInt(255),rand.nextInt(255),rand.nextInt(255), 180));
                g.fillRect(x, y, 100, 100);
            
        
        return image;
    

    // the image panel that resizes according to zoom level
    private class ImagePanel extends JPanel 
        private final BufferedImage image = getImage();

        @Override
        public void paintComponent(Graphics g) 
            super.paintComponent(g);
            Graphics2D g2 = (Graphics2D)g.create();
            g2.scale(scale, scale);
            g2.drawImage(image, 0, 0, null);
            g2.dispose();
        

        @Override
        public Dimension getPreferredSize() 
            return new Dimension((int)Math.round(IMAGE_SIZE * scale), (int)Math.round(IMAGE_SIZE * scale));
        
    

    // the current zoom level (100 means the image is shown in original size)
    private double zoom = 100;
    // the current scale (scale = zoom/100)
    private double scale = 1;

    // the last seen scale
    private double lastScale = 1;

    public void alignViewPort(Point mousePosition) 
        // if the scale didn't change there is nothing we should do
        if (scale != lastScale) 
            // compute the factor by that the image zoom has changed
            double scaleChange = scale / lastScale;

            // compute the scaled mouse position
            Point scaledMousePosition = new Point(
                    (int)Math.round(mousePosition.x * scaleChange),
                    (int)Math.round(mousePosition.y * scaleChange)
            );

            // retrieve the current viewport position
            Point viewportPosition = scrollPane.getViewport().getViewPosition();

            // compute the new viewport position
            Point newViewportPosition = new Point(
                    viewportPosition.x + scaledMousePosition.x - mousePosition.x,
                    viewportPosition.y + scaledMousePosition.y - mousePosition.y
            );

            // update the viewport position
            // IMPORTANT: This call doesn't always update the viewport position. If the call is made twice
            // it works correctly. However the screen still "flickers".
            scrollPane.getViewport().setViewPosition(newViewportPosition);

            // debug
            if (!newViewportPosition.equals(scrollPane.getViewport().getViewPosition())) 
                System.out.println("Error: " + newViewportPosition + " != " + scrollPane.getViewport().getViewPosition());
            

            // remember the last scale
            lastScale = scale;
        
    

    // reference to the scroll pane container
    private final JScrollPane scrollPane;

    // constructor
    public ZoomScrollPanel() 
        // initialize the frame
        JFrame frame = new JFrame();
        frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
        frame.setSize(600, 600);

        // initialize the components
        final ImagePanel imagePanel = new ImagePanel();
        final JPanel centerPanel = new JPanel();
        centerPanel.setLayout(new GridBagLayout());
        centerPanel.add(imagePanel);
        scrollPane = new JScrollPane(centerPanel);
        scrollPane.setVerticalScrollBarPolicy(ScrollPaneConstants.VERTICAL_SCROLLBAR_ALWAYS);
        scrollPane.setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_ALWAYS);
        frame.add(scrollPane);

        // add mouse wheel listener
        imagePanel.addMouseWheelListener(new MouseAdapter() 
            @Override
            public void mouseWheelMoved(MouseWheelEvent e) 
                super.mouseWheelMoved(e);
                // check the rotation of the mousewheel
                int rotation = e.getWheelRotation();
                boolean zoomed = false;
                if (rotation > 0) 
                    // only zoom out until no scrollbars are visible
                    if (scrollPane.getHeight() < imagePanel.getPreferredSize().getHeight() ||
                            scrollPane.getWidth() < imagePanel.getPreferredSize().getWidth()) 
                        zoom = zoom / 1.3;
                        zoomed = true;
                    
                 else 
                    // zoom in until maximum zoom size is reached
                    double newCurrentZoom = zoom * 1.3;
                    if (newCurrentZoom < 1000)  // 1000 ~ 10 times zoom
                        zoom = newCurrentZoom;
                        zoomed = true;
                    
                
                // check if a zoom happened
                if (zoomed) 
                    // compute the scale
                    scale = (float) (zoom / 100f);

                    // align our viewport
                    alignViewPort(e.getPoint());

                    // invalidate and repaint to update components
                    imagePanel.revalidate();
                    scrollPane.repaint();
                
            
        );

        // display our frame
        frame.setVisible(true);
    

    // the main method
    public static void main(String[] args) 
        new ZoomScrollPanel();
    

注意:我也看过JScrollPane setViewPosition After "Zoom"这里的问题,但不幸的是问题和解决方案略有不同,不适用。


编辑

我已经通过使用 hack 解决了这个问题,但是我仍然没有更深入地了解根本问题是什么。发生的情况是,当调用 setViewPosition 时,某些内部状态更改会触发对 setViewPosition 的额外调用。这些额外的调用只是偶尔发生。当我阻止它们时,一切正常。

为了解决这个问题,我只是引入了一个新的布尔变量“blocked = false;”并替换了行

    scrollPane = new JScrollPane(centerPanel);

    scrollPane.getViewport().setViewPosition(newViewportPosition);

    scrollPane = new JScrollPane();

    scrollPane.setViewport(new JViewport() 
        private boolean inCall = false;
        @Override
        public void setViewPosition(Point pos) 
            if (!inCall || !blocked) 
                inCall = true;
                super.setViewPosition(pos);
                inCall = false;
            
        
    );

    scrollPane.getViewport().add(centerPanel);

     blocked = true;
     scrollPane.getViewport().setViewPosition(newViewportPosition);
     blocked = false;

如果有人能理解这一点,我仍然非常感激!

为什么这个 hack 有效?有没有更简洁的方法来实现相同的功能?

【问题讨论】:

是否打算在视图被最大程度缩小时,第一次缩放操作已经缩放到与我的鼠标所在位置不同的位置? 编辑了示例并添加了 scrollPane.setVerticalScrollBarPolicy(ScrollPaneConstants.VERTICAL_SCROLLBAR_​​ALWAYS);scrollPane.setHorizo​​ntalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_​​ALWAYS);这应该有助于最初的跳跃。 它不能解决最初的跳转,我也添加了这些以查看问题是否来自计算滚动条的大小到该位置(它不应该),但事实并非如此。已经很奇怪的一件事是您的图像没有恒定的大小。将框架设置为全屏尺寸,最大程度地放大,将框架调整为非常小的尺寸,然后缩小 - 你会得到一个很小的图像。这是故意的吗? (1) 关于初始跳转:图像必须跳转到某种程度,因为缩小时它应该居中。一旦出现滚动条,缩放应该始终正常工作。添加滚动条(我之前的评论)意味着初始跳转不是“到中心”而是“边界”。所以它们会产生我想要的初始跳跃效果。 (2) 图像大小取决于变焦。您可以缩小的最大量取决于窗口大小(一旦滚动条消失,您就不能再进一步缩小)。所以行为符合预期。 如果您将帧大小设置为 200x200 而不是 600x600,则第一个缩放操作会按预期工作(编辑:取决于缩放位置是否离边界足够远)。问题似乎在于您的计算,而不是 setViewPosition 的行为。至于第(1)点,缩小一下就忽略了。无论帧大小和滚动条的可见性如何,这不是您想要放大鼠标所在的像素吗? 【参考方案1】:

这是完整的、功能齐全的代码。我仍然不明白为什么需要 hack,但至少它现在可以按预期工作:

import javax.swing.*;
import java.awt.*;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseWheelEvent;
import java.awt.image.BufferedImage;
import java.util.Random;

/**
 * Zoom-able scroll panel
 */
public class ZoomScrollPanel 

    // the size of our image
    private final static int IMAGE_SIZE = 600;

    // create an image to display
    private BufferedImage getImage() 
        BufferedImage image = new BufferedImage(IMAGE_SIZE, IMAGE_SIZE, BufferedImage.TYPE_INT_RGB);
        Graphics g = image.getGraphics();
        // draw the small pixel first
        Random rand = new Random();
        for (int x = 0; x < IMAGE_SIZE; x += 10) 
            for (int y = 0; y < IMAGE_SIZE; y += 10) 
                g.setColor(new Color(rand.nextInt(255),rand.nextInt(255),rand.nextInt(255)));
                g.fillRect(x, y, 10, 10);
            
        
        // draw the larger transparent pixel second
        for (int x = 0; x < IMAGE_SIZE; x += 100) 
            for (int y = 0; y < IMAGE_SIZE; y += 100) 
                g.setColor(new Color(rand.nextInt(255),rand.nextInt(255),rand.nextInt(255), 180));
                g.fillRect(x, y, 100, 100);
            
        
        return image;
    

    // the image panel that resizes according to zoom level
    private class ImagePanel extends JPanel 
        private final BufferedImage image = getImage();

        @Override
        public void paintComponent(Graphics g) 
            super.paintComponent(g);
            Graphics2D g2 = (Graphics2D)g.create();
            g2.scale(scale, scale);
            g2.drawImage(image, 0, 0, null);
            g2.dispose();
        

        @Override
        public Dimension getPreferredSize() 
            return new Dimension((int)Math.round(IMAGE_SIZE * scale), (int)Math.round(IMAGE_SIZE * scale));
        
    

    // the current zoom level (100 means the image is shown in original size)
    private double zoom = 100;
    // the current scale (scale = zoom/100)
    private double scale = 1;

    // the last seen scale
    private double lastScale = 1;

    // true if currently executing setViewPosition
    private boolean blocked = false;

    public void alignViewPort(Point mousePosition) 
        // if the scale didn't change there is nothing we should do
        if (scale != lastScale) 
            // compute the factor by that the image zoom has changed
            double scaleChange = scale / lastScale;

            // compute the scaled mouse position
            Point scaledMousePosition = new Point(
                    (int)Math.round(mousePosition.x * scaleChange),
                    (int)Math.round(mousePosition.y * scaleChange)
            );

            // retrieve the current viewport position
            Point viewportPosition = scrollPane.getViewport().getViewPosition();

            // compute the new viewport position
            Point newViewportPosition = new Point(
                    viewportPosition.x + scaledMousePosition.x - mousePosition.x,
                    viewportPosition.y + scaledMousePosition.y - mousePosition.y
            );

            // update the viewport position
            blocked = true;
            scrollPane.getViewport().setViewPosition(newViewportPosition);
            blocked = false;

            // remember the last scale
            lastScale = scale;
        
    

    // reference to the scroll pane container
    private final JScrollPane scrollPane;

    // constructor
    public ZoomScrollPanel() 
        // initialize the frame
        JFrame frame = new JFrame();
        frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
        frame.setSize(600, 600);

        // initialize the components
        final ImagePanel imagePanel = new ImagePanel();
        final JPanel centerPanel = new JPanel();
        centerPanel.setLayout(new GridBagLayout());
        centerPanel.add(imagePanel);
        scrollPane = new JScrollPane();

        scrollPane.setViewport(new JViewport() 
            private boolean inCall = false;
            @Override
            public void setViewPosition(Point pos) 
                if (!inCall || !blocked) 
                    inCall = true;
                    super.setViewPosition(pos);
                    inCall = false;
                
            
        );

        scrollPane.getViewport().add(centerPanel);
        scrollPane.setVerticalScrollBarPolicy(ScrollPaneConstants.VERTICAL_SCROLLBAR_ALWAYS);
        scrollPane.setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_ALWAYS);
        frame.add(scrollPane);

        // add mouse wheel listener
        imagePanel.addMouseWheelListener(new MouseAdapter() 
            @Override
            public void mouseWheelMoved(MouseWheelEvent e) 
                super.mouseWheelMoved(e);
                // check the rotation of the mousewheel
                int rotation = e.getWheelRotation();
                boolean zoomed = false;
                if (rotation > 0) 
                    // only zoom out until no scrollbars are visible
                    if (scrollPane.getHeight() < imagePanel.getPreferredSize().getHeight() ||
                            scrollPane.getWidth() < imagePanel.getPreferredSize().getWidth()) 
                        zoom = zoom / 1.3;
                        zoomed = true;
                    
                 else 
                    // zoom in until maximum zoom size is reached
                    double newCurrentZoom = zoom * 1.3;
                    if (newCurrentZoom < 1000)  // 1000 ~ 10 times zoom
                        zoom = newCurrentZoom;
                        zoomed = true;
                    
                
                // check if a zoom happened
                if (zoomed) 
                    // compute the scale
                    scale = (float) (zoom / 100f);

                    // align our viewport
                    alignViewPort(e.getPoint());

                    // invalidate and repaint to update components
                    imagePanel.revalidate();
                    scrollPane.repaint();
                
            
        );

        // display our frame
        frame.setVisible(true);
    

    // the main method
    public static void main(String[] args) 
        new ZoomScrollPanel();
    

【讨论】:

【参考方案2】:

前段时间我遇到了同样的问题。我在 JScrollPane 的 Viewport 中存储了一些可缩放/可缩放的内容(SWT 小部件),并实现了一些功能以启用平移和缩放内容。如果基本相同,我没有查看您的代码,但是我观察到的问题完全一样。从右侧/底部向外放大时,有时,视图位置会跳到中心一点(从我的角度来看,这肯定指向一个比例因子)。使用加倍的“setViewPosition”以某种方式增强了行为,但仍然不可用。

经过一番调查,我发现我这边的问题是在我更改滚动面板内内容的比例因子的那一刻和在滚动面板中设置视图位置的那一刻之间。问题是滚动面板在布局完成之前不知道内容大小的更新。所以基本上,它是根据旧的内容大小、范围大小和视图位置来更新位置。

所以,在我这边,这很有帮助。

// updating scroll panel content scale goes here

viewport.doLayout();

// setting view position in viewport goes here

检查方法 BasicScrollPaneUI#syncScrollPaneWithViewport() 对我来说非常有用。

【讨论】:

以上是关于可缩放的 JScrollPane - setViewPosition 无法更新的主要内容,如果未能解决你的问题,请参考以下文章

在两页模式下如何实现可缩放的 UIPageViewController?

在可缩放的滚动视图中禁用图像视图缩放

如何制作可缩放的线性布局? [关闭]

如何使用 UIScrollView 和 CATiledLayer 在可缩放的 UIView 上绘制标记

Highcharts 时间序列,可缩放的图表;Highcharts X 轴翻转曲线图;Highcharts 带标记曲线图

HTML5 Canvas 中可拖动、可缩放的图像