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

完全缩小图像后,应将其水平和垂直居中。当两个滚动条都出现时,缩放应始终相对于鼠标坐标进行,即,在缩放事件之前和之后,图像的同一点应位于鼠标下方。

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

我没有任何解释为什么会这样。但是,我有信心这不是数学问题。



以下是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的其他调用。这些额外的调用仅偶尔发生。当我阻止它们时,一切都将正常运行。

    为了解决这个问题,我只介绍了一个新的 boolean 变量“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;
    

    如果有人能体会到这一点,我仍然非常感谢!

    为什么这种骇客运作?有没有更干净的方法来实现相同的功能?

    最佳答案

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

    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();
        }
    }
    

    09-28 13:01