2007年12月7日金曜日

JavaFX言語の展開:クラス属性の初期値の設定場所

これまで、クラスの属性(attribute)が初期値を持つ場合、クラス本体の外でその設定を行っていました。これからは、Javaのように、クラスの本体内で属性の宣言と初期値の設定を行うようになります。
(旧)
class Foo {
attribute bar: Boolean;
}

attribute Foo.bar = true;
(新)
class Foo {
attribute bar: Boolean = true;
}

2007年12月6日木曜日

JavaFX言語の展開:関数と操作の統合

11月27日の投稿でも少しふれましたが、JavaFX言語も、コンパイラのサポートとともに、より明快で使いやすいものへと発展しようとしています。
そこで、JavaFXはどのように変わるのか、ということについて検討中の話題も含めて整理しておきたいと思います。
まずは、「functionとoperationの統合」についてです。
現状のインタプリタ版では、手続きの記述方法として関数(
function)と操作(operation)の2つが用意されています。関数は、return文を用いて必ず値を返し、結合演算子(bind)を明示的に指定しなくても、その本体は差分評価(incremental evaluation)されます。それに対して、操作の方は、必ずしも値を返す必要がなく、関数では使えない制御文が使えます。その代わり、操作の本体は関数のように暗黙的に差分評価されることはありません。
しかし、手続きとして関数と操作の2種類があると紛らわしいので、次のように関数
に統合されることになります(「旧」はこれまでのインタプリタ版で用いられていた構文を、「新」はコンパイラ版で新しく適用される構文を意味します)。
(旧)
class Foo {
function times2(x) { return x * 2; }
operation print(s) { System.out.println(s); }
}
(新)
class Foo {
function times2(x) { return x * 2; }
function print(s) { System.out.println(s); }
}

2007年12月5日水曜日

JavaFXでアラーム時計を作る(完成で~す!)

タイマの残り時間が0になるとアラームが鳴るようになりましたので、タイマの時間をUIから設定できるようにします。また、タイマが作動中に電話などの割り込みが入ったときは、カウントダウンを一時中断できるようにもしてみます。
完成したタイマのウィンドウイメージは、右の図のようになります(昔よく見かけた柱時計みたいになってしまいました)。
タイマの時間をスピナで設定し、スタートボタンを押すとカウントダウンを開始し、ストップボタンを押すとカウントダウンを停止します。時間設定のスピナは、残り時間のディジタル表示器の役目もしてくれます。
コード自体は、いたってシンプルなので、
JavaFX Script プログラミング言語 リファレンス深く学ぼう JavaFX Script 言語を参考に読んでいただければ、何を行っているかすぐ分かると思います。
と言うわけで、ごちゃごちゃ書かずに、変更版の
アラーム時計JavaFXファイル「AlarmClock.fx」のソースコード全体を載せてしまいます。

package timer;

import javafx.ui.*;
import javafx.ui.canvas.*;
import java.util.Date;
import java.lang.Math;
import java.lang.System;
import timer.Sound;

public class Timer {
private attribute elapsed: Number;
public attribute seconds: Number;
public attribute minutes: Integer;
public attribute hours: Integer;
public attribute count: Number;
public attribute startTime: Number;
public attribute elapsedTime: Number;
public attribute remainingTime: Number;
public attribute running: Boolean;
public attribute alarmEnabled: Boolean;
}

attribute Timer.elapsed = bind if running
then [1..10] dur 1000 linear while running continue if running
else 0;

trigger on Timer.elapsed = value {
var now = new Date();
elapsedTime = now.getTime() / 1000 - startTime;
remainingTime = count - elapsedTime;
running = if (elapsedTime >= count) then false else running;
seconds = remainingTime % 60;
minutes = ((Integer)remainingTime / 60) % 60;
hours = ((Integer)remainingTime / 60) / 60;
hours = if (elapsedTime >= count) then 0 else hours;
minutes = if (elapsedTime >= count) then 0 else minutes;
seconds = if (elapsedTime >= count) then 0 else seconds;
}

trigger on Timer.running = value {
if ((alarmEnabled == true) and (remainingTime <= 0)) {
alarmEnabled = false;
Sound.alarm();
}
}


