Pull to refresh
0
ALEE Software
ПО стандартизации и управления качеством

Создаём свой Look and Feel — Часть I

Reading time 25 min
Views 21K
Добрый день, Хабражитель!

Уже достаточно много было рассказано о Swing и графике, также немного было упомянуто о различных незаурядных возможностях, предоставляемых сторонними библиотеками.

Сегодня же я представлю на Ваш суд «последний рубеж» погружения во все те возможности, которые предлагает нам Swing – создание своего Look and Feel класса, а также всего того, что потребуется по ходу дела.

Сразу скажу — если Вы хотите взяться за написание своего примера, а не просто «подсмотреть» пару интересных моментов — будьте готовы угробить потратить на это дело много времени, если Вы конечно собираетесь выпустить что-то более-менее живое и юзабельное в любых приложениях в качестве «обёртки», а также собираетесь в дальнейшем поддерживать созданный вами LaF.

Но даже если Вы не собираетесь делать свой LaF – советую всё же ознакомиться с содержанием. Уверен Вы найдёте много нового и интересного. И даже, возможно, поймёте в чём был корень той или иной интерфейсной проблемы, которая, вероятно, мучала Вас долгие месяцы и годы.

Tip: Если Вы не ещё не знакомы с графикой и Swing в Java более-менее тесно — советую сперва перечитать несколько статей на тему (к примеру оффициальный туториал, статью по Swing от Skipy или же мои предыдущие вводные статьи).

MyLookAndFeel.java


Итак, не теряя времени, начнём с основы — сам LookAndFeel класс. Пронаследуем наш MyLookAndFeel класс от BasicLookAndFeel, а не «пустого» LookAndFeel, так как в нём уже реализованы многие вещи, как то: константы для различных UI-классов, стандартные горячие клавиши компонентов, цветовые схемы и многое другое, без чего большинство UI-классов даже не сможет корректно инициализироваться (тем более что большую часть этих переменных нет необходимости переопределять).

Итак, используем готовый BasicLookAndFeel и переопределим несколько обязательных методов, описывающих наш будущий LaF:
public class MyLookAndFeel extends BasicLookAndFeel
{
    public String getDescription ()
    {
        // Описание LaF
        return "Cross-platform Java Look and Feel";
    }

    public String getName ()
    {       
        // Имя LaF
        return "MyLookAndFeel";
    }

    public String getID ()
    {           
        // Уникальный идентификатор LaF
        return getName ();
    }

    public boolean isNativeLookAndFeel ()
    {               
        // Привязан ли данный LaF к текущей платформе (является ли его нативной имплементацией)
        return false;
    }

    public boolean isSupportedLookAndFeel ()
    {
        // Поддерживается ли данный LaF на текущей платформе
        return true;
    }
}

Теперь остаётся применить одну «волшебную» строку перед созданием любого приложения и оно будет использовать наш LaF:
UIManager.setLookAndFeel MyLookAndFeel.class.getCanonicalName () );

Вот что мы получим на простом примере:


У данного LaF'а уже есть один заметный плюс — он полностью кросс-платформенный, т. е. в текущей реализации он будет выглядеть ужастно одинаково что на Windows, что на Mac OS, что на Linux системах.

Остаётся поработать над косметикой — именно этим мы и займёмся!..

Стилизация компонентов


Итак, нам потребуется несколько вещей для видоизменения каждого компонента, начнём с самой очевидной — создание UI класса для выбранного компонента.

Для простоты возьмём кнопку — наиболее знакомый кому бы то ни было компонент.
По аналогии с BasicLookAndFeel для каждого J-компонента существует BasicUI-реализация, которая сильно упрощает и сводит к минимуму необходимые для стилизации компонентов операции. Для кнопки, не сложно догадаться, это BasicButtonUI – именно его мы и будем наследовать и переопределять.

