JAVAFX - The hidden column's mysterious solution - javafx

I have a TableView which shows all the data from a Derby table, except the "ID" of the items (I don't want the user to see it.)
The method is simple, I made an SQL statement to select all the attributes but I did not ask for the ID column. I got back a TableData and this variable is shown in the TableView with all of the records (It is a simple namelist).
I want to allow the user to delete from the table with a "delete button".
So first, there should be an "OnAction" method, (when the user clicks the delete button) when we gather the ACTUAL selected row's (if not null) ID, and we send a statement to the database to delete it, where the (HIDDEN) ID of the selected item can be found at the derby table.
Since it is a Namelist, and the user is allowed to make another record with the exactly same data, in the tableview the records can be clones, with no differences. (only the ID is uniqe, but the tableview does not contain the id-s, that makes it so hard).
So how could we delete the selected line without knowing it's ID? Or how can we know the ID when it is not shown in the table? (searching on the name is not working, since there can be several records with the same name)
What is the "hidden column's mysterious solution"? :)

Your question has a simple solution. If you don't want to show the ID on the tableview, you don't have to. TableView binds with your class, in your case user, and not with its fields. When you add the values to the tableview, you can control which fields you want to show on it. Whether you show it on the tableview or not, you still have your complete user object with you (which still has the ID).
Now, there are multiple ways to do this. One of the way is to bind the id with the delete button that you want to show in each row. Whenever the delete button is pressed, delete the item from the DB and delete it from the tableview.
I have create an working example which shows how this work. I have hardcoded my values and instead of deleting it from the database, I am printing the ID value on the console.
In this example, the Person class can be considered equivalent to your user, each of which has an id and other attributes.
import javafx.application.Application;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.geometry.Insets;
import javafx.scene.Group;
import javafx.scene.Scene;
import javafx.scene.control.*;
import javafx.scene.control.cell.PropertyValueFactory;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.layout.VBox;
import javafx.scene.text.Font;
import javafx.stage.Stage;
public class TableViewSample extends Application {
private TableView<Person> table = new TableView<Person>();
private final ObservableList<Person> data =
FXCollections.observableArrayList(
new Person(10, "Jacob", "Smith", "jacob.smith#example.com"),
new Person(20, "Isabella", "Johnson", "isabella.johnson#example.com"),
new Person(30, "Ethan", "Williams", "ethan.williams#example.com"),
new Person(40, "Emma", "Jones", "emma.jones#example.com"),
new Person(50, "Michael", "Brown", "michael.brown#example.com")
);
public static void main(String[] args) {
launch(args);
}
#Override
public void start(Stage stage) {
Scene scene = new Scene(new Group());
stage.setTitle("Table View Sample");
stage.setWidth(600);
stage.setHeight(500);
final Label label = new Label("Address Book");
label.setFont(new Font("Arial", 20));
table.setEditable(true);
TableColumn firstNameCol = new TableColumn("First Name");
firstNameCol.setMinWidth(100);
firstNameCol.setCellValueFactory(
new PropertyValueFactory<Person, String>("firstName"));
TableColumn lastNameCol = new TableColumn("Last Name");
lastNameCol.setMinWidth(100);
lastNameCol.setCellValueFactory(
new PropertyValueFactory<Person, String>("lastName"));
TableColumn emailCol = new TableColumn("Email");
emailCol.setMinWidth(200);
emailCol.setCellValueFactory(
new PropertyValueFactory<Person, String>("email"));
TableColumn deleteCol = new TableColumn("Delete");
deleteCol.setMinWidth(100);
deleteCol.setCellFactory(param -> new ButtonCell());
deleteCol.setCellValueFactory(
new PropertyValueFactory<Person, String>("id"));
table.setItems(data);
table.getColumns().addAll(firstNameCol, lastNameCol, emailCol, deleteCol);
final VBox vbox = new VBox();
vbox.setSpacing(5);
vbox.setPadding(new Insets(10, 0, 0, 10));
vbox.getChildren().addAll(label, table);
((Group) scene.getRoot()).getChildren().addAll(vbox);
stage.setScene(scene);
stage.show();
}
public static class Person {
private final SimpleIntegerProperty id;
private final SimpleStringProperty firstName;
private final SimpleStringProperty lastName;
private final SimpleStringProperty email;
private Person(Integer id, String fName, String lName, String email) {
this.id = new SimpleIntegerProperty(id);
this.firstName = new SimpleStringProperty(fName);
this.lastName = new SimpleStringProperty(lName);
this.email = new SimpleStringProperty(email);
}
public int getId() {
return id.get();
}
public void setId(int id) {
this.id.set(id);
}
public String getFirstName() {
return firstName.get();
}
public void setFirstName(String fName) {
firstName.set(fName);
}
public String getLastName() {
return lastName.get();
}
public void setLastName(String fName) {
lastName.set(fName);
}
public String getEmail() {
return email.get();
}
public void setEmail(String fName) {
email.set(fName);
}
}
private class ButtonCell extends TableCell<Person, Integer> {
Image buttonDeleteImage = new Image("https://cdn1.iconfinder.com/data/icons/nuove/22x22/actions/fileclose.png");
final Button cellDeleteButton = new Button("", new ImageView(buttonDeleteImage));
ButtonCell() {
cellDeleteButton.setOnAction(actionEvent -> {
System.out.println("Deleted Id : " + getItem());// Make a DB call and delete the person with ID
getTableView().getItems().remove(getIndex());
});
}
#Override
protected void updateItem(Integer t, boolean empty) {
super.updateItem(t, empty);
if (!empty) {
setGraphic(cellDeleteButton);
} else {
setGraphic(null);
}
}
}
}
To do it without binding ID
You need :
deleteCol.setCellValueFactory(p -> {
return new ReadOnlyObjectWrapper<Person>((Person)p.getValue());
});
The custom ButtonCell should extend TableCell<Person, Person>
The logic to delete item becomes :
System.out.println("Deleted ID : " +
getItem().getId());// Make a DB call and delete the person with ID
getTableView().getItems().remove(getIndex());
Complete Example:
public class TableViewSample extends Application {
private TableView<Person> table = new TableView<Person>();
private final ObservableList<Person> data =
FXCollections.observableArrayList(
new Person(10, "Jacob", "Smith", "jacob.smith#example.com"),
new Person(20, "Isabella", "Johnson", "isabella.johnson#example.com"),
new Person(30, "Ethan", "Williams", "ethan.williams#example.com"),
new Person(40, "Emma", "Jones", "emma.jones#example.com"),
new Person(50, "Michael", "Brown", "michael.brown#example.com")
);
public static void main(String[] args) {
launch(args);
}
#Override
public void start(Stage stage) {
Scene scene = new Scene(new Group());
stage.setTitle("Table View Sample");
stage.setWidth(600);
stage.setHeight(500);
final Label label = new Label("Address Book");
label.setFont(new Font("Arial", 20));
table.setEditable(true);
TableColumn firstNameCol = new TableColumn("First Name");
firstNameCol.setMinWidth(100);
firstNameCol.setCellValueFactory(
new PropertyValueFactory<Person, String>("firstName"));
TableColumn lastNameCol = new TableColumn("Last Name");
lastNameCol.setMinWidth(100);
lastNameCol.setCellValueFactory(
new PropertyValueFactory<Person, String>("lastName"));
TableColumn emailCol = new TableColumn("Email");
emailCol.setMinWidth(200);
emailCol.setCellValueFactory(
new PropertyValueFactory<Person, String>("email"));
TableColumn deleteCol = new TableColumn("Delete");
deleteCol.setMinWidth(100);
deleteCol.setCellFactory(param -> new ButtonCell());
deleteCol.setCellValueFactory(p -> {
return new ReadOnlyObjectWrapper<Person>((Person)p.getValue());
});
table.setItems(data);
table.getColumns().addAll(firstNameCol, lastNameCol, emailCol, deleteCol);
final VBox vbox = new VBox();
vbox.setSpacing(5);
vbox.setPadding(new Insets(10, 0, 0, 10));
vbox.getChildren().addAll(label, table);
((Group) scene.getRoot()).getChildren().addAll(vbox);
stage.setScene(scene);
stage.show();
}
public static class Person {
private final SimpleIntegerProperty id;
private final SimpleStringProperty firstName;
private final SimpleStringProperty lastName;
private final SimpleStringProperty email;
private Person(Integer id, String fName, String lName, String email) {
this.id = new SimpleIntegerProperty(id);
this.firstName = new SimpleStringProperty(fName);
this.lastName = new SimpleStringProperty(lName);
this.email = new SimpleStringProperty(email);
}
public int getId() {
return id.get();
}
public void setId(int id) {
this.id.set(id);
}
public String getFirstName() {
return firstName.get();
}
public void setFirstName(String fName) {
firstName.set(fName);
}
public String getLastName() {
return lastName.get();
}
public void setLastName(String fName) {
lastName.set(fName);
}
public String getEmail() {
return email.get();
}
public void setEmail(String fName) {
email.set(fName);
}
}
private class ButtonCell extends TableCell<Person, Person> {
Image buttonDeleteImage = new Image("https://cdn1.iconfinder.com/data/icons/nuove/22x22/actions/fileclose.png");
final Button cellDeleteButton = new Button("", new ImageView(buttonDeleteImage));
ButtonCell() {
cellDeleteButton.setOnAction(actionEvent -> {
System.out.println("Deleted ID : " +
getItem().getId());// Make a DB call and delete the person with ID
getTableView().getItems().remove(getIndex());
});
}
#Override
protected void updateItem(Person t, boolean empty) {
super.updateItem(t, empty);
if (!empty) {
setGraphic(cellDeleteButton);
} else {
setGraphic(null);
}
}
}
}

