Как предотвратить зависание JComboBox при использовании пользовательского ListCellRenderer

Я делаю выбор шрифта, используя JComboBox и пользовательский ListCellRenderer. Я хочу, чтобы JComboBox отображал все доступные шрифты, причем каждое имя шрифта отображалось в своем собственном шрифте. В настоящее время я использую около 500 шрифтов.

Пример ListCellRenerer, который обеспечивает эту функциональность:

private class ComboBoxRenderer extends JLabel implements ListCellRenderer {

    private JLabel label = new JLabel("Test");

    @Override
    public Component getListCellRendererComponent(JList list, Object value,
            int index, boolean isSelected, boolean cellHasFocus) {

        Font tempFont = label.getFont();
        setFont(new Font((String) value, tempFont.getStyle(),
                tempFont.getSize()));

        setText((String) value);

        return this;
    }
}

Проблема в том, что при использовании этого рендерера JComboBox перестает отвечать на запросы во время выполнения программы. При первом нажатии на поле со списком, чтобы открыть список, для загрузки списка требуется несколько секунд. При втором щелчке сразу отображается список.

Если прокомментировать строку

setFont(new Font((String) value, tempFont.getStyle(),tempFont.getSize()));

, поле со списком работает нормально.

Как можно предотвратить эту невосприимчивость?


