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