Frame {
var t = Timer {seconds:0, minutes:0, hours:0, count:0,
running:false, alarmEnabled:false}
title: "JavaFX アラーム"
height: 350
width: 215
onClose: operation() {System.exit(0);}
content: BorderPanel {
center: Canvas {
content:
Group {
var font = new Font("Dialog", "PLAIN", 15)
var secs = bind t.seconds
var mins = bind t.minutes + secs / 60
var hrs = bind t.hours + mins / 60
content:
[Rect {
height: 400
width: 225
fill: Color {red:0.9, green:0.9, blue:0.9}
},
Circle {cx: 100, cy: 100, radius: 80, fill: white, stroke: black, strokeWidth: 1},
Group {
transform: translate(100, 100)
content: foreach (i in [1..12])
Text {
var radians = Math.toRadians(30 * i - 90)
transform: [translate((70 * Math.cos(radians)), (70 * Math.sin(radians)))]
content: "{i}"
valign: MIDDLE
halign: CENTER
}
},
Group {
transform: translate(100, 100)
var hourHand =
Line {x1: 0, y1: 0, x2: 0, y2: -35,
strokeWidth: 4, stroke: black
transform: bind rotate((hrs * 30), 0, 0)
}
var minuteHand =
Line {x1: 0, y1: 0, x2: 0, y2: -55,
strokeWidth: 2, stroke: blue,
transform: bind rotate((mins * 6), 0, 0)
}
var secondHand =
Line {x1: 0, y1: 0, x2: 0, y2: -75,
strokeWidth: 1, stroke: red,
transform: bind rotate((t.seconds * 6), 0, 0)
}
content: [hourHand, minuteHand, secondHand]
},
Circle {cx: 100, cy: 100, radius: 3, fill: black, stroke: black }]
},
}
bottom: Box {
background: white
orientation: VERTICAL
content:
[FlowPanel {
content:
[Spinner {
enabled: bind not(t.running)
min: 0
max: 23
value: bind t.hours
},
SimpleLabel {
enabled: bind not(t.running)
text: "時"
}]
},
FlowPanel {
content:
[Spinner {
enabled: bind not(t.running)
min: 0
max: 59
value: bind t.minutes
},
SimpleLabel {
enabled: bind not(t.running)
text: "分"
}]
},
FlowPanel {
content:
[Spinner {
enabled: bind not(t.running)
min: 0
max: 59
value: bind t.seconds
},
SimpleLabel {
enabled: bind not(t.running)
text: "秒"
}]
},
FlowPanel {
content:
[Button {
enabled: bind t.running
text: "ストップ"
action: bind operation() {
t.running = false;
}
},
RigidArea {
width: 20
},
Button {
enabled: bind not(t.running)
text: "スタート"
action: operation() {
var date = new Date();
t.count = t.hours * 3600 + t.minutes * 60 + t.seconds;
t.startTime = date.getTime() / 1000;
t.remainingTime = t.count;
if (t.count <> 0) {
t.alarmEnabled = true;
t.running = true;
}
}
}]
}]
}
}
visible: true
}


2007年12月4日火曜日

JavaFXでアラーム時計を作る(3)

アラーム音再生やメッセージ表示の準備ができたので、JavaFXのプログラムからこれを呼び出せるようにしてみます(以降、いずれもNetBeansで作業を行う場合を例に記述しています)。
作成したアラーム出力Javaアプリケーションプロジェクトの、「〈プロジェクト名〉\build\classes\〈パッケージ名〉\」ディレクトリの下に構築されたクラスファイル(私の場合は、「Sound..class」)と、コピーしておいたサウンドファイル(同様に、「TestMusic.wav」)を、ともに、アラーム時計JavaFXアプリケーションプロジェクトの方の「〈プロジェクト名〉\build\classes\〈パッケージ名〉\」ディレクトリの下にコピーします。
そして、12月2日の投稿で紹介した上記のアラーム時計
JavaFXファイル(同じく「AlarmClock.fx」)に、以下の変更を加えます。
まず、アラーム出力クラスのインポート文を追加します(以下、いずれも私が指定したパッケージ名やファイル名で記述します)。

import timer.Sound;

このとき、「Sound」の下に赤い波線が表示され、「undefined type ...」のエラーアイコンが表示されますが、無視します(エラーメッセージが表示されますが、ちゃんとクラスファイルを認識しています)。
そして、Timer.elapsedの置換triggerアクションの定義に続けて、次のTimer.runningの置換triggerアクションの定義を追加します。

trigger on Timer.running = value {
if (remainingTime <= 0) {Sound.alarm();} }

