takeda_san’s blog

JavaFXと機械学習を頑張る方向。

テーブル内のチェックボックスをクリックしたときに行を選択したことにしたい

おのれ、チェックボックス

こんな画面、ギョームアプリケーションを作ったことがある方なら一度はあると思います。
チェックボックスを選択して、その行に対して操作を行うみたいなやつ。

f:id:takeda_san:20170725225412p:plain

JavaFXでは、CheckBoxTableCellというチェックボックス入りのテーブルセルクラスが標準であるのでそれを使います。
標準であるのはありがたいんですが、このクラス、選択時にテーブルのOnMouseClickedが動かないし、行を選択したことにもならないのよね。

普通ならテーブルの行をクリックすると、こんな感じで青く色が反転して選択状態になるんだけど。

f:id:takeda_san:20170725225943p:plain

チェックボックスをクリックすると

f:id:takeda_san:20170725230044p:plain

行は選択状態にならんのかい… ちなみに、テーブル側でOnMouseClickedを設定しておいても動きません。

チェックボックスを押したときでも、行を選択状態にしたい!!!
というわけで、(ムリヤリ)挙動を変えて使いやすくしましょう。

プログラム

今回もみんな大好きOracleのサンプルプログラムをすこし改変して使います。

docs.oracle.com

また、プログラム全文のせちゃいます。

メインクラス

package application;

import javafx.application.Application;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.geometry.Insets;
import javafx.scene.Group;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.control.cell.PropertyValueFactory;
import javafx.scene.layout.VBox;
import javafx.scene.text.Font;
import javafx.stage.Stage;

public class Main extends Application {
    private TableView<Person> table = new TableView<>();
    private ObservableList<Person> data = FXCollections.observableArrayList(
            new Person(false, "Jacob", "Smith", "jacob.smith@example.com"),
            new Person(false, "Isabella", "Johnson", "isabella.johnson@example.com"),
            new Person(false, "Ethan", "Williams", "ethan.williams@example.com"),
            new Person(false, "Emma", "Jones", "emma.jones@example.com"),
            new Person(false, "Michael", "Brown", "michael.brown@example.com"));

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

    @Override
    public void start(Stage stage) {
        Scene scene = new Scene(new Group());
        stage.setTitle("Table View Sample");
        stage.setWidth(515);
        stage.setHeight(500);

        final Label label = new Label("Address Book");
        label.setFont(new Font("Arial", 20));

        CheckBoxColumn checkBoxCol = new CheckBoxColumn();
        checkBoxCol.setMinWidth(50);

        TableColumn<Person, String> firstNameCol = new TableColumn<>("First Name");
        firstNameCol.setMinWidth(100);
        firstNameCol.setCellValueFactory(new PropertyValueFactory<>("firstName"));

        TableColumn<Person, String> lastNameCol = new TableColumn<>("Last Name");
        lastNameCol.setMinWidth(100);
        lastNameCol.setCellValueFactory(new PropertyValueFactory<>("lastName"));

        TableColumn<Person, String> emailCol = new TableColumn<>("Email");
        emailCol.setMinWidth(200);
        emailCol.setCellValueFactory(new PropertyValueFactory<>("email"));

        // なぜかテーブル全体をeditableにしないとチェックボックスが押せない…
        table.setEditable(true);

        // マウスクリック時に動作する
        table.setOnMouseClicked(event -> {
            System.out.println("行が選択されたぜ!!");
        });

        table.setItems(data);
        table.getColumns().add(checkBoxCol);
        table.getColumns().add(firstNameCol);
        table.getColumns().add(lastNameCol);
        table.getColumns().add(emailCol);

        final VBox vbox = new VBox();
        vbox.setSpacing(5);
        vbox.setPadding(new Insets(10, 0, 0, 10));
        vbox.getChildren().addAll(label, table);

        ((Group) scene.getRoot()).getChildren().addAll(vbox);

        stage.setScene(scene);
        stage.show();
    }

}

チェックボックスカラムのクラス

package application;