Во всех UI-классах помимо специализированных методов (для каждого класса они свои, в зависимости от компонента, для которого данный UI) есть несколько стандартных методов, унаследованных от ComponentUI — для начала рассмотрим их:

  1. public void installUI ( JComponent c )
    Данный метод предназначен для «инсталляции» UI-класса на определённый J-компонент. Именно в этом методе стоит определять какие-либо слушатели для J-компонента или какие-либо другие данные, необходимые для корректной отрисовки и работы Вашего UI.
    Вызывается этот метод из updateUI () при создании любого J-компонента.

  2. public void uninstallUI ( JComponent c )
    В данном методе весьма желательно удалять все добавленные Вами на компонент слушатели, а также очищать память от ненужных более данных.
    Вызывается этот метод из setUI ( ComponentUI newUI ) при смене у компонента его текущего UI, или же при полном удалении компонента и «очищении» его зависимостей.

  3. public void paint ( Graphics g, JComponent c )
    Данный метод занимается непосредственно отрисовкой компонента. Именно этот метод является ключевым в UI-классах, так как он определяет визуальное оформление компонента при любом его состоянии.
    Данный метод вызывается косвенно при любой отрисовке/перерисовке компонента из paintComponent ( Graphics g ), который присутствует у любого наследника JСomponent класса, т. е. данный метод отрабатывает только при реальной потребности перерисовки, а также с учётом необходимого clip'а (всё это реальзовано в paintComponent ( Graphics g )).

  4. public void update ( Graphics g, JComponent c )
    Данный метод является лишь небольшим дополнением к paint ( Graphics g, JComponent c ), но именно он вызывается из paintComponent ( Graphics g ) у JСomponent'ов и далее вызывает описанный ранее paint ( Graphics g, JComponent c ) из UI-класса. По факту данный метод реализует затирание «устаревшего» фона объекта и далее отрисовку по «чистому листу».

  5. public Dimension getPreferredSize ( JComponent c )
    Данный метод определяет желаемый размер компонента, на который установлен данный UI-класс.
    Вызывается он из одноимённого метода getPreferredSize () у JСomponent'а или любого его наследника при необходимости пересчёта желаемого размера компонента.

  6. public Dimension getMinimumSize ( JComponent c )
    public Dimension getMaximumSize(JComponent c)

    Два данных метода определяют минимальный и максимальный размеры компонентов и могут быть учтены различными Layout-менеджерами при ресайзинге. По умолчанию они возвращают то же значение, что и getPreferredSize ( JComponent c ), реализованный в UI-классе и описанный ранее.

  7. public boolean contains ( JComponent c, int x, int y )
    Данный метод определяет принадлежит ли точка данному компоненту, или же нет. Необходим он, по большей части, для корректной обработки событий мыши. При любом событии мыши, оно передаётся самому верхнему компоненту, «замеченному» в данной точке. И именно для того, чтобы узнать какой из компонентов находится на самом верху в заданной точке последовательно опрашиваются компоненты, располагающиеся там. Т. е. Засчёт данного метода можно, например, заставить события проходить «мимо» Вашего компонента в тех местах, где он фактически не отрисован и необходимости в получении события нет (например пустая область справа от табов или же угловые области у круглой кнопки).
    Вызывается данный метод из contains ( int x, int y ), присутствующего у всех наследников класса JComponent.
Ещё об одном важном методе я расскажу чуть позже…

Tip: В ComponentUI есть ещё несколько методов, но они нам сегодня не понадобятся и весьма вряд ли будут использованы Вами когда-либо, поэтому я сэкономлю время и позволю себе пропустить их описание.

Дабы не запутаться, ещё раз приведу список упомянутых классов/методов:

JComponent
  • updateUI ()
  • setUI ( ComponentUI newUI )
  • paintComponent ( Graphics g )
  • getPreferredSize ()
  • contains ( int x, int y )
ComponentUI
  • public void installUI ( JComponent c )
  • public void uninstallUI ( JComponent c )
  • public void paint ( Graphics g, JComponent c )
  • public void update ( Graphics g, JComponent c )
  • public Dimension getPreferredSize ( JComponent c )
  • public Dimension getMinimumSize ( JComponent c )
  • public Dimension getMaximumSize ( JComponent c )
  • public boolean contains ( JComponent c, int x, int y )
Собственно, на основе этих базовых методов из ComponentUI и строятся все UI-классы любых компонентов. Для упрощения Вашей жизни в BasicUI-классах также представлено множество других методов, конкретизирующих и упрощающих отрисовку того или иного компонента.

Ещё немного о LookAndFeel


Добавлю ещё немного описания построения LookAndFeel класса, чтобы в дальнейшем больше к нему не возвращаться.

Помимо описанной выше возможности указания UI классов для стандартных компонентов в UIDefaults при нициализации LookAndFeel'а есть также и множество других вещей, которыми заправляет LookAndFeel класс. Большинство из них Вам вряд ли потребуется реализовывать/переопределять, так как они уже есть в BasicLookAndFeel классе, но для порядка стоит знать, что же там творится.

