Java Forum / GUI / February 2004
Zooming/Scaling a JPanel
Jonathan Fuerth - 25 Feb 2004 23:03 GMT 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.
Brian Pipa - 26 Feb 2004 03:00 GMT <SNIP>
> 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: <SNIP>
Take a look at the source for the TouchGraph library at http://www.touchgraph.com. It solved this very problem and it works great. Look at the zoom code (I can't think of which class that is offhand). I recently used TG for a rather detailed project and had to make a bunch of mods to it to make it work exactly like I wanted. If you can't find the exact class, email me and I'll find it and let you know.
Brian
 Signature --- MP3 Automagic CD Cover Creator http://maccc.filenabber.com
Bjørn Børresen - 26 Feb 2004 08:26 GMT Hi Jonathan!
How are the draggable / selectable objects implemented?
I've created a similar application (with scaling) using a JInternalFrame as the 'document' and the Composite pattern (see javaworld.com, or the Gang of Four book) for the objects that are displayed on the document. That way I can just adjust the size of the JInternalPane (and the the white background), and then pass the scale to each and one of the objects when painting. When the user clicks on the document, the object list is traversed to find the object at that scaled location.
- Bjørn
> I've made a Swing-based application for designing relational database > schemas, where the majority of screen area is devoted to a "play pen" [quoted text clipped - 243 lines] > redispatching events with the Glass Pane, email me and I will send you > the (broken) code.
 Signature Using M2, Opera's revolutionary e-mail client: http://www.opera.com/m2/
