Skip to main content

JXLayer 3.0 - Painting implementation

Posted by alexfromsun on July 17, 2008 at 4:26 AM PDT

The JXLayer's functionality consists of two parts: painting issues and input event processing.

In this entry I'll describe painting in details, the second part will come shortly.

Painting issues

With the JXLayer you can easily customize the visual appearance of your component and the most trivial example is painting on top of a component. However you should know some details to implement it properly.

Imagine that you need to implement a custom panel with a red line goes from one corner to another, like this:

demo screenshot

Since the panel may contain child components we need to make sure that this line will paint over them. For this reason, overridden JComponent.paintComponent() will not work for us, because it is called before child components are painted. The only solution in this case is to override JComponent.paint() method and do our custom painting after the entire component is painted.

class MyPanel extends JPanel {

    // Note that we override paint(), not paintComponent()
    // because we want to paint over panel's children  
    public void paint(Graphics g) {
        super.paint(g);
        g.setColor(Color.RED);
        g.drawLine(0,getHeight(), getWidth(), 0);
    }
}

Let's put some components inside this panel and have a look:

demo screenshot

Wow, it seems to work! But...

It works only when the whole panel is repainted, if you move the mouse over the buttons and type something in the textField, you'll see this picture:

demo screenshot

Here we found the main problem that prevents us from decorating the Swing containers:


When a component is repainted, the paint() method from its parent doesn't get called.


There are three known solutions, how to make the container repaint when any of its children are repainted:

  1. Make every child component not opaque.
  2. Implement and set a custom global RepaintManager.
  3. Put a transparent panel on top of our container.

All three method do the same thing - they repaint a part of a container when its child component is repainted, therefore none of them is visibly faster then the others. For JXLayer implementation I examined all of them:

First method is not the best, because a component may look differently depending on its opaque state. JLabel is a good example - by default, the component is translucent, but if you set its opaque property to true, you'll see the solid background. If a component is opaque, we can't predict how it will look like if we make it not opaque, that's why we can't arbitrarily change this state.

Custom RepaintManager doesn't change the state of the components and generally works well, for example SwingX library substitues the system RepaintManager with their own RepaintManagerX. However, RepaintManager is a global resource and your own implementation will work well only if no other component tries to set another one. I wanted JXLayer to be compatible with libraries like SwingX and that's why I couldn't use my own RepaintManager.

I found the transparent panel approach the best choice, because it doesn't affect component's state nor any global settings. I only needed to introduce a transparent panel which covers the whole layer, it's not a problem because I have full control of the hierarchy of JXLayer. This transparent panel called glassPane and you can get it by JXLayer.getGlassPane(). Note that it has nothing to do (except the name) with the glassPane from JFrame.


When a component is covered by another transparent component, they are always repainted together and the painting starts from their common ancestor, in our case this is JXLayer component.


Swing is smart enough to not paint more than necessary. If a JXLayer child is repainted, it doesn't mean that the whole JXLayer is repainted - painting happens only within the bounds of the child component.

Conclusion

The JXLayer's glassPane guarantees that layer's paint() method will be notified when any of its child components are repainted. It allows us to implement LayerUIs that paint over the layer:

class MyLayerUI extends AbstractLayerUI {

    // Override paintLayer(), not paint()
    protected void paintLayer(Graphics2D g2, JXLayer l) {
       
        // You can paint before the layer
        // it makes sense if you make the layer not opaque
        // in the installUI() method of this LayerUI
       
        super.paintLayer(g2, l);
       
        // You can paint after the layer
        // and your painting will always cover all layer's child component
    }
}

You can even paint the layer's content more than once!

In this demo when you move the mouse over the button, the animation is started and the layer is painted as is, then the scaling transformation is applied and it is painted once again. FlashButtonDemo is added to the jxlayer's demo package.

See you on the JXLayer forum



Thanks

alexp

Related Topics >>

Comments

That is exactly the problem I found. No need to apologize, I take critism very well, just another opportunity to learn :-) I put the wrapper component because a lot of people don't like to write a few extra lines of code. I posted the test case at the forum.

Hello Navin I can see the problem now, thanks ! I run your test case, quickly click another application and when the layer is locked the locked visual effect doesn't appear, it shows up only if you click the test application Is it the problem you found? I did that statement about DisableComponent because in my opinion you can do well without it. JXLayer is wrapper for a component and this class is one more wrapper for another one (two more wrappers for me). Anyway I am sorry for being too critical. I'll look how to solve this problem shortly, meanwhile could you please post this test case to JXLayer forum? http://forums.java.net/jive/forum.jspa?forumID=140 It is more convenient to communicate there Thank you Navin See you

