Wednesday, December 20, 2006

componentMoved and componentResized on JFrame

While trying to store the state of an applications I found some oddity's when you add a ComponentListener to a JFrame like so:



frame.addComponentListener(new ComponentListener() {
public void componentResized(ComponentEvent e) {
// do something like save the size in preferences
System.out.println("RESIZED");

}

public void componentMoved(ComponentEvent e) {
// do something like save the location in preferences
System.out.println("MOVED");
}

public void componentShown(ComponentEvent e) {

}

public void componentHidden(ComponentEvent e) {

}
});


Now if you resize the frame, you will see RESIZE printed to the console only a single time after you release the mouse. If you move the frame, you will see MOVE printed repeatedly, many times over. Move gets fired as you are dragging. So the problem here is that you may only want to perform the action after the move is completed. You might think that you could add a MouseListener to the frame and catch the mouseReleased event, but the mouse events are not fired on the window's title bar unfortunately so that does not work.

A Swing developer asked about this in an Ask the Experts Transcript, and I quote the response from the AWT Technical Lead, Oleg Sukhodolsky:


This is not a bug. Such behavior was introduced to compensate for some
performance problems we had in the past. Every resize event for a top level
causes relay outing. This operation could be very expensive. The good news for
you is that this is controllable behavior -- you can either use the
Toolkit.setDynamicLayout() method or set the awt.dynamicLayoutSupported Java
system property to true.
Soooo, how do you get around this? Well one idea is to use a javax.swing.Timer to make the action occur every couple of seconds while you are dragging and it will only fire once after you've stopped dragging.


public class WindowPrefsListener implements ComponentListener {
private Timer timer;
private JFrame frame;
private int counter;
private static final int DELAY = 2000;

public WindowPrefsListener(JFrame frame) {
this.frame = frame;
timer = new Timer(DELAY, new AbstractAction() {
public void actionPerformed(ActionEvent e) {
System.out.println("timer action " + counter++ + "!");
savePrefs();
}
});
timer.setRepeats(false);
}

private void savePrefs() {
Prefs.saveSize(frame);
Prefs.saveLocation(frame);

}

/**
* compoment resized actually works normally with a single event after the mouse is released,
* but supposedly it might behave differently on different platforms.
*
* @param e
*/
public void componentResized(ComponentEvent e) {
timer.start();

}

public void componentMoved(ComponentEvent e) {
timer.start();
}

public void componentShown(ComponentEvent e) {

}

public void componentHidden(ComponentEvent e) {

}
}
And that's that. Try it out, you'll see that the event gets fired at most once every two seconds (since the DELAY is set to two seconds).

No comments: