© The Author(s), under exclusive license to APress Media, LLC, part of Springer Nature 2022
K. Sharan, P. SpäthLearn JavaFX 17https://doi.org/10.1007/978-1-4842-7848-2_13

13. Understanding TableView

Kishori Sharan1   and Peter Späth2
(1)
Montgomery, AL, USA
(2)
Leipzig, Sachsen, Germany
 
In this chapter, you will learn:
  • What a TableView is

  • How to create a TableView

  • About adding columns to a TableView

  • About populating a TableView with data

  • About showing and hiding and reordering columns in a TableView

  • About sorting and editing data in a TableView

  • About adding and deleting rows in a TableView

  • About resizing columns in a TableView

  • About styling a TableView with CSS

The examples of this chapter lie in the com.jdojo.control package. In order for them to work, you must add a corresponding line to the module-info.java file:
...
opens com.jdojo.control to javafx.graphics, javafx.base;
...

Some of the longer listings of this chapter are shown in abbreviated manner. For the complete listings, please consult the download area for the book.

What Is a TableView?

TableView is a powerful control to display and edit data in a tabular form from a data model. A TableView consists of rows and columns. A cell is an intersection of a row and a column. Cells contain the data values. Columns have headers that describe the type of data they contain. Columns can be nested. Resizing and sorting of column data have built-in support. Figure 13-1 shows a TableView with four columns that have the header text Id, First Name, Last Name, and Birth Date. It has five rows, with each row containing data for a person. For example, the cell in the fourth row and third column contains the last name Boyd.
Figure 13-1

A TableView showing a list of persons

TableView is a powerful, but not simple, control. You need to write a few lines of code to use even the simplest TableView that displays some meaningful data to users. There are several classes involved in working with TableView. I will discuss these classes in detail when I discuss the different features of the TableView:
  • TableView

  • TableColumn

  • TableRow

  • TableCell

  • TablePosition

  • TableView.TableViewFocusModel

  • TableView.TableViewSelectionModel

The TableView class represents a TableView control. The TableColumn class represents a column in a TableView. Typically, a TableView contains multiple instances of TableColumn. A TableColumn consists of cells, which are instances of the TableCell class. A TableColumn uses two properties to populate cells and render values in them. It uses a cell value factory to extract the value for its cells from the list of items. It uses a cell factory to render data in a cell. You must specify a cell value factory for a TableColumn to see some data in it. A TableColumn uses a default cell factory that knows how to render text and a graphic node.

The TableRow class inherits from the IndexedCell class. An instance of TableRow represents a row in a TableView. You would almost never use this class in an application unless you want to provide a customized implementation for rows. Typically, you customize cells, not rows.

An instance of the TableCell class represents a cell in a TableView. Cells are highly customizable. They display data from the underlying data model for the TableView. They are capable of displaying data as well as graphics.

The TableColumn, TableRow, and TableCell classes contain a tableView property that holds the reference of the TableView that contains them. The tableView property contains null when the TableColumn does not belong to a TableView.

A TablePosition represents the position of a cell. Its getRow() and getColumn() methods return the indexes of the row and column, respectively, to which the cell belongs.

The TableViewFocusModel class is an inner static class of the TableView class. It represents the focus model for the TableView to manage focus for rows and cells.

The TableViewSelectionModel class is an inner static class of the TableView class. It represents the selection model for the TableView to manage selection for rows and cells.

Like ListView and TreeView controls, TableView is virtualized. It creates just enough cells to display the visible content. As you scroll through the content, the cells are recycled. This helps keep the number of nodes in the scene graph to a minimum. Suppose you have 10 columns and 1000 rows in a TableView, and only 10 rows are visible at a time. An inefficient approach would be to create 10,000 cells, one cell for each piece of data. The TableView creates only 100 cells, so it can display ten rows with ten columns. As you scroll through the content, the same 100 cells will be recycled to show the other visible rows. Virtualization makes it possible to use TableView with a large data model without performance penalty for viewing the data in a chunk.

For examples in this chapter, I will use the Person class from Chapter 11 on MVC. The Person class is in the com.jdojo.mvc.model package. Before I start discussing the TableView control in detail, I will introduce a PersonTableUtil class , as shown in Listing 13-1. I will reuse it several times in the examples presented. It has static methods to return an observable list of persona and instances of the TableColumn class to represent columns in a TableView.
// PersonTableUtil.java
package com.jdojo.control;
import com.jdojo.mvc.model.Person;
import java.time.LocalDate;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.scene.control.TableColumn;
import javafx.scene.control.cell.PropertyValueFactory;
public class PersonTableUtil {
    /* Returns an observable list of persons */
    public static ObservableList<Person> getPersonList() {
      Person p1 =
            new Person("Ashwin", "Sharan", LocalDate.of(2012, 10, 11));
      Person p2 =
            new Person("Advik", "Sharan", LocalDate.of(2012, 10, 11));
      Person p3 =
            new Person("Layne", "Estes", LocalDate.of(2011, 12, 16));
      Person p4 =
            new Person("Mason", "Boyd", LocalDate.of(2003, 4, 20));
      Person p5 =
            new Person("Babalu", "Sharan", LocalDate.of(1980, 1, 10));
      return FXCollections.<Person>observableArrayList(p1, p2, p3, p4, p5);
    }
    /* Returns Person Id TableColumn */
    public static TableColumn<Person, Integer> getIdColumn() {
        TableColumn<Person, Integer> personIdCol = new TableColumn<>("Id");
        personIdCol.setCellValueFactory(
               new PropertyValueFactory<>("personId"));
        return personIdCol;
    }
    /* Returns First Name TableColumn */
    public static TableColumn<Person, String> getFirstNameColumn() {
      TableColumn<Person, String> fNameCol =
            new TableColumn<>("First Name");
      fNameCol.setCellValueFactory(new PropertyValueFactory<>("firstName"));
      return fNameCol;
    }
    /* Returns Last Name TableColumn */
    public static TableColumn<Person, String> getLastNameColumn() {
        TableColumn<Person, String> lastNameCol =
               new TableColumn<>("Last Name");
        lastNameCol.setCellValueFactory(
               new PropertyValueFactory<>("lastName"));
        return lastNameCol;
    }
    /* Returns Birth Date TableColumn */
    public static TableColumn<Person, LocalDate> getBirthDateColumn() {
        TableColumn<Person, LocalDate> bDateCol =
            new TableColumn<>("Birth Date");
        bDateCol.setCellValueFactory(
               new PropertyValueFactory<>("birthDate"));
        return bDateCol;
    }
}
Listing 13-1

A PersonTableUtil Utility Class

Subsequent sections will walk you through the steps to display and edit data in a TableView.

Creating a TableView

In the following example, you will use the TableView class to create a TableView control. TableView is a parameterized class, which takes the type of items the TableView contains. Optionally, you can pass the model into its constructor that supplies the data. The constructor creates a TableView without a model. The following statement creates a TableView that will use objects of the Person class as its items:
TableView<Person> table = new TableView<>();
When you add the preceding TableView to a scene, it displays a placeholder, as shown in Figure 13-2. The placeholder lets you know that you need to add columns to the TableView. There must be at least one visible leaf column in the TableView data.
Figure 13-2

A TableView with no columns and data showing a placeholder

You would use another constructor of the TableView class to specify the model. It accepts an observable list of items. The following statement passes an observable list of Person objects as the initial data for the TableView:
TableView<Person> table = new TableView<>(PersonTableUtil.getPersonList());

Adding Columns to a TableView

An instance of the TableColumn class represents a column in a TableView. A TableColumn is responsible for displaying and editing the data in its cells. A TableColumn has a header that can display header text, a graphic, or both. You can have a context menu for a TableColumn, which is displayed when the user right-clicks inside the column header. Use the contextMenu property to set a context menu.

The TableColumn<S, T> class is a generic class. The S parameter is the item type, which is of the same type as the parameter of the TableView. The T parameter is the type of data in all cells of the column. For example, an instance of the TableColumn<Person, Integer> may be used to represent a column to display the ID of a Person, which is of int type; an instance of the TableColumn<Person, String> may be used to represent a column to display the first name of a person, which is of String type. The following snippet of code creates a TableColumn with First Name as its header text:
TableColumn<Person, String> fNameCol = new TableColumn<>("First Name");
A TableColumn needs to know how to get the value (or data) for its cells from the model. To populate the cells, you need to set the cellValueFactory property of the TableColumn. If the model for a TableView contains objects of a class that is based on JavaFX properties, you can use an object of the PropertyValueFactory class as the cell value factory, which takes the property name. It reads the property value from the model and populates all of the cells in the column, as in the following code:
// Use the firstName property of Person object to populate the column cells
PropertyValueFactory<Person, String> fNameCellValueFactory =
        new PropertyValueFactory<>("firstName");
fNameCol.setCellValueFactory(fNameCellValueFactory);

You need to create a TableColumn object for each column in the TableView and set its cell value factory property. The next section will explain what to do if your item class is not based on JavaFX properties, or you want to populate the cells with computed values.

The last step in setting up a TableView is to add TableColumns to its list of columns. A TableView stores references of its columns in an ObservableList<TableColumn> whose reference can be obtained using the getColumns() method of the TableView:
// Add the First Name column to the TableView
table.getColumns().add(fNameCol);
That is all it takes to use a TableView in its simplest form, which is not so “simple” after all! The program in Listing 13-2 shows how to create a TableView with a model and add columns to it. It uses the PersonTableUtil class to get the list of persons and columns. The program displays a window as shown in Figure 13-3.
// SimplestTableView.java
package com.jdojo.control;
import com.jdojo.mvc.model.Person;
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.TableView;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
public class SimplestTableView extends Application {
        public static void main(String[] args) {
                Application.launch(args);
        }
        @Override
        public void start(Stage stage) {
                // Create a TableView with a list of persons
                TableView<Person> table =
                         new TableView<>(PersonTableUtil.getPersonList());
                // Add columns to the TableView
                table.getColumns().addAll(
                         PersonTableUtil.getIdColumn(),
                         PersonTableUtil.getFirstNameColumn(),
                         PersonTableUtil.getLastNameColumn(),
                         PersonTableUtil.getBirthDateColumn());
                VBox root = new VBox(table);
                root.setStyle("""
                         -fx-padding: 10;
                   -fx-border-style: solid inside;
                   -fx-border-width: 2;
                   -fx-border-insets: 5;
                   -fx-border-radius: 5;
                   -fx-border-color: blue;""");
                Scene scene = new Scene(root);
                stage.setScene(scene);
                stage.setTitle("Simplest TableView");
                stage.show();
        }
}
Listing 13-2

Using TableView in Its Simplest Form

Figure 13-3

A window with a TableView that displays four columns and five rows

TableView supports nesting of columns. For example, you can have two columns, First and Last, nested inside a Name column. A TableColumn stores the list of nested columns in an observable list whose reference can be obtained using the getColumns() method of the TableColumn class. The innermost nested columns are known as leaf columns . You need to add the cell value factories for the leaf columns. Nested columns only provide visual effects. The following snippet of code creates a TableView and adds an Id column and two leaf columns, First and Last, that are nested in the Name column. The resulting TableView is shown in Figure 13-4. Note that you add the topmost columns to the TableView, not the nested columns. TableView takes care of adding all nested columns for the topmost columns. There is no limit on the level of column nesting.
// Create a TableView with data
TableView<Person> table = new TableView<>(PersonTableUtil.getPersonList());
// Create leaf columns - Id, First and Last
TableColumn<Person, String> idCol = new TableColumn<>("Id");
idCol.setCellValueFactory(new PropertyValueFactory<>("personId"));
TableColumn<Person, String> fNameCol = new TableColumn<>("First");
fNameCol.setCellValueFactory(new PropertyValueFactory<>("firstName"));
TableColumn<Person, String> lNameCol = new TableColumn<>("Last");
lNameCol.setCellValueFactory(new PropertyValueFactory<>("lastName"));
// Create Name column and nest First and Last columns in it
TableColumn<Person, String> nameCol = new TableColumn<>("Name");
nameCol.getColumns().addAll(fNameCol, lNameCol);
// Add columns to the TableView
table.getColumns().addAll(idCol, nameCol);
Figure 13-4

A TableView with nested columns

The following methods in the TableView class provide information about visible leaf columns:
TableColumn<S,?> getVisibleLeafColumn(int columnIndex)
ObservableList<TableColumn<S,?>> getVisibleLeafColumns()
int getVisibleLeafIndex(TableColumn<S,?> column)

The getVisibleLeafColumn() method returns the reference of the column for the specified column index. The column index is counted only for the visible leaf column, and the index starts at zero. The getVisibleLeafColumns() method returns an observable list of all visible leaf columns. The getVisibleLeafIndex() method returns the column reference for the specified column index of a visible leaf column.

Customizing TableView Placeholder

TableView displays a placeholder when it does not have any visible leaf columns or content. Consider the following snippet of code that creates a TableView and adds columns to it:
TableView<Person> table = new TableView<>();
table.getColumns().addAll(PersonTableUtil.getIdColumn(),
                   PersonTableUtil.getFirstNameColumn(),
                   PersonTableUtil.getLastNameColumn(),
                   PersonTableUtil.getBirthDateColumn());
Figure 13-5 shows the results of the preceding TableView. Columns and a placeholder are displayed, indicating that the TableView does not have data.
Figure 13-5

A TableView control with columns and no data

You can replace the built-in placeholder using the placeholder property of the TableView. The value for the property is an instance of the Node class. The following statement sets a Label with a generic message as a placeholder:
table.setPlaceholder(new Label("No visible columns and/or data exist."));
You can set a custom placeholder to inform the user of the specific condition that resulted in showing no data in the TableView. The following statement uses binding to change the placeholder as the conditions change:
table.placeholderProperty().bind(
    new When(new SimpleIntegerProperty(0)
                 .isEqualTo(table.getVisibleLeafColumns().size()))
            .then(new When(new SimpleIntegerProperty(0)
                              .isEqualTo(table.getItems().size()))
                      .then(new Label("No columns and data exist."))
                      .otherwise(new Label("No columns exist.")))
            .otherwise(new When(new SimpleIntegerProperty(0)
                           .isEqualTo(table.getItems().size()))
                           .then(new Label("No data exist."))
                           .otherwise((Label)null)));

Populating a TableColumn with Data

Cells in a row of a TableView contain data related to an item such as a person, a book, and so forth. Data for some cells in a row may come directly from the attributes of the item or they may be computed.

TableView has an items property of the ObservableList<S> type. The generic type S is the same as the generic type of the TableView. It is the data model for the TableView. Each element in the items list represents a row in the TableView. Adding a new item to the items list adds a new row to the TableView. Deleting an item from the items list deletes the corresponding row from the TableView.

Tip

Whether updating an item in the items list updates the corresponding data in the TableView depends on how the cell value factory for the column is set up. I will discuss examples of both kinds in this section.

The following snippet of code creates a TableView in which a row represents a Person object. It adds data for two rows:
TableView<Person> table = new TableView<>();
Person p1 = new Person("John", "Jacobs", null);
Person p2 = new Person("Donna", "Duncan", null);
table.getItems().addAll(p1, p2);
Adding items to a TableView is useless unless you add columns to it. Among several other things, a TableColumn object defines
  • Header text and graphic for the column

  • A cell value factory to populate the cells in the column

The TableColumn class gives you full control over how cells in a column are populated. The cellValueFactory property of the TableColumn class is responsible for populating cells of the column. A cell value factory is an object of the Callback class, which receives a TableColumn.CellDataFeatures object and returns an ObservableValue.

The CellDataFeatures class is a static inner class of the TableColumn class, which wraps the reference of the TableView, TableColumn, and the item for the row for which the cells of the column are being populated. Use the getTableView(), getTableColumn(), and getValue() methods of the CellDataFeatures class to get the reference of the TableView, TableColumn, and the item for the row, respectively.

