3

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

Jhonny007
  • 1,698
  • 1
  • 13
  • 33
  • If I get this right you want to use multiple LAF in your Swing application? There's already a few questions on SO about this, such as [this one](http://stackoverflow.com/questions/446056/). – Laf Nov 09 '15 at 20:43
  • I already found that thread, but all links there are down. My research on substance revealed, that Substance is an external LookAndFeel, that ONLY enables you to switch between Substance LookAndFeels. – Jhonny007 Nov 09 '15 at 21:22
  • Just so you know, Swing wasn't actually meant to change the look and feel dynamically, it's kind of a fluky side effect which may not always work. Oh and for the love of sanity, DON'T change the UI defaults in the `paintComponent` method, that's just asking for no end of trouble. Instead, when required use `UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());` and `SwingUtiltiies#updateComponentTreeUI` and pass in the most top level container – MadProgrammer Nov 09 '15 at 22:11
  • Yeah I know that... doesn't change the fact that I need to get a workaround running. – Jhonny007 Nov 09 '15 at 22:12

1 Answers1

4

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);
            }

        }

    }

}
Community
  • 1
  • 1
MadProgrammer
  • 343,457
  • 22
  • 230
  • 366
  • The problem is I need the look and feel change on EVERY repaint. But ONLY for components in my editor area. The only result where I could achieve that was inside the paintComponent method. – Jhonny007 Nov 09 '15 at 22:44
  • A solution might be to launch the "preview" in a separate JVM, in which you can isolate and change the look and feel without effecting any other components. A look and feel change is global by default, that's how it's suppose to work – MadProgrammer Nov 09 '15 at 22:49
  • Sadly the client insists on having the rendering live. But I just found a solution. It is ugly as hell put it works -.- – Jhonny007 Nov 09 '15 at 23:04
  • @Jhonny007 There comes a point where you need to explain to the client that the impossible is impossible. The API is limited in it's capabilities by designs and choice which are beyond your control and the client should accept that sometimes, their dream isn't possible in the form that they want it, but can be accomplished in different ways, which won't explode in a horrible mess if you look at it side ways – MadProgrammer Nov 09 '15 at 23:06