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

Углубляясь в Graphics2D

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

Сегодня я опять постараюсь привлечь Ваше внимание к некоторым сторонам и тонкостям работы с графикой в Java. Я уже кратко описал в предыдущей статье некоторые доступные средства и способы создания компонентов и UI, но это лишь вершина айсберга. Именно поэтому я хочу уделить отдельное внимание (и статью) именно работе с графикой. Естественно имеется в виду Graphics2D – Java 3D это большая отдельная тема (возможно о ней еще пойдет речь в дальнейшем, но не сегодня).

Итак, из предыдущей статьи Вам уже должны быть известны некоторые основы построения компонентов — постараемся расширить эти знания.

Начнем с того, что если рассматривать любой компонент с точки зрения MVC – он состоит из 3ех частей:
Model – модель, которая хранит в себе данные о состоянии компонента и на основе которой строится внешний вид
View – непосредственно визуальное отображения компонента
Controller – отвечает за управление компонентом (события от клавиатуры, мыши и прочих устройств ввода)

Фактически, все стандартные компоненты Swing построены по паттерну MVC. К примеру в JButton — ButtonModel отвечает за поведение и состояние кнопки (Controller и Model), а ButtonUI в свою очередь за внешнее её представление (View). В итоге на долю самого класс JButton практически ничего не остаётся. Речь пойдет по большей части о реализации внешнего представления компонентов (View), и если уточнять — о Graphics2D, на основе которого, фактически, рисуется весь интерфейс.

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


Встречают по одёжке

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

Итак, сегодня мы разберём стандартные средства, предоставляемые Graphics2D, а также некоторые мтеодики и хитрости для отрисовки компонентов любой сложности.

Понятное дело, что без толковой идеи никакие средства не помогут сделать что-либо, но тут я увы — бессилен. Вероятно в этом Вам сможет помочь дизайнер/UX-специалист, если с фантазией всё совсем плохо. Честно скажу, что бывает достаточно трудно «выдавить» из себя как будет выглядеть какой-либо новый компонент. Бывает, что это даже сложнее и занимает большее время, нежели непосредственно написание рабочего кода и его отладка.

В любом случае, перейдём к делу…

Небольшое оглавление

  1. Фигуры
  2. RenderingHints
  3. Заливка
  4. Совмещение при отрисовке
  5. Stroke
  6. Тени
  7. Корректный «clipping»
  8. Анимация
  9. Несколько хитростей
  10. WebLookAndFeel
  11. В заключение...
  12. Ресурсы

Фигуры

Без фигур — никуда. Для любого компонента, любой простейшей вещи потребуется отрисовывать контуры частей. Делать это вручную попиксельно — задача не из приятных. Тем более если нужно добавить к отрисовке сглаживание или какие-либо другие эффекты. Слава богу Вам и не потребуется делать это вручную — все стандартные фигуры (Line2D, Rectangle2D, RoundRectangle2D, Ellipse2D, Arc2D и др.), которые могут понадобиться для отрисовки уже реализованы — остается лишь указать их координаты и размеры для отрисовки в определенном месте.

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

Также есть некоторые отдельные проекты, имеющие различные специфичные формы:
http://java-sl.com/shapes.html
http://geosoft.no/graphics/
http://designervista.com/shapes/index.php
http://www.jfree.org/jcommon/

Описывать каждую отдельную фигуру нет смысла — подробно во всех деталях о них можно прочитать здесь или здесь (в обоих описаниях есть также и примеры использования).

Я бы только хотел здесь немного уточнить о том, как происходит отрисовка фигуры и что влияет на итоговый результат. Предположим у вас есть некая фигура (Shape shape):
  • Вы можете использовать 2 различных метода — g2d.fill(shape) и g2d.draw(shape). Fill – Заполняет все пиксели внутри фигуры, draw – рисует контур фигуры.
  • За цвет или цвета, в которых происходит отрисовка отвечает установленный наследник класса Paint (g2d.setPaint(Paint paint)) – он предоставляет отрисовщику цвета под каждый отдельный пиксель области. Самым простым вариантом данного режима является любой цвет (Color) – он просто возвращает на каждый пиксель один и тот же цвет. Более сложными примерами являются, к примеру, градиентные и текстурные заливки (GradientPaint, TexturePaint и пр.).
  • На толщину, частоту (или же dash) и тип объединения на углах и краях линии границы отрисовываемого контура фигуры влияет установленный Stroke (g2d.setStroke(Stroke)).
  • На сглаживание, качество отрисовки фигур и текста, а также другие моменты влияют различные параметры приведенные главой ниже.


RenderingHints

