ページ

2013年7月17日水曜日

JavaFX - TableView : AutoTableColumnGeneration (2)

前回は、TableViewのTableColumnを表示したいモデルの内容から、自動的に作成するユーティリティクラスについて、考えてみました。今回は、そのユーティリティクラスを実際に使ってみたいと思います。

まずは、表示するモデルとして以下のようなものを考え、AutoTableColumnアノテーションでTableColumnのプロパティを指定してみます。

class UserModel {
    @AutoTableColumn(name = "ユーザ名", order = 2, prefWidth = 120)
    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); }

    @AutoTableColumn(name = "年齢", order = 4, prefWidth = 60)
    public IntegerProperty ageProperty() { return age; }
    private final IntegerProperty age = new SimpleIntegerProperty(this, "age");
    public final int getAge() { return age.get(); }
    public final void setAge(int age) { this.age.set(age); }

    @AutoTableColumn(name = "点数", order = 5, prefWidth = 60)
    public DoubleProperty pointProperty() { return point; }
    private final DoubleProperty point = new SimpleDoubleProperty(this, "point");
    public final double getPoint() { return point.get(); }
    public final void setPoint(double point) { this.point.set(point); }

    @AutoTableColumn(name = "誕生日", order = 3, prefWidth = 180)
    public ObjectProperty<Date> birthdayProperty() { return birthday; }
    private final ObjectProperty<Date> birthday = new SimpleObjectProperty<Date>(this, "birthday");
    public final Date getBirthday() { return birthday.get(); }
    public final void setBirthday(Date birthday) { this.birthday.set(birthday); }

    @AutoTableColumn(name = "管理者", order = 1, prefWidth = 60, cellStyleClass = "manager-table-cell", sortable = false, resizable = false)
    public BooleanProperty managerProperty() { return manager; }
    private final BooleanProperty manager = new SimpleBooleanProperty(this, "manager");
    public final boolean isManager() { return manager.get(); }
    public final void setManager(boolean manager) { this.manager.set(manager); }

    @AutoTableColumn(sortable = false, resizable = false)
    public ObjectProperty<Color> colorProperty() { return color; }
    private final ObjectProperty<Color> color = new SimpleObjectProperty<Color>(this, "color");
    public final Color getColor() { return color.get(); }
    public final void setColor(Color color) { this.color.set(color); }

    public UserModel() {}

    public UserModel(String name, int age, double point, Date birthday, Color color) {
        this(name, age, point, birthday, color, false);
    }

    public UserModel(String name, int age, double point, Date birthday, Color color, boolean manager) {
        setName(name);
        setAge(age);
        setPoint(point);
        setBirthday(birthday);
        setColor(color);
        setManager(manager);
    }
}

いろいろな型のプロパティを定義し、それぞれにAutoTableColumnアノテーションを付与してみました。

次に、表示するViewを以下のように定義します。

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

    <StackPane>
        <TableView fx:id="userTableView" editable="true"
                   AutoTableColumnGeneration.enable="true"/>
    </StackPane>
</Scene>

TableViewのみ定義し、AutoTableColumnGenerationのenableプロパティをtrueに設定し、自動的にTableColumnが生成されるようにします。

このViewに対するモデルを以下のようにします。

class AutoTableColumnGenerationDemoModel {
    public ReadOnlyObjectProperty<ObservableList<UserModel>> usersProperty() { return users.getReadOnlyProperty(); }
    private final ReadOnlyObjectWrapper<ObservableList<UserModel>> users = new ReadOnlyObjectWrapper<>(this, "users");
    public final ObservableList<UserModel> getUsers() { return users.get(); }
    protected final void setUsers(ObservableList<UserModel> users) { this.users.set(users); }

    public void loadUsers() {
        setUsers(
            FXCollections.<UserModel>observableArrayList(
                new UserModel("User 1", 40, 2.54, new GregorianCalendar(1973, 4, 25).getTime(), Color.ALICEBLUE, true),
                new UserModel("User 2", 33, 3.21, new GregorianCalendar(1980, 1, 3).getTime(), Color.CHARTREUSE),
                new UserModel("User 3", 35, 1.05, new GregorianCalendar(1978, 8, 15).getTime(), Color.BURLYWOOD),
                new UserModel("User 4", 38, 8.5, new GregorianCalendar(1975, 7, 13).getTime(), Color.MEDIUMSEAGREEN, true),
                new UserModel("User 5", 30, 0.35, new GregorianCalendar(1983, 11, 2).getTime(), Color.SIENNA)
            )
        );
    }
}

サンプルデータをとして、5件のデータを設定するようにしています。

また、このViewに対するコントローラーを以下のようにします。