When the TableView needs the value for a cell, it calls the call() method of the cell value factory object of the column to which the cell belongs. The call() method is supposed to return the reference of an ObservableValue object, which is monitored for any changes. The returned ObservableValue object may contain any type of object. If it contains a node, the node is displayed as a graphic in the cell. Otherwise, the toString() method of the object is called, and the returned string is displayed in the cell.

The following snippet of code creates a cell value factory using an anonymous class. The factory returns the reference of the firstName property of the Person class. Note that a JavaFX property is an ObservableValue.
import static javafx.scene.control.TableColumn.CellDataFeatures;
...
// Create a String column with the header "First Name" for Person object
TableColumn<Person, String> fNameCol = new TableColumn<>("First Name");
// Create a cell value factory object
Callback<CellDataFeatures<Person, String>, ObservableValue<String>> fNameCellFactory =
  new Callback<CellDataFeatures<Person, String>, ObservableValue<String>>() {
    @Override
    public ObservableValue<String> call(CellDataFeatures<Person,
            String> cellData) {
      Person p = cellData.getValue();
      return p.firstNameProperty();
}};
// Set the cell value factory
fNameCol.setCellValueFactory(fNameCellFactory);
Using a lambda expression to create and set a cell value factory comes in handy. The preceding snippet of code can be written as follows:
TableColumn<Person, String> fNameCol = new TableColumn<>("First Name");
fNameCol.setCellValueFactory(cellData ->
    cellData.getValue().firstNameProperty());
When a JavaFX property supplies values for cells in a column, creating the cell value factory is easier if you use an object of the PropertyValueFactory class . You need to pass the name of the JavaFX property to its constructor. The following snippet of code does the same as the code shown earlier. You would take this approach to create TableColumn objects inside the utility methods in the PersonTableUtil class .
TableColumn<Person, String> fNameCol = new TableColumn<>("First Name");
fNameCol.setCellValueFactory(new PropertyValueFactory<>("firstName"));
Tip

Using JavaFX properties as the value supplied for cells has a big advantage. The TableView keeps the value in the property and the cell in sync. Changing the property value in the model automatically updates the value in the cell.

TableColumn also supports POJO (Plain Old Java Object) as items in the TableView. The disadvantage is that when the model is updated, the cell values are not automatically updated. You use the same PropertyValueFactory class to create the cell value factory. The class will look for the public getter and setter methods with the property name you pass. If only the getter method is found, the cell will be read-only. For an xxx property, it tries looking for getXxx() and setXxx() methods using the JavaBeans naming conventions. If the type of xxx is boolean, it also looks for the isXxx() method. If a getter or a setter method is not found, a runtime exception is thrown. The following snippet of code creates a column with the header text Age Category:
TableColumn<Person, Person.AgeCategory> ageCategoryCol =
    new TableColumn<>("Age Category");
ageCategoryCol.setCellValueFactory(new PropertyValueFactory<>("ageCategory"));

It indicates that the item type is Person and the column type is Person.AgeCategory. It passes ageCategory as the property name into the constructor of the PropertyValueFactory class. First, the class will look for an ageCategory property in the Person class. The Person class does not have this property. Therefore, it will try using the Person class as a POJO for this property. Then it will look for getAgeCategory() and setAgeCategory() methods in the Person class. It finds only the getter method, getAgeCategory(), and hence it will make the column read-only.

The values in the cells of a column do not necessarily have to come from JavaFX or POJO properties. They can be computed using some logic. In such cases, you need to create a custom cell value factory and return a ReadOnlyXxxWrapper object that wraps the computed value. The following snippet of code creates an Age column that displays a computed age in years:
TableColumn<Person, String> ageCol = new TableColumn<>("Age");
ageCol.setCellValueFactory(cellData -> {
        Person p = cellData.getValue();
        LocalDate dob = p.getBirthDate();
        String ageInYear = "Unknown";
        if (dob != null) {
                long years = YEARS.between(dob, LocalDate.now());
                if (years == 0) {
                        ageInYear = "< 1 year";
                } else if (years == 1) {
                        ageInYear = years + " year";
                } else {
                        ageInYear = years + " years";
                }
        }
        return new ReadOnlyStringWrapper(ageInYear);
});
This completes the different ways of setting the cell value factory for cells of a column in a TableView. The program in Listing 13-3 creates cell value factories for JavaFX properties, a POJO property, and a computed value. It displays a window as shown in Figure 13-6.
// TableViewDataTest.java
// ...find in the book's download area
Listing 13-3

Setting Cell Value Factories for Columns

Figure 13-6

A TableView having columns for JavaFX properties, POJO properties, and computed values

Cells in a TableView can display text and graphics. If the cell value factory returns an instance of the Node class, which could be an ImageView, the cell displays it as graphic. Otherwise, it displays the string returned from the toString() method of the object. It is possible to display other controls and containers in cells. However, a TableView is not meant for that, and such uses are discouraged. Sometimes, using a specific type of control in a cell, for example, a check box, to show or edit a boolean value provides a better user experience. I will cover such customization of cells shortly.

Using a Map As Items in a TableView

Sometimes, data in a row for a TableView may not map to a domain object, for example, you may want to display the result set of a dynamic query in a TableView. The items list consists of an observable list of Map. A Map in the list contains values for all columns in the row. You can define a custom cell value factory to extract the data from the Map. The MapValueFactory class is especially designed for this purpose. It is an implementation of the cell value factory, which reads data from a Map for a specified key.

The following snippet of code creates a TableView of Map . It creates an Id column and sets an instance of the MapValueFactory class as its cell value factory specifying the idColumnKey as the key that contains the value for the Id column. It creates a Map and populates the Id column using the idColumnKey . You need to repeat these steps for all columns and rows.
TableView<Map> table = new TableView<>();
// Define the column, its cell value factory and add it to the TableView
String idColumnKey = "id";
TableColumn<Map, Integer> idCol = new TableColumn<>("Id");
idCol.setCellValueFactory(new MapValueFactory<>(idColumnKey));
table.getColumns().add(idCol);
// Create and populate a Map an item
Map row1 = new HashMap();
row1.put(idColumnKey, 1);
// Add the Map to the TableView items list
table.getItems().add(row1);
The program in Listing 13-4 shows how to use the MapValueFactory as the cell value factory for columns in a TableView. It displays the person’s data returned by the getPersonList() method in the PersonTableUtil class.
// TableViewMapDataTest.java
// ...find in the book's download area
Listing 13-4

Using MapValueFactory As a Cell Value Factory for Cells in a TableView

Showing and Hiding Columns

By default, all columns in a TableView are visible. The TableColumn class has a visible property to set the visibility of a column. If you turn off the visibility of a parent column, a column with nested columns, all of its nested columns will also be invisible:
TableColumn<Person, String> idCol = new TableColumn<>("Id");
// Make the Id column invisible
idCol.setVisible(false);
...
// Make the Id column visible
idCol.setVisible(true);
Sometimes, you may want to let the user control the visibility of columns. The TableView class has a tableMenuButtonVisible property. If it is set to true, a menu button is displayed in the header area:
// Create a TableView
TableView<Person> table = create the TableView here...
// Make the table menu button visible
table.setTableMenuButtonVisible(true);
Clicking the menu button displays a list of all leaf columns. Columns are displayed as radio menu items that can be used to toggle their visibility. Figure 13-7 shows a TableView with four columns. Its tableMenuButtonVisible property is set to true. The figure shows a menu with all column names with a check mark. The menu is displayed when the menu button is clicked. The check marks beside the column names indicate that the columns are visible. Clicking the column name toggles its visibility.
Figure 13-7

A TableView with menu button to toggle the visibility of columns

Reordering Columns in a TableView

You can rearrange columns in a TableView in two ways:
  • By dragging and dropping columns to a different position

  • By changing their positions in the observable list as returned by the getColumns() method of the TableView class

The first option is available by default. The user needs to drag and drop a column at the new position. When a column is reordered, its position in the columns list is changed. The second option will reorder the column directly in the columns list.

