JavaFX Controls, Layouts & FXML

Lists, Tables & ComboBoxes

18 min Lesson 2 of 12

Lists, Tables & ComboBoxes

Three controls dominate data-presentation in almost every real JavaFX application: ListView, TableView, and ComboBox. Each follows the same architectural philosophy — a Model-View separation where the control renders whatever data you put into an ObservableList, and updates itself automatically when that list changes. Understanding this pattern once means you understand all three controls.

The ObservableList Foundation

Before touching any control, internalize this: JavaFX data controls do not hold copies of your data. They hold a reference to an ObservableList<T> and watch it for changes. Add an item to the list, the control repaints. Remove one, the row disappears. You never call a "refresh" method.

import javafx.collections.FXCollections; import javafx.collections.ObservableList; ObservableList<String> names = FXCollections.observableArrayList( "Alice", "Bob", "Carol" ); names.add("Dave"); // the control updates itself — no extra code needed

ListView

ListView<T> renders a scrollable list of items, one per row. The generic type can be anything — String, a model class, an enum.

import javafx.scene.control.ListView; import javafx.collections.FXCollections; ListView<String> listView = new ListView<>( FXCollections.observableArrayList("Alpha", "Beta", "Gamma", "Delta") ); listView.setPrefHeight(200); // Read the selected item listView.getSelectionModel().selectedItemProperty().addListener( (obs, oldVal, newVal) -> System.out.println("Selected: " + newVal) ); // Multi-select mode listView.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE); ObservableList<String> selected = listView.getSelectionModel().getSelectedItems();
SelectionModel is the bridge to the user's choice. getSelectionModel() returns a MultipleSelectionModel that exposes both selectedItemProperty() (single binding) and getSelectedItems() (observable collection for multi-select). Always read selection through this API — never traverse the cell tree yourself.

Custom Cell Rendering with Cell Factories

By default, ListView calls toString() on each item. For richer rows — icons, buttons, formatted text — supply a cell factory: a Callback that the list calls once per visible cell to create a ListCell<T>. JavaFX then recycles those cells as the user scrolls (the same principle as RecyclerView on Android).

import javafx.scene.control.ListCell; listView.setCellFactory(lv -> new ListCell<String>() { @Override protected void updateItem(String item, boolean empty) { super.updateItem(item, empty); // ALWAYS call super first if (empty || item == null) { setText(null); setGraphic(null); } else { setText(item.toUpperCase()); } } });
Always call super.updateItem() first and always handle the empty case. Skipping either step causes phantom content to appear in blank rows — a very common JavaFX beginner bug. Cells are reused; if you do not clear them when empty == true, the old data bleeds into empty slots.

TableView

TableView<T> presents a collection of objects in a grid of columns. Each TableColumn<T, CellValue> knows how to extract one field from an object of type T.

import javafx.beans.property.SimpleStringProperty; import javafx.beans.property.SimpleIntegerProperty; import javafx.scene.control.TableColumn; import javafx.scene.control.TableView; import javafx.scene.control.cell.PropertyValueFactory; // --- Model class with JavaFX properties --- public class Product { private final SimpleStringProperty name; private final SimpleIntegerProperty price; public Product(String name, int price) { this.name = new SimpleStringProperty(name); this.price = new SimpleIntegerProperty(price); } public String getName() { return name.get(); } public int getPrice() { return price.get(); } public SimpleStringProperty nameProperty() { return name; } public SimpleIntegerProperty priceProperty() { return price; } } // --- Build the table --- TableView<Product> table = new TableView<>(); TableColumn<Product, String> nameCol = new TableColumn<>("Name"); nameCol.setCellValueFactory(new PropertyValueFactory<>("name")); TableColumn<Product, Integer> priceCol = new TableColumn<>("Price ($)"); priceCol.setCellValueFactory(new PropertyValueFactory<>("price")); table.getColumns().addAll(nameCol, priceCol); ObservableList<Product> products = FXCollections.observableArrayList( new Product("Keyboard", 49), new Product("Mouse", 29), new Product("Monitor", 299) ); table.setItems(products);

PropertyValueFactory uses reflection to call the getter whose name matches the string you pass. The cleaner, refactoring-safe alternative is a lambda cell-value factory that reads the JavaFX property directly:

nameCol.setCellValueFactory(cellData -> cellData.getValue().nameProperty());

Editable Cells

Making a table cell editable requires two steps: tell the table it is editable, then give the column an editable cell factory and a setOnEditCommit handler that writes the new value back to the model.

import javafx.scene.control.cell.TextFieldTableCell; table.setEditable(true); nameCol.setEditable(true); nameCol.setCellFactory(TextFieldTableCell.forTableColumn()); nameCol.setOnEditCommit(event -> { event.getRowValue().nameProperty().set(event.getNewValue()); });
Expose JavaFX Property objects from your model. Controls like TableView and ListView bind most efficiently to Property objects (SimpleStringProperty, SimpleIntegerProperty, etc.). If a property changes programmatically, the table cell updates automatically — no manual refresh calls needed.

ComboBox

ComboBox<T> combines a button that shows the current selection with a drop-down list. It also supports an optional editable text field when setEditable(true) is called.

import javafx.scene.control.ComboBox; ObservableList<String> options = FXCollections.observableArrayList( "Small", "Medium", "Large", "X-Large" ); ComboBox<String> sizeBox = new ComboBox<>(options); sizeBox.setValue("Medium"); // set a default selection // React to selection changes sizeBox.valueProperty().addListener( (obs, oldVal, newVal) -> System.out.println("Size: " + newVal) ); // Or read on demand String chosen = sizeBox.getValue();

ComboBox with Custom Objects

When the generic type is not a String, supply a StringConverter so the control knows how to display items in both the button face and the drop-down list.

import javafx.util.StringConverter; ComboBox<Product> productBox = new ComboBox<>(products); productBox.setConverter(new StringConverter<Product>() { @Override public String toString(Product p) { return p == null ? "" : p.getName() + " ($" + p.getPrice() + ")"; } @Override public Product fromString(String s) { return null; } // not editable });

Comparing the Three Controls

  • ListView: Single-column list of items. Use for navigation panels, option lists, or any scrollable collection where one piece of data per row is enough.
  • TableView: Multi-column grid. Use when each item has several fields you need to show side-by-side, with optional sorting and editing.
  • ComboBox: Compact selection control. Use inside forms where space is limited and the user picks one value from a bounded set.

Summary

All three controls share the ObservableList data model — change the list, the UI updates automatically. ListView and TableView use cell factories for custom rendering and follow a cell-recycling scheme that handles thousands of rows efficiently. TableView ties columns to model properties either through PropertyValueFactory or lambda factories, and supports in-place editing via setOnEditCommit. ComboBox integrates cleanly into forms and gains display customization through a StringConverter. In the next lesson you will arrange these controls on screen using HBox and VBox layout panes.

ES
Edrees Salih
1 hour ago

We are still cooking the magic in the way!