Разработка → Улучшаем интерфейс Java-приложения

mgarin 18 апреля 2011 в 14:57 71,7k
Добрый день, Хабражитель!

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

Итак, в данном посте я постарался изложить самые важные и значимые на мой взгляд моменты по работе со Swing и графикой — как создавать компоненты, как стилизовать интерфейс, чего делать не стоит и многое другое…


Витая в облаках


Думаю, что многих удерживает от изучения или разработки на Java вопросы «как сделать оформление своего приложения ярким и запоминающимся?» и «разве это возможно на Java?» и увы, действительно практически нет ни одного крупного ресурса по данной теме, которые бы полностью или хотя бы частично пролили свет на ситуацию.

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

Что же мы имеем?


Пожалуй стоит начать с основ, оттолкнувшись от которых можно будет обсудить «более высокие материи».

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

Итак, построение интерфейса Java-приложения основывается на библиотеке Swing, которая содержит в себе все базовые компоненты, которые возможно встретить в различных ОС. Они могут выглядеть по разному, иметь немного различные функциональные особенности (на различных ОС), но по сути — они выполняют одно и то же предназначение. Это такие компоненты как кнопки (JButton), текстовые поля (JTextField, JPasswordField), лэйблы (JLabel), текстовые области (JTextArea, JEditorPane), скроллы (JScrollBar, JScrollPane) и пр.

Swing в «базовой комплектации» позволяет использовать несколько разных LaF'ов (Look and Feel, можно также назвать их «скинами» к Swing) для изменения внешнего вида всех компонентов Вашего приложения «разом» — Metal LaF (стандартный Java-стиль), Nimbus LaF (просто отдельный специфичный стиль), System LaF (стиль Вашей ОС). Впрочем есть много отдельно дописанных LaF'ов оформленных в полноценные библиотеки, которые можно легко и быстро использовать в любом проекте.
У каждого отдельного компонента есть возможность задания UI-класса, который определяет как он будет отрисован и иногда — какую функциональность он будет иметь, всё зависит от реализации. Непосредственно LaF определяет UI сразу для всего дерева компонентов, доступных в Вашем приложении. Вы можете как использовать LaF, так и менять стиль отдельных компонентов задавая им UI. Также можно выставить определенный LaF (системный, например) и затем, при необходимости, изменить UI отдельных элементов.

Приведу несколько ресурсов с различными кастомными LaF'ами, которые можно быстро приспособить для использования:

Отдельные наиболее известные и проработанные стили

Tiny LaF — аналог классического стиля Windows XP


Quaqua LaF — аналог Mac OS X интерфейса с множеством возможностей по кастомизации компонентов


Substance LaF


Synthetica LaF


Alloy LaF
image

JGoodies


Несколько наборов стилей

Набор #1 — сайт посвященный различным известным LaF для Java-приложений
Набор #2 — небольшая статья со списком разных LaF
Набор #3 – еще одна статья на тему LaF

Важно также отметить, что не все LaF'ы могут быть использованы под любой ОС. Некоторые специализируются на определенных ОС, как к примеру Quaqua LaF. Его, конечно, возможно запустить под Windows или Linux, и на первый взгляд проблем не возникнет, но некоторые компоненты могут вызывать ошибки при работе, вплоть до падения JVM. Некоторые другие стили могут и вовсе не запуститься на не предназначенной для использования ОС. Некоторые же стили запрещено использовать на определенных ОС по лицензионному соглашению.

Сторонние наработки


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

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

SWT (сайт) (мануал/доки) (примеры) (скачать)
Лицензия: EPL
Поддерживаемые платформы: Windows/Linux/MacOSX/Solaris and some others
SWT представляет собой обертку для удобного использования нативных компонентов разных операционных систем. Напрямую совмещать компоненты Swing и SWT не удастся, но есть несколько библиотек, предназначенных для этого (например эта).

SwingX (сайт) (мануал/доки) (скачать)
Лицензия: LGPL 2.1
Поддерживаемые платформы: All java-supported platforms
Достаточно старая и известная библиотека с расширенным набором Swing-компонентов. На данный момент проект находится в некоем переходном состоянии.

Jide (сайт) (мануал/доки) (скачать демо)
Лицензия: Commercial/GPL with classpath exception
Поддерживаемые платформы: All java-supported platforms
Одна из лучших коммерческих библиотек, предоставляющая широчайший спектр всевозможных компонентов.

Дополнительно

Вероятно Вам могут пригодиться библиотеки упомянутые в моей предыдущей статье для разработки приложений с использованием более сложных элементов (flash/video и т.п.). Могу только добавить еще одну библиотеку, которую я также использовал в нескольких проектах — JavaLayer – она позволяет воспроизводить mp3 файлы и mp3-стримы. Она проста в использовании, не требует никаких нативных вещей, кроссплатформенна и не ест ресурсов машины.