I have found an easier way, where the user can list anything, and he decides what to show and what to hide. This solution also works if we don't know what data will come to us, but We are sure that will be "ID" column we do not wan't to show in attention:
if (*ColumNameWeWantToHide*.equals(String.valueOf(columnName))) {
col.prefWidthProperty().bind(*TableViewName*.widthProperty().divide(100));
}
It makes us also able to work with ID numbers simply, but also we are abla to watch the ID if we want, but on default we do not see them. That is a good mixture of useability and easy coding.

Related

Javafx Tableview with InputStreamReader removes row color marker

i can't get any further. I have a standart Tableview and inserted the part from James_D which gives me a colored row and a different colored selected cell
see here.
This works realy pretty good. Now i need to read the data from an internet site.
After inserting the code to read the data with InputStreamReader the colored row is not displayed any more at all. No other error. The read date is ok.
About any help or alternatives I would be very glad.
Thanks.
deleted the first "multi file version" of this problem.
css file: selected-row-table.css
.table-row-cell:selected-row {
-fx-background-color: lightskyblue ;
}
New all in one Error example based on James_D Tableview without .fxml file. Needed css file (see above) with name "selected-row-table.css".
Same error after "new InputStreamreader...".
package com.example.james1;
import javafx.application.Application;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.BooleanBinding;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener.Change;
import javafx.collections.ObservableList;
import javafx.collections.ObservableSet;
import javafx.css.PseudoClass;
import javafx.geometry.Insets;
import javafx.scene.Group;
import javafx.scene.Scene;
import javafx.scene.control.*;
import javafx.scene.control.cell.PropertyValueFactory;
import javafx.scene.layout.VBox;
import javafx.scene.text.Font;
import javafx.stage.Stage;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.URL;
import java.net.URLConnection;
public class HelloApplication extends Application {
public static void main(String[] args) {
launch();
}
#Override
public void start(Stage stage) throws IOException {
Scene scene = new Scene(new Group());
scene.getStylesheets().add(getClass().getResource("selected-row-table.css").toExternalForm());
stage.setTitle("Table View Sample");
stage.setWidth(450);
stage.setHeight(600);
final Label label = new Label("Address Book");
label.setFont(new Font("Arial", 20));
final TableView<Person> table = new TableView<>();
final ObservableList<Person> data =
FXCollections.observableArrayList(
new Person("Jacob", "Smith", "jacob.smith#example.com"),
new Person("Isabella", "Johnson", "isabella.johnson#example.com"),
new Person("Ethan", "Williams", "ethan.williams#example.com"),
new Person("Emma", "Jones", "emma.jones#example.com"),
new Person("Michael", "Brown", "michael.brown#example.com")
);
table.getSelectionModel().setCellSelectionEnabled(true);
table.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE);
final PseudoClass selectedRowPseudoClass = PseudoClass.getPseudoClass("selected-row");
final ObservableSet<Integer> selectedRowIndexes = FXCollections.observableSet();
table.getSelectionModel().getSelectedCells().addListener((Change<? extends TablePosition> change) -> {
selectedRowIndexes.clear();
table.getSelectionModel().getSelectedCells().stream().map(TablePosition::getRow).forEach(row -> {
selectedRowIndexes.add(row);
});
});
table.setRowFactory(tableView -> {
final TableRow<Person> row = new TableRow<>();
BooleanBinding selectedRow = Bindings.createBooleanBinding(() ->
selectedRowIndexes.contains(new Integer(row.getIndex())), row.indexProperty(), selectedRowIndexes);
selectedRow.addListener((observable, oldValue, newValue) ->
row.pseudoClassStateChanged(selectedRowPseudoClass, newValue)
);
return row;
});
TableColumn<Person, String> firstNameCol = new TableColumn<>("First Name");
firstNameCol.setMinWidth(100);
firstNameCol.setCellValueFactory(new PropertyValueFactory<>("firstName"));
TableColumn<Person, String> lastNameCol = new TableColumn<>("Last Name");
lastNameCol.setMinWidth(100);
lastNameCol.setCellValueFactory(new PropertyValueFactory<>("lastName"));
TableColumn<Person, String> emailCol = new TableColumn<>("Email");
emailCol.setMinWidth(200);
emailCol.setCellValueFactory(new PropertyValueFactory<>("email"));
table.setItems(data);
table.getColumns().addAll(firstNameCol, lastNameCol, emailCol);
final VBox vbox = new VBox();
Button bt1 = new Button("add local");
Button bt2 = new Button("add internet");
vbox.setSpacing(5);
vbox.setPadding(new Insets(10, 0, 0, 10));
vbox.getChildren().addAll(label, table, bt1, bt2);
((Group) scene.getRoot()).getChildren().addAll(vbox);
stage.setScene(scene);
stage.show();
/*************************************************/
bt1.setOnAction(value -> {
data.add(new Person("xi.", "2023-01-01", "add Local "));
});
/*************************************************/
bt2.setOnAction(value -> {
BufferedReader br;
String strLine;
data.add(new Person("Do.", "start reading", "readHoli-1"));
try {
URL url = new URL("https://www.spiketime.de/feiertagapi/feiertage/csv/2023/2023");
URLConnection urlCon = url.openConnection(); //feastUrl.openConnection();
InputStreamReader isr = new InputStreamReader(urlCon.getInputStream()); // ERROR ---- DOES NOT WORK after this statement
data.add(new Person("Do.", "after creating streamreader", "TEST-2"));
br = new BufferedReader(isr);
while ((strLine = br.readLine()) != null) {
if (!strLine.contains("Bayern"))
continue;
System.out.println(strLine);
/* split string/check and add to holidays: here only print */
}
br.close();
isr.close();
} catch (Exception ex) {
ex.printStackTrace();
}
data.add(new Person("Do.", "end reading", "readHoli-2"));
});
}
public static class Person {
private final StringProperty firstName;
private final StringProperty lastName;
private final StringProperty email;
private Person(String fName, String lName, String email) {
this.firstName = new SimpleStringProperty(fName);
this.lastName = new SimpleStringProperty(lName);
this.email = new SimpleStringProperty(email);
}
public String getFirstName() {
return firstName.get();
}
public void setFirstName(String fName) {
firstName.set(fName);
}
public StringProperty firstNameProperty() {
return firstName;
}
public String getLastName() {
return lastName.get();
}
public void setLastName(String fName) {
lastName.set(fName);
}
public StringProperty lastNameProperty() {
return lastName;
}
public String getEmail() {
return email.get();
}
public void setEmail(String fName) {
email.set(fName);
}
public StringProperty emailProperty() {
return email;
}
}
}
All Bindings in javafx are implemented to do their best to not cause memory leaks. They do so (in the internal class BindingHelperObserver) by
keep a weak reference to the binding: this allows being claimed by garbage collection as soon as there is no strong reference path to the object
remove any listeners to their dependencies if the binding is gc'ed
The former is the reason for the problem described in the question because the cell factory creates the binding as a local reference which is collectable as soon as the method returns. Requesting an InputStream from a UrlConnection seems to trigger a garbage run, thus collecting the weakly referenced bindings of all rows.
To make them survive the life-time of its owner, the owner has to keep a strong reference to it, f.i. in the context of a cell factory:
subclass the row
let it have a field referencing the binding
install the binding/listener in its constructor
Example:
table.setRowFactory(tableView -> {
final TableRow<Person> row = new TableRow<>() {
private BooleanBinding selectedRow;
{
selectedRow = Bindings.createBooleanBinding(
() -> selectedRowIndexes.contains(getIndex()),
indexProperty(),
selectedRowIndexes);
selectedRow.addListener((observable, oldValue, newValue) -> {
pseudoClassStateChanged(selectedRowPseudoClass, newValue);
});
}
};
return row;
});

