つらねの日記

プログラムの進捗やゲームをプレイした感想などを書き連ねる日記。

Java, Processingで非矩形ウィンドウ

矩形でない自由な形状のウィンドウ。夢があるとは思いませんか。

前置き

以前スケジューラーの類を作った時からちょくちょく非矩形ウィンドウは気になって調べていた。当時はJavaの環境を構築していなかったためProcessingでやろうとしていたが、知識がなかったためにJavaのコードがProcessingでも使えるものがあるということが理解できておらず大変苦戦したことを覚えている。
調べた結果、作るためには透明ウィンドウを作るとよいということはわかったが、描画部分も含めウィンドウ全体の不透明度が下がったりして思うとおりに行かなかったこともあった。最終的には作ることができたが、バッファリングも使わず毎フレームcolor(0, 0, 0, 0)で1pixelsずつ全ピクセルを埋めるなどという非効率的極まりない方法をとっていたため、ちらつきが激しかった。加えてProcessingが標準で用意してくれるFrameを放棄して新たに別のJFrameを作ることにより実現していた。

背景を投影する方法について

今思い出してみると、昔HSPを触っていたころからスクリーンショットを利用してウィジェットを作ろうとしていたことも思い出すと、かなり昔からウィジェットやらデスクトップ上を動き回る物体やらを作りたかったのかもしれない。
少し前に知ったGhostライブラリもこの方法をとっていて、プログラムを実行した際にとったスクリーンショットを描画するという手法をとっている。この場合、背景を投影しているだけなのでマウスイベントは透過せず、後ろの物が見えていても後ろの物はクリックできない。さらに背景が変わってもウィンドウ上の投影物は変わることがない。

しかしながら、求めているのは非矩形ウィンドウなのである。枠と一緒に描画部分まで透明になったり、描画していないところまでクリックできてしまっては困るのである。(どうでもいい話だが、艦これはちゃんと艦娘をクリックしないと反応を返さないが、刀剣乱舞は適当な背景をクリックした際でも反応を返す。)

JFrameの透明化が簡単になっていた

先の艦と刀を平行するにあたり - つらねの日記スクリプト機能を実装するにあたり、スクリプトが実行中かそうでないかを視覚化するためにどうせならウィジェットを使うかという発想に至ったのである。
そこで久しぶりに透明ウィンドウについて調べてみると、なんと簡単な方法が出来ているではないか。昔は装飾を無効化してOpacityを設定してどうのこうのとかやらされた記憶があるのにだ。簡単に、さくっと。

OpacityFrame.java
import java.awt.Color;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.MouseInfo;
import java.awt.Point;
import java.awt.image.BufferedImage;

import javax.swing.JFrame;


public class OpacityFrame extends JFrame implements Runnable{
	private BufferedImage buffer;
	private BufferedImage object;
	private double angle, aangle;
	private Point p=null;

	private OpacityFrame(){
		setSize(300, 300);
		setAlwaysOnTop(true);
		
		//枠をなくす
		setUndecorated(true);
		//透明にする
		setBackground(new Color(0, 0, 0, 0));

		buffer=new BufferedImage(getWidth(), getHeight(), BufferedImage.TYPE_INT_ARGB);
		object=createObjectImage();

		setVisible(true);

		new Thread(this).start();
	}

	//適当なものを作る
	private BufferedImage createObjectImage() {
		BufferedImage img=new BufferedImage(getWidth(), getHeight(), BufferedImage.TYPE_INT_ARGB);
		Graphics g=img.getGraphics();

		double r1=0, r2=0, r4=0, r5=0;
		final double t1=0.2, t2=1, t4=2*t2+t1, t5=4*t2+t1, 
				h2=0.5, h4=0.4, h5=0.3;

		g.setColor(new Color(80, 240, 140, 30));
		for(int i=0;i<20000;i++){
			double b=Math.sin(r2);
			double a=(b*h2+1)*70;
			double x=(a*(Math.sin(r1)+Math.sin(r4)*h4+Math.sin(r5)*h5));
			double y=(a*(Math.cos(r1)+Math.cos(r4)*h4+Math.cos(r5)*h5));
			int X=(int)Math.round(x);
			int Y=(int)Math.round(y);
			int S=(int)Math.round((x*x+y*y)/3000+2);
			g.fillOval(getWidth()/2+X, getHeight()/2+Y, S, S);
			r1+=t1/200;
			r2+=t2/200;
			r4+=t4/200;
			r5+=t5/200;
		}

		return img;
	}


