ついにベールを脱いだJavaFX

第7回クラスを作る

今まで紹介してきたサンプルはすべて、JavaFX Scriptの標準APIを使用して作成してきました。しかし、本格的なアプリケーションを作成するには、標準APIだけではいかんともしがたい部分があります。特にMVCでいうところのM、つまりモデルの部分は標準APIだけで表すには難しい場合が多くあります。

そこで、今回はクラスを自作することに挑戦してみましょう。

クラスの定義

とりあえず、クラスを1つ作ってみましょう。名字と名前を保持するクラスです。

リスト1
class Name {
    var firstName: String;
    var lastName: String;
 
    function fullname(): String {
        return "{firstName} {lastName}";
    }
}

クラスの定義はclassで行います。波括弧の中では、アトリビュートと関数の定義を行います。アトリビュートの定義は、変数と同じくvarで行います。Preview SDKまではアトリビュートの定義はattributeだったのですが、JavaFX 1.0で変更されました。

実をいうとこの変更に伴って、アトリビュートという言い方は使わずに、すべて変数として表すことになりました。しかし、単に変数と書いたときにインスタンス変数なのかローカル変数なのか、もしくはスクリプト変数なのか区別しにくいため、この連載では以前からなじみのあるアトリビュートを使い続けることにします。

関数も波括弧の中で定義します。インタープリタ版では関数の実体は波括弧の外で行っていましたが、これも変更されました。

上のNameクラスの場合、firstNameとlastNameという2つのアトリビュートを持ち、いずれも型はStringです。そして、fullnameという関数を持ち、戻り値の型はStringです。

では、このクラスを使ってみましょう。

リスト2
var name = Name {
    firstName: "Yuichi"
    lastName: "Sakuraba"
}
 
println("Full Name: {name.fullname()}");

実行結果を以下に示します。

図1
Full Name: Yuichi Sakuraba

アトリビュートにデフォルト値を持たせるには、アトリビュートの定義で値を代入します。アトリビュートにデフォルト値を設定しない場合は表1のようになります。

表1 アトリビュートのデフォルト値
デフォルト値
String "" (null)
Integer 0
Number 0.0
Duration 0ms
Boolean false
オブジェクト null
関数 null
シーケンス [ ] (null)

Stringとシーケンスにnullと記載しているのは、空文字や要素のないシーケンスがnullと同等に扱われるためです。これも実際に試してみましょう。

リスト3
class Photograph {
    var title: String;
    var width: Integer;
    var height: Integer;
    var resizable: Boolean;
    var image: Image;
 
    // デフォルト値の代入
    var kind: String = "JPEG";
}
  
var photo = Photograph {};
 
println("Title: {photo.title}");
println("Width: {photo.width}");
println("Resizable: {photo.resizable}");
println("Image: {photo.image}");
println("Kind: {photo.kind}");

kindアトリビュートだけデフォルト値を設定しました。titleアトリビュートやwidthアトリビュートはデフォルト値はなく、オブジェクト生成時も値を代入していません。このスクリプトを実行すると、次のようになりました。

図2
Title: 
Width: 0
Resizable: false
Image: null
Kind: JPEG

この結果からもStringが空文字、Integerが0、そしてBooleanがfalseになることがわかります。また、オブジェクトはnullになります。

次に関数型のアトリビュートを定義してみましょう。関数型のアトリビュートを定義するには、引数と戻り値の型も定義しなくてはなりません。引数の型指定にはコロンはあってもなくてもかまいません。また、関数型のアトリビュートで定義したメソッドをコールするのは、普通の関数をコールするのとまったく変わりません。

リスト4
class Printer {
    // 引数、戻り値なし
    var reset: function(): Void;
 
    // 引数、戻り値あり
    var print: function(String): Boolean;
    // こちらでも OK
//  var print: function(:String): Boolean;
}
 
var printer = Printer {
    reset: function(): Void {
        println("reset");
    }
 
    print: function(text: String): Boolean {
        println("Print: {text}");
        return true;
    }
}
 
printer.print("Hello, World!");

resetアトリビュートは引数がなく、戻り値の型はVoidです。printアトリビュートは引数の型がString、戻り値の型がBooleanになります。実行結果を次に示します。

図3
Print: Hello, World!

varだけでなく、defで変数を定義することも可能です。第3回でdefを使うと、値を一度しか代入できないことを説明しました。それはクラス定義でも同じです。

リスト5
class MathConst {
    def pi: Number = 3.1415;
}

継承

次に継承させたクラスを作ってみましょう。サブクラスを作るにはJavaと同じくextendsを使用します。

リスト6
class Foo {
    function foo() {
        println("Foo.foo");
    }
}
 
class Bar extends Foo {
    function bar() {
        println("Bar.bar");
    }
}

var bar = Bar {}
bar.foo();
bar.bar();

BarクラスがFooクラスのサブクラスになります。実行すると、次のようになりました。

図4
Foo.foo
Bar.bar

スーパークラスの関数をオーバライドする場合、functionの前にoverrideと書きます。これはJavaの@Overrideアノテーションと同じですが、overrideを省略することはできません。また、サブクラスがスーパークラスの関数をコールするには、Javaと同じくsuperを使用します。

リスト7
class Foo {
    function foo() {
        println("Foo.foo");
    }
}
 
class Bar  extends Foo{
    override function foo() {
        super.foo();
        println("Bar.foo");
    }
}
 
var bar = Bar {}
bar.foo();

BarクラスがFooクラスのfoo関数をオーバーライドし、内部でFooクラスのfoo関数をコールしています。では、実行してみましょう。

図5
Foo.foo
Bar.foo

また、スーパークラスのアトリビュートのデフォルト値を書き換える場合もoverrideを使用します。

リスト8
class Foo {
    var foo = "FOO";
}

class Bar extends Foo {
    override var foo = "BAR";
}

var bar = Bar {};
println(bar.foo);

fooアトリビュートのデフォルト値を"FOO"から"BAR"に変更しています。実行すると以下のようになります。

図6
BAR

JavaFX Scriptにはインターフェースは存在しませんが、アブストラクトクラスは作成できます。アブストラクトクラスはクラス定義の前にabstractを記述し、アブストラクト関数にもabstractを書きます。もちろん、アブストラクトクラスはオブジェクトを作成することができないので、サブクラスを作成してオブジェクトを生成します。

サブクラスでスーパークラスのアブストラクトメソッドを定義する場合も、overrideは必要です。

リスト9
abstract class Foo {
    abstract function foo(): Void;
}
 
class Bar  extends Foo {
    override function foo(): Void {
        println("Bar.foo");
    }
}

var bar = Bar {}
bar.foo();

また、Javaとは異なり、JavaFX Scriptでは多重継承が可能です。多重継承を行うにはextendsの後にカンマ区切りでクラス名を列挙します。

リスト10
class Yoo {}
class Hoo {}

class YooHoo extends Yoo, Hoo {}

JavaFX ScriptではJavaのクラスのサブクラスを作成することも可能なのですが、すべてのクラスのサブクラスを作成できるわけではありません。JavaFX Scriptで作成できるJavaのサブクラスは、デフォルトコンストラクタ、つまり引数のないコンストラクタを持つクラスに限られます。

たとえば、java.io.FileReaderクラスはデフォルトコンストラクタがありません。このためFileReaderクラスのサブクラスを作成しようとしても、コンパイルエラーが発生します図7⁠。

図7 NetBeansでのコンパイルエラー
図1 NetBeansでのコンパイルエラー

また、Javaのインターフェースを継承することも可能です。JavaFX Scriptにはインターフェースはないので、Javaのインターフェースは普通のクラスとして扱います。たとえば、以下のようなJavaのインターフェースがあったとします。

リスト11
public interface Hello {
    public void sayHello(String name);
}

このHelloインターフェースを実装する場合でも、JavaFX Scriptではextendsを使用します。

リスト12
class FXHello extends Hello {
    override function sayHello(name: String): Void {
        println("Hello, {name}!");
    }
}

インターフェースのメソッドをオーバライドする場合でも、overrideを記述する必要があります。

初期化ブロック

JavaFX Scriptにはコンストラクタはないことは、みなさんご存じの通りです。オブジェクト生成時にアトリビュートに値を代入するだけであればいいのですが、オブジェクト生成時に何らかの処理を行いたい時があります。たとえば、通信を行うクラスのオブジェクトを生成すると、通信先に対してコネクトする処理などです。

このような場合、2つの方法を使用することができます。

  • initブロック
  • postinitブロック

initブロックはオブジェクト生成の最終段階で処理されるブロックです。initブロックの処理後にオブジェクトの生成が完了します。一方のpostinitブロックはオブジェクト生成が終了した後に処理されるブロックです。この2種類のブロックはどのように使い分ければいいのでしょう。その答えは、次のスクリプトを実行してみればわかります。

リスト13
class Foo {
    init {
        println("Foo.init");
    }
  
    postinit {
        println("Foo.postinit");
    }
}
 
class Bar extends Foo{
    init {
        println("Bar.init");
    }
  
    postinit {
        println("Bar.postinit");
    }
}
 
Bar{}