How to implement correct doubleclick-to-edit in TableView

Problem
The TableView seems to go into edit mode as soon as you click on an already selected cell. This is unwanted and inconsistent behavior, because the selection changes if you click outside of the current selection. It doesn't change when you click inside the current selection.
If anything, a real double click should be required to go into edit mode, at least when you work on a desktop.
Reproduction:
Create a TableView with this selection mode:
// cell selection mode instead of row selection
table.getSelectionModel().setCellSelectionEnabled(true);
// allow selection of multiple cells
table.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE);
Now select multiple cells (via shift keypress). Then click on a single cell of the currently selected cells.
The current behavior:
the table goes into edit mode
Question
The requested behavior:
only the selection should change, i. e. the single cell should be selected; edit mode should only happen on a double-click
How can you achieve this?
Code
Here's a full example. I took the code from the Oracle samples website, added the above lines and pre-selected all cells. When you click on a single cell after program start, the table goes directly into edit mode:
public class TableViewSample extends Application {
private TableView<Person> table = new TableView<Person>();
private final ObservableList<Person> data =
FXCollections.observableArrayList(
new Person("Jacob", "Smith", "jacob.smith#example.com"),
new Person("Isabella", "Johnson", "isabella.johnson#example.com"),
new Person("Ethan", "Williams", "ethan.williams#example.com"),
new Person("Emma", "Jones", "emma.jones#example.com"),
new Person("Michael", "Brown", "michael.brown#example.com"));
final HBox hb = new HBox();
public static void main(String[] args) {
launch(args);
}
#Override
public void start(Stage stage) {
Scene scene = new Scene(new Group());
stage.setTitle("Table View Sample");
stage.setWidth(450);
stage.setHeight(550);
final Label label = new Label("Address Book");
label.setFont(new Font("Arial", 20));
table.setEditable(true);
TableColumn firstNameCol = new TableColumn("First Name");
firstNameCol.setMinWidth(100);
firstNameCol.setCellValueFactory(
new PropertyValueFactory<Person, String>("firstName"));
firstNameCol.setCellFactory(TextFieldTableCell.forTableColumn());
firstNameCol.setOnEditCommit(
new EventHandler<CellEditEvent<Person, String>>() {
#Override
public void handle(CellEditEvent<Person, String> t) {
((Person) t.getTableView().getItems().get(
t.getTablePosition().getRow())
).setFirstName(t.getNewValue());
}
}
);
TableColumn lastNameCol = new TableColumn("Last Name");
lastNameCol.setMinWidth(100);
lastNameCol.setCellValueFactory(
new PropertyValueFactory<Person, String>("lastName"));
lastNameCol.setCellFactory(TextFieldTableCell.forTableColumn());
lastNameCol.setOnEditCommit(
new EventHandler<CellEditEvent<Person, String>>() {
#Override
public void handle(CellEditEvent<Person, String> t) {
((Person) t.getTableView().getItems().get(
t.getTablePosition().getRow())
).setLastName(t.getNewValue());
}
}
);
TableColumn emailCol = new TableColumn("Email");
emailCol.setMinWidth(200);
emailCol.setCellValueFactory(
new PropertyValueFactory<Person, String>("email"));
emailCol.setCellFactory(TextFieldTableCell.forTableColumn());
emailCol.setOnEditCommit(
new EventHandler<CellEditEvent<Person, String>>() {
#Override
public void handle(CellEditEvent<Person, String> t) {
((Person) t.getTableView().getItems().get(
t.getTablePosition().getRow())
).setEmail(t.getNewValue());
}
}
);
table.setItems(data);
table.getColumns().addAll(firstNameCol, lastNameCol, emailCol);
final TextField addFirstName = new TextField();
addFirstName.setPromptText("First Name");
addFirstName.setMaxWidth(firstNameCol.getPrefWidth());
final TextField addLastName = new TextField();
addLastName.setMaxWidth(lastNameCol.getPrefWidth());
addLastName.setPromptText("Last Name");
final TextField addEmail = new TextField();
addEmail.setMaxWidth(emailCol.getPrefWidth());
addEmail.setPromptText("Email");
final Button addButton = new Button("Add");
addButton.setOnAction(new EventHandler<ActionEvent>() {
#Override
public void handle(ActionEvent e) {
data.add(new Person(
addFirstName.getText(),
addLastName.getText(),
addEmail.getText()));
addFirstName.clear();
addLastName.clear();
addEmail.clear();
}
});
hb.getChildren().addAll(addFirstName, addLastName, addEmail, addButton);
hb.setSpacing(3);
final VBox vbox = new VBox();
vbox.setSpacing(5);
vbox.setPadding(new Insets(10, 0, 0, 10));
vbox.getChildren().addAll(label, table, hb);
((Group) scene.getRoot()).getChildren().addAll(vbox);
stage.setScene(scene);
stage.show();
// cell selection mode instead of row selection
table.getSelectionModel().setCellSelectionEnabled(true);
// allow selection of multiple cells
table.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE);
// select all for demo purposes
table.getSelectionModel().selectAll();
}
public static class Person {
private final SimpleStringProperty firstName;
private final SimpleStringProperty lastName;
private final SimpleStringProperty email;
private Person(String fName, String lName, String email) {
this.firstName = new SimpleStringProperty(fName);
this.lastName = new SimpleStringProperty(lName);
this.email = new SimpleStringProperty(email);
}
public String getFirstName() {
return firstName.get();
}
public void setFirstName(String fName) {
firstName.set(fName);
}
public String getLastName() {
return lastName.get();
}
public void setLastName(String fName) {
lastName.set(fName);
}
public String getEmail() {
return email.get();
}
public void setEmail(String fName) {
email.set(fName);
}
}
}
You can try to clear selection on mouse pressed event:
table.addEventFilter( MouseEvent.MOUSE_PRESSED, new EventHandler<MouseEvent>()
{
#Override
public void handle( MouseEvent event )
{
if( event.isControlDown()) {
return;
}
if ( table.getEditingCell() == null) {
table.getSelectionModel().clearSelection();
}
}
});

