TableView and ListValueFactory in JavaFX

The TableView control is pretty complete to use and very convenient. But the classical use of it specify that a line is represented by an instance of an Object, usually a property bean. But in some cases it would be very useful to have a line composed by multiple instances of a specific class, i.e. having an instance per column, and each column should display a particular property of your instance. A concrete example would be the management interface of translations in NetBeans. Indeed, this screen display all properties files of a particular resource bundle, having a column for the translations keys and as many columns as needed to display the translations for each keys.

netbeans_i18n_editor

As said above, we could need an instance of object per column, meaning multiple instances per line. So a line of the TableView should be a List<? extends Object>. The problem is to know which data to display in a particular column.

ListValueFactory

As you may know, you can set CellValueFactory on a TableColumn, which will display the value in the cell. For example, you can set a PropertyValueFactory that will look for a property of a bean, and will display it. But that works if you have a single Object per line, not a collection. So we will create our own implementation that will check a property of a bean inside a list.

public class ListValueFactory<T, V> implements Callback<TableColumn.CellDataFeatures<List<T>, V>, ObservableValue<V>> {
  
  private String propertyName;

  public ListValueFactory(String propertyName) {
    this.propertyName = propertyName;
  }
    
  @Override
  public ObservableValue<V> call(TableColumn.CellDataFeatures<List<T> , V> p) {
        
    int index = 0;
        
    ListIterator<TableColumn<List<T>, ?>> iterator = 
       p.getTableView().getColumns().listIterator();
        
    TableColumn tmpColumn;
    while(iterator.hasNext()) {
      tmpColumn = iterator.next();

      if(tmpColumn == p.getTableColumn())
        break;
      else
        index++;
    }

    T object = p.getValue().get(index);

    try {
      Method method = object.getClass().getMethod(propertyName.concat("Property"));
      return (Property<V>) method.invoke(object);
    } catch (Exception ex) {
      Logger.getLogger(ListValueFactory.class.getName()).log(Level.SEVERE, null, ex);
      return null;
    }
  }
}

As you can see, the implementation is totally generic. First, we try to know the index of the column and we suppose that this index is the same index of the data in the list of data. WARNING: this is totally wrong if, in your UI, you move columns ; so take this into consideration because the implementation is not shown here. Once we have the index, we look for the data in the list, and finally, using so introspection, we look for the getter of the property in order to get the value. And that’s it.

Usage

To use this implementation in your code, you juste have to do the following:

TableView<List<Person>> tableView = new TableView<>();
TableColumn<String> columnOne = new TableColumn<>("First name one");
columnOne.setCellValueFactory(new ListValueFactory<List<Person>, String>("firstName"));
TableColumn<String> columnTwo = new TableColumn<>("First name two");
columnTwo.setCellValueFactory(new ListValueFactory<List<Person>, String>("firstName"));
tableView.getColumns().addAll(columnOne, columnTwo);
Advertisements

TableView: column’s width using percentage in JavaFX

In my previous post you’ve successfully learned how to simply use a TableView. It is obvious that a tables has columns and columns have a size. Sometimes you would like to specify the width of your columns using a percentage. Unfortunately it’s not a native feature of JavaFX 2. Well this ain’t a problem because, again, it is very simple to implement it yourself by creating a custom control.

PTableColumn

Creating a custom control in our case is very simple because what we need to do it basically already done. Indeed what we need is to know the size of our table and then compute our column’s width according a given percentage. All others behaviours are already managed by the TableColumn class. So we will create a class named PTableColumn (‘P’ standing obviously for percentage) and which extends the TableColumn class.

public class PTableColumn<S, T> extends javafx.scene.control.TableColumn<S, T> {

  private final DoubleProperty percentageWidth = new SimpleDoubleProperty(1);

  public PTableColumn() {
    tableViewProperty().addListener(new ChangeListener<TableView<S>>() {

      @Override
      public void changed(ObservableValue<? extends TableView<S>> ov, TableView<S> t, TableView<S> t1) {
        if(PTableColumn.this.prefWidthProperty().isBound()) {
          PTableColumn.this.prefWidthProperty().unbind();
        }

        PTableColumn.this.prefWidthProperty().bind(t1.widthProperty().multiply(percentageWidth));
      }
    });
  }
    
  public final DoubleProperty percentageWidthProperty() {
    return this.percentageWidth;
  }
    
  public final double getPercentageWidth() {
    return this.percentageWidthProperty().get();
  }
    
  public final void setPercentageWidth(double value) throws IllegalArgumentException {
    if(value >= 0 && value <= 1) {
      this.percentageWidthProperty().set(value);
    } else {
      throw new IllegalArgumentException(String.format("The provided percentage width is not between 0.0 and 1.0. Value is: %1$s", value));
    }
  }
}

As you can notice, we add a listener to the TableView property of the TableColumn. So each time the table changes, you just update the column’s width. The prefWidthProperty is bound to our percentageWidthPropery using a NumberBinding. So each time our property changes, the width is recalculated.

Use it

Now the only thing left to do is use our custom column in our FXML. Don’t forget to import it!

<!-- ... -->
<?import com.twasyl.envviewer.controls.*?>
<!-- ... -->
<TableView fx:id="propertiesTable">
  <columns>
    <PTableColumn text="Type" fx:id="typeColumn" percentageWidth="0.08">
      <!-- ... -->
    </PTableColumn>
    <!-- ... -->
  </columns>