FooクラスとBarクラスにinitブロックとpostinitブロックを定義しました。これを実行するとどうなるでしょう。BarクラスがFooクラスのサブクラスということを考えなくてはいけません。

図8
Foo.init
Bar.init
Foo.postinit
Bar.postinit

この結果から、スーパークラスのinitブロックが処理された後、サブクラスのinitブロックが処理されることがわかります。そして、サブクラスのinitブロックが終了してから、スーパークラスのpostintブロックが処理されます。そして、最後にサブクラスのpostinitブロックが処理されます。

この順番を理解していれば、initブロックとpostinitブロックを使い分けることができるのではないでしょうか。

アクセス修飾子

今までの説明ではわざと省略していたのですが、JavaFX Scriptにもアクセス修飾子があります。しかも、Javaにはないアクセス修飾子もあります。表2に基本的なアクセス修飾子を示しました。

表2 基本的なアクセス修飾子
アクセス修飾子 説明
デフォルト クラス/アトリビュート/変数を定義したスクリプト内部だけからアクセス可能
package クラス/アトリビュート/変数を定義したパッケージ内からのみアクセス可能。Javaのパッケージプライベートに相当
protected クラス/アトリビュート/変数を定義したパッケージ内およびサブクラスからアクセス可能
public どこからでもアクセス可能

privateがないことに気がつかれた方も多いのではないでしょうか。JavaFX Scriptではprivateに相当するのが、デフォルトのアクセス修飾です。privateと異なるのが、クラス内ではなくスクリプト内ということです。つまりvarやdefの前に何も書かなければ、そのスクリプトファイルの中でしか使えないということです。

デフォルトのアクセス修飾を変更したため、Javaにはないpackageアクセス修飾子が導入されています。これがJavaのデフォルトアクセス修飾であるパッケージプライベートに相当します。protectedとpublicはJavaと同等です。

表2のアクセス修飾子はクラスの定義にも、アトリビュートや変数の定義にも使用することができます。これ以外にアトリビュートおよび変数にしか使用できないアクセス修飾子があります。

表3 アトリビュート/変数に使用できるアクセス修飾子
アクセス修飾子 説明
public-read 読み込みはどこからでも行えるが、書き込みはできない
public-init 初期化だけ可能で、読み込みはどこからでも行うことが可能。初期化後に書き込みをすることはできない

public-readは、Javaでゲッターメソッドだけあり、セッターメソッドがないような場合に相当します。たとえば、次のように使用します。

リスト14
class Foo {
    public var x: Integer;
    public-read var squareX = bind x * x;
}

var foo = Foo{
    x: 10
}
println("Foo.x {foo.squareX}");
foo.x = 20;
println("Foo.x {foo.squareX}");

squareXアトリビュートはxアトリビュートにバインドしているため、値を直接変更することはできません。そこで、public-readにし、読み込みだけはできるようにしています。このスクリプトを実行した結果を次に示します。

図9
Foo.x 100
Foo.x 400

public-initは、オブジェクト生成時に値を初期化することができるアクセス修飾子です。初期化してしまった後は値を変更することができません。defで定義してしまうと、定義と同時に初期化を行わなければなりません。したがって、値の代入をオブジェクト生成時に行いたい時には、defは使用できません。このような場合にpublic-initを使用します。

public-readとpublic-initは他のアクセス修飾子と合せて使用することも可能です。たとえば、次のように記述します。

リスト15
class Foo {
    public var x: Integer;
    // パッケージ内のみ読み込み可能
    public-read package var squareX = bind x * x;
}

このように記述することでパッケージ内だけ読み込みができることになります。

カスタムコンポーネントを作成する

最後にクラスの定義に関するサンプルを紹介しましょう。

今まで紹介してきたサンプルは、円にしろボタンにしろ、すべてJavaFX Scriptが提供している標準のAPIを使用してきました。しかし、標準のAPIだけでなく、自分なりのコンポーネントを使いたい場合もあります。そこで、コンポーネントを自作、つまりカスタムコンポーネントを作ってみることにします。

題材として非矩形のボタンを取りあげます。筆者はよくプレゼンテーションなどで図10にあるようなキャラクタを描くのですが、このキャラクタをボタンにしてしまいましょう。

今回作成したサンプルコードは以下よりダウンロードできます。

図10 題材とするキャラクタ
図10 題材とするキャラクタ

カスタムコンポーネントはjavafx.scene.CustomNodeクラスを継承させて作ります。CustomNodeクラスはアブストラクトクラスで、create関数がアブストラクトで宣言されています。カスタムコンポーネントの描画はこのcreate関数で行います。JavaFX Scriptでは描画する対象はすべてNodeクラスで表すことができます。そこで、カスタムコンポーネントでの描画をNodeオブジェクトで表し、そのNodeオブジェクトをcreate関数の戻り値とします。

