JavaFX Custom Control following MVC

概要 Abstract

コントロールはユーザが操作できるノードです. 標準で用意されているコントロールも沢山ありますが, 自分で実装したい時もあります. そんなカスタムコントロールMVC 設計パターンに沿って実装してみました.

この記事では MVC 設計パターンについて沿ったカスタムコントロールの実装の仕方を曲線入力コントロールの例を用いて説明します.

Control is a node which user can manipulate. There are many standard controls. However, sometimes, you want to implement a control to customize its behavior. I implemented the custom control, following MVC design pattern.

With an example of CurveInputControl, this article explains how to implement custom controls following MVC.

MVC

JavaFX のドキュメントによると, Control は MVC 設計パターンに従っています. MVC 設計パターンは GUI アプリケーションの設計パターンの1つです. MVC は Model, View, Controller の頭文字です. Model は プログラムの機能を担い, 状態と状態を操作するメソッドを持ちます. View は UI を担い, ユーザの操作を認識し, また, 見た目を定義します. Controller は View からユーザの入力を受け取って Model のメソッドを呼び出します.

ユーザが UI 上で操作すると View のイベントハンドラが呼び出されます. これらのイベントハンドラの処理は Controller で定義されます. Controller で定義されるこれらの処理は Model のメソッドを呼び出します. Model は自分のメソッドが呼び出されると自分の機能を果たしてその結果を View に通知します. View は Model の通知を受けて見た目を変更します.

JavaFX では Control クラスが Model, Skin インターフェイスが View となっており, Controller に当たるものは公開されている API にはありません.

According to the document of JavaFX, Controls follow MVC (Model-View-Controller) design pattern. MVC design pattern is a design pattern of GUI application. Model manages functions of the program. It has its state and methods to change the state. View manages UI. View recognizes user manipulations, and defines how to render. Controller accepts inputs of user from the View, and calls methods of the Model.

When user manipulates on the UI, the event handlers of the View is called. These event handlers are defined by the Controller. They calls methods of the Model. Then, the Model performs its function, and notifies the result to the View. The View accepts the notification by the Model, and changes its appearance.

In JavaFX, Control class is the Model, and Skin interface is the View. There is no Controller in public API.

実装 Implementation

ここで用いる曲線入力コントロールの例はマウスドラッグで曲線を入力するコントロールです. 曲線は点列データで表されます. 1本の曲線が入力されると CurveInputEvent が発行されます. 曲線入力コントロールの onCurveInputDone プロパティのイベントハンドラがそのイベントを受け取ります.

An example of CurveInputView control is a control to input curve by dragging. Curve is presented with list of points. When a curve is input, CurveInputEvent is fired. A event handler onCurveInputDone property handles the event.

Model

コントロールは Model です. コントロールは Control クラスを継承して作成します. Control クラスの createDefaultSkin メソッドをオーバーライドして, コントロールに適用するスキンを生成するようにします.

Model であるコントロールの状態が変化した時はスキンに対して見た目の変更が必要である事を通知しなければいけません. まず, Control クラスの getSkin メソッドでスキンを取得します. Control クラスの getSkin メソッドを呼び出すとコントロールに適用されているスキンを取得できます. 次に, スキンの UI 変更メソッドを呼び出して描画に必要なデータを渡します.

Control is the Model. Inheriting the Control class, you write custom control. Override createDefultSkin method so that creates skin for custom control.

When state of control as Model is changed, control must notify skin that it must change its appearance. First, get the skin of the control calling the getSkin method of the Control class. And, call the rendering method of the skin with data to use on redering.

onCurveInputDone

プロパティを定義する時はプロパティ本体のフィールド, Setter, Getter, そしてプロパティを返すメソッドを作成します. SimpleObjectProperty コンストラクタの第1引数はそのプロパティを保持するオブジェクト, 第2引数はプロパティ名文字列, そして, 第3引数はプロパティが保持する値の初期値です.