Jonathan Fuerth - 26 Feb 2004 21:15 GMT > How are the draggable / selectable objects implemented? They are direct JComponent subclasses. They subscribe to themselves as MouseXXXListeners, then reposition themselves in response to mouseDragged and mouseReleased events. Similarly, they select themselves (and fire an event) on mousePressed events. The play pen to which they belong receives the selection events and deselects other play pen components depending on the state of the modifier keys (to allow multiple selection similar to that in JTree and JList).
> I've created a similar application (with scaling) using a JInternalFrame as > the 'document' and the Composite pattern (see javaworld.com, or the Gang of [quoted text clipped - 3 lines] > painting. When the user clicks on the document, the object list is > traversed to find the object at that scaled location. Am I right in believing this is similar to my proposed solution #4 (quoted below), or does your JInternalFrame contain only other JInternalFrames as children? I've never tried that; is it possible?
What I'm really hoping for is a generally useful subclass of JPanel that can scale all of its contents (an arbitrary collection of JComponents that I didn't necessarily write) to any size without their knowledge. It feels like it should be possible, but I'm starting to believe it's not.
Possibly another symptom of the same deficiency in Swing which aborted my attempt at solution #3 (see below) is that it doesn't work to call paint(g) on a JComponent from within the paint method of a JComponent which is not its parent. That is to say, the painting implementation of JComponent seems to rely on more than the Graphics you give it. English is failing me. I mean:
// BrokenMirror.java import javax.swing.*; import java.awt.*;
/** * Displays a non-interactive mirror of another JComponent. * Doesn't quite work. */ public class BrokenMirror extends JPanel { protected JComponent subject;
public BrokenMirror(JComponent subject) { this.subject = subject; }
public Dimension getPreferredSize() { return subject.getPreferredSize(); }
public void paintComponent(Graphics g) { super.paintComponent(g); subject.paint(g); }
public static void main(String args[]) { final JFrame subjectFrame = new JFrame("Subject"); JPanel myPane = new JPanel(new BorderLayout()); myPane.add(new JButton("North"), BorderLayout.NORTH); myPane.add(new JButton("Center"), BorderLayout.CENTER); myPane.add(new JButton("South"), BorderLayout.SOUTH); subjectFrame.setContentPane(new JScrollPane(myPane)); subjectFrame.pack(); subjectFrame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
BrokenMirror mirror = new BrokenMirror ((JComponent) subjectFrame.getContentPane()); final JFrame mirrorFrame = new JFrame("Mirror"); mirrorFrame.setContentPane(mirror); mirrorFrame.setLocation(subjectFrame.getWidth(), 0); mirrorFrame.pack(); mirrorFrame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); SwingUtilities.invokeLater(new Runnable() { public void run() { subjectFrame.setVisible(true); mirrorFrame.setVisible(true); } }); } }
This example breaks differently depending on which type of components are being mirrored, and actually works pretty well if you aren't using a JScrollPane. You may need to move and resize each of the frames a few times to see the problem, which is that the mirror frame shows a (mostly) grey rectangle rather than a replica of the subject frame. The glitch is immediately apparent on JDK 1.3.1 linux, and takes some abuse before breaking on JDK 1.4.1_03 windows.
This example isn't totally bogus; it could be extended to a nice print preview panel if it worked, and supported scaling and a different colour model (greyscale or CMYK). Am I overlooking something obvious that would make it work?
-Jonathan Fuerth
--
Below is my current best candidate for a solution to my problem (#4) from the original post:
> > 3. Instead of containing the table and relationship components as > > children, modify the play pen to hide them from swing and call [quoted text clipped - 8 lines] > > extend ComponentUI. (I'm starting to think this is the only way to > > go...) Bjørn Børresen - 27 Feb 2004 08:21 GMT >> How are the draggable / selectable objects implemented? > [quoted text clipped - 5 lines] > play pen components depending on the state of the modifier keys (to > allow multiple selection similar to that in JTree and JList). Ok, I have done this a bit different.
I have a CompositeView object which extends JComponent and contains a list of objects. In it's paintComponent() method I just draw a filled white rectangle to create a document, and then traverse the list and paint each of the components in it.
Only the CompositeView component has a mouselistener. When a click / drag is performed, the X/Y is scaled and then the list of objects is traversed to find out if there are any objects at that position (if multiple exists, it will be chosen from their zOrder, which is basically just an int in the object).
The objects are all subclasses of an abstract class; CompositeComponent, which also extends JComponent. Each of the objects has a draw(g2d) method. Scaling is performed by CompositeView so it doesn't need to be done there.
> Am I right in believing this is similar to my proposed solution #4 > (quoted below), or does your JInternalFrame contain only other > JInternalFrames as children? I've never tried that; is it possible? No, I only have one JInternalFrame - which contains the CompositeView object. All my objects extends JComponent though ...
> What I'm really hoping for is a generally useful subclass of JPanel > that can scale all of its contents (an arbitrary collection of > JComponents that I didn't necessarily write) to any size without their > knowledge. It feels like it should be possible, but I'm starting to > believe it's not. I think this would be very hard. Many of the swing components does not scale well when just handing them a scaled g2d instance. Tweaking is often needed.
> Possibly another symptom of the same deficiency in Swing which aborted > my attempt at solution #3 (see below) is that it doesn't work to call > paint(g) on a JComponent from within the paint method of a JComponent > which is not its parent. That is to say, the painting implementation > of JComponent seems to rely on more than the Graphics you give it. > English is failing me. I mean: This shoudln't be a problem, I think. I pass my CompositeView object around to paint it in eg. full screen mode etc. Maybe you need to do a custom implementation of paintComponent() in all your objects? ..
Best regards, Bjørn Børresen ----------------- http://www.bie.no
Using M2, Opera's revolutionary e-mail client: http://www.opera.com/m2/
Jonathan Fuerth - 27 Feb 2004 20:24 GMT > Only the CompositeView component has a mouselistener. When a click / drag > is performed, the X/Y is scaled and then the list of objects is traversed > to find out if there are any objects at that position (if multiple exists, > it will be chosen from their zOrder, which is basically just an int in the > object). This makes sense. The objects in your CompisiteView don't need to know about the magnification of the view they belong to, and could even belong to several views at differing magnifications if you wanted. I like that.
> The objects are all subclasses of an abstract class; CompositeComponent, > which also extends JComponent. Each of the objects has a draw(g2d) method. > Scaling is performed by CompositeView so it doesn't need to be done there. Right. I already achieved that with my straight containment approach, but it was scaling and redispatching the events that killed me. Remember, JComponent extends Container, so JComponent follows the CompositeView pattern already. i.e. you can treat a JPanel with a tree of descendants as a single generic component.
Having said that, I think I will take your advice and follow this approach. It's how all the open source graph and structured drawing frameworks seem to do it. I will give up my dream of a generic scalable JPanel. :)
> > What I'm really hoping for is a generally useful subclass of JPanel > > that can scale all of its contents (an arbitrary collection of [quoted text clipped - 5 lines] > scale well when just handing them a scaled g2d instance. Tweaking is often > needed. I disagree here.. I haven't come across any Swing components that paint badly when scaled. The only unaesthetic results I've noticed are rectangles where the parallel edges paint with different widths (one pixel off due to rounding). This only happens at certain zoom factors, and can be mitigated by setting the interpolation hint in the Graphics2D. The text scales especially nicely since the fonts simply render as if you specifically asked for the larger/smaller size.
One thing that would look terrible when significantly enlarged would be an ImageIcon, but you could just avoid using them in scalable panels.
-Jonathan Fuerth
Free MagazinesGet these publications absolutely FREE for up to 12 months. There are no hidden fees and no obligation. Simply choose a title, complete the application form and submit it. Read more ...
|
|
|