There is no easy way to disable the default column-reordering feature. If you want to disable the feature, you would need to add a ChangeListener to the ObservableList returned by the getColumns() method of the TableView. When a change is reported, reset the columns so they are in the original order again.

To enable or disable the column-reordering feature, use the setReorderable() method on the columns:
table.getColumns().forEach(c -> {
      boolean b = ...; // determine whether column is reorderable
      c.setReorderable(b);
});

Sorting Data in a TableView

TableView has built-in support for sorting data in columns. By default, it allows users to sort data by clicking column headers. It also supports sorting data programmatically. You can also disable sorting for a column or all columns in a TableView.

Sorting Data by Users

By default, data in all columns in a TableView can be sorted. Users can sort data in columns by clicking the column headers. The first click sorts the data in ascending order. The second click sorts the data in descending order. The third click removes the column from the sort order list.

By default, single column sorting is enabled. That is, if you click a column, the records in the TableView are sorted based on the data only in the clicked column. To enable multicolumn sorting, you need to press the Shift key while clicking the headers of the columns to be sorted.

TableView displays visual clues in the headers of the sorted columns to indicate the sort type and the sort order. By default, a triangle is displayed in the column header indicating the sort type. It points upward for the ascending sort type and downward for the descending sort type. The sort order of a column is indicated by dots or a number. Dots are used for the first three columns in the sort order list. A number is used for the fourth column onward. For example, the first column in the sort order list displays one dot, the second two dots, the third three dots, the fourth a number 4, the fifth a number 5, and so forth.

Figure 13-8 shows a TableView with four columns. The column headers are showing the sort type and sort orders. The sort types are descending for Last Name and ascending for others. The sort orders are 1, 2, 3, and 4 for Last Name, First Name, Birth Date, and Id, respectively. Notice that dots are used for the sort orders in the first three columns, and a number 4 is used for the Id column because it is fourth on the sort order list. This sorting is achieved by clicking column headers in the following order: Last Name (twice), First Name, Birth Date, and Id.
Figure 13-8

Column headers showing the sort type and sort order

Sorting Data Programmatically

Data in columns can be sorted programmatically. The TableView and TableColumn classes provide a very powerful API for sorting. The sorting API consists of several properties and methods in the two classes. Every part and every stage of sorting are customizable. The following sections describe the API with examples.

Making a Column Sortable

The sortable property of a TableColumn determines whether the column is sortable. By default, it is set to true. Set it to false to disable the sorting for a column:
// Disable sorting for fNameCol column
fNameCol.setSortable(false);

Specifying the Sort Type of a Column

A TableColumn has a sort type, which can be ascending or descending. It is specified through the sortType property. The ASCENDING and DESCENDING constants of TableColumn.SortType enum represent the ascending and descending, respectively, sort types for columns. The default value for the sortType property is TableColumn.SortType.ASCENDING. The DESCENDING constant is set as follows:
// Set the sort type for fNameCol column to descending
fNameCol.setSortType(TableColumn.SortType.DESCENDING);

Specifying the Comparator for a Column

A TableColumn uses a Comparator to sort its data. You can specify the Comparator for a TableColumn using its comparator property . The comparator is passed in the objects in two cells being compared. A TableColumn uses a default Comparator, which is represented by the constant TableColumn.DEFAULT_COMPARATOR. The default comparator compares data in two cells using the following rules:
  • It checks for null values. The null values are sorted first. If both cells have null, they are considered equal.

  • If the first value being compared is an instance of the Comparable interface, it calls the compareTo() method of the first object passing the second object as an argument to the method.

  • If neither of the preceding two conditions are true, it converts the two objects into strings calling their toString() methods and uses a Comparator to compare the two String values.

In most cases, the default comparator is sufficient. The following snippet of code uses a custom comparator for a String column that compares only the first characters of the cell data:
TableColumn<Person, String> fNameCol = new TableColumn<>("First Name");
...
// Set a custom comparator
fNameCol.setComparator((String n1, String n2) -> {
        if (n1 == null && n2 == null) {
                return 0;
        }
        if (n1 == null) {
                return -1;
        }
        if (n2 == null) {
                return 1;
        }
        String c1 = n1.isEmpty()? n1:String.valueOf(n1.charAt(0));
        String c2 = n2.isEmpty()? n2:String.valueOf(n2.charAt(0));
        return c1.compareTo(c2);
});

Specifying the Sort Node for a Column

The TableColumn class contains a sortNode property , which specifies a node to display a visual clue in the column header about the current sort type and sort order for the column. The node is rotated by 180 degrees when the sort type is ascending. The node is invisible when the column is not part of the sort. By default, it is null and the TableColumn provides a triangle as the sort node.

Specifying the Sort Order of Columns

The TableView class contains several properties that are used in sorting. To sort columns, you need to add them to the sort order list of the TableView. The sortOrder property specifies the sort order. It is an ObservableList of TableColumn. The order of a TableColumn in the list specifies the order of the column in the sort. Rows are sorted based on the first column in the list. If values in two rows in the column are equal, the second column in the sort order list is used to determine the sort order of the two rows and so on.

The following snippet of code adds two columns to a TableView and specifies their sort order. Notice that both columns will be sorted in ascending order, which is the default sort type. If you want to sort them in descending order, set their sortType property as follows:
// Create a TableView with data
TableView<Person> table = new TableView<>(PersonTableUtil.getPersonList());
TableColumn<Person, String> lNameCol = PersonTableUtil.getLastNameColumn();
TableColumn<Person, String> fNameCol = PersonTableUtil.getFirstNameColumn();
// Add columns to the TableView
table.getColumns().addAll(lNameCol, fNameCol );
// Add columns to the sort order to sort by last name followed by first name
table.getSortOrder().addAll(lNameCol, fNameCol);

The sortOrder property of the TableView is monitored for changes. If it is modified, the TableView is sorted immediately based on the new sort order. Adding a column to a sort order list does not guarantee inclusion of the column in sorting. The column must also be sortable to be included in sorting. The sortType property of the TableColumn is also monitored for changes. Changing the sort type of a column, which is in the sort order list, resorts the TableView data immediately.

Getting the Comparator for a TableView

TableView contains a read-only comparator property , which is a Comparator based on the current sort order list. You rarely need to use this Comparator in your code. If you pass two TableView items to the compare() method of the Comparator, it will return a negative integer, zero, or a positive integer indicating that the first item is less than, equal to, or greater than the second item, respectively.

Recall that TableColumn also has a comparator property, which is used to specify how to determine the order of values in the cells of the TableColumn. The comparator property of the TableView combines the comparator properties of all TableColumns in its sort order list.

Specifying the Sort Policy

A TableView has a sort policy to specify how the sorting is performed. It is a Callback object. The TableView is passed in as an argument to the call() method. The method returns true if the sorting succeeds. It returns false or null if the sorting fails.

The TableView class contains a DEFAULT_SORT_POLICY constant, which is used as a default sort policy for a TableView. It sorts the items list of the TableView using its comparator property. Specify a sort policy to take full charge of the sorting algorithm. The call() method of the sort policy Callback object will perform the sorting of the items of the TableView.

As a trivial example, setting the sort policy to null will disable the sorting, as no sorting will be performed when sorting is requested by the user or program:
TableView<Person> table = ...
// Disable sorting for the TableView
table.setSortPolicy(null);
Sometimes, it is useful to disable sorting temporarily for performance reasons. Suppose you have a sorted TableView with a large number of items, and you want to make several changes to the sort order list. Every change in the sort order list will trigger a sort on the items. In this case, you may disable the sorting by setting the sort policy to null, make all your changes, and enable the sorting by restoring the original sort policy. A change in the sort policy triggers an immediate sort. This technique will sort the items only once:
TableView<Person> table = ...
...
// Store the current sort policy
Callback<TableView<Person>, Boolean> currentSortPolicy =
    table.getSortPolicy();
// Disable the sorting
table.setSortPolicy(null)
// Make all changes that might need or trigger sorting
...
// Restore the sort policy that will sort the data once immediately
table.setSortPolicy(currentSortPolicy);