	public void paint(Graphics g){
		Graphics2D g2=(Graphics2D)buffer.getGraphics();

		//background処理
		g2.setBackground(new Color(0, 0, 0, 0));
		g2.clearRect(0, 0, getWidth(), getHeight());

		//適当なものを描画
		g2.translate(getWidth()/2, getHeight()/2);
		g2.rotate(angle);
		g2.drawImage(object, -getWidth()/2, -getHeight()/2, this);

		//background処理
		((Graphics2D)g).setBackground(new Color(0, 0, 0, 0));
		g.clearRect(0, 0, getWidth(), getHeight());

		g.drawImage(buffer, 0, 0, this);
	}

	public static void main(String[] args) {
		new OpacityFrame();
	}

	@Override
	public void run() {
		while(true){
			repaint();
			java.awt.Point pp=MouseInfo.getPointerInfo().getLocation();
			setLocation(pp.x-getWidth()/2,pp.y-getHeight()/2);

			if(p==null)
				p=new Point();
			else
				aangle+=(pp.x-p.x)*Math.PI/1000;
			p.setLocation(pp);
			angle+=aangle;
			aangle*=0.95;

			try {
				Thread.sleep(20l);
			} catch (InterruptedException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
		}
	}

}

これはマウスを追従し、x方向の動きに応じて回転する物体である。
swingのJFrameやJDialogなどの枠をなくし、背景を透明にしたうえで適当なものを描画することで非矩形ウィンドウを作ることができる。
因みに静的なものの場合はpaint内部のbackgroundにあたる処理は行う必要がなくなる。
またダブルバッファ的なことを用いることによってちらつきを軽減することができる。

これで自由な形状のウィジェットやデスクトップ上を縦横無尽に動き回るキャラクターなどを作ることができるはずなので、良いアイディアが浮かぶか素材が用意できたら作りたい。

Processingでもできるか

でだ。これを書いているときに描画面倒だなと思ったわけだ。
Processingで以前作った時はもともとの物を廃棄したが、それを廃棄せずに透明化することはできないかと考えたわけだ。

できなかった

同様の手法を用いて、非矩形ウィンドウまでは作ることができたものの、なぜかpaint内部の処理が最初の1F~4F程度しか反映されなかった。関数自体は呼ばれているにも関わらずだ。
わけがわからないのでframeのClassを確認してみたところ、processing/PApplet.java at master · processing/processing · GitHubではawt.Frameとなっており、Processing内部でprintln(frame.getClass())と書いてみたところswing.JFrameだった。完全に理解不能だ。

OpacityFrame.processing
import javax.swing.*;
import java.awt.*;

final Color OPACITY=new Color(0, 0, 0, 0);

F f;

//PGraphics g;
void setup() {
  size(200, 200);
  frame.removeNotify();
  frame.setUndecorated(true);

  frame.setBackground(OPACITY);
  g=createGraphics(width, height);

  f=new F();
}

class F extends JFrame {
  public F() {
    setSize(width, height);
    setUndecorated(true);
    setBackground(OPACITY);
    setVisible(true);
  }

  public void paint(Graphics gg) {
    Graphics2D g2=(Graphics2D)gg;

    g2.setBackground(OPACITY);
    g2.clearRect(0, 0, width, height);

    g2.drawImage(g.image, 0, 0, frame);
    g2.setColor(Color.white);
    g2.drawString("Java", 0, 10);
  }
}

void draw() {
  loadPixels();
  java.util.Arrays.fill(g.pixels, OPACITY.getRGB());
  updatePixels();
  noFill();
  stroke(0);
  strokeWeight(20);
  rect(0, 0, width, height);
  noStroke();
  fill(#ff0000);
  ellipse(frameCount*10, height/2, 10, 10);
  fill(#0000ff);
  ellipse(width/2, frameCount*10, 10, 10);

  repaint();
  f.repaint();
}

void paint(Graphics gg) {
  println(frameCount);
  Graphics2D g2=(Graphics2D)gg;

  g2.setBackground(OPACITY);
  g2.clearRect(0, 0, width, height);

  g2.drawImage(g.image, 0, 0, frame);
  g2.setColor(Color.white);
  g2.drawString("Processing", 0, 10);
}

本当に描画が出来ているかどうかすら危ういと考えたので確認するためにJFrameを召喚して描画してみたが、PGraphics自体には描画がなされている模様だ。まあ結果論としてPGraphicsをGraphicsに比較的簡単に描画できるようにはなったため、オリジナルを廃棄するにせよ次からはこれを活用して行きたい。