Понятие интерфейса в java. Приемы программирования: пример применения интерфейсов. Файловая структура Java-проекта

Скажу сразу - при работе с графикой, скорее всего, со временем Вам придется воспользоваться всеми предоставленными инструментами без исключения для достижения наилучшего визуального эффекта. Подробное описание о том «что и как» можно найти - это официальный туториал по Graphics2D. Его должно быть более чем достаточно, чтобы ввести Вас в курс дела.

Я уже привел небольшой пример написания своего UI, но есть и другие варианты кастомизации интерфейса. Каждый отдельный J-компонент производит свою Lightweight-отрисовку при помощи метода paint(), который можно легко переопределить и изменить. Напрямую (не всегда, но чаще всего) его лучше не использовать (не буду вдаваться в подробности, так как это целая тема для отдельного топика). Для следующего примера используем метод paintComponent(). Рассмотрим как его можно применить поближе…

Начну с примера - текстовое поле с визуальным фидбэком при отсутствии содержимого:

JTextField field = new JTextField()
{
private boolean lostFocusOnce = false ;
private boolean incorrect = false ;

{
// Слушатели для обновления состояния проверки
addFocusListener (new FocusAdapter()
{
public void focusLost (FocusEvent e)
{
lostFocusOnce = true ;

repaint ();
}
});
addCaretListener (new CaretListener()
{
public void caretUpdate (CaretEvent e)
{
if (lostFocusOnce)
{
incorrect = getText ().trim ().equals ("" );
}
}
});
}

protected void paintComponent (Graphics g)
{
super.paintComponent (g);

// Расширенная отрисовка при некорректности данных
if (incorrect)
{
Graphics2D g2d = (Graphics2D) g;

// Включаем антиалиасинг для гладкой отрисовки
g2d.setRenderingHint (RenderingHints.KEY_ANTIALIASING,
RenderingHints.VALUE_ANTIALIAS_ON);

// Получаем отступы внутри поля
Insets insets;
if (getBorder () == null )
{
insets = new Insets (2, 2, 2, 2);
}
else
{
insets = getBorder ().getBorderInsets (this );
}

// Создаем фигуру в виде подчеркивания текста
GeneralPath gp = new GeneralPath (GeneralPath.WIND_EVEN_ODD);
gp.moveTo (insets.left, getHeight () - insets.bottom);
for (int i = 0; i < getWidth () - insets.right - insets.left; i += 3)
{
gp.lineTo (insets.left + i,
getHeight () - insets.bottom - ((i / 3) % 2 == 1 ? 2: 0));
}

// Отрисовываем её красным цветом
g2d.setPaint (Color.RED);
g2d.draw (gp);
}
}
};

Наличие содержимого перепроверяется при печати и потере фокуса полем. Переключившись на другой компонент мы увидим как отрисовывается наше дополнение к JTextField"у:

Полный код примера можно взять .

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

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

За основу берутся 8 изображений 16х16 - 4 состояния фона чекбокса и 4 состояния галки (5 на самом деле, но 5ое мы добавим програмно):

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

public static List BG_STATES = new ArrayList ();
public static List CHECK_STATES = new ArrayList ();

static
{
// Иконки состояния фона
for (int i = 1; i <= 4; i++)
{
BG_STATES.add (new ImageIcon (
MyCheckBox.class .getResource ("icons/states/" + i + ".png" )));
}

// Дополнительное "пустое" состояние выделения

new BufferedImage (16, 16, BufferedImage.TYPE_INT_ARGB)));

// Состояния выделения
for (int i = 1; i <= 4; i++)
{
CHECK_STATES.add (new ImageIcon (
MyCheckBox.class .getResource ("icons/states/c" + i + ".png" )));
}
}

private Map iconsCache = new HashMap ();

private synchronized void updateIcon ()
{
// Обновляем иконку чекбокса
final String key = bgIcon + "," + checkIcon;
if (iconsCache.containsKey (key))
{
// Необходимая иконка уже была ранее использована
setIcon (iconsCache.get (key));
}
else
{
// Создаем новую иконку совмещающую в себе фон и состояние поверх
BufferedImage b = new BufferedImage (BG_STATES.get (0).getIconWidth (),
BG_STATES.get (0).getIconHeight (), BufferedImage.TYPE_INT_ARGB);
Graphics2D g2d = b.createGraphics ();
g2d.drawImage (BG_STATES.get (bgIcon).getImage (), 0, 0,
BG_STATES.get (bgIcon).getImageObserver ());
g2d.drawImage (CHECK_STATES.get (checkIcon).getImage (), 0, 0,
CHECK_STATES.get (checkIcon).getImageObserver ());
g2d.dispose ();

ImageIcon icon = new ImageIcon (b);
iconsCache.put (key, icon);
setIcon (icon);
}
}

Остается добавить несколько обработчиков переходов состояний и мы получим анимированный переход между ними:

Получившийся компонент очень легко будет видоизменить к примеру в радиобаттон, или же добавить в него больше состояний перехода для более плавной анимации и т.п.
Собственно, полный рабочий код и изображения выложил отдельно .

Таким образом путей кастомизации элементов есть достаточно много (даже я, наверно, не знаю/не догадываюсь о некоторых из них). Какой способ выбирать - зависит от ситуации - что необходимо получить, какие изменения возможно сделать в существующем компоненте и т.д.

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


Не буду врать, на данный UI кнопки ушло достаточно много времени, но он создавался без помощи и подсказок дизайнера чистыми средствами Graphics2D и Swing. можно скачать и изучить полный сурс и демо данного UI, если Вам стало интересно. В нем использован достаточно большой спектр возможностей Graphics2D и применены некоторые хитрости, которые могут зачастую оказаться полезными.

Итак, думаю достаточно разговоров о графике - о ней более подробно я расскажу в будущих топиках, а сейчас приведу немного интересного материала, который я наработал за достаточно долгое время «общения» со Swing и Graphics2D.

DnD и GlassPane

Думаю первое - Вам всем более чем известно, как и проблемы с ним связанные. Насчет второго - вероятно Вы вскользь слышали о GlassPane или может даже видели это старинное изображение (которое до сих пор актуально, между прочем) об устройстве различных слоев стандартных фреймов. Что же тут такого и зачем я вспомнил об этом? И тем более, как связаны DnD и GlassPane спросите Вы? Вот именно о том, как их связать и что из этого может выйти я и хочу рассказать в этой главе.