Tableview Integer sorting bugged?

I have a tableview and one of my columns consist of Integer values:
1
444
-9
If I sort on this column, I would expect 444 to be the "highest" number and therefor be the first row, but JavaFX believes 9 is higher?
Any tips?
MVCE:
public class TableViewTEST extends Application {
private TableView table = new TableView();
final ObservableList<Person> data = FXCollections.observableArrayList(
new Person("1", "Smith", "jacob.smith#example.com"),
new Person("9", "Johnson", "isabella.johnson#example.com"),
new Person("444", "Williams", "ethan.williams#example.com")
);
#Override
public void start(Stage stage) {
table.getItems().addAll(data);
Scene scene = new Scene(new Group());
stage.setTitle(
"Table View Sample");
stage.setWidth(
300);
stage.setHeight(
500);
final Label label = new Label("Address Book");
label.setFont(
new Font("Arial", 20));
TableColumn firstNameCol = new TableColumn("First");
TableColumn lastNameCol = new TableColumn("Last Name");
TableColumn emailCol = new TableColumn("Email");
firstNameCol.setCellValueFactory(
new PropertyValueFactory<Person, String>("firstName")
);
lastNameCol.setCellValueFactory(
new PropertyValueFactory<Person, String>("lastName")
);
emailCol.setCellValueFactory(
new PropertyValueFactory<Person, String>("email")
);
table.getColumns()
.addAll(firstNameCol, lastNameCol, emailCol);
final VBox vbox = new VBox();
vbox.setSpacing(
5);
vbox.setPadding(
new Insets(10, 0, 0, 10));
vbox.getChildren()
.addAll(label, table);
((Group) scene.getRoot()).getChildren().addAll(vbox);
stage.setScene(scene);
stage.show();
}
/**
* #param args the command line arguments
*/
public static void main(String[] args) {
launch(args);
}
}
Edit
Okay, I have found out the reason for this is because I use String in the model.. So the updated question is, how can you create a new sorting functionality on the table column so that it sorts as integer even though there's string in it?
Your values are Strings, so they are sorted lexicographically; since the character '4' comes before the character '9', '444' comes before '9'.
If you make those fields integer fields, then they will be sorted numerically. It's helpful here to use properly typed TableViews and TableColumns, instead of raw types.
import javafx.application.Application;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.geometry.Insets;
import javafx.scene.Group;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.control.cell.PropertyValueFactory;
import javafx.scene.layout.VBox;
import javafx.scene.text.Font;
import javafx.stage.Stage;
public class TableViewSortTest extends Application {
private TableView<Person> table = new TableView<>();
final ObservableList<Person> data = FXCollections.observableArrayList(
new Person(1, "Smith", "jacob.smith#example.com"),
new Person(9, "Johnson", "isabella.johnson#example.com"),
new Person(444, "Williams", "ethan.williams#example.com")
);
#Override
public void start(Stage stage) {
table.getItems().addAll(data);
Scene scene = new Scene(new Group());
stage.setTitle(
"Table View Sample");
stage.setWidth(
300);
stage.setHeight(
500);
final Label label = new Label("Address Book");
label.setFont(
new Font("Arial", 20));
TableColumn<Person, Integer> idCol = new TableColumn<>("Id");
TableColumn<Person, String> lastNameCol = new TableColumn<>("Last Name");
TableColumn<Person, String> emailCol = new TableColumn<>("Email");
idCol.setCellValueFactory(
new PropertyValueFactory<Person, Integer>("id")
);
lastNameCol.setCellValueFactory(
new PropertyValueFactory<Person, String>("name")
);
emailCol.setCellValueFactory(
new PropertyValueFactory<Person, String>("email")
);
table.getColumns()
.addAll(idCol, lastNameCol, emailCol);
final VBox vbox = new VBox();
vbox.setSpacing(
5);
vbox.setPadding(
new Insets(10, 0, 0, 10));
vbox.getChildren()
.addAll(label, table);
((Group) scene.getRoot()).getChildren().addAll(vbox);
stage.setScene(scene);
stage.show();
}
public static class Person {
private final IntegerProperty id = new SimpleIntegerProperty(this, "id");
private final StringProperty name = new SimpleStringProperty(this, "name");
private final StringProperty email = new SimpleStringProperty(this, "email");
public Person(int id, String name, String email) {
this.id.set(id);
this.name.set(name);
this.email.set(email);
}
public final IntegerProperty idProperty() {
return this.id;
}
public final int getId() {
return this.idProperty().get();
}
public final void setId(final int id) {
this.idProperty().set(id);
}
public final StringProperty nameProperty() {
return this.name;
}
public final java.lang.String getName() {
return this.nameProperty().get();
}
public final void setName(final java.lang.String name) {
this.nameProperty().set(name);
}
public final StringProperty emailProperty() {
return this.email;
}
public final java.lang.String getEmail() {
return this.emailProperty().get();
}
public final void setEmail(final java.lang.String email) {
this.emailProperty().set(email);
}
}
/**
* #param args the command line arguments
*/
public static void main(String[] args) {
launch(args);
}
}
You can create a Comparator:
firstNameCol.setComparator(new CustomComparator());
EDIT
Assuming you have a String type in your column, as it was originally posted, and you want to sort it numerically trying to parse all the strings, and in case there'are some non-integer strings, sort these lexicographically, this will be a valid Comparator:
private class CustomComparator implements Comparator<String>{
#Override
public int compare(String o1, String o2) {
if (o1 == null && o2 == null) return 0;
if (o1 == null) return -1;
if (o2 == null) return 1;
Integer i1=null;
try{ i1=Integer.valueOf(o1); } catch(NumberFormatException ignored){}
Integer i2=null;
try{ i2=Integer.valueOf(o2); } catch(NumberFormatException ignored){}
if(i1==null && i2==null) return o1.compareTo(o2);
if(i1==null) return -1;
if(i2==null) return 1;
return i1-i2;
}
}

