Home | Contact Us | FAQ | Search & Site Map | Link to Us
Sign In | Join | Other 45 Sites in Network
HomeAnnouncementsWhite Papers
Discussion GroupsFirst AidDatabasesJavaBeansGUIJava 3DVirtual MachineCORBASecurityToolsGeneral
Java DirectoryOpen Source ProjectsSample Book ChaptersUser GroupsWeb Resources
Related Topics
Databases.NETMore Topics ...

Java Forum / GUI / February 2004

Tip: Looking for answers? Try searching our database.

Zooming/Scaling a JPanel

Thread view: 
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 Magazines

Get 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 ...

Oracle MagazineNetwork ComputingComputer WorldBio-IT WorldeWeekInformation WeekInfosecurity
 
Sign In
Join
My Latest Posts
My Monitored Threads
My Blog
My Photo Gallery
My Profile
My Homepage

Start New Thread
Enable EMail Alerts
Rate this Thread



©2008 Advenet LLC   Privacy Policy - Terms of Use
This website includes both content owned or controlled by Advenet as well as content owned or controlled by third parties.