Итак, рассмотрим методы представленные в классе LookAndFeel:
  1. installColors ( JComponent c, String defaultBgName, String defaultFgName )
    installColorsAndFont ( JComponent c, String defaultBgName, String defaultFgName, String defaultFontName )
    installBorder ( JComponent c, String defaultBorderName )
    uninstallBorder ( JComponent c )
    installProperty ( JComponent c, String propertyName, Object propertyValue )
    makeKeyBindings ( Object[] keyBindingList )
    makeInputMap ( Object[] keys )
    makeComponentInputMap ( JComponent c, Object[] keys )
    loadKeyBindings ( InputMap retMap, Object[] keys )
    makeIcon ( final Class<?> baseClass, final String gifFile )
    getDesktopPropertyValue ( String systemPropertyName, Object fallbackValue )

    Весь этот огромный набор статичных методов имеется в LookAndFeel классе фактически для одной цели — упрощённого использования ключей и установки значений, получаемых по этим ключам, различным компонентам. Будь то цвет фона или шрифта, горячие клавиши для текстовых полей или бордер компонента. Используются они по большей части в отдельных UI-классах и в UIDefaults. Особого смысла описывать назначение каждого отдельного метода нет, так как оно более чем подробно описано прямо в комментариях перед каждым методом.

  2. public LayoutStyle getLayoutStyle ()
    Данный метод возвращает инстанс класса LayoutStyle, который определяет поведение компонентов в различных лэйаутах. К примеру он может определять какой отступ предпочтителен между лэйблом и текстовым полем, расположенными друг за другом на одном контейнере (да, именно вплоть до таких конкретных случаев). Впрочем это достаточно абстрактная вещь и в реальности мало какие лэйауты используют определённые в этом классе параметры и этот момент даже отметили разработчики в комментариях к классу. К примеру в Swing единственным использующим LayoutStyle лэйаутом является GroupLayout, что и не странно, учитывая его специфику.

  3. public void provideErrorFeedback ( Component component )
    Данный метод вызывается каждый раз, когда пользователь производить какое-либо недопустимое действие (к примеру нажимает Backspace в пустом текстовом поле или пытается удалить текст нередактируемого поля и т. п.). В стандартной реализации подобные случае просто вызывают метод Toolkit.getDefaultToolkit ().beep (), который воспроизводит короткий звуковой сигнал некорректности операции, стандартный для текущей ОС. Впрочем, Вы всегда можете изменить данное поведение на любое другое (к примеру вовсе убрать этот раздражающий *beep*).

  4. public Icon getDisabledIcon ( JComponent component, Icon icon )
    Данный метод возвращает соответствующее текущему LookAndFeel'у отображение заблокированной иконки. Этот метод используется для отображения иконок заблокированной кнопки, лэйбла и заблокированных элементов других стандартных компонентов. Если у Вас есть более удачный/быстрый способ создания «заблокированного» вида иконки — дерзайте переопределить данный метод!

  5. public Icon getDisabledSelectedIcon ( JComponent component, Icon icon )
    Этот метод идентичен предыдщему, только работает для «выбранных» иконок. В стандартной реализации банально вызывает предыдущий метод.

  6. public String getName ()
    public String getID ()
    public String getDescription ()
    public boolean isNativeLookAndFeel ()
    public boolean isSupportedLookAndFeel ()

    Эти методы уже были описаны ранее при создании LookAndFeel класса. Они просто предоставляют базовую информацию о данном LookAndFeel'е.

  7. public boolean getSupportsWindowDecorations ()
    Данный метод более интересен — он сообщает UIManager'у, способен ли RootPaneUI, возвращаемый данным LookAndFeel'ом декорировать окно. Т. е. создавать оформление окна заместо стандартного системного оформления. В случае если данный метод возвращает true, Вы реализовали специфичное оформление окна в Вашем RootPaneUI – достаточно выполнить два следующих метода:
    JFrame.setDefaultLookAndFeelDecorated ( true );
    JDialog.setDefaultLookAndFeelDecorated ( true );
    для обёртки любых инстансов JDialog/JFrame в специальное оформление. О том же, как создать подобное оформление, дабы не распыляться, я напишу отдельным топиком в своё время.

  8. public void initialize ()
    public void uninitialize ()

    Данные методы вызываются непосредственно при установке и, соответственно, смене LookAndFeel'а через UIManager. В них можно завернуть создание каких-либо глобальных слушателей и создание каких-либо переменных, необходимых для корректной работы Вашего LookAndFeel'а.

  9. public UIDefaults getDefaults()
    Данный метод является, по большому счёту, основой базового оформления всех компонентов. Он предоставляет всё то огромное множество различных переменных (шрифты, цвета, хоткеи и пр.) необходимое для работы LookAndFeel'а и отдельных его UI классов. В BasicLookAndFeel данный метод разбивается на 3 раздельных части:
    1. initClassDefaults ( UIDefaults table )
      Именно этот метод мы переопределили в MyLookAndFeel для «подмены» UI класса кнопки и именно его стоит переопределять для этого дела.

    2. initSystemColorDefaults ( UIDefaults table )
      В этом методе определяются стандартные цвета различных элементых (как ни странно — hex-значениями).

    3. initComponentDefaults ( UIDefaults table )
      В этом же самом крупном из всех методе определяются шрифты, цвета, горячие клавиши, бордеры и прочие мелочи каждого отдельного компонента, если это необходимо.