class AutoTableColumnGenerationDemoSceneController {
    public ObjectProperty<AutoTableColumnGenerationDemoModel> modelProperty() { return model; }
    private final ObjectProperty<AutoTableColumnGenerationDemoModel> model = new SimpleObjectProperty<AutoTableColumnGenerationDemoModel>(this, "model") {
        private AutoTableColumnGenerationDemoModel currentModel;
        @Override
        protected void invalidated() {
            if (currentModel != null) { AutoTableColumnGenerationDemoSceneController.this.unbind(currentModel); }
            currentModel = get();
            if (currentModel != null) { AutoTableColumnGenerationDemoSceneController.this.bind(currentModel); }
        }
    };
    public final AutoTableColumnGenerationDemoModel getModel() { return model.get(); }
    public final void setModel(AutoTableColumnGenerationDemoModel model) { this.model.set(model); }
    
    @FXML
    private Scene scene;
    @FXML
    private TableView<UserModel> userTableView;
    
    public void performOn(Stage stage) {
        stage.setScene(scene);
        stage.setTitle("Auto TableColumn Generation Demo");
        stage.sizeToScene();
        stage.centerOnScreen();
        stage.show();
    }
    
    public AutoTableColumnGenerationDemoSceneController with(AutoTableColumnGenerationDemoModel model) {
        setModel(model);
        if (model != null) { model.loadUsers(); }
        
        return this;
    }

    protected void bind(AutoTableColumnGenerationDemoModel model) {
        userTableView.itemsProperty().bind(model.usersProperty());
    }

    protected void unbind(AutoTableColumnGenerationDemoModel model) {
        userTableView.itemsProperty().unbind();
    }
    
    @FXML
    protected void initialize() {
        AutoTableColumnGeneration.registerCellFactory(Date.class, TextFieldTableCell.forTableColumn(new DateTimeStringConverter("yyyy/MM/dd")));
        AutoTableColumnGeneration.registerCellFactory(Color.class, ColorTableCell.forTableColumn());
    }
}

型がDateの場合は、デフォルトでは、「yyyy/MM/dd HH:mm:ss」の書式で表示するようにしていましたが、今回は、「yyyy/MM/dd」の書式で表示したいので、AutoTableColumnGenerationのregisterCellFactoryメソッドで、cellFactoryを登録しています。また、型がColorの項目は、編集時にColorPickerで値を選択するようにしたいので、そのようなセルをColorTableCellとして作成し、型がColorの場合のcellFactoryとして登録しています。

ColorTableCellについては、以下のようになります。

class ColorTableCell<S> extends TableCell<S, Color> {
    private Rectangle colorRectangle;

    private ColorTableCell() {
        getStyleClass().add("color-table-cell");
        setContentDisplay(ContentDisplay.GRAPHIC_ONLY);
        setAlignment(Pos.CENTER);
        setPrefHeight(40);
    }

    public static <S> Callback<TableColumn<S, Color>, TableCell<S, Color>> forTableColumn() {
        return new Callback<TableColumn<S, Color>, TableCell<S, Color>>() {
            @Override
            public TableCell<S, Color> call(TableColumn<S, Color> tableColumn) {
                return new ColorTableCell<>();
            }
        };
    }

    @Override
    protected void updateItem(Color color, boolean empty) {
        super.updateItem(color, empty);

        if (empty) {
            setGraphic(null);
            return;
        }

        if (colorRectangle == null) { colorRectangle = createColorRectangle(); }
        colorRectangle.setFill(color);
        setGraphic(colorRectangle);
    }

    @Override
    public void startEdit() {
        if (!isEditable()) { return; }
        if (!getTableView().isEditable() || !getTableColumn().isEditable()) { return; }

        super.startEdit();

        ColorPicker colorPicker = new ColorPicker((Color)colorRectangle.getFill());
        colorPicker.setOnAction(event -> ColorTableCell.this.commitEdit(((ColorPicker) event.getSource()).getValue()));
        colorPicker.setOnHidden(event -> ColorTableCell.this.cancelEdit());
        setGraphic(colorPicker);
    }

    @Override
    public void cancelEdit() {
        if (!isEditing()) { return; }

        super.cancelEdit();

        setGraphic(colorRectangle);
    }

    private Rectangle createColorRectangle() {
        return new Rectangle(30, 30);
    }
}

最後に、Applicationクラスを以下のようにします。

public class AutoTableColumnGenerationDemo extends Application {
    @Override
    public void start(Stage primaryStage) throws Exception {
        FXController.of(new AutoTableColumnGenerationDemoSceneController())
            .fromDefaultLocation()
            .load()
            .with(new AutoTableColumnGenerationDemoModel())
            .performOn(primaryStage);
    }

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

実行結果は、以下のようになります。

TableViewで、表示するモデルの内容から、TableColumnを自動的に生成するユーティリティクラスを使ってみました。このようなユーティリティクラスを作成しておくと、モデルを定義するだけで、TableViewへの表示が容易になります。JavaFXでは、現状では基本的なクラスが提供されているだけとなっていますので、このようなユーティリティクラスをいろいろと作成しておくと良いかもしれません。