Сглаживание входит в набор стандартных средств, идущих «в комплекте» с Graphics2D:
g2d.setRenderingHint ( RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON );
Этого достаточно, чтобы включить сглаживание всех отрисовываемых в дальнейшем фигур.
Главное не забывать отключать сглаживание после Ваших операций, если Вы не хотите, чтобы всё что отрисуется после также использовало сглаживание — к примеру если Вы реализуете свою отрисовку фона кнопки и не хотите вызвать сглаживания отрисовываемого по умолчанию текста.

Значение данного параметра (RenderingHints.KEY_ANTIALIASING) влияет также и на сглаживание текста, если у него установлен выбор по умолчанию.
Возможно отдельно включить/отключить необходимость сглаживания текста:
g2d.setRenderingHint ( RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON );
При установке данного параметра в любой кроме VALUE_TEXT_ANTIALIAS_DEFAULT он будет игнорировать значение RenderingHints.KEY_ANTIALIASING.

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

(Пример и исходный код можно загрузить здесь)

Если внимательно посмотреть — можно заметить также и много других доступных настроек в RenderingHints:
KEY_ANTIALIASING — настройка сглаживания фигур (и текста)
KEY_RENDERING — настройка качества/скорости рендеринга
KEY_DITHERING — смешивание цветов при ограниченной палитре
KEY_TEXT_ANTIALIASING — настройка сглаживания текста
KEY_TEXT_LCD_CONTRAST — контрастность текста (от 100 до 250) при отрисовке с использованием специального сглаживания текста
KEY_FRACTIONALMETRICS — настройка «аккуратности» отрисовки текстовых символов
KEY_INTERPOLATION — настройка, отвечающая за видоизменение пикселей изображений при отрисовке (например при развороте изображения)
KEY_ALPHA_INTERPOLATION — настройка качества/скорости обработки альфа значений
KEY_COLOR_RENDERING — настройка качества/скорости обработки цвета
KEY_STROKE_CONTROL — настройка возможности видоизменения геометрии фигур для улучшения итогового вида

Большинство из этих настроек обычно остаются в состоянии «по умолчанию». Впрочем они могут быть весьма полезны в некоторых специфичных случаях.
К примеру при установке KEY_INTERPOLATION в VALUE_INTERPOLATION_BILINEAR можно избежать потери качества изображения при его видоизменении (вращении/сжатии/сдвиге и пр.) или же улучшить контрастность текста на Вашем фоне, изменив KEY_TEXT_LCD_CONTRAST, не затрагивая при этом кода отрисовки текста.

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

Заливка

Есть несколько доступных доступных наследников класса Paint, которые позволяют по-различному закрашивать/заливать отрисовываемые фигуры:
  • Color – самый простой, позволяет отрисовывать или заполнять фигуру одним цветом.
  • TexturePaint — позволяет использовать для заполнения/отрисовки имеющийся BufferedImage как фоновое изображение.
  • GradientPaint – простой линейный градиент из одной точки в другую с начальным и конечным цветами.
  • LinearGradientPaint – более сложный линейный градиент из одной точки в другую с любым количеством промежуточных цветов с переопределяемыми расстояниями между ними.
  • RadialGradientPaint – круговой градиент с указанием центра круга и радиуса, а также цветов и расстояний между ними аналогично LinearGradientPaint'у.


Для наглядности — небольшой пример с использованием 3ех различных градиентов для заливки трех равных частей на компоненте:

(Пример и исходный код можно загрузить здесь)

Кстати, вероятно не все знают, что в любых заполнениях/отрисовках при указании цвета можно также указывать его прозрачность (alpha).
Например new Color ( 255, 255, 255, 128 ) — прозрачный на 50% белый цвет. Alpha в данном случае — 128. Она может изменяться в пределах от 0 до 255. 0 — полностью прозрачный цвет, 255 — полностью непрозрачный (используется по умолчанию).


Совмещение при отрисовке

Итак, мы планомерно переходим к более сложным вещам…
Совмещение (иначе говоря — Composite) позволяет указывать различные «режимы» совмещения новых отрисовываемых фигур/изображений с уже имеющимися на холсте пикселями.

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

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

Помимо Composite возможно также создавать свои формы с использованием стандартных и объединением их различными геометрическими операциями. Небольшой пример на данную тему:

(Пример и исходный код можно загрузить здесь)

Для создания данной фигуры потребовалось всего 2 строки:
Area area = new Area ( new Ellipse2D.Double ( 0, 0, 75, 75 ) );
area.add ( new Area ( new Ellipse2D.Double ( 0, 50, 75, 75 ) ) );

Что самое интересное — граница новой фигуры не включает в себя внутренние части границ эллипсов (те что попали внутрь противоположно эллипса).

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

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

Stroke

… или как подсказал TheShock «обводка».

Фактически Stroke предоставляет возможность задания стиля бордера рисуемого любым вызовом метода draw у графики.
В стандартном JDK существует только 1 реализация интерфейса Stroke — BasicStroke. Он позволяет задавать ширину линии, как объединяются линии на углах и как они выглядят на коцах, а также создавать пунктирные линии.