Пожалуй теперь Вы достаточно узнали, чтобы преступить к первому UI-классу…

MyButtonUI.java


Как я уже писал выше — мы будем наследовать наш UI-класс от BasicButtonUI. Для начала накидаем простенький интерфейс для кнопки и убедимся, что всё работает именно так, как мы думаем:
public class MyButtonUI extends BasicButtonUI
{
    public void installUI ( JComponent c )
    {
        // Обязательно оставляем установку UI, реализованную в Basic UI классе
        super.installUI ( c );

        // Устанавливаем желаемые настройки JButton'а
        // Для абстракции используем AbstractButton, так как в нём есть всё необходимое нам
        AbstractButton button = ( AbstractButton ) c;
        button.setOpaque ( false );
        button.setFocusable ( true );
        button.setMargin ( new Insets ( 0, 0, 0, 0 ) );
        button.setBorder ( BorderFactory.createEmptyBorder ( 4, 4, 4, 4 ) );
    }

    public void paint ( Graphics g, JComponent c )
    {
        Graphics2D g2d = ( Graphics2D ) g;
        g2d.setRenderingHint ( RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON );

        AbstractButton button = ( AbstractButton ) c;
        ButtonModel buttonModel = button.getModel ();

        // Формой кнопки будет закруглённый прямоугольник

        // Фон кнопки
        g2d.setPaint ( new GradientPaint ( 0, 0, Color.WHITE, 0, c.getHeight (),
                new Color ( 200, 200, 200 ) ) );
        // Закгругление необходимо делать больше, чем при отрисовке формы,
        // иначе светлый фон будет просвечивать по краям
        g2d.fillRoundRect ( 0, 0, c.getWidth (), c.getHeight (), 8, 8 );

        // Бордер кнопки
        g2d.setPaint ( Color.GRAY );
        // Важно помнить, что форму необходимо делать на 1px меньше, чем ширина/высота компонента,
        // иначе правый и нижний края фигуры вылезут за границу компонента и не будут видны
        // К заливке это не относится, так как последняя колонка/строка пикселей игнорируется при заполнении
        g2d.drawRoundRect ( 0, 0, c.getWidth () - 1, c.getHeight () - 1, 6, 6 );

        // Сдвиг отрисовки при нажатой кнопке
        if ( buttonModel.isPressed () )
        {
            g2d.translate ( 1, 1 );
        }

        // Отрисовка текста и иконки изображения
        super.paint ( g, c );
    }
}
Теперь необходимо MyButtonUI использовать в MyLookAndFeel, чтобы он автоматически устанавливался всем кнопкам. Для этого в MyLookAndFeel классе наследуем метод initClassDefaults ( UIDefaults table ) и переопределяем необходимое значение:
protected void initClassDefaults ( UIDefaults table )
{
    // По прежнему оставляем дефолтную инициализацию, так как мы пока что не реализовали все
    // различные UI-классы для J-компонентов
    super.initClassDefaults ( table );

    // А вот, собственно, самое важное
    table.put ( "ButtonUI", MyButtonUI.class.getCanonicalName () );
}
Теперь запускаем наш старый пример и готовимся увидеть нашу «обновлённую» кнопку:


Хм, что-то пошло не так… Неужели мы где-то накосячили в UI-классе?
Попробуем напрямую задать UI кнопке:
final JButton jButton = new JButton ( "JButton" );
jButton.setUI ( new MyButtonUI () );
add ( jButton );
Пробуем:

Нет, с UI всё в порядке. Что же не так?

Именно над этим моментом я бился, наверное, целый день. В итоге, конечно же, всё оказалось до банального просто — мы упустили ещё один метод, который необходимо обязательно описать в первую очередь в нашем MyButtonUI классе — createUI ( JComponent c ). Как же я его упустил спросите Вы? Очень просто — иногда бывает черезчур вредно и недальновидно полагаться на IDE — можно упустить важные моменты из виду (я ни в коем случае не упрекаю IDE ибо виной тому моя невнимательность).