Alex, I am guessing you didn't like calls to invalidate etc. or is it something else ? I took your code as an example and found a situation where it break. Also in the code below if you comment timer.start() and uncomment action listener for the button it works. Not sure why. package testdisable; import org.jdesktop.jxlayer.plaf.ext.LockableUI; import org.jdesktop.jxlayer.JXLayer; import java.awt.*; import java.awt.event.ActionListener; import java.awt.event.ActionEvent; import javax.swing.*; /** * @author Navin Jha */ public class TestDisabler { public static void main(String[] args) { SwingUtilities.invokeLater(new Runnable() { public void run() { final JFrame f = new JFrame(); JPanel panel = new JPanel(new GridLayout(1, 1)); JButton button = new JButton("test"); panel.add(button); final JButton lockUnlockButton = new JButton("lock"); final LockableUI ui = new LockableUI() { @Override public void paint(Graphics g, JComponent c) { super.paint(g, c); if (isLocked()) { g.setColor(new Color(128, 128, 128, 128)); g.fillRect(0, 0, c.getWidth(), c.getHeight()); } } }; JXLayer layer = new JXLayer(button, ui); f.add(layer); final Timer timer = new Timer(5000, new ActionListener() { public void actionPerformed(ActionEvent e) { ui.setLocked(true); lockUnlockButton.setText("Locked Now!"); } }); f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); f.add(lockUnlockButton, BorderLayout.SOUTH); f.pack(); // comment this and uncomment the add action listener and it works timer.start(); /* lockUnlockButton.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { timer.start(); } });*/ f.setVisible(true); } }); } }

Hello Navin Here is my test which works fine for me, click the lower button, switch to another application and see the big button is locked in 5 second import org.jdesktop.jxlayer.JXLayer; import org.jdesktop.jxlayer.plaf.ext.LockableUI; import javax.swing.*; import java.awt.event.ActionListener; import java.awt.event.ActionEvent; import java.awt.*; public class MyTest { private static void createGui() { final JFrame frame = new JFrame(); frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); final LockableUI ui = new LockableUI() { @Override public void paint(Graphics g, JComponent c) { super.paint(g, c); if (isLocked()) { g.setColor(new Color(128, 128, 128, 128)); g.fillRect(0, 0, c.getWidth(), c.getHeight()); } } }; JXLayer layer = new JXLayer(new JButton("Lockable"), ui); frame.add(layer); final JButton button = new JButton("Lock in 5 sec"); frame.add(button, BorderLayout.SOUTH); final Timer timer = new Timer(5000, new ActionListener() { public void actionPerformed(ActionEvent e) { ui.setLocked(true); button.setText("Locked!"); } }); button.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { timer.start(); } }); frame.setSize(200, 200); frame.setLocationRelativeTo(null); frame.setVisible(true); } public static void main(String[] args) throws Exception { SwingUtilities.invokeLater(new Runnable() { public void run() { MyTest.createGui(); } }); } } I didn't dig up your code to see where the problem is, to tell the truth this DisableComponent class hurts me Thanks alexp

Alex, I finally was able to built a simple test case, I am simply pasting the two files. Once the test window comes up just focus on other applications and make sure the test application is not overlapping any other application. package testdisable; // standard packages import java.awt.Cursor; import java.awt.Color; import java.awt.Container; import java.awt.Graphics2D; import javax.swing.JComponent; // third party packages import org.jdesktop.jxlayer.JXLayer; import org.jdesktop.jxlayer.plaf.ext.LockableUI; /** * A Wrapper that allows a JComponent to be wrapped in a JXLayer so that the JComponent * and all it's children can be enabled/disabled. * * @author Navin Jha */ public class DisableComponent { // wrapper jxlayer private final JXLayer layer; private final JComponent component; // locking componets private final LockableUI lockableUI; /** * @param component that need to be enabled for enable/disable of itself and it's * child components. */ public DisableComponent(JComponent component) { lockableUI = new TranslucentLayer(); layer = new JXLayer(component, lockableUI); this.component = component; // by default use default cursor, locked layer uses busy cursor by default lockableUI.setLockedCursor(Cursor.getDefaultCursor()); } /** * @param enable - if true, enable the component else disable the component. */ public void setEnabled(boolean enable) { lockableUI.setLocked(!enable); if (!enable) { component.invalidate(); Container container = component.getParent(); if (container != null) container.validate(); else component.validate(); } } /** * @return The JXLayer that needs to be added instead of component passed as parameter. */ public JXLayer getJXLayer() { return layer; } // create custom LayerUI private class TranslucentLayer extends LockableUI { @Override protected void paintLayer(Graphics2D g2, JXLayer l) { // this paints layer as is super.paintLayer(g2, l); // custom painting: // here we paint translucent foreground // over the whole layer g2.setColor(new Color(135, 206, 250, 75)); g2.fillRect(0, 0, l.getWidth(), l.getHeight()); } } } --------------------------------------------------------------------------------- package testdisable; import java.awt.*; import java.awt.event.ActionListener; import java.awt.event.ActionEvent; import javax.swing.*; /** * @author Navin Jha */ public class TestDisabler { public static void main(String[] args) { try { UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName()); } catch (Exception e) { System.out.println("error setting l &f " + e); } JFrame f = new JFrame(); JPanel panel = new JPanel(new GridLayout(5, 1)); JButton button = new JButton("test"); JLabel titleLabel = new JLabel("test some background"); JTextField text = new JTextField("text"); JSpinner spinner = new JSpinner(new SpinnerNumberModel()); JComboBox combo = new JComboBox(new String[]{"Bravo", "Alpha", "Charlie", "Chuck", "Delta", "Alpho"}); panel.add(titleLabel); panel.add(spinner); panel.add(button); panel.add(text); panel.add(combo); final DisableComponent d = new DisableComponent(panel); f.add(d.getJXLayer(), BorderLayout.CENTER); final JButton lockUnlockButton = new JButton("lock/unlock"); lockUnlockButton.addActionListener(new ActionListener() { private boolean flag = true; public void actionPerformed(ActionEvent e) { d.setEnabled(flag); flag = flag ? false : true; } }); Timer timer = new Timer(10000, new ActionListener() { public void actionPerformed(ActionEvent e) { d.setEnabled(false); lockUnlockButton.setText("Lock Now!"); } }); f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); f.add(lockUnlockButton, BorderLayout.SOUTH); f.pack(); timer.start(); f.setVisible(true); } }