Для задания Stroke в коде необходимо сделать следующее:
g2d.setStroke ( new BasicStroke ( 2f ) );
Данный пример заставит все последующие бордеры отрисовываться шириной в 2 пикселя.
Кстати, не пугайтесь, что ширину и некоторые другие параметры возможно задать в float (хотя пиксели и должны быть целыми числами) — нецелые числа будут лишь создавать «размазанные» линии/очертания при отрисовке, что в некоторых случаях может даже пригодиться.

Подробнее о возможностях BasicStroke можно прочитать, например, здесь.

Хотя в JDK и входит лишь одна имплементация Stroke существуют другие проекты и примеры, описывающие реальные возможности данного инструмента, но об этом позже.

Тени

Стандартной реализации теней в Java2D нет, но есть достаточно много способов для достижения «эффекта» тени — именно о них я и расскажу в данной главе.
Пожалуй, начнем с наиболее простых вариантов…

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

На втором изображении — более узкий и опрятный вариант данного вида тени.
(Пример и исходный код можно загрузить здесь)

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

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

(Пример и исходный код можно загрузить здесь)

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

Градиентная тень
Данный вариант основывается на использовании градиентных заливок по краям фигуры.
Фактически, мы имеем 8 частей по краям в случае прямоугольника, в которых потребуется заливка:

В 4ех случаях — LinearGradientPaint, в других 4ех случаях — RadialGradientPaint. В итоге получается вот такая опрятная тень:

(Пример и исходный код можно загрузить здесь)

Также можно варьировать расположение градиента и получать другие интересные варианты, например такой:

(Пример и исходный код можно загрузить здесь)

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

Тень, получаемая видоизменением Stroke при отрисовке
Если быть точнее – фигура отрисовывается несколько раз в цикле с изменяемым цветом/прозрачностью и Stroke'ом, что позволяет создать подобие тени:

(Пример и исходный код можно загрузить здесь)

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

(Пример и исходный код можно загрузить здесь)

Также саму отрисовку тени можно легко вынести в отдельный независимый метод:
private void drawShade ( Graphics2D g2d, RoundRectangle2D rr, Color shadeColor, int width )
{
  Composite comp = g2d.getComposite ();
  Stroke old = g2d.getStroke ();
  width = width * 2;
  for ( int i = width; i >= 2; i -= 2 )
  {
    float opacity = ( float ) ( width - i ) / ( width - 1 );
    g2d.setColor ( shadeColor );
    g2d.setComposite ( AlphaComposite.getInstance ( AlphaComposite.SRC_OVER, opacity ) );
    g2d.setStroke ( new BasicStroke ( i ) );
    g2d.draw ( rr );
  }
  g2d.setStroke ( old );
  g2d.setComposite ( comp );
}

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

(Пример и исходный код можно загрузить здесь)

Конечно, в некоторых случаях такая тень подойдет, но всё же, хотелось бы получить более сглаженные/градиентные края у самой тени. В решении данной проблемы нам поможет фильтрация. Если точнее — мы используем специальный фильтр на полученную «тень» изображения для её более реалистичного вида:

(Пример и исходный код можно загрузить здесь)

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

Кстати говоря, данную тень можно использовать и для фигур, если предварительно нанести их на отдельное изображение, на основе которого отработает фильтр. Это может быть вполне применимо например если фигура(ы) не изменяются динамично при исполнении приложения — достаточно «заснять» её один раз и использовать полученное изображение при отрисовке. Впрочем лично мне данный вариант не очень нравится в виду его затратности по ресурсам, поэтому я исключил его из списка.

Корректный «clipping»

Хотелось бы отдельно рассказать ещё об одном важном аспекте, необходимом при работе с графикой и создании «корректных» компонентов — работе с clip'ом, или же — отсечение ненужных частей при отрисовке.

Для использования данного инструмента достаточно задать форму, по которой будет происходить «отсечение»:
g.setClip ( x, y, width, height );
g.setClip ( new Rectangle ( x, y, width, height ) );
g.setClip ( new Rectangle2D.Double ( x, y, width, height ) );
Все три данных способа приведут к установлению одинаковой прямоугольной области отсечения.

Есть множество случаев, когда данный инструмент может пригодиться.
Во первых — при отрисовке любого компонента всегда предустановлена определённая форма отсечения (обычно это прямоугольник (bounds) компонента) — она не даёт компоненту «вылезать» за свои границы. Её обязательно надо учитывать при установке своей специфичной области отсечения. Достаточно просто это можно сделать вот так:
Shape oldClip = g.getClip ();
Shape newClip = new Rectangle ( x, y, width, height );
Area clip = new Area ( oldClip );
clip.intersect ( new Area ( newClip ) );
g.setClip ( clip );
Фактически Вы объединяете имеющуюся область отсечения с новой. Таким образом Вы не потеряете ограничение в виде границ компонента, но и добавите новое, необходимое Вам.

