JavaFXで自作のシーングラフのノードを作成する

JavaFXでは描画対象のオブジェクトは「シーングラフのノード(以下、ノード)」と呼ばれ、javafx.scene.Nodeを基底クラスとしたクラスのインスタンスになります。JavaFX 1.2.1時点でそれなりの数のノードは用意されていますが、それなりに凝ったGUIを作成するには足りません。しかし、JavaFXではノードを自作しやすいようにデザインされているため、非常に簡単にカスタムノードを作る事ができます。今回は、カスタムノードをどのように設計するかを解説します。
最初に最初にNodeを起点としたクラスの階層構造を把握します。これを把握しているとどのクラスを拡張して、カスタムノードを作れば良いかが解りやすくなります。尚、具象クラスはもっとたくさんありますが、このくらいを把握しておけば良いでしょう。

javafx.scene.Node
javafx.scene.shape.Shape
┃ ┗ javafx.scene.shape.Path
javafx.scene.Parent
  ┣ javafx.scene.Group
  ┃ ┗ javafx.scene.layout.Container
  ┃   ┣ javafx.scene.layout.HBox
  ┃   ┗ javafx.scene.layout.Panel
  ┗ javafx.scene.CustomeNode
    ┗ javafx.scene.Control

それでは、それぞれの役割と使い方をみていきます。

javafx.scene.Node

ノードの基底クラスになります。レイアウト周りで重要になるのは、layoutX,layoutYの2つでこの2つの値が親ノードに対する相対位置を決める要素になります。また、layoutBoundsはそのノードがどれだけの「大きさ」を持っているかを表す値です。これらの要素によってそのノードをどのように配置されるかが決まります。
Node自体をそのまま使用する事はほとんどありませんが、Nodeではなにが設定できるかはある程度把握しておくと便利です。

javafx.scene.shape.Shape/javafx.scene.Parent

Nodeの直系の子孫にはShapeとParentがあります。ShapeとParentの違いはレイアウトという概念の有無です。Shapeの場合、レイアウトという概念はなく、Rectangle(矩形)やCircle(円)がそのまま描画され、Parentの場合は通常は複数のノードを組み合わせたノードとなるため、どのようにレイアウトされるかがノードによって変わってきます。
ShapeとParentもそのまま使う事はほとんどありません。

javafx.scene.shape.Path

Shapeのサブクラスには様々な基本図形がありますが、Shapeを拡張して基本図形を作る事はありません。というのもShapeを拡張するには実装依存の箇所を触る必要がありますので、今のバージョンで動くものを作ったとしてもバージョンアップにより使えなくなる可能性が高いのです。
通常、独自の図形を作りたい場合は、Pathのサブクラスを定義します。Pathは直線やカーブなどを自由に記述できるShapeですので、座標計算が可能であれば大抵の図形は表現できる訳です。例えば三角形は次のようになります。

import javafx.scene.shape.Path;
import javafx.scene.shape.VLineTo;
import javafx.scene.shape.LineTo;
import javafx.scene.shape.MoveTo;
import javafx.scene.shape.HLineTo;

public class Triangle extends Path {
    public var width:Number = 10 on replace {reshape()};
    public var height:Number = 10 on replace {reshape()};
    function reshape() {
        this.elements = 
            [
                MoveTo { x:0, y:0 },
                VLineTo { y:width },
                LineTo { x:height, y:width/2 }
                LineTo { x:0, y:0 }
            ];
    }
}

elementsの内容をかえていけば、星形や台形など自由に図形を作る事ができるでしょう。

javafx.scene.Group

GroupはParentのサブクラスで、複数のコンポーネントをまとめて扱う為のノードです。変数contentの中に任意の数のノードを設定し、それらをシーケンスの頭から順番に描画していきます。この時、特別なレイアウトは行いません。したがって、各ノードはlayoutX,layoutYを相対位置として描画される絶対位置的なレイアウトとなります。
Group自体は1つのノードになるため、Groupを対象として描画位置を指定したり、エフェクトをかけたりする事ができます。GroupのlayoutBoundsは、内包するノードをすべて内包するような最小の矩形になります。
Groupのサンプルは次のようになります。