Данным метод присутствует в ComponentUI, но он… public static!? Спрашивается зачем в абстрактном ComponentUI статичный метод, который нельзя переопределить (собственно поэтому я и не увидел его сразу)?

Дело в том, что данный метод вызывается при создании UI в UIDefaults классе через Reflection, поэтому в нашем случае, так как мы этот метод не создали, при вызове был использован первый попавшийся под руку — его реализация в BasicButtonUI. А она, если посмотреть в исходный код, выглядит вот так:
public static ComponentUI createUI(JComponent c) {
    AppContext appContext = AppContext.getAppContext();
    BasicButtonUI buttonUI =
            (BasicButtonUI) appContext.get(BASIC_BUTTON_UI_KEY);
    if (buttonUI == null) {
        buttonUI = new BasicButtonUI();
        appContext.put(BASIC_BUTTON_UI_KEY, buttonUI);
    }
    return buttonUI;
}
Собственно, вот и причина, почему наш MyButtonUI не был использован. Исправим эту оплошность и добавим createUI ( JComponent c ) метод в MyButtonUI:
public static ComponentUI createUI ( JComponent c )
{
    // Создаём инстанс нашего UI
    return new MyButtonUI ();
}
Запустим наш пример ещё раз, но уже без насильной установки UI JButton'у:

Ура, работает!

Tip: Если один и тот же Ваш UI класс может быть использован для всех компонентов определённого типа, то стоит делать его. В нашем случае с MyButtonUI это также возможно, поэтому немного изменим приведённый выше метод:
private static MyButtonUI instance = null;

public static ComponentUI createUI ( JComponent c )
{
    // Создаём инстанс нашего UI
    if ( instance == null )
    {
        instance = new MyButtonUI ();
    }
    return instance;
}
При таком использовании UI мы сможем сэкономить, как на востребованной памяти, так и на излишних инстансах MyButtonUI класса, создаваемых для каждого отдельного JButton'а. Впрочем если Вы хотите добавить возможность стилизации UI каждого компонента отдельно — данный вариант не подойдёт.

Ложка дёгтя


Теперь мы знаем как создавать и устанавливать в LookAndFeel'е свой собственный UI-класс для кнопки, но что насчёт других компонентов? Всё то же, всё там же — необходимо просто дополнить метод initClassDefaults ( UIDefaults table ) необходимыми нам UI-классами. В финальном виде он должен выглядеть примерно так:
protected void initClassDefaults ( UIDefaults table )
{
    // Label
    table.put ( "LabelUI", ... );
    table.put ( "ToolTipUI", ... );

    // Button
    table.put ( "ButtonUI", ... );
    table.put ( "ToggleButtonUI", ... );
    table.put ( "CheckBoxUI", ... );
    table.put ( "RadioButtonUI", ... );

    // Menu
    table.put ( "MenuBarUI", ... );
    table.put ( "MenuUI", ... );
    table.put ( "PopupMenuUI", ... );
    table.put ( "MenuItemUI", ... );
    table.put ( "CheckBoxMenuItemUI", ... );
    table.put ( "RadioButtonMenuItemUI", ... );
    table.put ( "PopupMenuSeparatorUI", ... );

    // Separator
    table.put ( "SeparatorUI", ... );

    // Scroll
    table.put ( "ScrollBarUI", ... );
    table.put ( "ScrollPaneUI", ... );

    // Text
    table.put ( "TextFieldUI", ... );
    table.put ( "PasswordFieldUI", ... );
    table.put ( "FormattedTextFieldUI", ... );
    table.put ( "TextAreaUI", ... );
    table.put ( "EditorPaneUI", ... );
    table.put ( "TextPaneUI", ... );

    // Toolbar
    table.put ( "ToolBarUI", ... );
    table.put ( "ToolBarSeparatorUI", ... );

    // Table
    table.put ( "TableUI", ... );
    table.put ( "TableHeaderUI", ... );

    // Chooser
    table.put ( "ColorChooserUI", ... );
    table.put ( "FileChooserUI", ... );

    // Container
    table.put ( "PanelUI", ... );
    table.put ( "ViewportUI", ... );
    table.put ( "RootPaneUI", ... );
    table.put ( "TabbedPaneUI", ... );
    table.put ( "SplitPaneUI", ... );

    // Complex components
    table.put ( "ProgressBarUI", ... );
    table.put ( "SliderUI", ... );
    table.put ( "SpinnerUI", ... );
    table.put ( "TreeUI", ... );
    table.put ( "ListUI", ... );
    table.put ( "ComboBoxUI", ... );

    // Desktop pane
    table.put ( "DesktopPaneUI", ... );
    table.put ( "DesktopIconUI", ... );
    table.put ( "InternalFrameUI", ... );

    // Option pane
    table.put ( "OptionPaneUI", ... );
}
Понятное дело, на месте каждого «…» должен быть путь к конкретному UI-классу, определяющему внешний вид того или иного J-компонента.