JavaFX Table/TreeTable Cell Overflow

I want to get the text in a treetable cell to overflow into the adjacent cells. I believe that I read a while ago that there is no Span function in tables or treetables, but it might be accomplished through css.
See this image. I want the date in the first cell to extend past the others so it can be read.
http://imgur.com/KvK0adK
I thought I would use -overflow: visible; but I read in the Oracle documentation:
JavaFX CSS does not support CSS layout properties such as float, position, overflow, and width.
Can anyone point me in the right direction here? CSS would be preferable, but would a custom TreeTableRow be the right solution?
Thanks in advance.
Sample Solution
Here is an overflow cell for a TableView, you could probably adapt it for a TreeTableView.
class OverflowCell extends TableCell<Person, String> {
private Label overflowLabel = new Label();
private Group overflowGroup = new Group(overflowLabel);
public OverflowCell() {
// destroy the clip.
clipProperty().addListener((observable, oldClip, newClip) -> {
if (newClip != null) {
setClip(null);
}
});
}
#Override
protected void updateItem(String item, boolean empty) {
super.updateItem(item, empty);
if (empty || item == null) {
setGraphic(null);
} else {
overflowLabel.setText(item);
setGraphic(
overflowGroup
);
}
}
}
Normally the cells in a TableView are clipped, so the listener is added in the constructor of the OverflowCell to remove any clip added to the cell. The updateItem call uses a Group so that the Label is not resized to a size smaller than it's preferred size and elided.
There are probably other ways to do this. A custom row factory might be an alternate solution. This OverflowCell hack was just a simple thing which came to mind.
Sample Code
Here is it in action, you can see how the last name in the first line overflows into the next column.
Apologies for the amount of code here.
import javafx.application.Application;
import javafx.beans.property.SimpleStringProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.geometry.Insets;
import javafx.scene.Group;
import javafx.scene.Scene;
import javafx.scene.control.*;
import javafx.scene.control.cell.PropertyValueFactory;
import javafx.scene.layout.VBox;
import javafx.scene.text.Font;
import javafx.stage.Stage;
public class TableCellOverflow extends Application {
private TableView<Person> table = new TableView<>();
private final ObservableList<Person> data =
FXCollections.observableArrayList(
new Person("Jacob", "Krzyzanowski", ""),
new Person("Isabella", "Johnson", "isabella.johnson#example.com"),
new Person("Ethan", "Williams", "ethan.williams#example.com"),
new Person("Emma", "Jones", "emma.jones#example.com"),
new Person("Michael", "Brown", "michael.brown#example.com")
);
public static void main(String[] args) {
launch(args);
}
#Override
public void start(Stage stage) {
Scene scene = new Scene(new Group());
stage.setTitle("Table View Sample");
table.setPrefHeight(200);
final Label label = new Label("Address Book");
label.setFont(new Font("Arial", 20));
TableColumn<Person, String> firstNameCol = new TableColumn<>("First Name");
firstNameCol.setMaxWidth(80);
firstNameCol.setCellValueFactory(
new PropertyValueFactory<>("firstName"));
firstNameCol.getStyleClass().add("left-header");
TableColumn<Person, String> lastNameCol = new TableColumn<>();
lastNameCol.setMaxWidth(60);
lastNameCol.setCellValueFactory(
new PropertyValueFactory<>("lastName"));
lastNameCol.setCellFactory(param -> new OverflowCell());
TableColumn<Person, String> emailCol = new TableColumn<>("Email");
emailCol.setMaxWidth(100);
emailCol.setCellValueFactory(
new PropertyValueFactory<>("email"));
table.setItems(data);
table.getColumns().addAll(firstNameCol, lastNameCol, emailCol);
final VBox vbox = new VBox();
vbox.setSpacing(5);
vbox.setPadding(new Insets(10));
vbox.getChildren().addAll(label, table);
((Group) scene.getRoot()).getChildren().addAll(vbox);
stage.setScene(scene);
stage.show();
}
class OverflowCell extends TableCell<Person, String> {
private Label overflowLabel = new Label();
private Group overflowGroup = new Group(overflowLabel);
public OverflowCell() {
// destroy the clip.
clipProperty().addListener((observable, oldClip, newClip) -> {
if (newClip != null) {
setClip(null);
}
});
}
#Override
protected void updateItem(String item, boolean empty) {
super.updateItem(item, empty);
if (empty || item == null) {
setGraphic(null);
} else {
overflowLabel.setText(item);
setGraphic(
overflowGroup
);
}
}
}
public static class Person {
private final SimpleStringProperty firstName;
private final SimpleStringProperty lastName;
private final SimpleStringProperty email;
private Person(String fName, String lName, String email) {
this.firstName = new SimpleStringProperty(fName);
this.lastName = new SimpleStringProperty(lName);
this.email = new SimpleStringProperty(email);
}
public String getFirstName() {
return firstName.get();
}
public void setFirstName(String fName) {
firstName.set(fName);
}
public String getLastName() {
return lastName.get();
}
public void setLastName(String fName) {
lastName.set(fName);
}
public String getEmail() {
return email.get();
}
public void setEmail(String fName) {
email.set(fName);
}
}
}