では、さっそく作っていきましょう。今までのサンプルはスクリプトファイルは1つでしたが、今回はカスタムコンポーネントの定義は別ファイルとし、カスタムコンポーネントを使用するスクリプトをメインスクリプトとします。ここでは図8のキャラクタのボタンなので、カスタムコンポーネントのクラス名はSmilingButtonにしました。

まずプロジェクトを作成します。プロジェクト名はSmilingButtonにしました。プロジェクトを作成したら、SmilingButtonクラスを作成しましょう。

今まで、スクリプトファイル名は適当につけていても問題はありませんでした。しかし、複数のスクリプトファイルがある場合はそれでは困ります。メインスクリプトファイル以外のファイルは、ファイル名をそのファイルで定義するパブリックなクラス名とします。これはJavaと同じですね。

ここではSmilingButtonクラスを作成するので、ファイル名はSmilingButton.fxファイルとしました。パッケージは筆者のドメインを使用して、net.javainthebox.javafxにしました。

空のファイルにまずカスタムコンポーネントのひな形を作ってしまいましょう。これもコードパレットから作成することができます。CustomNodeはStageなどと同じApplicationsカテゴリにあります。CustomNodeをエディタ領域にドラッグ&ドロップすると以下のようなコードが生成されます。

すでにcreate関数が定義されています。CustomNodeクラスのcreate関数をオーバーライドするのでoverrideキーワードが関数定義に付加されています。

リスト16
package net.javainthebox.javafx;

import javafx.scene.CustomNode;
import javafx.scene.Group;
import javafx.scene.Node;

class MyCustomNode extends CustomNode {

    public override function create(): Node {
        return Group {
            content: []
        };
    }
}

赤字で示したjavafx.scene.Groupクラスは、複数のノードをまとめてグループ化するクラスです。Groupオブジェクト自体は描画されませんが、contentアトリビュートの要素をまとめ、グループとして移動や拡大などを行うことができます。また、ノードの重なり合い属性を設定することもできます。これにはblendModeアトリビュートを使用し、javafx.scene.effect.BlendModeクラスで定義されている定数を値として代入します。

まず、クラス名をSmilingButtonに変更します。そして、顔の輪郭となる円を加えましょう。コードパレットからCircleをドラッグして、contentアトリビュートの[ ]の中にドロップします。そして、以下のようにボタンの中心とサイズをアトリビュートとして定義し、それを使用して円の描画位置を決めます。また、顔の色は肌色(red:255, green:204, blue:153)としました。

リスト17
class SmilingButton extends CustomNode {
    public var centerX: Number;
    public var centerY: Number;
    public var size: Number;
 
    public override function create(): Node {
        return Group {
            content: [
                Circle {
                    centerX: centerX
                    centerY: centerY
                    radius: size
                    fill: Color.rgb(255, 204, 153)
                    stroke: Color.BLACK
                }
            ]
        };
    }
}

この段階で一度実行してみましょう。しかし、このスクリプトファイルはメインスクリプトファイルではないので、プロジェクトを実行はできません。そこで、現在編集しているスクリプトファイルを実行します。ファイルの実行はメニューバーの[実行⁠⁠→⁠ファイルを実行]を選択します。もしくはShiftキーとファンクションキーのF6を同時に押します。

しかし、何も表示されません。つまり、このスクリプトファイルではクラスの定義だけで、オブジェクトを生成していないからです。メインスクリプトファイルの場合、そのままオブジェクト生成のためのコードを記述できます。しかし、メインスクリプトファイル以外のスクリプトファイルではこれができません。メインスクリプトファイル以外のスクリプトファイルで、クラス定義以外のスクリプトを記述するにはrun関数を定義して、そこに記述します。

リスト18
class SmilingButton extends CustomNode {
    public var centerX: Number;
    public var centerY: Number;
    public var size: Number;

    public override function create(): Node {
        return Group {
            content: [
                Circle {
                    centerX: centerX
                    centerY: centerY
                    radius: size
                    fill: Color.rgb(255, 204, 153)
                    stroke: Color.BLACK
                }
            ]
        };
    }
}

// プレビューのための関数
function run() {
    Stage {
        scene: Scene {
            width: 150
            height: 150
            content: [
                SmilingButton {
                    centerX: 75
                    centerY: 75
                    size: 50
                }
            ]
        }
    }
}