Sorting Data Manually

TableView contains a sort() method that sorts the items in the TableView using the current sort order list. You may call this method to sort items after adding a number of items to a TableView. This method is automatically called when the sort type of a column, the sort order, or sort policy changes.

Handling Sorting Event

TableView fires a SortEvent when it receives a request for sorting and just before it applies the sorting algorithm to its items. Add a SortEvent listener to perform any action before the actual sorting is performed:
TableView<Person> table = ...
table.setOnSort(e -> {/* Code to handle the sort event */});
If the SortEvent is consumed, the sorting is aborted. If you want to disable sorting for a TableView, consume the SortEvent as follows:
// Disable sorting for the TableView
table.setOnSort(e -> e.consume());

Disabling Sorting for a TableView

There are several ways you can disable sorting for a TableView :
  • Setting the sortable property for a TableColumn disables sorting only for that column. If you set the sortable property to false for all columns in a TableView, the sorting for the TableView is disabled.

  • You can set the sort policy for the TableView to null.

  • You can consume the SortEvent for the TableView.

  • Technically, it is possible, though not recommended, to override the sort() method of the TableView class and provide an empty body for the method.

The best way to disable sorting partially or completely for a TableView is to disable sorting for some or all of its columns.

Customizing Data Rendering in Cells

A cell in a TableColumn is an instance of the TableCell class, which displays the data in the cell. A TableCell is a Labeled control, which is capable of displaying text, a graphic, or both.

You can specify a cell factory for a TableColumn. The job of a cell factory is to render the data in the cell. The TableColumn class contains a cellFactory property , which is a Callback object. Its call() method is passed in the reference of the TableColumn to which the cell belongs. The method returns an instance of TableCell. The updateItem() method of the TableCell is overridden to provide the custom rendering of the cell data.

TableColumn uses a default cell factory if its cellFactory property is not specified. The default cell factory displays the cell data depending on the type of the data. If the cell data comprise a node, the data are displayed in the graphic property of the cell. Otherwise, the toString() method of the cell data is called, and the returned string is displayed in the text property of the cell.

Up to this point, you have been using a list of Person objects as the data model in the examples for displaying data in a TableView. The Birth Date column is formatted as yyyy-mm-dd, which is the default ISO date format returned by the toString() method of the LocalDate class . If you would like to format birth dates in the mm/dd/yyyy format, you can achieve this by setting a custom cell factory for the Birth Date column:
TableColumn<Person, LocalDate> birthDateCol = ...;
birthDateCol.setCellFactory (col -> {
    TableCell<Person, LocalDate> cell =
        new TableCell<Person, LocalDate>() {
          @Override
          public void updateItem(LocalDate item, boolean empty) {
              super.updateItem(item, empty);
              // Cleanup the cell before populating it
              this.setText(null);
              this.setGraphic(null);
              if (!empty) {
                  // Format the birth date in mm/dd/yyyy format
                  String formattedDob =
                      DateTimeFormatter.ofPattern("MM/dd/yyyy").
                         format(item);
                  this.setText(formattedDob);
              }
          }
    };
    return cell;
});

You can also use the preceding technique to display images in cells. In the updateItem() method , create an ImageView object for the image and display it using the setGraphic() method of the TableCell. TableCell contains tableColumn, tableRow, and tableView properties that store the references of its TableColumn, TableRow, and TableView, respectively. These properties are useful to access the item in the data model that represents the row for the cell.

If you replace the if statement in the preceding snippet of code with the following code, the Birth Date column displays the birth date and age category, for example, 10/11/2012 (BABY):
if (!empty) {
    String formattedDob =
         DateTimeFormatter.ofPattern("MM/dd/yyyy").format(item);
    if (this.getTableRow() != null ) {
        // Get the Person item for this cell
        int rowIndex = this.getTableRow().getIndex();
        Person p = this.getTableView().getItems().get(rowIndex);
        String ageCategory = p.getAgeCategory().toString();
        // Display birth date and age category together
        this.setText(formattedDob + " (" + ageCategory + ")" );
    }
}
The following are subclasses of TableCell that render cell data in different ways. For example, a CheckBoxTableCell renders cell data in a check box, and a ProgressBarTableCell renders a number using a progress bar:
  • CheckBoxTableCell

  • ChoiceBoxTableCell

  • ComboBoxTableCell

  • ProgressBarTableCell

  • TextFieldTableCell

The following snippet of code creates a column labeled Baby? and sets a cell factory to display the value in a CheckBoxTableCell. The forTableColumn(TableColumn<S, Boolean> col) method of the CheckBoxTableCell class returns a Callback object that is used as a cell factory:
// Create a "Baby?" column
TableColumn<Person, Boolean> babyCol = new TableColumn<>("Baby?");
babyCol.setCellValueFactory(cellData -> {
        Person p = cellData.getValue();
        Boolean v =  (p.getAgeCategory() == Person.AgeCategory.BABY);
        return new ReadOnlyBooleanWrapper(v);
});
// Set a cell factory that will use a CheckBox to render the value
babyCol.setCellFactory(CheckBoxTableCell.<Person>forTableColumn(babyCol));

Please explore the API documentation for other subclasses of the TableCell and how to use them. For example, you can display a combo box with a list of choices in the cells of a column. Users can select one of the choices as the cell data.

Listing 13-5 has a complete program to show how to use custom cell factories. It displays a window as shown in Figure 13-9. The program uses a cell factory to format the birth date in mm/dd/yyyy format and a cell factory to display whether a person is a baby using a check box.
// TableViewCellFactoryTest.java
// ...find in the book's download area
Listing 13-5

Using a Custom Cell Factory for a TableColumn

Figure 13-9

Using custom cell factories to format data in cells and display cell data in check boxes

Selecting Cells and Rows in a TableView

TableView has a selection model represented by its property selectionModel. A selection model is an instance of the TableViewSelectionModel class , which is an inner static class of the TableView class. The selection model supports cell-level and row-level selection. It also supports two selection modes: single and multiple. In the single selection mode, only one cell or row can be selected at a time. In the multiple selection mode, multiple cells or rows can be selected. By default, single-row selection is enabled. You can enable multirow selection, as follows:
TableView<Person> table = ...
// Turn on multiple-selection mode for the TableView
TableViewSelectionModel<Person> tsm = table.getSelectionModel();
tsm.setSelectionMode(SelectionMode.MULTIPLE);
The cell-level selection can be enabled by setting the cellSelectionEnabled property of the selection model to true, as in the following snippet of code. When the property is set to true, the TableView is put in cell-level selection mode, and you cannot select an entire row. If multiple selection mode is enabled, you can still select all cells in a row. However, the row itself is not reported as selected as the TableView is in the cell-level selection mode. By default, cell-level selection mode is false.
// Enable cell-level selection
tsm.setCellSelectionEnabled(true);
The selection model provides information about the selected cells and rows. The isSelected(int rowIndex) method returns true if the row at the specified rowIndex is selected. Use the isSelected(int rowIndex, TableColumn<S,?> column) method to know if a cell at the specified rowIndex and column is selected. The selection model provides several methods to select cells and rows and get the report of selected cells and rows:
  • The selectAll() method selects all cells or rows.

  • The select() method is overloaded. It selects a row, a row for an item, and a cell.

  • The isEmpty() method returns true if there is no selection. Otherwise, it returns false.

  • The getSelectedCells() method returns a read-only ObservableList<TablePosition> that is the list of currently selected cells. The list changes as the selection in the TableView changes.

  • The getSelectedIndices() method returns a read-only ObservableList<Integer> that is the list of currently selected indexes. The list changes as the selection in the TableView changes. If row-level selection is enabled, an item in the list is the row index of the selected row. If cell-level selection is enabled, an item in the list is the row index of the row in which one or more cells are selected.

  • The getSelectedItems() method returns a read-only ObservableList<S> where S is the generic type of the TableView. The list contains all items for which the corresponding row or cells have been selected.

  • The clearAndSelect() method is overloaded. It lets you clear all selections before selecting a row or a cell.

  • The clearSelection() method is overloaded. It lets you clear selections for a row, a cell, or the entire TableView.

