ページ

2013年8月11日日曜日

JavaFX - ChartDataSource (2)

前回は、PieChart.Dataに変換するためのユーティリティクラスを考えてみました。今回は、このユーティリティクラスを利用して、以下のようなアプリケーションを作成してみたいと思います。

TableViewで表示されているデータをPieChartで表示し、TableViewで値を変更すると、その変更がPieChartに反映されるようにしています。また、PieChartに表示するTableViewのデータを選択できるようにもしています。

まずは、TableViewに表示するデータを以下のように定義します。

class RecordModel {
    @AutoTableColumn(order = 1, editable = false, cellStyleClass = {"name-table-cell"})
    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(order = 2, prefWidth = 70, cellStyleClass = "value-table-cell")
    public IntegerProperty value1Property() { return value1; }
    private final IntegerProperty value1 = new SimpleIntegerProperty(this, "value1");
    public final int getValue1() { return value1.get(); }
    public final void setValue1(int value1) { this.value1.set(value1); }

    @AutoTableColumn(order = 3, prefWidth = 70, cellStyleClass = "value-table-cell")
    public IntegerProperty value2Property() { return value2; }
    private final IntegerProperty value2 = new SimpleIntegerProperty(this, "value2");
    public final int getValue2() { return value2.get(); }
    public final void setValue2(int value2) { this.value2.set(value2); }

    public RecordModel(String name, int value1, int value2) {
        setName(name);
        setValue1(value1);
        setValue2(value2);
    }
}

PieChartに表示する値として、value1とvalue2を定義しています。AutoTableColumnについては、こちらを参照してください。

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

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

    <SplitPane dividerPositions="0.45">
        <HBox id="recordRegion">
            <TableView fx:id="recordTableView"
                       AutoTableColumnGeneration.enable="true"
                       editable="true"/>

            <VBox>
                <Button fx:id="addRecordButton" text="+" prefWidth="35"/>
                <Button fx:id="removeRecordButton" text="-" prefWidth="35"/>
            </VBox>
        </HBox>

        <VBox id="chartRegion">
            <Label text="Select pieValue property name."/>

            <HBox id="valueConditionRegion">
                <fx:define>
                    <ToggleGroup fx:id="valuePropertyNameGroup"/>
                </fx:define>
                <RadioButton text="Value1" toggleGroup="$valuePropertyNameGroup"
                             userData="value1"/>
                <RadioButton text="Value2" toggleGroup="$valuePropertyNameGroup"
                             userData="value2"/>
            </HBox>

            <PieChart fx:id="recordPieChart"/>
        </VBox>
    </SplitPane>
</Scene>

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

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

    public ObjectProperty<TableView.TableViewSelectionModel<RecordModel>> recordSelectionModelProperty() { return recordSelectionModel; }
    private final ObjectProperty<TableView.TableViewSelectionModel<RecordModel>> recordSelectionModel = new SimpleObjectProperty<>(this, "recordSelectionModel");
    public final TableView.TableViewSelectionModel<RecordModel> getRecordSelectionModel() { return recordSelectionModel.get(); }
    public final void setRecordSelectionModel(TableView.TableViewSelectionModel<RecordModel> recordSelectionModel) { this.recordSelectionModel.set(recordSelectionModel); }

    public ObjectProperty<String> pieChartDataNameStepProperty() { return pieChartDataNameStep; }
    private final ObjectProperty<String> pieChartDataNameStep = new SimpleObjectProperty<>(this, "pieChartDataNameStep", "name");
    public final String getPieChartDataNameStep() { return pieChartDataNameStep.get(); }
    public final void setPieChartDataNameStep(String pieChartDataNameStep) { this.pieChartDataNameStep.set(pieChartDataNameStep); }

    public ObjectProperty<String> pieChartDataValueStepProperty() { return pieChartDataValueStep; }
    private final ObjectProperty<String> pieChartDataValueStep = new SimpleObjectProperty<>(this, "pieChartDataValueStep", "value1");
    public final String getPieChartDataValueStep() { return pieChartDataValueStep.get(); }
    public final void setPieChartDataValueStep(String pieChartDataValueStep) { this.pieChartDataValueStep.set(pieChartDataValueStep); }

    public ObjectProperty<Toggle> selectedPieChartDataValueStepProperty() { return selectedPieChartDataValueStep; }
    private final ObjectProperty<Toggle> selectedPieChartDataValueStep = new SimpleObjectProperty<Toggle>(this, "selectedPieChartDataValueStep") {
        @Override
        protected void invalidated() {
            if (get() != null) {
                setPieChartDataValueStep(get().getUserData().toString());
            }
        }
    };
    public final Toggle getSelectedPieChartDataValueStep() { return selectedPieChartDataValueStep.get(); }
    public final void setSelectedPieChartDataValueStep(Toggle selectedPieChartDataValueStep) { this.selectedPieChartDataValueStep.set(selectedPieChartDataValueStep); }

    private int currentLastRecordIndex = 0;

    public void loadRecords() {
        setRecords(
            FXCollections.<RecordModel>observableArrayList(
                new RecordModel("Data 1", 10, 30),
                new RecordModel("Data 2", 38, 22),
                new RecordModel("Data 3", 20, 43),
                new RecordModel("Data 4", 40, 11),
                new RecordModel("Data 5", 30, 25)
            )
        );

        currentLastRecordIndex = 5;
    }

    public void addNewRecord() {
        if (getRecords() == null) { return; }

        getRecords().add(new RecordModel(String.format("Data %1$s", ++currentLastRecordIndex), 0, 0));
    }

    public void removeSelectedRecord() {
        if (getRecords() == null) { return; }
        if (getRecordSelectionModel() == null) { return; }

        getRecords().remove(getRecordSelectionModel().getSelectedItem());
    }
}

PieChart.DataのnameプロパティにはRecordModelのnameプロパティの値を、PieChart.DataのpieValueプロパティには、ラジオボタンで選択されたRecordModelのプロパティの値をバインドできるようにしています。

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

class PieChartDynamicUpdateDemoSceneController {
    public ObjectProperty<PieChartDynamicUpdateDemoModel> modelProperty() { return model; }
    private final ObjectProperty<PieChartDynamicUpdateDemoModel> model = new SimpleObjectProperty<PieChartDynamicUpdateDemoModel>(this, "model") {
        private PieChartDynamicUpdateDemoModel currentModel;
        @Override
        protected void invalidated() {
            PieChartDynamicUpdateDemoSceneController.this.unbind(currentModel);
            currentModel = get();
            PieChartDynamicUpdateDemoSceneController.this.bind(currentModel);
        }
    };
    public final PieChartDynamicUpdateDemoModel getModel() { return model.get(); }
    public final void setModel(PieChartDynamicUpdateDemoModel model) { this.model.set(model); }

    @FXML
    private Scene scene;
    @FXML
    private TableView<RecordModel> recordTableView;
    @FXML
    private Button addRecordButton;
    @FXML
    private Button removeRecordButton;
    @FXML
    private ToggleGroup valuePropertyNameGroup;
    @FXML
    private PieChart recordPieChart;

    private final PieChartDataSource<RecordModel> recordPieChartDataSource = new PieChartDataSource<>();

    public void performOn(Stage stage) {
        stage.setScene(scene);
        stage.setTitle("PieChart Dynamic Update Demo");
        stage.sizeToScene();
        stage.centerOnScreen();
        stage.show();
    }

    public PieChartDynamicUpdateDemoSceneController with(PieChartDynamicUpdateDemoModel model) {
        setModel(model);
        if (model != null) { model.loadRecords(); }

        return this;
    }

    protected void bind(PieChartDynamicUpdateDemoModel model) {
        if (model == null) { return; }

        recordTableView.itemsProperty().bindBidirectional(model.recordsProperty());
        model.recordSelectionModelProperty().bind(recordTableView.selectionModelProperty());

        model.selectedPieChartDataValueStepProperty().bind(valuePropertyNameGroup.selectedToggleProperty());

        recordPieChartDataSource.namePropertyStepProperty().bind(model.pieChartDataNameStepProperty());
        recordPieChartDataSource.pieValuePropertyStepProperty().bind(model.pieChartDataValueStepProperty());
        recordPieChartDataSource.itemsProperty().bind(model.recordsProperty());

        valuePropertyNameGroup.selectToggle(valuePropertyNameGroup.getToggles().get(0));
    }

