非矩形フレーム、あるいは透明フレーム

これもJava SE 6u10 の機能なんですが、JavaFX で非矩形フレームや透明フレームを扱うことができるようになりました。

Java SE 6u10 以前の Java では、java.awt.Robot を使って、擬似透明フレームを作ることはできたんですけど、あくまでも擬似は擬似。

やっと、できるようになってうれしい限りなんですが、Java 単体ではできないのかなぁ。

透明にするのは簡単です。JavaFX Script ではフレームは javafx.application.Frame クラスと javafx.ext.swing.SwingFrame クラスの 2 つがあるのですが、透明にできるのは Frame クラスだけです。

やり方は簡単。WindowStyle を TRANSPARENT にするだけ。後は Stage でコンテンツを指定します。ちなみに、Stage はデフォルトで背景が白になっているので、背景を消すため fill: null とします。

ちなみのちなみ。WindowStyle は他に DECORATED と UNDECORATED があります。

ソースはこんな感じ。これで背景が透明になります。

Frame {
    width: 100
    height: 100

    // WindowStyle は TRANSPARENT
    windowStyle: javafx.application.WindowStyle.TRANSPARENT
    
    visible: true

    stage: Stage {
        // 背景を塗らないようにする
        fill: null
        content: [ ... ]
    }
}

背景が透明にできるというと、やりたくなるのが xeyes。
目がマウスカーソルの方を向くというあれです。

というわけで作ってみました。

f:id:skrb:20081108003538p:image

参考までにソースを示しておきます。
ドラッグすると移動、マウスの左ボタン以外をクリックするとポップアップメニューで終了させることができます。

import javafx.animation.KeyFrame;
import javafx.animation.Timeline;
import javafx.application.Frame;
import javafx.application.Stage;
import javafx.application.WindowStyle;
import javafx.input.MouseEvent;
import javafx.scene.geometry.Circle;
import javafx.scene.geometry.Ellipse;
import javafx.scene.paint.Color;

import java.lang.Math;
import java.lang.System;
import java.awt.MouseInfo;
import java.awt.Point;
import java.awt.PointerInfo;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import javax.swing.JPopupMenu;
import javax.swing.JMenuItem;

var rightX: Number;
var rightY: Number;
var leftX: Number;
var leftY: Number;

var strokeWidth: Number;
var eyeRadius = bind strokeWidth;

var frame: Frame;
var width = 150;
var height = 100;

// ポップアップメニュー用のアクションリスナー
class ExitAction extends ActionListener {
    function actionPerformed(event: ActionEvent) {
        System.exit(1);
    }
};

// ポップアップメニューを表示
function showPopupmenu(event:MouseEvent) {
    if (event.getButton() != 1) {
        var popup = new javax.swing.JPopupMenu();
        var menuItem = new javax.swing.JMenuItem("Exit");
        menuItem.addActionListener(ExitAction{});
        popup.add(menuItem);
                        
        popup.show(event.awtMouseEvent.getComponent(), 
                   event.getX(), event.getY());
    }
};

// フレームの移動 (ドラッグ時)
function moveFrame(event:MouseEvent) {
    var x = event.getDragX() as Integer;
    var y = event.getDragY() as Integer;
    frame.x = frame.x + x;
    frame.y = frame.y + y;
};

// 目玉の位置を計算
function calcEyePosition(x: Number, y: Number, cx: Number, cy: Number): Point {
    // 角度は arctan で求めます
    var theta = Math.atan2(y - cy, x - cx);
    var hr = width/4 - strokeWidth - eyeRadius;
    
    var eyeX: Number;
    var eyeY: Number;
    
    // マウスカーソルの位置が目の内部だったら、カーソルの位置に目玉を描画
    if (Math.abs(x - cx) > Math.abs(hr * Math.cos(theta))) {
        eyeX = cx + hr * Math.cos(theta);
    } else {
        eyeX = x;
    }
    
    var vr = height/2 - strokeWidth - eyeRadius;
    if (Math.abs(y - cy) > Math.abs(vr * Math.sin(theta))) {
        eyeY = height - cy - vr * Math.sin(theta);
    } else {
        eyeY = height - y;
    }
    
    return new Point(eyeX as Integer, eyeY as Integer);
};

frame = Frame {
    title: "FXEyes"
    width: bind width
    height: bind height

    // ここで透明にする
    windowStyle: WindowStyle.TRANSPARENT
    
    visible: true

    stage: Stage {
        // 背景を透明にする
        fill: null
        content: [
            Ellipse { // 左目
                // フレームの幅と高さで目の大きさを決めているのですが、
                // フレームの大きさを可変することはやっていません
                centerX: bind width/4
                centerY: bind height/2
                radiusX: bind width/4 - strokeWidth/2
                radiusY: bind height/2 - strokeWidth/2
                stroke: Color.BLACK
                strokeWidth: bind strokeWidth
                fill: Color.WHITE

                onMousePressed: function(event:MouseEvent) {
                    showPopupmenu(event);
                }
                
                onMouseDragged: function(event:MouseEvent) {
                    moveFrame(event);
                }
            },
            Circle {  // 左の目玉
                centerX: bind leftX
                centerY: bind leftY
                radius: bind eyeRadius
                fill: Color.BLACK
            },
            Ellipse {  // 右目
                centerX: bind width/4 * 3
                centerY: bind height/2
                radiusX: bind width/4 - strokeWidth/2
                radiusY: bind height/2 - strokeWidth/2
                stroke: Color.BLACK
                strokeWidth: bind strokeWidth
                fill: Color.WHITE

                onMousePressed: function(event:MouseEvent) {
                    showPopupmenu(event);
                }

                onMouseDragged: function(event:MouseEvent) {
                    moveFrame(event);
                }
            },
            Circle {  // 右の目玉
                centerX: bind rightX
                centerY: bind rightY
                radius: bind eyeRadius
                fill: Color.BLACK
            }
        ]

    }
};

var timeline = Timeline {
    repeatCount: Timeline.INDEFINITE
    keyFrames : [
        KeyFrame {
            // 50ms ごとに目玉の位置を更新
            time : 50ms
            action: function() {
                var point:PointerInfo = MouseInfo.getPointerInfo();
                var x = point.getLocation().getX();
                var y = point.getLocation().getY();
                
                x = x - frame.x;
                y = - y + frame.y + frame.height;
          
                // ほんとはフレームの大きさは可変にしていないので、
                // こんなこといらないんですが...
                if (width < height) {
                    strokeWidth = if (width > 100) 10 else width/10;
                } else {
                    strokeWidth = if (height > 100) 10 else height/10;
                }    
                strokeWidth = if (strokeWidth < 1) 1 else strokeWidth;

                var p = calcEyePosition(x, y, width/4, height/2);
                System.out.println("x {p.x} y {p.y}");
                leftX = p.x;
                leftY = p.y;
                
                p = calcEyePosition(x, y, width/4*3, height/2);
                rightX = p.x;
                rightY = p.y;
            }
        }
    ]
};
timeline.start();