Чтож, начнем по порядку - что мы знаем о DnD?
У некоторых Swing-компонентов есть готовые реализации для драга (JTree и JList к примеру) - для других можно достаточно легко дописать свою. Чтобы не бросаться словами на ветер - приведу небольшой пример DnD стринга из лэйбла:

JLabel label = new JLabel ("Небольшой текст для DnD" );
label.setTransferHandler (new TransferHandler()
{
public int getSourceActions (JComponent c)
{
return TransferHandler.COPY;
}

public boolean canImport (TransferSupport support)
{
return false ;
}

protected Transferable createTransferable (JComponent c)
{
return new StringSelection (((JLabel) c).getText ());
}
});
label.addMouseListener (new MouseAdapter()
{
public void mousePressed (MouseEvent e)
{
if (SwingUtilities.isLeftMouseButton (e))
{
JComponent c = (JComponent) e.getSource ();
TransferHandler handler = c.getTransferHandler ();
handler.exportAsDrag (c, e, TransferHandler.COPY);
}
}
});

Теперь возможно перетащить текст данного лэйбла прямо из интерфейса в любое другое место, куда возможно вставить текст через дроп.
Фактически - TransferHandler овечает за то, какие данные при драге отдает компонент и как компонент использует входящие данные при драге на него.

Но что делать, если необходимо отследить последовательность действий пользователя при самом перетаскивании?
Для этого есть отдельная возможность повесить слушатель:

DragSourceAdapter dsa = new DragSourceAdapter()
{
public void dragEnter (DragSourceDragEvent dsde)
{
// При входе драга в область какого-либо компонента
}

public void dragExit (DragSourceEvent dse)
{
// При выходе драга в область какого-либо компонента
}

public void dropActionChanged (DragSourceDragEvent dsde)
{
// При смене действия драга
}

public void dragOver (DragSourceDragEvent dsde)
{
// При возможности корректного завершения драга
}

public void dragMouseMoved (DragSourceDragEvent dsde)
{
// При передвижении драга
}

public void dragDropEnd (DragSourceDropEvent dsde)
{
// При завершении или отмене драга
}
};
DragSource.getDefaultDragSource ().addDragSourceListener (dsa);
DragSource.getDefaultDragSource ().addDragSourceMotionListener (dsa);

Остался последний момент - обозначить роль GlassPane. GlassPane, фактически, позволяет располагать/отрисовывать на себе компоненты, как и любой другой контейнер, но его особенность в том, что он лежит поверх всех Swing-компонентов, когда виден. Т.е. если мы что-либо отрисуем на нем, то оно накроет весь находящийся под ним интерфейс. Это позволяет размещать компоненты независимо от основного контейнера в любом месте, создавать любые визуальные эффекты и делать другие занятные вещи.

Приведу для большего понимания небольшой пример подобного «эффекта» - фрейм с несколькими Swing-компонентами на нем. При клике в любой части окна будет появляться эффект «распозающегося» круга, который виден поверх всех элементов. Что самое интересное - подобный эффект не съедает ресурсов и не требует большой груды кода. Не верите? - посмотрите демо и загляните в исходник, вложенный в jar.