Теперь Вы можете хотя бы приблизительно представить, сколько работы (и времени) потребует полное переопределение и реализация всех UI для стандартных Swing'овых компонентов.

Но не всё так плохо — если Вы исходно определились с концепцией/внешним видом Вашего LookAndFeel'а — дело пойдёт намного быстрее. Как я убедился на своём опыте — реализация большинства UI сводится к использованию нескольких стандартных методов, которые можно абстрагировать, вынести в отдельные утилит-классы и использовать повсеместно (к примеру отрисовка бордеров, фона, создание стандартных фигур и т.п.). Требуется лишь немного «магии» в некоторых сложных компонентах для доведения до ума их внешнего вида.

Итак, продолжим наше знакомство с различными UI классами, а точнее — рассмотрим предлагаемые BasicUI-классами методы и возможности, а также некоторые тонкости и сложные моменты, связанные с каждым отдельным UI и требующие той самой «магии».

Пожалуй для порядка пойдём прямо по списку, приведённому в коде выше…

Текстовые компоненты


BasicLabelUI – JLabel

Один из самых простых компонентов. В большинстве LookAndFeel'ов LabelUI либо вовсе не переопределён и используется BasicLabelUI, либо он лишь немного видоизменён. Например в WindowsLabelUI иначе работает отрисовка мнемоник-подчёркивания — оно видно только при зажатии Alt.

Всё что необходимо — отрисовка обычного или же HTML текста, а также мнемоник-подчёркивания — есть в BasicLabelUI.

Основная хитрость, кстати, в корректной отрисовке текста — для неё активно используются методы из SwingUtilities/SwingUtilities2. Сперва определяется расположение текста, а затем используется стандартный метод для его отрисовки :
SwingUtilities2.drawStringUnderlineCharAt ( l, g, s, mnemIndex, textX, textY );
Фактически — основную работу выполняет класс SwingUtilities2.

Если Вам интересно узнать больше — можете досконально изучить BasicLabelUI класс. Если Вы уже вполне хорошо ориентируетесь в графике никаких проблем возникнуть не должно.

Не будем задерживаться и пойдём дальше.

BasicToolTipUI – JToolTip

Не менее простой чем JLabel компонент. Фактически JToolTip (говоря абстрактно) это текст на фоне и с неким бордером.

Впрочем есть один небольшой нюанс — данный компонент при отображении располагается либо на JLayeredPane окна, в котором был показан тултип, либо на отдельном JWindow, если тултип выходит за рамки окна. Этот нюанс стоит учесть при отрисовке и избегать каких-либо специфичных форм тултипа и придерживаться «более-менее» прямоугольной.

Также если самому JToolTip'у задать setOpaque ( false ) и он будет находиться в рамках текущего окна, то он будет действительно прозрачен. В случае же отображения тултипа в отдельном JWindow этот трюк уже не сработает.

Пока что для выхода из этой ситуации есть несколько трюков — делать окно прозрачным недавно появившимися возможностями (AWTUtilities в версиях 1.6 u10 + или же методы Window в 1.7+) или же делать «скриншот» области, над которой показывается тултип и помещать его в качестве фона.

У первого варианта минус в том, что данная фича не везде работает корректно. У второго минус в статичности и скорости работы, т. е. если под окном тултипа была какая-либо анимация — её отсутствие будет заметно. Впрочем, в контексте тултипа это вряд ли играет большую роль.

Кнопки


BasicButtonUI – JButton

В примере MyButtonUI, приведённом выше, я не использовал методы определённые в BasicButtonUI, а просто полностью переопределил метод paint ( Graphics g, JComponent c ).

Делать так или же наследовать методы:
paintButtonPressed ( Graphics g, AbstractButton b )
paintText ( Graphics g, AbstractButton b, Rectangle textRect, String text )
paintIcon ( Graphics g, JComponent c, Rectangle iconRect )
paintFocus ( Graphics g, AbstractButton b, Rectangle viewRect, Rectangle textRect, Rectangle iconRect )

