ページ

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に対して、同様のユーティリティクラスを考えてみたいと思います。