Как стать автором
Обновить

Окна «неправильной» формы, Java 6 & 7 ed

Время на прочтение7 мин
Количество просмотров12K
Симпотичная?

По следам бременских музыкантов


Как, наверное, многие помнят, в прошлом веке (еще во времена Windows 2000), было модно создавать всяческие splash-экраны и миниаппликации в окнах непрямоугольной формы (как и необычные элементы управления).
Писались эти понты на С\С++ с применением WinAPI с использованием т.н. регионов. Дело это было не таким простым, поскольку приходилось не только спотыкаться о косяки и Windows и языка, но и просчет полигонов для отрисовки тоже отпугивал. Поэтому, «нарисовав» одно-два округлых окошка, я отложил эту тему в долгий ящик.
И вот в этот понедельник промелькнула статья «Окна «неправильной» формы», снова обратившая моё внимание к этой теме. Ожидая узнать, что в .NET для этих целей реализованы функции-обёртки WinAPI, был разочарован, увидев описания внешних функций. И тут я, как программист в основном на Java, вспомнил, что, тогда ещё Sun, обещал ввести функции для отрисовки окна произвольной формы.



Чашечка кофе


Ну что ж, какие возможности предоставил нам Sun/Oracle?
  1. JNI/JNA:
    • зависит от платформы
    • писать дополнительный нативный код для каждой системы
  2. Эмуляция прозрачности путем прорисовки фона:
    • окно всё равно кликабельно
    • медленная прорисовка
    • заметны задержки при перемещении окна
    • не реагирует оперативно на изменение фона (особенно, беда с анимацией на фоне)
  3. Класс com.sun.awt.AWTUtilities:
    • доступен с версии 1.6_10 только в Sun VM
    • внутренний класс, поддержка которого не гарантируется
  4. Публичные методы классов 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:
  1. Перед тем, как показывать окошко, уберём полосу заголовка (отключим стандартные декорации ОС):
    this.setUndecorated(true);
  2. Попросим возможность сделать окно полупрозрачным:
    com.sun.awt.AWTUtilities.setWindowOpaque(this, false);
  3. Установим степень прозрачности окна:
    com.sun.awt.AWTUtilities.setWindowOpacity(this, .75);

    Значение второго аргумента должно находиться в пределах 0 — 1 (в нашем
    случае 75%).
    Можно даже сделать окно абсолютно прозрачным, но оно будет реагировать на
    нажатия
  4. сконструировав нужный Shape, установим нужную нам форму для окна:
    com.sun.awt.AWTUtilities.setWindowShape(this, s);

    Теперь, магическим образом, всё, что находится за пределами нашей фигуры, не
    будет не только не рисоваться, но и не реагировать на нажатия (вспомним
    подход из книги "Swing Hacks")
  5. Раз мы лишились стандартной полоски заголовка, но хотим перемещать окно,
    добавим возможность перемещать окно щелчком в любой части окна (для этого я
    использовал класс MoveMouseListener из той же книги)
  6. Добавим кнопку «Закрыть» в окно, тоже нестандартную

Вот это формы!


Следующая задача: из любой картинки с прозрачным фоном, хорошо отделённого от переднего плана, составить её замкнутый контур. Для своих извращённых пыток я нагуглил вот такую прелестную барышню:
Барышня не моя, это только пример
Вначале я подумывал реализовать метод выпуклой оболочки (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);
}

Разумеется, для корректного восстановления необходим правильный файл — для простоты я не провожу никаких проверок, и никакие исключения не обрабатываются (а вообще нужно бы!). Формат файла такой:
  1. Направление обхода контура: по часовой стрелке или против (целое)
  2. Сколько угодно блоков:
    1. Тип кривой (целое)
    2. Опорные точки кривой (массив из 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. Чтобы уменьшить разницу версий, специфичные запросы вынес в методы класса, а вспомогательные функции — в отдельный класс утилит.



Чтиво по теме:


Теги:
Хабы:
+27
Комментарии87

Публикации

Истории

Работа

Java разработчик
358 вакансий

Ближайшие события

Weekend Offer в AliExpress
Дата20 – 21 апреля
Время10:00 – 20:00
Место
Онлайн
Конференция «Я.Железо»
Дата18 мая
Время14:00 – 23:59
Место
МоскваОнлайн