ページ

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種類のタイプが用意されていますので、状況に応じて使い分けていくとよいようです。