Java – zoomable JScrollPane – setviewposition cannot be updated

I'm trying to encode scalable images in JScrollPane

When the image is fully zoomed out, it should be centered horizontally and vertically When both scroll bars appear, the zoom should always occur relative to the mouse coordinates, that is, the same point of the image should be under the mouse before and after the zoom event

I almost achieved my goal Unfortunately, the "scrollpane. Getviewport(). Setviewposition()" method sometimes fails to update the view position correctly In most cases, the method is called twice (hacker!) This problem is overcome, but the view still flickers

I didn't explain why But I believe it's not a mathematical problem

The following is MWe To view my questions, you can do the following:

>Zoom in until you have some scroll bars (about 200% zoom) > click the scroll bar to scroll to the lower right corner > place the mouse in the corner and zoom in twice The second time you will see how the scroll position jumps to the center

I would really appreciate it if someone could tell me the problem thank you!

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),180));
                g.fillRect(x,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,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();
    }
}

Note: I also checked the problem of JScrollPane setviewposition after "zoom" here, but unfortunately, the problem is slightly different from the solution and is not applicable

edit

I've solved this problem by using hack, but I'm still not closer to understanding what the underlying problem is What happens is that when setviewposition is called, some internal state changes trigger other calls to setviewposition These extra calls happen occasionally When I stopped them, everything was perfect

To solve this problem, I briefly introduced a new boolean variable "blocked = false;" And replaced the line

scrollPane = new JScrollPane(centerPanel);

and

scrollPane.getViewport().setViewPosition(newViewportPosition);

with

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

and

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

If anyone can understand this, I will still be very grateful!

Why does this hacker work? Is there a more concise way to achieve the same function?

Solution

This is a complete and fully functional code I still don't understand why hacking is necessary, but at least it works as expected now:

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,(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),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();
    }
}
The content of this article comes from the network collection of netizens. It is used as a learning reference. The copyright belongs to the original author.
THE END
分享
二维码
< <上一篇
下一篇>>