ちょっとしたアプリを作成してみる

11月です。JavaFXの動きはこの所はあまりないようです、次期バージョンの1.3の開発は進んでいるようですが、オーサリングツールなどまだ大きな発表はないようで。

さて、これまで色々と小ネタを積み重ねてきましたが、どれも「ちょっとした業務アプリを作る為のフレームワークにしたい」という目標がありました。プロジェクトサイトの方ではそろそろチケット管理やドキュメンテーション化を進め、適当なタイミングで最初のバージョンをリリースしたいかと思う所です。とりあえずはリポジトリも整理し、サンプルアプリを1つ作ってみたので、紹介します。内容はアドレス帳のアプリで、ソースコードこちらです。サンプルの実行はこちらからどうぞ(WebStart)。


仕様

アプリケーションとしては特別なものではなく、アドレスの一覧と追加・削除ができるシンプルなものです。一覧はデフォルトではテーブル(グリッド)がないため、カスタムコンポーネントとして作ったものを利用しており、ページング用のコンポーネントも使用しています。追加や削除の時はダイアログを表示するようにしており、こちらも自作コンポーネントです。
また、Sandboxで実行できる範囲でデータの永続化も行っています。保存のタイミングはアプリを閉じた時で、データはJSON形式でJavaFXのローカルストレージに保存されています。アプリを起動し直せば、前に終了したデータは復元されます。

実装

さて、これくらいの仕様を満たすためにどのくらいの実装コードが必要なのかという事ですが、200行以内で実現していますので紹介しましょう。

Address.fx

ただのデータクラスです。

public class Address {
    public var name:String;
    public var mailAddress:String;
    override function toString(): String {
        "{name}<{mailAddress}>"
    }
}
AddressStorage.fx

アドレス帳のストレージクラスです。

public class AddressStorage {
    var storage = Storage {
        source: "sunflower.sample.addressbook"
    };

    public function load(): Address[] {
        if (storage.totalBytes == storage.availableBytes) return [];
        var obj =
        JSONDeserializer {
            className: "jp.sunflower.javafx.sample.addressbook.Address"
            source: storage
        }.parse();
        for (item in (obj as com.sun.javafx.runtime.sequence.ArraySequence)) {
            item as Address
        }
    }

    public function save(addresses:Address[]):Void {
        JSONSerializer {
            fxObject: addresses as Object
        }.write(storage);
    }

}

Storageからのデータ取得と保存を行っています。内部的にはPullParserで保存をしているのですが、リフレクションAPIを使ってAddressのメンバ変数をJSONに書き出し、読み込み時には構築し直すユーティリティクラスを使っています。

Main.fx

エントリポイントです。ちょっとだけ機能を拡張したAppStageを使います。

AppStage {
    title: "アドレス帳"
    style: StageStyle.TRANSPARENT;
    scene: AppScene {
        width: 600, height: 500
        views: [AddressBookView {}]
    }
}

AddressBookView.fx

メインとなるViewクラスです。

public class AddressBookView extends AppView {
    /** save data to storage when close */
    override var onClose = function():Void {
        try {
            storage.save(items);
        } catch(e:Exception) {
            e.printStackTrace();
        }
    };
    init {
        title = "アドレス帳";
    }
    var pagination:Pagination = Pagination {
        maxPageIndex: bind grid.maxPageIndex + 1
        pageIndex: bind grid.pageIndex + 1
        goNextPage: function() { grid.goNextPage(); }
        goPreviousPage: function() { grid.goPreviousPage(); }
    };
    var items: Address[];
    var grid:Grid = Grid {
        cellInfos: Grid.cellInfos {
            title: [##[NAME]"Name", ##[ADDRESS]"Address"]
            width: [150, 300]
            stringOf: [
                function(item):String { (item as Address).name }
                function(item):String { (item as Address).mailAddress }
            ]
       }.cellInfos
       items: bind items
    };
    var formModel:FormModel = FormModel {
        fields: [
            CharField {
                name: "name"
                label: ##"Name"
                width: 100
                required: false
            }
            CharField {
                name: "mailAddress"
                label: ##"Address"
                width: 200
                required: false
            }
        ]
        modelClass: "jp.sunflower.javafx.sample.addressbook.Address"
    };
    var form:Form = Form {
        labelWidth: 100, fieldWidth: 200
        hgap: 5, vgap: 5
        model: formModel
    };
    var addContainer = VBox {
        layoutX: 10, layoutY: 10
        spacing: 5, nodeHPos: HPos.CENTER
        content: [
            form,
            Button {
                text: ##[ADD]"Add"
                action: function() {
                    addAddress();
                }
            }
        ]
    };
    var addButton:Button;
    var removeButton:Button;
    var addAddressWindow:ModalWindow;
    var control = HBox {
        spacing: 8, nodeVPos: VPos.CENTER
        content: [
            pagination,
            addButton = Button {
                text: ##[ADD]"Add"
                action: function() {
                    addAddressWindow = show("アドレス追加", addContainer, function(result:Boolean){});
                }
            }
            removeButton = Button {
                text: ##[REMOVE]"Remove"
                disable: bind not grid.selected;
                action: function() {
                    confirmMessage("削除確認", "削除します。よろしいですか?", function(result:Boolean){
                        if (not result) return;
                        delete items[grid.selectedRowIndex] from items;
                    });
                }
            }
        ]
    };
    var storage = AddressStorage {};
    postinit {
        items = storage.load();
        if (items.size() == 0) {
            items = [
               Address { name: "Jhon", mailAddress: "jhon@example.com" }
               Address { name: "Mike", mailAddress: "mike@example.com" }
               Address { name: "Poal", mailAddress: "poal@example.com" }
            ];
        }
    }
    function addAddress(): Void {
        if (formModel.getText("mailAddress") != "") {
            var newAddress = formModel.createModelObject() as Address;
            confirmMessage("追加確認", "追加します。よろしいですか?", function(result:Boolean){
                if (not result) return;
                insert newAddress before items[0];
                formModel.clear();
                addAddressWindow.close();
            });
        } else {
            addAddressWindow.close();
        }
    }
    override function createView():Node {
        VBox {
            layoutX: 10, layoutY: 10
            width: bind width - 20
            spacing: 5, nodeHPos: HPos.LEFT
            content: [
                control,
                grid
            ]
        }
    }
}

グリッド、ナビゲーション、ポップアップする入力フォームなどが宣言的に定義されており、あとはそれぞれのコントロール(ボタン)のアクションを定義しています。例えば、アドレス帳のシーケンスに対してアドレスが追加された場合の処理などが書かれていない事に注目してください。シーケンスitemsが増えるのですが、その結果グリッドの行が増えたりナビゲーションのカウントが変化するはずです。しかし、それらの処理はbindや裏側で処理されている為、(面倒な)記述をする必要がないわけです。コードのほとんどがグリッドやボタンの定義と、アクションであることに注目して下さい。
また、RubyScalaのように関数をそのまま関数に渡せる所もJavaFXの魅力です。GUIの場合、どうしてもコールバック関数などが必要になる場面が多いです。例えばダイアログの結果をもらってなにか処理をするなどです。確かにブロックするような形で待つのも1つの実装ですが、ダイアログに表示する内容と、OKを押されたときの処理を渡す方が便利です。

というわけで、少しはJavaFXのポテンシャルに気づいていただければ幸いです