    protected void unbind(PieChartDynamicUpdateDemoModel model) {
        if (model == null) { return; }

        recordTableView.itemsProperty().unbindBidirectional(model.recordsProperty());
        model.recordSelectionModelProperty().unbind();

        model.selectedPieChartDataValueStepProperty().unbind();

        recordPieChartDataSource.namePropertyStepProperty().unbind();
        recordPieChartDataSource.pieValuePropertyStepProperty().unbind();
        recordPieChartDataSource.itemsProperty().unbind();
    }

    protected void addRecord() {
        if (getModel() == null) { return; }

        getModel().addNewRecord();
        recordTableView.requestFocus();
        recordTableView.getSelectionModel().selectLast();
    }

    protected void removeRecord() {
        if (getModel() == null) { return; }

        getModel().removeSelectedRecord();
        recordTableView.requestFocus();
    }

    @FXML
    protected void initialize() {
        recordPieChartDataSource.setPieChart(recordPieChart);
        addRecordButton.setOnAction(e -> addRecord());
        removeRecordButton.setOnAction(e -> removeRecord());
    }
}

前回作成したPieChartDataSourceに対して、必要なプロパティをバインドして、RecordModelのデータをPieChartに表示できるように設定しています。

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

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

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

次回は、XYChartに対して、同様のユーティリティクラスを考えてみたいと思います。

2013年8月3日土曜日

JavaFX - ChartDataSource (1)

JavaFXでチャートを表示するには、各チャートのモデルに値を設定して、そのモデルをチャートに適用することになります。例えば、PieChartの場合はPieChart.Data、XYChartの場合はXYChart.Dataになります。そのため、チャート以外にも同じデータを、TableViewなど他の場面でも使用するために、独自でモデルを作成した場合でも、チャートに表示するためには、モデルの変換が必要になってきます。そこで、今回はこのモデルの変換を行うユーティリティクラスを考えてみたいと思います。

まずは、PieChartの場合を考えてみます。PieChartのモデルとなるPieChart.Dataには、以下のプロパティが定義されています。

  • ReadOnlyObjectProperty<PieChart> chart
  • StringProperty name
  • DoubleProperty pieValue

そこで、PieChartと独自で作成したモデルから、これらのプロパティを設定したPieChart.Dataに変換することができるようにします。

名称としては、PieChartDataSourceとし、以下のようなプロパティを定義します。

public class PieChartDataSource<T> {
    public ObjectProperty<PieChart> pieChartProperty() { return pieChart; }
    public ObjectProperty<ObservableList<T>> itemsProperty() { return items; }

    public ObjectProperty<String> namePropertyStepProperty() { return namePropertyStep; }
    public ObjectProperty<String> pieValuePropertyStepProperty() { return pieValuePropertyStep; }

    public ObjectProperty<ObservableValueSelection<T, String>> namePropertySelectionProperty() { return namePropertySelection; }
    public ObjectProperty<ObservableValueSelection<T, Double>> pieValuePropertySelectionProperty() { return pieValuePropertySelection; }
 }

PieChartと表示したいモデルのコレクションを指定し、各プロパティが設定されたときに、設定されたモデルをPieChart.Dataに変換し、PieChartに適用します。

指定されたモデルからPieChart.DataのnameとpieValueのプロパティに対する値の取得については、それぞれに対するモデルのプロパティを文字列で、namePropertyStepPropertyとpieValuePropertyStepPropertyに指定するようにします。

namePropertyStepPropertyとpieValuePropertyStepPropertyに設定された文字列から、各プロパティの値を取得する実装をnamePropertySelectionPropertyとpieValuePropertySelectionPropertyに指定します。ObservableValueSelectionは、以下のようなFunctionalInterfaceとして定義します。

@FunctionalInterface
public interface ObservableValueSelection<T, E> {
    ObservableValue<E> select(T root, String step);
}

PieChart.Dataへの変換は、各プロパティの値が変更されたときに行うようにします。

private final ObjectProperty<PieChart> pieChart = new SimpleObjectProperty<PieChart>(this, "pieChart") {
    private PieChart currentPieChart;

    @Override
    protected void invalidated() {
        if (currentPieChart != null) { PieChartDataSource.this.unbindFrom(currentPieChart); }
        currentPieChart = get();
        if (currentPieChart != null) { PieChartDataSource.this.bindTo(currentPieChart); }
    }
};

private final ObjectProperty<ObservableList<T>> items = new SimpleObjectProperty<ObservableList<T>>(this, "items") {
    private ObservableList<T> currentItems;
    @Override
    protected void invalidated() {
        if (currentItems != null) { PieChartDataSource.this.unbind(currentItems); }
        currentItems = get();
        if (currentItems != null) { PieChartDataSource.this.bind(currentItems); }
    }
};

private final ObjectProperty<String> namePropertyStep = new SimpleObjectProperty<String>(this, "namePropertyStep") {
    @Override
    protected void invalidated() {
        rebind();
    }
};

private final ObjectProperty<String> pieValuePropertyStep = new SimpleObjectProperty<String>(this, "pieValuePropertyStep") {
    @Override
    protected void invalidated() {
        rebind();
    }
};

private final ObjectProperty<ObservableValueSelection<T, String>> namePropertySelection = new SimpleObjectProperty<ObservableValueSelection<T, String>>(this, "namePropertySelection", PropertySelector::select) {
    @Override
    protected void invalidated() {
        rebind();
    }
};

private final ObjectProperty<ObservableValueSelection<T, Double>> pieValuePropertySelection = new SimpleObjectProperty<ObservableValueSelection<T, Double>>(this, "pieValuePropertySelection", PropertySelector::select) {
    @Override
    protected void invalidated() {
        rebind();
    }
};

private final ObjectProperty<ObservableList<PieChart.Data>> pieChartData = new SimpleObjectProperty<>(this, "pieChartData", FXCollections.<PieChart.Data>observableArrayList());
private final Map<T, PieChart.Data> itemPieChartDataMap = new HashMap<>();

private final ListChangeListener<T> itemListChangeListener = change -> {
    while (change.next()) {
        if (change.wasPermutated()) {
            Map<Integer, PieChart.Data> permutatedItem = new HashMap<>();
            for (int index = change.getFrom(); index < change.getTo(); ++index) {
                permutatedItem.put(change.getPermutation(index), pieChartData.get().get(index));
            }
            permutatedItem.forEach(pieChartData.get()::set);
        } else if (change.wasUpdated()) {
        } else {
            change.getRemoved().forEach(item -> {
                if (itemPieChartDataMap.containsKey(item)) {
                    pieChartData.get().remove(itemPieChartDataMap.remove(item));
                }
            });
            change.getAddedSubList().forEach(item -> pieChartData.get().add(createPieChartDataFrom(item)));
        }
    }
};

protected void bindTo(PieChart chart) {
    chart.dataProperty().bind(pieChartData);
}

protected void unbindFrom(PieChart chart) {
    chart.dataProperty().unbind();
}

protected void bind(ObservableList<T> items) {
    constructPieChartDataFrom(items);
}

protected void unbind(ObservableList<T> items) {
    pieChartData.get().clear();
    itemPieChartDataMap.clear();
    if (items != null) { items.removeListener(itemListChangeListener); }
}

protected void rebind() {
    unbind(getItems());
    bind(getItems());
}

protected void constructPieChartDataFrom(ObservableList<T> items) {
    if (items == null) { return; }
    if (getNamePropertyStep() == null || getPieValuePropertyStep() == null) { return; }

    for (T item : items) {
        pieChartData.get().add(createPieChartDataFrom(item));
    }

    items.addListener(itemListChangeListener);
}

private PieChart.Data createPieChartDataFrom(T item) {
    PieChart.Data data = new PieChart.Data("", 0);

    data.nameProperty().bind(getNamePropertySelection().select(item, getNamePropertyStep()));
    data.pieValueProperty().bind(getPieValuePropertySelection().select(item, getPieValuePropertyStep()));

    itemPieChartDataMap.put(item, data);

    return data;
}

使い方としては、チャートに表示したい以下のようなモデルのコレクションを保持するモデルを考えます。

interface RecordModel {
    StringProperty nameProperty();
    IntegerProperty valueProperty();
}

この場合、以下のように設定します。