дело сугубо Ваше и зависит, скорее, от сложности отрисовываемой кнопки.

При желании можно и вовсе разделить отрисовку на части своими методами. К примеру вынести в отдельные методы отрисовку фона и текста с иконкой, но не более.

В любом случае — результат будет один и тот же, если нигде не ошибиться конечно же.

BasicToggleButtonUI – JToggleButton

Для JToggleButton нет ToggleButtonUI класса — только BasicToggleButtonUI, наследуемый от BasicButtonUI и фактически разницы между ними почти никакой нет. Только что в JToggleButton модель кнопки может принимать состояние selected=true, а в JButton нет, хотя сама модель в обоих случаях одна и та же.

Соответственно и визуальное различие заключается лишь в том, что JToggleButton может оставаться в pressed состоянии без удержания компонента мышью. Ни отрисовка, ни логика при этом фактически никак не меняется.

Поэтому мне кажется логичным реализовать UI как JButton так и JToggleButton одним и тем же классом или же просто пронаследовать для визуальной чистоты (MyToggleButtonUI extends MyButtonUI), ну и конечно же не забыть добавить статичный метод createUI ( JComponent c ) в MyToggleButtonUI.

BasicRadioButtonUI – JRadioButton

Аналогично JToggleButton – у данного компонента нет RadioButtonUI класса. BasicRadioButtonUI же унаследован от BasicToggleButtonUI и отличается лишь отрисовываемой иконкой и отсутствием стандартного фона.

BasicCheckBoxUI – JCheckBox

Данный UI полностью дублирует BasicRadioButtonUI, за исключением иконки.

Меню


BasicMenuBarUI — JMenuBar

Данный компонент представлет из себя «подставку» под JMenu компоненты и сам по себе кроме фона ничего не отрисовывает.

Впрочем при переопределении BasicMenuBarUI стоит также переопределить и бордер компонента, отсающийся «по наследию».

Ещё одна особенность — на Mac OS оформление и сам UI, а также все остальные UI меню и элементов меню не будут иметь смысла, если используется интеграция с системным меню-баром.
т. е. при использовании:
System.setProperty ( "apple.laf.useScreenMenuBar", "true" );
при запуске на Mac OS.

BasicMenuItemUI — JMenuItem

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

В BasicMenuItemUI есть несколько исходно разнесённых методов:
  1. paintMenuItem ( Graphics g, JComponent c, Icon checkIcon, Icon arrowIcon, Color background, Color foreground, int defaultTextIconGap )
    Основной метод, собирающий воедино отрисовку разных частей элемента меню.

  2. paintBackground ( Graphics g, JMenuItem menuItem, Color bgColor )
    Отрисовывает фон элемента меню.

  3. paintCheckIcon( Graphics g, MenuItemLayoutHelper lh, MenuItemLayoutHelper.LayoutResult lr, Color holdc, Color foreground )
    Отрисовывает иконку выбора у JRadioButtonMenuItem или JCheckBoxMenuItem.

  4. paintIcon( Graphics g, MenuItemLayoutHelper lh, MenuItemLayoutHelper.LayoutResult lr, Color holdc )
    Отрисовывает иконку, предоставляемую JMenuItem'ом или JMenu.

  5. paintText( Graphics g, MenuItemLayoutHelper lh, MenuItemLayoutHelper.LayoutResult lr )
    Отрисовывает текст элемента меню.

  6. paintAccText( Graphics g, MenuItemLayoutHelper lh, MenuItemLayoutHelper.LayoutResult lr )
    Отрисовывает текст хоткея, повешенного на данный элемент меню (например «Alt+F4»).

  7. paintArrowIcon( Graphics g, MenuItemLayoutHelper lh, MenuItemLayoutHelper.LayoutResult lr, Color foreground )
    Отрисовывает стрелку, указывающую на подменю в JMenu.


Что самое интересное — половина из этих вещей не нужна в обычном JMenuItem. Но, забегая вперёд, скажу что данный класс реализует визуальное представление всех четырёх различных элементов меню — JMenu, JMenuItem, JCheckBoxMenuItem, JRadioButtonMenuItem.

И это только начало всей каши, дальше — лучше.

Все вычисления размеров и расположений отдельных элементов вынесено в класс MenuItemLayoutHelper, который вообще находится в другом пакете (sun.swing) и невозможно никак изменить. Также данный класс не входит в некоторые старые сборки JDK и OpenJDK.

