ページ

2013年6月12日水曜日

JavaFX - TableView : Nested TableColumn

TableViewで、以下のように、複数の列に対してヘッダーを表示してみたいと思います。

TableColumnは、TableColumnのリストを保持しており、ネストすることができますので、簡単に複数の列に対してヘッダーを表示することができます。FXMLファイルでは以下のように定義することになります。

<Scene xmlns:fx="http://javafx.com/fxml"
       fx:id="scene"
       width="640" height="480">
    <stylesheets>
        <URL value="@NestedTableColumnDemoSceneStyle.css"/>
    </stylesheets>

    <StackPane>
        <TableView fx:id="recordTableView">
            <columns>
                <TableColumn fx:id="recordIdTableColumn" text="Id"/>
                <TableColumn fx:id="recordNameTableColumn" text="Name"/>
                <TableColumn fx:id="recordItemTableColumn" text="Item">
                    <columns>
                        <TableColumn fx:id="itemNameTableColumn" text="Name"/>
                        <TableColumn fx:id="itemValueTableColumn" text="Value"/>
                        <TableColumn fx:id="itemDescriptionTableColumn" text="Description"/>
                    </columns>
                </TableColumn>
                <TableColumn fx:id="recordNoteTableColumn" text="Note" prefWidth="240"/>
            </columns>
        </TableView>
    </StackPane>
</Scene>

上記のように定義したTableColumnに対して、以下のモデルを表示してみたいと思います。

class RecordModel {
    public IntegerProperty idProperty() { return id; }
    private final IntegerProperty id = new SimpleIntegerProperty(this, "id");
    public final int getId() { return id.get(); }
    public final void setId(int id) { this.id.set(id); }

    public StringProperty nameProperty() { return name; }
    private final StringProperty name = new SimpleStringProperty(this, "name");
    public final String getName() { return name.get(); }
    public final void setName(String name) { this.name.set(name); }

    public ObjectProperty<ItemModel> itemProperty() { return item; }
    private final ObjectProperty<ItemModel> item = new SimpleObjectProperty<ItemModel>(this, "item");
    public final ItemModel getItem() { return item.get(); }
    public final void setItem(ItemModel item) { this.item.set(item); }

    public StringProperty noteProperty() { return note; }
    private final StringProperty note = new SimpleStringProperty(this, "note");
    public final String getNote() { return note.get(); }
    public final void setNote(String note) { this.note.set(note); }

    public RecordModel() {}

    public RecordModel(int id, String name, ItemModel item, String note) {
        setId(id);
        setName(name);
        setItem(item);
        setNote(note);
    }
}

class ItemModel {
    public StringProperty nameProperty() { return name; }
    private final StringProperty name = new SimpleStringProperty(this, "name");
    public final String getName() { return name.get(); }
    public final void setName(String name) { this.name.set(name); }

    public IntegerProperty valueProperty() { return value; }
    private final IntegerProperty value = new SimpleIntegerProperty(this, "value");
    public final int getValue() { return value.get(); }
    public final void setValue(int value) { this.value.set(value); }

    public StringProperty descriptionProperty() { return description; }
    private final StringProperty description = new SimpleStringProperty(this, "description");
    public final String getDescription() { return description.get(); }
    public final void setDescription(String description) { this.description.set(description); }

    public ItemModel() {}

    public ItemModel(String name, int value, String description) {
        setName(name);
        setValue(value);
        setDescription(description);
    }
}

RecordModelのリストをTableViewに表示することにし、ネストしているTableColumnには、RecordModelが保持しているItemModelを表示することにします。

まず、Viewに対するモデルを以下のように定義します。

class NestedTableColumnDemoModel {
    public ObjectProperty<ObservableList<RecordModel>> recordsProperty() { return records; }
    private final ObjectProperty<ObservableList<RecordModel>> records = new SimpleObjectProperty<ObservableList<RecordModel>>(this, "records");
    public final ObservableList<RecordModel> getRecords() { return records.get(); }
    public final void setRecords(ObservableList<RecordModel> records) { this.records.set(records); }

    public void loadRecords() {
        setRecords(
            FXCollections.<RecordModel>observableArrayList(
                new RecordModel(1, "Record 1", new ItemModel("Item 1", 2500, "Description 1"), "Note 1..."),
                new RecordModel(2, "Record 2", new ItemModel("Item 2", 1500, "Description 2"), "Note 2..."),
                new RecordModel(3, "Record 3", new ItemModel("Item 3", 4500, "Description 3"), "Note 3..."),
                new RecordModel(4, "Record 4", new ItemModel("Item 4", 8500, "Description 4"), "Note 4..."),
                new RecordModel(5, "Record 5", new ItemModel("Item 5", 3500, "Description 5"), "Note 5...")
            )
        );
    }
}

次に、Viewのコントローラーを以下のように定義します。

