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

Обычное окно 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 — нет, потому что ресурсы внутри архива не являются обычными файлами. Для боевого приложения форму лучше подготовить заранее и положить в ресурсы либо кешировать в пользовательском каталоге.
Итого получаем настоящее окно произвольной формы: прозрачное, кликабельное только по видимой области и способное перемещаться без системного заголовка. Теперь можно делать интерфейс в виде облака. Делать интерфейс в виде облака не обязательно, но технически вам уже никто не запретит.



