По следам бременских музыкантов
Как, наверное, многие помнят, в прошлом веке (еще во времена Windows 2000), было модно создавать всяческие splash-экраны и миниаппликации в окнах непрямоугольной формы (как и необычные элементы управления).
Писались эти понты на С\С++ с применением WinAPI с использованием т.н. регионов. Дело это было не таким простым, поскольку приходилось не только спотыкаться о косяки и Windows и языка, но и просчет полигонов для отрисовки тоже отпугивал. Поэтому, «нарисовав» одно-два округлых окошка, я отложил эту тему в долгий ящик.
И вот в этот понедельник промелькнула статья «Окна «неправильной» формы», снова обратившая моё внимание к этой теме. Ожидая узнать, что в .NET для этих целей реализованы функции-обёртки WinAPI, был разочарован, увидев описания внешних функций. И тут я, как программист в основном на Java, вспомнил, что, тогда ещё Sun, обещал ввести функции для отрисовки окна произвольной формы.
Чашечка кофе
Ну что ж, какие возможности предоставил нам Sun/Oracle?
- JNI/JNA:
- зависит от платформы
- писать дополнительный нативный код для каждой системы
- Эмуляция прозрачности путем прорисовки фона:
- окно всё равно кликабельно
- медленная прорисовка
- заметны задержки при перемещении окна
- не реагирует оперативно на изменение фона (особенно, беда с анимацией на фоне)
- Класс com.sun.awt.AWTUtilities:
- доступен с версии 1.6_10 только в Sun VM
- внутренний класс, поддержка которого не гарантируется
- Публичные методы классов java.awt.Window, java.awt.GraphicsEnvironment, java.awt.GraphicsConfiguration:
- доступен с версии 1.7.0, ура!
- что и досадно: как насчет OpenJDK?
На данный момент, наиболее приемлемым является предпоследний способ. Итак, задача у нас такая: Нарисовать полупрозрачное окно сложной непрямоугольной формы (на основе изображения), с фоновой картинкой, с возможностью перемещать окно, изменять размеры окна, размещать любые элементы управления (как стандартные, так и тоже произвольной формы)
Регионы, области, края
Покурив, покурив мануалы и туториалы, стало ясно, что за форму окна, как и любые геометрические фигуры, отвечают классы, реализующие интерфейс java.awt.Shape. Т.е., если нам удастся сгенерировать замкнутый Shape по нашей картинке, аналогично региону в Windows, задача будет решена.
Однако, по сложившейся кроссплатформенной традиции, нужно убедиться, что сама ОС умеет рисовать такие окна. Используя AWTUtilities, пишем, например, такой код:
if((com.sun.awt.AWTUtilities.isTranslucencySupported (com.sun.awt.AWTUtilities.Translucency.PERPIXEL_TRANSLUCENT)) && (com.sun.awt.AWTUtilities.isTranslucencyCapable (GraphicsEnvironment.getLocalGraphicsEnvironment().getDefaultScreenDevice().getDefaultConfiguration()))) { // необходимый код }
PERPIXEL_TRANSLUCENT означает, что графическая система способна выводить не только целиком прозрачные окна, но и, грубо говоря, «иметь дырки» в окошках, а также, что текущее устройство вообще умеет работать с прозрачностью (увы, Windows 98 в пролёте) Можно, конечно, и не проверять, но тогда при попытке использовать эти функции вылетит соответствующее исключение.
Ещё, может статься, что в системе установлена старая Java и никаких AWTUtilities не будет вообще — тогда можно воспользоваться вспомогательным классом AWTUtilitiesWrapper из этой статьи, который работает с AWTUtilities через отражение. Я не стал его использовать, поскольку достаточна вероятность, что у всех обновлённая Java, а код становится более непонятным. Будем работать напрямую с AWTUtilities:
- Перед тем, как показывать окошко, уберём полосу заголовка (отключим стандартные декорации ОС):
this.setUndecorated(true);
- Попросим возможность сделать окно полупрозрачным:
com.sun.awt.AWTUtilities.setWindowOpaque(this, false);
- Установим степень прозрачности окна:
com.sun.awt.AWTUtilities.setWindowOpacity(this, .75);
Значение второго аргумента должно находиться в пределах 0 — 1 (в нашем
случае 75%).
Можно даже сделать окно абсолютно прозрачным, но оно будет реагировать на
нажатия - сконструировав нужный Shape, установим нужную нам форму для окна:
com.sun.awt.AWTUtilities.setWindowShape(this, s);
Теперь, магическим образом, всё, что находится за пределами нашей фигуры, не
будет не только не рисоваться, но и не реагировать на нажатия (вспомним
подход из книги "Swing Hacks") - Раз мы лишились стандартной полоски заголовка, но хотим перемещать окно,
добавим возможность перемещать окно щелчком в любой части окна (для этого я
использовал класс MoveMouseListener из той же книги) - Добавим кнопку «Закрыть» в окно, тоже нестандартную
Вот это формы!
Следующая задача: из любой картинки с прозрачным фоном, хорошо отделённого от переднего плана, составить её замкнутый контур. Для своих извращённых пыток я нагуглил вот такую прелестную барышню:
Вначале я подумывал реализовать метод выпуклой оболочки (convex hull), но впоследствии отказался — наша барышня не только очень даже выпуклая, но местами и впуклая. На иное моих глупых мозгов в вечерний период не хватило, кроме как использовать решение «в лоб»: пробежаться по пикселам, удаляя прозрачные пиксели из начального прямоугольника . Получился такой код:
static Shape contour(final BufferedImage i) { final int w = i.getWidth(); final int h = i.getHeight(); final Area s = new Area(new Rectangle(w, h)); final Rectangle r = new Rectangle(0, 0, 1, 1); for (r.y = 0; r.y < h; r.y++) { System.out.println(r.y + "/" + h); for (r.x = 0; r.x < w; r.x++) { if ((i.getRGB(r.x, r.y) & 0xFF000000) != 0xFF000000) { s.subtract(new Area( r )); } } } return s; }
Прозрачными пикселями здесь считаются и полупрозрачные тоже (т.е. у которых alpha<255) — разумеется, если нужно, можно указать и некое пороговое значение, или даже назначить любой цвет скажем, белый: 0xFFFFFF). Но это неважно, важным оказалось другое — скорость, ведь никто не будет ждать 10 минут при загруженности проца 100%, пока наша Intro соизволит показаться. Да и генерировать форму каждый раз излишне, лучше сохранить где-либо, что бы потом быстро загрузить и показать.
R Tape loading error
Java имеет встроенные средства для сохранения и загрузки любых примитивов и объектов, которые реализуют интерфейс ava.io.Serializable, а также рекурсивно сериализуемы. Но вот беда: ни Shape, ни один из его реализующих классов не являются сериализуемыми! Довольно долго промучавшись, удалось получить интерфейс java.awt.geom.PathIterator, который позволяет пробежаться по контуру, что бы сохранить его, и класс java.awt.geom.GeneralPath, в который можно записать сохранённый ранее контур. И вот что получилось:
static void save(final Shape s, final DataOutput os) throws IOException { final PathIterator pi = s.getPathIterator(null); os.writeInt(pi.getWindingRule()); System.out.println(pi.getWindingRule()); while (!pi.isDone()) { final double[] coords = new double[6]; final int type = pi.currentSegment(coords); os.writeInt(type); System.out.println(type); for (final double coord : coords) { os.writeDouble(coord); System.out.println(coord); } System.out.println(""); pi.next(); } }
Использование интерфейса java.io.DataOutput позволяет сохранить данные куда угодно — я использовал редко встречающийся класс java.io.RandomAccessFile, его реализующий, но можно писать и в java.io.ObjectOutputStream. Можно(и даже лучше) сохранить файл рядом с class-файлами, чтобы потом его можно было бы достать даже из архива с апплетом.
static Shape load(final DataInput is) throws IOException { final GeneralPath gp = new GeneralPath(is.readInt()); final double[] data = new double[6]; CYC: while (true) { final int type = is.readInt(); for (int i = 0; i < data.length; i++) { data[i] = is.readDouble(); } switch (type) { case PathIterator.SEG_MOVETO: gp.moveTo(data[0], data[1]); break; case PathIterator.SEG_LINETO: gp.lineTo(data[0], data[1]); break; case PathIterator.SEG_QUADTO: gp.quadTo(data[0], data[1], data[2], data[3]); break; case PathIterator.SEG_CUBICTO: gp.curveTo(data[0], data[1], data[2], data[3], data[4], data[5]); break; case PathIterator.SEG_CLOSE: break CYC; } } return gp.createTransformedShape(null); }
Разумеется, для корректного восстановления необходим правильный файл — для простоты я не провожу никаких проверок, и никакие исключения не обрабатываются (а вообще нужно бы!). Формат файла такой:
- Направление обхода контура: по часовой стрелке или против (целое)
- Сколько угодно блоков:
- Тип кривой (целое)
- Опорные точки кривой (массив из 6 дробных)
Дополнительно вполне стандартным образом рисуем фоновую картинку — перегрузкой метода paint(Graphics g), а точнее — paintComponent(Graphics g)
Всё или ничего
Ну если уж мы сами рисуем окно, то для законченности нужно еще нарисовать и собственные элементы управления. Пока ограничусь только кнопкой «Закрыть».
- Нативные элементы управления SWT:
- вид зависит от платформы
- Расширение элементов управления Swing:
- для каждого элемента писать свой класс
- затруднительная смена внешнего вида (тем)
- Написание собственного Look-and-Feel:
- хлопотно
- конечно, есть же Synth...
- Декорирование элементов управления Swing c помощью javax.swing.plaf.LayerUI
- доступен только с версии 1.7.0 :(
Для демонстрации я пошёл вторым путём:
- Генерируем картинку и контур точно так же, как и для окна
- Расширяем класс JButton
- Делаем прозрачным:
this.setOpaque(false);
- Запрещаем прорисовку фона:
this.setContentAreaFilled(false);
- Также не будем рисовать ни рамку, ни линию фокуса:
this.setFocusPainted(false); this.setBorderPainted(false);
- Устанавливаем явные размеры, чтобы никакие менеджеры компоновки
(LayoutManager) не попортили картинку:
this.setSize(this.result.size); this.setMinimumSize(this.result.size); this.setMaximumSize(this.result.size);
- Делаем прозрачным:
- Замещаем метод paintComponent(Graphics g) для прорисовки картинки
- Замещаем метод contains(int x, int y) делегируя такому же метода для нашего контура
Finita la comedia
Всё! Делаем завершающие штрихи: Стандартным образом добавляем кнопку в окно и навешиваем на неё обработчик. Запускаем и любуемся:
Как и многие свои проекты, храню на xp-dev.com: страница проекта, svn-репозиторий
Там лежат два проекта для Eclipse Helios: Shaped для Java6 и Shaped7 для Java7. Чтобы уменьшить разницу версий, специфичные запросы вынес в методы класса, а вспомогательные функции — в отдельный класс утилит.
Чтиво по теме:
- Окна «неправильной» формы — .NET — Хабрахабр
- How to Create Translucent and Shaped Windows
- Java 7 — Translucency and shaped windows — Blog(-Baptiste Wicht-)
- How to Create Translucent and Shaped Windows (The Java Tutorials — Creating
a GUI With JFC-Swing — Using Other Swing Features) - The Synth Look and Feel (The Java Tutorials — Creating a GUI With JFC-Swing
— Modifying the Look and Feel) - How to Decorate Components with the JLayer Class (The Java Tutorials —
Creating a GUI With JFC-Swing — Using Other Swing Features) - «Swing Hacks: Tips and Tools for Killer GUIs» by Joshua Marinacci, Chris Adamson