今まで紹介してきたサンプルはすべて、JavaFX Scriptの標準APIを使用して作成してきました。しかし、本格的なアプリケーションを作成するには、標準APIだけではいかんともしがたい部分があります。特にMVCでいうところのM、つまりモデルの部分は標準APIだけで表すには難しい場合が多くあります。
そこで、今回はクラスを自作することに挑戦してみましょう。
クラスの定義
とりあえず、クラスを1つ作ってみましょう。名字と名前を保持するクラスです。
クラスの定義はclassで行います。波括弧の中では、アトリビュートと関数の定義を行います。アトリビュートの定義は、変数と同じくvarで行います。Preview SDKまではアトリビュートの定義はattributeだったのですが、JavaFX 1.0で変更されました。
実をいうとこの変更に伴って、アトリビュートという言い方は使わずに、すべて変数として表すことになりました。しかし、単に変数と書いたときにインスタンス変数なのかローカル変数なのか、もしくはスクリプト変数なのか区別しにくいため、この連載では以前からなじみのあるアトリビュートを使い続けることにします。
関数も波括弧の中で定義します。インタープリタ版では関数の実体は波括弧の外で行っていましたが、これも変更されました。
上のNameクラスの場合、firstNameとlastNameという2つのアトリビュートを持ち、いずれも型はStringです。そして、fullnameという関数を持ち、戻り値の型はStringです。
では、このクラスを使ってみましょう。
実行結果を以下に示します。
アトリビュートにデフォルト値を持たせるには、アトリビュートの定義で値を代入します。アトリビュートにデフォルト値を設定しない場合は表1のようになります。
表1 アトリビュートのデフォルト値
型 |
デフォルト値 |
String |
"" (null) |
Integer |
0 |
Number |
0.0 |
Duration |
0ms |
Boolean |
false |
オブジェクト |
null |
関数 |
null |
シーケンス |
[ ] (null) |
Stringとシーケンスにnullと記載しているのは、空文字や要素のないシーケンスがnullと同等に扱われるためです。これも実際に試してみましょう。
kindアトリビュートだけデフォルト値を設定しました。titleアトリビュートやwidthアトリビュートはデフォルト値はなく、オブジェクト生成時も値を代入していません。このスクリプトを実行すると、次のようになりました。
この結果からもStringが空文字、Integerが0、そしてBooleanがfalseになることがわかります。また、オブジェクトはnullになります。
次に関数型のアトリビュートを定義してみましょう。関数型のアトリビュートを定義するには、引数と戻り値の型も定義しなくてはなりません。引数の型指定にはコロンはあってもなくてもかまいません。また、関数型のアトリビュートで定義したメソッドをコールするのは、普通の関数をコールするのとまったく変わりません。
resetアトリビュートは引数がなく、戻り値の型はVoidです。printアトリビュートは引数の型がString、戻り値の型がBooleanになります。実行結果を次に示します。
varだけでなく、defで変数を定義することも可能です。第3回でdefを使うと、値を一度しか代入できないことを説明しました。それはクラス定義でも同じです。
継承
次に継承させたクラスを作ってみましょう。サブクラスを作るにはJavaと同じくextendsを使用します。
BarクラスがFooクラスのサブクラスになります。実行すると、次のようになりました。
スーパークラスの関数をオーバライドする場合、functionの前にoverrideと書きます。これはJavaの@Overrideアノテーションと同じですが、overrideを省略することはできません。また、サブクラスがスーパークラスの関数をコールするには、Javaと同じくsuperを使用します。
BarクラスがFooクラスのfoo関数をオーバーライドし、内部でFooクラスのfoo関数をコールしています。では、実行してみましょう。
また、スーパークラスのアトリビュートのデフォルト値を書き換える場合もoverrideを使用します。
fooアトリビュートのデフォルト値を"FOO"から"BAR"に変更しています。実行すると以下のようになります。
JavaFX Scriptにはインターフェースは存在しませんが、アブストラクトクラスは作成できます。アブストラクトクラスはクラス定義の前にabstractを記述し、アブストラクト関数にもabstractを書きます。もちろん、アブストラクトクラスはオブジェクトを作成することができないので、サブクラスを作成してオブジェクトを生成します。
サブクラスでスーパークラスのアブストラクトメソッドを定義する場合も、overrideは必要です。
また、Javaとは異なり、JavaFX Scriptでは多重継承が可能です。多重継承を行うにはextendsの後にカンマ区切りでクラス名を列挙します。
JavaFX ScriptではJavaのクラスのサブクラスを作成することも可能なのですが、すべてのクラスのサブクラスを作成できるわけではありません。JavaFX Scriptで作成できるJavaのサブクラスは、デフォルトコンストラクタ、つまり引数のないコンストラクタを持つクラスに限られます。
たとえば、java.io.FileReaderクラスはデフォルトコンストラクタがありません。このためFileReaderクラスのサブクラスを作成しようとしても、コンパイルエラーが発生します(図7)。
また、Javaのインターフェースを継承することも可能です。JavaFX Scriptにはインターフェースはないので、Javaのインターフェースは普通のクラスとして扱います。たとえば、以下のようなJavaのインターフェースがあったとします。
このHelloインターフェースを実装する場合でも、JavaFX Scriptではextendsを使用します。
インターフェースのメソッドをオーバライドする場合でも、overrideを記述する必要があります。
初期化ブロック
JavaFX Scriptにはコンストラクタはないことは、みなさんご存じの通りです。オブジェクト生成時にアトリビュートに値を代入するだけであればいいのですが、オブジェクト生成時に何らかの処理を行いたい時があります。たとえば、通信を行うクラスのオブジェクトを生成すると、通信先に対してコネクトする処理などです。
このような場合、2つの方法を使用することができます。
initブロックはオブジェクト生成の最終段階で処理されるブロックです。initブロックの処理後にオブジェクトの生成が完了します。一方のpostinitブロックはオブジェクト生成が終了した後に処理されるブロックです。この2種類のブロックはどのように使い分ければいいのでしょう。その答えは、次のスクリプトを実行してみればわかります。
FooクラスとBarクラスにinitブロックとpostinitブロックを定義しました。これを実行するとどうなるでしょう。BarクラスがFooクラスのサブクラスということを考えなくてはいけません。
この結果から、スーパークラスの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でゲッターメソッドだけあり、セッターメソッドがないような場合に相当します。たとえば、次のように使用します。
squareXアトリビュートはxアトリビュートにバインドしているため、値を直接変更することはできません。そこで、public-readにし、読み込みだけはできるようにしています。このスクリプトを実行した結果を次に示します。
public-initは、オブジェクト生成時に値を初期化することができるアクセス修飾子です。初期化してしまった後は値を変更することができません。defで定義してしまうと、定義と同時に初期化を行わなければなりません。したがって、値の代入をオブジェクト生成時に行いたい時には、defは使用できません。このような場合にpublic-initを使用します。
public-readとpublic-initは他のアクセス修飾子と合せて使用することも可能です。たとえば、次のように記述します。
このように記述することでパッケージ内だけ読み込みができることになります。
カスタムコンポーネントを作成する
最後にクラスの定義に関するサンプルを紹介しましょう。
今まで紹介してきたサンプルは、、円にしろボタンにしろ、すべてJavaFX Scriptが提供している標準のAPIを使用してきました。しかし、標準のAPIだけでなく、自分なりのコンポーネントを使いたい場合もあります。そこで、コンポーネントを自作、つまりカスタムコンポーネントを作ってみることにします。
題材として非矩形のボタンを取りあげます。筆者はよくプレゼンテーションなどで図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キーワードが関数定義に付加されています。
赤字で示したjavafx.scene.Groupクラスは、複数のノードをまとめてグループ化するクラスです。Groupオブジェクト自体は描画されませんが、contentアトリビュートの要素をまとめ、グループとして移動や拡大などを行うことができます。また、ノードの重なり合い属性を設定することもできます。これにはblendModeアトリビュートを使用し、javafx.scene.effect.BlendModeクラスで定義されている定数を値として代入します。
まず、クラス名をSmilingButtonに変更します。そして、顔の輪郭となる円を加えましょう。コードパレットからCircleをドラッグして、contentアトリビュートの[ ]の中にドロップします。そして、以下のようにボタンの中心とサイズをアトリビュートとして定義し、それを使用して円の描画位置を決めます。また、顔の色は肌色(red:255, green:204, blue:153)としました。
この段階で一度実行してみましょう。しかし、このスクリプトファイルはメインスクリプトファイルではないので、プロジェクトを実行はできません。そこで、現在編集しているスクリプトファイルを実行します。ファイルの実行はメニューバーの[実行]→[ファイルを実行]を選択します。もしくはShiftキーとファンクションキーのF6を同時に押します。
しかし、何も表示されません。つまり、このスクリプトファイルではクラスの定義だけで、オブジェクトを生成していないからです。メインスクリプトファイルの場合、そのままオブジェクト生成のためのコードを記述できます。しかし、メインスクリプトファイル以外のスクリプトファイルではこれができません。メインスクリプトファイル以外のスクリプトファイルで、クラス定義以外のスクリプトを記述するにはrun関数を定義して、そこに記述します。
赤字で示したように、run関数を定義し、描画に必要最低限の記述を行います。これで、[ファイルを実行]で描画が行われます(図11)。また、同様にプレビューも可能になります。
次に耳を描きます。耳は楕円で表します。顔を表す円より先に描画することで、耳が半分だけ表示されます。楕円は円と同じようにコードパレットのBasic Animationにあります。Ellipseが楕円を表します。Ellipseをドラッグして、Circleオブジェクトの前にドロップします。耳は2つあるので、Ellipseを2回ドラッグ&ドロップします。
そして、サイズと位置を調節して耳に見えるようにします。
この段階でプレビューしたのが図12です。ずいぶん顔らしくなってきました。
次に口を作ります。口は楕円と円を組み合わせて作りました。口の上の部分が円弧、下の部分が楕円弧になります。つまり、図13のようにまず楕円を描き、次に円との差分を取るという方法を使います。このように図形と図形の差分を取るには、javafx.scene.shape.ShapeSubtractクラスを使用できます。
ShapeSubtractクラスは基のシェイプをaアトリビュート、差分を取るシェイプをbアトリビュートで表します。口を描画するには次のように記述します。
作成した変数mouthを輪郭を表すGroupオブジェクトのcontentアトリビュートの最後に加えます。この状態で実行したのが図14です。
最後に目を付け加えましょう。目は円弧なのでjavafx.scene.shape.Arcクラスをそのまま使用します。ここまでのSwingButtonクラスを以下に示します。実行すると、図15のように顔ができました。
同じように驚いた顔も作成します(コードは最後に示します)。図16からわかるように、驚いた顔の目と顔は楕円を組み合わせて作っています。
そして、これを基にボタンがクリックされているか否かによって表情を変える部分を作成しましょう。まず、目と口を表すアトリビュートを作成し、また笑った表情と驚いた表情の目と口もそれぞれアトリビュートとして定義します。これらのアトリビュートはデフォルトのアクセス修飾子、つまりスクリプトの中だけでアクセスできるようにしました。
ここでは、前述したinitブロックで、これらのアトリビュートの初期化を行っています。なお、スクリプトが長くなってきたので、わかりやすくするためシェイプのアトリビュート初期化部分などを省略させていただきました。
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の値によって、目と口を変化させるという処理を置換トリガで行います。
pressedアトリビュートに置換トリガを付加する場合でも、スーパークラスの定義を変更することになるのでoverrideを付加させます。あとはon replaceのブロックの中でeyesとmouthの値を変更します。
eyesアトリビュートとmouthアトリビュートが変更された時に描画に反映されるように、create関数でのeyesアトリビュートとmouthアトリビュートを使用する場所にbindを記述しておきます。
これで、SmilingButtonクラスが完成しました。ファイルを実行、もしくはプレビューでマウスクリックに応じて表情が変化することを確認してください。
では、次にSmilingButtonクラスを使用するクラスを作りましょう。こちらは、main.fxというファイル名にしました。クリックに応じて、ボタンの下の文字列が変更するというスクリプトです。SmilingButtonクラスはパッケージを設定しましたが、main.fxはデフォルトパッケージにしました。
このmian.fxスクリプトではSmilingButtonクラスのonMousePressedアトリビュートとonMouseReleasedアトリビュートを設定しています。これでも、正しく動作するはずです。さっそくプロジェクトを実行してみましょう。
ところが、コンパイルエラーが発生してしまいました。図17にコンパイルエラーを示しました。
先ほどアクセス修飾子の説明をしたばかりなのに、ここではコードパレットが作成した雛形のままデフォルトのアクセス修飾で使っていました。このため、デフォルトパッケージからはSmilingButtonクラスにアクセスできないわけです。これを修正するにはリスト25のように、クラス定義にpublicを付加します。
さあ、気を取り直して、実行してみましょう。クリックしたら、顔の表情が変化するとともに、ウィンドウ下部に文字列が表示されます。また、非矩形のボタンであることを確かめるため、顔以外の部分をクリックしてみてください。ボタンの近くであっても、ボタンの外側であれば表情が変わらないはずです。
なお、図18はクリックするとアプレットで動作するページに移動します。ぜひ、試してみてください。
さて、今回はクラスを作成する方法と、そのサンプルとして非矩形のボタンを作成しました。いかがだったでしょうか。
次回はアニメーションを解説する予定です。お楽しみに。