recordPieChartDataSource = new PieChartDataSource<>();
recordPieChartDataSource.setPieChart(recordPieChart);

recordPieChartDataSource.setNamePropertyStep("name");
recordPieChartDataSource.setPieValuePropertyStep("value");
recordPieChartDataSource.itemsProperty().bind(model.recordsProperty());

今回は、PieChartに対して、PieChart.Dataへ変換するユーティリティクラスについて考えてみました。次回は、このクラスを用いて、TableViewに表示されたデータをPieChartに表示し、TableViewで値を変更するとPieChartにもその変更が適用されるようなアプリケーションを作成してみたいと思います。

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を動的に作成するユーティリティクラスについて考えてみました。次回は、このクラスを実際に使ってみたいと思います。

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で、複数の列に対してヘッダを簡単に表示することができますので、いろいろと活用していきたいと思います。

2013年5月30日木曜日

JavaFX - TableView : Simple CRUD

TableViewを利用して、基本的なデータの表示・編集・追加・削除を行なってみたいと思います。

TableViewへのデータの表示は、itemsPropertyに表示したいモデルのObservableListを設定し、データの追加・削除についてはそのObservableListにモデルを追加・削除することによって行います。

では、実際に以下のようなものを作成してみたいと思います。

Name, Gender, Email, Ageを持つUserを表示しています。[+]ボタンで新規にUserを追加し、[-]ボタンで選択されているUserを削除します。

まずは、表示するUserのモデルを以下のように定義します。

public interface UserModel {
    StringProperty nameProperty();
    ObjectProperty<GenderModel> genderProperty();
    StringProperty emailProperty();
    IntegerProperty ageProperty();
}

GenderModelは以下のようになります。

public enum GenderModel {
    MALE,
    FEMALE;
}

次に、このUserModelを表示するViewに対するモデルを以下のように定義します。

public interface UserListModel {
    ObjectProperty<ObservableList<UserModel>> usersProperty();
    ObjectProperty<TableView.TableViewSelectionModel<UserModel>> userSelectionModelProperty();

    void loadUsers();
    void addNewUser();
    void removeSelectedUser();
}

UserModelの読み込み・追加・削除を行うメソッドを定義しています。また、TableViewのselectionModelプロパティからTableViewで選択されている状態を取得したいので、それをバインドして取得できるようにuserSelectionModelプロパティを定義しています。

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

<fx:root xmlns:fx="http://javafx.com/fxml" type="javafx.scene.layout.HBox">
    <TableView fx:id="userTableView" editable="true">
        <columns>
            <TableColumn fx:id="nameTableColumn" text="Name" prefWidth="100"/>
            <TableColumn fx:id="genderTableColumn" text="Gender" prefWidth="80"/>
            <TableColumn fx:id="emailTableColumn" text="Email" prefWidth="160"/>
            <TableColumn fx:id="ageTableColumn" text="Age" prefWidth="60"/>
        </columns>
    </TableView>

    <VBox id="actionButtonRegion">
        <Button fx:id="addUserButton" text="+" prefWidth="35"/>
        <Button fx:id="removeUserButton" text="-" prefWidth="35"/>
    </VBox>
</fx:root>

UserModelを表示するTableViewと、UserModelの追加・削除のためのボタンを定義しています。

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

public class UserListView extends HBox {
    public ObjectProperty<UserListModel> modelProperty() { return model; }
    private final ObjectProperty<UserListModel> model = new SimpleObjectProperty<UserListModel>(this, "model") {
        private UserListModel currentModel;

        @Override
        protected void invalidated() {
            UserListView.this.unbind(currentModel);
            currentModel = get();
            UserListView.this.bind(currentModel);
        }
    };
    public final UserListModel getModel() { return model.get(); }
    public final void setModel(UserListModel model) { this.model.set(model); }

    @FXML
    private TableView<UserModel> userTableView;
    @FXML
    private TableColumn<UserModel, String> nameTableColumn;
    @FXML
    private TableColumn<UserModel, GenderModel> genderTableColumn;
    @FXML
    private TableColumn<UserModel, String> emailTableColumn;
    @FXML
    private TableColumn<UserModel, Integer> ageTableColumn;
    @FXML
    private Button addUserButton;
    @FXML
    private Button removeUserButton;

    public UserListView() {
        getStyleClass().add("user-list-view");
        initializeComponent();
    }

    protected void initializeComponent() {
        FXController.of(this).fromDefaultLocation().load();
    }

    protected void bind(UserListModel model) {
        if (model == null) { return; }

        userTableView.itemsProperty().bindBidirectional(model.usersProperty());
        model.userSelectionModelProperty().bind(userTableView.selectionModelProperty());
    }

    protected void unbind(UserListModel model) {
        if (model == null) { return; }

        userTableView.itemsProperty().unbindBidirectional(model.usersProperty());
        model.userSelectionModelProperty().unbind();
    }

    protected void addUser() {
        if (getModel() == null) { return; }

        getModel().addNewUser();
        userTableView.requestFocus();
    }

    protected void removeUser() {
        if (getModel() == null) { return; }

        getModel().removeSelectedUser();
        userTableView.requestFocus();
    }

    @FXML
    protected void initialize() {
        nameTableColumn.setCellValueFactory(new PropertyValueFactory<UserModel, String>("name"));
        nameTableColumn.setCellFactory(TextFieldTableCell.<UserModel>forTableColumn());

        genderTableColumn.setCellValueFactory(new PropertyValueFactory<UserModel, GenderModel>("gender"));
        genderTableColumn.setCellFactory(ChoiceBoxTableCell.<UserModel, GenderModel>forTableColumn(GenderModel.values()));

        emailTableColumn.setCellValueFactory(new PropertyValueFactory<UserModel, String>("email"));
        emailTableColumn.setCellFactory(TextFieldTableCell.<UserModel>forTableColumn());

        ageTableColumn.setCellValueFactory(new PropertyValueFactory<UserModel, Integer>("age"));
        ageTableColumn.setCellFactory(TextFieldTableCell.<UserModel, Integer>forTableColumn(new IntegerStringConverter()));

        addUserButton.setOnAction(new EventHandler<ActionEvent>() {
            @Override
            public void handle(ActionEvent event) {
                addUser();
            }
        });

        removeUserButton.setOnAction(new EventHandler<ActionEvent>() {
            @Override
            public void handle(ActionEvent event) {
                removeUser();
            }
        });
    }
}

UserListModelを設定したときに、UserListModelで定義されているプロパティとTableViewのプロパティをバインドしています。[+]ボタンが押下されたときに、UserListModelのaddNewUserメソッドを呼び出してUserModelを追加し、[-]ボタンが押下されたときに、UserListModelのremoveSelectedUserメソッドを呼び出して選択されているUserModelを削除するようにしています。

以上で準備ができましたので、このUserListViewを表示していきます。

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

public interface TableViewSimpleCRUDDemoModel {
    ObjectProperty<UserListModel> userListModelProperty();

    void initialize();
}

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

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

    <StackPane>
        <UserListView fx:id="userListView"/>
    </StackPane>
</Scene>

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

public class TableViewSimpleCRUDDemoSceneController {
    public ObjectProperty<TableViewSimpleCRUDDemoModel> modelProperty() { return model; }
    private final ObjectProperty<TableViewSimpleCRUDDemoModel> model = new SimpleObjectProperty<TableViewSimpleCRUDDemoModel>(this, "model") {
        private TableViewSimpleCRUDDemoModel currentModel;
        @Override
        protected void invalidated() {
            TableViewSimpleCRUDDemoSceneController.this.unbind(currentModel);
            currentModel = get();
            TableViewSimpleCRUDDemoSceneController.this.bind(currentModel);
        }
    };
    public final TableViewSimpleCRUDDemoModel getModel() { return model.get(); }
    public final void setModel(TableViewSimpleCRUDDemoModel model) { this.model.set(model); }

    @FXML
    private Scene scene;
    @FXML
    private UserListView userListView;

    public void performOn(Stage stage) {
        stage.setScene(scene);
        stage.setTitle("Simple CRUD for Table View Demo");
        stage.sizeToScene();
        stage.centerOnScreen();
        stage.show();
    }

    public TableViewSimpleCRUDDemoSceneController with(TableViewSimpleCRUDDemoModel model) {
        setModel(model);
        if (model != null) { model.initialize(); }

        return this;
    }

