JFrame произвольной формы

Произвольная форма окна, на примере Microsoft Word

Обычное окно Swing похоже на кирпич: прямоугольное, с рамкой и унылой системной шапкой. Для формы ввода это нормально, но иногда дизайнер приносит облако, шестерёнку или говорящего хомяка и требует сделать из этого окно. Спорить уже поздно — придётся вырезать JFrame по картинке.

Фокус довольно простой. Берём PNG с прозрачным фоном, проходим по его пикселям и собираем геометрическую маску только из непрозрачной части. Затем отдаём эту маску окну. Всё, что не попало в неё, перестаёт существовать не только визуально: по прозрачным углам можно нажимать на окна, лежащие ниже.

Для примера понадобятся:

  • PNG с альфа-каналом;
  • окно без стандартной рамки;
  • Shape, повторяющий непрозрачную часть картинки;
  • немного ручного труда, чтобы окно без заголовка снова можно было таскать мышкой.

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

AnyFrame.java

package ru.jcup.education.graphics.swing.anyshape;

import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Point;
import java.awt.Shape;
import java.awt.Toolkit;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.MouseMotionListener;
import java.awt.image.BufferedImage;

import javax.swing.JFrame;
import javax.swing.SwingUtilities;

import ru.jcup.education.graphics.java2d.utils.LoadImage;

import com.sun.awt.AWTUtilities;

public class AnyFrame extends JFrame implements MouseListener, MouseMotionListener {

	private static final long serialVersionUID = 1L;
	private BufferedImage image;

	AnyFrame() {
		this.setUndecorated(true);

		String imagePath = "/ru/jcup/education/graphics/resource/png/cloud.png";

		image = LoadImage.loadBufferedImage(imagePath);
		this.setSize(image.getWidth(), image.getHeight());

		Shape frameShape = ShapeUtils.prepareShape(imagePath, image);

		AWTUtilities.setWindowShape(this, frameShape);
		AWTUtilities.setWindowOpacity(this, 0.90f);
		AWTUtilities.setWindowOpaque(this, false);

		centerScreen(this);
		this.addMouseListener(this);
		this.addMouseMotionListener(this);
		this.setVisible(true);
	}

	@Override
	public void paint(Graphics g) {
		g.drawImage(image, 0, 0, image.getWidth(), image.getHeight(), null);
	}

	private void centerScreen(JFrame frame) {
		Dimension dim = Toolkit.getDefaultToolkit().getScreenSize();
		int w = frame.getSize().width;
		int h = frame.getSize().height;
		int x = (dim.width - w) / 2;
		int y = (dim.height - h) / 2;
		frame.setLocation(x, y);
	}

	private Point start_drag;
	private Point start_loc;

	@Override
	public void mouseDragged(MouseEvent e) {
		final Point current = this.getScreenLocation(e);
		SwingUtilities.getRoot(this).setLocation(
				new Point(this.start_loc.x + current.x - this.start_drag.x,
						this.start_loc.y + current.y - this.start_drag.y));
	}

	@Override
	public void mouseMoved(MouseEvent arg0) {
	}

	@Override
	public void mouseClicked(MouseEvent arg0) {
	}

	@Override
	public void mouseEntered(MouseEvent arg0) {
	}

	@Override
	public void mouseExited(MouseEvent arg0) {
	}

	@Override
	public void mousePressed(MouseEvent e) {
	    this.start_drag = this.getScreenLocation(e);
	    this.start_loc = SwingUtilities.getRoot(this).getLocation();
	}

	@Override
	public void mouseReleased(MouseEvent arg0) {
	}

	private Point getScreenLocation(final MouseEvent e) {
		final Point cursor = e.getPoint();
		final Point target_location = this.getLocationOnScreen();
		return new Point(target_location.x + cursor.x, target_location.y + cursor.y);
	}

}

Первым делом вызываем setUndecorated(true). После показа окна делать это уже поздно: Swing успеет создать нативную рамку и справедливо пошлёт нас с переделками.

Размер окна берём прямо из изображения. Метод prepareShape строит форму, а три вызова AWTUtilities применяют её, задают общую прозрачность и разрешают прозрачные пиксели. В результате фон PNG исчезает вместе с прямоугольными углами окна.