В итоге, если Вас так или иначе не устраивает расположение элементов или их размеры — приходится прибегать к исхищрениям и уловкам и всячески обходить стандартные данные MenuItemLayoutHelper'а (к примеру мне нужно было сделать выравнивание всех элементов меню в линию с пробелом в иконку 16х16, даже если ни у одного элемента она не указана).

После пары дней убитых на бесполезную войну с ним я отказался от затеи и, использовав часть кода MenuItemLayoutHelper, написал свой собственный класс для подсчётов. Ещё, как вариант, можно вовсе отказаться от использования MenuItemLayoutHelper и реализовать все подсчёты заново.

BasicMenuUI – JMenu

Данный UI 1 в 1 повторяет реализацию BasicMenuItemUI, но Вам придётся выбрать меньшую из зол — либо наследовать свою реализацию BasicMenuItemUI и копировать логику работы JMenu из BasicMenuUI, либо наследовать BasicMenuUI и копировать визуальную реализацию из Вашего наследника BasicMenuItemUI.

Я выбрал второй вариант, так как всё же проще разбираться в своём коде, да и поддерживать любые изменения логики будет куда сложнее, нежели дублировать визуальное оформление.

BasicCheckBoxMenuItemUI – JCheckBoxMenuItem
BasicRadioButtonMenuItemUI – JRadioButtonMenuItem

Оба этих UI класса можно без зазрений совести расширять от Вашей реализации BasicMenuItemUI и переопределить лишь метод отрисовки выбора чекбокса/радиокнопки.

А можно вовсе реализовать эту отрисовку прямо в Вашем наследнике BasicMenuItemUI, а два данных UI класса просто расширить от него, добавив лишь статичный createUI ( JComponent c ) метод.

BasicPopupMenuSeparatorUI – JSeparator

Данный класс представляет лишь немного видоизменённый BasicSeparatorUI и используется исключительно в JPopupMenu для разделения элементов меню. Сам компонент JSeparator представляет из себя простую отрисованную полосу/линию/черту и используется для визуального разделения логических частей интерфейса.

При переопределении данного UI класса достаточно просто изменить стандартный метод paint ( Graphics g, JComponent c ), отрисовав в нём желаемый вид разделителя.

BasicPopupMenuUI – JPopupMenu

Собственно, последний компонент из серии меню — JPopupMenu. Вся необходимая для работы данного компонента логика реализована в BasicPopupMenuUI, поэтому нам лишь остаётся изменить внешний вид на свой вкус.

Собственно здесь два момента — бордер меню, который определит отступ от края попап-меню до внутренних элементов и отрисовка фона меню.

Бордер стоит установить в методе installUI ( JComponent c ), а отрисовку, как обычно, выполнить в методе paint ( Graphics g, JComponent c ).

Также, если Вам хочется экзотики, можно реализовать отрисовку выделения элементов попап-меню на его фоне, а не внутри каждого отдельного элемента меню. Это может быть полезно в случае если Вам необходим некоторый отступ от края до элементов, но выделение Вы хотите рисовать на всю ширину меню вплоть до самого края (такое выделение можно наблюдать, к примеру, в системных попап-меню на Mac OS).

Для этого правда придётся дополнительно в UI-классе запомнить текущее попап-меню и при отрисовке фона узнавать состояние элементов, а также корректно прослушивать смену выделения для перерисовки фона.

Продолжение следует...


Через некоторое время я опубликую II часть, в которой подробно опишу все оставшиеся BasicUI-классы, а также приведу несколько готовых примеров реализации UI компонентов.

Если Вы уже прочитали и освоили весь приведённый материал — можете попробовать приступить к реализации своего LaF'а или же просто попробовать переопределить несколько стандартных UI-классов.

Также Вы можете покопаться в исходных кодах WebLookAndFeel библиотеки, над которой я работаю в данный момент и на основе которой я смог достаточно основательно попрактиковаться в написании UI. Там Вы сможете найти полноценную реализацию всех стандартных UI, большинство которых я описал или ещё опишу в этой и следующей статьях, а также некоторые другие интерфейсные «плюшки».

Более подробно о ней, а также оставшихся UI классах читайте в продолжении примерно через неделю. И спасибо за внимание!

Если у Вас есть какие-либо вопросы по Java LookAndFeel/UI — не стесняйтейсь и отписывайтесь в комментариях! Постараюсь ответить на них по мере возможностей.

P.S. Отдельное спасибо проекту dumpz.org за приятную читабельную подсветку кода.
Tags:
Hubs:
+55
Comments 40
Comments Comments 40

Articles

Information

Website
www.alee.ru
Registered
Founded
Employees
31–50 employees
Location
Россия