Can a JavaFX TableColumn's header text be made to bleed over to another column?

I have a TableView with two TableColumns. On the left is a wide column that needs to be able to show entries up to 30 characters. On the right is a narrow column that needs to be able to show just one character. But the title of the right column needs to be a long word (it is required). But I don't have the luxury in terms of space to have a column as fat as the required header text. If I make the right column as wide as its header text, the data in the left column gets truncated. I would like the header text of the column on the right to bleed over into the column on the left (there is no worry of it running into the left column's header text). Is this possible by setting some fx css of my TableView and/or one or both of the TableColumns?
Here is an example where the column header "Long Last Name" bleeds into the column header to the left of it.
bleed.css
.column-header.left-header > .label {
-fx-alignment: baseline-left;
}
.column-header.bleed-header > .label {
-fx-alignment: baseline-right;
-fx-padding: 0 3 0 0;
}
Code snippet which enables the "bleed"
TableColumn<Person, String> lastNameCol = new TableColumn<>();
lastNameCol.setMinWidth(80);
lastNameCol.setCellValueFactory(
new PropertyValueFactory<>("lastName"));
lastNameCol.getStyleClass().add("bleed-header");
Label headerLabel = new Label("Long Last Name");
headerLabel.setMinWidth(Control.USE_PREF_SIZE);
lastNameCol.setGraphic(headerLabel);
The solution required a bit of trickery:
Use of css to set alignment properties.
Use a label graphic for the header.
Supplying the label graphic gives you direct control of the minimum width (to prevent the default behaviour of the header label being elided when it is too long).
Executable code sample
import javafx.application.Application;
import javafx.beans.property.SimpleStringProperty;
import javafx.collections.*;
import javafx.geometry.Insets;
import javafx.scene.*;
import javafx.scene.control.*;
import javafx.scene.control.cell.PropertyValueFactory;
import javafx.scene.layout.VBox;
import javafx.scene.text.Font;
import javafx.stage.Stage;
public class TableViewHeaderBleed extends Application {
private TableView<Person> table = new TableView<>();
private final ObservableList<Person> data =
FXCollections.observableArrayList(
new Person("Jacob", "Smith", "jacob.smith#example.com"),
new Person("Isabella", "Johnson", "isabella.johnson#example.com"),
new Person("Ethan", "Williams", "ethan.williams#example.com"),
new Person("Emma", "Jones", "emma.jones#example.com"),
new Person("Michael", "Brown", "michael.brown#example.com")
);
public static void main(String[] args) {
launch(args);
}
#Override
public void start(Stage stage) {
Scene scene = new Scene(new Group());
stage.setTitle("Table View Sample");
table.setPrefHeight(200);
final Label label = new Label("Address Book");
label.setFont(new Font("Arial", 20));
TableColumn<Person, String> firstNameCol = new TableColumn<>("First Name");
firstNameCol.setMinWidth(100);
firstNameCol.setCellValueFactory(
new PropertyValueFactory<>("firstName"));
firstNameCol.getStyleClass().add("left-header");
TableColumn<Person, String> lastNameCol = new TableColumn<>();
lastNameCol.setMinWidth(80);
lastNameCol.setCellValueFactory(
new PropertyValueFactory<>("lastName"));
lastNameCol.getStyleClass().add("bleed-header");
Label headerLabel = new Label("Long Last Name");
headerLabel.setMinWidth(Control.USE_PREF_SIZE);
lastNameCol.setGraphic(headerLabel);
TableColumn<Person, String> emailCol = new TableColumn<>("Email");
emailCol.setMinWidth(100);
emailCol.setCellValueFactory(
new PropertyValueFactory<>("email"));
table.setItems(data);
table.getColumns().addAll(firstNameCol, lastNameCol, emailCol);
final VBox vbox = new VBox();
vbox.setSpacing(5);
vbox.setPadding(new Insets(10));
vbox.getChildren().addAll(label, table);
((Group) scene.getRoot()).getChildren().addAll(vbox);
scene.getStylesheets().add(getClass().getResource("bleed.css").toExternalForm());
stage.setScene(scene);
stage.show();
}
public static class Person {
private final SimpleStringProperty firstName;
private final SimpleStringProperty lastName;
private final SimpleStringProperty email;
private Person(String fName, String lName, String email) {
this.firstName = new SimpleStringProperty(fName);
this.lastName = new SimpleStringProperty(lName);
this.email = new SimpleStringProperty(email);
}
public String getFirstName() {
return firstName.get();
}
public void setFirstName(String fName) {
firstName.set(fName);
}
public String getLastName() {
return lastName.get();
}
public void setLastName(String fName) {
lastName.set(fName);
}
public String getEmail() {
return email.get();
}
public void setEmail(String fName) {
email.set(fName);
}
}
}

Resources