</TableView>

Well as I said, really easy.

The source code is available here.

TableView: display tables in JavaFX

If you were used to use JTable in Swing in order to display tables in your UI, you probably know that was kind of difficult to use and customise. In JavaFX you have what is called a TableView which is complex component allowing you to do a lot of stuff. In this post we will try to cover some basics about the TableView.

Main classes

In order to display tables in JavaFX you will use a TableView. The TableView will host TableColumns. That are the two main classes you will use in order to create a table. The TableRow class also exists but you won’t use it much, except if you want to create your own rowFactory. But that is a bit out of the scope of this post.

Bring up some Generics

A table is a container that will host some kind of data: a product, an account, an item, etc. So your TableView will host some kind of data. So the following declaration makes sense:

TableView<Property> propertiesTable = new ...

Columns will host a specific value of an item in the table, like the name of the product for example. And the name is a string. So declaring columns will be like this:

TableColumn<Property, String> nameColumn;
TableColumn<Property, String> valueColumn;

The power of properties

One of the great concept of JavaFX 2 is the properties and binding (read about here). And it is very useful in our case. Indeed we know what to display in our table, we have the data but I don’t want to say each time I need a new entry in my table that the name is in the first column, the value in the second, and so on. So let’s have a look. First of all I’m going to create a JavaBean with properties:

public class Property implements Serializable {
  public enum Type {
    SYSTEM, ENVIRONMENT
  }

  private final StringProperty name = new SimpleStringProperty();
  private final StringProperty value = new SimpleStringProperty();
  private final ObjectProperty<Type> type = new SimpleObjectProperty<>();
    
  public Property() {
  }
    
  public Property(String name, String value, Type type) {
    this.name.set(name);
    this.value.set(value);
    this.type.set(type);
  }
    
  public final StringProperty nameProperty() {
    return this.name;
  }
    
  public final String getName() {
    return this.nameProperty().get();
  }
    
  public final void setName(String name) {
    this.nameProperty().set(name);
  }
    
  public final StringProperty valueProperty() {
    return this.value;
  }
    
  public final String getValue() {
    return this.valueProperty().get();
  }
    
  public final void setValue(String value) {
    this.valueProperty().set(value);
  }
    
  public final ObjectProperty<Type> typeProperty() {
    return this.type;
  }
    
  public final Type getType() {
    return this.typeProperty().get();
  }

  public final void setType(Type type) {
    this.typeProperty().set(type);
  }
}

This quite simple. But now the real power will be revealed. We will create our table completely in FXML, without anything else. But you can of course create your table completely using pure Java code (just have a look on the Oracle website).

<BorderPane prefHeight="800" prefWidth="1000" fx:id="rootPane"
    xmlns:fx="http://javafx.com/fxml"
    fx:controller="com.twasyl.envviewer.controllers.EnvViewerController">
  <center>
    <TableView fx:id="propertiesTable">
      <columns>
        <TableColumn text="Type" fx:id="typeColumn">
          <cellValueFactory>
            <PropertyValueFactory property="type" />
          </cellValueFactory>
        </TableColumn>
        <TableColumn text="Name" fx:id="nameColumn">
          <cellValueFactory>
            <PropertyValueFactory property="name" />
          </cellValueFactory>
        </TableColumn>
        <TableColumn text="Value" fx:id="valueColumn">
          <cellValueFactory>
            <PropertyValueFactory property="value" />
          </cellValueFactory>
        </TableColumn>
      </columns>
    </TableView>
  </center>
</BorderPane>

And now take a look at the controller:

public class EnvViewerController implements Initializable {
    
  @FXML
  private Pane rootPane;
  @FXML
  private TableView<Property> propertiesTable;
  @FXML
  private TableColumn<Property, String> nameColumn;
  @FXML
  private TableColumn<Property, String> valueColumn;
  @FXML
  private TableColumn<Property, Type> typeColumn;
    
  @Override
  public void initialize(URL url, ResourceBundle rb) {
  }
}

As you can read, each TableColumn tag has a cellValueFactory one, which have a PropertyValueFactory. And this last one has an attribute named property. So what about everything? That simply means the column is bound to a property of the main content of the table. In other words, our table contains some Property objects (our JavaBean class) and the first column is bound to the type property, the second to the name one and the third to the value. Easy right? Here is a little screenshot:
Empty EnvViewer
Now we will modify our controller class in order to add some items to our table:

@Override
public void initialize(URL url, ResourceBundle rb) {
        
  for(Map.Entry entry : System.getProperties().entrySet()) {
    propertiesTable.getItems().add(new Property(entry.getKey().toString(), entry.getValue().toString(), Type.SYSTEM));
  }
        
  for(Map.Entry entry : System.getenv().entrySet()) {
    propertiesTable.getItems().add(new Property(entry.getKey().toString(), entry.getValue().toString(), Type.ENVIRONMENT));
  }
}

And this is what you get:
EnvViewer filled
And the TableView comes with nice features like having sortable columns by default, you can move columns, and so on. These features were long to develop in Swing so JavaFX brings some really nice feature natively.

Of course you can do a lot more with TableViews, like editing the data, respond to user’s clicks, dynamically add data, customise the look & feel and so on. Maybe I’ll blog about some other features.

The code is from a little app I’ve done and that I’ll update to provide new features. You can find it here.

PS: this blog post took me much more time to write than coding the app, just saying!