import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.event.Event;
import javafx.scene.control.TableColumn;
import javafx.scene.control.cell.CheckBoxTableCell;
import javafx.scene.control.cell.PropertyValueFactory;
import javafx.scene.input.MouseButton;
import javafx.scene.input.MouseEvent;

public class CheckBoxColumn extends TableColumn<Person, Boolean> {

    public CheckBoxColumn() {
        // Personのcheckedプロパティと紐づける
        this.setCellValueFactory(new PropertyValueFactory<>("checked"));

        this.setCellFactory(column -> {

            // CheckBoxTableCellの挙動を定義する
            CheckBoxTableCell<Person, Boolean> cell = new CheckBoxTableCell<Person, Boolean>(index -> {
                BooleanProperty selected = new SimpleBooleanProperty(
                        this.getTableView().getItems().get(index).isChecked());

                selected.addListener((ov, o, n) -> {
                    // チェックボックスの状態が変わったらPersonのデータも更新する
                    this.getTableView().getItems().get(index).setChecked(n);

                    // チェックボックスが押されたので、行を選択したことにする
                    this.getTableView().getSelectionModel().select(index);

                    // テーブルにクリックイベントを飛ばす
                    Event.fireEvent(column.getTableView(), new MouseEvent(MouseEvent.MOUSE_CLICKED, 0, 0, 0, 0,
                            MouseButton.PRIMARY, 1, true, true, true, true, true, true, true, true, true, true, null));
                });

                return selected;
            });

            return cell;
        });
    }
}

テーブルに表示するモデルのクラス

package application;

import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleStringProperty;

public class Person {

    private SimpleBooleanProperty checked;
    private SimpleStringProperty firstName;
    private SimpleStringProperty lastName;
    private SimpleStringProperty email;

    public Person(boolean checked, String fName, String lName, String email) {
        this.checked = new SimpleBooleanProperty(checked);
        this.firstName = new SimpleStringProperty(fName);
        this.lastName = new SimpleStringProperty(lName);
        this.email = new SimpleStringProperty(email);
    }

    public SimpleBooleanProperty checkedProperty() {
        return checked;
    }

    public boolean isChecked() {
        return checked.get();
    }

    public void setChecked(boolean checked) {
        this.checked.set(checked);
    }

    public String getFirstName() {
        return firstName.get();
    }

    public void setFirstName(String fName) {
        firstName.set(fName);
    }

    public String getLastName() {
        return lastName.get();
    }

    public void setLastName(String fName) {
        lastName.set(fName);
    }

    public String getEmail() {
        return email.get();
    }

    public void setEmail(String fName) {
        email.set(fName);
    }
}

長々とプログラムを載せましたが、大事なのはチェックボックスカラムクラスのここだけ。

// チェックボックスが押されたので、行を選択したことにする
this.getTableView().getSelectionModel().select(index);

// テーブルにクリックイベントを飛ばす
Event.fireEvent(column.getTableView(), new MouseEvent(MouseEvent.MOUSE_CLICKED, 0, 0, 0, 0,
        MouseButton.PRIMARY, 1, true, true, true, true, true, true, true, true, true, true, null));

まずは、上の一行のほうから。
テーブルではSelectionModelというクラスを使って、任意に行の選択状態を変更したり、選択されている行のモデルデータを取得することができます。
これが、地味に便利。
今回は、チェックボックスの選択状態が変更されたセルの行番号indexを使って、それと同じ行を選択したことにしています。

次に、見るからに脳みそ筋肉な次の実装。
これは、第一引数のノードで第二引数のイベントを発生させるという処理を書いています。
テーブルにイベントが飛ばないならば、自分でイベントを生成して飛ばしてしまおうというわけです。

実行結果

f:id:takeda_san:20170725232446p:plain

行が選択されたぜ!!

にしても、テーブル周りの実装が、私には高度すぎて時間がかかる…
androidとかやっている人なら、もうちょっとセンス良く書けるのかしら。

// なぜかテーブル全体をeditableにしないとチェックボックスが押せない…
table.setEditable(true);

これな…