class NestedTableColumnDemoSceneController {
    public ObjectProperty<NestedTableColumnDemoModel> modelProperty() { return model; }
    private final ObjectProperty<NestedTableColumnDemoModel> model = new SimpleObjectProperty<NestedTableColumnDemoModel>(this, "model") {
        private NestedTableColumnDemoModel currentModel;
        @Override
        protected void invalidated() {
            if (currentModel != null) { NestedTableColumnDemoSceneController.this.unbind(currentModel); }
            currentModel = get();
            if (currentModel != null) { NestedTableColumnDemoSceneController.this.bind(currentModel); }
        }
    };
    public final NestedTableColumnDemoModel getModel() { return model.get(); }
    public final void setModel(NestedTableColumnDemoModel model) { this.model.set(model); }
    
    @FXML
    private Scene scene;
    @FXML
    private TableView<RecordModel> recordTableView;
    @FXML
    private TableColumn<RecordModel, Integer> recordIdTableColumn;
    @FXML
    private TableColumn<RecordModel, String> recordNameTableColumn;
    @FXML
    private TableColumn<RecordModel, ItemModel> recordItemTableColumn;
    @FXML
    private TableColumn<RecordModel, String> itemNameTableColumn;
    @FXML
    private TableColumn<RecordModel, Integer> itemValueTableColumn;
    @FXML
    private TableColumn<RecordModel, String> itemDescriptionTableColumn;
    @FXML
    private TableColumn<RecordModel, String> recordNoteTableColumn;
    
    public void performOn(Stage stage) {
        stage.setScene(scene);
        stage.setTitle("Nested Column Demo");
        stage.sizeToScene();
        stage.centerOnScreen();
        stage.show();
    }
    
    public NestedTableColumnDemoSceneController with(NestedTableColumnDemoModel model) {
        setModel(model);
        if (model != null) { model.loadRecords(); }
        
        return this;
    }

    protected void bind(NestedTableColumnDemoModel model) {
        recordTableView.itemsProperty().bind(model.recordsProperty());
    }

    protected void unbind(NestedTableColumnDemoModel model) {
        recordTableView.itemsProperty().unbind();
    }
    
    @FXML
    protected void initialize() {
        recordIdTableColumn.setCellValueFactory(PropertySelector.<RecordModel, Integer>forTableColumn("id"));
        recordNameTableColumn.setCellValueFactory(PropertySelector.<RecordModel, String>forTableColumn("name"));
        itemNameTableColumn.setCellValueFactory(PropertySelector.<RecordModel, String>forTableColumn("item.name"));
        itemValueTableColumn.setCellValueFactory(PropertySelector.<RecordModel, Integer>forTableColumn("item.value"));
        itemDescriptionTableColumn.setCellValueFactory(PropertySelector.<RecordModel, String>forTableColumn("item.description"));
        recordNoteTableColumn.setCellValueFactory(PropertySelector.<RecordModel, String>forTableColumn("note"));
    }
}

TableColumnのcellValueFactoryの設定には、ネストしているTableColumnに対しても他のTableColumnと同様に設定することができます。モデルとしては、TableViewに設定したモデル、ここではRecordModelが渡されますので、RecordModelが保持しているItemModelの値を設定したい場合は、RecordModelから辿っていく必要があります。例えば、ItemModelのnamePropertyの値を設定したい場合は、以下のようになります。

itemNameTableColumn.setCellValueFactory(new Callback<TableColumn.CellDataFeatures<RecordModel, String>, ObservableValue<String>>() {
    @Override
    public ObservableValue<String> call(TableColumn.CellDataFeatures<RecordModel, String> recordModelStringCellDataFeatures) {
        return recordModelStringCellDataFeatures.getValue().getItem().nameProperty();
    }
});

今回は、プロパティを辿っていくためのヘルパーとして、PropertySelectorというものを作成して使っています。PropertySelectorの定義は、以下のようになります。

public final class PropertySelector {
    public static <S, T> Callback<TableColumn.CellDataFeatures<S, T>, ObservableValue<T>> forTableColumn(final String step);
    public static <T> ReadOnlyProperty<T> select(Object root, String step);
}

stepには、「.」で区切ってプロパティを指定します。取得できるプロパティは、指定した名称が設定されているプロパティ、あるいは、指定した名称+Propertyの名称のメソッドから取得されるプロパティ、あるいは、is+指定した名称、get+指定した名称のメソッドから取得される値をReadOnlyPropertyでラップしたもののいずれかを、順に検索し取得できたものとなります。同じようなもので、Bindingsクラスのselectメソッドがあります。

最後に、Applicationクラスを作成しておきます。

public class NestedTableColumnDemo extends Application {
    @Override
    public void start(Stage primaryStage) {
        FXController.of(new NestedTableColumnDemoSceneController())
            .fromDefaultLocation()
            .load()
            .with(new NestedTableColumnDemoModel())
            .performOn(primaryStage);
    }

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

JavaFXのTableColumnは、ネストすることができますので、TableViewで、複数の列に対してヘッダを簡単に表示することができますので、いろいろと活用していきたいと思います。