Если вернуться к главе о создании теней, а если быть точнее к 4 пункту — его как раз можно улучшить возможным отсечением части тени:
public static void drawShade ( Graphics2D g2d, Shape shape, Color shadeColor, int width,
                Shape clip, boolean round )
{
  Shape oldClip = g2d.getClip ();
  if ( clip != null )
  {
    Area finalClip = new Area ( clip );
    finalClip.intersect ( new Area ( oldClip ) );
    g2d.setClip ( finalClip );
  }

  Composite comp = g2d.getComposite ();
  float currentComposite = 1f;
  if ( comp instanceof AlphaComposite )
  {
    currentComposite = ( ( AlphaComposite ) comp ).getAlpha ();
  }

  Stroke old = g2d.getStroke ();
  width = width * 2;
  for ( int i = width; i >= 2; i -= 2 )
  {
    float opacity = ( float ) ( width - i ) / ( width - 1 );
    g2d.setColor ( shadeColor );
    g2d.setComposite ( AlphaComposite
        .getInstance ( AlphaComposite.SRC_OVER, opacity * currentComposite ) );
    g2d.setStroke (
        new BasicStroke ( i, round ? BasicStroke.CAP_ROUND : BasicStroke.CAP_BUTT,
            BasicStroke.JOIN_ROUND ) );
    g2d.draw ( shape );
  }
  g2d.setStroke ( old );
  g2d.setComposite ( comp );

  if ( clip != null )
  {
    g2d.setClip ( oldClip );
  }
}

Таким образом в этот метод становится возможно дополнительно передать желаемую область отсечения — всё остальное сделает сам метод.

Как показывают некоторые проведённые тесты — обрезание неотрисовываемых частей (к примеру того, что выходит за экран) не даёт никакого значимого прироста скорости работы. Впрочем это и понятно, ведь все вычисления типа «что, где и как рисовать» и сама отрисовка по прежнему отрабатывают, даже если установлен clip в 1 «доступный» пиксель. Так что «ручная» оптимизация будет куда более полезной в таком деле.

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


Анимация

Итак, пора совместить часть полученных знаний и сделать что-то более интересное.

Сама по себе анимация достаточно проста для понимания и представляет лишь изменение отрисовываемых объектов с течением времени. Впрочем на практике возникает гораздо больше вопросов и проблем.

В зависимости от вида анимации может потребоваться достаточно много дополнительного кода, отвечающего за «развитие событий» и отображающего изменения. Важно также не забывать и об оптимизации при перерисовке — т. е. желательно перерисовывать только те области анимируемого компонента, в которых произошли изменения. Для этого достаточно вызывать метод repaint ( new Rectangle ( x, y, width, height ) ).

Рассмотрим небольшой пример реализации анимации — создадим эфект пробегающего по тексту JLabel'а блика. Для этого нам сперва необходимо определиться с тем, как это будет выглядеть, чтобы чётко представлять, что нам потребуется для реализации.

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

Итак, примерно вот так будет выглядеть отрисовка компонента:
private boolean animating = false;
private int animationX = 0;
private int animationLength = 140;

private float[] fractions = { 0f, 1f };
private Color[] colors = new Color[]{ new Color ( 200, 200, 200 ), new Color ( 0, 0, 0 ) };

protected void paintComponent ( Graphics g )
{
  // Создаём изображение, на котором будет отрисован только лишь текст без фона
  BufferedImage bi =
      new BufferedImage ( getWidth (), getHeight (), BufferedImage.TYPE_INT_ARGB );
  Graphics2D g2d = bi.createGraphics ();
  g2d.setFont ( g.getFont () );

  // Отрисовываем текст
  super.paintComponent ( g2d );

  // При действующей анимации рисуем блик
  if ( animating )
  {
    g2d.setRenderingHint ( RenderingHints.KEY_ANTIALIASING,
        RenderingHints.VALUE_ANTIALIAS_ON );

    g2d.setComposite ( AlphaComposite.getInstance ( AlphaComposite.SRC_IN ) );
    g2d.setPaint ( new RadialGradientPaint ( animationX - animationLength / 2,
        getHeight () / 2, animationLength / 2, fractions, colors ) );
    g2d.fillRect ( animationX - animationLength, 0, animationLength, getHeight () );
  }

  // Переносим полученное изображение на исходный компонент
  g2d.dispose ();
  g.drawImage ( bi, 0, 0, null );
}
Основная суть скрыта в создании отдельного изображения, на которое рисуется текст, а также установке Composite при отрисовке блика.

Изображение необходимо для того, чтобы на нём были заняты лишь те пиксели, на которых присутствует текст, иначе при стандартной отрисовке на приходящем в метод Graphics AlphaComposite.SRC_IN заполнит весь заливаемый прямоугольник указанным градиентом, так как помимо текста на графике уже будет присутсвовать отрисованный низлежащей панелью (панелей) фон.

Итак, теперь нам остаётся реализовать таймер, срабатывающий, скажем, при входе курсора в область JLabel'а:
private static class AnimatedLabel extends JLabel
{
  public AnimatedLabel ( String text )
  {
    super ( text );
    setupSettings ();
  }

  private void setupSettings ()
  {
    // Для спрятывания фона
    setOpaque ( false );
    // Для более очевидного отображения эффекта
    setFont ( getFont ().deriveFont ( Font.BOLD ).deriveFont ( 30f ) );

    // Слушатель, инициирующий анимацию
    addMouseListener ( new MouseAdapter()
    {
      public void mouseEntered ( MouseEvent e )
      {
        startAnimation ();
      }
    } );
  }

  private Timer animator = null;
  private boolean animating = false;
  private int animationX = 0;
  private int animationLength = 140;

  private float[] fractions = { 0f, 1f };
  private Color[] colors = new Color[]{ new Color ( 200, 200, 200 ), new Color ( 0, 0, 0 ) };

  private void startAnimation ()
  {
    // Если анимация уже идёт, игнорируем запрос
    if ( animator != null && animator.isRunning () )
    {
      return;
    }

    // Начинаем анимацию
    animating = true;
    animationX = 0;
    animator = new Timer ( 1000 / 48, new ActionListener()
    {
      public void actionPerformed ( ActionEvent e )
      {
        // Увеличиваем координату вплоть до достижения ею конца компонента
        if ( animationX < getWidth () + animationLength )
        {
          animationX += 10;
          AnimatedButton.this.repaint ();
        }
        else
        {
          animator.stop ();
        }
      }
    } );
    animator.start ();
  }

  protected void paintComponent ( Graphics g )
  {
    //
  }
}
Сомневаюсь, что в данном куске кода необходимо что-либо объяснять (помимо того, что описано комментариями).

В итоге мы получаем вот такой вот забавный эффект.

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

Несколько хитростей

Иногда не достаточно просто знать стандартные средства, чтобы сделать всё что Вам необходимо. Приходится изобретать различные «хитрые» вещи. Я бы хотел поделиться тем что я смог найти в сети и «изобретёнными велосипедами» с Вами в нескольких отдельных примерах. Итак, перейдем к делу…

Сглаживание края изображения
Предположим, нам нужно обрезать изображение по определённой форме, но стандартные средства типа clip'а при отрисовке приведут к плачевному результату. В данном случае стоит воспользоваться AlphaComposite'ом:
ImageIcon icon = new ImageIcon ( iconPath );

BufferedImage roundedImage = new BufferedImage ( icon.getIconWidth (), icon.getIconHeight (),
        BufferedImage.TYPE_INT_ARGB );
Graphics2D g2d = roundedImage.createGraphics ();
g2d.setRenderingHint ( RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON );
g2d.setPaint ( Color.WHITE );
g2d.fillRoundRect ( 0, 0, icon.getIconWidth (), icon.getIconHeight (), 10, 10 );
g2d.setComposite ( AlphaComposite.getInstance ( AlphaComposite.SRC_IN ) );
g2d.drawImage ( icon.getImage (), 0, 0, null );
g2d.dispose ();

ImageIcon roundedIcon = new ImageIcon ( roundedImage );
Таким образом мы сперва отрисовываем сглаженный в углах загруглённый прямоугольник, а затем используя его как трафарет накладываем сверху изображение.

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

Использование blur/shadow фильтров
На том же ресурсе можно также найти весьма интересную статью по blur'у.
Может быть полезно тем, кто занимается работой с изображениями.

Использование GlyphVector
Одним из «напряжных» моментов при работе с графикой является отрисовка текста, особенно если этот текст может изменяться. Для корректного позиционирования текста придётся вычислять его размеры и отрисовывать основываясь на них.
Для таких вычислений есть два средства:
1. FontMetrics
Его можно получить непосредственно из инстанса Graphics2D (g2d.getFontMetrics ()).
Он позволяет определять раличные размеры отступов и высот установленного шрифта.
2. GlyphVector
Данный вариант будет более полезен в случаях, когда надо отцентровать текст по Y-координате, т. к. он позволяет точно узнать размеры определённого текста:
FontMetrics fm = g2d.getFontMetrics ();
GlyphVector gv = g2d.getFont ().createGlyphVector ( fm.getFontRenderContext (), "Text" );
Rectangle visualBounds = gv.getVisualBounds ().getBounds ();
Также из GlyphVector'а можно получить достаточно много полезной информации, например внешнюю границу текста (непосредственно её форма):
Shape textOutline = gv.getOutline ();

Создание своего Tooltip-менеджера
В Swing есть достаточно много неприглядных вещей, которые, впрочем, можно легко сгладить изменив UI компонентов или поработав с их компоновкой. В некоторых случаях всё не так просто.

Именно к таким случаям относится и система тултипов, реализованная для всех J-компонентов. Так как все показываемые тултипы отображаются на отдельном строго прямоугольном попапе (lightweight или heavyweight в зависимости от того, попадает ли тултип в границы окна) — мы ограничены этой областью и формой, что весьма печально. И это то в эпоху опрятных вэб-интерфейсов и «закруглённых» форм!

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

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

Сама идея состоит из нескольких частей:
1. Отдельный менеджер (TooltipManager.java), который бы при необходимости создавал GlassPane для определённого окна, на котором открывается тултип и запоминал его. Дальнейшее создание тултипа происходило бы непосредственно в GlassPane.
2. GlassPane (TooltipGlassPane.java) представляющий из себя прозрачную панель, пропускающую любые события и отображающую тултипы в нужный момент
3. Сам тултип (CustomTooltip.java) — стандартный J-компонент, отображающий любое содержимое в приятном оформлении в зависимости от расположения на GlassPane

В итоге показываемые тултипы будут выглядеть примерно вот так:


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

Редактируемый список
Помимо дописания своих «велосипедов» в некоторых случаях достаточно легко и изящно можно используя стандартные средства дополнить функционал существующих компонентов.

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

Во-первых, необходимо создать интерфейс, который будет реализовывать сам редактор:
public interface ListEditor
{
  public void installEditor ( JList list, Runnable startEdit );

  public boolean isCellEditable ( JList list, int index, Object value );

  public JComponent createEditor ( JList list, int index, Object value );

  public Rectangle getEditorBounds ( JList list, int index, Object value, Rectangle cellBounds );

  public void setupEditorActions ( JList list, Object value, Runnable cancelEdit,
                   Runnable finishEdit );

  public Object getEditorValue ( JList list, int index, Object oldValue );

  public boolean updateModelValue ( JList list, int index, Object value, boolean updateSelection );

  public void editStarted ( JList list, int index );

  public void editFinished ( JList list, int index, Object oldValue, Object newValue );

  public void editCancelled ( JList list, int index );
}
Наследник данного интерфейса будет предоставлять всё необходимое для создания и отображения редактора на списке. Вот только каждый раз наследовать и определять полный набор этих функций весьма накладно, давайте сделаем абстрактный класс, реализующий более-менее общую часть для различных редакторов:
public abstract class AbstractListEditor implements ListEditor
{
  protected int editedCell = -1;

  public void installEditor ( final JList list, final Runnable startEdit )
  {
    // Слушатели, инициирующие редактирование
    list.addMouseListener ( new MouseAdapter()
    {
      public void mouseClicked ( MouseEvent e )
      {
        if ( e.getClickCount () == 2 && SwingUtilities.isLeftMouseButton ( e ) )
        {
          startEdit.run ();
        }
      }
    } );
    list.addKeyListener ( new KeyAdapter()
    {
      public void keyReleased ( KeyEvent e )
      {
        if ( e.getKeyCode () == KeyEvent.VK_F2 )
        {
          startEdit.run ();
        }
      }
    } );
  }

  public boolean isCellEditable ( JList list, int index, Object value )
  {
    return true;
  }

  public Rectangle getEditorBounds ( JList list, int index, Object value, Rectangle cellBounds )
  {
    // Стандартные размеры редактора идентичные размерам редактируемой ячейки
    return new Rectangle ( 0, 0, cellBounds.width, cellBounds.height + 1 );
  }

  public boolean updateModelValue ( JList list, int index, Object value, boolean updateSelection )
  {
    // Обновление модели при завершении редактирования
    ListModel model = list.getModel ();
    if ( model instanceof DefaultListModel )
    {
      ( ( DefaultListModel ) model ).setElementAt ( value, index );
      list.repaint ();
      return true;
    }
    else if ( model instanceof AbstractListModel )
    {
      final Object[] values = new Object[ model.getSize () ];
      for ( int i = 0; i < model.getSize (); i++ )
      {
        if ( editedCell != i )
        {
          values[ i ] = model.getElementAt ( i );
        }
        else
        {
          values[ i ] = value;
        }
      }
      list.setModel ( new AbstractListModel()
      {
        public int getSize ()
        {
          return values.length;
        }

        public Object getElementAt ( int index )
        {
          return values[ index ];
        }
      } );
      return true;
    }
    else
    {
      return false;
    }
  }

