Zooming/Scaling a JPanel
From: Jonathan Fuerth (jf.clj_at_bluecow.net)
Date: 02/26/04
- Next message: Andrew Harker: "Re: multiple listeners"
- Previous message: Brian Pipa: "Re: draw directed graph with nested panels"
- Next in thread: Brian Pipa: "Re: Zooming/Scaling a JPanel"
- Reply: Brian Pipa: "Re: Zooming/Scaling a JPanel"
- Reply: Bjørn Børresen: "Re: Zooming/Scaling a JPanel"
- Messages sorted by: [ date ] [ thread ] [ subject ] [ author ]
Date: 25 Feb 2004 15:03:32 -0800
I've made a Swing-based application for designing relational database
schemas, where the majority of screen area is devoted to a "play pen"
component which works much like a JDesktopPane: It contains draggable,
selectable rectangular components which represent the database's
tables. Unlike a JDesktopPane, it also contains non-draggable,
non-rectangular components that represent the relationships between
the tables. When you drag a table, its relationships follow it
around.
The "play pen" extends JPanel, and it contains the tables and
relationships as children, each of which extend JComponent directly.
The tables and relationships use the Swing UI delegate pattern so that
we can use the UIManager's PLAF to support user-preferred diagram
notations (currently, only Information Engineering notation is
implemented but IDEF1X will also be available before we release the
product).
All of the above works rather well, and it runs at useable speed on
our "base case" pentium 266 laptop even though Swing is supposed to be
slow. But I digress.
The problem I've been struggling with is how to make the play pen view
scale to the user's preferred size. The obvious solution is to
transform the graphics in the play pen's paint method:
/** paints the play pen and descendants at user-supplied zoom factor.
*/
public void paint(Graphics g) {
Graphics2D g2 = (Graphics2D) g;
AffineTransform backup = g2.getTransform();
g2.scale(zoom, zoom);
super.paint(g);
g2.setTransform(backup);
}
This actually works (with some additional tweaking to the play pen
class) until you (the app user) try to interact with the components
inside the play pen. Naturally, they are still at their normal,
unscaled positions and the Mouse*Events are delivered to them as such.
I can think of four ways to proceed, and I've already tried two of
them:
1. Put a GlassPane over the frame which contains the play pen (among
other things). Scale all the mouse events which occur over the play
pen, then dispatch them. Dispatch other events as-is. (tried this;
it's non-trivial)
2. Replace the system event queue with a custom one that modifies
mouse events which are over the play pen. (have not attempted this)
3. Instead of containing the table and relationship components as
children, modify the play pen to hide them from swing and call
paint(g) on each table and relationship directly. This way, all mouse
events will hit the play pen and it can dispatch them to the various
children on its own terms. (tried this; Swing components need to
belong to a visible parent to inherit foreground/background colours,
font, mysterious swing painting optimisation hints, etc, etc..)
4. Like method 3, but rewrite the table and relationship components so
they no longer extend JComponent, and their UI delegates no longer
extend ComponentUI. (I'm starting to think this is the only way to
go...)
Since this type of thing would be generally useful for (say) the
visually impaired, presenting Swing applications on LCD projectors,
and interactive print previews, I'm hoping someone has already solved
this and google has failed me. :)
Here's a SSCE of the general case: A JPanel that can scale whatever
you put in it. This one contains two JButtons and a TitledBorder, but
try it with other stuff. It almost works; there are some visual
problems I can explain and others I can't. The worst is that
FlowLayout apparently depends on its container's getPreferredSize()
method, which causes trouble when it centres the contents. This isn't
a problem in my real app because I'm using a custom layout manager.
See for yourself in the example.
I would love to recieve any advice on how I should proceed, and new
directions I haven't considered are especially welcome!
// ZoomScale.java
import java.awt.BorderLayout;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.FlowLayout;
import java.awt.Graphics2D;
import java.awt.Graphics;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.geom.AffineTransform;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.beans.PropertyVetoException;
import javax.swing.BorderFactory;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.SwingUtilities;
public class ZoomScale {
public static void main(String args[]) {
JPanel cp = new JPanel(new BorderLayout());
ZoomPanel zp = new ZoomPanel(1.0);
JButton zoomIn = new JButton("Zoom In");
JButton zoomOut = new JButton("Zoom Out");
zoomIn.addActionListener(new ZoomAction(zp, 0.4));
zoomOut.addActionListener(new ZoomAction(zp, -0.4));
zp.setBorder
(BorderFactory.createTitledBorder("This stuff zooms"));
zp.add(zoomIn);
zp.add(zoomOut);
cp.add(new JScrollPane(zp), BorderLayout.CENTER);
JPanel southPanel = new JPanel(new BorderLayout());
JPanel buttonPanel = new JPanel(new FlowLayout());
zoomIn = new JButton("Zoom In");
zoomOut = new JButton("Zoom Out");
zoomIn.addActionListener(new ZoomAction(zp, 0.4));
zoomOut.addActionListener(new ZoomAction(zp, -0.4));
buttonPanel.add(zoomIn);
buttonPanel.add(zoomOut);
JLabel readoutLabel = new JLabel("Zoom Factor 1.0");
new ReadoutUpdater(zp, readoutLabel);
southPanel.add(readoutLabel, BorderLayout.CENTER);
southPanel.add(buttonPanel, BorderLayout.SOUTH);
cp.add(southPanel, BorderLayout.SOUTH);
final JFrame frame = new JFrame("Zoom example");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setContentPane(cp);
SwingUtilities.invokeLater(new Runnable() {
public void run() {
frame.pack();
frame.setVisible(true);
}
});
}
public static class ZoomPanel extends JPanel {
protected double zoom;
public ZoomPanel(double initialZoom) {
super(new FlowLayout());
setName("Zoom Panel");
zoom = initialZoom;
}
public void paint(Graphics g) {
super.paintComponent(g); // clears background
Graphics2D g2 = (Graphics2D) g;
AffineTransform backup = g2.getTransform();
g2.scale(zoom, zoom);
super.paint(g);
g2.setTransform(backup);
}
public boolean isOptimizedDrawingEnabled() {
return false;
}
public Dimension getPreferredSize() {
Dimension unzoomed
= getLayout().preferredLayoutSize(this);
Dimension zoomed
= new Dimension((int) ((double) unzoomed.width*zoom),
(int) ((double) unzoomed.height*zoom));
System.out.println("PreferredSize: Unzoomed "+unzoomed);
System.out.println("PreferredSize: Zoomed "+zoomed);
return zoomed;
}
public void setZoom(double newZoom)
throws PropertyVetoException {
if (newZoom <= 0.0) {
throw new PropertyVetoException
("Zoom must be positive-valued",
new PropertyChangeEvent(this,
"zoom",
new Double(zoom),
new Double(newZoom)));
}
double oldZoom = zoom;
if (newZoom != oldZoom) {
Dimension oldSize = getPreferredSize();
zoom = newZoom;
Dimension newSize = getPreferredSize();
firePropertyChange("zoom", oldZoom, newZoom);
firePropertyChange("preferredSize",
oldSize, newSize);
revalidate();
repaint();
}
}
public double getZoom() {
return zoom;
}
}
public static class ZoomAction implements ActionListener {
protected double amount;
protected ZoomPanel zp;
public ZoomAction(ZoomPanel zp, double amount) {
this.amount = amount;
this.zp = zp;
}
public void actionPerformed(ActionEvent e) {
try {
zp.setZoom(zp.getZoom() + amount);
} catch (PropertyVetoException ex) {
JOptionPane.showMessageDialog
((Component) e.getSource(),
"Couldn't change zoom: "+ex.getMessage());
}
}
}
public static class ReadoutUpdater
implements PropertyChangeListener {
protected ZoomPanel zp;
protected JLabel label;
public ReadoutUpdater(ZoomPanel zp, JLabel label) {
this.zp = zp;
this.label = label;
zp.addPropertyChangeListener(this);
}
public void propertyChange(PropertyChangeEvent e) {
if ("zoom".equals(e.getPropertyName())) {
label.setText("Zoom Factor "+e.getNewValue());
}
}
}
}
Thanks everyone
-Jonathan Fuerth
PS: if you want to see my aborted attempt at catching and
redispatching events with the Glass Pane, email me and I will send you
the (broken) code.
- Next message: Andrew Harker: "Re: multiple listeners"
- Previous message: Brian Pipa: "Re: draw directed graph with nested panels"
- Next in thread: Brian Pipa: "Re: Zooming/Scaling a JPanel"
- Reply: Brian Pipa: "Re: Zooming/Scaling a JPanel"
- Reply: Bjørn Børresen: "Re: Zooming/Scaling a JPanel"
- Messages sorted by: [ date ] [ thread ] [ subject ] [ author ]