これで、準備が整いました。最後に、アラーム時計
アプリケーションのプロジェクトを構築しなおして(このとき、「Clean and Build Project」を実行しないように注意します。せっかくコピーしたアラーム出力クラスファイルとサウンドファイルが消されてしまいます)、プロジェクトを実行します。
アラーム時計のウィンドウが表示され、タイマに設定した時間(現在は10秒固定)が経過すると、アラーム音が鳴り、メッセージウィンドウが表示されます。









ここまでくれば、アラームタイマはほとんど完成したも同然です。あとは、タイマの時間を設定できるようにするだけですが、今日はここまで。いつものように、ソースコード全体を載せておきます。

package timer;

import javafx.ui.*;
import javafx.ui.canvas.*;
import java.util.Date;
import java.lang.Math;
import java.lang.System;
import timer.Sound;

public class Timer {
private attribute elapsed: Number;
public attribute seconds: Number;
public attribute minutes: Integer;
public attribute hours: Integer;
public attribute count: Number;
public attribute startTime: Number;
public attribute elapsedTime: Number;
public attribute remainingTime: Number;
public attribute running: Boolean;
}

attribute Timer.elapsed = bind if running
then [1..10] dur 1000 linear while running continue if running
else 0;

trigger on Timer.elapsed = value {
var now = new Date();
elapsedTime = now.getTime() / 1000 - startTime;
remainingTime = count - elapsedTime;
running = if (elapsedTime >= count) then false else true;
seconds = remainingTime % 60;
minutes = ((Integer)remainingTime / 60) % 60;
hours = ((Integer)remainingTime / 60) / 60;
hours = if (elapsedTime >= count) then 0 else hours;
}

trigger on Timer.running = value {
if (remainingTime <= 0) {Sound.alarm();}
}

var date = new Date();

Frame {
var t = Timer {seconds:10, minutes:0, hours:0, count:10,
startTime:(date.getTime() / 1000), running:true}
title: "JavaFX アラーム"
height: 225
width: 215
onClose: operation() {System.exit(0);}
content:
Canvas {
content:
Group {
var font = new Font("Dialog", "PLAIN", 15)
var secs = bind t.seconds
var mins = bind t.minutes + secs / 60
var hrs = bind t.hours + mins / 60
content:
[Rect {
height: 225
width: 225
fill: white
},
Circle {cx: 100, cy: 100, radius: 80, fill: white, stroke: black, strokeWidth: 1},
Group {
transform: translate(100, 100)
content: foreach (i in [1..12])
Text {
var radians = Math.toRadians(30 * i - 90)
transform: [translate((70 * Math.cos(radians)), (70 * Math.sin(radians)))]
content: "{i}"
valign: MIDDLE
halign: CENTER
}
},
Group {
transform: translate(100, 100)
var hourHand =
Line {x1: 0, y1: 0, x2: 0, y2: -35,
strokeWidth: 4, stroke: black
transform: bind rotate((hrs * 30), 0, 0)
}
var minuteHand =
Line {x1: 0, y1: 0, x2: 0, y2: -55,
strokeWidth: 2, stroke: blue,
transform: bind rotate((mins * 6), 0, 0)
}
var secondHand =
Line {x1: 0, y1: 0, x2: 0, y2: -75,
strokeWidth: 1, stroke: red,
transform: bind rotate((t.seconds * 6), 0, 0)
}
content: [hourHand, minuteHand, secondHand]
},
Circle {cx: 100, cy: 100, radius: 3, fill: black, stroke: black }]
},
}
visible: true
}

2007年12月3日月曜日

JavaFXでアラーム時計を作る(2)

せっかく、時計がカウントダウン表示されるようになったので、時間になったらアラーム音が鳴って、メッセージウィンドウが表示されるようにしてみます。
残念ながら、現状のJavaFX自体には、サウンド再生サポートはありません(アニメーションと同期させてサウンドを再生するなどの応用を考えると、JavaFXでサウンドの再生やアニメーションとの同期などを簡潔に記述できるといいのですが…)。そこで、Javaのプログラムで音の再生とメッセージの表示を行うクラスを作成し、JavaFXのプログラムからそれを呼び出すことにしました。
NetBeansでJavaアプリケーションのプロジェクト(名前は任意)を作成し、パッケージ名を「timer」、ファイル名を「Sound.java」として、クラスファイルを作成します(パッケージ名は、作成済みのJavaFXファイルで指定したパッケージ名にあわせました。ファイル名は、その名前でJavaFXプログラムから参照しさえすれば、何でもかまいません)。
サウンドの再生には、Java Appletなどで使うAudioClipクラスを利用します。ただ、Appletを作るわけではないので、java.applet.AppletクラスのクラスメソッドnewAudioClip()でサウンドファイルを読み込むようにします。このメソッドの引数には、サウンドファイルのURLを指定しないといけないのですが、それでは不便なので、作成するクラスの基底クラスclassからgetResource()メソッドを呼びだして、サウンドファイルのURLを取得し、引数に指定します。
AudioClipを生成したら、あとは、そのplay()メソッドを呼び出してサウンドをワンタイム再生するか、loop()メソッドとstop()メソッドで繰り返し再生とその停止を行うようにします。私は、メッセージウィンドウの表示に先立ってサウンドの繰り返し再生を起動し、メッセージウィンドウが閉じられたら再生を停止するようにしました。
サウンドファイルのタイプとしては、「.au」ファイルと「.wav」ファイルのどちらも指定できます。自分のパソコンから好きな音のファイルを捜してきて(ネットワークからダウンロードしてもいいのですが、得体の知れないものは怖いので)、そのファイルを、作成するクラスの「.class」ファイルが出力されるディレクトリ(プロジェクトファイルの下の「build\classes\〈パッケージ名〉\」ディレクトリ)にコピーします(このとき、ファイル検索ウィンドウからファイルを持ってくるときは、ファイルの移動を行わないように注意してください。不用意にドラッグするとファイルが移動され、元の場所からなくなってしまいます)。私は、「TestMusic.wav」という、ちょっと軽快な音楽が演奏されるファイルを選びました。
メッセージウィンドウの表示には、おなじみのJOptionPane.showMessageDialog()を使用しました。
showMessageDialog(VBのMsgboxに相当)は、ちょっとしたデバッグなどにも使えて便利なのですが、ダイアログウィンドウが必ずしもウィンドウスタックの最前面に表示されるわけではない、という難点(?)があります。この点については、同じように不便だと思われた方がいらしたようで、改造版を作られています。興味のある方は、Googleなどで検索してみてください。
作成したプロジェクトを構築して実行すると、サウンドが再生されて、メッセージダイアログが表示されます。
話が長くなってしまいましたので、今日はここまでです。最後にソースを載せておきます(いつものように、インデントが消えてしまっています。HTMLで編集すればいいのでしょうけれど…)。

package timer;

import java.applet.Applet;
import java.applet.AudioClip;
import javax.swing.JOptionPane;

public class Sound {

public static void alarm() {
AudioClip ac
= Applet.newAudioClip(Sound.class.getResource("TestMusic.wav"));
// ac.play();
ac.loop();
JOptionPane.showMessageDialog(null, "時間ですよ!", "警告!警告!", JOptionPane.WARNING_MESSAGE);
ac.stop();
}

public static void main(String[] args) {
Sound.alarm();
}
}

2007年12月2日日曜日

JavaFXでアラーム時計を作る

11月28日の投稿でアナログ時計のアプリケーションを紹介しましたが、それにちょっと手を入れて、アラームタイマを作ってみます。まずは、10秒固定でカウントダウン表示するようにしてみます。
NetBeansで、適当な名前でJavaFXプロジェクトを作ります。今回は、package名を設定することにし、「timer」を指定しました。ファイル名も何でも良いのですが、「AlarmClock.fx」としました。
まず、11月28日に投稿したコードをそのままコピーしてファイルに貼り付けます。
次に、コードの先頭にpackage宣言

package timer;

を追加します。次に、Timerクラスのminutesとhoursの型をIntegerに変更します。

public attribute minutes: Integer;
public attribute hours: Integer;

更に、Timerクラスに次の4つのattributeを追加します。

public attribute count: Number;
public attribute startTime: Number;
public attribute elapsedTime: Number;
public attribute remainingTime: Number;

そして、Timer.elapsedの設定コードの「continue if true」の部分を「continue if running」と書き換えます。

attribute Timer.elapsed = bind if running
then [1..10] dur 1000 linear while running continue if running
else 0;

次に、Timer.elapsedの置換triggerアクションの定義を次のように変えます。

trigger on Timer.elapsed = value {
var now = new Date();
elapsedTime = now.getTime() / 1000 - startTime;
remainingTime = count - elapsedTime;
running = if (elapsedTime >= count) then false else true;
seconds = remainingTime % 60;
minutes = ((Integer)remainingTime / 60) % 60;
hours = ((Integer)remainingTime / 60) / 60;
hours = if (elapsedTime >= count) then 0 else hours;
}