One more thing: I put a print statement inside in the above paintLayer(...) method and In case when lock effect doesn't take place, the statement is not printed so that means it is not getting called in that case. I click on the window header and paintLayer(...) does get called and everything happens as desired.

something I should have mentioned: I have custom layer which I copied your blog I guess: private class TranslucentLayer extends LockableUI { @Override protected void paintLayer(Graphics2D g2, JXLayer l) { // this paints layer as is super.paintLayer(g2, l); // custom painting: // here we paint translucent foreground // over the whole layer g2.setColor(new Color(135, 206, 250, 75)); g2.fillRect(0, 0, l.getWidth(), l.getHeight()); } } Is it possible that the getWdith() and getHeight() return zero for some reason and then then when I click on the window has non zero values ?

Alex, I need extract code from my application for. Here is what is happening. I am clicking at other application windows and at some point an event happens that is suppose to lock the window. I can see thevent being logged in the log file but the window not being in the locked state. The moment I click on it gets locked. I even tried something like this: lockableUI.setLocked(!enable); if (!enable) { component.invalidate(); Container container = component.getParent(); if (container != null) container.validate(); else component.validate(); } where the "component" is the component being wrapped. I will try to get the test case shortly. Thanks.

Hello Navin Could you provide a small runnable test case for this problem? (I quickly created one, it works well for me) Thanks alexp

I said it too soon. Even in GUI thread setLocked(true) doesn't work if the window is not focussed. setLocked(false) does work though. -Navin

Ignore my question, it was my fault. It was being executed in a listener that I assumed to be a gui thread and it was not. I sincerely apologize. -Navin

Alex, When window is not focussed it seems that the setLocked(..) method for LockableUI does not paint. If you click on the window it immediately paints. Any way to show the locked effect even when window is not focussed? This is very useful when user has mutiple application running he needs to be alerted of something and the window is not focussed. -Navin

Hello polygoncell When JTextField gets the focus, the mouse cursor starts blinking, it leads to the textField's repaint and that's why paintlayer() is also called there is no problem as for SwingAppFramework - I prefer to discuss it on the dedicated alias, see https://appframework.dev.java.net Thanks alexp

Hello Alexp thanks for your nice comments on my blog. I really appreciate it! I have posted a new comment. Please take a look, thanks. Another issue: I just found something strange in the example TextValidationDemo, IconValidationUI: if the JTextField gets the focus, the paintlayer() method will be called again and again without an end. It hapens only with the IconValidationUI. Is that okay? BTW: I heard that you got the Swing app framework under your control. That is a great news! Thanks for your choice. I am using this framework heavily in my project and really like to see the rebirth of the framework. What I am actually waiting for are the MultiFrameApplication and the TrayApplication(based on the TrayIcon from java 6. Yes I have read Hans Mueler's last email). The SingleFrameApplication is good but not suitable for some a little bit complicated Swing applications. best regards polygoncell

Hello Polygoncell

Thank for your blog, I appreciate it I'll post my comment there

alexp

Hello Alexp, The JXLayer is great! I really like it. I have write a blog for using the JXLayer and the hibernate validator for building a common validation module for Swing application. Would you like to take a look? Maybe it will help some people who want to use the JXLayer for validation. here is the blog: http://polygoncell.blogspot.com/2008/07/validation-module-for-swing-appl...

Hello Jmborer
The purpose of isOptimizedPaintingEnabled() is to enable correct painting of overlapped components
e.g. JRootPane has it overridden to to return false when its glassPane is visible,
JXLayer does the same

Thanks
alexp

Hmm. Interesting point. We had that kind of challenge: when we added a translucent panel (which means opaque), we encountered the repainting issue. Another way to fix that is to override method isOptimizedPaintingEnabled(false) (or something like that) on the parent component.

Hello Polygoncell I had a look and posted a comment Thanks alexp

Hello Alexp, After getting the suggustion from you, I have post an update blog. Would you like to take a look? Thanks. here is it: http://polygoncell.blogspot.com/2008/07/update-common-validation-module-... regards Polygoncell