Skinnable Control in JavaFX 1.2
今回は JavaFX 1.2 で微妙に変更された Control をテーマにしてみました。
JavaFX 1.2 では 密かに javafx.scene.control.Behavior というクラスが新しく追加されています。
しかし、一体何のため?
考えるより、まずはサンプルを作ってみました。
今回、作成したのは お馴染みの 『進捗状況とは関係なく、ひたすらくるくる回るだけの ProgressIndicator』 です。
標準 API にも同じものが用意されていますが...
- 見た目があまり好みでない
- 使い勝手があまり良くない
微妙に前回までの非同期処理から抜け出せていないのは気のせい...?
で、完成したものはこんな感じ...
← 実際に動くもの...
今回、試してみて感じたこと...
かなりコードがスッキリ書けるようになったというのが第一印象です。
新たに Behavior が追加されたことで、曖昧だった Control, Skin の役割が
- Control は 外部とのインターフェース。
- Skin は 出力する Node の生成。
- Behavior は Skin の操作。
Behavior がなかったころは、Skin の操作を Control に実装するべきか、Skin に実装するべきか朝まで悩んだものです。
これでまた一つ悩みごとが減り、ぐっすり眠れそうです...
また、役割が明確化され、実装が分離されれば、再利用性、カスタマイズ性の向上が期待できます。
見た目だけ変えたいのであれば、Skin だけ変更すればよいし、振る舞いだけ変えたければ、Behavior だけ変更すればよい...
と言うものの、残念ながら、JavaFX 1.2 では 各 Control の Skin, Behavior の実装は 標準 API として公開されていません。
どうやら、com.sun.javafx.scene.control.caspian パッケージに 各 Control の Skin が実装されているみたい。
ちなみに、標準 Skin の実装のことを Caspian と呼ぶらしい... (JavaFX - Skinnable Controls 参照)
なぜ、Caspian を標準 API として公開しないのだろうか? そうすればもっと楽に見た目を変更できるだろうに...
もう1つ残念なのは、Skin の behavior 属性が public-read protected であることです。
これでは、Skin の外部から Behavior を変更することができません。
なぜ、public-read protected なのだろうか? せめて public-init か public-read package にしてほしかったなぁ...
Control の skin 属性は public なのに、何か特別な理由でもあるのだろうか?
[Control] CircleProgressIndicator.fx
import javafx.scene.control.Control; import javafx.scene.control.Skin; import javafx.scene.paint.Color; public class CircleProgressIndicator extends Control { public-init var radius = 10.0; public-init var interval = 1.0s; public-init var delay = 0.5s; public var foreground = Color.web("#DDDDDD"); public var background = Color.web("#000000"); override var width = bind 2 * radius; override var height = bind 2 * radius; init { visible = false; opacity = 0.0; } protected override function createDefaultSkin(): Skin { return CircleProgressIndicatorSkin {}; } public function start(): Void { (skin.behavior as CircleProgressIndicatorBehavior).start(); } public function stop(): Void { (skin.behavior as CircleProgressIndicatorBehavior).stop(); } }
[Skin] CircleProgressIndicatorSkin.fx
import javafx.scene.Group; import javafx.scene.control.Skin; import javafx.scene.effect.GaussianBlur; import javafx.scene.shape.Rectangle; import javafx.scene.transform.Rotate; import javafx.util.Math; package def BLADE_NUM = 12; public class CircleProgressIndicatorSkin extends Skin { var progressBehavior = bind behavior as CircleProgressIndicatorBehavior; var progressControl = bind control as CircleProgressIndicator; var width = bind control.width; var height = bind control.height; var radius = bind progressControl.radius; var barWidth = bind radius / 5; var barHeight = bind radius / 2; var degree = 360.0 / BLADE_NUM; var backRect = Rectangle { width: bind width height: bind height fill: bind progressControl.background opacity: 0.8 }; package var indicator = for (i in [1..BLADE_NUM]) Rectangle { translateX: bind (width - barWidth) / 2 translateY: bind height / 2 - radius; width: bind barWidth height: bind barHeight arcWidth: bind Math.max(barWidth - 0.5, 0) arcHeight: bind Math.max(barWidth - 0.5, 0) transforms: Rotate { pivotX: bind barWidth / 2, pivotY: bind radius, angle: bind (i - 1) * degree }; fill: bind progressControl.foreground, effect: GaussianBlur { radius: 1.5 } }; init { behavior = CircleProgressIndicatorBehavior {}; node = Group { content: [ backRect, indicator ] }; } override function intersects(localX: Number, localY: Number, localWidth: Number, localHeight: Number) { return node.intersects(localX, localY, localWidth, localHeight); } override function contains(localX: Number, localY: Number) { return node.contains(localX, localY); } }
[Behavior] CircleProgressIndicatorBehavior.fx
import javafx.animation.KeyFrame; import javafx.animation.Timeline; import javafx.animation.transition.FadeTransition; import javafx.scene.control.Behavior; import javafx.util.Math; public class CircleProgressIndicatorBehavior extends Behavior { var progressSkin = bind skin as CircleProgressIndicatorSkin; var progressControl = bind skin.control as CircleProgressIndicator; var location: Number on replace { for (i in [0..CircleProgressIndicatorSkin.BLADE_NUM - 1]) { var k = Math.abs(i - location); progressSkin.indicator[i].opacity = if (k <= 1) 1.00 else if (k <= 2) 0.80 else if (k <= 3) 0.60 else if (k <= 4) 0.40 else if (k <= 5) 0.20 else if (k <= 6) 0.20 else if (k <= 7) 0.20 else if (k <= 8) 0.20 else if (k <= 9) 0.20 else if (k <= 10) 0.40 else if (k <= 11) 0.60 else if (k <= 12) 0.80 else 0.0; } } var timeline = Timeline { repeatCount: Timeline.INDEFINITE keyFrames: [ KeyFrame { time: 0s values: [ location => 0.0 ] }, KeyFrame { time: bind progressControl.interval values: [ location => CircleProgressIndicatorSkin.BLADE_NUM ] } ] } var fadeIn = FadeTransition { node: bind progressControl duration: bind progressControl.delay toValue: 1.0 } var fadeOut = FadeTransition { node: bind progressControl duration: bind progressControl.delay toValue: 0.0 action: function() { progressControl.toBack(); progressControl.visible = false; timeline.stop(); } } public function start(): Void { timeline.playFromStart(); progressControl.visible = true; progressControl.toFront(); fadeIn.playFromStart(); } public function stop(): Void { fadeOut.playFromStart(); } }
Main.fx
import javafx.scene.Scene; import javafx.scene.control.Hyperlink; import javafx.scene.layout.VBox; import javafx.scene.text.Font; import javafx.scene.text.Text; import javafx.stage.Stage; var indicator: CircleProgressIndicator = CircleProgressIndicator { radius: 20 width: bind indicator.scene.width height: bind indicator.scene.height } var text: Text = Text { content: "Hello World!!" x: 10, y: 40 font: Font { size: 28 } } var link = Hyperlink { text: "もう一度" font: Font {size: 12 } action: function() { InitializeTask {}.start(); } } var content = VBox { translateX: 5, translateY: 5, spacing: 5 visible: false; content: [ text, link ] } Stage { title : "JavaFX - Progress Indicator" scene: Scene { width: 320, height: 240 content: [ indicator, content ] } } InitializeTask {}.start(); class InitializeTask extends ActionTask { init { action = function () { // 時間のかかる初期化処理... java.lang.Thread.sleep(5000); } onStart = function() { content.visible = false; indicator.start(); } onDone = function() { content.visible = true; indicator.stop(); } } }