SimpleObjectProperty クラスの invalidated メソッドはいままでプロパティが保持していた値が無効になった時に呼び出されます. 例えば, 別の値が設定された時や, 別のプロパティをバインドした時です. onCurveInputDone プロパティでは今まで保持していたイベントハンドラが無効になった時に, 新しく保持するイベントハンドラをコントロールイベントハンドラに設定し直しています.

When you define a property, you must write field of the property, setter, getter, and Property getter. The first argument of the constructor of SimpleObjectProperty class is the object which has the property, the second argument is a property name string, the third argument is initial value for the value the property has.

invalidated method of SimpleObjectProperty is called when the value the property has is invalidated. For example,the method is called when different value is set as the value the property has, or when the property binds different property. When the event handler which onCurveInputDone property had is invalideted, onCurveInputDone property resets the new event handler as an event handler of the custom control.

Source code

Control クラスを継承した CurveInput クラスで Model の実装を行ったものが次の CurveInput です.

The following CurveInput is a class which is the Model and extends Control class.

View

View は Skin インターフェイスを実現して作成します. Skin インターフェイスには getNode メソッド, getSkinnable メソッド, そして dispose メソッドがあり, 全てをオーバーライドしなければいけません. コントロールの見た目を Skin インターフェイスで実現する時は, シーングラフを組み立てて SKin で保持します.

Implementing Skin interface, you write View. You must override getNode method, getSkinnable method, and dispose method of Skin interface. When implementing skin, you assemble scene graph that defines the appearance of the custom control.

getNode

スキンではシーングラフを組み立ててコントロールの見た目を定義し, そのルートノードを保持します. そして, Skin インターフェイスの getNode メソッドをオーバーライドして, スキンが持つシーングラフのルートノードを返すようにします.

コントロールJavaFX アプリケーションのシーングラフに追加すると, ウィンドウを表示する時に, コントロールのcreateDefaultSkin メソッドが呼び出されます. このメソッドによって生成されたスキンは, コントロールに割り当てられます. この割り当て処理を行う時に, そのスキンの getNode メソッドが呼び出されます. そして, このメソッドが返すノードがコントロールの子ノードに追加されます.

Skin has the root node of the scene graph defines appearance of the custom control. And, you must override getNode method so that returns the root node.

If the control object was added to the scene graph of the JavaFX application, when the window is shown, createDefaultSkin method of the control is called. Next, getNode method of the skin is called and the node returned by the method is added to children of the control. These are how to apply the skin to the control.

getSkinnable

Skin インターフェイスの getSkinnable メソッドをオーバーライドして, スキンの適用対象のコントロールを返すようにします. 従って, スキンは適用対象のコントロールを保持します.

You must override getSkinnable method so that it returns the control which is applied this skin. This is the reason why skin has the control.

dispose

Skin インターフェイスの dispose メソッドは Skin が不要になった時に呼び出されます. もし必要なら, C++ のデストラクタの様にリソースの解放も行えます. このメソッドを呼び出した後は getNode メソッドと getSkinnable メソッドは null を返さなければいけません. このメソッドをオーバーライドして, スキンが保持するコントロールとシーングラフのルートを null にしてリソースを解放するようにします.

dispose method of Skin interface is called when skin is disused. This method looks like destructors of C++. With this method, you can release resources in the skin. After this method is called, getNode method and getSkinnable must return null. Overriding this method, you set the control the skin has and root node to null, and release resources.

レンダリングメソッド rendering method

Model の状態が変化した時, Model から見た目の変更が必要であると通知されます. この時, Model からスキンの UI 変更メソッドが呼び出され, 描画に必要なデータが渡されます.

When the state of the Model is changed, the Model notifies that it is required to change the UI. At this time, the Model calls rendering method of the skin and gives data for rendering.

Source code

Skin インターフェイスを実現した CurveInputSkin クラスで View を実装したものが次の CurveInputSkin です.

The following CurveInputSkin is a class which is the View and implements Skin interface.

Controller

コントローラは公開されている API にはありません. コントローラは fxml のコントローラと同様に作成できます.