赤字で示したように、run関数を定義し、描画に必要最低限の記述を行います。これで、⁠ファイルを実行]で描画が行われます図11⁠。また、同様にプレビューも可能になります。

図11 SmilingButtonのプレビュー
図11 SmilingButtonのプレビュー

次に耳を描きます。耳は楕円で表します。顔を表す円より先に描画することで、耳が半分だけ表示されます。楕円は円と同じようにコードパレットのBasic Animationにあります。Ellipseが楕円を表します。Ellipseをドラッグして、Circleオブジェクトの前にドロップします。耳は2つあるので、Ellipseを2回ドラッグ&ドロップします。

そして、サイズと位置を調節して耳に見えるようにします。

リスト19
class SmilingButton extends CustomNode {
    public var centerX: Number;
    public var centerY: Number;
    public var size: Number;

    public override function create(): Node {
        return Group {
            content: [
                Ellipse {
                    centerX: centerX - size,
                    centerY: centerY
                    radiusX: size * 0.2
                    radiusY: size * 0.3
                    fill: Color.rgb(255, 204, 153)
                    stroke: Color.BLACK
                },
                Ellipse {
                    centerX: centerX + size,
                    centerY: centerY
                    radiusX: size * 0.2
                    radiusY: size * 0.3
                    fill: Color.rgb(255, 204, 153)
                    stroke: Color.BLACK
                },
                Circle {
                    centerX: centerX
                    centerY: centerY
                    radius: size
                    fill: Color.rgb(255, 204, 153)
                    stroke: Color.BLACK
                }
            ]
        };
    }
}

この段階でプレビューしたのが図12です。ずいぶん顔らしくなってきました。

図12 耳を付け加える
図12 耳を付け加える

次に口を作ります。口は楕円と円を組み合わせて作りました。口の上の部分が円弧、下の部分が楕円弧になります。つまり、図13のようにまず楕円を描き、次に円との差分を取るという方法を使います。このように図形と図形の差分を取るには、javafx.scene.shape.ShapeSubtractクラスを使用できます。

図13 口を作る
図13 口を作る

ShapeSubtractクラスは基のシェイプをaアトリビュート、差分を取るシェイプをbアトリビュートで表します。口を描画するには次のように記述します。

リスト20
// 笑っている口は楕円から円を差し引いた形
var mouth = ShapeSubtract  {
    // 楕円
    a: Ellipse {
        centerX: centerX
        centerY: centerY + size * 0.55
        radiusX: size * 0.15
        radiusY: size * 0.3
    }
    // 円
    b: Circle {
        centerX: centerX,
        centerY: centerY + size * 0.05
        radius: size * 0.5

    }
    fill: Color.RED
    stroke: Color.BLACK
};

作成した変数mouthを輪郭を表すGroupオブジェクトのcontentアトリビュートの最後に加えます。この状態で実行したのが図14です。

図14 口を付け加える
図14 口を付け加える

最後に目を付け加えましょう。目は円弧なのでjavafx.scene.shape.Arcクラスをそのまま使用します。ここまでのSwingButtonクラスを以下に示します。実行すると、図15のように顔ができました。

リスト21
class SmilingButton extends CustomNode {
    public var centerX: Number;
    public var centerY: Number;
    public var size: Number;

    public override function create(): Node {
        // 笑っている口は楕円から円を差し引いた形
        var mouth = ShapeSubtract  {
                // 楕円
                a: Ellipse {
                centerX: centerX
                centerY: centerY + size * 0.55
                radiusX: size * 0.15
                radiusY: size * 0.3
            }
                // 円
                b: Circle {
                centerX: centerX,
                centerY: centerY + size * 0.05
                radius: size * 0.5

            }
            fill: Color.RED
            stroke: Color.BLACK
        };

        // 笑っている目は2つの円弧
        var eyes = Group {
            content: [
                    Arc {
                    centerX: centerX - size * 0.4
                    centerY: centerY + size * 0.1
                    radiusX: size * 0.2
                    radiusY: size * 0.2
                    startAngle: 0,
                    length: 180
                    type: ArcType.OPEN
                    fill: null
                    stroke: Color.BLACK
                },
                Arc {
                    centerX: centerX + size * 0.4
                    centerY: centerY + size * 0.1
                    radiusX: size * 0.2
                    radiusY: size * 0.2
                    startAngle: 0,
                    length: 180
                    type: ArcType.OPEN
                    fill: null
                    stroke: Color.BLACK
                }
            ]
        };

        return Group {
            content: [
                Ellipse {
                    centerX: centerX - size,
                    centerY: centerY
                    radiusX: size * 0.2
                    radiusY: size * 0.3
                    fill: Color.rgb(255, 204, 153)
                    stroke: Color.BLACK
                },
                Ellipse {
                    centerX: centerX + size,
                    centerY: centerY
                    radiusX: size * 0.2
                    radiusY: size * 0.3
                    fill: Color.rgb(255, 204, 153)
                    stroke: Color.BLACK
                },
                Circle {
                    centerX: centerX
                    centerY: centerY
                    radius: size
                    fill: Color.rgb(255, 204, 153)
                    stroke: Color.BLACK
                },
                mouth, eyes
            ]
        };
    }
}
図15 顔が完成
図15 顔が完成

