列数が可変なTableView実装

本日はTableViewを使った実装。

月間の労働時間を記録する表を作ることを例にします。
縦軸にメンバーを、横軸に日付を取ることにします。
縦軸を可変にすることはできるけど、横軸を可変にするってのがよくわからない・・・・。

以下のような画面を出すための実装方法を検討してみました。



まず、データ構造ですが、日ごとの作業量を持つAmountPropertyクラス、作業者名を持つUserPropertyクラスを作成します。
UserPropertyは1ヶ月分のAmountPropertyを保持しており、ここでAmountPropertyはMapで構成し、キーは日付の情報とします。

以下の2つのPropertyクラスのソースを示します。

  • UserProperty
package net.atlabo.costcalc.property;

import java.util.HashMap;
import java.util.Map;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;

/**
 * ユーザ情報を保持するプロパティ
 * @author Atsushi Nakamoto
 */
public class UserProperty {
    /** ユーザ名称 */
    private StringProperty name = new SimpleStringProperty();
    /** 
     * AmountPropetyクラスをMapにして保持するPropertyクラスのインスタンス 
     * Mapのキー名は日付を表す文字列となる
     */
    private ObjectProperty<Map<String, AmountProperty>> amount = new SimpleObjectProperty<Map<String, AmountProperty>>();
    
    /**
     * ユーザ名称を取得する
     * @return 
     */
    public String getName() {
        return name.get();
    }
    
    /**
     * ユーザ名称を設定する
     * @param name
     * @return 
     */
    public UserProperty setName(String name) {
        this.name.set(name);
        return this;
    }
    
    /**
     * AmountProprtyを格納したマップクラスを設定する
     * @param amountMap
     * @return 
     */
    public UserProperty setAmount(Map<String, AmountProperty> amountMap) {
        this.amount.set(amountMap);
        return this;
    }
    
    /**
     * AmountPropertyクラスを取得する
     * @param key マップのキー名(キー名は日付情報文字列となる)
     * @return 
     */
    public AmountProperty getAmountProperty(String key) {
        // 設定作業を行わない場合、amount.getでNULLが返却されるため、空マップを作成する
        if (amount.get() == null) {
            amount.set(new HashMap<String, AmountProperty>());
        }
        
        // キー名が含まれていない場合、空のAmountPropertyクラスインスタンスを生成してマップに設定
        if (!amount.get().containsKey(key)) {
            amount.get().put(key, new AmountProperty(0d));
        }
        
        return amount.get().get(key);
    }
}
  • AmountProperty
package net.atlabo.costcalc.property;

import javafx.beans.property.DoubleProperty;
import javafx.beans.property.SimpleDoubleProperty;

/**
 * 量を保持するプロパティ
 * @author Atsushi Nakamoto
 */
public class AmountProperty {
    /***/
    private DoubleProperty amount = new SimpleDoubleProperty();
    
    /**
     * コンストラクタ
     * @param amount 
     */
    public AmountProperty(double amount) {
        this.amount.set(amount);
    }
    
    /**
     * 量をDoublePropertyのまま取得する
     * @return 
     */
    public DoubleProperty amountProperty() {
        return amount;
    }
}


次にTableViewを表示するためのメインクラスを示します。

package net.atlabo.costcalc;

import java.text.SimpleDateFormat;
import java.util.*;
import javafx.application.Application;
import javafx.beans.value.ObservableValue;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.scene.Scene;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableColumn.CellDataFeatures;
import javafx.scene.control.TableView;
import javafx.scene.control.cell.PropertyValueFactory;
import javafx.stage.Stage;
import javafx.util.Callback;
import net.atlabo.costcalc.property.AmountProperty;
import net.atlabo.costcalc.property.UserProperty;

/**
 * @author Atsushi Nakamoto
 */
public class Main extends Application {
    private SimpleDateFormat df = new SimpleDateFormat("d日(EEE)");
    
    /**
     * メインメソッド
     */
    public static void main(String[] args) {
        Application.launch(args);
    }