コントローラ内にイベントハンドラを作成します. スキンの中で, スキンのシーングラフを構成するノードにこのイベントハンドラを登録します. コントローラのイベントハンドラは View からの入力に応じて, Model のメソッドを呼び出して, Model を操作します. 従って, コントローラはコントロールを保持します.

There is no controller in public API. The controller is similar to controllers of fxml.

In the controller class, event handlers are defined. These event handlers are registered to corresponding nodes in the scene graph of the view in skin class. Calling the methods of the Model depending on input from the View, event handlers manipulates the Model. This is Why controller has the Model.

Source code

イベントハンドラを定義したコントローラが次の CurveInputController です.

The following CurveInputController is a class which defines event handlers.

Event

Source code

曲線入力イベント発生時の点列データを保持するイベントクラスを実装したものが次の CurveInputEvent です.

The following CurveInputEvent is a class which has points data at the time the CurveInputEvent was fired.

クラス図 Class Diagram

今回作成する主なクラスの継承関係を次のクラス図に示します.

The following class diagram shows inheritance relations between primary classes.

f:id:Jumpaku:20161127201112p:plain:h400

流れ Flow

コントロールが表示されるまでの流れを次のシーケンス図に示します.

The following sequence diagram shows how the control is displayed.

f:id:Jumpaku:20161127152502p:plain:h400

イベントが処理されるまでの流れを次のシーケンス図に示します.

The following sequence diagram shows how the event is handled.

f:id:Jumpaku:20161127152517p:plain

動作確認 Test

次の Main クラスで, 作成した CurveInput クラスを使ってみます.

I tested whether the custom control works with the following source code.

このウィンドウが表示されました.

The following window was displayed.

f:id:Jumpaku:20161121150352p:plain:h400

また, 次の出力がされました.

And, the following is the output.

Handled CurveInputEvent.
points.size() == 304

きちんと動きました.

It looks that the program works well.

反省と展望

最初は Skin の getNode のノードがどんなノードを返せば良いのか, Control のどのメソッドをどうオーバーライドしたら良いのか, 分かりませんでした. プログラムが見えない所でどう動いているのかを知るのは難しかったです. 私が解釈している MVC に沿って, きちんと動くコントロールを作成できたと思います.

次は fxml で作ります.

I couldn't understand what node should I return at getNode of Skin, and what method should of Control class I override. It was difficult to know how objects work. However, following the MVC that I understood, I could make the custom control which works well.

I am going to make a custom control with fxml.

JavaFX Simple Sketch

ソースコード Source Code

ドラッグで入力された点を直線で繋いで曲線を描くプログラムです. ユーザはマウスのボタンを押し, ドラッグし, そして離すことで点を入力します.

Using polyline of dragged points, this program draws curves. To input points, you press a button of mouse, drag the mouse, and release the button of the mouse.

f:id:Jumpaku:20161006223332p:plain

説明 Explanation

main で launch を呼び出されると InputLine オブジェクトが生成され, start が 実行されます.

24行目から28行目まででは次のように canvasイベントハンドラが設定されます.

  • マウスが押された時 : previous に始点を設定する.
  • マウスがドラッグされた時と離された時 : drawLine を呼び出す.

31, 32行目では canvas をウィンドウに貼り付けて, それを表示しています.

35行目の drawLine は今回のマウスイベントの位置を取得し, 前回の点から strokeLine を利用して線分を伸ばします. その後, 今回の点を保存します.

When launch in main is called, start of new InputLines object is called.

From line 24 to line 28, event handlers of canvas are set as follows;

  • When mouse is pressed, previous is initialized by beginning point.
  • When mouse is dragged or released, drawLine is called.

Line 31 sets canvas to the window and line 32 shows that.

Using strokeLine, drawLine at line 35 draws line from the point of current mouse event to previous point. After that, save current point.

なんとなクエスト IndistinQuest

f:id:Jumpaku:20161002133748p:plain f:id:Jumpaku:20161002133810p:plain f:id:Jumpaku:20161002133821p:plain

"直感とセンスで弱点を見極めろ!!!"