Кстати говоря, есть достаточно интересная библиотека на эту тему, заодно предоставляющая дополнительный скролл-функционал и несколько других вкусностей - JXLayer (офф сайт) (описание #1 описание #2 описание #3). К сожалению проекты хостящиеся на сайте java сейчас находтся не в лучшем состоянии, поэтому приходится ссылаться на отдельные ресурсы.

Итак теперь объединим всё что я уже описал в данной главе и сделаем, наконец, что-то полноценное. К примеру - отображение драга панели с компонентами внутри окна:

При драге панели за лэйбл появляется полупрозрачная копия панели показывающая где именно будет размещена панель при завершении драга. Также клавишей ESC можно отменить перемещение.
Рабочий пример и исходный код можно взять .

Конечно, для реализации именно данной функциональности не стоило бы прибегать к использованию DnD - есть более короткие пути. Впрочем, всё зависит от ситуации. Данный вариант позволяет отрисовывать перетаскивание независимо от других компонентов в окне. Также можно на основе него, к примеру, реализовать драг панели между окнами приложения.

AWTUtilities

Так как уже достаточно давно в JDK6 включили некоторые будущие нововведения из 7ки, не могу обойти их стороной, так как с их помощью возможно много чего сделать приложив при этом гораздо меньшие усилия.
Итак, нас интересует несколько методов из AWTUtilities:
  1. AWTUtilities.setWindowShape(Window, Shape) - позволяет установить любому окну определенную форму (будь то круг или хитрый полигон). Для корректной установки формы окно не должно быть декорировано нативным стилем (setUndecorated(true)).
  2. AWTUtilities.setWindowOpacity (Window, float) – позволяет задать прозрачность окна от 0 (полностью прозрачно) до 1 (непрозрачно). Окно при этом может быть декорировано нативным стилем.
  3. AWTUtilities.setWindowOpaque (Window, boolean) – позволяет полностью спрятать отображение фона и оформления окна, но при этом любой размещенный на нем компонент будет виден. Для корректной установки данного параметра окно также как и в п.1 не должно быть декорировано нативным стилем.

Что же нам это дает? На самом деле - достаточно широкий спектр возможностей. Окнам своего приложения можно задавать любые хитрые формы какие Вам только потребуются, делать «дырки» посреди приложения, создавать кастомные тени под окно, делать приятные на вид попапы и пр. и пр.

Если переходить к конкретике - setWindowShape на деле я никогда не использую, так как задаваемая окну форма строго обрезается по краю и не очень приятно выглядит. На помощь приходит setWindowOpaque - спрятав оформление и фон окна можно с помощью контейнера с кастомным отрисованным фоном создавать абсолютно любые окна. Приведу небольшой пример использования (в нем также есть также использованы некоторые приемы из предыдущих глав поста):

можно взять работающий jar с исходным кодом. Честно скажу, что потратил на данный пример не более десяти минут (из них - минут пять прикидывал как расположить элементы внутри диалога:). Естественно это лишь один из вариантов примемения этих новых возможностей - на самом деле их куда больше.

Единственная неприятная мелочь в использовании AWTUtilities – нестабильная работа на Linux-системах. Т.е. Не везде и не всегда корректно отрабатывает прозрачность окон. Не уверен, проблемы ли это текущей JDK или же ОС.

Создание своих интерактивных компонентов

Я уже поверхностно рассказал о том, как создавать компоненты, UI и некоторые «навороты» для интерфейса своего приложения, но что же делать, если нам нужно добавить функциональную часть к компоненту или создать свой совершенно новый компонент с некоей функциональностью и стилизацией? Стилизовать стандартные компоненты и делать из них отдельные части нового компонента достаточно долгое и нудное занятие, тем более что при малейшем изменении в одном из компонентов вся схема может поехать. Именно в таких случаях стоит сделать свой компонент «с нуля».

Итак, за основу лучше лучше всего взять JComponent и используя paint-методы отрисовать его содержимое. Фактически JСomponent сам по себе - чистый холст с некоторыми зашитыми улучшениями для отрисовками и готовыми стандартными методами setEnabled/setFont/setForeground/setBackground и т.п. Как использовать (и использовать ли их) решать Вам. Все, что Вы будете добавлять в методы отрисовки станет частью компонента и будет отображаться при добавлении его в какой-либо контейнер.

Кстати, небольшое отступление, раз уж зашла речь о контейнерах, - любой наследник и сам JComponent являются контейнерами, т.е. могут содержать в себе другие компоненты, которые будет расположены в зависимости от установленного компоненту лэйаута. Что же творится с отрисовкой чайлд-компонентов, лежащих в данном и как она связана с отрисовкой данного компонента? Ранее я не вдавался подробно в то, как устроены и связаны paint-методы Jcomponent"а, теперь же подробно опишу это…

Фактически, paint() метод содержит в себе вызовы 3ех отдельных методов - paintComponent, paintBorder и paintChildren. Конечно же дополнительно он обрабатывает некоторые «особенные» случаи отрисовки как, например печать или перерисовку отдельной области. Эти три метода всегда вызываются в показанной на изображении выше последовательности. Таким образом сперва идет отрисовка самого компонента, затем поверх рисуется бордер и далее вызывается отрисовка чайлд-компонентов, которые в свою очередь также вызывают свой paint() метод и т.д. Естественно есть еще и различные оптимизации, предотвращающие лишние отрисовки, но об этом подробнее я напишу потом.

Компонент отрисован, но статичен и представляет собой лишь изображение. Нам необходимо обработать возможность управления им мышью и различными хоткеями.
Для этого, во-первых, необходимо добавить соответствующие слушатели (MouseListener/MouseMotionListener/KeyListener) к самому компоненту и обрабатывать отдельные действия.

Чтобы не объяснять все на пальцах, приведу пример компонента, позволяющего визуально ресайзить переданный ему ImageIcon:

можно взять рабочий пример с исходным кодом внутри.

При создании данного компонента я бы выделил несколько важных моментов:

  1. Определяемся с функционалом и внешним видом компонента - в данном случае это область с размещенным на ней изображением, бордером вокруг изображения и 4мя ресайзерами по углам. Каждый из ресайзеров позволяет менять размер изображения. Также есть возможность передвигать изображение по области, «схватив» его за центр.
  2. Определяем все необходимые для работы компонента параметры - в данном случае это само изображение и его «опорные» точки (верхний левый и правый нижний углы). Также есть ряд переменных, которые понадобятся при реализации ресайза и драга изображения.
  3. Накидываем заготовку для компонента (желательно отдельный класс, если Вы собираетесь его использовать более раза) - в данном случае создаем класс ImageResizeComponent, определяем все необходимые для отрисовки параметры, переопределяем метод paintComponent() и отрисовываем содержимое. Также переопределяем метод getPreferredSize(), чтобы компонент сам мог определить свой «желаемый» размер.
  4. Реализовываем функциональную часть компонента - в данном случае нам будет достаточно своего MouseAdapter"а для реализации ресайза и передвижения. При нажатии мыши в области проверяем координаты и сверяем имх с координатами углов и самого изображения - если нажатие произошло в районе некого угла - запоминаем его и при драге изменяем его координату, ежели нажатие пришлось на изображение - запоминаем начальные его координаты и при перетаскивании меняем их. И наконец, последний штрих - в mouseMoved() меняем курсор в зависимости от контрола под мышью.
Ничего сложного, правда? С реализацией «кнопочных» частей других компонентов всё еще проще - достаточно проверять, что нажатие пришлось в область кнопки. Параллельно с отслеживанием событий можно также визуально менять отображение компонента (как сделано в данном примере на ресайзерах). В общем сделать можно всё, на что хватит фантазии.

Конечно, я просто описал свои шаги при создании компонентов - они не являются чем-то обязательным и могут быть легко расширены и дополнены.

Важно помнить

Есть несколько вещей, которые стоит при любой работе с итерфейсными элементами в Swing – приведу их в этой отдельной небольшой главе.
  1. Любые операции вызова перерисовки/обновления компонентов должны производится внутри Event Dispatch потока для избежания визуальных «подвисаний» интерфейса Вашего приложения. Выполнить любой код в Event Dispatch потоке достаточно легко:
    SwingUtilities.invokeLater (new Runnable()
    {
    public void run ()
    {
    // Здесь располагаете исполняемый код
    }
    });
    Важно также помнить, что события, вызываемые из различных listener"ов стандартных компонентов (ActionListener/MouseListener и пр.) исходно вызываются из этого потока и не требуют дополнительной обработки.
  2. Из любых paint-методов ни в коем случае нельзя влиять на интерфейс, так как это может привести к зацикливаниям или дедлокам в приложении.
  3. Представьте ситуацию - из метода paint Вы изменяете состояние кнопки, скажем, через setEnabled(enabled). Вызов этого метода заставляет компонент перерисоваться и заново вызвать метод paint и мы опять попадаем на этот злополучный метод. Отрисовка кнопки зациклиться и интерфейс приложения будет попросту висеть в ожидании завершения отрисовки (или как минимум съест добрую часть процессора). Это самый простой вариант подобной ошибки.
  4. Не стоит производить тяжёлые вычисления внутри Event Dispatch потока. Вы можете произвести эти вычисления в отдельном обычном потоке и затем только вызвать обновление через SwingUtilities.invokeLater().
  5. Не стоит, также, производить тяжёлые вычисления внутри методов отрисовки. По возможности стоит выносить их отдельно и кэшировать/запоминать. Это позволит ускорить отрисовку компонентов, улучшить общую отзывчиввость приложения и снизить нагрузку на Event Dispatch поток.
  6. Для обвноления внешнего вида компонентов используйте метод repaint() (или же repaint(Rectangle) – если Вам известна точная область для обновления), сам метод repaint необходимо исполнять в Event Dispatch потоке. Для обновления же расположения элементов в лэйауте используйте метод revalidate() (его нет необходимости исполнять в Event Dispatch потоке). Метод updateUI() может помочь в некоторых случаях для полного обновления элемента (например смене данных таблицы), но его нужно использовать аккуратно, так как он также отменит установленный Вами UI и возьмет UI предоставляемый текущим LaF"ом.
  7. Полная установка LaF всему приложению отменит любые ваши вручную заданные в ходе работы UI компонентов и установит поверх них те UI, которые предлагает устанавливаемый LaF. Поэтому лучше производить установку LaF при загрузке приложения до открытия каких-либо окон и показа визуальных элементов.
Следовние этим простым пунктам позволит Вам не беспокоиться о возникновении непредвиденных «тормозов» или дедлоков в интерфейсе приложения.
Думаю, что этот список можно дополнить еще несколькими пунктами, но они уже будут весьма специфичны/необязательны.

Итоги

Используя, совмещая и варьируя вышеописанные возможности и особенности работы с интерфейсами возможно добиться любого желаемого результата. Необходимо лишь проявить немного фантазии в этом деле. Надеюсь даже базовые примеры данного поста дали Вам достаточно много пищи для размышлений и, возможно, помогли с чем-либо.

Еще хотелось бы добавить, что возможно, название статьи может показаться Вам слишком громким и не полностью раскрытым - действительно, я все же не дизайнер и не юзабилити-специалист. Я лишь предлагаю Вам инструменты/способы, которые позволят расширить и улучшить интерфейс приложения. Непосредственно бОльшая (не обязательно вся) часть работы по созданию графики для слайдера/кнопки или определению общего стиля компонентов и самого приложения всегда остается за дизайнером - от этого никуда не уйти.

Все примеры статьи в едином «флаконе». Из начального окна можно выбрать желаемый пример: Добавить метки

Интерфейс – это контракт, в рамках которого части программы, зачастую написанные разными людьми, взаимодействуют между собой и со внешними приложениями. Интерфейсы работают со слоями сервисов, безопасности, DAO и т.д. Это позволяет создавать модульные конструкции, в которых для изменения одного элемента не нужно трогать остальные.

Новички часто спрашивают, чем интерфейс отличается от абстрактного класса . Интерфейсы в Java компенсируют отсутствие множественного наследования классов. У класса-потомка может быть только один абстрактный класс-родитель, а вот интерфейсов класс может применять (имплементировать) сколько угодно.

Интерфейс на Java объявляют примерно так же, как и класс:

public interface MyInterface { void do_something (){ // ... } default void say_goodbye (String userName ) { System . out. println("Пока, " + userName+ "! Заходи ещё." ); } }

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

Методы по умолчанию впервые появились в Java 8. Их обозначают модификатором default. В нашем примере это метод say_goodbye, реализация которого прописана прямо в интерфейсе. Дефолтные методы изначально готовы к использованию, но при необходимости их можно переопределять в применяющих интерфейс классах.

Функциональный интерфейс Java

Если у интерфейса только один абстрактный метод, перед нами функциональный интерфейс. Его принято помечать аннотацией @FunctionalInterface, которая указывает компилятору, что при обнаружении второго абстрактного метода в этом интерфейсе нужно сообщить об ошибке. Стандартных (default) методов у интерфейса может быть множество – в том числе принадлежащих классу java.lang.Object.

Как выглядит функциональный интерфейс на Java:

@FunctionalInterface public interface GaMechanic { void new_deck(); default int deal_cards(int num_of_players) { // тело метода } default int check_your_cards(int hand) { //... } default int battle(Card player1_Card, Card player2_Card) { //... } }

Функциональные интерфейсы появились в Java 8. Они обеспечили поддержку лямбда-выражений, использование которых делает код лаконичным и понятным:

Button. setOnAction(event - > // если происходит событие System . out. println("Обрабатываем нажатие кнопки." ));

В той же версии появились пакеты встроенных интерфейсов: java.util.function и java.util.stream.

Реализация интерфейсов классами Java

Допустим, есть интерфейс Edible, которым пользуются классы Fruit, Vegetable, Fish. Экземпляры этих классов можно создавать так:

Edible a1 = new Fruit ("Яблоко" , “Антоновка”); Edible a2 = new Fish ("Нерка слабосолёная" , “Тихий океан”, 150 );

Хорошим тоном считается давать интерфейсам названия с окончанием -able/-ible - это показывает, что с объектами, имплементирующими интерфейс, можно что-то делать: Edible (можно есть), Moveable (можно двигать), Clickable (реагирует на клик) и т.д.

Обратите внимание на разницу в конструкторах: для фруктов задаём название и сорт, для рыбы – название, район вылова и вес порции в граммах. Но ссылки на оба объекта храним в переменных одного типа – «Съестное».

Интерфейсы и полиморфизм

Пример выше иллюстрирует один из трех основополагающих принципов ООП - полиморфизм. Мы раскрыли одно и то же явление - съедобность - через несколько классов, свойства и методы которых частично отличаются. Представление разных форм одного явления - это и есть полиморфизм. Если нужно, такую систему всегда можно расширить и скорректировать. В нашем случае - добавить новые виды съестного и методы их приготовления.

В Java полиморфизм можно реализовать через:

  • наследование - с переопределением параметров и методов базового класса;
  • абстрактные классы - шаблоны для раздельной реализации в разных классах;
  • интерфейсы - для имплементации классами.

Интерфейс выручает в ситуации, когда при создании переменной мы не знаем, объект какого класса ей будет присвоен.

Чтобы было проще понять интерфейсы в Java, можно их представлять как полностью абстрактные классы, то есть классы все методы которых не имеют реализации, а только лишь объявлены. Но так было до Java 8, в которой появилась возможность создавать в интерфейсе реализацию метода по умолчанию. Интерфейсы так же могут содержать поля, но все они будут объявлены как final и static , то есть по существу являются константами. Кроме того, интерфейсы служат для реализации подобия множественного наследования в Java, которое в чистом виде в Java не поддерживается.

Как уже говорилось, унаследоваться (extends ) в Java можно только от одного класса, каждый класс В или С происходит из неполной семьи, как показано на рисунке а ниже. Все классы происходят только от "Адама", от класса Object. Но часто возникает необходимость породить класс D от двух классов B и С, как показано на рисунке б . Это называется множественным наследованием (multiple inheritance ). В множественном наследовании нет ничего плохого. Трудности возникают, если классы B и C сами порождены от одного класса А, как показано на рисунке в . Это так называемое "ромбовидное" наследование.

В самом деле, пусть в классе А определен метод f(), к которому мы обращаемся из некоторого метода класса D. Можем мы быть уверены, что метод f() выполняет то, что написано в классе А, т. е. это метод A.f()? Может, он переопределен в классах B и С? Если так, то каким вариантом мы пользуемся: В.f() или C.f()? Конечно, допустимо определить экземпляры классов и обращаться к методам этих экземпляров, но это совсем другая ситуация.

В различных языках программирования этот вопрос решается по-разному, главным образом уточнением имени метода f(). Но при этом всегда нарушается принцип KISS. Вокруг множественного наследования всегда много споров, есть его ярые приверженцы и столь же ярые противники. Не будем встревать в эти споры, наше дело — наилучшим образом использовать средства языка для решения своих задач.

Создатели языка Java после долгих споров и размышлений поступили радикально — запретили множественное наследование классов вообще. При расширении класса после слова extends можно написать только одно имя суперкласса. С помощью уточнения super можно обратиться только к членам непосредственного суперкласса.

Но что делать, если все-таки при порождении надо использовать несколько предков? Например, у нас есть общий класс автомобилей Automobile, от которого можно породить класс грузовиков Truck и класс легковых автомобилей Car. Но вот надо описать пикап Pickup. Этот класс должен наследовать свойства и грузовых, и легковых автомобилей.

В таких случаях используется еще одна конструкция языка Java — интерфейс. Внимательно проанализировав ромбовидное наследование, теоретики ООП выяснили, что проблему создает только реализация методов, а не их описание.

Интерфейс (interface ), в отличие от класса, содержит только константы, заголовки методов, и с Java 8 может содержать реализацию методов по умолчанию.

Интерфейсы тоже размещаются в файлах с расширением.java, которые в свою очередь могут находится в пакетах и подпакетах, часто в тех же самых, что и классы, и тоже компилируются в class-файлы.

Описание интерфейса начинается со слова interface , перед которым может стоять модификатор public , означающий, как и для класса, что интерфейс доступен всюду. Если же модификатора public нет, интерфейс будет виден только в своем пакете.

После слова interface записывается имя интерфейса, потом может стоять слово extends и список интерфейсов-предков через запятую. Таким образом, одни интерфейсы могут порождаться от других интерфейсов, образуя свою, независимую от классов, иерархию, причем в ней допускается множественное наследование интерфейсов. В этой иерархии нет корня, общего предка.

Затем в фигурных скобках записываются в любом порядке константы, заголовки методов и реализации методов по умолчанию. Можно сказать, что в интерфейсе все методы абстрактные (те у которых нет реализации по умолчанию), но слово abstract писать не надо. Константы всегда статические, но слова static и final указывать не нужно. Все эти модификаторы принимаются по умолчанию. Все константы и методы в интерфейсах всегда открыты, не обязательно даже указывать модификатор public .

Вот какую схему можно предложить для иерархии автомобилей:

interface Automobile{ . . . }
interface Car extends Automobile{ . . . }
interface Truck extends Automobile{ . . . }
interface Pickup extends Car, Truck{ . . . }

Таким образом, интерфейс — это только набросок, эскиз. В нем указано, что делать, но не указано, как это делать (если нет реализации по умолчанию).

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

Как же тогда использовать интерфейсы, если невозможно создать объекты на их основе?

Использовать нужно не интерфейс, а его реализацию (implementation ). Реализация интерфейса — это класс, в котором расписываются методы одного или нескольких интерфейсов. В заголовке класса после его имени или после имени его суперкласса, если он есть, записывается слово implements и, через запятую, перечисляются имена интерфейсов.

Вот как можно реализовать иерархию автомобилей:

interface Automobile{ . . . }
interface Car extends Automobile{ . . . }
class Truck implements Automobile{ . . . }
class Pickup extends Truck implements Car{ . . . }

interface Automobile{ . . . }
interface Car extends Automobile{ . . . }
interface Truck extends Automobile{ . . . }
class Pickup implements Car, Truck{ . . . }

Реализация интерфейса может быть неполной, некоторые методы интерфейса могут быть реализованы, а другие — нет. Такая реализация — абстрактный класс, его обязательно надо пометить модификатором abstract .

Если класс реализует более одного интерфейса, то он должен предоставить реализацию каждого метода каждого интерфейса либо он должен быть объявлен как abstract .

Методы, которые реализуют интерфейс, должны быть объявлены как public .

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

Слева представлены основной класс с методом main() и интерфейс в котором мы опеделили метод getSound() с реализацией по умолчанию, которая будет срабатывать если в классе имплементирующем этот интерфейс не будет описана реализация данного метода.

Стоит обратить внимание что в классе AnimalSound мы создали массив объектных ссылок типа Soud (интерфейсный тип) и поместили туда ссылки на объекты, реализующие данный интерфейс.

Программа генерирует следующий вывод:

И теперь посмотрим на классы Cow, Cat и Dog:

Как видно наши классы Cow, Cat и Dog которые имплементируют интерфейс Sound имеют каждый свою реализацию метода getSound().

И приведу еще один простой пример, того что класс может реализовывать несколько интерфейсов.\

Как видно из примера класс MaxMin реализует два интрефейса.

Скажу сразу - при работе с графикой, скорее всего, со временем Вам придется воспользоваться всеми предоставленными инструментами без исключения для достижения наилучшего визуального эффекта. Подробное описание о том «что и как» можно найти - это официальный туториал по Graphics2D. Его должно быть более чем достаточно, чтобы ввести Вас в курс дела.

Я уже привел небольшой пример написания своего UI, но есть и другие варианты кастомизации интерфейса. Каждый отдельный J-компонент производит свою Lightweight-отрисовку при помощи метода paint(), который можно легко переопределить и изменить. Напрямую (не всегда, но чаще всего) его лучше не использовать (не буду вдаваться в подробности, так как это целая тема для отдельного топика). Для следующего примера используем метод paintComponent(). Рассмотрим как его можно применить поближе…

Начну с примера - текстовое поле с визуальным фидбэком при отсутствии содержимого:

JTextField field = new JTextField()
{
private boolean lostFocusOnce = false ;
private boolean incorrect = false ;

{
// Слушатели для обновления состояния проверки
addFocusListener (new FocusAdapter()
{
public void focusLost (FocusEvent e)
{
lostFocusOnce = true ;

repaint ();
}
});
addCaretListener (new CaretListener()
{
public void caretUpdate (CaretEvent e)
{
if (lostFocusOnce)
{
incorrect = getText ().trim ().equals ("" );
}
}
});
}

protected void paintComponent (Graphics g)
{
super.paintComponent (g);

// Расширенная отрисовка при некорректности данных
if (incorrect)
{
Graphics2D g2d = (Graphics2D) g;

// Включаем антиалиасинг для гладкой отрисовки
g2d.setRenderingHint (RenderingHints.KEY_ANTIALIASING,
RenderingHints.VALUE_ANTIALIAS_ON);

// Получаем отступы внутри поля
Insets insets;
if (getBorder () == null )
{
insets = new Insets (2, 2, 2, 2);
}
else
{
insets = getBorder ().getBorderInsets (this );
}

// Создаем фигуру в виде подчеркивания текста
GeneralPath gp = new GeneralPath (GeneralPath.WIND_EVEN_ODD);
gp.moveTo (insets.left, getHeight () - insets.bottom);
for (int i = 0; i < getWidth () - insets.right - insets.left; i += 3)
{
gp.lineTo (insets.left + i,
getHeight () - insets.bottom - ((i / 3) % 2 == 1 ? 2: 0));
}

// Отрисовываем её красным цветом
g2d.setPaint (Color.RED);
g2d.draw (gp);
}
}
};

Наличие содержимого перепроверяется при печати и потере фокуса полем. Переключившись на другой компонент мы увидим как отрисовывается наше дополнение к JTextField"у:

Полный код примера можно взять .

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

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

За основу берутся 8 изображений 16х16 - 4 состояния фона чекбокса и 4 состояния галки (5 на самом деле, но 5ое мы добавим програмно):

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

public static List BG_STATES = new ArrayList ();
public static List CHECK_STATES = new ArrayList ();

static
{
// Иконки состояния фона
for (int i = 1; i <= 4; i++)
{
BG_STATES.add (new ImageIcon (
MyCheckBox.class .getResource ("icons/states/" + i + ".png" )));
}

// Дополнительное "пустое" состояние выделения

new BufferedImage (16, 16, BufferedImage.TYPE_INT_ARGB)));

// Состояния выделения
for (int i = 1; i <= 4; i++)
{
CHECK_STATES.add (new ImageIcon (
MyCheckBox.class .getResource ("icons/states/c" + i + ".png" )));
}
}

private Map iconsCache = new HashMap ();

private synchronized void updateIcon ()
{
// Обновляем иконку чекбокса
final String key = bgIcon + "," + checkIcon;
if (iconsCache.containsKey (key))
{
// Необходимая иконка уже была ранее использована
setIcon (iconsCache.get (key));
}
else
{
// Создаем новую иконку совмещающую в себе фон и состояние поверх
BufferedImage b = new BufferedImage (BG_STATES.get (0).getIconWidth (),
BG_STATES.get (0).getIconHeight (), BufferedImage.TYPE_INT_ARGB);
Graphics2D g2d = b.createGraphics ();
g2d.drawImage (BG_STATES.get (bgIcon).getImage (), 0, 0,
BG_STATES.get (bgIcon).getImageObserver ());
g2d.drawImage (CHECK_STATES.get (checkIcon).getImage (), 0, 0,
CHECK_STATES.get (checkIcon).getImageObserver ());
g2d.dispose ();

ImageIcon icon = new ImageIcon (b);
iconsCache.put (key, icon);
setIcon (icon);
}
}

Остается добавить несколько обработчиков переходов состояний и мы получим анимированный переход между ними:

Получившийся компонент очень легко будет видоизменить к примеру в радиобаттон, или же добавить в него больше состояний перехода для более плавной анимации и т.п.
Собственно, полный рабочий код и изображения выложил отдельно .

Таким образом путей кастомизации элементов есть достаточно много (даже я, наверно, не знаю/не догадываюсь о некоторых из них). Какой способ выбирать - зависит от ситуации - что необходимо получить, какие изменения возможно сделать в существующем компоненте и т.д.

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


Не буду врать, на данный UI кнопки ушло достаточно много времени, но он создавался без помощи и подсказок дизайнера чистыми средствами Graphics2D и Swing. можно скачать и изучить полный сурс и демо данного UI, если Вам стало интересно. В нем использован достаточно большой спектр возможностей Graphics2D и применены некоторые хитрости, которые могут зачастую оказаться полезными.

Итак, думаю достаточно разговоров о графике - о ней более подробно я расскажу в будущих топиках, а сейчас приведу немного интересного материала, который я наработал за достаточно долгое время «общения» со Swing и Graphics2D.

DnD и GlassPane

Думаю первое - Вам всем более чем известно, как и проблемы с ним связанные. Насчет второго - вероятно Вы вскользь слышали о GlassPane или может даже видели это старинное изображение (которое до сих пор актуально, между прочем) об устройстве различных слоев стандартных фреймов. Что же тут такого и зачем я вспомнил об этом? И тем более, как связаны DnD и GlassPane спросите Вы? Вот именно о том, как их связать и что из этого может выйти я и хочу рассказать в этой главе.

Чтож, начнем по порядку - что мы знаем о DnD?
У некоторых Swing-компонентов есть готовые реализации для драга (JTree и JList к примеру) - для других можно достаточно легко дописать свою. Чтобы не бросаться словами на ветер - приведу небольшой пример DnD стринга из лэйбла:

JLabel label = new JLabel ("Небольшой текст для DnD" );
label.setTransferHandler (new TransferHandler()
{
public int getSourceActions (JComponent c)
{
return TransferHandler.COPY;
}

public boolean canImport (TransferSupport support)
{
return false ;
}

protected Transferable createTransferable (JComponent c)
{
return new StringSelection (((JLabel) c).getText ());
}
});
label.addMouseListener (new MouseAdapter()
{
public void mousePressed (MouseEvent e)
{
if (SwingUtilities.isLeftMouseButton (e))
{
JComponent c = (JComponent) e.getSource ();
TransferHandler handler = c.getTransferHandler ();
handler.exportAsDrag (c, e, TransferHandler.COPY);
}
}
});

Теперь возможно перетащить текст данного лэйбла прямо из интерфейса в любое другое место, куда возможно вставить текст через дроп.
Фактически - TransferHandler овечает за то, какие данные при драге отдает компонент и как компонент использует входящие данные при драге на него.

Но что делать, если необходимо отследить последовательность действий пользователя при самом перетаскивании?
Для этого есть отдельная возможность повесить слушатель:

DragSourceAdapter dsa = new DragSourceAdapter()
{
public void dragEnter (DragSourceDragEvent dsde)
{
// При входе драга в область какого-либо компонента
}

public void dragExit (DragSourceEvent dse)
{
// При выходе драга в область какого-либо компонента
}

public void dropActionChanged (DragSourceDragEvent dsde)
{
// При смене действия драга
}

public void dragOver (DragSourceDragEvent dsde)
{
// При возможности корректного завершения драга
}

public void dragMouseMoved (DragSourceDragEvent dsde)
{
// При передвижении драга
}

public void dragDropEnd (DragSourceDropEvent dsde)
{
// При завершении или отмене драга
}
};
DragSource.getDefaultDragSource ().addDragSourceListener (dsa);
DragSource.getDefaultDragSource ().addDragSourceMotionListener (dsa);

Остался последний момент - обозначить роль GlassPane. GlassPane, фактически, позволяет располагать/отрисовывать на себе компоненты, как и любой другой контейнер, но его особенность в том, что он лежит поверх всех Swing-компонентов, когда виден. Т.е. если мы что-либо отрисуем на нем, то оно накроет весь находящийся под ним интерфейс. Это позволяет размещать компоненты независимо от основного контейнера в любом месте, создавать любые визуальные эффекты и делать другие занятные вещи.

Приведу для большего понимания небольшой пример подобного «эффекта» - фрейм с несколькими Swing-компонентами на нем. При клике в любой части окна будет появляться эффект «распозающегося» круга, который виден поверх всех элементов. Что самое интересное - подобный эффект не съедает ресурсов и не требует большой груды кода. Не верите? - посмотрите демо и загляните в исходник, вложенный в jar.

Кстати говоря, есть достаточно интересная библиотека на эту тему, заодно предоставляющая дополнительный скролл-функционал и несколько других вкусностей - JXLayer (офф сайт) (описание #1 описание #2 описание #3). К сожалению проекты хостящиеся на сайте java сейчас находтся не в лучшем состоянии, поэтому приходится ссылаться на отдельные ресурсы.

Итак теперь объединим всё что я уже описал в данной главе и сделаем, наконец, что-то полноценное. К примеру - отображение драга панели с компонентами внутри окна:

При драге панели за лэйбл появляется полупрозрачная копия панели показывающая где именно будет размещена панель при завершении драга. Также клавишей ESC можно отменить перемещение.
Рабочий пример и исходный код можно взять .

Конечно, для реализации именно данной функциональности не стоило бы прибегать к использованию DnD - есть более короткие пути. Впрочем, всё зависит от ситуации. Данный вариант позволяет отрисовывать перетаскивание независимо от других компонентов в окне. Также можно на основе него, к примеру, реализовать драг панели между окнами приложения.

AWTUtilities

Так как уже достаточно давно в JDK6 включили некоторые будущие нововведения из 7ки, не могу обойти их стороной, так как с их помощью возможно много чего сделать приложив при этом гораздо меньшие усилия.
Итак, нас интересует несколько методов из AWTUtilities:
  1. AWTUtilities.setWindowShape(Window, Shape) - позволяет установить любому окну определенную форму (будь то круг или хитрый полигон). Для корректной установки формы окно не должно быть декорировано нативным стилем (setUndecorated(true)).
  2. AWTUtilities.setWindowOpacity (Window, float) – позволяет задать прозрачность окна от 0 (полностью прозрачно) до 1 (непрозрачно). Окно при этом может быть декорировано нативным стилем.
  3. AWTUtilities.setWindowOpaque (Window, boolean) – позволяет полностью спрятать отображение фона и оформления окна, но при этом любой размещенный на нем компонент будет виден. Для корректной установки данного параметра окно также как и в п.1 не должно быть декорировано нативным стилем.

Что же нам это дает? На самом деле - достаточно широкий спектр возможностей. Окнам своего приложения можно задавать любые хитрые формы какие Вам только потребуются, делать «дырки» посреди приложения, создавать кастомные тени под окно, делать приятные на вид попапы и пр. и пр.

Если переходить к конкретике - setWindowShape на деле я никогда не использую, так как задаваемая окну форма строго обрезается по краю и не очень приятно выглядит. На помощь приходит setWindowOpaque - спрятав оформление и фон окна можно с помощью контейнера с кастомным отрисованным фоном создавать абсолютно любые окна. Приведу небольшой пример использования (в нем также есть также использованы некоторые приемы из предыдущих глав поста):

можно взять работающий jar с исходным кодом. Честно скажу, что потратил на данный пример не более десяти минут (из них - минут пять прикидывал как расположить элементы внутри диалога:). Естественно это лишь один из вариантов примемения этих новых возможностей - на самом деле их куда больше.

Единственная неприятная мелочь в использовании AWTUtilities – нестабильная работа на Linux-системах. Т.е. Не везде и не всегда корректно отрабатывает прозрачность окон. Не уверен, проблемы ли это текущей JDK или же ОС.

Создание своих интерактивных компонентов

Я уже поверхностно рассказал о том, как создавать компоненты, UI и некоторые «навороты» для интерфейса своего приложения, но что же делать, если нам нужно добавить функциональную часть к компоненту или создать свой совершенно новый компонент с некоей функциональностью и стилизацией? Стилизовать стандартные компоненты и делать из них отдельные части нового компонента достаточно долгое и нудное занятие, тем более что при малейшем изменении в одном из компонентов вся схема может поехать. Именно в таких случаях стоит сделать свой компонент «с нуля».

Итак, за основу лучше лучше всего взять JComponent и используя paint-методы отрисовать его содержимое. Фактически JСomponent сам по себе - чистый холст с некоторыми зашитыми улучшениями для отрисовками и готовыми стандартными методами setEnabled/setFont/setForeground/setBackground и т.п. Как использовать (и использовать ли их) решать Вам. Все, что Вы будете добавлять в методы отрисовки станет частью компонента и будет отображаться при добавлении его в какой-либо контейнер.

Кстати, небольшое отступление, раз уж зашла речь о контейнерах, - любой наследник и сам JComponent являются контейнерами, т.е. могут содержать в себе другие компоненты, которые будет расположены в зависимости от установленного компоненту лэйаута. Что же творится с отрисовкой чайлд-компонентов, лежащих в данном и как она связана с отрисовкой данного компонента? Ранее я не вдавался подробно в то, как устроены и связаны paint-методы Jcomponent"а, теперь же подробно опишу это…

Фактически, paint() метод содержит в себе вызовы 3ех отдельных методов - paintComponent, paintBorder и paintChildren. Конечно же дополнительно он обрабатывает некоторые «особенные» случаи отрисовки как, например печать или перерисовку отдельной области. Эти три метода всегда вызываются в показанной на изображении выше последовательности. Таким образом сперва идет отрисовка самого компонента, затем поверх рисуется бордер и далее вызывается отрисовка чайлд-компонентов, которые в свою очередь также вызывают свой paint() метод и т.д. Естественно есть еще и различные оптимизации, предотвращающие лишние отрисовки, но об этом подробнее я напишу потом.

Компонент отрисован, но статичен и представляет собой лишь изображение. Нам необходимо обработать возможность управления им мышью и различными хоткеями.
Для этого, во-первых, необходимо добавить соответствующие слушатели (MouseListener/MouseMotionListener/KeyListener) к самому компоненту и обрабатывать отдельные действия.

Чтобы не объяснять все на пальцах, приведу пример компонента, позволяющего визуально ресайзить переданный ему ImageIcon:

можно взять рабочий пример с исходным кодом внутри.

При создании данного компонента я бы выделил несколько важных моментов:

  1. Определяемся с функционалом и внешним видом компонента - в данном случае это область с размещенным на ней изображением, бордером вокруг изображения и 4мя ресайзерами по углам. Каждый из ресайзеров позволяет менять размер изображения. Также есть возможность передвигать изображение по области, «схватив» его за центр.
  2. Определяем все необходимые для работы компонента параметры - в данном случае это само изображение и его «опорные» точки (верхний левый и правый нижний углы). Также есть ряд переменных, которые понадобятся при реализации ресайза и драга изображения.
  3. Накидываем заготовку для компонента (желательно отдельный класс, если Вы собираетесь его использовать более раза) - в данном случае создаем класс ImageResizeComponent, определяем все необходимые для отрисовки параметры, переопределяем метод paintComponent() и отрисовываем содержимое. Также переопределяем метод getPreferredSize(), чтобы компонент сам мог определить свой «желаемый» размер.
  4. Реализовываем функциональную часть компонента - в данном случае нам будет достаточно своего MouseAdapter"а для реализации ресайза и передвижения. При нажатии мыши в области проверяем координаты и сверяем имх с координатами углов и самого изображения - если нажатие произошло в районе некого угла - запоминаем его и при драге изменяем его координату, ежели нажатие пришлось на изображение - запоминаем начальные его координаты и при перетаскивании меняем их. И наконец, последний штрих - в mouseMoved() меняем курсор в зависимости от контрола под мышью.
Ничего сложного, правда? С реализацией «кнопочных» частей других компонентов всё еще проще - достаточно проверять, что нажатие пришлось в область кнопки. Параллельно с отслеживанием событий можно также визуально менять отображение компонента (как сделано в данном примере на ресайзерах). В общем сделать можно всё, на что хватит фантазии.

Конечно, я просто описал свои шаги при создании компонентов - они не являются чем-то обязательным и могут быть легко расширены и дополнены.

Важно помнить

Есть несколько вещей, которые стоит при любой работе с итерфейсными элементами в Swing – приведу их в этой отдельной небольшой главе.
  1. Любые операции вызова перерисовки/обновления компонентов должны производится внутри Event Dispatch потока для избежания визуальных «подвисаний» интерфейса Вашего приложения. Выполнить любой код в Event Dispatch потоке достаточно легко:
    SwingUtilities.invokeLater (new Runnable()
    {
    public void run ()
    {
    // Здесь располагаете исполняемый код
    }
    });
    Важно также помнить, что события, вызываемые из различных listener"ов стандартных компонентов (ActionListener/MouseListener и пр.) исходно вызываются из этого потока и не требуют дополнительной обработки.
  2. Из любых paint-методов ни в коем случае нельзя влиять на интерфейс, так как это может привести к зацикливаниям или дедлокам в приложении.
  3. Представьте ситуацию - из метода paint Вы изменяете состояние кнопки, скажем, через setEnabled(enabled). Вызов этого метода заставляет компонент перерисоваться и заново вызвать метод paint и мы опять попадаем на этот злополучный метод. Отрисовка кнопки зациклиться и интерфейс приложения будет попросту висеть в ожидании завершения отрисовки (или как минимум съест добрую часть процессора). Это самый простой вариант подобной ошибки.
  4. Не стоит производить тяжёлые вычисления внутри Event Dispatch потока. Вы можете произвести эти вычисления в отдельном обычном потоке и затем только вызвать обновление через SwingUtilities.invokeLater().
  5. Не стоит, также, производить тяжёлые вычисления внутри методов отрисовки. По возможности стоит выносить их отдельно и кэшировать/запоминать. Это позволит ускорить отрисовку компонентов, улучшить общую отзывчиввость приложения и снизить нагрузку на Event Dispatch поток.
  6. Для обвноления внешнего вида компонентов используйте метод repaint() (или же repaint(Rectangle) – если Вам известна точная область для обновления), сам метод repaint необходимо исполнять в Event Dispatch потоке. Для обновления же расположения элементов в лэйауте используйте метод revalidate() (его нет необходимости исполнять в Event Dispatch потоке). Метод updateUI() может помочь в некоторых случаях для полного обновления элемента (например смене данных таблицы), но его нужно использовать аккуратно, так как он также отменит установленный Вами UI и возьмет UI предоставляемый текущим LaF"ом.
  7. Полная установка LaF всему приложению отменит любые ваши вручную заданные в ходе работы UI компонентов и установит поверх них те UI, которые предлагает устанавливаемый LaF. Поэтому лучше производить установку LaF при загрузке приложения до открытия каких-либо окон и показа визуальных элементов.
Следовние этим простым пунктам позволит Вам не беспокоиться о возникновении непредвиденных «тормозов» или дедлоков в интерфейсе приложения.
Думаю, что этот список можно дополнить еще несколькими пунктами, но они уже будут весьма специфичны/необязательны.

Итоги

Используя, совмещая и варьируя вышеописанные возможности и особенности работы с интерфейсами возможно добиться любого желаемого результата. Необходимо лишь проявить немного фантазии в этом деле. Надеюсь даже базовые примеры данного поста дали Вам достаточно много пищи для размышлений и, возможно, помогли с чем-либо.

Еще хотелось бы добавить, что возможно, название статьи может показаться Вам слишком громким и не полностью раскрытым - действительно, я все же не дизайнер и не юзабилити-специалист. Я лишь предлагаю Вам инструменты/способы, которые позволят расширить и улучшить интерфейс приложения. Непосредственно бОльшая (не обязательно вся) часть работы по созданию графики для слайдера/кнопки или определению общего стиля компонентов и самого приложения всегда остается за дизайнером - от этого никуда не уйти.

Все примеры статьи в едином «флаконе». Из начального окна можно выбрать желаемый пример:

  • interface
  • dnd
  • customization
  • интерфейс
  • кастомизация
  • Добавить метки
    • Сергей Савенков

      какой то “куцый” обзор… как будто спешили куда то