ページ

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では、現状では基本的なクラスが提供されているだけとなっていますので、このようなユーティリティクラスをいろいろと作成しておくと良いかもしれません。

2013年7月16日火曜日

JavaFX - TableView : AutoTableColumnGeneration (1)

TableViewでは、表示したい列を静的に定義して、表示するモデルのコレクションを指定して内容を表示しますが、今回は、表示したい列をモデルから動的に生成するユーティリティクラスを考えてみたいと思います。

このユーティリティクラスは、以下のようにスタティックプロパティでTableViewに指定すると、TableViewのitemsプロパティに値が設定されたときに、設定されたモデルの内容から、TableColumnを動的に作成して、TableViewに追加するようにします。

<TableView AutoTableColumnGeneration.enable="true"/>

まずは、クラス名をAutoTableColumnGenerationとして作成し、スタティックプロパティを定義します。

public final class AutoTableColumnGeneration {
    private static final String AUTO_TABLE_COLUMN_GENERATION_ENABLE = "auto-table-column-generation-enable";

    private static final ChangeListener<? super ObservableList> TABLE_VIEW_ITEMS_CHANGE_LISTENER = new ChangeListener<ObservableList>() {
        @Override
        public void changed(ObservableValue<? extends ObservableList> observableValue, ObservableList oldItems, ObservableList newItems) {
            AutoTableColumnGeneration.generateTableColumns((TableView)((ReadOnlyProperty)observableValue).getBean(), newItems);
        }
    };

    public static <S> boolean isEnable(TableView<S> tableView) {
        Objects.requireNonNull(tableView);

        if (!tableView.hasProperties()) { return false; }

        Object enable = tableView.getProperties().get(AUTO_TABLE_COLUMN_GENERATION_ENABLE);
        return enable != null && (boolean)enable;
    }

    public static <S> void setEnable(TableView<S> tableView, boolean enable) {
        Objects.requireNonNull(tableView);

        tableView.getProperties().put(AUTO_TABLE_COLUMN_GENERATION_ENABLE, enable);
        if (enable) {
            tableView.itemsProperty().addListener(TABLE_VIEW_ITEMS_CHANGE_LISTENER);
        } else {
            tableView.itemsProperty().removeListener(TABLE_VIEW_ITEMS_CHANGE_LISTENER);
        }
    }

    private static void generateTableColumns(TableView tableView, ObservableList items) {
        tableView.getColumns().clear();
        if (items == null || items.isEmpty()) { return; }

        ...

    }
}

TableViewのitemsプロパティに値が設定されたときに、TableColumnを生成するようにします。TableColumnの生成は、指定されたモデルに定義されているReadOnlyPropertyから生成するようにします。そこで、戻り値がReadOnlyPropertyに代入することができるメソッドをリフレクションで取得し、TableColumnのインスタンスを生成し、TableViewにそのインスタンスを追加するようにします。このとき、TableViewのeditableプロパティがtrueの場合に各項目が編集できるように、cellFactoryを設定しておきます。cellFactoryの設定は、取得したReadOnlyPropertyの種類によって、以下のようにします。

Property Cell Converter
StringProperty TextFieldTableCell
BooleanProperty CheckBoxTableCell
IntegerProperty TextFieldTableCell IntegerStringConverter
LongProperty TextFieldTableCell LongStringConverter
FloatProperty TextFiledTableCell FloatStringConverter
DoubleProperty TextFiledTableCell DoubleStringConverter

また、ObjectPropertyで、オブジェクトの型がDateの場合は、TextFieldTableCellをDateTimeStringConverterで、日付の書式を「yyyy/MM/dd HH:mm:ss」として生成するようにします。

上記以外の場合でも、cellFactoryを指定できるように、以下のメソッドを用意しておきます。

public static <S, T> void registerCellFactory(Class<T> cellValueClass, Callback<TableColumn<S, T>, TableCell<S, T>> cellFactory);

セルに設定する値の型をキーにして、cellFactoryを設定するようにします。

以上で、TableColumnを動的に作成することができますが、表示する列の順番は未定となり、また、各TableColumnの詳細の設定ができませんので、これらの設定ができるように、以下のアノテーションを定義しておきます。

@Target({ ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
public @interface AutoTableColumn {
    String id() default "";
    String name() default "";
    int order() default Integer.MAX_VALUE;
    boolean editable() default true;
    double prefWidth() default 80;
    double maxWidth() default 5000;
    double minWidth() default 10;
    boolean resizable() default true;
    boolean sortable() default true;
    TableColumn.SortType sortType() default TableColumn.SortType.ASCENDING;
    String[] styleClass() default "";
    String[] cellStyleClass() default "";
}

TableColumnに設定することができるプロパティの値をこのアノテーションで指定できるようにします。AutoTableColumnGenerationでは、このアノテーションが指定されている場合は、設定されている値をTableColumnのプロパティにそれぞれ設定するようにします。ただし、orderについては、TableColumnの表示順を指定し、値の小さい順に表示するようにし、cellStyleClassについては、各セルに指定されているStyleClassを設定するようにします。

今回は、TableViewのTableColumnを動的に作成するユーティリティクラスについて考えてみました。次回は、このクラスを実際に使ってみたいと思います。