"Find enemy's week point with your feeling!!"

ダウンロード DOWNLOAD
Releases · IndistinQuest/Game · GitHub

紹介 Description

我々は "なんとなクエスト" というゲームを作りました. このゲームはコマンド選択型の戦闘をするゲームです. エネミィの画像と説明文からなんとなく弱点を見極めて, コマンドを選択してください. 倒したエネミィの数と残り時間からスコアが算出されます.

We released a game "なんとなクエスト". This is a command selection battle game. Watch enemy's illustration and reed description. Find enemy's week point with your feeling and select attack command. After battles, your score is calculated using the number of defeated enemies and time.

ゲーム情報

  • タイトル : なんとなクエス
  • 作者 : なんとなクエスト プロジェクト (雄洋, HataG, Namba, Jumpaku, うっひょい)
  • ジャンル : コマンド選択型戦闘
  • プレイ時間 : 10分程度
  • プラットフォーム : Windows
  • リリース : 2016年 10月 01日
  • 言語 : 日本語
  • 利用ライブラリ : Siv3D

Information

  • Title : なんとなクエスト (IndistinQuest)
  • Developers : なんとなクエスト プロジェクト (雄洋, HataG, Namba, Jumpaku, うっひょい)
  • Genle : command selection type battle
  • Playtime : about 10 minutes
  • Platform : Windows
  • Released on : October 1, 2016
  • language : Japanese
  • Library : Siv3D

遊び方 How to play

ゲームを始めるには"なんとなクエスト.exe"を実行して"START"ボタンを押してください. 詳しいルールはゲーム内の"RULE"を見てください.

To start the game, execute "なんとなクエスト.exe", and press "START" button. To see detail rule, press "RULE" button on title scene.

リンク Link

スペクトルバスター Spectrum Buster

f:id:Jumpaku:20160918145312p:plain f:id:Jumpaku:20160918145321p:plain

"恥ずかしさをかなぐり捨てろ!!"

"Don't be shy!!"

経緯 Proccess

サークルの合宿でアイデアソン, ハッカソンをしました. 我々はSiv3DのFFTの機能を利用して簡単なゲームを作成しました.

We held an ideathon and a hackathon. With FFT of Siv3D, we developed a simple game.

紹介 Description

音声をフーリエ変換して, 周波数スペクトルを敵に当てるゲームです. プレイヤは220Hz~880Hzの声を出してください. 左の敵には低音, 右の敵には高音を出しましょう. また, 上の敵には大きな声を出しましょう. 制限時間は30[s]です.

恥ずかしさをかなぐり捨ててできるだけ沢山の敵を倒してください.

Shoot enemies with frequency spectrum of your voice. Input your 220Hz~880Hz voice. To buster left enemy, input low-pitch sound. To buster right enemy, input high-pitch sound. To buster above enemy, input megavolume sound. Time limit is 30[s].

Don't be shy and buster many enemies!!!

ゲーム情報

  • タイトル : スペクトルバスター
  • 作者 : MPC (室蘭プログラミングクラブ) 合宿DE合宿 2016 チーム3
  • ジャンル : FFTシューティング
  • プレイ時間 : 30s程度
  • プラットフォーム : Windows
  • リリース : 2016年 9月 18日
  • 言語 : 日本語
  • 利用ライブラリ : Siv3D

Information

  • Title : Spectrum Buster
  • Developer : MPC (Muroran Programming Club) "合宿DE合宿 2016 Team 3"
  • Genle: FFTSTG
  • Playtime : 30s
  • Platform : Windows
  • Released on : 2016年 9月 18日
  • language : Japanese
  • Library : Siv3D

リンク Links

JavaFX 3 Canvas

Canvasクラス Canvas class

JavaFXCanvasクラスはProcessingやJavaScriptcanvasと同じ様に使う事ができます.

Canvas class of JavaFX can be used the same as Processing or canvas of JavaScript.

GraphicContext

まずCanvasクラスが持っているGraphicContextを取得します.

First, get GraphicContext of Canvas class.