Есть конечно еще некоторые другие библиотеки (как коммерческие, так и свободные), но они либо морально устарели, либо содержат очень мало полезных вещей. Итак, Вы уже посмотрели какие LaF можно использовать, какие есть разные готовые компоненты в различных библиотеках, но в итоге — Вы всё еще не удовлетворены — часть компонентов имеет не тот стиль, какой хотелось бы, библиотеки уже успели десять раз морально устареть, обнаруживаются фатальные ошибки внутри библиотек, поддержка дает слабину… Что же делать? Однозначно — взять одного свободного дизайнера и код в свои руки!..

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

Написание своего UI


Сперва уделю немного времени этому моменту, так как не всегда необходимы какие-то хитрые компоненты, а достаточно стилизованных стандартных. Именно в таких случаях стоит прибегнуть к написанию своего небольшого UI-класса к J-компоненту, тем более что ко всем стандартным Swing-компонентам без исключения имеются Basic-UI классы, которые позволят Вам быстро и без лишней мороки создать свою стилизацию для компонента. Приведу небольшой пример создания UI для JSlider.

Для начала создадим класс, переопределяющий BasicSliderUI, и опишем его внешний вид имеющимися изображениями:
public class MySliderUI extends BasicSliderUI
{
  public static final ImageIcon BG_LEFT_ICON =
      new ImageIcon ( MySliderUI.class.getResource ( "icons/bg_left.png" ) );
  public static final ImageIcon BG_MID_ICON =
      new ImageIcon ( MySliderUI.class.getResource ( "icons/bg_mid.png" ) );
  public static final ImageIcon BG_RIGHT_ICON =
      new ImageIcon ( MySliderUI.class.getResource ( "icons/bg_right.png" ) );
  public static final ImageIcon BG_FILL_ICON =
      new ImageIcon ( MySliderUI.class.getResource ( "icons/bg_fill.png" ) );
  public static final ImageIcon GRIPPER_ICON =
      new ImageIcon ( MySliderUI.class.getResource ( "icons/gripper.png" ) );
  public static final ImageIcon GRIPPER_PRESSED_ICON =
      new ImageIcon ( MySliderUI.class.getResource ( "icons/gripper_pressed.png" ) );

  public MySliderUI ( final JSlider b )
  {
    super ( b );

    // Для корректной перерисовки слайдера
    b.addChangeListener ( new ChangeListener()
    {
      public void stateChanged ( ChangeEvent e )
      {
        b.repaint ();
      }
    } );
    b.addMouseListener ( new MouseAdapter()
    {
      public void mousePressed ( MouseEvent e )
      {
        b.repaint ();
      }

      public void mouseReleased ( MouseEvent e )
      {
        b.repaint ();
      }
    } );
  }

  // Возвращаем новый размер гриппера
  protected Dimension getThumbSize ()
  {
    return new Dimension ( GRIPPER_ICON.getIconWidth (), GRIPPER_ICON.getIconHeight () );
  }

  // Отрисовываем сам гриппер в необходимом месте
  public void paintThumb ( Graphics g )
  {
    int positionX = thumbRect.x + thumbRect.width / 2;
    int positionY = thumbRect.y + thumbRect.height / 2;
    g.drawImage ( isDragging () ? GRIPPER_PRESSED_ICON.getImage () : GRIPPER_ICON.getImage (),
        positionX - GRIPPER_ICON.getIconWidth () / 2,
        positionY - GRIPPER_ICON.getIconHeight () / 2, null );
  }

  // Отрисовываем «путь» слайдера
  public void paintTrack ( Graphics g )
  {
    if ( slider.getOrientation () == JSlider.HORIZONTAL )
    {
      // Сам путь
      g.drawImage ( BG_LEFT_ICON.getImage (), trackRect.x,
          trackRect.y + trackRect.height / 2 - BG_LEFT_ICON.getIconHeight () / 2, null );
      g.drawImage ( BG_MID_ICON.getImage (), trackRect.x + BG_LEFT_ICON.getIconWidth (),
          trackRect.y + trackRect.height / 2 - BG_MID_ICON.getIconHeight () / 2,
          trackRect.width - BG_LEFT_ICON.getIconWidth () - BG_RIGHT_ICON.getIconWidth (),
          BG_MID_ICON.getIconHeight (), null );
      g.drawImage ( BG_RIGHT_ICON.getImage (),
          trackRect.x + trackRect.width - BG_RIGHT_ICON.getIconWidth (),
          trackRect.y + trackRect.height / 2 - BG_RIGHT_ICON.getIconHeight () / 2, null );

      // Заполнение участка пути слева от гриппера
      g.drawImage ( BG_FILL_ICON.getImage (), trackRect.x + 1,
          trackRect.y + trackRect.height / 2 - BG_FILL_ICON.getIconHeight () / 2,
          thumbRect.x + thumbRect.width / 2 - trackRect.x - BG_LEFT_ICON.getIconWidth (),
          BG_FILL_ICON.getIconHeight (), null );
    }
    else
    {
      // Для вертикального слайдера аналог приведен в приложенном jar'е с демкой и сурцами
    }
  }
}
Как можно заметить, все необходимые переменные для отрисовки присутствуют в Basic-UI классе и мы можем их напрямую использовать:
thumbRect – прямоугольник гриппера
trackRect — прямоугольник «пути» слайдера
На самом деле большую часть необходимых перерисовок вызывает сам Basic-UI класс и Вам не нужно об этом беспокоиться (например при нажатии на гриппер или его перетаскивании).