    @Override
    public void start(Stage stage) {
        // カラム(列)リストを生成
        List<TableColumn> columnList = new ArrayList<TableColumn>();
        
        // TableColumnを生成してカラムリストに追加
        TableColumn dateCol = new TableColumn("Member");
        dateCol.setCellValueFactory(new PropertyValueFactory("name"));
        columnList.add(dateCol);
        
        // ダミー用データ生成
        ObservableList<UserProperty> data = createDummyData();
        
        // 今月1日を取得する
        final Calendar cal = new GregorianCalendar();
        cal.set(Calendar.DAY_OF_MONTH, 1);
        
        // 今月末日を取得する
        Calendar end = new GregorianCalendar();
        end.add(Calendar.MONTH, 1);
        end.set(Calendar.DAY_OF_MONTH, 1);
        end.add(Calendar.DAY_OF_MONTH, -1);
        
        
        // 今月1日から末日までループしてカラムを生成、カラムリストに追加する
        TableColumn tableColumn = null;
        for (; !cal.after(end); cal.add(Calendar.DAY_OF_MONTH, 1)) {
            // 日付を取得する(マップのキーとしても使用する)
            final String theDay = df.format(cal.getTime());
            // テーブルカラムを生成
            tableColumn = new TableColumn(theDay);
            // ボディ部に設定する値を設定する
            tableColumn.setCellValueFactory(
                new Callback<CellDataFeatures<UserProperty, String>, ObservableValue>() {
                    public ObservableValue call(CellDataFeatures<UserProperty, String> p) {
                        return p.getValue().getAmountProperty(theDay).amountProperty();
                    }
                }
            );
            // カラムリストにカラムを追加
            columnList.add(tableColumn);   
        }
        
        // TableViewを生成
        TableView table = new TableView();
        // 生成したカラムリストを追加する
        table.getColumns().addAll(columnList);
        // データをテーブルの中に突っ込む
        table.setItems(data);

        Scene scene = new Scene(table);
        stage.setTitle("Table View Sample");
        stage.setWidth(800d);
        stage.setHeight(640d);
        stage.setScene(scene);
        stage.show();
    }
    
    /**
     * ダミーデータを生成する
     * この部分は本来DBから取得するなりファイルから取得するなり・・・・
     * @return 
     */ 
    private ObservableList<UserProperty> createDummyData() {
        // 今月1日に7.75設定する
        Calendar cal = new GregorianCalendar();
        cal.set(Calendar.DAY_OF_MONTH, 1);
        Map<String, AmountProperty> map = new HashMap<String, AmountProperty>();
        map.put(df.format(cal.getTime()), new AmountProperty(7.75d));
        
        return FXCollections.observableArrayList(
                new UserProperty().setName("Andrew"),
                new UserProperty().setName("Jack").setAmount(map),
                new UserProperty().setName("Peter"),
                new UserProperty().setName("George"),
                new UserProperty().setName("Ema"),
                new UserProperty().setName("Michael"),
                new UserProperty().setName("Hudson"),
                new UserProperty().setName("Lee"));
    }
}


Mainクラスのソースを追っていきます。


startメソッドが実質メインスレッドとなっています。


まずは、ArrayListインスタンスを生成します。
ArrayListはTableColumnの型指定をしておきます。
このオブジェクトは最後にまとめて登録するので、カラム内容は適宜このArrayListオブジェクトにaddしていくことになります。


まず最初に1番左の要素であるMember列を作成するため、TableColumnクラスのインスタンスを生成します。
コンストラクタはヘッダのラベルを示します。

更に、TableColumn#setCellValueFactoryメソッドを呼び出して、この列に表示する値を設定します。
ここのnew PropertyValueFactory("name")はUserProperty#getNameメソッドで取得できる値となりますが、
ここではまだデータとなるUserPropertyクラスのCollectionオブジェクトの設定は行っていません。


ここまでくるとArrayListインスタンスにTableColumnインスタンスを設定して1列目の設定が完了します。


ここから先が可変な列の生成になります。
今回のソースコードではシステム日付の月の1ヶ月分を可変な列とします。


ループ開始点である、今月1日とループの終端である今月末をそれぞれ保持し、
開始点から、1日ずつ日付をずらしながらforループをかけて月末までのカラムを設定します。


for文の中の説明です。
まずはTableColumnのインスタンスを生成します。
コンストラクタには日付と曜日を設定し、ヘッダのラベルを出力することにします。


次が重要ですが、実際のデータ部を設定するには、先ほどのTableColumn#setCellValueFactoryを呼び出すのですが、
引数にはCallBackの無名クラスを作り、callメソッドを実装します。
callメソッドのp.getValue()で、UserPropertyのインスタンスが取得できます。


ここではUserProperty内のMapクラスにアクセスするために日付をキーにしてAmountPropertyクラスのインスタンスを取得しています。
AmountProperty#amountPropertyで設定したDoublePropertyを返却することで、これをcallメソッドの戻り値とします。
そして、TableColumnの1件1件をArrayListインスタンスにaddしていきます。


最後にTableViewクラスのインスタンスを生成します。
ここまでで生成したArrayListインスタンスを設定し、更にTableView#setItemsでデータを突っ込むことができます。
ソースコードはサンプルなので、ここではcreateDummyData()でサンプルデータを生成するようになっています。
サンプルデータ生成のソースコードが長くなっても見難いだけなので、
Jackさんの1日分に7.75を設定しているだけです。
サンプルデータはObservableList型にする必要があります。


ここまでで上記の表示ができることになります。
ああ、めちゃくちゃ読みにくい・・・・。
装飾もない見出しもないでかなり読みにくいですが、ポイントは、Callbackクラスの無名クラスを実装するする箇所かな。