  public void editStarted ( JList list, int index )
  {
    // Сохранение индекса редактирумой ячейки
    editedCell = index;
  }

  public void editFinished ( JList list, int index, Object oldValue, Object newValue )
  {
    // Очистка индекса редактируемой ячейки
    editedCell = -1;
  }

  public void editCancelled ( JList list, int index )
  {
    // Очистка индекса редактируемой ячейки
    editedCell = -1;
  }

  public boolean isEditing ()
  {
    // Проверка активности редактора
    return editedCell != -1;
  }
}
Теперь на основе этого абстрактного класса будет весьма просто реализовать, к примеру, текстовый редактор для списка — он будет выглядеть примерно так — WebStringListEditor.java.

Остаётся последний момент — метод установки редактора в список. Вынесем его в отдельный класс и сделаем статичным для удобства:
public class ListUtils
{
  public static void installEditor ( final JList list, final ListEditor listEditor )
  {
    // Собственно код, начинающий редактирование в списке
    final Runnable startEdit = new Runnable()
    {
      public void run ()
      {
        // Проверка на наличие выделенной ячейки
        final int index = list.getSelectedIndex ();
        if ( list.getSelectedIndices ().length != 1 || index == -1 )
        {
          return;
        }

        // Проверка на возможность редактирования выделенной ячейки
        final Object value = list.getModel ().getElementAt ( index );
        if ( !listEditor.isCellEditable ( list, index, value ) )
        {
          return;
        }

        // Создаём редактор
        final JComponent editor = listEditor.createEditor ( list, index, value );

        // Устанавливаем его размеры и слушатели для ресайза
        editor.setBounds ( computeCellEditorBounds ( index, value, list, listEditor ) );
        list.addComponentListener ( new ComponentAdapter()
        {
          public void componentResized ( ComponentEvent e )
          {
            checkEditorBounds ();
          }

          private void checkEditorBounds ()
          {
            Rectangle newBounds =
                computeCellEditorBounds ( index, value, list, listEditor );
            if ( newBounds != null && !newBounds.equals ( editor.getBounds () ) )
            {
              editor.setBounds ( newBounds );
              list.revalidate ();
              list.repaint ();
            }
          }
        } );

        // Добавляем компонент поверх списка
        list.add ( editor );
        list.revalidate ();
        list.repaint ();

        // Забираем фокус в редактор
        if ( editor.isFocusable () )
        {
          editor.requestFocus ();
          editor.requestFocusInWindow ();
        }

        // Создаём методы отмены и завершения редактирования
        final Runnable cancelEdit = new Runnable()
        {
          public void run ()
          {
            list.remove ( editor );
            list.revalidate ();
            list.repaint ();

            listEditor.editCancelled ( list, index );
          }
        };
        final Runnable finishEdit = new Runnable()
        {
          public void run ()
          {
            Object newValue = listEditor.getEditorValue ( list, index, value );
            boolean changed =
                listEditor.updateModelValue ( list, index, newValue, true );

            list.remove ( editor );
            list.revalidate ();
            list.repaint ();

            if ( changed )
            {
              listEditor.editFinished ( list, index, value, newValue );
            }
            else
            {
              listEditor.editCancelled ( list, index );
            }
          }
        };
        listEditor.setupEditorActions ( list, value, cancelEdit, finishEdit );

        // Оповещаем о начале редактирования
        listEditor.editStarted ( list, index );
      }
    };
    listEditor.installEditor ( list, startEdit );
  }

  private static Rectangle computeCellEditorBounds ( int index, Object value, JList list,
                            ListEditor listEditor )
  {
    // Метод возвращающий расположение редактора на списке
    Rectangle cellBounds = list.getCellBounds ( index, index );
    if ( cellBounds != null )
    {
      Rectangle editorBounds = listEditor.getEditorBounds ( list, index, value, cellBounds );
      return new Rectangle ( cellBounds.x + editorBounds.x, cellBounds.y + editorBounds.y,
          editorBounds.width, editorBounds.height );
    }
    else
    {
      return null;
    }
  }
}

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


Главное не забывать изменять методы установки/получения значения в/из редактора, если Вы пользуетесь не String'ами в модели. Для этого достаточно переопределить два метода (конечно, в зависимости от сложности необходимого редактора) в WebStringListEditor.java — createEditor и getEditorValue.

Web Look And Feel

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

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

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

Собственно, в библиотеку вошли как большинство техник, описанных в данной статье, так и множество других интересных и полезных вещей по работе с графикой:
  • Собственно Web LaF – отдельный полноценный кросс-платформенный pure-java LaF
  • Набор дополнительных компонентов (также стилизованных под Web LaF) — FileChooser/ColorChooser/Gallery/Calendar и множество других (большое множество!)
  • Набор утилит-классов для: работы с графикой, файлами, текстом, изображениями и многими другими аспектами (к примеру для упрощения скачивания файлов по URL, чтения изображений, различных операций с ними и т. п.)