В этом UI я немного схитрил и не переопределял метод calculateTrackRect(), который определяет размеры «пути» слайдера. Я просто отцентровал отрисованные изображения по центру имеющихся размеров. Но это — дело вкуса. Другой вопрос, что preffered-размер будет вычисляться исходя из размера гриппера и пути, просто в данном случае гриппер всяко больше пути.

Итак, таким несложным образом мы получили совершенно по-новому выглядящий слайдер. Теперь для использования его в приложении достаточно задать его как UI для любого JSlider'а:
JSlider mySlider = new JSlider ();
mySlider.setUI ( new GuiSliderUI ( mySlider ) );
И мы получим вот такой вот стилизованный слайдер:

Полный код с примером и графикой можно взять здесь (в нем также реализован вертикальный вариант слайдера, исходные java-классы находятся внутри jar'а).

Понятное дело — каждый отдельный Basic-UI (BasicButtonUI, BasicTextFieldUI, BasicTableUI, BasicTabbedPaneUI и пр.) имеет свои особенности отрисовки частей компонента, впрочем ничего сложного там нет и разобраться не составит труда, тем более что все имеющиеся методы в исходных кодах JDK достаточно подробно прокомментированы и описаны.

Чтобы логически завершить разговор об UI добавлю, что для всех известных J-компонентов помимо Basic-UI есть реализации под различные LaF'ы, к примеру: WindowsSliderUI, MetalSliderUI и прочие. Обычно их называют в соответствии с названием библиотеки или её назначением (для нативных ОС компонентов к примеру — OsnameComponentUI). Иногда их возможно использовать без установки общего LaF, но не всегда — есть такая возможность или нет зависит сугубо от реализации библиотеки (если например при установке LaF'а происходит загрузка стилей, без которых отдельные UI работать просто не будут).

Итак, в данном примере я воспользовался готовой графикой, чтобы создать опрятный визуальный компонент, но можно нарисовать и отличный визуальный компонент пользуясь только стандартными средствами Java, а точнее средствами Graphics2D – об этом пойдет речь далее…

Работа с Graphics2D


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

Отмечу, что во все методы «paint» (а также «paintComponent», «print» и прочие) приходит Graphics, а не Grahics2D, впрочем можно не боясь приводить (cast) его к Grahics2D, если вы работаете с каким-либо J-компонентом или его наследником. Почему же так сложилось? Это, можно так сказать, небольшие остатки старых частей из которых вырос Swing и не думаю, что стоит глубоко в это вдаваться на данный момент. Подробнее данный вопрос освещен в части статьи Skipy об внутреннем устройстве Swing.

Также сразу скажу, что в данной главе приведены лишь несколько вариантов использования графики — на самом деле область её применения гораздо шире. Как раз подробнее о графике и всём, что с ней связано вероятно пойдет речь в моем следующем тематическом топике о Java (при положительном исходе данного и наличии интереса у читателей, конечно). Но хватит спойлеров, вернемся к теме...


Итак, что же мы имеем? Какие возможности дает нам Graphics2D?
Перечислю основные из них:


Скажу сразу — при работе с графикой, скорее всего, со временем Вам придется воспользоваться всеми предоставленными инструментами без исключения для достижения наилучшего визуального эффекта. Подробное описание о том «что и как» можно найти здесь — это официальный туториал по 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;
        incorrect = getText ().trim ().equals ( "" );
        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<ImageIcon> BG_STATES = new ArrayList<ImageIcon> ();
public static List<ImageIcon> CHECK_STATES = new ArrayList<ImageIcon> ();

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

  // Дополнительное "пустое" состояние выделения
  CHECK_STATES.add ( new ImageIcon (
      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<String, ImageIcon> iconsCache = new HashMap<String, ImageIcon> ();

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 при загрузке приложения до открытия каких-либо окон и показа визуальных элементов.
Следовние этим простым пунктам позволит Вам не беспокоиться о возникновении непредвиденных «тормозов» или дедлоков в интерфейсе приложения.
Думаю, что этот список можно дополнить еще несколькими пунктами, но они уже будут весьма специфичны/необязательны.

Итоги


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

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

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

А также исходники этого последнего «общего» примера.

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

Отдельное спасибо Source Code Highlighter'y за читабельную подсветку кода.

Update1: Выложен новый jar компонента для ресайза изображения с исправлениями
Update2: Выложен новый jar кастомного диалога с исправлениями и плавным появлением/скрытием
Update3: Выложен общий jar со всеми примерами статьи и отдельно все исходники
Проголосовать:
+108
Сохранить: