鹿駆動勉強会

JavaFX つながりで @hakurai さんが奈良で勉強会をするというので、すかさず参加すると宣言したのが 1 月のこと。それがあれよあれよという間に、能楽堂でやる 鹿駆動勉強会 ということになってしまいました。

まぁ、楽しければいいんですけどね。

他の人よりも少しだけ長めの 15 分をいただいて、JavaFX のことをしゃべってきました。

といっても、15 分しかないので、どういうことができるかを中心に、あまり内容に深入りせずに広く浅くという感じです。

今回は、最後の部分で BGM がかぶってしまってぐちゃぐちゃになってしまったのが失敗でした。普段は、1 時間ぐらいしゃべるのが普通なので、BGM を複数用意してあったとしても、その間の時間間隔は十分にあるため、かぶるということを気にしないでも大丈夫だったのですが...

時間が短い LT では、ちゃんとそのあたりまだプレゼンの設計をしなくてはダメですね。まだまだ修業が足りません。

JavaOne Tokyo 2012

JavaOne のレポートは本家の blog に書いたので、そちらをご覧ください (1 日目2 日目)。

で、JavaOne では 2 つのセッションを担当させていただきました。1 つが Java SE 7 の NIO.2 について、もう 1 つが JavaFX でした。

とはいっても、JavaFX の普通のセッションは絶対あるだろうということで、Oracle の人が話しそうにもない内容で Call for Papers に出したのでした。その思惑があたったのか、JavaOne で話すことになったわけです。

ということで、JavaFX といいつつ FXML と CSS が中心のセッションを行いました。FXML: 8、CSS: 2 ぐらいの割合です。

なぜ XML なのかというのは、JavaFX 1.x の頃でいえばなぜ宣言的文法なのかと同じです。やっぱり Java だと GUI を書きにくいんですよね。そこが伝われば、櫻庭としては御の字です。

JavaFX + JUnit で JavaScript のユニットテストをする その 2

昨日のエントリー は、思っていたよりも反響を呼んでしまってビックリしている櫻庭です。

今日はちょっとだけ追加。

というのも、Jenkins の川口さん ( id:kkawa ) から次のようなことを聞かれたからです。


<p id="addtext"></p>

そして、HTML でボタンなどをクリックした時にコールされる関数が次のようなものだったとします。

function addText(arg) {
    var element = document.getElementById('addtext');
    element.innerText = arg;
}

タグにテキストを追加するという関数です。

これを WebEngine を使って、コールすることを考えます。基本的には昨日の手法と同じです。

では、DummyApplication クラスに addTextTest メソッドを追加します。

    public Object addTextTest(Object obj) throws InterruptedException {
        arguments.put(obj);        

        Platform.runLater(new Runnable() {
            @Override
            public void run() {
                try {
                    Object obj = arguments.take();
                    engine.executeScript("addText('"+obj+"');");
                    
                    Document document = engine.getDocument();
                    Element element = document.getElementById("addtext");
                    results.put(element.getTextContent());
                } catch (InterruptedException ex) {}
            }
        });
            
        return results.take();
    }

昨日は WebEngine クラスの executeScript メソッドの戻り値をキューに入れていたのですが、今日はそうではありません。

その代わりに、DOM の要素を取得して、そのコンテンツをキューに入れています。

JUnit のテストコードでは次のように記述しました。

    @Test
    public void addTextTest() throws InterruptedException {
        System.out.println("addTextTest");
        
        String expected = "Hello, World!";
        String result = (String)dummy.addTextTest(expected);
        assertThat(result, is(expected));

        expected = "";
        result = (String)dummy.addTextTest(expected);
        assertThat(result, is(expected));
    }

このようにすれば、DOM のチェックもできるので、DOM を操作するタイプの JavaScript の関数でもテストできます。

そういえば、昨日は 1 つのテストしか実行していなかったので、 @Before アノテーションJavaFX の EDT を立ち上げて、@After アノテーションで EDT をシャットダウンするので動作していました。

でも、複数のテストを行いのであれば、@BeforeClass で EDT を立ち上げて、@AfterClass で EDT をシャットダウンしなくてはダメですね。

さて、川口さんには DOM のテストもできるとツィートしたのですが、その後再び川口さんから返信。


package test;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import javafx.application.Application;
import org.junit.AfterClass;
import org.junit.BeforeClass;
import org.junit.Test;

import static org.junit.Assert.assertThat;
import static org.hamcrest.CoreMatchers.is;

public class JSTest {
    private static DummyApplication dummy;
    private static ExecutorService service;
    
    public JSTest() {}

    @BeforeClass
    public static void setUpClass() throws Exception {
        service = Executors.newFixedThreadPool(1);
        service.submit(new Runnable() {
            @Override
            public void run() {
                Application.launch(test.DummyApplication.class);
            }
        });
        
        Thread.sleep(1000L);
        
        dummy = DummyApplication.getInstance();
    }

    @AfterClass
    public static void tearDownClass() {
        service.shutdown();
        dummy.shutdown();
    }

    @Test
    public void numberTest() throws InterruptedException {
        System.out.println("numberTest");
        
        Integer result = (Integer)dummy.numberTest(new Integer(3));
        assertThat(result, is(new Integer(4)));

        result = (Integer)dummy.numberTest("23");
        assertThat(result, is(new Integer(24)));
    }

    @Test
    public void addTextTest() throws InterruptedException {
        System.out.println("addTextTest");
        
        String expected = "Hello, World!";
        String result = (String)dummy.addTextTest(expected);
        assertThat(result, is(expected));

        expected = "";
        result = (String)dummy.addTextTest(expected);
        assertThat(result, is(expected));
    }
}

次に DummyApplication.java です。

package test;

import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
import javafx.application.Application;
import javafx.application.Platform;
import javafx.scene.web.WebEngine;
import javafx.stage.Stage;
import org.w3c.dom.Document;
import org.w3c.dom.Element;

public class DummyApplication extends Application {
    private static DummyApplication instance;
    
    private WebEngine engine;
    private BlockingQueue<Object> arguments = new LinkedBlockingQueue<>(); 
    private BlockingQueue<Object> results = new LinkedBlockingQueue<>(); 
    
    @Override
    public void start(Stage stage) throws Exception {
        instance = this;

        engine = new WebEngine();
        engine.load( [index.htmlのURL] );
    }
    
    public static DummyApplication getInstance() {
        return instance;
    }
    
    public void shutdown() {
        Platform.runLater(new Runnable() {
            @Override
            public void run() {
                Platform.exit();
            }
        });
    }

    public Object numberTest(Object obj) throws InterruptedException {
        arguments.put(obj);        

        Platform.runLater(new Runnable() {
            @Override
            public void run() {
                try {
                    Object obj = arguments.take();
                    Object result = (Integer)engine.executeScript("numberTest("+obj+");");
                    results.put(result);
                } catch (InterruptedException ex) {}
            }
        });
            
        return results.take();
    }

    public Object addTextTest(Object obj) throws InterruptedException {
        arguments.put(obj);        

        Platform.runLater(new Runnable() {
            @Override
            public void run() {
                try {
                    Object obj = arguments.take();
                    engine.executeScript("addText('"+obj+"');");
                    
                    Document document = engine.getDocument();
                    Element element = document.getElementById("addtext");
                    results.put(element.getTextContent());
                } catch (InterruptedException ex) {}
            }
        });
            
        return results.take();
    }
}

JavaFX + JUnit で JavaScript のユニットテストをする

(追記) コードが間違っていたので、次の日に修正版を書きました。


今日はちょっとマニアックなネタです。

というのも、@mike_neck が悩んでいるようだったので...

JavaFX には WebView というブラウザーのコントロールがあります。それを使って JavaScript のテストをしてしまおうというのが今日のお題です。

ただ、問題は @mike_neck も悩んでいるように、スレッドの問題です。

JUnitJUnit のスレッドで動作し、JavaFXJavaFX のイベントディスパッチスレッド (EDT) で動作します。

問題はその間をどうやって取り持ってあげるかです。

JUnit は自身のスレッド @Before のメソッドを実行して、@Test のメソッドを実行していきます。つまり、主導権を握っているのは JUnit であって、テストされる側が自分で JUnit のスレッドをコントロールすることはできません。

一方の JavaFX は Application.launch メソッドで EDT を起動すると、後は EDT が start メソッドをコールし、イベント駆動でよしなにやってくれます。

でも、テストをするためには JUnit のスレッドから JavaFX のスレッドをコントロールしなくてはいけません。

じゃあ、どうするか。JavaFX の EDT から JUnit のスレッドにアクセスすることはできそうにないので、JUnit のスレッドから JavaFX の EDT に アクセスするようにします。

そのために使用するのが Platforrm.runLater メソッドです。

なんか聞いたことがあるようなメソッド名ではないですか。Swing 知っている人は分かると思うのですが、SwingUtilities.invokeLater メソッドの JavaFX 版なのです。

runLater メソッドの引数には Runnable オブジェクトを記述します。すると、Runnable インタフェースの run メソッドが EDT で実行されます。

この runLater メソッドを使って JUnit のスレッドから JavaFX の EDT にアクセスします。しかし、runLater メソッドの引数は Runnable インタフェースだというのが問題です。

つまり、引数も戻り値もないのです。

そこで、どうするか。ここではブロッキングキューを使用しました。引数用のブロッキングキュート、戻り値用のブロッキングキューを用意し、それらを経由してスレッド間の値の引き渡しを行っています。

そういえば、ちょっと注意すべき所に、Application.launch メソッドがブロックするメソッドだということです。なので、不用意に JUnit のスレッドで Application.launch メソッドをコールしてしまうと、次に進まなくなってしまいます。

今回、テストするのは次の JavaScript です (mike_neck が使っているものそのままです)。ファイル名は test.js となっています。

function numberTest(arg) {
    return 1 + arg;
}

function stringTest(arg) {
    return arg + "_" + "test";
}

function objectTest(arg) {
    arg.test = 'value';
    return arg;
}

(function() {
    var element = document.getElementById('loaded');
    element.innerText = 'loaded';
})();

この test.js を読み込んでいるのが index.html です。

<!DOCTYPE HTML>
<html lang="ja">
<head>
    <title>test page</title>
</head>
<body>
<h1 id="title">title</h1>
<p id="test">test</p>
<p id="loaded"></p>
<script lang="javascript" type="text/javascript" src="test.js"></script>
</body>
</html>

この index.html を JavaFX で読み込んでユニットテストを行います。

では、順々に解説していきます。まずテストのクラスです。

public class JSTest {
    private DummyApplication dummy;
    private ExecutorService service;
    
    public JSTest() {}

    @Before
    public void setUp() throws Exception {
        service = Executors.newFixedThreadPool(1);
        service.submit(new Runnable() {
            @Override
            public void run() {
                Application.launch(test.DummyApplication.class);
            }
        });
        
        Thread.sleep(1000L);
        
        dummy = DummyApplication.getInstance();
    }

まず、JavaFX の EDT を起動しなくてはいけないので、@Before のメソッドで行うことにします。

(追記) @Before ではなくて、@BeforeClass です。詳細はこちらにあります。


前述したように Application.launch メソッドはブロックしてしまうので、JUnit のスレッドとは別のスレッドで起動します。

ここでは Concurrency Utils の ExecutorService インタフェースを使いましたけど、昔ながらの Thread クラスと Runnable インタフェースで書いてもかまいません。

JavaFX では必ず Application クラスのサブクラスを作成して、Application.launch メソッドに渡す必要があります。そこで、ここではダミーのクラスである DummyApplication クラスを作成しました。

(ちなみに、launch メソッドの引数にクラスクラスを指定しない場合は、launch メソッドを記述したクラスを使用するようになっています。)

これで JavaFX の EDT は起動できたのですが、問題は EDT を扱うために Application オブジェクトを取得しておかなくてはいけないということです。

ところが、Application オブジェクトを生成するのは EDT なので、そのままだと取得できないのです。そこでオブジェクトを取得するため、シングルトンのように getInstance メソッドを定義しました。とはいっても、シングルトンではないですが。

DummyApplication クラスの該当部分を以下に示しておきます。

public class DummyApplication extends Application {
    private static DummyApplication instance;
    
    private WebEngine engine;
    
    @Override
    public void start(Stage stage) throws Exception {
        instance = this;

        engine = new WebEngine();
        engine.load( [index.htmlのURL] );
    }
    
    public static DummyApplication getInstance() {
        return instance;
    }

JavaFX では start メソッドでシーングラフを作成するのですが、ここでは表示する必要はないので HTML ファイルをパースする WebEngine オブジェクトだけをロードしてあります。

そういえば、WebEngine クラスって、WebView クラスとペアで使わなくてはいけないと思っている人が多いようですが、単独でも使えるんです。

では、本命のユニットテストです。

ここでは、test.js の numberTest 関数をテストするメソッドを作成しました。

    @Test
    public void numberTest() throws InterruptedException {
        Integer result = (Integer)dummy.numberTest(new Integer(3));
        assertThat(result, is(new Integer(4)));

        result = (Integer)dummy.numberTest("23");
        assertThat(result, is(new Integer(24)));
    }

実際に numberTest 関数を実行しているのは DummyApplication クラスです。

こうやって見ると、ごくごく普通のテストメソッドのようですね。

では DummyApplication クラスの numberTest メソッドを見てみましょう。

    private BlockingQueue<Object> arguments = new LinkedBlockingQueue<>(); 
    private BlockingQueue<Object> results = new LinkedBlockingQueue<>(); 

    public Object numberTest(Object obj) throws InterruptedException {
        arguments.put(obj);        

        Platform.runLater(new Runnable() {
            @Override
            public void run() {
                try {
                    Object obj = arguments.take();
                    Object result = (Integer)engine.executeScript("numberTest("+obj+");");
                    results.put(result);
                } catch (InterruptedException ex) {}
            }
        });
            
        return results.take();
    }

前述したように JUnit のスレッドと JavaFX の EDT 間の値の引き渡しにはブロッキングキューを使用します。ここでは引数用の arguments 変数、戻り値用に results 変数を使用しています。

numberTest メソッドでは、まずブロッキングキューの arguments に引数をエンキューしています。

その後に Platform.runLater メソッドです。

run メソッドでは arguments から引数をデキューします。ブロッキングキューはその名の通り、キューに要素がなければ take メソッドがブロックします。これで、スレッド間の同期を図ることができます。

arguments から取り出した引数を使用して、WebEngine クラスの executeScript メソッドをコールします。executeScript メソッドが HTML に含まれるスクリプトを実行するためのメソッドになります。

executeScript の戻り値は、今度は results にエンキューします。そして、JUnit のスレッドにもどって results からデキューし、それをメソッドの戻り値として返します。

ここでは簡単化のために戻り値が null の場合を扱っていないのですが、本格的にテストする場合は考慮しなくてはダメですね。

これで、後は JUnit 側で assertThat メソッドで検証すればいいということになります。

ここまではやったので、Jetty を組み合わせる部分などはまかせた > @mike_neck

最後に今回使用したテストクラスの JSTest クラスと DummyApplication クラスの全体を示しておきます。

まず、JSTest クラスです。

package test;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import javafx.application.Application;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import static org.junit.Assert.assertThat;
import static org.hamcrest.CoreMatchers.is;

public class JSTest {
    private DummyApplication dummy;
    private ExecutorService service;
    
    public JSTest() {}

    @Before
    public void setUp() throws Exception {
        service = Executors.newFixedThreadPool(1);
        service.submit(new Runnable() {
            @Override
            public void run() {
                Application.launch(test.DummyApplication.class);
            }
        });
        
        Thread.sleep(1000L);
        
        dummy = DummyApplication.getInstance();
    }

    @After
    public void tearDown() {
        service.shutdown();
        dummy.shutdown();
    }

    @Test
    public void numberTest() throws InterruptedException {
        Integer result = (Integer)dummy.numberTest(new Integer(3));
        assertThat(result, is(new Integer(4)));

        result = (Integer)dummy.numberTest("23");
        assertThat(result, is(new Integer(24)));
    }
}

次に DummyApplication クラスです。

package test;

import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
import javafx.application.Application;
import javafx.application.Platform;
import javafx.scene.web.WebEngine;
import javafx.stage.Stage;

public class DummyApplication extends Application {
    private static DummyApplication instance;
    
    private WebEngine engine;
    private BlockingQueue<Object> arguments = new LinkedBlockingQueue<>(); 
    private BlockingQueue<Object> results = new LinkedBlockingQueue<>(); 
    
    @Override
    public void start(Stage stage) throws Exception {
        instance = this;

        engine = new WebEngine( [index.htmlのURL] );
        engine.load();
    }
    
    public static DummyApplication getInstance() {
        return instance;
    }
    
    public void shutdown() {
        Platform.runLater(new Runnable() {
            @Override
            public void run() {
                Platform.exit();
            }
        });
    }

    public Object numberTest(Object obj) throws InterruptedException {
        arguments.put(obj);        

        Platform.runLater(new Runnable() {
            @Override
            public void run() {
                try {
                    Object obj = arguments.take();
                    Object result = (Integer)engine.executeScript("numberTest("+obj+");");
                    results.put(result);
                } catch (InterruptedException ex) {}
            }
        });
            
        return results.take();
    }
}

コントロール その 1

JavaFX シリーズ目次

Swing では GUI で表示するオブジェクトをコンポーネントと呼びますが、JavaFX ではコントロール (正確には UI Controls) と呼びます。

FlexVB でもコントロールと呼ぶので、違和感はないかもしれません。

どうでもいいですけど、この UI Controls はもともと Caspian というプロジェクトで作られていました。もうほとんど残っていないですが、たまーに caspian という記述が残っていることがあります。

さて、このコントロール、Swing のコンポーネントと同じように使うことができます。

とりあえず、Swing コンポーネントとの対応を見てみましょう。

Swing JavaFX
JApplet ×
JButton Button
JCheckBox CheckBox
JCheckMenuItem CheckMenuItem
JColorChooser ×
JComboBox ChoiceBox
JDesktopPane ×
JDialog ×
JEditorPane HTMLEditor
JFileChooser FileChooser
JFormattedTextEditor ×
JFrame Stage
JInternalFrame ×
JLabel Label
JLayer × (必要なし)
JLayeredPane StackPane
JList ListView
JMenu Menu
JMenuBar MenuBar
JMenuItem MenuItem
JOptionPane ×
JPanel Pane
JPasswordField PasswordField
JPopupMenu ContextMenu
JProgressBar ProgressBar
JRadioButton RadioButton
JRadioButtonMenuItem RadioMenuItem
JRootPane Scene
JScrollPane ScrollPane
JSeparator Separator
JSlider Slider
JSpinner ×
JSplitPane SplitPane
JTabbedPane TabPane
JTable TableView
JTextArea TextArea
JTextField TextField
JTextPane × (HTMLEditor)
JToggleButton ToggleButton
JToolBar ToolBar
JToolTip Tooltip
JTree TreeView
JViewport ×
JWindow Window
× Accordion
× CustomMenu
× Hyperlink
× MenuButton
× ProgressIndicator
× SplitMenuButton
× TitledPane

ほとんどの Swing コンポーネントに対応したコントロールがあるのですが、ダイアログ系がないのがイタイですね。

いちおう、Stage をダイアログの代わりにすることができるのですが...

ちなみに FileChooser はありますが、厳密にいうとコントロールではなく、Stage と同列になっています。

Swing にはないコントロールとして、Accordion や Hyperlink、ProgressIndicator などがあります。

Accordion は Flex などでよく使われる上下に開くタイプのコンテナ (?) です。ProgressIndicator は処理待ちの時に円形にグルグル回るコントロールです。

では、実際に主なコントロールを使ってみましょう。

その前に、以下で使用するコードはすべて次のテンプレートで書いてます。

import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.layout.FlowPane;
import javafx.stage.Stage;

public class Test extends Application {
   
    @Override
    public void start(Stage stage) {
        stage.setTitle("Control Demo");
     
        FlowPane root = new FlowPane();
        root.setHgap(10);
        root.setLayoutX(20); root.setLayoutY(20);
        Scene scene = new Scene(root, 400, 200);
        stage.setScene(scene);

        /*
          ここにコントロールのコードを記入します
         */
        
        stage.show();
    }

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

コメントのところに、コードを書いてみてください。

コントロールの大きさによってステージのサイズも変化させているので、そこら辺は適当にやってくださいww

Label

ラベルは使用頻度では一番多いかもしれないですね。文字列を表示するコントロールです。

javafx.scene.text.Text というクラスもあるのですが、こちらは本当に文字列だけです。それに対し、Label クラスはアイコンも使用することができます。

まずは単純な文字列。

    Label label = new Label("Label");
    root.getChildren().add(label);

ラベルで表示している文字列の取得は getText メソッド、文字列の設定は setText メソッドでできます。

f:id:skrb:20120305214356p:image

では、次にアイコンをつけてみましょう。

Swing ではアイコンは Icon クラスで表していましたが、JavaFX では Node クラスです。なので、何でもありです。

    Label label1 = new Label("Label 1",
                             new Rectangle(2, 2, 6, 6));
    root.getChildren().add(label1);

    Label label2 = new Label("Label 2",
                             new ImageView(new Image("src/bullet.png")));
    root.getChildren().add(label2);

アイコンはコンストラクタの第 2 引数で指定します。後から指定するには、setGraphic メソッドを使用します。ここでは、アイコンとして四角と、イメージをロードして使用しました。

f:id:skrb:20120305214551p:image

Button

使用頻度ではボタンも多いですね。Button クラスは JButton クラスとほとんど使い方は同じです。

まずは、テキストだけのボタンです。

    Button button = new Button("Button");
    button.setOnAction(new EventHandler<ActionEvent>() {
        @Override
        public void handle(ActionEvent event) {
            System.out.println("Button is clicked.");
        }            
    });
    root.getChildren().add(button);

ボタンに表示するテキストは setText メソッドでも設定できます。

イベントについては、また別の機会に詳しく書こうと思っていますが、Swing のようにイベントの種類ごとにリスナがあるのではなく、すべて EventHandler クラスで扱います。イベントの種類はジェネリクスで指定します。

これを実行すると、下図のようになります。


f:id:skrb:20120305214954p:image


次にアイコンつきのボタンです。

Button クラスのアイコンも Node クラスです。なので、何でもありです。

    Button button1 = new Button("Button1", 
                                new Rectangle(2, 2, 6, 6));
    root.getChildren().add(button1);

    Button button2 = new Button("Button2", 
                                new ImageView(new Image("src/bullet.png")));
    root.getChildren().add(button2);
        
    Button button3 = new Button("Button3",
                                new RadioButton("RadioButton"));
    root.getChildren().add(button3);

ここでは、ボタンを 3 つ並べています。四角とイメージは Label の時と同じです。最後は、ボタンの中にラジオボタンが入ります。

こんなんでもありなのです。

実行すると、こうなります。

f:id:skrb:20120305215216p:image

なんか不思議な感じですよね。でも、ちゃんと動作します。ラジオボタンもちゃんとチェックできます。

f:id:skrb:20120305215300p:image

TextField

続いて、TextField です。

Swing の JTextField クラスだとコンストラクタにはカラム数を指定しますが、TextField クラスでは setPrefColumnCount メソッドで指定します。

    TextField field = new TextField("TextField");
    field.setPrefColumnCount(20);
    root.getChildren().add(field);

実行してみましょう。

f:id:skrb:20120305232231p:image

TextField に入力された文字列を取得するには、getText メソッドを使用します。

ちなみに、JavaFX はまだフォント周りのバグが結構残っていて、JavaFX 2.0 だと日本語を入力するとカーソルがずれるというバグがあります。

Windows 版の JavaFX 2.1 だと直っているのですが、Linux 版はまだバグったままです。

Linux 版で日本語を入力するとこうなります。

f:id:skrb:20120306002939p:image

Linux 版の品質はまだまだといったところでしょう。

さて、3 つのコントロールができたので、3 つを組み合わせてみましょう。

ボタンをクリックしたら、テキストフィールドに入力した文字列をラベルにセットするというサンプルです。

import javafx.application.Application;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.TextField;
import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;

public class ControlsDemo extends Application {
    private TextField field;
    private Label label;
    
    @Override
    public void start(Stage stage) {
        stage.setTitle("Control Demo");
     
        VBox root = new VBox(10);
        root.setAlignment(Pos.CENTER);

        Scene scene = new Scene(root, 340, 120);
        stage.setScene(scene);

        HBox hbox = new HBox(10);
        hbox.setAlignment(Pos.CENTER);
        root.getChildren().add(hbox);

        field = new TextField();
        field.setPrefColumnCount(20);
        hbox.getChildren().add(field);

        Button button = new Button("Update");
        button.setOnAction(new EventHandler<ActionEvent>() {
            @Override
            public void handle(ActionEvent event) {
                // テキストフィールドの文字列をラベルにセットする
                String text = field.getText();
                label.setText(text);
            }            
        });
        hbox.getChildren().add(button);
        
        label = new Label();
        root.getChildren().add(label);
        
        // ステージの表示
        stage.show();
    }

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

レイアウトの部分はあまり見ないでおいて、コントロールの部分だけ見てください。

ボタンがクリックされたら、TextField.getText メソッドで文字列を取得し、Label.setText でセットしているだけです。

実行すると...

f:id:skrb:20120306004449p:image

テキストフィールドに入力して、ボタンをクリックします。

f:id:skrb:20120306004554p:image

次回はまた違うコントロールを使ってみましょう。

JavaFX のプレゼンテーションをするよ

今月から 4 月にかけて、JavaFX のプレゼンで各地を回ります。お近くで開催の際には、ぜひご参加ください。

最後の鹿駆動勉強会は奈良です。

岡山と奈良は比較的初心者向けの話にしようと思っています。でも、JavaOne は初心者向けのセッションはすでにあるので、もうちょっと突っこんだ話にするつもりです。

シーングラフ

JavaFX シリーズ目次

前回の Hello, World! の時に GUI はツリー構造で表せるということを書きました。

たとえば、下の絵に示した GUI はこんな構造になります。

[Scene]-[VBox]-+-[HBox]-+-[Label]
               |        +-[TextField]
               |        +-[Button]
               +-[WebView]

f:id:skrb:20120215212004p:image


このような GUI を表すツリー構造を JavaFX ではシーングラフと呼びます。

グラフといっても、棒グラフとか円グラフとかのグラフではなく、グラフ理論のグラフです。

もともと、シーングラフは 3D CG で使われていた言葉で、Java 3D もシーングラフを使用して 3D CG を構築します。でも、3D に限定する必要はまったくなく Adobe の Illsutrator などもシーングラフを使っています。

今までの Swing も同じようにツリー構造で GUI を表していました。しかし、ツリーに追加できるのは Swing のコンポーネントだけという違いがあります。JavaFX ではコントロールはもちろん、四角や丸などの今までは Java 2D で描画していた要素もシーングラフに記述することができます。

たとえば、下の GUI は...

[Scene]-[Group]-+-[Circle]
                +-[GridPane]-+-[Label]
                             +-[TextField]
                             +-[Label]
                             +-[TextField]

f:id:skrb:20120215214807p:image

Swing であれば、Circle は Java 2D で書かなくてはいけなかったので、JComponent のサブクラスを作成して paintComponent メソッドをオーバーライドする必要がありました。

でも、JavaFX だったら、単純にシーングラフに追加すればいいだけです。

現状、JavaFX は 3D の機能は限定的なのですが、視点の位置 (カメラ) やライトなどもシーングラフに記述することができます。

しかし、前回も書いたように Java だとシーングラフが分かりにくい。ということで、これからは FXML が主流になると思います。

たとえば、はじめのブラウザの場合、FXML がどうなるかというと、こんな感じ。

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

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

<VBox xmlns:fx="http://javafx.com/fxml"
      prefWidth="1000" prefHeight="740"
      fx:controller="custombrowser.CustomBrowser">
    <children>
        <HBox>
            <children>
                <Label text="URL:" />
                <TextField prefColumnCount="40" />
                <Button text="Load" />
            </children>
        </HBox>
        <WebView />
    </children>
</VBox>

でも、これだけじゃステージに該当するコントロールが表示されるだけです。

ブラウザには何も表示されませんし、ボタンを押しても何も変化はありません。FXML は GUI の構造を記述することはできますが、振る舞いは記述できません*1。振る舞いは Java で書く必要があります。

ということは FXML で書いた要素と Java を連携させる必要があります。次回はそこら辺について書く予定です。

*1:厳密には書くことができるのですが、それはまた別の機会に書きます。