Также вот некоторые технические «плюсы» и особенности:
  • Полностью кросс-платформенный и легко настраиваемый LookAndFeel (непосредственно — проверяется на Windows XP/Vista/7, Mac OS X 10.6.7, Ubuntu 9/11)
  • Минимальные требования к памяти (легко запустится на -Xms16m -Xmx16m и будет проводить даже тяжёлые операции с изображениями, не говоря уж о работе LaF и различных утилит)
  • Single-jar bundle – т. е. всё необходимое включено в единый jar библиотеки, работающий для всех операционных систем
  • Требуется JDK 1.6 update 20 и новее (про JDK 7 пока ничего не могу сказать — не пробовал, но по идее никаких deprecated-вещей и тестовых фич в коде не использовано)

Подробнее о ней можно почитать на отдельном сайте.

Многим данная библиотека может показаться неким «велосипедом» и кто-то возможно будет утверждать что подобный функционал уже присутствует в других известных библиотеках…
На это я могу сказать две вещи:
Во-первых — другие библиотеки предоставляют свои компоненты в лучшем случае с возможностью стилизации под тот или иной UI (и то зачастую с костылями). Эта же библиотека содержит уже стилизованный под общий вид WebLaF набор дополнительных компонентов, который будет со временем расширяться.
Во-вторых — различных вещей, которые я добавил и ещё только собираюсь добавить в библиотеку нигде нет. Я ни нашёл ни единой толковой реализации ColorChooser'а в просторах сети, которой бы можно было заменить ужастный JColorChooser. Иных реализаций JFileChooser'а и вовсе нет. Конечно есть SWT, но честно говоря с ним возникают другие проблемы, сложности и ограничения, в которые предлагаю пока не углубляться и откинуть этот вариант — всё же речь идёт о Swing.

Итак, для возможности «пощупать» интерфейс и компоненты я добавил в библиотеку класс с небольшой демонстрацией завершённых компонентов:

(Пример и исходный код можно загрузить здесь)

Полный исходный код и дистрибутив библиотеки доступен на сайте:
http://weblookandfeel.com/download/

На данный момент библиотека ещё не завершена и имеются небольшие недоработки в LookAndFeel'е, «несглаженные» места в коде, а также некоторый функционал и внешний вид остаётся весьма «спорным» и ещё будет изменён в лучшую сторону.

В связи со всем вышесказанным — буду рад услышать любые комментарии, предложения и конструктивную критику на тему :)

В заключение...

Надеюсь теперь Ваши познания в плане графики в Java немного структурировались и стали более осязаемы и применимы на практике.

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

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

Следующая статья, скорее всего, будет посвящена написанию собственного LookAndFeel'а (с примерами и в картинках, естественно), а также некоторым особенностям UI отдельных J-компонентов.

Ресурсы

Различные сторонние ресурсы по тематике:
(включая приведённые в статье)

MVC
http://lib.juga.ru/article/articleview/163/1/68/

Shapes
http://java.sun.com/developer/technicalArticles/GUI/java2d/java2dpart1.html
http://www.datadisk.co.uk/html_docs/java/graphics_java2d.htm

Extended Shapes
http://java-sl.com/shapes.html
http://geosoft.no/graphics/
http://designervista.com/shapes/index.php
http://www.jfree.org/jcommon/

Composite
http://download.oracle.com/javase/tutorial/2d/advanced/compositing.html

BasicStroke
http://www.projava.net/Glava9/Index13.htm

Extended Strokes
http://www.jhlabs.com/java/java2d/strokes/

Blurring
http://www.jhlabs.com/ip/blurring.html

Использованные в WebLookAndFeel библиотеки:

java-image-scaling
http://code.google.com/p/java-image-scaling/

TableLayout
http://java.net/projects/tablelayout

Data Tips
К сожалению на данный момент доступных ресурсов по данной библиотеке нет

Jericho HTML Parser
http://jericho.htmlparser.net/docs/index.html

и наборы иконок:

Fugue icons
http://code.google.com/p/fugue-icons-src/

Fatcow icons
http://www.fatcow.com/free-icons

Отдельное спасибо...


проекту Source Code Highlighter за читаемую подсветку кода:
http://virtser.net/blog/post/source-code-highlighter.aspx

а также Хостингу картинок за хранение изображений:
http://hostingkartinok.com/

Upd1: Немного подправил ссылки и иллюстрации
Upd2: Обновлены дистрибутивы библиотеки с фиксами разных неточностей и возникших проблем
Upd3: Подправлены неточности и кривые изображения в статье
Tags:
Hubs:
+74
Comments 71
Comments Comments 71

Articles

Information

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