javaswinglook-and-feeluimanageruidefaults

Changing Look And Feel by Swapping UIDefaults


I am writing a GUI Builder and want the user to be able to change the LookAndFeel of the GUI he builds. The LookAndFeel should only be changed for the Components inside the editor area. The rest of the Application should remain with the SystemLookAndFeel.

The great problem is, that the LookAndFeel is implemented as a Singleton and changing the LookAndFeel multiple times during the Application causes a lot of bugs.

I started experimenting with Buttons: I tried setting the ButtonUI to MetalButtonUI, but they didn't render properly. So I debugged the default paintComponent method of JButton and saw that the ButtonUI still needed the UIDefaults, which were not complete since they were the WindowsUIDefaults.

My current solution is to set the MetalLookAndFeel, save the UIDefaults, then change the LookAndFeel to SystemLookAndFeel and save those UIDefaults aswell and everytime I draw a Button inside the editor I swap the UIDefaults.

Here is the Code:

public class MainClass{
    public static Hashtable systemUI;
    public static Hashtable metalUI;

    public static void main(String[] args) {
         EventQueue.invokeLater(new Runnable() {
             public void run() {
                 try {
                    UIManager.setLookAndFeel(UIManager.getCrossPlatformLookAndFeelClassName());
                    metalUI = new Hashtable();
                    metalUI.putAll(UIManager.getDefaults());

                    UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
                    systemUI = new Hashtable();
                    systemUI.putAll(UIManager.getDefaults());
                } catch (Exception e) {
                    e.printStackTrace();
                }
                /* ...
                 * Generate JFrame and other stuff
                 * ...
                 */
            }
        });
    }
}

public class MyButton extends JButton {
    public MyButton(String text) {
        super(text);
        ui = MetalButtonUI.createUI(this);
    }

    @Override public synchronized void paintComponent(Graphics g) {
        UIManager.getDefaults().putAll(Application.metalUI);

        super.paintComponent(g);

        UIManager.getDefaults().putAll(Application.systemUI);
    }
}

As you can see here the result is pretty good. On the left is the MetalLaF as it should look and on the right, how it gets rendered in my application. The gradient is painted correctly, but the Border and the Font aren't.

So I need to know why not all elements of the LaF are beeing applied to the Button and how to fix that.

-

Edit: I found an ugly solution. The LookAndFeel has to be changed before Button creation, because the Graphics object will be created in the Constructor. After the super constructor was called you can change the LookAndFeel back.

Next you need to change the LookAndFeel before the Component is painted/repainted. The only point I got it working was in paintComponent before super is called. You can change it back after super is called.

Code:

import javax.swing.*;
import javax.swing.plaf.metal.MetalButtonUI;
import java.awt.*;

public class MainClass {

    public static void main(String[] args) {
        EventQueue.invokeLater(new Runnable() {
            public void run() {
                try {
                    UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
                } catch (Exception e) {
                    e.printStackTrace();
                }

                JFrame f = new JFrame("Test");
                f.setDefaultCloseOperation(f.getExtendedState() | JFrame.EXIT_ON_CLOSE);

                try {
                    UIManager.setLookAndFeel(UIManager.getCrossPlatformLookAndFeelClassName());
                } catch (Exception ex) {
                }
                f.setLayout(new FlowLayout());

                f.add(new MyButton("MetalButton"));
                f.add(new JButton("SystemButton"));

                f.pack();
                f.setVisible(true);
            }
        });
    }
}

class MyButton extends JButton {
    public MyButton(String text) {
        super(text);
        try {
            UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
        } catch (Exception e) {
        }
        ui = MetalButtonUI.createUI(this);
    }

    @Override public synchronized void paintComponent(Graphics g) {
        try {
            UIManager.setLookAndFeel(UIManager.getCrossPlatformLookAndFeelClassName());

            super.paintComponent(g);

            UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
        } catch (Exception e) {
        }
    }
}

Edit 2: Do not use this unless absolutely necessery!

This is extremely unstable. After a lot of testing I found it less buggy when I just swapped the UIDefaults instead of the whole LookAndFeel, but I do not recommend doing any of those.

Edit 3: The best solution I found was using JavaFX as a GUI. I inserted a swing node into the Editor area and now can modify the Look and Feel of the swing components as often as I want without any noticeable side effects.

Rant: If you can always choose JavaFX if you want to modify the style of your application. CSS makes it as easy as possible without any side effects ever!


Much Thanks