Group {
    content: [
        Rectangle {
            width: 10, height: 10 
            fill:Color.RED
        }
        Rectangle {
            layoutY: 20
            width: 10, height: 10 
            fill:Color.BLUE
        }
    ]
}

javafx.scene.layout.Container

ContainerはGroupのサブクラスで、Resizableをmixinしたノードです。Containerを単体で使う事はなく、通常はContainerのサブクラスを作成してレイアウトコンテナを定義します。例えば、VBoxやHBoxはContainerのサブクラスです。
Groupとは何が違うのかと言えば、Resizableをmixinすることで明示的な幅と高さを持つという事です。Groupの大きさはあくまで内包するノードに依存しますが、Contanierでは先に幅と高さを指定した中でレイアウトすると考えれば良いでしょう。ただし、VBoxなど実装次第では内包するノードに依存した幅と高さが算出されるので、実装依存です。
Containerでは奨励サイズ(幅・高さ)という概念があります。Containerは可能な限り内包するノードを奨励サイズで描画するように振る舞います。
Containerを作るにはサイズの算出などいろいろな要素があるのですが、基本的には次のようにdoLayoutの中で各ノードの位置を調整するような実装となります。

public MyLayout extends Container {
    override function doLayout() {
        for (node in getManaged(this.content)) {
             node.layoutX = ...;
             node.layoutY = ...;
        }
    }
}

javafx.scene.CustomeNode

CustomeNodeはParentのサブクラスで、名前の通りカスタムノードを作成するために必要な基本クラスとなります。CustomeNodeの仕組みは単純で、他のノードをラップするようなノードです。CustomeNodeのサブクラスでは、create関数をオーバーライドし、ラップするノードを生成します。
この時、生成するノードは単一のノードですが、Groupや各種レイアウトで複数のノードを内包していても構いません。したがって、単純な図形を幾つか組み合わせて描画できるノードをカスタムノードとするのは非常に簡単です。
CustomeNodeを作る場合に注意する点は、create関数がスーパークラスのinitで呼び出される事です。これはカスタムノードのinitの実行前になります。したがって、create関数の中でノードの生成を行わなければなりません。サンプルを示します。

public class MyNode extends CustomNode {
    override function create():Node {
        Group {
            content: [...] // content here
        };
    }
}

javafx.scene.Control

ControlはCustomeNodeのサブクラスで、TextBoxなどのユーザと対話する種類のノードの基底クラスになります。Controlの特徴は、描画するノードを定義するSkinと、描画するノードに対する操作や状態を管理するBehivarを分離しているところにあります。したがって、同じTextBoxであってもその表示(Skin)を差し替える事で見た目をかえる事が簡単になるようなデザインになっています。尚、カスタムコントロールの作成方法は今回は割愛です。

まとめ

図形を作りたい場合はパラメータを指定した再利用可能な図形を作る場合は、Pathのサブクラスを作成します。例えば、三角形ノードを作る場合です。

複数のノードを組み合わせて作る再利用可能なノードを作りたい場合は、CustomeNodeのサブクラスを作成します。ただし、再利用する必要がなければ、Groupのインスタンスで十分でしょう。
例えば、画像とその説明文を1つにまとめたラベル付き画像を作る場合です。

複数のノードをあるルールにしたがって配置したい場合は、Containerのサブクラスを作成します。
また、Panelのインスタンスを作成する方法もあります。Contanerのサブクラスではレイアウト系の関数をオーバーライドしますが、Panelでは関数型変数にレイアウト関数を定義する形式です。再利用しない場合は、Panelでも十分でしょう。
例えば、ノードを螺旋状に配置するようなレイアウトを作る場合です。

尚、実際に作る場合はこららの指針をもとに設計しますが、嵌りどころとしてはノードのサイズを適切に設定する事と初期化のタイミング(順番)です。それらは、またの機会に記事にしてみようと思います。

JavaFXでは非常にカスタムノードが作りやすくなっています。現在の標準ライブラリでは物足りないノード類ですが、作ろうと思えば凝ったコンポーネントを比較的短期間で実装できてしまうので、それほど悲観はしていません。また、カスタムノードを使いやすく設計できるのも強みになっています。