    protected void bind(TableViewSimpleCRUDDemoModel model) {
        if (model == null) { return; }

        userListView.modelProperty().bindBidirectional(model.userListModelProperty());
    }

    protected void unbind(TableViewSimpleCRUDDemoModel model) {
        if (model == null) { return; }

        userListView.modelProperty().unbindBidirectional(model.userListModelProperty());
    }
}

あとは、各モデルのデフォルトの実装を行っておきます。

まずは、UserModelのデフォルトの実装を以下のようにします。

public class DefaultUserModel implements UserModel {
    @Override
    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); }

    @Override
    public ObjectProperty<GenderModel> genderProperty() { return gender; }
    private final ObjectProperty<GenderModel> gender = new SimpleObjectProperty<>(this, "gender");
    public final GenderModel getGender() { return gender.get(); }
    public final void setGender(GenderModel gender) { this.gender.set(gender); }

    @Override
    public StringProperty emailProperty() { return email; }
    private final StringProperty email = new SimpleStringProperty(this, "email");
    public final String getEmail() { return email.get(); }
    public final void setEmail(String email) { this.email.set(email); }

    @Override
    public IntegerProperty ageProperty() { return ageProperty; }
    private final IntegerProperty ageProperty = new SimpleIntegerProperty(this, "ageProperty");
    public final int getAge() { return ageProperty.get(); }
    public final void setAge(int ageProperty) { this.ageProperty.set(ageProperty); }

    protected DefaultUserModel(String name, GenderModel gender, String email, int age) {
        setName(name);
        setGender(gender);
        setEmail(email);
        setAge(age);
    }

    public static DefaultUserModel asMale(String name, String email, int age) {
        return new DefaultUserModel(name, GenderModel.MALE, email, age);
    }

    public static DefaultUserModel asFemale(String name, String email, int age) {
        return new DefaultUserModel(name, GenderModel.FEMALE, email, age);
    }

    public static DefaultUserModel newUser() {
        return new DefaultUserModel("Unknown", GenderModel.MALE, "", 20);
    }
}

次に、UserListModelのデフォルトの実装を以下のようにします。

public class DefaultUserListModel implements UserListModel {
    @Override
    public ObjectProperty<ObservableList<UserModel>> usersProperty() { return users; }
    private final ObjectProperty<ObservableList<UserModel>> users = new SimpleObjectProperty<>(this, "users");
    public final ObservableList<UserModel> getUsers() { return users.get(); }
    public final void setUsers(ObservableList<UserModel> users) { this.users.set(users); }

    @Override
    public ObjectProperty<TableView.TableViewSelectionModel<UserModel>> userSelectionModelProperty() { return userSelectionModel; }
    private final ObjectProperty<TableView.TableViewSelectionModel<UserModel>> userSelectionModel = new SimpleObjectProperty<>(this, "userSelectionModel");
    public final TableView.TableViewSelectionModel<UserModel> getUserSelectionModel() { return userSelectionModel.get(); }
    public final void setUserSelectionModel(TableView.TableViewSelectionModel<UserModel> userSelectionModel) { this.userSelectionModel.set(userSelectionModel); }

    @Override
    public void loadUsers() {
        setUsers(
            FXCollections.<UserModel>observableArrayList(
                DefaultUserModel.asMale("User 1", "user1@email.com", 25),
                DefaultUserModel.asFemale("User 2", "user2@email.com", 28),
                DefaultUserModel.asFemale("User 3", "user3@email.com", 24),
                DefaultUserModel.asMale("User 4", "user4@email.com", 21),
                DefaultUserModel.asFemale("User 5", "user5@email.com", 26)
            )
        );
    }

    @Override
    public void addNewUser() {
        if (getUsers() == null) { setUsers(FXCollections.<UserModel>observableArrayList()); }

        getUsers().add(DefaultUserModel.newUser());
        if (getUserSelectionModel() != null) { getUserSelectionModel().selectLast(); }
    }

    @Override
    public void removeSelectedUser() {
        if (getUsers() == null) { return; }
        if (getUserSelectionModel() == null) { return; }

        getUsers().remove(getUserSelectionModel().getSelectedItem());
    }
}

loadUsersでは、サンプルデータとして5件のUserModelを設定しています。

最後に、TableViewSimpleCRUDDemoModelのデフォルトの実装を以下のようにします。

public class DefaultTableViewSimpleCRUDDemoModel implements TableViewSimpleCRUDDemoModel {
    @Override
    public ObjectProperty<UserListModel> userListModelProperty() { return userListModel; }
    private final ObjectProperty<UserListModel> userListModel = new SimpleObjectProperty<>(this, "userListModel");
    public final UserListModel getUserListModel() { return userListModel.get(); }
    public final void setUserListModel(UserListModel userListModel) { this.userListModel.set(userListModel); }

    @Override
    public void initialize() {
        setUserListModel(new DefaultUserListModel());
        getUserListModel().loadUsers();
    }
}

以上ですべての実装が完了しましたので、Applicationクラスを以下のようにして実行します。

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

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

TableViewを利用して、基本的なデータの表示・編集・追加・削除を行なってみました。データの操作はモデル側で行い、Viewへの表示については、モデルのプロパティをViewのプロパティにバインドすることで行うことになります。

2013年5月19日日曜日

JavaFX - Custom Editable ListCell (3)

前回は、FXMLで定義したViewを使用して、ListCellに表示する方法をみていきました。今回は、それを再利用できるようなクラスとして作成してみたいと思います。

まずは、ListCellに表示するViewのインターフェイスを以下のように定義します。

public interface GraphicListCellView<T> {
    ObjectProperty<T> modelProperty();

    void appearOn(ListCell<T> cell);

    <E extends Event> void addEventHandler(EventType<E> eventType, EventHandler<? super E> eventHandler);
    <E extends Event> void removeEventHandler(EventType<E> eventType, EventHandler<? super E> eventHandler);
    void requestFocus();

    public static class EditEvent<T> extends Event {
        private static final EventType EDIT_ANY = new EventType(Event.ANY, "EDIT");
        private static final EventType EDIT_CANCEL = new EventType(editAny(), "EDIT_CANCEL");
        private static final EventType EDIT_COMMIT = new EventType(editAny(), "EDIT_COMMIT");

        public static <T> EventType<EditEvent<T>> editAny() { return EDIT_ANY; }
        public static <T> EventType<EditEvent<T>> editCancel() { return EDIT_CANCEL; }
        public static <T> EventType<EditEvent<T>> editCommit() { return EDIT_COMMIT; }

        private final T newValue;

        public EditEvent(EventType<? extends EditEvent> eventType, T newValue) {
            super(eventType);

            this.newValue = newValue;
        }

        public T getNewValue() {
            return newValue;
        }
    }
}

Viewに表示するモデルのプロパティとListCellに表示するためのメソッドを定義しています。また、編集時のイベントの登録と編集時にフォーカスを要求できるように、Nodeに定義されているメソッドも定義しています。さらに、編集時のイベントも定義しています。

次に、表示用のViewのモデルの内容と、編集用のViewのモデルの内容の変換を行うために、以下のインターフェイスを定義しておきます。

public interface GraphicListCellModelConverter<T> {
    void mergeEditModel(T editModel, T model);
    T toEditModel(T model);
}

mergeEditModelメソッドでは、editModelの内容をmodelの内容に適用し、toEditModelメソッドでは、modelの内容をコピー返し、編集用のViewのモデルに設定できるようにします。

以上で、準備ができましたので、ListCellを継承したクラスを作成していきます。名称は、GraphicListCellとしてみます。

public class GraphicListCell<T> extends ListCell<T> {
}

表示用のViewと編集用のViewを生成するための、Callbackインターフェイスを実装したファクトリを指定してインスタンスを生成できるように、以下のようなファクトリメソッドを定義します。

public static <T> Callback<ListView<T>, ListCell<T>> forListView(Callback<GraphicListCell<T>, GraphicListCellView<T>> viewFactory) {
    return forListView(viewFactory, null);
}

public static <T> Callback<ListView<T>, ListCell<T>> forListView(
    Callback<GraphicListCell<T>, GraphicListCellView<T>> viewFactory,
    Callback<GraphicListCell<T>, GraphicListCellView<T>> editViewFactory
) {
    return forListView(viewFactory, editViewFactory, null);
}

public static <T> Callback<ListView<T>, ListCell<T>> forListView(
    final Callback<GraphicListCell<T>, GraphicListCellView<T>> viewFactory,
    final Callback<GraphicListCell<T>, GraphicListCellView<T>> editViewFactory,
    final GraphicListCellModelConverter<T> converter
) {
    return new Callback<ListView<T>, ListCell<T>>() {
        @Override
        public ListCell<T> call(ListView<T> listView) {
            return new GraphicListCell<>(viewFactory, editViewFactory, converter);
        }
    };
}

protected GraphicListCell(
    Callback<GraphicListCell<T>, GraphicListCellView<T>> viewFactory,
    Callback<GraphicListCell<T>,GraphicListCellView<T>> editViewFactory,
    GraphicListCellModelConverter<T> converter
) {
    setViewFactory(Objects.requireNonNull(viewFactory));
    setEditViewFactory(editViewFactory);
    setConverter(converter);

    getStyleClass().add("graphic-list-cell");
    setContentDisplay(ContentDisplay.GRAPHIC_ONLY);
}

public ObjectProperty<Callback<GraphicListCell<T>, GraphicListCellView<T>>> viewFactoryProperty() { return viewFactory; }
private final ObjectProperty<Callback<GraphicListCell<T>, GraphicListCellView<T>>> viewFactory = new SimpleObjectProperty<Callback<GraphicListCell<T>, GraphicListCellView<T>>>(this, "viewFactory");
public final Callback<GraphicListCell<T>, GraphicListCellView<T>> getViewFactory() { return viewFactory.get(); }
public final void setViewFactory(Callback<GraphicListCell<T>, GraphicListCellView<T>> viewFactory) { this.viewFactory.set(viewFactory); }

public ObjectProperty<Callback<GraphicListCell<T>, GraphicListCellView<T>>> editViewFactoryProperty() { return editViewFactory; }
private final ObjectProperty<Callback<GraphicListCell<T>, GraphicListCellView<T>>> editViewFactory = new SimpleObjectProperty<Callback<GraphicListCell<T>, GraphicListCellView<T>>>(this, "editViewFactory");
public final Callback<GraphicListCell<T>, GraphicListCellView<T>> getEditViewFactory() { return editViewFactory.get(); }
public final void setEditViewFactory(Callback<GraphicListCell<T>, GraphicListCellView<T>> editViewFactory) { this.editViewFactory.set(editViewFactory); }

public ObjectProperty<GraphicListCellModelConverter<T>> converterProperty() { return converter; }
private final ObjectProperty<GraphicListCellModelConverter<T>> converter = new SimpleObjectProperty<>(this, "converter");
public final GraphicListCellModelConverter<T> getConverter() { return converter.get(); }
public final void setConverter(GraphicListCellModelConverter<T> converter) { this.converter.set(converter); }

private GraphicListCellView<T> view;
private GraphicListCellView<T> editView;

ViewのファクトリとGraphicListModelConverterについては、プロパティとして定義しています。

まず、updateItemメソッドをオーバーライドします。

@Override
protected void updateItem(T item, boolean empty) {
    super.updateItem(item, empty);

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

    if (view == null) { view = getViewFactory().call(this); }
    view.modelProperty().set(item);
    view.appearOn(this);
}

Viewの生成には、コンストラクタで指定したファクトリを利用して行い、GraphicListCellViewインターフェイスのメソッドを利用して必要な処理を行っています。

次に、startEditメソッドをオーバーライドします。

@Override
public void startEdit() {
    if (!isEditable()) { return; }
    if (!getListView().isEditable()) { return; }
    if (editView == null && getEditViewFactory() == null) { return; }

    super.startEdit();

    if (editView == null) { editView = createEditView(); }
    editView.modelProperty().set(
        getConverter() == null ? view.modelProperty().get() : getConverter().toEditModel(view.modelProperty().get())
    );
    editView.appearOn(this);
    Platform.runLater(new Runnable() {
        @Override
        public void run() {
            editView.requestFocus();
        }
    });
}

protected GraphicListCellView<T> createEditView() {
    GraphicListCellView<T> editView = getEditViewFactory().call(this);
    editView.addEventHandler(GraphicListCellView.EditEvent.<T>editCommit(), new EventHandler<GraphicListCellView.EditEvent<T>>() {
        @Override
        public void handle(GraphicListCellView.EditEvent<T> event) {
            GraphicListCell.this.commitEdit(event.getNewValue());
        }
    });
    editView.addEventHandler(GraphicListCellView.EditEvent.<T>editCancel(), new EventHandler<GraphicListCellView.EditEvent<T>>() {
        @Override
        public void handle(GraphicListCellView.EditEvent<T> event) {
            GraphicListCell.this.cancelEdit();
        }
    });
    return editView;
}

Viewの生成には、コンストラクタで指定したファクトリを利用して行い、GraphicListCellViewインターフェイスのメソッドを利用して必要な処理を行っています。また、GraphicListCellModelConverterが設定されていない場合は、表示用のViewのモデルをそのまま編集用のViewのモデルに設定していますので、編集内容がそのまま表示内容に反映されることになります。

次に、cancelEditメソッドをオーバーライドします。

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

    super.cancelEdit();

    view.appearOn(this);
}

GraphicListCellViewインターフェイスのメソッドを利用して必要な処理を行なっています。

最後に、commitEditメソッドをオーバーライドします。

@Override
public void commitEdit(T newValue) {
    if (!isEditing()) { return; }

    if (getConverter() != null) { getConverter().mergeEditModel(newValue, view.modelProperty().get()); }

    super.commitEdit(newValue);
}

GraphicListCellModelConverterが設定されている場合に、更新された内容を表示用のViewのモデルに反映するようにしています。

以上で、完了になります。

では、作成したGraphicListCellを前回の内容に適用してみたいと思います。

まず、前回作成したUserListCellは不要になります。また、UserViewについては、以下のように、GraphicListCellViewを実装し、appearOnメソッドの実装を追加します。

public class UserView extends GridPane implements GraphicListCellView<UserModel> {
    // 前回と同じ内容

    @Override
    public void appearOn(ListCell<UserModel> cell) {
        Objects.requireNonNull(cell).setGraphic(this);
    }
}

UserEditViewについても同様に、GraphicListCellViewを実装し、appearOnメソッドの実装を追加します。また、EditEventの定義については、GraphicListCellViewで行なっていますので、不要となりますので削除します。

public class UserEditView extends GridPane implements GraphicListCellView<UserModel> {
    // 前回と同じ内容で、EditEventの定義を削除

    @Override
    public void appearOn(ListCell<UserModel> cell) {
        Objects.requireNonNull(cell).setGraphic(this);
    }
}

また、EditEventの定義が変わりましたので、イベントの発生部分を以下のように修正しておきます。

applyButton.setOnAction(new EventHandler<ActionEvent>() {
    @Override
    public void handle(ActionEvent actionEvent) {
        fireEvent(new EditEvent<>(EditEvent.editCommit(), getModel()));
    }
});

cancelButton.setOnAction(new EventHandler<ActionEvent>() {
    @Override
    public void handle(ActionEvent actionEvent) {
        fireEvent(new EditEvent<>(EditEvent.editCancel(), null));
    }
});

最後に、ListViewのcellFactoryの設定部分をGraphicListCellを用いて、以下のようにします。

userListView.setCellFactory(
    GraphicListCell.<UserModel>forListView(
        new Callback<GraphicListCell<UserModel>, GraphicListCellView<UserModel>>() {
            @Override
            public GraphicListCellView<UserModel> call(GraphicListCell<UserModel> cell) {
                return new UserView();
            }
        },
        new Callback<GraphicListCell<UserModel>, GraphicListCellView<UserModel>>() {
            @Override
            public GraphicListCellView<UserModel> call(GraphicListCell<UserModel> cell) {
                return new UserEditView();
            }
        },
        new GraphicListCellModelConverter<UserModel>() {
            @Override
            public void mergeEditModel(UserModel editModel, UserModel model) {
                model.merge(editModel);
            }

            @Override
            public UserModel toEditModel(UserModel model) {
                return model.clone();
            }
        })
);

以上で、修正は完了になります。あとは、前回と同じ内容で実行することができます。

FXMLで定義したViewを使用して、ListCellに表示するための再利用可能なクラスを作成してみました。TableCellやTreeCellについても、同様になりますので、このようなクラスを作成しておくと、ListView・TableView・TreeViewに表示する内容をFXMLで定義できますので、簡単に表示内容をいろいろとカスタマイズすることができるようになります。

2013年5月14日火曜日

JavaFX - Custom Editable ListCell (2)

前回は、ListCellを継承したクラスの作成方法についてみていきました。今回は、実際に以下のようなものを作成してみたいと思います。

まずは、リストに表示するUserModelを以下のように定義します。

interface UserModel extends Cloneable {
    StringProperty nameProperty();
    StringProperty emailProperty();
    ObjectProperty<GenderModel> genderProperty();

    UserModel clone();
    void merge(UserModel user);
}

Userの名前とメールアドレスと性別を表すプロパティを定義しています。cloneメソッドは、編集モードになったときに現在の表示内容をコピーして、編集用のビューにその内容を渡すときに利用し、mergeメソッドは、編集モードでコミットされた内容を現在の表示内容に適用するときに利用します。性別を表すGenderModelについては、以下のようになります。

enum GenderModel {
    MALE {
        @Override
        protected String getImageName() {
            return "Person_Male.png";
        }
    },
    FEMALE {
        @Override
        protected String getImageName() {
            return "Person_Female.png";
        }
    };

    public ReadOnlyObjectProperty<Image> imageProperty() {
        if (image == null) {
            image = new ReadOnlyObjectWrapper<>(this, "image", new Image(getClass().getResourceAsStream(getImageName())));
        }
        return image.getReadOnlyProperty();
    }
    private ReadOnlyObjectWrapper<Image> image;
    public final Image getImage() { return imageProperty().get(); }
    protected final void setImage(Image image) { this.image.set(image); }

    protected abstract String getImageName();
}

性別を表示するときの画像のプロパティを定義しています。

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

<fx:root xmlns:fx="http://javafx.com/fxml"
         type="javafx.scene.layout.GridPane">
    <ImageView fx:id="genderImageView"
               GridPane.rowIndex="0" GridPane.columnIndex="0"
               GridPane.rowSpan="2"
               fitWidth="80" fitHeight="80"/>

    <Label fx:id="nameLabel"
           GridPane.rowIndex="0" GridPane.columnIndex="1"/>

    <Label fx:id="emailLabel"
           GridPane.rowIndex="1" GridPane.columnIndex="1"/>
</fx:root>
class UserView extends GridPane {
    public ObjectProperty<UserModel> modelProperty() { return model; }
    private final ObjectProperty<UserModel> model = new SimpleObjectProperty<UserModel>(this, "model") {
        private UserModel currentModel;

        @Override
        protected void invalidated() {
            if (currentModel != null) { UserView.this.unbind(currentModel); }
            currentModel = get();
            if (currentModel != null) { UserView.this.bind(currentModel); }
        }
    };
    public final UserModel getModel() { return model.get(); }
    public final void setModel(UserModel model) { this.model.set(model); }

    @FXML
    private ImageView genderImageView;
    @FXML
    private Label nameLabel;
    @FXML
    private Label emailLabel;

    public UserView() {
        initializeComponent();
        getStyleClass().add("user-view");
    }

    protected void initializeComponent() {
        FXController.of(this).fromDefaultLocation().load();
    }

    protected void bind(UserModel model) {
        genderImageView.imageProperty().bind(Bindings.<Image>select(modelProperty(), "gender", "image"));
        nameLabel.textProperty().bind(model.nameProperty());
        emailLabel.textProperty().bind(model.emailProperty());
    }

    protected void unbind(UserModel model) {
        genderImageView.imageProperty().unbind();
        nameLabel.textProperty().unbind();
        emailLabel.textProperty().unbind();
    }
}

UserModelが設定されたときに、UserModelのプロパティを各コントロールのプロパティにバインドしています。また、StyleClassを設定して、CSSファイルでスタイルを設定できるようにしています。FXControllerについては、こちらを参照してください。また、UserModelを編集するViewとコントローラを以下のように定義します。

<fx:root xmlns:fx="http://javafx.com/fxml"
         type="javafx.scene.layout.GridPane">
    <Label GridPane.rowIndex="0" GridPane.columnIndex="0"
           text="Name:"/>
    <TextField fx:id="nameTextField"
               GridPane.rowIndex="0" GridPane.columnIndex="1"/>

    <Label GridPane.rowIndex="1" GridPane.columnIndex="0"
           text="Email:"/>
    <TextField fx:id="emailTextField"
               GridPane.rowIndex="1" GridPane.columnIndex="1"/>

    <Label GridPane.rowIndex="2" GridPane.columnIndex="0"
           text="Gender:"/>
    <ComboBox fx:id="genderComboBox"
              GridPane.rowIndex="2" GridPane.columnIndex="1"/>

    <HBox id="actionRegion"
          GridPane.rowIndex="3" GridPane.columnSpan="2">
        <Button fx:id="applyButton"
                text="Apply"/>
        <Button fx:id="cancelButton"
                text="Cancel"/>
    </HBox>
</fx:root>
class UserEditView extends GridPane {
    public static final EventType<EditEvent> EDIT_ANY = new EventType<>(Event.ANY, "EDIT");
    public static final EventType<EditEvent> EDIT_CANCEL = new EventType<>(EDIT_ANY, "EDIT_CANCEL");
    public static final EventType<EditEvent> EDIT_COMMIT = new EventType<>(EDIT_ANY, "EDIT_COMMIT");

    public ObjectProperty<UserModel> modelProperty() { return model; }
    private final ObjectProperty<UserModel> model = new SimpleObjectProperty<UserModel>(this, "model") {
        private UserModel currentModel;

        @Override
        protected void invalidated() {
            if (currentModel != null) { UserEditView.this.unbind(currentModel); }
            currentModel = get();
            if (currentModel != null) { UserEditView.this.bind(currentModel); }
        }
    };
    public final UserModel getModel() { return model.get(); }
    public final void setModel(UserModel model) { this.model.set(model); }

    @FXML
    private TextField nameTextField;
    @FXML
    private TextField emailTextField;
    @FXML
    private ComboBox<GenderModel> genderComboBox;
    @FXML
    private Button applyButton;
    @FXML
    private Button cancelButton;

    public UserEditView() {
        initializeComponent();
        getStyleClass().add("user-edit-view");
    }

    protected void initializeComponent() {
        FXController.of(this).fromDefaultLocation().load();
    }

    protected void bind(UserModel model) {
        nameTextField.textProperty().bindBidirectional(model.nameProperty());
        emailTextField.textProperty().bindBidirectional(model.emailProperty());
        genderComboBox.getSelectionModel().select(model.genderProperty().get());
        model.genderProperty().bind(genderComboBox.getSelectionModel().selectedItemProperty());
    }

    protected void unbind(UserModel model) {
        nameTextField.textProperty().unbindBidirectional(model.nameProperty());
        emailTextField.textProperty().unbindBidirectional(model.emailProperty());
        model.genderProperty().unbind();
    }

    @FXML
    protected void initialize() {
        genderComboBox.getItems().addAll(GenderModel.values());

        applyButton.setOnAction(new EventHandler<ActionEvent>() {
            @Override
            public void handle(ActionEvent actionEvent) {
                fireEvent(new EditEvent(EDIT_COMMIT, getModel()));
            }
        });
        cancelButton.setOnAction(new EventHandler<ActionEvent>() {
            @Override
            public void handle(ActionEvent actionEvent) {
                fireEvent(new EditEvent(EDIT_CANCEL, null));
            }
        });
    }

    @Override
    public void requestFocus() {
        nameTextField.requestFocus();
        nameTextField.selectAll();
    }

    public static class EditEvent extends Event {
        private final UserModel newValue;

        public EditEvent(EventType<? extends EditEvent> eventType, UserModel newValue) {
            super(eventType);

            this.newValue = newValue;
        }

        public UserModel getNewValue() {
            return newValue;
        }
    }
}

基本的には、表示用のコントローラと同じですが、編集がコミットされた場合やキャンセルされた場合に、イベントを発生するようにしています。

以上で、表示用のViewと編集用のViewができましたので、これらを利用してUserModelを表示するListCellを継承したクラスを作成してみたいと思います。以下のようになります。