person Datoraki    schedule 05.05.2011    source источник
comment
Вы можете попробовать создать кеш из Font объектов. Используется для кэширования Font и FontMetric еще в 90-х. Вы можете создать JLabel для каждого Font.   -  person Tom Hawtin - tackline    schedule 05.05.2011
comment
но с тестом if (isSelected){ или cellHasFocus   -  person mKorbel    schedule 05.05.2011
comment
Только что заметил, что вы говорите, что это медленно только в первый раз, и вы используете 500 шрифтов. Думаю, загрузить 500 шрифтов — непростая задача.   -  person Tom Hawtin - tackline    schedule 05.05.2011
comment
@Tom Hawtin - не думайте, так как комментирование строки, которая создает отображаемый шрифт, помогает   -  person kleopatra    schedule 05.05.2011


Ответы (2)


Что происходит, так это то, что внутренности комбо пытаются найти предпочтительный размер динамически. Для этого он перебирает все элементы в списке, передает средству визуализации элементы для измерения предпочтительного размера компонента визуализации.

Вы можете предотвратить это, установив прототипЗначение для измерения, тогда размер измеряется один раз с использованием этого прототипа.

 comboBox.setPrototypeDisplayValue(sampleFont);

Редактировать: как обнаружил @Boro, этого недостаточно - он устанавливает прототип только для самого comboBox, а не для списка во всплывающем окне (как и должно быть, насколько это может быть безумно глючным... возможно). Чтобы взломать, мы должны установить его вручную, вот фрагмент кода, с которым можно поиграть

public class ComboWithPrototype {

    private JComponent createContent() {
        final Font[] systemFonts = GraphicsEnvironment
                .getLocalGraphicsEnvironment().getAllFonts();

        final JComboBox box = new JComboBox();
        box.setRenderer(new ComboBoxRenderer());
        box.setPrototypeDisplayValue(systemFonts[0]);
        Accessible a = box.getUI().getAccessibleChild(box, 0);
        if (a instanceof javax.swing.plaf.basic.ComboPopup) {
            JList popupList = ((javax.swing.plaf.basic.ComboPopup) a).getList();
            // route the comboBox' prototype to the list
            // should happen in BasicComboxBoxUI
            popupList.setPrototypeCellValue(box.getPrototypeDisplayValue());
        }
        Action action = new AbstractAction("set model") {

            @Override
            public void actionPerformed(ActionEvent e) {
                box.setModel(new DefaultComboBoxModel(systemFonts));
            }
        };
        JComponent panel = new JPanel(new BorderLayout());
        panel.add(box);
        panel.add(new JButton(action), BorderLayout.SOUTH);
        return panel;
    }

    public static void main(String[] args) {
        SwingUtilities.invokeLater(new Runnable() {
            public void run() {
                JFrame frame = new JFrame("");
                frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
                frame.add(new ComboWithPrototype().createContent());
                frame.setLocationRelativeTo(null);
                frame.pack();
                frame.setVisible(true);
            }
        });
    }

Пользовательский ListCellRenderer (слегка изменен, чтобы ожидать элементы типа Font)

private class ComboBoxRenderer extends DefaultListCellRenderer {

    private Font baseFont = new JLabel("Test").getFont();

    @Override
    public Component getListCellRendererComponent(JList list, Object value,
            int index, boolean isSelected, boolean cellHasFocus) {

        super.getListCellRendererComponent(list, value, index, isSelected,
                cellHasFocus);
        if (value instanceof Font) {

            Font font = (Font) value;
            setFont(new Font(font.getName(), baseFont.getStyle(), baseFont.getSize())); 
            setText(font.getName());
        }

        return this;
    }
}
person kleopatra    schedule 05.05.2011
comment
@kleopatra, можете ли вы показать пример того, как он используется, поскольку я пытаюсь его использовать, но безуспешно. Всегда имейте задержку независимо от ее установки до или после установки рендерера. Font[] opts = new Font[]{....}; JComboBox p = new JComboBox(opts); p.setRenderer(new ComboBoxRenderer()); p.setPrototypeDisplayValue(largestFont); Я могу опубликовать SSCCE, если это необходимо, хотя я изменил тело рендера. - person Boro; 05.05.2011
comment
@Boro - f*, вы правы, установка этого свойства кажется недостаточной: она предотвращает цикл по всем значениям для изменения размера самого combo, но все еще зацикливается при отображении всплывающего окна. .. - person kleopatra; 05.05.2011
comment
@kleopatra Да, это то, что я испытал. - person Boro; 05.05.2011
comment
@kleopatra Вот и все (+1 за это). По-прежнему существует очень-очень небольшая задержка перед тем, как список будет показан изначально, но я считаю, что это связано с тем, что он составляется впервые. Все вместе это решение намного быстрее, чем без него. Молодец. - person Boro; 05.05.2011
comment
@Boro - спасибо :-) Хм ... не вижу какой-либо задержки (и на самом деле не должен, он показывает только 8 или около того элементов) - может быть, зависит от машины? Себе на заметку: попробуйте на нетбуке - person kleopatra; 05.05.2011
comment
@kleopatra, возможно, вы правы в отношении проблемы, зависящей от машины. Чтобы быть точным, после некоторого тестирования с использованием всплывающего окна во всплывающем окне со списком и глобального прослушивателя щелчков мыши. Похоже, что без вашего кода это занимает около 4,5 с вначале и 1 мс после. Где с вашим кодом сначала требуется около 1 с, а после - 1 мс. Проверено на CoreDuo 2,4 ГГц Win7. - person Boro; 05.05.2011
comment
@Boro .. странно, у меня контекст примерно такой же и я не вижу никакой (читай ноль, пшик, нада :-) задержки при открытии комбо. Просто чтобы быть уверенным, что мы говорим об одном и том же: показать пользовательский интерфейс (шрифты в массиве, но комбо все еще пусто), вызвать действие для установки модели комбо (происходит мгновенно), нажмите на комбо-стрелку вниз, чтобы открыть всплывающее окно (происходит мгновенно). - person kleopatra; 05.05.2011
comment
@kleopatra Я так думаю. Может быть, вы можете поделиться своим классом, чтобы я мог проверить? Возможно, я делал какие-то странные вещи. - person Boro; 05.05.2011
comment
@Boro, в этом нет ничего особенного - просто добавьте к фрагментам самый простой клей и бросьте все компоненты (комбо, кнопку, связанную с действием) в рамку, и готово. - person kleopatra; 05.05.2011
comment
@kleopatra Это то, что я сделал. И то, что я заметил, ничего не меняет, если вы добавляете модель во время выполнения или при запуске. В обоих случаях это работает одинаково, сначала всегда очень-очень небольшая задержка. В любом случае не заморачивайтесь, это неплохо, учитывая возможную альтернативу 5s :) - person Boro; 05.05.2011
comment
@Boro - изменен фрагмент, чтобы он был ближе к компилируемому/запускаемому. Из представлений о разнице (лично я не считаю задержку в 1 секунду очень незначительной, все, что превышает несколько десятков миллисекунд, ощутимо, а все, что заметно, плохо ;-) Последняя попытка: какая у вас версия jdk? у меня 6у25 - person kleopatra; 05.05.2011
comment
был неправ, мой jdk - 6u24 ;-) Не то чтобы я ожидаю большой разницы между u24 и u25, просто чтобы быть точным - person kleopatra; 05.05.2011
comment
@kleopatra Все равно не годится. Задержка 1 с всегда в начале. мой jdk был 6u11, но обновился до 6u25 и все тот же. - person Boro; 05.05.2011
comment
@Boro - какой-нибудь специальный LAF по умолчанию? Вы можете добавить журнал println в средство визуализации и убедиться, что он вызывается восемь раз (видимые строки в списке) или около того при открытии всплывающего окна? - person kleopatra; 05.05.2011
comment
@kleopatra Я не устанавливаю LAF. getLookAndFeel() возвращает UIManager.getLookAndFeel()=[The Java(tm) Look and Feel - javax.swing.plaf.metal.MetalLookAndFeel]. Строка, добавленная к рендеру System.out.println("counter="+counter++);, сначала печатает до counter=5, затем установка модели печатает до counter=7, а затем показывает действие всплывающего окна со списком до counter=37. Любая идея? Какой у тебя ЛАФ? Большое спасибо за ваш интерес к этой проблеме. Было бы неплохо, если бы кто-нибудь еще мог сказать, присутствует ли он на их машине или нет. - person Boro; 05.05.2011