It is often a requirement to make some changes or take an action when a cell or row selection changes in a TableView. For example, a TableView may act as a master list in a master-detail data view. When the user selects a row in the master list, you want to refresh the data in the detail view. If you are interested in handling the selection change event, you need to add a ListChangeListener to one of the ObservableLists returned by the preceding listed methods that reports on the selected cells or rows. The following snippet of code adds a ListChangeListener to the ObservableList returned by the getSelectedIndices() method to track the row selection change in a TableView:
TableView<Person> table = ...
TableViewSelectionModel<Person> tsm = table.getSelectionModel();
ObservableList<Integer> list = tsm.getSelectedIndices();
// Add a ListChangeListener
list.addListener((ListChangeListener.Change<? extends Integer> change) -> {
        System.out.println("Row selection has changed");
});

Editing Data in a TableView

A cell in a TableView can be edited. An editable cell switches between editing and nonediting modes. In editing mode, cell data can be modified by the user. For a cell to enter editing mode, the TableView, TableColumn, and TableCell must be editable. All three of them have an editable property, which can be set to true using the setEditable(true) method. By default, TableColumn and TableCell are editable. To make cells editable in a TableView, you need to make the TableView editable :
TableView<Person> table = ...
table.setEditable(true);
The TableColumn class supports three types of events:
  • onEditStart

  • onEditCommit

  • onEditCancel

The onEditStart event is fired when a cell in the column enters editing mode. The onEditCommit event is fired when the user successfully commits the editing, for example, by pressing the Enter key in a TextField. The onEditCancel event is fired when the user cancels the editing, for example, by pressing the Esc key in a TextField.

The events are represented by an object of the TableColumn.CellEditEvent class. The event object encapsulates the old and new values in the cell, the row object from the items list of the TableView, TableColumn, TablePosition indicating the cell position where the editing is happening, and the reference of the TableView. Use the methods of the CellEditEvent class to get these values.

Making a TableView editable does not let you edit its cell data. You need to do a little more plumbing before you can edit data in cells. Cell-editing capability is provided through specialized implementation of the TableCell class. The JavaFX library provides a few of these implementations. Set the cell factory for a column to use one of the following implementations of the TableCell to edit cell data:
  • CheckBoxTableCell

  • ChoiceBoxTableCell

  • ComboBoxTableCell

  • TextFieldTableCell

Editing Data Using a Check Box

A CheckBoxTableCell renders a check box inside the cell. Typically, it is used to represent a boolean value in a column. The class provides a way to map other types of values to a boolean value using a Callback object. The check box is selected if the value is true. Otherwise, it is unselected. Bidirectional binding is used to bind the selected property of the check box and the underlying ObservableValue. If the user changes the selection, the underlying data are updated and vice versa.

You do not have a boolean property in the Person class. You must create a boolean column by providing a cell value factory, as shown in the following code. If a Person is a baby, the cell value factory returns true. Otherwise, it returns false.
TableColumn<Person, Boolean> babyCol = new TableColumn<>("Baby?");
babyCol.setCellValueFactory(cellData -> {
        Person p = cellData.getValue();
        Boolean v =  (p.getAgeCategory() == Person.AgeCategory.BABY);
        return new ReadOnlyBooleanWrapper(v);
});
Getting a cell factory to use CheckBoxTableCell is easy. Use the forTableColumn() static method to get a cell factory for the column:
// Set a CheckBoxTableCell to display the value
babyCol.setCellFactory(CheckBoxTableCell.<Person>forTableColumn(babyCol));

A CheckBoxTableCell does not fire the cell-editing events. The selected property of the check box is bound to the ObservableValue representing the data in the cell. If you are interested in tracking the selection change event, you need to add a ChangeListener to the data for the cell.

Editing Data Using a Choice Box

A ChoiceBoxTableCell renders a choice box with a specified list of values inside the cell. The type of values in the list must match the type of the TableColumn. The data in a ChoiceBoxTableCell are displayed in a Label when the cell is not being edited. A ChoiceBox is used when the cell is being edited.

The Person class does not have a gender property. You want to add a Gender column to a TableView<Person>, which can be edited using a choice box. The following snippet of code creates the TableColumn and sets a cell value factory, which sets all cells to an empty string. You would set the cell value factory to use the gender property of the Person class if you had one.
// Gender is a String, editable, ComboBox column
TableColumn<Person, String> genderCol = new TableColumn<>("Gender");
// Use an appropriate cell value factory.
// For now, set all cells to an empty string
genderCol.setCellValueFactory(cellData -> new ReadOnlyStringWrapper(""));
You can create a cell factory that uses a choice box for editing data in cells using the forTableColumn() static method of the ChoiceBoxTableCell class. You need to specify the list of items to be displayed in the choice box:
// Set a cell factory, so it can be edited using a ChoiceBox
genderCol.setCellFactory(
        ChoiceBoxBoxTableCell.<Person, String>forTableColumn(
               "Male", "Female")
);
When an item is selected in the choice box, the item is set to the underlying data model. For example, if a column is based on a property in the domain object, the selected item will be set to the property. You can set an onEditCommit event handler that is fired when the user selects an item. The following snippet of code adds such a handler for the Gender column that prints a message on the standard output:
// Add an onEditCommit handler
genderCol.setOnEditCommit(e -> {
        int row = e.getTablePosition().getRow();
        Person person = e.getRowValue();
        System.out.println("Gender changed (" + person.getFirstName() +
               " " + person.getLastName() + ")" + " at row " + (row + 1) +
           ". New value = " + e.getNewValue());
});

Clicking a selected cell puts the cell into editing mode. Double-clicking an unselected cell puts the cell into editing mode. Changing the focus to another cell or selecting an item from the list puts the editing cell into nonediting mode, and the current value is displayed in a Label.

Editing Data Using a Combo Box

A ComboBoxTableCell renders a combo box with a specified list of values inside the cells. It works similar to a ChoiceBoxTableCell. Please refer to the section “Editing Data Using a Choice Box” for more details.

Editing Data Using a TextField

A TextFieldTableCell renders a TextField inside the cell when the cell is being edited where the user can modify the data. It renders the cell data in a Label when the cell is not being edited.

Clicking a selected cell or double-clicking an unselected cell puts the cell into editing mode, which displays the cell data in a TextField. Once the cell is in editing mode, you need to click in the TextField (one more click!) to put the caret in the TextField so you can make changes. Notice that you need a minimum of three clicks to edit a cell, which is a pain for those users who have to edit a lot of data. Let’s hope that the designers of the TableView API will make data editing less cumbersome in future releases.

If you are in the middle of editing a cell data, press the Esc key to cancel editing, which will return the cell to nonediting mode and reverts to the old data in the cell. Pressing the Enter key commits the data to the underlying data model if the TableColumn is based on a Writable ObservableValue.

If you are editing a cell using a TextFieldTableCell, moving the focus to another cell, for example, by clicking another cell, cancels the editing and puts the old value back in the cell. This is not what a user expects. At present, there is no easy solution for this problem. You will have to create a subclass of TableCell and add a focus change listener, so you can commit the data when the TextField loses focus.

Use the forTableColumn() static method of the TextFieldTableCell class to get a cell factory that uses a TextField to edit cell data. The following snippet of code shows how to do it for a First Name String column:
TableColumn<Person, String> fNameCol = new TableColumn<>("First Name");
fNameCol.setCellFactory(TextFieldTableCell.<Person>forTableColumn());
Sometimes , you need to edit nonstring data using a TextField, for example, for a date. The date may be represented as an object of the LocalDate class in the model. You may want to display it in a TextField as a formatted string. When the user edits the date, you want to commit the data to the model as a LocalDate. The TextFieldTableCell class supports this kind of object-to-string and vice versa conversion through a StringConverter. The following snippet of code sets a cell factory for a Birth Date column with a StringConverter, which converts a string to a LocalDate and vice versa. The column type is LocalDate. By default, the LocalDateStringConverter assumes a date format of mm/dd/yyyy.
TableColumn<Person, LocalDate> birthDateCol = new TableColumn<>("Birth Date");
LocalDateStringConverter converter = new LocalDateStringConverter();
birthDateCol.setCellFactory(
    TextFieldTableCell.<Person, LocalDate>forTableColumn(converter));