class UserListCell extends ListCell<UserModel> {
    private UserView userView;
    private UserEditView userEditView;

    public UserListCell() {
        getStyleClass().add("user-list-cell");
        setContentDisplay(ContentDisplay.GRAPHIC_ONLY);
    }

    public static Callback<ListView<UserModel>, ListCell<UserModel>> forListView() {
        return new Callback<ListView<UserModel>, ListCell<UserModel>>() {
            @Override
            public ListCell<UserModel> call(ListView<UserModel> userModelListView) {
                return new UserListCell();
            }
        };
    }

    @Override
    protected void updateItem(UserModel user, boolean empty) {
        super.updateItem(user, empty);

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

        if (userView == null) { userView = new UserView(); }
        userView.setModel(user);
        setGraphic(userView);
    }

    @Override
    public void startEdit() {
        if (!isEditable()) { return; }
        if (!getListView().isEditable()) { return; }

        super.startEdit();

        if (userEditView == null) { userEditView = createUserEditView(); }
        userEditView.setModel(userView.getModel().clone());
        setGraphic(userEditView);
        Platform.runLater(new Runnable() {
            @Override
            public void run() {
                userEditView.requestFocus();
            }
        });
    }

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

        super.cancelEdit();

        setGraphic(userView);
    }

    @Override
    public void commitEdit(UserModel newUser) {
        if (!isEditing()) { return; }

        userView.getModel().merge(newUser);

        super.commitEdit(newUser);
    }

    private UserEditView createUserEditView() {
        UserEditView userEditView = new UserEditView();
        userEditView.addEventHandler(UserEditView.EDIT_COMMIT, new EventHandler<UserEditView.EditEvent>() {
            @Override
            public void handle(UserEditView.EditEvent event) {
                UserListCell.this.commitEdit(event.getNewValue());
            }
        });
        userEditView.addEventHandler(UserEditView.EDIT_CANCEL, new EventHandler<UserEditView.EditEvent>() {
            @Override
            public void handle(UserEditView.EditEvent event) {
                UserListCell.this.cancelEdit();
            }
        });
        return userEditView;
    }
}

updateItemメソッド、startEditメソッド、cancelEditメソッド、commitEditメソッドで、それぞれ必要な処理を実装しています。また、Callbackインターフェイスの実装を取得するファクトリメソッドも用意しています。

以上で、UserModelを表示するListCellの実装が完了しましたので、これらを利用してみます。

まずは、UserModelのデフォルトの実装を以下のように実装します。

class DefaultUserModel implements UserModel {
    @Override
    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); }

    @Override
    public StringProperty emailProperty() { return email; }
    private final StringProperty email = new SimpleStringProperty(this, "email");
    public final String getEmail() { return email.get(); }
    public final void setEmail(String email) { this.email.set(email); }

    @Override
    public ObjectProperty<GenderModel> genderProperty() { return gender; }
    private final ObjectProperty<GenderModel> gender = new SimpleObjectProperty<>(this, "gender");
    public final GenderModel getGender() { return gender.get(); }
    public final void setGender(GenderModel gender) { this.gender.set(gender); }

    protected DefaultUserModel(String name, String email, GenderModel gender) {
        setName(name);
        setEmail(email);
        setGender(gender);
    }

    public static DefaultUserModel asMale(String name, String email) {
        return new DefaultUserModel(name, email, GenderModel.MALE);
    }

    public static DefaultUserModel asFemale(String name, String email) {
        return new DefaultUserModel(name, email, GenderModel.FEMALE);
    }

    @Override
    public UserModel clone() {
        return new DefaultUserModel(getName(), getEmail(), getGender());
    }

    @Override
    public void merge(UserModel user) {
        setName(user.nameProperty().get());
        setEmail(user.emailProperty().get());
        setGender(user.genderProperty().get());
    }
}

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

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

    <StackPane>
        <ListView fx:id="userListView"
                  editable="true"/>
    </StackPane>
</Scene>

このシーンに対するモデルを以下のように定義します。

interface CustomListCellDemoModel {
    ReadOnlyObjectProperty<ObservableList<UserModel>> usersProperty();

    void loadUsers();
}

このシーンのコントローラを以下のようにします。

class CustomListCellDemoSceneController {
    public ObjectProperty<CustomListCellDemoModel> modelProperty() { return model; }
    private final ObjectProperty<CustomListCellDemoModel> model = new SimpleObjectProperty<CustomListCellDemoModel>(this, "model") {
        private CustomListCellDemoModel currentModel;

        @Override
        protected void invalidated() {
            if (currentModel != null) { CustomListCellDemoSceneController.this.unbind(currentModel); }
            currentModel = get();
            if (currentModel != null) { CustomListCellDemoSceneController.this.bind(currentModel); }
        }
    };
    public final CustomListCellDemoModel getModel() { return model.get(); }
    public final void setModel(CustomListCellDemoModel model) { this.model.set(model); }

    @FXML
    private Scene scene;
    @FXML
    private ListView<UserModel> userListView;

    public void performOn(Stage stage) {
        stage.setScene(scene);
        stage.setTitle("Custom ListCell Demo");
        stage.sizeToScene();
        stage.centerOnScreen();
        stage.show();
    }

    public CustomListCellDemoSceneController with(CustomListCellDemoModel model) {
        setModel(model);
        if (model != null) { model.loadUsers(); }

        return this;
    }

    protected void bind(CustomListCellDemoModel model) {
        userListView.itemsProperty().bind(model.usersProperty());
    }

    protected void unbind(CustomListCellDemoModel model) {
        userListView.itemsProperty().unbind();
    }

    @FXML
    protected void initialize() {
        userListView.setCellFactory(UserListCell.forListView());
    }
}

このシーンのモデルのデフォルトの実装を以下のように実装します。

class DefaultCustomListCellDemoModel implements CustomListCellDemoModel {
    @Override
    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); }

    @Override
    public void loadUsers() {
        setUsers(
            FXCollections.<UserModel>observableArrayList(
                DefaultUserModel.asMale("User 1", "user1@email.com"),
                DefaultUserModel.asFemale("User 2", "user2@email.com"),
                DefaultUserModel.asMale("User 3", "user3@email.com"),
                DefaultUserModel.asMale("User 4", "user4@email.com"),
                DefaultUserModel.asFemale("User 5", "user5@email.com")
            )
        );
    }
}

サンプルデータとして、適当に5件のUserを設定しています。

最後に、アプリケーションクラスを以下のようにします。

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

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

カスタムListCellの作成には、updateItemメソッドをオーバーライドして、表示内容を実装するようにします。また、編集可能とする場合は、startEditメソッド、cancelEditメソッド、commitEditメソッドをオーバーライドして、編集時の処理を実装するようにします。基本的には、これだけの実装で作成できますので、単なる文字列だけをリストとして表示するのではなく、あるモデルの内容を画像等も含めて一覧として表示したりすることが容易にできそうですので、いろいろなリストの表示ができそうです。

次回は、今回のようにノードをリストのセルとして表示するセルを、再利用できるようなクラスとして作成してみたいと思います。

JavaFX - Custom Editable ListCell (1)

JavaFXのListViewで、表示内容をカスタマイズしたい場合は、ListCellを継承してカスタマイズしたい内容を実装し、ListViewのcellFactoryで、そのクラスのインスタンスを返すようにします。

javafx.scene.control.cellパッケージに、クラスライブラリで提供されているクラスがありますので、利用できるものがある場合は、これらを利用していくのが良いでしょう。これらのクラスは、forから始まるCallbackインターフェイスの実装を返すファクトリメソッドが用意されていますので、これをListViewのcellFactoryで設定するだけで利用できるようになっています。

例えば、文字列をリストで表示し、TextFieldで内容を編集したい場合は、TextFieldListCellを利用して、以下のようにListViewのcellFactoryで設定します。

listView.setCellFactory(TextFieldListCell.forListView());

また、以下のようにFXMLファイルに記述することもできます。

<ListView fx:id="listView" editable="true">
    <cellFactory>
        <TextFieldListCell fx:factory="forListView"/>
    </cellFactory>
</ListView>

単一の文字列を表示して、それをTextFieldやComboBox等で編集したい場合は、javafx.scene.control.cellパッケージで提供されているものを利用すれば良いのですが、複数の項目を表示したり、それらを編集できるようにしたい場合は、javafx.scene.control.cellパッケージで提供されているものでは対応できませんので、ListCellを継承して作成する必要があります。