@kleopatra, пожалуйста, обратите внимание на меня, но setPrototypeDisplayValue похоже на ленивый выбор, моя поправка

import java.awt.BorderLayout;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.GraphicsEnvironment;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.event.ItemEvent;
import java.awt.event.ItemListener;
import javax.swing.DefaultListCellRenderer;
import javax.swing.JComboBox;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JList;
import javax.swing.JPopupMenu;
import javax.swing.JScrollPane;
import javax.swing.JViewport;
import javax.swing.plaf.basic.BasicComboBoxRenderer;

public class SystemFontDisplayer extends JFrame {

    private static final long serialVersionUID = 1L;
    private JComboBox fontsBox;

    public SystemFontDisplayer() {

        GraphicsEnvironment ge = GraphicsEnvironment.getLocalGraphicsEnvironment();
        String[] fontFamilyNames = ge.getAvailableFontFamilyNames();
        fontsBox = new JComboBox(fontFamilyNames);
        fontsBox.setSelectedItem(0);
        fontsBox.setRenderer(new ComboRenderer(fontsBox));
        fontsBox.addItemListener(new ItemListener() {

            @Override
            public void itemStateChanged(ItemEvent e) {
                if (e.getStateChange() == ItemEvent.SELECTED) {
                    final String fontName = fontsBox.getSelectedItem().toString();
                    fontsBox.setFont(new Font(fontName, Font.PLAIN, 16));
                }
            }
        });
        fontsBox.setSelectedItem(0);
        fontsBox.getEditor().selectAll();
        add(fontsBox, BorderLayout.CENTER);
        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        setPreferredSize(new Dimension(400, 60));
        setLocation(200, 105);
        pack();

        java.awt.EventQueue.invokeLater(new Runnable() {

            @Override
            public void run() {
                fontsBox.setPopupVisible(true);
                fontsBox.setPopupVisible(false);
            }
        });
        setVisible(true);
    }

    public static void main(String arg[]) {
        java.awt.EventQueue.invokeLater(new Runnable() {

            @Override
            public void run() {
                SystemFontDisplayer systemFontDisplayer = new SystemFontDisplayer();
            }
        });
    }

    private class ComboRenderer extends BasicComboBoxRenderer {

        private static final long serialVersionUID = 1L;
        private JComboBox comboBox;
        final DefaultListCellRenderer defaultRenderer = new DefaultListCellRenderer();
        private int row;

        private ComboRenderer(JComboBox fontsBox) {
            comboBox = fontsBox;
        }

        private void manItemInCombo() {
            if (comboBox.getItemCount() > 0) {
                final Object comp = comboBox.getUI().getAccessibleChild(comboBox, 0);
                if ((comp instanceof JPopupMenu)) {
                    final JList list = new JList(comboBox.getModel());
                    final JPopupMenu popup = (JPopupMenu) comp;
                    final JScrollPane scrollPane = (JScrollPane) popup.getComponent(0);
                    final JViewport viewport = scrollPane.getViewport();
                    final Rectangle rect = popup.getVisibleRect();
                    final Point pt = viewport.getViewPosition();
                    row = list.locationToIndex(pt);
                }
            }
        }

        @Override
        public Component getListCellRendererComponent(JList list, Object value, int index, boolean isSelected, boolean cellHasFocus) {
            super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus);
            if (list.getModel().getSize() > 0) {
                manItemInCombo();
            }
            final JLabel renderer = (JLabel) defaultRenderer.getListCellRendererComponent(list, value, row, isSelected, cellHasFocus);
            final Object fntObj = value;
            final String fontFamilyName = (String) fntObj;
            setFont(new Font(fontFamilyName, Font.PLAIN, 16));
            return this;
        }
    }
}
person mKorbel    schedule 05.05.2011
comment
-1 за (неудачную) попытку заново изобрести велосипед. Прочтите API-документ setPrototypeDisplayVaule, чтобы узнать, что он существует именно по той причине, по которой он нужен здесь, а именно для не перебора всех элементов только для получения подсказок по размеру. - person kleopatra; 06.05.2011
comment
еще один -1 для выполнения совершенно неповоротливых вещей, таких как (неполный список): а) позволить рендереру иметь ссылку на целевой comboBox б) создавать JList при каждом вызове getRendererComponent, в) настраивать неиспользуемый рендерер при каждом вызове.. . - person kleopatra; 06.05.2011
comment
@ kleopatra :-) этот рендерер работает без Loop внутри JList :-), это из-за моей лени удалять вызов manItemInCombo(); - person mKorbel; 06.05.2011
comment
@kleopatra - я думаю о том, чтобы проголосовать за mKorbel... ;-) потому что его решение устанавливает разную высоту для списков шрифтов, ваше решение устанавливает одинаковую (может быть, слишком маленькую, но уж точно слишком большую для некоторых шрифтов) высоту для все записи. - person bobndrew; 27.09.2012