Изменение внешнего вида путем замены UIDefaults

Я пишу GUI Builder и хочу, чтобы пользователь мог изменить LookAndFeel графического интерфейса, который он создает. LookAndFeel следует изменять только для компонентов внутри области редактора. Остальная часть приложения должна оставаться с SystemLookAndFeel.

Большая проблема заключается в том, что LookAndFeel реализован как Singleton, и многократное изменение LookAndFeel во время приложения вызывает множество ошибок.

Я начал экспериментировать с кнопками: я попытался установить для ButtonUI значение MetalButtonUI, но они не отображались должным образом. Итак, я отладил метод paintComponent по умолчанию для JButton и увидел, что ButtonUI все еще нуждается в UIDefaults, которые не были полными, поскольку они были WindowsUIDefaults.

Мое текущее решение состоит в том, чтобы установить MetalLookAndFeel, сохранить UIDefaults, затем изменить LookAndFeel на SystemLookAndFeel и также сохранить эти UIDefaults, и каждый раз, когда я рисую кнопку внутри редактора, я меняю UIDefaults.

Вот код:

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

Как вы можете видеть здесь, результат довольно хороший. Слева показан MetalLaF, как он должен выглядеть, а справа — как он отображается в моем приложении. Градиент прорисован правильно, а Граница и Шрифт — нет.

Поэтому мне нужно знать, почему не все элементы LaF применяются к кнопке и как это исправить.

-

Изменить: я нашел некрасивое решение. LookAndFeel необходимо изменить перед созданием Button, потому что объект Graphics будет создан в Конструкторе. После вызова суперконструктора вы можете изменить LookAndFeel обратно.

Затем вам нужно изменить LookAndFeel до того, как компонент будет окрашен/перекрашен. Единственный момент, когда я заработал, был в paintComponent до вызова super. Вы можете изменить его обратно после вызова super.

Код:

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

Редактировать 2: Не используйте это без крайней необходимости!

Это крайне нестабильно. После долгих тестов я обнаружил, что в нем меньше ошибок, когда я просто поменял местами UIDefaults вместо всего LookAndFeel, но я не рекомендую делать ничего из этого.

Редактировать 3. Лучшим решением, которое я нашел, было использование JavaFX в качестве графического интерфейса. Я вставил узел свинга в область редактора и теперь могу изменять внешний вид компонентов свинга так часто, как захочу, без каких-либо заметных побочных эффектов.

Rant: Если вы всегда можете выбрать JavaFX, если хотите изменить стиль своего приложения. CSS делает это максимально простым без каких-либо побочных эффектов!


Большое спасибо

Джонни


person Jhonny007    schedule 09.11.2015    source источник
comment
Если я правильно понял, вы хотите использовать несколько LAF в своем приложении Swing? На SO уже есть несколько вопросов по этому поводу, например этот.   -  person Laf    schedule 09.11.2015
comment
Я уже нашел эту тему, но все ссылки там не работают. Мое исследование вещества показало, что вещество — это внешний вид и ощущение, которое ТОЛЬКО позволяет вам переключаться между видом и ощущением вещества.   -  person Jhonny007    schedule 10.11.2015
comment
Просто чтобы вы знали, Swing на самом деле не предназначен для динамического изменения внешнего вида, это своего рода случайный побочный эффект, который может не всегда работать. О, и из любви к здравомыслию НЕ изменяйте значения пользовательского интерфейса по умолчанию в методе paintComponent, это просто вызывает бесконечные проблемы. Вместо этого при необходимости используйте UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName()); и SwingUtiltiies#updateComponentTreeUI и передайте в контейнер самого верхнего уровня.   -  person MadProgrammer    schedule 10.11.2015
comment
Да, я знаю, что... это не меняет того факта, что мне нужно запустить обходной путь.   -  person Jhonny007    schedule 10.11.2015


Ответы (1)


Отказ от ответственности

Swing Look And Feel не предназначен для переключения после его первой инициализации, на самом деле это своего рода случайный побочный эффект, который возможен. Некоторым видам и ощущениям, а также некоторым компонентам может не понравиться, что вы это делаете, и они могут вести себя не так, как в обычных условиях.

Возможное решение

Из любви к здравомыслию НЕ изменяйте значения пользовательского интерфейса по умолчанию в методе paintComponent (вообще не изменяйте состояние пользовательского интерфейса из любого метода paint НИКОГДА, рисование рисует только текущее состояние), это просто требует бесконечности. неприятностей.

Вместо этого при необходимости используйте UIManager.setLookAndFeel(,,,) и SwingUtiltiies#updateComponentTreeUI и передайте в контейнер самого верхнего уровня.

Например...

У меня плохое предчувствие по этому поводу

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

        }

    }

}
person MadProgrammer    schedule 09.11.2015
comment
Проблема в том, что мне нужно, чтобы внешний вид менялся при КАЖДОЙ перекраске. Но ТОЛЬКО для компонентов в моей области редактора. Единственный результат, которого я смог достичь, был внутри метода paintComponent. - person Jhonny007; 10.11.2015
comment
Решением может быть запуск предварительного просмотра в отдельной JVM, в которой вы можете изолировать и изменить внешний вид, не затрагивая другие компоненты. Изменение внешнего вида по умолчанию является глобальным, так оно и должно работать. - person MadProgrammer; 10.11.2015
comment
К сожалению, клиент настаивает на рендеринге вживую. Но я только что нашел решение. Это уродливо, как черт возьми, это работает -.- - person Jhonny007; 10.11.2015
comment
@ Jhonny007 Наступает момент, когда вам нужно объяснить клиенту, что невозможное невозможно. API ограничен в своих возможностях дизайном и выбором, которые находятся вне вашего контроля, и клиент должен признать, что иногда его мечта невозможна в той форме, в которой он этого хочет, но может быть достигнута различными способами, которые не будут взорваться в ужасном беспорядке, если вы посмотрите на это сбоку - person MadProgrammer; 10.11.2015