The program in Listing 13-6 shows how to edit data in a TableView using different types of controls. The TableView contains Id, First Name, Last Name, Birth Date, Baby, and Gender columns. The Id column is noneditable. The First Name, Last Name, and Birth Date columns use TextFieldTableCell, so they can be edited using a TextField . The Baby column is a noneditable computed field and is not backed by the data model. It uses CheckBoxTableCell to render its values. The Gender column is an editable computed field. It is not backed by the data model. It uses a ComboBoxTableCell that presents the user a list of values (Male and Female) in editing mode. When the user selects a value, the value is not saved to the data model. It stays in the cell. An onEditCommit event handler is added that prints the gender selection on the standard output. The program displays a window as shown in Figure 13-10, where it can be seen that you have already selected a gender value for all persons. The Birth Date value for the fifth row is being edited.
// TableViewEditing.java
// ...find in the book's download area
Listing 13-6

Editing Data in a TableView

Figure 13-10

A TableView with a cell in editing mode

Editing Data in TableCell Using Any Control

In the previous section, I discussed editing data in cells of a TableView using different controls, for example, TextField, CheckBox, and ChoiceBox. You can subclass TableCell to use any control to edit cell data. For example, you may want to use a DatePicker to select a date in cells of a date column or RadioButtons to select from multiple options. The possibilities are endless.

You need to override four methods of the TableCell class :
  • startEdit()

  • commitEdit()

  • cancelEdit()

  • updateItem()

The startEdit() method for the cell transitions from nonediting mode to editing mode. Typically, you set the control of your choice in the graphic property of the cell with the current data.

The commitEdit() method is called when the user action, for example, pressing the Enter key in a TextField, indicates that the user is done modifying the cell data and the data need to be saved in the underlying data model. Typically, you do not need to override this method as the modified data are committed to the data model if the TableColumn is based on a Writable ObservableValue.

The cancelEdit() method is called when the user action, for example, pressing the Esc key in a TextField, indicates that the user wants to cancel the editing process. When the editing process is cancelled, the cell returns to nonediting mode. You need to override this method and revert the cell data to their old values.

The updateItem() method is called when the cell needs to be rendered again. Depending on the editing mode, you need to set the text and graphic properties of the cell appropriately.

Now let’s develop a DatePickerTableCell class that inherits from the TableCell class. You can use instances of DatePickerTableCell when you want to edit cells of a TableColumn using a DatePicker control. The TableColumn must be of LocalDate. Listing 13-7 has the complete code for the DatePickerTableCell class .
// DatePickerTableCell.java
// ...find in the book's download area
Listing 13-7

The DatePickerTableCell Class to Allow Editing Table Cells Using a DatePicker Control

The DatePickerTableCell class supports a StringConverter and the editable property value for the DatePicker. You can pass them to the constructors or the forTableColumn() methods. It creates a DatePicker control when the startEdit() method is called for the first time. A ChangeListener is added that commits the data when a new date is entered or selected. Several versions of the forTableColumn() static methods are provided that return cell factories. The following snippet of code shows how to use the DatePickerTableCell class :
TableColumn<Person, LocalDate> birthDateCol = ...
// Set a cell factory for birthDateCol. The date format is mm/dd/yyyy
// and the DatePicker is editable.
birthDateCol.setCellFactory(DatePickerTableCell.<Person>forTableColumn());
// Set a cell factory for birthDateCol. The date format is "Month day, year"
// and and the DatePicker is non-editable
StringConverter converter = new LocalDateStringConverter("MMMM dd, yyyy");
birthDateCol.setCellFactory(DatePickerTableCell.<Person>forTableColumn(
    converter, false));
The program in Listing 13-8 uses DatePickerTableCell to edit data in the cells of a Birth Date column. Run the application and then double-click a cell in the Birth Date column. The cell will display a DatePicker control. You cannot edit the date in the DatePicker, as it is noneditable. You will need to select a date from the pop-up calendar.
// CustomTableCellTest.java
// ...find in the book's download area
Listing 13-8

Using DatePickerTableCell to Edit a Date in Cells

Adding and Deleting Rows in a TableView

Adding and deleting rows in a TableView are easy. Note that each row in a TableView is backed by an item in the items list. Adding a row is as simple as adding an item in the items list. When you add an item to the items list, a new row appears in the TableView at the same index as the index of the added item in the items list. If the TableView is sorted, it may need to be resorted after adding a new row. Call the sort() method of the TableView to resort the rows after adding a new row.

You can delete a row by removing its item from the items list. An application provides a way for the user to indicate the rows that should be deleted. Typically, the user selects one or more rows to delete. Other options are to add a Delete button to each row or to provide a Delete check box to each row. Clicking the Delete button should delete the row. Selecting the Delete check box for a row indicates that the row is marked for deletion.

The program in Listing 13-9 shows how to add and delete rows to a TableView. It displays a window with three sections:
  • The Add Person form at the top has three fields to add person details and an Add button. Enter the details for a person and click the Add button to add a record to the TableView. Error checking is skipped in the code.

  • In the middle, you have two buttons. One button is used to restore the default rows in the TableView. Another button deletes the selected rows.

  • At the bottom, a TableView is displayed with some rows. The multirow selection is enabled. Use the Ctrl or Shift key with the mouse to select multiple rows.

// TableViewAddDeleteRows.java
// ...find in the book's download area
Listing 13-9

Adding and Deleting Rows in a TableView

Most of the logic in the code is simple. The deleteSelectedRows() method implements the logic to delete the selected rows. When you remove an item from the items list, the selection model does not remove its index. Suppose the first row is selected. If you remove the first item from the items list, the second row, which becomes the first row, is selected. To make sure that this does not happen, you clear the selection for the row before you remove it from the items list. You delete rows from last to first (higher index to lower index) because when you delete an item from the list, all of the items after the deleted items will have different indexes. Suppose you have selected rows at indexes 1 and 2. Deleting a row at index 1 first changes the index of the index 2 to 1. Performing deletion from last to first takes care of this issue.

Scrolling in a TableView

TableView automatically provides vertical and horizontal scrollbars when rows or columns fall beyond the available space. Users can use the scrollbars to scroll to a specific row or column. Sometimes, you need programmatic support for scrolling. For example, when you append a row to a TableView, you may want the row visible to the user by scrolling it to the view. The TableView class contains four methods that can be used to scroll to a specific row or column:
  • scrollTo(int rowIndex)

  • scrollTo(S item)

  • scrollToColumn(TableColumn<S,?> column)

  • scrollToColumnIndex(int columnIndex)

The scrollTo() method scrolls the row with the specified index or item to the view. The scrollToColumn() and scrollToColumnIndex() methods scroll to the specified column and columnIndex, respectively.

TableView fires a ScrollToEvent when there is a request to scroll to a row or column using one of the abovementioned scrolling methods. The ScrollToEvent class contains a getScrollTarget() method that returns the row index or the column reference depending on the scroll type:
TableView<Person> table = ...
// Add a ScrollToEvent for row scrolling
table.setOnScrollTo(e -> {
        int rowIndex = e.getScrollTarget();
        System.out.println("Scrolled to row " + rowIndex);
});
// Add a ScrollToEvent for column scrolling
table.setOnScrollToColumn(e -> {
        TableColumn<Person, ?> column = e.getScrollTarget();
        System.out.println("Scrolled to column " + column.getText());
});
Tip

The ScrollToEvent is not fired when the user scrolls through the rows and columns. It is fired when you call one of the four scrolling-related methods of the TableView class.

Resizing a TableColumn

