ПО для электронных архивов и библиотек, оцифровка
41,48
рейтинг
1 сентября 2011 в 17:38

Разработка → Углубляясь в Graphics2D

Добрый день, Хабражители!

Сегодня я опять постараюсь привлечь Ваше внимание к некоторым сторонам и тонкостям работы с графикой в 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: Подправлены неточности и кривые изображения в статье
Автор: @mgarin
ALEE Software
рейтинг 41,48
ПО для электронных архивов и библиотек, оцифровка

Комментарии (71)

  • +2
    Зачет!
    • +3
      Что же, спасибо :)
      Надеюсь что много ещё кому пригодится изложенная информация
      • +1
        Не сомневайся))
  • +1
    Stroke.
    Это единственная функцональность, которую я не знаю как можно «толково» перевести на русский язык...


    Может «окантовка»?
    • 0
      Это логичный, но не совсем точный вариант.
      Боюсь если бы я сказал — «установите окантовку ...» — меня бы вряд ли поняли бы…
      Особенно те, кто в курсе что такое Stroke.

      Хотя может мне только так кажется :)
      • 0
        Были ещё и другие идеи:
        бордер / ход / граница
        но как-то всё не то, всё же…
        • 0
          «штрих»?
          • 0
            Один из параметров конструктора BasicStroke является «dash» — это фактически и есть «штрих» (или «штрихи»). Так что вряд ли Stroke можно так назвать…
            • 0
              dash — черта, тире
              • 0
                • 0
                  Не угодишь Вам :)
                  • 0
                    Собтсвенно, и не спрашивал бы, если бы можно было обойтись одним словарём :)
                    Вообще ещё узнаю у одного знакомого, но уже завтра…
      • +1
        «Обводка»?
        • 0
          А вот это уже мысль :)
          Наверное так и оставлю, спасибо за идею!
  • 0
    Dash — существительное:
    тире
    штрих
    черта

    В контексте Stroke — шрих, не иначе :)
  • +1
    Симпатичный LaF. Чем-то напоминает Alloy. Вставьте пожалуйста на сайт онлайн демо например с SwingSet2.
    А для Graphics2D самая лучшая на сей момент библиотека — JavaFX :)
    • 0
      Очень сомневаюсь что оно так для всех.
      Скажем крупные приложения, написанные на Swing очень вряд ли будут переводить на JavaFX.
      Впрочем то же касается и новых приложений, ибо JavaFX сейчас в бете.

      Насчёт демки — Вы имеете в виду добавить запускаемое через .jnlp демо на основе SwingSet2?
      Если так — то идея достаточно хорошая, добавлю помимо основного демо.
    • 0
      Кстати насчёт Alloy — отчасти он вдозновил меня на идею написания своего LaF'а.
      Хотелось что-то лёгкое, не напрягающее глаз, но при этом достаточно элегантное и приятное «на ощупь».
      Есть конечно шероховатости ещё, но со временем они уйдут.
      • 0
        Да, Alloy был мой любимый laf, тока ему антиалайсинга шрифтов не хватало. Да, стоит демку с .jnlp или аплетом сунуть, чтобы сразу его можно было «пощупать». И спасибо за старания.
        • 0
          Думаю .jnlp добавлю. Но swing-set демки всё же не то — там мало что будет видно,
          ведь помимо LaF'а есть ещё и доп. компоненты и многое другое.
          В общем как закончу некоторые фиксы по библиотеке, займусь «презентабельным» видом :)
  • 0
    Пытаюсь подключить Web LAF к существующему проекту. JDK 1.6.0.22. Выдает exception
    java.lang.NoClassDefFoundError: sun/swing/MenuItemLayoutHelper
    at com.alee.laf.menu.WebMenuUI.paintMenuItem(WebMenuUI.java:109)

    Какую версию JDK все же надо иметь, чтобы LAF заработал?
    • 0
      У Вас случаем не OpenJDK используется?
      • 0
        Да нет. Релиз под x86
        • 0
          Странно, вероятно данный класс появился в ещё более позднем релизе…
          Я сейчас заменю его и выложу обновлённую версию, так как Вы уже не первый, кто сталкивается с его отсутствием.
          • 0
            Да я обновлю JDK — не проблема. Просто вы указывали что Web LAF доступен с JDK версии 1.6u20. Вот я и спросил.
            • 0
              Нет нет, он и должен быть доступен с той версии (даже, на самом деле, с ещё более ранней).
              Так что в любом случае стоит прикрыть эту «дыру» :)
              • 0
                Обновил JDK. Заметил такой баг в меню. Если нет ни одной иконки то вертикальная черта пересекает текст
                • 0
                  Да, это ещё одна причина использовать свой класс для определения размеров меню и элементов меню. Уже работаю над этим.
              • 0
                И еще, вы почему-то убрали кнопки из JScrollBar.
                • 0
                  Честно говоря, за последний наверно год — не использовал эти кнопки ни в одном приложении, а места они отъедают прилично, если делать скролл шире для кнопок.
                  В общем, как мне кажется, весьма бесполезный функционал.

                  Может в дальнейшем просто оставлю возможность их отображать, но придётся для этого немного видоизменить сами скролл-бары.
                  • 0
                    Ну у меня используется для навигации между транзакциями. Так хотел заказчик.
                    • 0
                      Хм, сложно представить, честно говоря без представления о Вашем интерфейсе…
              • 0
                Словил такой exception и не могу открыть один из фреймов

                java.lang.IllegalArgumentException: Start point cannot equalendpoint
                  at java.awt.LinearGradientPaint.<init>(LinearGradientPaint.java:271)
                  at java.awt.LinearGradientPaint.<init>(LinearGradientPaint.java:221)
                  at java.awt.LinearGradientPaint.<init>(LinearGradientPaint.java:116)
                  at com.alee.laf.toolbar.WebToolBarUI.paint(WebToolBarUI.java:185)


                * This source code was highlighted with Source Code Highlighter.
                • 0
                  Это случилось потому, что у Вас где-то используется JToolbar с нулевыми (или мизерными) размерами. Косяк я подправлю, но Вам тоже стоит посмотреть где же у Вас такое чудо :)

                  Хотя, может просто тулбар изначально появляется с нулевыми размерами и только потом сайзится. В таком случае это также возможно.
                  • 0
                    Да, есть такое чудо. Просто другие скины на это не ругались.
                    • 0
                      Да, это конечно косяк, уже подправил.
                      Остаётся только меню оживить…
                • 0
                  Всё, поправил и перезалил на сайт (ссылки из этого топика ведут туда же, так что они тоже, фактически, обновлены).

                  Проблемы с меню также должны уйти (с некорректным расположением без иконок).
                  • 0
                    Кстати, у вас на сайте какая-то проблема с mime-type или Content-Disposition в download секции. Т.к. по клику на jar файл в хроме он начинает открываться в браузере вместо диалога загрузки.
                    • 0
                      Знаю, сейчас вот как раз трясу админа, чтобы он толково настроил отдачу файлов, так как я сам в настройке сайт-хостингов не силён :)
                    • 0
                      Слава гуглу, таки поправил эту проблему :)
                      Теперь всё должно нормально качаться
                  • 0
                    Почему-то кастомный JList с чекбоксами по типу такого
                    www.devx.com/tips/Tip/5342 не хочет работать. Галочки не ставятся
                  • 0
                    Ну и в ComponentOrientation.RIGHT_TO_LEFT, к сожалению, многое отображается не правильно
                  • 0
                    Редактируемые комбобоксы, почему-то, не отличаются по внешнему виду от нередактируемых.
                    • 0
                      Суть в том, что для рендереров и редакторов необходимы чекбоксы и радиокнопки без анимации, т.е. при создании модели с чекбоксами в том примере что вы указали необходимо отключить анимацию чекбоксов.
                      • 0
                        Я бы не обращал на это внимание, если бы это также некорректно работало в Substance, JTatto, JGoodies и т.д. Но в этих библиотеках все работает без дополнительных твиков.
                        Я наверное вас замучил? Если что — извините.
                        • 0
                          Я не уверен, но разве в указанных стилях присутствует анимация в чекбоксе?
                          Проблема возникает именно из-за неё.

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

                          Как и самих компонентов по типу чекбокс-списка, дерева и пр.
                          • 0
                            Как в отдельных компонентах анимация есть. А вот уже в JList она отсутствует. Видно где-то проверяется на глобальном уровне
                            • 0
                              Вы, кстати, там свой рендерер используете или какой готовый?
                              Просто если готовый — вероятно в нём проверяется.
                              • 0
                                Рендерер свой, который расширяет DefaultListCellRenderer. Если быть точнее то SubstanceDefaultListCellRenderer. Но я пробовал и с DefaultListCellRenderer — и все равно с вашим LAF были проблемы.
                                • 0
                                  Ну дык, а попробуйте SubstanceLaF и DefaultListCellRenderer — вероятно что будут и у них проблемы :)
                                  • 0
                                    Попробовал. Проблема не наблюдается.
                                    • 0
                                      Ну тогда действительно глобально проверяется.
                                      В любом случае я уже запланировал подобные вещи, буду допиливать возможность удобного использования рендереров.
                                      • 0
                                        В любом случае спасибо вам за труды. Вы делаете полезное дело. LAF выглядит вполне симпатично. Жаль, что пока не могу его использовать из-за отсутствия RTL поддержки и других мелких косяков.
                                        • 0
                                          Вам спасибо за наведение на неточности и ошибки.

                                          Всё же очень бы хотелось услышать, где конкретно не учтён RTL и что за «другие мелкие косяки» (помимо кастомных рендереров). :)
                                          • +1
                                            Косяки я почти все указал:
                                            — это кастомные рендереры;
                                            — отсутствие кнопок в скролбаре;
                                            — также заметил глюк, что не всегда отрисовывается прогрессбар. Только не удается найти условия, при которых это происходит. Могу сказать, что используется прогресс бар у меня в потоке SwingWorker'а в отдельном диалоговом окне.

                                            В RTL:
                                            — неправильно отображается меню (Иконки остаются слева, стрелочка подменю направлена не в ту сторону).
                                            — подменю может уезжать на пикселей 20 от меню родителя.
                                            — заголовок и иконка JInternalFrame уезжает куда-то
                                            — кнопки управления JInternalFrame отображаются неправильно и не в том месте (остаются справа, когда должны уходить влево)
                                            это то, что заметил при беглом тестировании
                                            • 0
                                              Ясно, буду править :)

                                              Насчёт RTL в меню — страшно подумать зачем оно вовсе надо (но видимо надо). Это займет побольше времени.

                                              JDesktopPane/JInternalFrame до конца толком ещё не стилизованы. Над ними ещё буду работать.

                                              Насчёт прогресса — весьма странно. Нигде у себя не замечал подобных проблем (уже на реальных проектах).
                                              • 0
                                                RTL нужен для ряда языков, где письмо идет справа-на-лево.
                                                • 0
                                                  Это то понятно, просто очень редко когда эта возможность реально необходима, мне кажется…

                                                  Тем более если приложение в рамках ru/en/de/fr языков.
                                                  • 0
                                                    Отнюдь вас не вынуждаю заниматься поддержкой RTL. Это действительно специфическая ситуация и это не стоит больших усилий с вашей стороны.
                                                    • 0
                                                      Ну отчасти поддержка уже присутствует, просто в некоторых отдельных компонентах были проблемы с этим и я отложил реализацию. В первой же стабильной версии-релизе RTL будет поддерживаться.
                        • 0
                          И нет, не замучали :)
                          Чем больше проблем получится устранить — тем лучше.
                    • 0
                      Отключить можно вот так:
                      WebCheckBox webCheckBox = new WebCheckBox ();
                      webCheckBox.setAnimated ( false );

                      Или же вот так:
                      JCheckBox checkBox = new JCheckBox ();
                      ( ( WebCheckBoxUI ) checkBox.getUI () ).setAnimated ( false );
                    • 0
                      Чуть позже все подобные параметры можно будет также отключить и глобально для всего LaF'а.
  • +1
    Запустил пример в OpenJDK начал было радоваться что абсолютно все идентично с SunJDK, но все же один косяк вылез. В секции Textareas полетело все форматирование. Надеюсь этот косяк починят, как никак, но линия партии призывает всех переходить на OpenJDK. В остальном все отлично заработало, несмотря на то, что в OpenJDK есть известные проблемы с Java2D.
    • +1
      А можно более конкретно о том, что же полетело?
      Точнее что подразумевается под форматированием?
      • +1
        Ubuntu 11.04. OpenJDK 1.6 выглядит как то так. Ресайз окна не влияет ни на что. Но в остальном все работает отлично. Больше никаких отличий в поведении от sunjdk не замечено. Я так думаю что лайот менеджер врет.
        image
        • +1
          Всё проще — немного «кривое» поведение текстовых полей.
          Я не задавал размеры полей (их на этой вкладке 3 разных), поэтому они и съехали.

          «По хорошему» они должны были быть высотой в 4 строки каждый и preferred-ширины, но…
          Видимо, стандартная реализациия getPreferredSize у BasicScrollPaneUI в OpenJDK отличается и скроллы просто наврали с размерами (с шириной по тексту и высотой по 4 строкам).

          Я уже в процессе тестирования библиотеки под OpenJDK — постараюсь убрать все недочёты и сделать библиотеку ещё менее зависимой от таких вот «огрехов».

          Чуть позже отпишу что нашлось по теме — изложенное выше лишь мои предположения (не безосновательные, конечно, но всё же).
        • +1
          Достать бы только OpenJDK под винду…
          А то под виртуалкой это всё печально тестить
          • +1
            Да, это забавный квест. Я тоже пытался это сделать, но под виндой это можно сделать только из исходников. Причем компилировать нужно с помощью компилятора из cygwin в вижуал студии 2003… Мануал по компиляции напоминает сценарий фильма ужасов. Вообще, приемлемых способов установить OpenJDK под виндой нет, а значит и не будет пользователей, то и смысла тестировать нет. Лучше тестировать OpenJDK сразу на убунте. Проблем на виртуалке быть не должно, если конечно не использовать 3D. А если лень ставить вторую систему и не хочется возится с вируалкой, то можно установить нужный софт на LiveUSB.
            • +1
              Да, я уже прочитал мануал, порадовался списку необходимых вещей и операций…
              И мееедленно устанавливаю новую убунту (как раз 11.04) — на ней и погоняю.

              Просто VMWare как-то по чёрному сбоит на 7ке, пришлось переползти на VirtualBox.
            • +1
              Кстати, насчёт следующего обновления — оно будет включать доделки по UI JDesktopPane и JInternalFrame, фиксы для OpenJDK, LTR у компонентов и небольшие (но полезные) дополнения в UI кнопок. Возможно также и некоторые другие менее значимые мелочи.

              Думаю что к этим выходным (может даже и раньше) обновление будет готово.

              Останется только вопрос с JFileChooser/JColorChooser — с ними придётся поработать вплотную, чтобы переписать под мои готовые варианты.

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

Самое читаемое Разработка