Locked History Actions

JavaFX/TableViewCell

TableViewセルの扱い方

面倒で不安定なフィールド指定

TableViewに指定されるColumnのフィールドは文字列で指定される例が多い。その文字列から適切なフィールドとして使用できる「メソッド」が勝手に探し出される。 以下の例では、

  • "email"はemailProperty()メソッドが
  • "name"はgetName()メソッドが
  • "address"はaddressProperty()メソッドが

探し出される。通常は「フィールド名 + Property」というObservableValueが返されるメソッドが探し出されるが、なければ「get + 頭大文字フィールド名」というゲッターメソッドが採用されるようだ。 しかしこれには問題がある。

  • 列に指定するフィールド名が間違っていた場合に何もエラーが出ない、セルが空白になるだけ。
  • メソッド側の名称が間違っていた場合も同じ。
  • TableViewに表示した後でセル内容を変更したい場合には、必ずObservableValueにする必要がある。つまり、setName()メソッドでnameデータを変更しても(特殊な方法以外では)それをテーブル上の表示に反映する方法がない。たしかSwingでは行に対してupdateをかけると、その行を再表示してくれたと思うが、このような機能はない。

  • 文字列による結合なので、Obfuscatorにかけると全く動作しなくなる。
  • 文字列によるので、プログラムが複雑な場合にはどこから参照されているのかわからなくなる。
  • 行のフィールドが不必要なものでいっぱいになる。

import javafx.application.*;
import javafx.beans.property.*;
import javafx.scene.*;
import javafx.scene.control.*;
import javafx.scene.control.cell.*;
import javafx.stage.*;

public class CumbersomeFields extends Application {

  public static class Row {
    
    Row(String email, String name, String address) {
      this.emailWrapper.set(email);
      this.name = name;
      this.addressProperty.set(address);
    }
    
    private ReadOnlyStringWrapper emailWrapper = new ReadOnlyStringWrapper();
    public ReadOnlyStringProperty emailProperty() { return emailWrapper.getReadOnlyProperty(); }

    private String name;
    public String getName() { return name; }
    
    public SimpleStringProperty addressProperty = new SimpleStringProperty();
    public SimpleStringProperty addressProperty() { return addressProperty; }
  }
  
  @SuppressWarnings("unchecked")
  @Override
  public void start(Stage stage) throws Exception {    
    TableView<Row>tableView= new TableView<>();
    tableView.getColumns().addAll(
       new Column<Row, String>("EMAIL", "email").col,
       new Column<Row, String>("NAME", "name").col,
       new Column<Row, String>("ADDRESS", "address").col
    );
    tableView.getItems().addAll(
      new Row("email1", "name1", "address1"), 
      new Row("email2", "name2", "address")
    );
    stage.setScene(new Scene(tableView));
    stage.show();
  }
  
  static class Column<E, T> {
    TableColumn<E, T>col;
    Column(String title, String field) {
      col = new TableColumn<E, T>(title);
      col.setCellValueFactory(new PropertyValueFactory<E, T>(field));
    }
  }
  
  public static void main(String[] args) throws Exception {
    launch(args);
  }
}

改良版は以下。ここでは、

  • 文字列によるフィールド指定を排除。Obfuscatorセーフ。
  • 行オブジェクトの記述が少なくなる。この例では大して減っていないが、固定値が多い場合は大量に減る。

import javafx.application.*;
import javafx.beans.*;
import javafx.beans.property.*;
import javafx.beans.value.*;
import javafx.scene.*;
import javafx.scene.control.*;
import javafx.stage.*;

public class ImprovedFields extends Application {

  public static class Row {
    
    Row(String email, String name, String address) {
      this.emailWrapper.set(email);
      this.name = name;
      this.address.set(address);
    }
    
    private ReadOnlyStringWrapper emailWrapper = new ReadOnlyStringWrapper();
    public ReadOnlyStringProperty email() { return emailWrapper.getReadOnlyProperty(); }

    private String name;
    public String getName() { return name; }
    
    public SimpleStringProperty address = new SimpleStringProperty();
  }
  
  @SuppressWarnings("unchecked")
  @Override
  public void start(Stage stage) throws Exception {    
    TableView<Row>tableView= new TableView<>();
    tableView.getColumns().addAll(
       new Column<Row, String>("EMAIL", r->r.email()).col,
       new Column<Row, String>("NAME", r->fv(r.getName())).col,
       new Column<Row, String>("ADDRESS", r->r.address).col
    );
    tableView.getItems().addAll(
      new Row("email1", "name1", "address1"), 
      new Row("email2", "name2", "address")
    );
    stage.setScene(new Scene(tableView));
    stage.show();
  }

  private <T> FixedValueWrapper<T> fv(T value) {
    return new FixedValueWrapper<T>(value);
  }

  /** 固定値なのでリスナー登録等は一切無視 */
  public static class FixedValueWrapper<T> implements ObservableValue<T>{
    T value;
    public FixedValueWrapper(T value) {
      this.value = value;
    }
    public T getValue() {
      return value;
    }
    public void addListener(InvalidationListener listener) {}
    public void removeListener(InvalidationListener listener) {}
    public void addListener(ChangeListener<? super T> listener) {}
    public void removeListener(ChangeListener<? super T> listener) {}
  }

  public interface PropertyGetter<E, T> {
    public ObservableValue<T> get(E e);
  }
  
  static class Column<E, T> {
    TableColumn<E, T>col;
    Column(String title, PropertyGetter<E, T>getProperty) {
      col = new TableColumn<E, T>(title);
      col.setCellValueFactory(t->getProperty.get(t.getValue()));
    }
  }
  
  public static void main(String[] args) throws Exception {
    launch(args);
  }
}