I have a JavaFx TableView who are populate with data from a FilteredList and a search field for performing searching in list. I want to highlight dynamically the characters in table based on data from search field. How can i do that?
-
1This may be a good place to start. https://stackoverflow.com/questions/9128535/highlighting-strings-in-javafx-textarea. Given that it's a TableView, things will be more complicated. – SedJ601 Nov 15 '19 at 14:42
-
1Thank you @Sedrick – Nicolescu Ionut Lucian Nov 15 '19 at 14:44
2 Answers
This can be done, by providing Text objects in the cell factory. It makes things more complicated, as your table column values need to provide more information than just a string or number.
I would do this by creating a class that represents a portion of each cell’s text, be it part of a match or not:
public class TextSegment {
private final boolean match;
private final String text;
public TextSegment(String text,
boolean isMatch) {
this.text = Objects.requireNonNull(text, "Text cannot be null.");
this.match = isMatch;
}
public String getText() {
return text;
}
public boolean isMatch() {
return match;
}
@Override
public String toString() {
return "Segment[match=" + match + " \"" + text + "\"]";
}
public static List<TextSegment> search(String text,
String searchText,
boolean caseSensitive) {
if (text == null || text.isBlank() ||
searchText == null || searchText.isBlank()) {
return Collections.singletonList(new TextSegment(text, false));
}
List<TextSegment> segments = new ArrayList<>();
Pattern pattern = Pattern.compile(Pattern.quote(searchText),
caseSensitive ? 0 : Pattern.CASE_INSENSITIVE);
int lastEnd = 0;
Matcher matcher = pattern.matcher(text);
while (matcher.find()) {
int start = matcher.start();
if (start > lastEnd) {
segments.add(new TextSegment(
text.substring(lastEnd, start), false));
}
segments.add(new TextSegment(matcher.group(), true));
lastEnd = matcher.end();
}
String tail = text.substring(lastEnd);
if (!tail.isEmpty()) {
segments.add(new TextSegment(tail, false));
}
return segments;
}
}
The class which holds a row of data in the table would need an additional property, which holds data in the form of a list of instances of the above type:
private final StringProperty name;
private final ListProperty<TextSegment> nameMatches;
// ...
public StringProperty nameProperty() {
return name;
}
public ListProperty<TextSegment> nameMatchesProperty() {
return nameMatches;
}
Then, in the constructor, link them together:
this.name.addListener(
(o, old, newName) -> nameMatches.setAll(TextSegment.search(newName,
getSearchText(), isCaseSensitive())));
The TableColumn would be defined as:
TableColumn<RowItem, ObservableList<TextSegment>> nameColumn =
new TableColumn<>("Name");
nameColumn.setCellValueFactory(
f -> f.getValue().nameMatchesProperty());
And finally, the display of a sequence of TextSegments can be done as an adjacent list of Text
nodes:
nameColumn.setCellFactory(c -> new SearchableCell<>());
Where SearchableCell is a class that displays matched text by underlining it:
public class SearchableCell<S, T extends Iterable<TextSegment>>
extends TableCell<S, T> {
@Override
protected void updateItem(T item,
boolean empty) {
super.updateItem(item, empty);
setText(null);
if (empty || item == null) {
setGraphic(null);
} else {
HBox row = new HBox();
row.setAlignment(Pos.BASELINE_LEFT);
for (TextSegment segment : item) {
Text text = new Text(segment.getText());
text.setUnderline(segment.isMatch());
row.getChildren().add(text);
}
setGraphic(row);
}
}
}
Here is an example that puts it all together:
import java.text.NumberFormat;
import java.util.Objects;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.regex.Pattern;
import java.util.regex.Matcher;
import javafx.application.Application;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.StringProperty;
import javafx.beans.property.ListProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.SimpleListProperty;
import javafx.collections.ObservableList;
import javafx.collections.FXCollections;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.stage.Stage;
import javafx.scene.Scene;
import javafx.scene.control.CheckBox;
import javafx.scene.control.Label;
import javafx.scene.control.TableCell;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.control.TextField;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.text.Text;
public class TableHighlight
extends Application {
static class TextSegment {
private final boolean match;
private final String text;
public TextSegment(String text,
boolean isMatch) {
this.text = Objects.requireNonNull(text, "Text cannot be null.");
this.match = isMatch;
}
public String getText() {
return text;
}
public boolean isMatch() {
return match;
}
@Override
public String toString() {
return "Segment[match=" + match + " \"" + text + "\"]";
}
public static List<TextSegment> search(String text,
String searchText,
boolean caseSensitive) {
if (text == null || text.isBlank() ||
searchText == null || searchText.isBlank()) {
return Collections.singletonList(new TextSegment(text, false));
}
List<TextSegment> segments = new ArrayList<>();
Pattern pattern = Pattern.compile(Pattern.quote(searchText),
caseSensitive ? 0 : Pattern.CASE_INSENSITIVE);
int lastEnd = 0;
Matcher matcher = pattern.matcher(text);
while (matcher.find()) {
int start = matcher.start();
if (start > lastEnd) {
segments.add(new TextSegment(
text.substring(lastEnd, start), false));
}
segments.add(new TextSegment(matcher.group(), true));
lastEnd = matcher.end();
}
String tail = text.substring(lastEnd);
if (!tail.isEmpty()) {
segments.add(new TextSegment(tail, false));
}
return segments;
}
}
static class StateInfo {
private final StringProperty name;
private final StringProperty abbreviation;
private final StringProperty capital;
private final IntegerProperty population;
private final ListProperty<TextSegment> nameMatches;
private final ListProperty<TextSegment> abbreviationMatches;
private final ListProperty<TextSegment> capitalMatches;
private final StringProperty searchText;
private final BooleanProperty caseSensitive;
public StateInfo() {
this(null, null, null, 0);
}
public StateInfo(String name,
String abbreviation,
String capital,
int population) {
this.name = new SimpleStringProperty(this, "name",
name);
this.abbreviation = new SimpleStringProperty(this, "abbreviation",
abbreviation);
this.capital = new SimpleStringProperty(this, "capital",
capital);
this.population = new SimpleIntegerProperty(this, "population",
population);
searchText = new SimpleStringProperty(this, "searchText");
caseSensitive = new SimpleBooleanProperty(this, "caseSensitive");
nameMatches = new SimpleListProperty<>(this,
"nameMatches", FXCollections.observableArrayList());
abbreviationMatches = new SimpleListProperty<>(this,
"abbreviationMatches", FXCollections.observableArrayList());
capitalMatches = new SimpleListProperty<>(this,
"capitalMatches", FXCollections.observableArrayList());
this.name.addListener(
(o, old, newName) -> update(nameMatches, newName));
this.abbreviation.addListener(
(o, old, newAbbr) -> update(abbreviationMatches, newAbbr));
this.capital.addListener(
(o, old, newCapital) -> update(capitalMatches, newCapital));
searchText.addListener(o -> updateAllMatches());
caseSensitive.addListener(o -> updateAllMatches());
updateAllMatches();
}
private void update(ObservableList<TextSegment> segments,
String text) {
segments.setAll(TextSegment.search(text,
getSearchText(), isCaseSensitive()));
}
private void updateAllMatches() {
update(nameMatches, getName());
update(abbreviationMatches, getAbbreviation());
update(capitalMatches, getCapital());
}
public StringProperty nameProperty() { return name; }
public String getName() { return name.get(); }
public void setName(String name) { this.name.set(name); }
public StringProperty abbreviationProperty() { return abbreviation; }
public String getAbbreviation() { return abbreviation.get(); }
public void setAbbreviation(String abbr) { this.abbreviation.set(abbr); }
public StringProperty capitalProperty() { return capital; }
public String getCapital() { return capital.get(); }
public void setCapital(String capital) { this.capital.set(capital); }
public IntegerProperty populationProperty() { return population; }
public int getPopulation() { return population.get(); }
public void setPopulation(int population) { this.population.set(population); }
public StringProperty searchTextProperty() { return searchText; }
public String getSearchText() { return searchText.get(); }
public void setSearchText(String searchText) { this.searchText.set(searchText); }
public BooleanProperty caseSensitiveProperty() { return caseSensitive; }
public boolean isCaseSensitive() { return caseSensitive.get(); }
public void setCaseSensitive(boolean c) { caseSensitive.set(c); }
public ListProperty<TextSegment> nameMatchesProperty() {
return nameMatches;
}
public ObservableList<TextSegment> getNameMatches() {
return nameMatches.get();
}
public void setNameMatches(ObservableList<TextSegment> list) {
nameMatches.set(list);
}
public ListProperty<TextSegment> abbreviationMatchesProperty() {
return abbreviationMatches;
}
public ObservableList<TextSegment> getAbbreviationMatches() {
return abbreviationMatches.get();
}
public void setAbbreviationMatches(ObservableList<TextSegment> list) {
abbreviationMatches.set(list);
}
public ListProperty<TextSegment> capitalMatchesProperty() {
return capitalMatches;
}
public ObservableList<TextSegment> getCapitalMatches() {
return capitalMatches.get();
}
public void setCapitalMatches(ObservableList<TextSegment> list) {
capitalMatches.set(list);
}
@Override
public String toString() {
return String.format("%s[name=%s abbr=%s capital=%s pop=%,d]",
getName(), getAbbreviation(), getCapital(), getPopulation());
}
}
static class SearchableCell<S, T extends Iterable<TextSegment>>
extends TableCell<S, T> {
@Override
protected void updateItem(T item,
boolean empty) {
super.updateItem(item, empty);
setText(null);
if (empty || item == null) {
setGraphic(null);
} else {
HBox row = new HBox();
row.setAlignment(Pos.BASELINE_LEFT);
for (TextSegment segment : item) {
Text text = new Text(segment.getText());
text.setUnderline(segment.isMatch());
row.getChildren().add(text);
}
setGraphic(row);
}
}
}
static class NumberCell<S, T extends Number>
extends TableCell<S, T> {
private final NumberFormat format = NumberFormat.getInstance();
@Override
protected void updateItem(T item, boolean empty) {
super.updateItem(item, empty);
setGraphic(null);
if (empty || item == null) {
setText(null);
} else {
setText(format.format(item));
}
}
}
@Override
public void start(Stage stage) {
TableView<StateInfo> table = new TableView<>();
TableColumn<StateInfo, ObservableList<TextSegment>> nameColumn =
new TableColumn<>("Name");
TableColumn<StateInfo, ObservableList<TextSegment>> abbreviationColumn =
new TableColumn<>("Abbreviation");
TableColumn<StateInfo, ObservableList<TextSegment>> capitalColumn =
new TableColumn<>("Capital");
TableColumn<StateInfo, Number> populationColumn =
new TableColumn<>("Population");
table.getColumns().add(nameColumn);
table.getColumns().add(abbreviationColumn);
table.getColumns().add(capitalColumn);
table.getColumns().add(populationColumn);
nameColumn.setCellValueFactory(
f -> f.getValue().nameMatchesProperty());
abbreviationColumn.setCellValueFactory(
f -> f.getValue().abbreviationMatchesProperty());
capitalColumn.setCellValueFactory(
f -> f.getValue().capitalMatchesProperty());
populationColumn.setCellValueFactory(
f -> f.getValue().populationProperty());
nameColumn.setCellFactory(c -> new SearchableCell<>());
abbreviationColumn.setCellFactory(c -> new SearchableCell<>());
capitalColumn.setCellFactory(c -> new SearchableCell<>());
populationColumn.setCellFactory(c -> new NumberCell<>());
table.getItems().setAll(createData());
TextField searchField = new TextField();
searchField.textProperty().addListener((o, old, text) ->
table.getItems().forEach(s -> s.setSearchText(text)));
CheckBox caseSensitiveButton = new CheckBox("Case sensitive");
caseSensitiveButton.selectedProperty().addListener((o, old, selected) ->
table.getItems().forEach(s -> s.setCaseSensitive(selected)));
Label searchLabel = new Label("Search for: ");
searchLabel.setLabelFor(searchField);
HBox searchFields = new HBox(
searchLabel, searchField, caseSensitiveButton);
searchFields.setAlignment(Pos.BASELINE_LEFT);
HBox.setMargin(caseSensitiveButton, new Insets(0, 0, 0, 24));
searchFields.setPadding(new Insets(6, 6, 12, 6));
Scene scene = new Scene(
new BorderPane(table, searchFields, null, null, null));
stage.setScene(scene);
stage.setTitle("Table Search");
stage.show();
}
private static Collection<StateInfo> createData() {
return Arrays.asList(
new StateInfo("Alabama", "AL", "Montgomery", 543097),
new StateInfo("Alaska", "AK", "Juneau", 245813),
new StateInfo("Arizona", "AZ", "Phoenix", 651968),
new StateInfo("Arkansas", "AR", "Little Rock", 502304),
new StateInfo("California", "CA", "Sacramento", 719219),
new StateInfo("Colorado", "CO", "Denver", 632840),
new StateInfo("Connecticut", "CT", "Hartford", 510381),
new StateInfo("Delaware", "DE", "Dover", 322390),
new StateInfo("Florida", "FL", "Tallahassee", 734459),
new StateInfo("Georgia", "GA", "Atlanta", 657467),
new StateInfo("Hawaii", "HI", "Honolulu", 355123),
new StateInfo("Idaho", "ID", "Boise", 438552),
new StateInfo("Illinois", "IL", "Springfield", 637054),
new StateInfo("Indiana", "IN", "Indianapolis", 608353),
new StateInfo("Iowa", "IA", "Des Moines", 526024),
new StateInfo("Kansas", "KS", "Topeka", 485251),
new StateInfo("Kentucky", "KY", "Frankfort", 558550),
new StateInfo("Louisiana", "LA", "Baton Rouge", 582497),
new StateInfo("Maine", "ME", "Augusta", 334601),
new StateInfo("Maryland", "MD", "Annapolis", 604272),
new StateInfo("Massachusetts", "MA", "Boston", 627468),
new StateInfo("Michigan", "MI", "Lansing", 624735),
new StateInfo("Minnesota", "MN", "Saint Paul", 561118),
new StateInfo("Mississippi", "MS", "Jackson", 497755),
new StateInfo("Missouri", "MO", "Jefferson City", 612645),
new StateInfo("Montana", "MT", "Helena", 354102),
new StateInfo("Nebraska", "NE", "Lincoln", 385854),
new StateInfo("Nevada", "NV", "Carson City", 505732),
new StateInfo("New Hampshire", "NH", "Concord", 339115),
new StateInfo("New Jersey", "NJ", "Trenton", 636323),
new StateInfo("New Mexico", "NM", "Santa Fe", 419086),
new StateInfo("New York", "NY", "Albany", 673869),
new StateInfo("North Carolina", "NC", "Raleigh", 692241),
new StateInfo("North Dakota", "ND", "Bismarck", 253359),
new StateInfo("Ohio", "OH", "Columbus", 649413),
new StateInfo("Oklahoma", "OK", "Oklahoma City", 563297),
new StateInfo("Oregon", "OR", "Salem", 598673),
new StateInfo("Pennsylvania", "PA", "Harrisburg", 640353),
new StateInfo("Rhode Island", "RI", "Providence", 264329),
new StateInfo("South Carolina", "SC", "Columbia", 564903),
new StateInfo("South Dakota", "SD", "Pierre", 294078),
new StateInfo("Tennessee", "TN", "Nashville", 615455),
new StateInfo("Texas", "TX", "Austin", 755312),
new StateInfo("Utah", "UT", "Salt Lake City", 526851),
new StateInfo("Vermont", "VT", "Montpelier", 208766),
new StateInfo("Virginia", "VA", "Richmond", 655207),
new StateInfo("Washington", "WA", "Olympia", 627966),
new StateInfo("West Virginia", "WV", "Charleston", 361166),
new StateInfo("Wisconsin", "WI", "Madison", 581357),
new StateInfo("Wyoming", "WY", "Cheyenne", 192579)
);
}
public static class Main {
public static void main(String[] args) {
Application.launch(TableHighlight.class, args);
}
}
}

- 40,506
- 4
- 48
- 63
Highlighting Strings in JavaFX TextArea As in the comment with that you can hightlight text. What u also need to do is add a custom cell factory. https://docs.oracle.com/javafx/2/api/javafx/scene/control/TableColumn.html#setCellFactory(javafx.util.Callback) With that you can load your own custom ui in the tabel view cells that highlights texts/ Lables.

- 521
- 7
- 17