Системной шапки больше нет, а вместе с ней исчезла и возможность перетаскивания. Поэтому в mousePressed запоминаем начальную позицию курсора и окна, а в mouseDragged переносим корневой компонент на разницу координат. Никакой магии — обычное вычитание, просто на этот раз оно двигает целое окно.

Вырезаем форму по PNG

ShapeUtils.java

package ru.jcup.education.graphics.swing.anyshape;

import java.awt.Rectangle;
import java.awt.Shape;
import java.awt.geom.Area;
import java.awt.geom.GeneralPath;
import java.awt.geom.PathIterator;
import java.awt.image.BufferedImage;
import java.io.DataInput;
import java.io.DataOutput;
import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;

/**
 * This code get from http://habrahabr.ru/post/127240/
 * Method prepareShape write by Saa.
 * This code provide an opportunity automated create shape file at first load.
 */
public class ShapeUtils {

	public 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;
	}

	public 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);
			for (final double coord : coords) {
				os.writeDouble(coord);
			}
			pi.next();
		}
	}

	public 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);
	}

	public static Shape prepareShape(String imageFile, BufferedImage image) {
		String shapeFile = imageFile.substring(0, imageFile.lastIndexOf(".")) + ".shp";
		String imageFileFull = System.getProperty("user.dir") + File.separator + "src" + shapeFile;

		File file = new File(imageFileFull);
		if (!file.exists()) {
			System.out.println("Компилирую изображение в форму.");
			System.out.println(imageFileFull);
			Shape shapeImage = ShapeUtils.contour(image);
			try {
				DataOutput dataOuput = new RandomAccessFile(file, "rw");
				ShapeUtils.save(shapeImage, dataOuput);
			} catch (IOException e) {
				e.printStackTrace();
			}
			return shapeImage;
		} else {
			Shape shapeImage = null;
			//System.out.println("Загружаю форму изображения.");
			DataInput dataInput;
			try {
				dataInput = new RandomAccessFile(file, "r");
				shapeImage = ShapeUtils.load(dataInput);
			} catch (IOException e) {
				e.printStackTrace();
			}
			return shapeImage;
		}
	}

}

Метод contour начинает с прямоугольника размером во всё изображение и вычитает из него каждый прозрачный пиксель. Проверка идёт по альфа-каналу:

(image.getRGB(x, y) & 0xFF000000) != 0xFF000000

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

Попиксельное вычитание Area — занятие небыстрое. Чтобы не мучить процессор при каждом запуске, готовая форма сохраняется рядом с ресурсом в файл .shp. В следующий раз приложение просто читает набор сегментов и восстанавливает GeneralPath. Для маленькой картинки разница незаметна, а на здоровенной дизайнерской простыне кеш уже спасает нервы.

Запускаем без нарушения древних ритуалов

Swing-компоненты создаём в Event Dispatch Thread:

public static void main(String[] args) {
	SwingUtilities.invokeLater(AnyFrame::new);
}

В примере используется com.sun.awt.AWTUtilities. Когда код писался, это был рабочий способ, но класс внутренний и современной Java ничем вам не обязан. Начиная с Java 7 необходимые методы есть у самого Window, поэтому старые вызовы можно заменить:

setShape(frameShape);
setOpacity(0.90f);
setBackground(new Color(0, 0, 0, 0));

Для последней строки потребуется java.awt.Color. Поддержка прозрачности зависит от графического окружения и оконного менеджера. На обычном настольном Windows или Linux всё, скорее всего, заведётся, а в headless-режиме, удалённой консоли или особенно творческом композиторе — уже как договоритесь.

Ещё один важный момент: старый prepareShape пытается записать .shp внутрь каталога src. Из IDE это работает, а из собранного JAR — нет, потому что ресурсы внутри архива не являются обычными файлами. Для боевого приложения форму лучше подготовить заранее и положить в ресурсы либо кешировать в пользовательском каталоге.

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

Можно поставить столько лайков, сколько хочешьСколько хочешь лайков — столько и ставь
0просмотров

Комментарии