I am trying to make a zoomable map with Swing. The map is a JPanel in a JScrollPane. When zoomed in, the map changes size and paint() paints the elements in a different position. This all works great.
However, the ScrollPane didn't change the viewport while increasing the image size, so that zooming in always moved the elements I was looking at out of the screen. I tried to solve this with scrollRectToVisible()
, but I don't manage to get the right coordinates for the rectangle, either because I fail at doing the geometry or because I don't understand Swing all that well.
Here is what I have:
public class MapPanel extends JPanel {
[...]
public void setZoom(double zoom) {
// get the current viewport rectangle and its center in the scaled coordinate system
JViewport vp = (JViewport) this.getParent();
Rectangle rect = vp.getViewRect();
Point middle = getMiddle(rect);
Dimension dim = rect.getSize();
// zoom in
scaler.setZoom(zoom);
setPreferredSize(scaler.transform(dim));
this.revalidate();
// calculate the full size of the scaled coordinate system
Dimension fullDim = scaler.transform(dim);
// calculate the non-scaled center of the viewport
Point nMiddle = new Point((int) ((double) (middle.x)/fullDim.width*dim.width),(int) ((double) (middle.y)/fullDim.height*dim.height));
// this should do the trick, but is always a bit off towards the origin
scrollRectToVisible(getRectangleAroundPoint(nMiddle));
// the below alternative always zooms in perfectly to the center of the map
// scrollRectToVisible(getRectangleAroundPoint(new Point(400,300)));
}
private Rectangle getRectangleAroundPoint(Point p){
Point newP = scaler.transform(p);
Dimension d = railMap.getDimension();
Point corner = new Point(newP.x-d.width/2,newP.y-d.height/2);
return new Rectangle(corner,d);
}
private Point getMiddle(Rectangle r){
return new Point(r.x+r.width/2,r.y+r.height/2);
}
}
And here's the Scaler class (which does nothing very surprising, I think):
public class Scaler {
private double zoom = 1;
public void setZoom(double zoom) {
this.zoom = zoom;
}
public Point transform(Point2D p){
return new Point((int) (p.getX()*zoom), (int) (p.getY()*zoom));
}
public Dimension transform(Dimension d){
return new Dimension((int) (d.width*zoom), (int) (d.height*zoom));
}
}
Who can tell me where things are going wrong? It seems to me I did a valid calculation of the current center of the map, and with a fixed zoom point it does work...
Edit: so the hard thing here is to create the new viewport rectangle based on the old viewport rectangle.
If you want to let the frame appear at the center of your screen, you can set the location by: frame. setLocationRelativeTo(null); Note that you would want to invoke the above after you set the size (or pack() ) your frame.
We change the background colour to red, and give the JPanel a size using setSize(width, height). We also give the widget a location using setLocation(x pixels, y pixels). This sets the top left hand corner of the widget to the specified position.
JPanel, a part of the Java Swing package, is a container that can store a group of components. The main task of JPanel is to organize components, various layouts can be set in JPanel which provide better organization of components, however, it does not have a title bar.
I just did this really quick example, which basically tries to keep the scroll pane center around the middle of the supplied image
public class TestZooming {
public static void main(String[] args) {
new TestZooming();
}
public TestZooming() {
EventQueue.invokeLater(new Runnable() {
@Override
public void run() {
try {
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
} catch (ClassNotFoundException classNotFoundException) {
} catch (InstantiationException instantiationException) {
} catch (IllegalAccessException illegalAccessException) {
} catch (UnsupportedLookAndFeelException unsupportedLookAndFeelException) {
}
JFrame frame = new JFrame();
frame = new JFrame();
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setSize(400, 400);
frame.setLocationRelativeTo(null);
frame.setLayout(new BorderLayout());
final ZoomPane pane = new ZoomPane();
frame.add(new JScrollPane(pane));
frame.setVisible(true);
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
pane.centerInViewport();
}
});
}
});
}
protected class ZoomPane extends JPanel {
private Image background;
private Image scaled;
private float zoom = 1f;
private Dimension scaledSize;
private JViewport con;
public ZoomPane() {
try {
background = ImageIO.read(new File("..."));
scaled = background;
scaledSize = new Dimension(background.getWidth(this), background.getHeight(this));
} catch (IOException ex) {
ex.printStackTrace();
}
InputMap im = getInputMap(WHEN_IN_FOCUSED_WINDOW);
ActionMap am = getActionMap();
im.put(KeyStroke.getKeyStroke(KeyEvent.VK_PLUS, 0), "plus");
im.put(KeyStroke.getKeyStroke(KeyEvent.VK_EQUALS, InputEvent.SHIFT_DOWN_MASK), "plus");
im.put(KeyStroke.getKeyStroke(KeyEvent.VK_MINUS, 0), "minus");
am.put("plus", new AbstractAction() {
@Override
public void actionPerformed(ActionEvent e) {
setZoom(getZoom() + 0.1f);
}
});
am.put("minus", new AbstractAction() {
@Override
public void actionPerformed(ActionEvent e) {
setZoom(getZoom() - 0.1f);
}
});
setFocusable(true);
requestFocusInWindow();
}
@Override
public void addNotify() {
super.addNotify();
}
public float getZoom() {
return zoom;
}
public void setZoom(float value) {
if (zoom != value) {
zoom = value;
if (zoom < 0) {
zoom = 0f;
}
int width = (int) Math.floor(background.getWidth(this) * zoom);
int height = (int) Math.floor(background.getHeight(this) * zoom);
scaled = background.getScaledInstance(width, height, Image.SCALE_SMOOTH);
scaledSize = new Dimension(width, height);
if (getParent() instanceof JViewport) {
int centerX = width / 2;
int centerY = height / 2;
JViewport parent = (JViewport) getParent();
Rectangle viewRect = parent.getViewRect();
viewRect.x = centerX - (viewRect.width / 2);
viewRect.y = centerY - (viewRect.height / 2);
scrollRectToVisible(viewRect);
}
invalidate();
repaint();
}
}
@Override
public Dimension getPreferredSize() {
return scaledSize;
}
@Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
if (scaled != null) {
g.drawImage(scaled, 0, 0, this);
}
}
protected void centerInViewport() {
Container container = getParent();
if (container instanceof JViewport) {
JViewport port = (JViewport) container;
Rectangle viewRect = port.getViewRect();
int width = getWidth();
int height = getHeight();
viewRect.x = (width - viewRect.width) / 2;
viewRect.y = (height - viewRect.height) / 2;
scrollRectToVisible(viewRect);
}
}
}
}
As to why yours doesn't work, I can't say, I can't run the example, but maybe this will at least give you some ideas...
Solved it. Yay. Still not sure where it actually went wrong, but moving the original rectangle (thanks @MadProgrammer) rather than creating a new one and correct rounding in the scaler may have done the trick.
private Point getViewportCenter() {
JViewport vp = (JViewport) this.getParent();
Point p = vp.getViewPosition();
return new Point(p.x+vp.getWidth()/2,p.y+vp.getHeight()/2);
}
private void setViewportCenter(Point p) {
JViewport vp = (JViewport) this.getParent();
Rectangle viewRect = vp.getViewRect();
viewRect.x = p.x - viewRect.width / 2;
viewRect.y = p.y - viewRect.height / 2;
scrollRectToVisible(viewRect);
}
public void setZoom(double zoom) {
// determine unscaled center and dimensions
Point oCenter = scaler.inverseTransform(getViewportCenter());
Dimension dim = railMap.getDimension();
// zoom
scaler.setZoom(zoom);
// fix size and viewport
setPreferredSize(scaler.transform(dim));
setViewportCenter(scaler.transform(oCenter)); // should be a transformed point
// finish
invalidate();
repaint();
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With