ListCellを継承したクラスの実装としては、まず以下のメソッドをオーバーライドして、表示する内容について実装します。

void updateItem(T item, boolean empty)

updateItemメソッドの2番目の引数がtrueの場合は、このセルに表示するリストの内容がないことを表します。つまり、このセルに表示するリストの内容がない場合でもこのメソッドが呼び出されますので、2番目の引数の値に応じて、表示する内容を適切に実装する必要があります。また、スーパークラスでupdateItemの実装がされていますので、基本的には最初にスーパークラスのupdateItemメソッドを呼び出しておくのが良いでしょう。updateItemの実装例としては、以下のようになります。

@Override
protected void updateItem(T item, boolean empty) {
    super.updateImte(item, empty);

    if (empty) {
        // リストの内容がないときの表示内容を実装する。
        // 何も表示しない場合は、setGraphicメソッドとsetTextメソッドにnullを指定する。
        return;
    }

    // 引数のitemを利用して、表示する内容を実装する。
}

次に、編集を行う場合は、以下の3つのメソッドをオーバーライドして、編集に対する振る舞いを実装します。

void startEdit()
void cancelEdit()
void commitEdit(T newValue)

編集が開始された場合は、startEditメソッドが呼び出されます。このメソッドもupdateItemメソッドと同様に、スーパークラスでの実装がありますので、スーパークラスのstartEditメソッドを呼び出しておくのが良いでしょう。また、編集可能でない場合は、処理を行わにようにもしておきます。startEditの実装例としては、以下のようになります。

@Override
public void startEdit() {
    if (!isEditable()) { return; }
    if (!getListView().isEditable()) { return; }

    super.startEdit();

    // 編集開始時の処理を実装する。編集用のビューの設定等。
}

編集がキャンセルされた場合は、cancelEditメソッドが呼び出されます。このメソッドも同様に、スーパークラスでの実装がありますので、スーパークラスのcancelEditメソッドを呼び出しておくのが良いでしょう。また、編集中でない場合は、処理を行わないようにもしておきます。cancelEditの実装例としては、以下のようになります。

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

    super.cancelEdit();

    // 編集キャンセル時の処理を実装する。表示用のビューの設定等。
}

編集がコミットされた場合は、commitEditメソッドが呼び出されます。このメソッドも同様に、スーパークラスでの実装がありますので、スーパークラスのcommitEditメソッドを呼び出しておくのが良いでしょう。スーパークラスのcommitEditメソッドでは、updateItemメソッドが呼ばれ、updateItemのスーパークラスの実装では、編集中の場合はcancelEditメソッドを呼び出すようになっていますので、表示内容については特に実装する必要がありません。編集用のモデルの内容を、表示用のモデルに反映したい場合は、ここで実装します。また、編集中出ない場合は、処理を行わないようにもしておきます。commitEditの実装例としては、以下のようになります。

@Override
public void commitEdit(T newValue) {
    if (!isEditing()) { return; }

    supre.commitEdit(newValue);

    // 編集コミット時の処理を実装する。
}

次回は、上記の内容をふまえて、実際に編集可能なカスタムListCellを作成してみたいと思います。

2013年5月6日月曜日

JavaFX - Dragging gestures (6)

前回は、Platform-supported drag-and-drop gestureについて、どのようにして行うかをみていきました。今回は、Platform-supported drag-and-drop gestureを利用して、以下のような、テキストボックスの文字列をドラッグアンドドロップで、他の部分に移動するものを作成してみたいと思います。

まずは、Viewを作成します。

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

    <GridPane>
        <columnConstraints>
            <ColumnConstraints prefWidth="50"/>
            <ColumnConstraints prefWidth="50"/>
        </columnConstraints>

        <TextArea fx:id="dragSourceTextArea"
                  GridPane.columnIndex="0"
                  GridPane.halignment="CENTER" GridPane.valignment="CENTER"
                  GridPane.hgrow="ALWAYS" GridPane.vgrow="ALWAYS"
                  maxWidth="150" maxHeight="50"
                  text="Drag me!"/>

        <StackPane fx:id="dropTargetPane"
                   GridPane.columnIndex="1"
                   GridPane.halignment="CENTER" GridPane.valignment="CENTER"
                   GridPane.hgrow="ALWAYS" GridPane.vgrow="ALWAYS"
                   prefWidth="200" prefHeight="150"
                   maxWidth="-Infinity" maxHeight="-Infinity">
            <Label fx:id="dropTargetLabel"/>
        </StackPane>
    </GridPane>
</Scene>

ドラッグ元となるTextAreaとドラッグ先となるStackPaneを定義しています。

次に、ドラッグ元のTextAreaのDRAG_DETECTEDイベントで、TextAreaで選択されている文字列をClipboardに設定し、Platform-supported drag-and-drop gestureを開始します。

dragSourceTextArea.setOnDragDetected(new EventHandler<MouseEvent>() {
    @Override
    public void handle(MouseEvent event) {
        TextArea source = (TextArea)event.getSource();
        if (source.getSelectedText().isEmpty()) { return; }

        Dragboard board = source.startDragAndDrop(TransferMode.COPY_OR_MOVE);
        ClipboardContent content = new ClipboardContent();
        content.putString(source.getSelectedText());
        board.setContent(content);

        event.consume();
    }
});

ドラッグ先のStackPaneのDRAG_OVERイベントで、Dragboardに文字列が設定されている場合に、ドロップを受け入れるように設定します。

dropTargetPane.setOnDragOver(new EventHandler<DragEvent>() {
    @Override
    public void handle(DragEvent event) {
        Dragboard board = event.getDragboard();
        if (board.hasString()) {
            event.acceptTransferModes(TransferMode.COPY_OR_MOVE);
        }

        event.consume();
    }
});

ドラッグ先のStackPaneのDRAG_DROPPEDイベントで、Dragboardに設定されている文字列を取得して、Labelにその文字列を設定します。

dropTargetPane.setOnDragDropped(new EventHandler<DragEvent>() {
    @Override
    public void handle(DragEvent event) {
        Dragboard board = event.getDragboard();
        if (board.hasString()) {
            dropTargetLabel.setText(board.getString());
            event.setDropCompleted(true);
        }

        event.consume();
    }
});

また、ドラッグ中にカーソルがStackPane内に入ってきたときに、StackPaneの外枠を強調するために、DRAG_ENTEREDとDRAG_EXITEDイベントで、スタイルの変更をします。

dropTargetPane.setOnDragEntered(new EventHandler<DragEvent>() {
    @Override
    public void handle(DragEvent event) {
        ((Node)event.getSource()).getStyleClass().add("drop-target");
    }
});
dropTargetPane.setOnDragExited(new EventHandler<DragEvent>() {
    @Override
    public void handle(DragEvent event) {
        ((Node)event.getSource()).getStyleClass().remove("drop-target");
    }
});

最後に、ドラッグ元のTextAreaのDRAG_DONEイベントで、TransferMode.MOVEでドロップした場合は、ドラッグしているTextAreaで選択されている文字列を削除します。

dragSourceTextArea.setOnDragDone(new EventHandler<DragEvent>() {
    @Override
    public void handle(DragEvent event) {
        if (event.getTransferMode() == TransferMode.MOVE) {
            TextArea source = (TextArea)event.getSource();
            source.deleteText(source.getSelection());
        }

        event.consume();
    }
});

Platform-supported drag-and-drop gestureでは、基本的には、ドラッグ元のノードでは、DRAG_DETECTEDとDRAG_DONEイベントで、ドラッグ開始時とドロップ完了時の処理を記述し、ドラッグ先のノードでは、DRAG_OVERとDRAG_DROPPEDイベントで、ドロップに対する処理を記述することになります。また、必要に応じて、DRAG_ENTEREDとDRAG_EXITEDイベントを記述します。

同じようにして、以下のような、ListViewの項目の位置をドラッグアンドドロップで変更するものを作成してみました。

今回は、同じアプリケーション内で、Platform-supported drag-and-drop gestureを利用しましたが、通常は、クリップボートを利用して、他のアプリケーションとデータをやり取りする場合に利用する機会が多いと思います。

JavaFXでは、ドラッグの動作として、3種類のタイプが用意されていますので、状況に応じて使い分けていくとよいようです。