Whether a TableColumn is resizable by the user is specified by its resizable property. By default, a TableColumn is resizable. How a column in a TableView is resized is specified by the columnResizePolicy property of the TableView. The property is a Callback object. Its call() method takes an object of the ResizeFeatures class, which is a static inner class of the TableView class. The ResizeFeatures object encapsulates the delta by which the column is resized, the TableColumn being resized, and the TableView. The call() method returns true if the column was resized by the delta amount successfully. Otherwise, it returns false.

The TableView class provides two built-in resize policies as constants:
  • CONSTRAINED_RESIZE_POLICY

  • UNCONSTRAINED_RESIZE_POLICY

CONSTRAINED_RESIZE_POLICY ensures that the sum of the width of all visible leaf columns is equal to the width of the TableView. Resizing a column adjusts the width of all columns to the right of the resized column. When the column width is increased, the width of the rightmost column is decreased up to its minimum width. If the increased width is still not compensated, the width of the second rightmost column is decreased up to its minimum width and so on. When all columns to the right have their minimum widths, the column width cannot be increased any more. The same rule applies in the opposite direction when a column is resized to decrease its width.

When the width of a column is increased, UNCONSTRAINED_RESIZE_POLICY shifts all columns to its right by the amount the width is increased. When the width is decreased, columns to the right are shifted to the left by the same amount. If a column has nested columns, resizing the column evenly distributes the delta among the immediate children columns. This is the default column resize policy for a TableView:
TableView<Person> table = ...;
// Set the column resize policy to constrained resize policy
table.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY);
You can also create a custom column resize policy. The following snippet of code will serve as a template. You will need to write the logic to consume the delta, which is the difference between the new and old width of the column:
TableView<Person> table = new TableView<>(PersonTableUtil.getPersonList());
table.setColumnResizePolicy(resizeFeatures -> {
    boolean consumedDelta = false; double delta = resizeFeatures.getDelta();
    TableColumn<Person, ?> column = resizeFeatures.getColumn();
    TableView<Person> tableView = resizeFeatures.getTable();
    // Adjust the delta here...
    return consumedDelta;
});
You can disable column resizing by setting a trivial callback that does nothing. Its call() simply returns true indicating that it has consumed the delta:
// Disable column resizing
table.setColumnResizePolicy(resizeFeatures -> true);

Styling a TableView with CSS

You can style a TableView and all its parts, for example, column headers, cells, placeholder, and so forth. Applying a CSS to TableView is very complex and broad in scope. This section covers a brief overview of CSS styling for TableView. The default CSS style class name for a TableView is table-view. The default CSS style classes for a cell, a row, and a column header are table-cell, table-row-cell, and column-header, respectively:
/* Set the font for the cells */
.table-row-cell {
        -fx-font-size: 10pt;
        -fx-font-family: Arial;
}
/* Set the font size and text color for column headers */
.table-view .column-header .label{
        -fx-font-size: 10pt;
        -fx-text-fill: blue;
}
TableView supports the following CSS pseudo-classes:
  • cell-selection

  • row-selection

The cell-selection pseudo-class is applied when the cell-level selection is enabled, whereas the row-selection pseudo-class is applied for row-level selection. The constrained-resize pseudo-class is applied when the column resize policy is CONSTRAINED_RESIZE_POLICY.

Alternate rows in a TableView are highlighted by default. The following code removes the alternate row highlighting. It sets the white background color for all rows:
.table-row-cell {
    -fx-background-color: white;
}
.table-row-cell .table-cell {
        -fx-border-width: 0.25px;
        -fx-border-color: transparent gray gray transparent;
}
TableView shows empty rows to fill its available height. The following code removes the empty rows. In fact, it makes them appear as removed:
.table-row-cell:empty {
        -fx-background-color: transparent;
}
.table-row-cell:empty .table-cell {
        -fx-border-width: 0px;
}
TableView contains several substructures that can be styled separately:
  • column-resize-line

  • column-overlay

  • placeholder

  • column-header-background

The column-resize-line substructure is a Region and is shown when the user tries to resize a column. The column-overlay substructure is a Region and is shown as an overlay for the column being moved. The placeholder substructure is a StackPane and is shown when the TableView does not have columns or data, as in the following code:
/* Make the text in the placeholder red and bold */
.table-view .placeholder .label {
        -fx-text-fill: red;
        -fx-font-weight: bold;
}
The column-header-background substructure is a StackPane, and it is the area behind the column headers. It contains several substructures. Its filler substructure, which is a Region, is the area between the rightmost column and the right edge of the TableView in the header area. Its show-hide-columns-button substructure, which is a StackPane, is the area that shows the menu button to display the list of columns to show and hide. Please refer to the modena.css file and the JavaFX CSS Reference Guide for a complete list of properties of TableView that can be styled. The following code sets the filler background to white:
/* Set the filler background to white*/
.table-view .column-header-background .filler {
        -fx-background-color: white;
}

Summary

TableView is a control that is used to display and edit data in a tabular form. A TableView consists of rows and columns. The intersection of a row and a column is called a cell. Cells contain the data values. Columns have headers that describe the type of data they contain. Columns can be nested. Resizing and sorting of column data have built-in support. The following classes are used to work with a TableView control: TableView, TableColumn, TableRow, TableCell, TablePosition, TableView.TableViewFocusModel, and TableView.TableViewSelectionModel. The TableView class represents a TableView control. The TableColumn class represents a column in a TableView. Typically, a TableView contains multiple instances of TableColumn. A TableColumn consists of cells, which are instances of the TableCell class. A TableColumn is responsible for displaying and editing the data in its cells. A TableColumn has a header that can display header text, a graphic, or both. You can have a context menu for a TableColumn, which is displayed when the user right-clicks inside the column header. Use the contextMenu property to set a context menu.

The TableRow class inherits from the IndexedCell class. An instance of TableRow represents a row in a TableView. You almost never use this class in your application unless you want to provide a customized implementation for rows. Typically, you customize cells, not rows.

An instance of the TableCell class represents a cell in a TableView. Cells are highly customizable. They display data from the underlying data model for the TableView. They are capable of displaying data as well as graphics. Cells in a row of a TableView contain data related to an item such as a person, a book, and so forth. Data for some cells in a row may come directly from the attributes of the item, or they may be computed.

TableView has an items property of the ObservableList<S> type. The generic type S is the same as the generic type of the TableView. It is the data model for the TableView. Each element in the items list represents a row in the TableView. Adding a new item to the items list adds a new row to the TableView. Deleting an item from the items list deletes the corresponding row from the TableView.

The TableColumn, TableRow, and TableCell classes contain a tableView property that holds the reference of the TableView that contains them. The tableView property contains null when the TableColumn does not belong to a TableView.

A TablePosition represents the position of a cell. Its getRow() and getColumn() methods return the indexes of rows and columns, respectively, to which the cell belongs.

The TableViewFocusModel class is an inner static class of the TableView class. It represents the focus model for the TableView to manage focus for rows and cells.

The TableViewSelectionModel class is an inner static class of the TableView class. It represents the selection model for the TableView to manage selection for rows and cells.

By default, all columns in a TableView are visible. The TableColumn class has a visible property to set the visibility of a column. If you turn off the visibility of a parent column, a column with nested columns, all of its nested columns will be invisible.

You can rearrange columns in a TableView in two ways: by dragging and dropping columns to a different position or by changing their positions in the observable list of returned by the getColumns() method of the TableView class. The first option is available by default.

TableView has built-in support for sorting data in columns. By default, it allows users to sort data by clicking column headers. It also supports sorting data programmatically. You can also disable sorting for a column or all columns in a TableView.

TableView supports customization at several levels. It lets you customize the rendering of columns, for example, you can display data in a column using a check box, a combo box, or a TextField. You can also style a TableView using CSS.

The next chapter will discuss 2D shapes and how they can be added to a scene.

Tip

The TreeView and TreeTableView chapters from the previous edition were omitted in this book. The handling of those controls is very similar to table views, and the chapters were quite large, so in order to keep this edition at a reasonable extent there is just a concise introduction to tree controls in the appendix.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.16.137.108