Jhonny


Solution

  • Disclarimer

    Swing's Look And Feel isn't designed to be switched after it's first initalised, it's actually a kind of fluky side effect that it's possible. Some look and feels and some components might not like you doing this and may not behave as they might other wise under normal conditions.

    Possible solution

    For the love of sanity, DON'T change the UI defaults in the paintComponent method (don't change the state of the UI at all from within any paint method EVER, painting paints the current state only), that's just asking for no end of trouble.

    Instead, when required use UIManager.setLookAndFeel(,,,) and SwingUtiltiies#updateComponentTreeUI and pass in the most top level container

    For example...

    I have a bad feeling about this

    import java.awt.Component;
    import java.awt.EventQueue;
    import java.awt.GridBagConstraints;
    import java.awt.GridBagLayout;
    import java.awt.Insets;
    import java.awt.event.ActionEvent;
    import java.awt.event.ActionListener;
    import javax.swing.DefaultComboBoxModel;
    import javax.swing.DefaultListCellRenderer;
    import javax.swing.JComboBox;
    import javax.swing.JFrame;
    import javax.swing.JLabel;
    import javax.swing.JList;
    import javax.swing.JPanel;
    import javax.swing.JTextField;
    import javax.swing.SwingUtilities;
    import javax.swing.UIManager;
    import javax.swing.UnsupportedLookAndFeelException;
    
    public class LookAndFeelSwitcher {
    
        public static void main(String[] args) {
            new LookAndFeelSwitcher();
        }
    
        public LookAndFeelSwitcher() {
            EventQueue.invokeLater(new Runnable() {
                @Override
                public void run() {
                    try {
                        UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
                    } catch (ClassNotFoundException | InstantiationException | IllegalAccessException | UnsupportedLookAndFeelException ex) {
                        ex.printStackTrace();
                    }
    
                    JFrame frame = new JFrame("Testing");
                    frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
                    frame.add(new TestPane());
                    frame.pack();
                    frame.setLocationRelativeTo(null);
                    frame.setVisible(true);
                }
            });
        }
    
        public class TestPane extends JPanel {
    
            public TestPane() {
                setLayout(new GridBagLayout());
                GridBagConstraints gbc = new GridBagConstraints();
                gbc.gridx = 0;
                gbc.gridwidth = GridBagConstraints.REMAINDER;
                gbc.fill = GridBagConstraints.HORIZONTAL;
                gbc.insets = new Insets(2, 2, 2, 2);
    
                add(new JLabel("I have a bad feeling about this"), gbc);
                add(new JTextField("When this blows up in your face, don't blame me"), gbc);
    
                UIManager.LookAndFeelInfo[] lafs = UIManager.getInstalledLookAndFeels();
                DefaultComboBoxModel model = new DefaultComboBoxModel(lafs);
                JComboBox cb = new JComboBox(model);
                cb.setRenderer(new LookAndFeelInfoListCellRenderer());
                add(cb, gbc);
    
                String name = UIManager.getLookAndFeel().getName();
                for (int index = 0; index < model.getSize(); index++) {
                    UIManager.LookAndFeelInfo info = (UIManager.LookAndFeelInfo) model.getElementAt(index);
                    if (info.getName().equals(name)) {
                        model.setSelectedItem(info);
                        break;
                    }
                }
    
                cb.addActionListener(new ActionListener() {
                    @Override
                    public void actionPerformed(ActionEvent e) {
                        UIManager.LookAndFeelInfo info = (UIManager.LookAndFeelInfo) cb.getSelectedItem();
                        String className = info.getClassName();
                        try {
                            UIManager.setLookAndFeel(className);
                            SwingUtilities.updateComponentTreeUI(SwingUtilities.windowForComponent(TestPane.this));
                        } catch (ClassNotFoundException | InstantiationException | IllegalAccessException | UnsupportedLookAndFeelException ex) {
                            ex.printStackTrace();
                        }
                    }
                });
            }
    
            public class LookAndFeelInfoListCellRenderer extends DefaultListCellRenderer {
    
                @Override
                public Component getListCellRendererComponent(JList<?> list, Object value, int index, boolean isSelected, boolean cellHasFocus) {
                    if (value instanceof UIManager.LookAndFeelInfo) {
                        UIManager.LookAndFeelInfo info = (UIManager.LookAndFeelInfo) value;
                        value = info.getName();
                    }
                    return super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus);
                }
    
            }
    
        }
    
    }