GraphicsContext ctx = canvas.getGraphicsContext2D();

fill, stroke

図形の輪郭を描く時はGraphicContextクラスの"stroke"で始まるメソッドを使用します. また図形の塗り潰す時はGraphicContextクラスの"fill"で始まるメソッドを使用します.

To draw outline of shape, use GraphicContext class's methods starts with "stroke". To paint all over a shape, use GraphicContext class's methods starts with "fill".

ctx.strokeOval(p.getX() - 5, p.getY() - 5, 10, 10)

path

パスを描く時はまずGraphicContextクラスのbeginPathメソッドでパスを開始します. そしてmoveTo, lineTo, quadraticCurveTo, bezierCurveTo, arc, arcTo, appendSVGPath, rect, closePathなどのメソッドでパスを作ります. パスの内側を塗り潰す時はGraphicContextクラスのfillメソッド, パスの輪郭を描く時はstrokeメソッドを使用します.

To generate path, firstly use beginPath method of GraphicContext class. After that, construct path with moveTo, lineTo, quadraticCurveTo, bezierCurveTo, arc, arcTo, appendSVGPath, rect or closePath methods. Finally, if you want to paint all over the region inside the path, use fill method of GraphicContext class. If you want to draw a outline of the path, use stroke method.

ctx.beginPath();
ctx.moveTo(points.get(0).getX(), points.get(0).getY());
points.stream().skip(1).forEach(p -> ctx.lineTo(p.getX(), p.getY()));
ctx.stroke();

clear

キャンバスをクリアする時はGraphicContextクラスのclearRectメソッドを使用します.

To clear the canvas, use clearRect method of GraphicContext.

ctx.clearRect(0, 0, canvas.getWidth(), canvas.getHeight());

サンプル Sample

クリックされた点を直線で繋ぐプログラムです.

The following program draws lines between clicked points.

f:id:Jumpaku:20160729145906p:plain

View.fxml

<?xml version="1.0" encoding="UTF-8"?>

<?import javafx.scene.canvas.*?>
<?import java.lang.*?>
<?import java.util.*?>
<?import javafx.scene.*?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>

<AnchorPane id="AnchorPane" prefHeight="200" prefWidth="320" xmlns:fx="http://javafx.com/fxml/1" xmlns="http://javafx.com/javafx/8" fx:controller="canvassample.ViewController">
   <children>
      <Canvas fx:id="canvas" height="200.0" onMouseClicked="#handleClick" width="320.0" />
   </children>
</AnchorPane>

Main.java

package canvassample;

import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.stage.Stage;

public class Main extends Application {
    @Override
    public void start(Stage stage) throws Exception {
        Parent root = FXMLLoader.load(getClass().getResource("View.fxml"));
        stage.setScene(new Scene(root));
        stage.show();
    }

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

ViewController.java

package canvassample;

import java.net.URL;
import java.util.LinkedList;
import java.util.List;
import java.util.ResourceBundle;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.geometry.Point2D;
import javafx.scene.canvas.Canvas;
import javafx.scene.canvas.GraphicsContext;
import javafx.scene.input.MouseEvent;

public class ViewController implements Initializable {
    private final List<Point2D> points = new LinkedList<>();
    
    @FXML
    private Canvas canvas;
    
    @FXML
    synchronized private void handleClick(MouseEvent e) {
        points.add(new Point2D(e.getX(), e.getY()));
        GraphicsContext ctx = canvas.getGraphicsContext2D();
        ctx.clearRect(0, 0, canvas.getWidth(), canvas.getHeight());
        
        points.forEach(p -> ctx.strokeOval(p.getX() - 5, p.getY() - 5, 10, 10));
        
        ctx.beginPath();
        ctx.moveTo(points.get(0).getX(), points.get(0).getY());
        points.stream().skip(1).forEach(p -> ctx.lineTo(p.getX(), p.getY()));
        ctx.stroke();
    }
    
    @Override
    public void initialize(URL url, ResourceBundle rb) {
    }    
    
}