そして、Frameインスタンスの定義の前に次の行を追加し、

var date = new Date();

Frameインスタンス内のTimerインスタンス生成コードを、次のように変更すれば完成です。

var t = Timer {seconds:10, minutes:0, hours:0, count:10,
startTime:(date.getTime() / 1000), running:true}

とりあえず、カウントダウンする10秒は、ハードコードしました。最後に全体のコードを載せておきます。

package timer;

import javafx.ui.*;
import javafx.ui.canvas.*;
import java.util.Date;
import java.lang.Math;
import java.lang.System;

public class Timer {
private attribute elapsed: Number;
public attribute seconds: Number;
public attribute minutes: Integer;
public attribute hours: Integer;
public attribute count: Number;
public attribute startTime: Number;
public attribute elapsedTime: Number;
public attribute remainingTime: Number;
public attribute running: Boolean;
}

attribute Timer.elapsed = bind if running
then [1..10] dur 1000 linear while running continue if running
else 0;

trigger on Timer.elapsed = value {
var now = new Date();
elapsedTime = now.getTime() / 1000 - startTime;
remainingTime = count - elapsedTime;
running = if (elapsedTime >= count) then false else true;
seconds = remainingTime % 60;
minutes = ((Integer)remainingTime / 60) % 60;
hours = ((Integer)remainingTime / 60) / 60;
hours = if (elapsedTime >= count) then 0 else hours;
}

var date = new Date();

Frame {
var t = Timer {seconds:10, minutes:0, hours:0, count:10,
startTime:(date.getTime() / 1000), running:true}
title: "JavaFX アラーム"
height: 225
width: 215
onClose: operation() {System.exit(0);}
content:
Canvas {
content:
Group {
var font = new Font("Dialog", "PLAIN", 15)
var secs = bind t.seconds
var mins = bind t.minutes + secs / 60
var hrs = bind t.hours + mins / 60
content:
[Rect {
height: 225
width: 225
fill: white
},
Circle {cx: 100, cy: 100, radius: 80, fill: white, stroke: black, strokeWidth: 1},
Group {
transform: translate(100, 100)
content: foreach (i in [1..12])
Text {
var radians = Math.toRadians(30 * i - 90)
transform: [translate((70 * Math.cos(radians)), (70 * Math.sin(radians)))]
content: "{i}"
valign: MIDDLE
halign: CENTER
}
},
Group {
transform: translate(100, 100)
var hourHand =
Line {x1: 0, y1: 0, x2: 0, y2: -35,
strokeWidth: 4, stroke: black
transform: bind rotate((hrs * 30), 0, 0)
}
var minuteHand =
Line {x1: 0, y1: 0, x2: 0, y2: -55,
strokeWidth: 2, stroke: blue,
transform: bind rotate((mins * 6), 0, 0)
}
var secondHand =
Line {x1: 0, y1: 0, x2: 0, y2: -75,
strokeWidth: 1, stroke: red,
transform: bind rotate((t.seconds * 6), 0, 0)
}
content: [hourHand, minuteHand, secondHand]
},
Circle {cx: 100, cy: 100, radius: 3, fill: black, stroke: black }]
},
}
visible: true
}

2007年12月1日土曜日

コマンドラインからのJavaFXインタプリタの起動

(以下の例はWindowsの場合です。ほかのOS環境では読み替えてください)
コマンドラインからJavaFXインタプリタを起動するには、まず、PATH環境変数にOpenJFXをインストールしたディレクトリの下にある「trunk\bin」ディレクトリを設定します。例えば、OpenJFXをCドライブの下に「OpenJFX」という名前でインストールした場合は、「C:\OpenJFX\trunk\bin;」をPATH環境変数に追加します。
そして、コマンドプロンプトウィンドウを開いてJavaFXアプリケーションを保存したディレクトリに移動し、次のようにコマンドを入力します(「JfxApp.fx」アプリケーションを起動する場合の例)。
>javafx JfxApp
もし、CLASSPATH環境変数を設定しているために「Exception in thread "main" java.lang.NoClassDefFoundError: ...」と、エラー表示されてアプリケーションを起動できないときは(私の場合がそうでした)、次のようにコマンドを入力して、CLASSPATHの値を空にしてから実行します。
>set classpath=
>javafx JfxApp
(「set classpath=」で、現在のコマンドプロンプトウィンドウのCLASSPATH の設定を解除します。ほかのコマンドプロンプトウィンドウのCLASSPATH設定には影響を与えません)。