同じように驚いた顔も作成します(コードは最後に示します⁠⁠。図16からわかるように、驚いた顔の目と顔は楕円を組み合わせて作っています。

図16 驚いた顔
図16 驚いた顔

そして、これを基にボタンがクリックされているか否かによって表情を変える部分を作成しましょう。まず、目と口を表すアトリビュートを作成し、また笑った表情と驚いた表情の目と口もそれぞれアトリビュートとして定義します。これらのアトリビュートはデフォルトのアクセス修飾子、つまりスクリプトの中だけでアクセスできるようにしました。

ここでは、前述したinitブロックで、これらのアトリビュートの初期化を行っています。なお、スクリプトが長くなってきたので、わかりやすくするためシェイプのアトリビュート初期化部分などを省略させていただきました。

リスト22
class SmilingButton extends CustomNode {
    public-init var centerX: Number;
    public-init var centerY: Number;
    public-init var size: Number;

    // 笑っている目と口
    var smilingEyes: Node;
    var smilingMouth: Node;

    // 驚いている目と口
    var surprisedEyes: Node;
    var surprisedMouth: Node;

    var eyes: Node;
    var mouth: Node;

    init {
        // 笑っている目は2つの円弧
        smilingEyes = Group {
            content: [ Arc { ... }, Arc { ... } ]
        };

        // 驚いている目は楕円を重ねる
        surprisedEyes = Group {
            content: [ Ellipse { ... }, Ellipse { ... }, Ellipse { ... }, Ellipse { ... }]
        };

        // 笑っている口は楕円から円を差し引いた形
        smilingMouth = ShapeSubtract  { ... }

        // 驚いている口は楕円
        surprisedMouth = Ellipse { ... }

        // デフォルト値をセット
        eyes = smilingEyes;
        mouth  = smilingMouth;
    }

    public override function create(): Node {
        // 輪郭は円と楕円を組み合わせた形
        var profile =  Group {
            content: [ Ellipse { ... }, Ellipse { ... }, Circle { ... }]
        };

        // mouth と eyes は変化するのでバインドする
        return Group {
            content: bind [
                profile,
                mouth,
                eyes
            ]
        };
    }
}

smilingEyesが笑っている目、smilingMouthが笑っている口です。同じように、surprisedEyesが驚いている目、surprisedMouthが驚いている口です。initブロックの最後で、eyesとmouthにデフォルトの笑っている目と口を代入します。この状態で実行すると、図15と同じになるはずです。

次に、クリックされた時の処理です。

前回Nodeクラスはイベント処理用のonMousePressedなどの関数型のアトリビュートを持つと説明しました。ここでも、onMousePressedアトリビュートに目と口の変更を行う関数を代入すればいいでしょうか? 答えはダメです。

onMousePressedアトリビュートはSmilingButtonクラスを使用する側で書き換えることが可能です。つまり、SmilingButtonクラスで設定したonMousePressedアトリビュートがユーザによって書き換えられてしまうかもしれないのです。

Swingのように複数のイベントリスナが登録できればいいのですが、現状のAPIではそれはできません。SwingコンポーネントのprocessEventメソッドのような関数があればいいのですが、それもありません。

では、何を使うかというと、Nodeクラスのpublic-readなアトリビュートです。Nodeクラスにはfocusedやhover、pressedなどのpublic-readなアトリビュートがあります。これらのアトリビュートは型がBooleanで、その状態にあるかどうかを表しています。たとえば、pressedはマウスボタンを押している状態でtrue、離すとfalseになります。

そこで思い出していただきたいのが、置換トリガです。アトリビュートの値が更新された時に、決められた処理を行うようにするのが置換トリガです。ここでは、pressedの値によって、目と口を変化させるという処理を置換トリガで行います。

リスト23
class SmilingButton extends CustomNode {
    public-init var centerX: Number;
    public-init var centerY: Number;
    public-init var size: Number;

    // 笑っている目と口
    var smilingEyes: Node;
    var smilingMouth: Node;

    // 驚いている目と口
    var surprisedEyes: Node;
    var surprisedMouth: Node;

    var eyes: Node;
    var mouth: Node;

    // クリックに応じて表情を変化させる
    override var pressed on replace {
        if (pressed) {
            // クリックされた時は驚いた顔
            eyes = surprisedEyes;
            mouth = surprisedMouth;
        } else {
            // 通常は笑った顔
            eyes = smilingEyes;
            mouth = smilingMouth;
        }
    };

    init {
        // 笑っている目は2つの円弧
        smilingEyes = Group { ... };

        // 驚いている目は楕円を重ねる
        surprisedEyes = Group { ... };

        // 笑っている口は楕円から円を差し引いた形
        smilingMouth = ShapeSubtract  { ... }

        // 驚いている口は楕円
        surprisedMouth = Ellipse { ... }

        // デフォルト値をセット
        eyes = smilingEyes;
        mouth  = smilingMouth;
    }

    public override function create(): Node {
        // 輪郭は円と楕円を組み合わせた形
        var profile =  Group { ... };

        // mouth と eyes は変化するのでバインドする
        return Group {
            content: bind [
                profile,
                mouth,
                eyes
            ]
        };
    }
}

pressedアトリビュートに置換トリガを付加する場合でも、スーパークラスの定義を変更することになるのでoverrideを付加させます。あとはon replaceのブロックの中でeyesとmouthの値を変更します。

eyesアトリビュートとmouthアトリビュートが変更された時に描画に反映されるように、create関数でのeyesアトリビュートとmouthアトリビュートを使用する場所にbindを記述しておきます。

これで、SmilingButtonクラスが完成しました。ファイルを実行、もしくはプレビューでマウスクリックに応じて表情が変化することを確認してください。

では、次にSmilingButtonクラスを使用するクラスを作りましょう。こちらは、main.fxというファイル名にしました。クリックに応じて、ボタンの下の文字列が変更するというスクリプトです。SmilingButtonクラスはパッケージを設定しましたが、main.fxはデフォルトパッケージにしました。

リスト24
var label = "";

Stage {
    title: "Smiling Button"
    scene: Scene {
        width: 200
        height: 200
        content: [
            SmilingButton {
                centerX: 100
                centerY: 80
                size: 50

                onMousePressed: function(event: MouseEvent) {
                    label = "ビックリ!!"
                }
                onMouseReleased: function(event: MouseEvent) {
                    label = ""
                }
            },
            Text {
                font: Font {
                    size: 24
                }
                x: 50
                y: 180
                content: bind label
            }
        ]
    }
}

このmian.fxスクリプトではSmilingButtonクラスのonMousePressedアトリビュートとonMouseReleasedアトリビュートを設定しています。これでも、正しく動作するはずです。さっそくプロジェクトを実行してみましょう。

ところが、コンパイルエラーが発生してしまいました。図17にコンパイルエラーを示しました。

図17 コンパイルエラー(抜粋)
C:\NetBeansProjects\SmilingButton\src\main.fx:7:
net.javainthebox.javafx.SmilingButton は net.javainthebox.javafx で
script only (default) アクセスされます。
import net.javainthebox.javafx.SmilingButton;

先ほどアクセス修飾子の説明をしたばかりなのに、ここではコードパレットが作成した雛形のままデフォルトのアクセス修飾で使っていました。このため、デフォルトパッケージからはSmilingButtonクラスにアクセスできないわけです。これを修正するにはリスト25のように、クラス定義にpublicを付加します。

リスト25
public class SmilingButton extends CustomNode {
        ......

さあ、気を取り直して、実行してみましょう。クリックしたら、顔の表情が変化するとともに、ウィンドウ下部に文字列が表示されます。また、非矩形のボタンであることを確かめるため、顔以外の部分をクリックしてみてください。ボタンの近くであっても、ボタンの外側であれば表情が変わらないはずです。

なお、図18はクリックするとアプレットで動作するページに移動します。ぜひ、試してみてください。

図18 main.fxの実行例

図18 main.fxの実行例図18 main.fxの実行例

さて、今回はクラスを作成する方法と、そのサンプルとして非矩形のボタンを作成しました。いかがだったでしょうか。

次回はアニメーションを解説する予定です。お楽しみに。

リスト26 完成したSmilingButton.fx
package net.javainthebox.javafx;

import javafx.scene.CustomNode;
import javafx.scene.Group;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.input.MouseEvent;
import javafx.scene.paint.Color;
import javafx.scene.shape.Arc;
import javafx.scene.shape.ArcType;
import javafx.scene.shape.Circle;
import javafx.scene.shape.Ellipse;
import javafx.scene.shape.ShapeSubtract;
import javafx.stage.Stage;

public class SmilingButton extends CustomNode {
    public-init var centerX: Number;
    public-init var centerY: Number;
    public-init var size: Number;

    // 笑っている目と口
    var smilingEyes: Node;
    var smilingMouth: Node;

    // 驚いている目と口
    var surprisedEyes: Node;
    var surprisedMouth: Node;

    var eyes: Node;
    var mouth: Node;

    // クリックに応じて表情を変化させる
    override var pressed on replace {
        if (pressed) {
            // クリックされた時は驚いた顔
            eyes = surprisedEyes;
            mouth = surprisedMouth;
        } else {
            // 通常は笑った顔
            eyes = smilingEyes;
            mouth = smilingMouth;
        }
    };

    init {
        // 笑っている目は2つの円弧
        smilingEyes = Group {
            content: [
                Arc {
                    centerX: centerX - size * 0.4
                    centerY: centerY + size * 0.1
                    radiusX: size * 0.2
                    radiusY: size * 0.2
                    startAngle: 0,
                    length: 180
                    type: ArcType.OPEN
                    fill: null
                    stroke: Color.BLACK
                },
                Arc {
                    centerX: centerX + size * 0.4
                    centerY: centerY + size * 0.1
                    radiusX: size * 0.2
                    radiusY: size * 0.2
                    startAngle: 0,
                    length: 180
                    type: ArcType.OPEN
                    fill: null
                    stroke: Color.BLACK
                }
            ]
        };

        // 驚いている目は楕円を重ねる
        surprisedEyes = Group {
            content: [
                Ellipse {
                    centerX: centerX - size * 0.4,
                    centerY: centerY
                    radiusX: size * 0.2
                    radiusY: size * 0.3
                    fill: Color.WHITE
                    stroke: Color.BLACK
                },
                Ellipse {
                    centerX: centerX + size * 0.4,
                    centerY: centerY
                    radiusX: size * 0.2
                    radiusY: size * 0.3
                    fill: Color.WHITE
                    stroke: Color.BLACK
                },
                Ellipse {
                    centerX: centerX - size * 0.4,
                    centerY: centerY
                    radiusX: size * 0.05
                    radiusY: size * 0.1
                    fill: Color.BLACK
                    stroke: Color.BLACK
                }
                Ellipse {
                    centerX: centerX + size * 0.4,
                    centerY: centerY
                    radiusX: size * 0.05
                    radiusY: size * 0.1
                    fill: Color.BLACK
                    stroke: Color.BLACK
                }
            ]
        };

        // 笑っている口は楕円から円を差し引いた形
        smilingMouth = ShapeSubtract  {
            // 楕円
            a: Ellipse {
                centerX: centerX
                centerY: centerY + size * 0.55
                radiusX: size * 0.15
                radiusY: size * 0.3
            }
            // 円
            b: Circle {
                centerX: centerX,
                centerY: centerY + size * 0.05
                radius: size * 0.5

            }
            fill: Color.RED
            stroke: Color.BLACK
        };

        // 驚いている口は楕円
        surprisedMouth = Ellipse {
            centerX: centerX,
            centerY: centerY + size * 0.6
            radiusX: size * 0.1
            radiusY: size * 0.15
            fill: Color.RED
            stroke: Color.BLACK
        }

        // デフォルト値をセット
        eyes = smilingEyes;
        mouth  = smilingMouth;
   }

    public override function create(): Node {
        // 輪郭は円と楕円を組み合わせた形
        var profile =  Group {
            content: [
                Ellipse {
                    centerX: centerX - size,
                    centerY: centerY
                    radiusX: size * 0.2
                    radiusY: size * 0.3
                    fill: Color.rgb(255, 204, 153)
                    stroke: Color.BLACK
                },
                Ellipse {
                    centerX: centerX + size,
                    centerY: centerY
                    radiusX: size * 0.2
                    radiusY: size * 0.3
                    fill: Color.rgb(255, 204, 153)
                    stroke: Color.BLACK
                },
                Circle {
                    centerX: centerX,
                    centerY: centerY
                    radius: size
                    fill: Color.rgb(255, 204, 153)
                    stroke: Color.BLACK
                }
            ]
        };

        // mouth と eyes は変化するのでバインドする
        return Group {
            content: bind [
                profile,
                mouth,
                eyes
            ]
        };
    }
}

function run() {
    Stage {
        scene: Scene {
            width: 150
            height: 150
            content: [
                SmilingButton {
                    centerX: 75
                    centerY: 75
                    size: 50
                }
            ]
        }
    }
}

おすすめ記事

記事・ニュース一覧