We are running a JavaFX application that contains some editable table views. A new requested feature is: a button that adds a new row below the currently selected one and immediately starts to edit the first cell of the row.
We implemented this feature, which was not so complicated, but we experience a very strange behavior and after a couple of days investigating the issue we still have no idea what goes wrong.
What happens is that when one clicks the button it adds a new row but starts to edit to first cell not of the newly created row but on an arbitrary other row. Unfortunately, this issue is not 100% reproduceable. Sometimes it's working as expected but most often the row below the newly added row gets edited, but sometimes even completely different rows before and after the currently selected one.
Below you can find the source code of a stripped down version of a JavaFX TableView that can be used to see the issue. As already mentioned it is not 100% reproduceable. To see the issue you have to add a new row multiple times. Some times the issue occurs more often when scrolling the table up and down a couple of times.
Any help is appreciated.
Hint: we already played around with Platform.runlater() a lot, by placing the action implementation of the button inside a runlater(), but although the issue occurs less often then, it never disappeared completely.
The TableView:
package tableview;
import java.util.ArrayList;
import java.util.List;
import javafx.application.Application;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.event.ActionEvent;
import javafx.geometry.Insets;
import javafx.scene.Group;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.TableCell;
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;
import javafx.util.Callback;
#SuppressWarnings({ "rawtypes", "unchecked" })
public class SimpleTableViewTest extends Application {
private final ObservableList<Person> data = FXCollections.observableArrayList(createData());
private final TableView table = new TableView();
public static void main(String[] args) {
launch(args);
}
private static List<Person> createData() {
List<Person> data = new ArrayList<>();
for (int i = 0; i < 100; i++) {
data.add(new Person("Jacob", "Smith", "jacob.smith_at_example.com", "js_at_example.com"));
}
return data;
}
#Override
public void start(Stage stage) {
Scene scene = new Scene(new Group());
stage.setTitle("Table View Sample");
stage.setWidth(700);
stage.setHeight(550);
final Label label = new Label("Address Book");
label.setFont(new Font("Arial", 20));
// Create a customer cell factory so that cells can support editing.
Callback<TableColumn, TableCell> cellFactory = (TableColumn p) -> {
return new EditingCell();
};
// Set up the columns
TableColumn firstNameCol = new TableColumn("First Name");
firstNameCol.setMinWidth(100);
firstNameCol.setCellValueFactory(new PropertyValueFactory<Person, String>("firstName"));
firstNameCol.setCellFactory(cellFactory);
TableColumn lastNameCol = new TableColumn("Last Name");
lastNameCol.setMinWidth(100);
lastNameCol.setCellValueFactory(new PropertyValueFactory<Person, String>("lastName"));
lastNameCol.setCellFactory(cellFactory);
lastNameCol.setEditable(true);
TableColumn primaryEmailCol = new TableColumn("Primary Email");
primaryEmailCol.setMinWidth(200);
primaryEmailCol.setCellValueFactory(new PropertyValueFactory<Person, String>("primaryEmail"));
primaryEmailCol.setCellFactory(cellFactory);
primaryEmailCol.setEditable(false);
TableColumn secondaryEmailCol = new TableColumn("Secondary Email");
secondaryEmailCol.setMinWidth(200);
secondaryEmailCol.setCellValueFactory(new PropertyValueFactory<Person, String>("secondaryEmail"));
secondaryEmailCol.setCellFactory(cellFactory);
// Add the columns and data to the table.
table.setItems(data);
table.getColumns().addAll(firstNameCol, lastNameCol, primaryEmailCol, secondaryEmailCol);
table.setEditable(true);
// --- Here comes the interesting part! ---
//
// A button that adds a row below the currently selected one
// and immediatly starts editing it.
Button addAndEdit = new Button("Add and edit");
addAndEdit.setOnAction((ActionEvent e) -> {
int idx = table.getSelectionModel().getSelectedIndex() + 1;
data.add(idx, new Person());
table.getSelectionModel().select(idx);
table.edit(idx, firstNameCol);
});
final VBox vbox = new VBox();
vbox.setSpacing(5);
vbox.getChildren().addAll(label, table, addAndEdit);
vbox.setPadding(new Insets(10, 0, 0, 10));
((Group) scene.getRoot()).getChildren().addAll(vbox);
stage.setScene(scene);
stage.show();
}
}
The editable Table Cell:
package tableview;
import javafx.event.EventHandler;
import javafx.scene.control.ContentDisplay;
import javafx.scene.control.TableCell;
import javafx.scene.control.TextField;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;
public class EditingCell extends TableCell<Person, String> {
private TextField textField;
public EditingCell() {
}
#Override
public void cancelEdit() {
super.cancelEdit();
setText(getItem());
setContentDisplay(ContentDisplay.TEXT_ONLY);
}
#Override
public void startEdit() {
super.startEdit();
if (textField == null) {
createTextField();
}
setGraphic(textField);
setContentDisplay(ContentDisplay.GRAPHIC_ONLY);
}
#Override
public void updateItem(String item, boolean empty) {
super.updateItem(item, empty);
if (empty) {
setText(null);
setGraphic(null);
} else {
if (isEditing()) {
if (textField != null) {
textField.setText(getString());
}
setGraphic(textField);
setContentDisplay(ContentDisplay.GRAPHIC_ONLY);
} else {
setText(getString());
setContentDisplay(ContentDisplay.TEXT_ONLY);
}
}
}
private void createTextField() {
textField = new TextField(getString());
textField.setMinWidth(this.getWidth() - this.getGraphicTextGap() * 2);
textField.setOnKeyPressed(new EventHandler<KeyEvent>() {
#Override
public void handle(KeyEvent t) {
if (t.getCode() == KeyCode.ENTER) {
commitEdit(textField.getText());
} else if (t.getCode() == KeyCode.ESCAPE) {
cancelEdit();
}
}
});
}
private String getString() {
return getItem() == null ? "" : getItem().toString();
}
}
A Data Bean:
package tableview;
import javafx.beans.property.SimpleStringProperty;
public class Person {
private final SimpleStringProperty firstName;
private final SimpleStringProperty lastName;
private final SimpleStringProperty primaryEmail;
private final SimpleStringProperty secondaryEmail;
public Person() {
this(null, null, null, null);
}
public Person(String firstName, String lastName, String primaryEmail, String secondaryEmail) {
this.firstName = new SimpleStringProperty(firstName);
this.lastName = new SimpleStringProperty(lastName);
this.primaryEmail = new SimpleStringProperty(primaryEmail);
this.secondaryEmail = new SimpleStringProperty(secondaryEmail);
}
public SimpleStringProperty firstNameProperty() {
return firstName;
}
public String getFirstName() {
return firstName.get();
}
public String getLastName() {
return lastName.get();
}
public String getPrimaryEmail() {
return primaryEmail.get();
}
public SimpleStringProperty getPrimaryEmailProperty() {
return primaryEmail;
}
public String getSecondaryEmail() {
return secondaryEmail.get();
}
public SimpleStringProperty getSecondaryEmailProperty() {
return secondaryEmail;
}
public SimpleStringProperty lastNameProperty() {
return lastName;
}
public void setFirstName(String firstName) {
this.firstName.set(firstName);
}
public void setLastName(String lastName) {
this.lastName.set(lastName);
}
public void setPrimaryEmail(String primaryEmail) {
this.primaryEmail.set(primaryEmail);
}
public void setSecondaryEmail(String secondaryEmail) {
this.secondaryEmail.set(secondaryEmail);
}
}
The correct code of the buttons action implementation has to look like below. The important line to fix the described issue is 'table.layout()'.
Many thanks to fabian!
addAndEdit.setOnAction((ActionEvent e) -> {
int idx = table.getSelectionModel().getSelectedIndex() + 1;
data.add(idx, new Person());
table.getSelectionModel().select(idx);
table.layout();
table.edit(idx, firstNameCol);
});
Related
I am working on a TableView (FXML) where I want to have all the rows accompanied with a delete button at the last column.
Here's a video that shows what I mean: YouTube Delete Button in TableView
Here's what I have in my main controller class:
public Button del() {
Button del = new Button();
del.setText("X");
del.setPrefWidth(30);
del.setOnAction(new EventHandler<ActionEvent>() {
public void handle(ActionEvent event) {
int i = index.get();
if(i > -1) {
goals.remove(i);
list.getSelectionModel().clearSelection();
}
}
});
return del;
}
private SimpleIntegerProperty index = new SimpleIntegerProperty();
#Override
public void initialize(URL location, ResourceBundle resources){
//DateFormat df = new SimpleDateFormat("dd MMM yyyy");
sdate.setValue(LocalDate.now());
edate.setValue(LocalDate.now());
seq.setCellValueFactory(new PropertyValueFactory<Goals, Integer>("id"));
gol.setCellValueFactory(new PropertyValueFactory<Goals, String>("goal"));
sdt.setCellValueFactory(new PropertyValueFactory<Goals, Date>("sdte"));
edt.setCellValueFactory(new PropertyValueFactory<Goals, Date>("edte"));
prog.setCellValueFactory(new PropertyValueFactory<Goals, Integer>("pb"));
del.setCellValueFactory(new PropertyValueFactory<Goals, Button>("x"));
list.setItems(goals);
list.getSelectionModel().selectedItemProperty().addListener(new ChangeListener<Object>() {
#Override
public void changed(ObservableValue<?> observable,
Object oldValue, Object newValue) {
index.set(goals.indexOf(newValue));
System.out.println("Index is: "+goals.indexOf(newValue));
}
});
}
Each time I launch the application, I will try to click the delete button from random rows but it always delete the first row. I guess the addListener method I use for list is not properly implemented and indexOf(newValue) is always 0 at every initialisation.
However, it will work if I click a row first and then click the delete button. But this is not what I want. I want users to be able to delete any row if they press the delete button without selecting the row.
Appreciate your help guys!
You need a custom cell factory defined for the column containing the delete button.
TableColumn<Person, Person> unfriendCol = new TableColumn<>("Anti-social");
unfriendCol.setCellValueFactory(
param -> new ReadOnlyObjectWrapper<>(param.getValue())
);
unfriendCol.setCellFactory(param -> new TableCell<Person, Person>() {
private final Button deleteButton = new Button("Unfriend");
#Override
protected void updateItem(Person person, boolean empty) {
super.updateItem(person, empty);
if (person == null) {
setGraphic(null);
return;
}
setGraphic(deleteButton);
deleteButton.setOnAction(
event -> getTableView().getItems().remove(person)
);
}
});
Here is a sample app. It doesn't use FXML, but you could adapt it to work with FXML very easily. Just click on an "Unfriend" button in the "Anti-social" column to delete a friend. Do it a lot and you will soon run out of friends.
import javafx.application.Application;
import javafx.beans.property.ReadOnlyObjectWrapper;
import javafx.beans.property.SimpleStringProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.geometry.Insets;
import javafx.scene.Scene;
import javafx.scene.control.*;
import javafx.scene.control.cell.PropertyValueFactory;
import javafx.scene.layout.Priority;
import javafx.scene.layout.VBox;
import javafx.scene.text.Font;
import javafx.stage.Stage;
public class GestureEvents extends Application {
private TableView<Person> table = new TableView<>();
private final ObservableList<Person> data =
FXCollections.observableArrayList(
new Person("Jacob", "Smith"),
new Person("Isabella", "Johnson"),
new Person("Ethan", "Williams"),
new Person("Emma", "Jones"),
new Person("Michael", "Brown")
);
public static void main(String[] args) {
launch(args);
}
#Override
public void start(Stage stage) {
final Label label = new Label("Friends");
label.setFont(new Font("Arial", 20));
final Label actionTaken = new Label();
TableColumn<Person, Person> unfriendCol = new TableColumn<>("Anti-social");
unfriendCol.setMinWidth(40);
unfriendCol.setCellValueFactory(param -> new ReadOnlyObjectWrapper<>(param.getValue()));
unfriendCol.setCellFactory(param -> new TableCell<Person, Person>() {
private final Button deleteButton = new Button("Unfriend");
#Override
protected void updateItem(Person person, boolean empty) {
super.updateItem(person, empty);
if (person == null) {
setGraphic(null);
return;
}
setGraphic(deleteButton);
deleteButton.setOnAction(event -> data.remove(person));
}
});
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"));
table.setItems(data);
table.getColumns().addAll(unfriendCol, firstNameCol, lastNameCol);
table.setPrefHeight(250);
final VBox vbox = new VBox();
vbox.setSpacing(5);
vbox.setPadding(new Insets(10, 10, 10, 10));
vbox.getChildren().addAll(label, table, actionTaken);
VBox.setVgrow(table, Priority.ALWAYS);
stage.setScene(new Scene(vbox));
stage.show();
}
public static class Person {
private final SimpleStringProperty firstName;
private final SimpleStringProperty lastName;
private Person(String fName, String lName) {
this.firstName = new SimpleStringProperty(fName);
this.lastName = new SimpleStringProperty(lName);
}
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);
}
}
}
I want to apply a custom style using the setStyle method to the CheckBox of a TableView but I can not do it. I have tried to create a CheckBox inside the setCellFactory regardless of CheckBoxTableCell but in that case the value in the ObservableList<Person> is no longer updated.
My code is the following:
public class Person {
private String name;
private boolean accepted;
public Person(String name, boolean accepted) {
this.name = name;
this.accepted = accepted;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public boolean isAccepted() {
return accepted;
}
public void setAccepted(boolean accepted) {
this.accepted = accepted;
}
}
The main class:
import javafx.application.Application;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.value.ObservableValue;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableColumn.CellDataFeatures;
import javafx.scene.control.TableView;
import javafx.scene.control.cell.CheckBoxTableCell;
import javafx.scene.control.cell.PropertyValueFactory;
import javafx.scene.control.cell.TextFieldTableCell;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;
public class TableViewWithCheckBox extends Application {
#Override
public void start(Stage stage) {
TableView<Person> table = new TableView<>();
// Editable
table.setEditable(true);
TableColumn<Person, String> fullNameCol = new TableColumn<>("Name");
TableColumn<Person, Boolean> acceptedCol = new TableColumn<>("Accepted");
// NAME
fullNameCol.setCellValueFactory(new PropertyValueFactory<>("name"));
fullNameCol.setCellFactory(TextFieldTableCell.<Person> forTableColumn());
fullNameCol.setMinWidth(200);
// ACCEPTED
acceptedCol.setCellValueFactory((CellDataFeatures<Person, Boolean> param) -> {
Person person = param.getValue();
SimpleBooleanProperty booleanProp = new SimpleBooleanProperty(person.isAccepted());
booleanProp.addListener(
(ObservableValue<? extends Boolean> observable,
Boolean oldValue, Boolean newValue) -> {
person.setAccepted(newValue);
});
return booleanProp;
});
acceptedCol.setCellFactory((TableColumn<Person, Boolean> p) -> {
CheckBoxTableCell<Person, Boolean> cell = new CheckBoxTableCell<>();
cell.setAlignment(Pos.CENTER);
return cell;
});
ObservableList<Person> list = getPersonList();
table.setItems(list);
table.getColumns().addAll(fullNameCol, acceptedCol);
StackPane root = new StackPane();
root.setPadding(new Insets(5));
root.getChildren().add(table);
Scene scene = new Scene(root, 300, 300);
stage.setScene(scene);
stage.show();
}
private ObservableList<Person> getPersonList() {
Person person1 = new Person("John White", true);
Person person2 = new Person("Kevin Land", false);
Person person3 = new Person("Rouse Hill", true);
ObservableList<Person> list = FXCollections.observableArrayList(person1, person2, person3);
return list;
}
}
And the code with a custom CheckBox but that does not update the observable list:
acceptedCol.setCellFactory((TableColumn<Person, Boolean> success) -> {
TableCell<Person, Boolean> cell = new TableCell<Person, Boolean>(){
#Override
public void updateItem(Boolean item, boolean empty) {
super.updateItem(item, empty);
if(empty || item == null){
setGraphic(null);
} else {
CheckBox myCheckBox = new CheckBox();
myCheckBox.getStyleClass().add("myPersonalCheckBoxStyle");
myCheckBox.setSelected(item);
setGraphic(myCheckBox);
}
}
};
return cell;
});
CheckBoxTableCell does all the heavy lifting for you, just need a little help from your Person class which should use/expose properties (vs. plain fields with getters/setters only). Then configure the column with the acceptedProperty and apply a custom style to the cell.
In code snippets:
public class Person {
private BooleanProperty accepted;
public Person(String name, boolean single) {
this.accepted = new SimpleBooleanProperty(single);
...
}
public BooleanProperty acceptedProperty() {
return accepted;
}
public boolean isAccepted() {
return acceptedProperty().get();
}
....
Configuration of the column with cell/Value/Factory:
acceptedCol.setCellValueFactory(new PropertyValueFactory<>("accepted"));
acceptedCol.setCellFactory((TableColumn<Person, Boolean> p) -> {
CheckBoxTableCell<Person, Boolean> cell = new CheckBoxTableCell<>();
cell.getStyleClass().add("custom-cell");
cell.setAlignment(Pos.CENTER);
return cell;
});
Some custom style:
/**
* Just want to see an effect, doesn't matter what
*/
.custom-cell > .check-box {
-fx-background-color: blue;
}
.custom-cell > .check-box:selected {
-fx-background-color: red;
}
I have written this little example application:
package application;
import javafx.application.Application;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.event.EventHandler;
import javafx.scene.Scene;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableColumn.CellEditEvent;
import javafx.scene.control.TableView;
import javafx.scene.control.cell.PropertyValueFactory;
import javafx.scene.control.cell.TextFieldTableCell;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;
public class Main extends Application {
public class Person {
private StringProperty firstName = new SimpleStringProperty();
private StringProperty lastName = new SimpleStringProperty();
public Person(String firstName, String lastName) {
this.firstName.set(firstName);
this.lastName.set(lastName);
}
public String getFirstName() {
return firstName.get();
}
public String getLastName() {
return lastName.get();
}
public StringProperty firstNameProperty() {
return firstName;
}
public StringProperty lastNameProperty() {
return lastName;
}
}
#Override
public void start(Stage primaryStage) {
try {
StackPane root = new StackPane();
TableView<Person> tv = new TableView<>();
TableColumn<Person, String> col = new TableColumn<Person, String>("FirstName");
col.setCellValueFactory(new PropertyValueFactory<Person, String>("firstName"));
tv.getColumns().add(col);
tv.setEditable(true);
col = new TableColumn<Person, String>("LastName");
col.setCellValueFactory(new PropertyValueFactory<Person, String>("lastName"));
col.setCellFactory(TextFieldTableCell.forTableColumn());
col.setOnEditCommit(new EventHandler<TableColumn.CellEditEvent<Person, String>>() {
#Override
public void handle(CellEditEvent<Person, String> event) {
System.out.println(tv.getItems().get(1).getLastName());
}
});
tv.getColumns().add(col);
for (int i = 0; i < 30; i++) {
tv.getItems().add(new Person("Test" + i, "Test" + i));
}
root.getChildren().add(tv);
Scene scene = new Scene(root, 400, 200);
primaryStage.setScene(scene);
primaryStage.show();
tv.addEventFilter(MouseEvent.MOUSE_RELEASED, event -> {
// ...
});
} catch (Exception e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
launch(args);
}
}
I want to perform action when the ScrollBar has reached the bottom. Then I want to reload more datas from the database. But only then, when the user has seen all the already loaded datas (= scrollbar on the bottom). Do you have nice suggestions to solve this issue?
My first idea was to catch the MOUSE_RELEASED event (when the users drags the bar) of the TableView and then to check the position of the ScrollBar:
- getValue() gets the position of the bar
- getMax() the maximum value (=bottom).
But I can't find a way (without using the css-selector via this method) to get the ScrollBar from a given TableView. So I can't check the position of it in a certain TableView.
Do you have any ideas??
I am excited. Thanks for your help.
The only way to get the scroll bar is via a lookup, which is a bit of a hack, but it will work as long as you do it after the table has been rendered on the scene. You need
ScrollBar verticalBar = (ScrollBar) table.lookup(".scroll-bar:vertical");
Note that there's no need to mess with user events: you can just observe the scroll bar's value property directly:
verticalBar.valueProperty().addListener((obs, oldValue, newValue) -> {
if (newValue.doubleValue() >= verticalBar.getMax()) {
// add more data...
}
});
SSCCE:
import java.util.ArrayList;
import java.util.List;
import java.util.function.Function;
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.beans.value.ObservableValue;
import javafx.concurrent.Task;
import javafx.scene.Scene;
import javafx.scene.control.ScrollBar;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.layout.BorderPane;
import javafx.stage.Stage;
public class AddMoreTableDataOnScrollToBottom extends Application {
#Override
public void start(Stage primaryStage) {
TableView<Item> table = new TableView<>();
table.getColumns().add(column("Item", Item::nameProperty));
table.getColumns().add(column("Value", Item::valueProperty));
addMoreData(table, 20);
Scene scene = new Scene(new BorderPane(table), 400, 400);
primaryStage.setScene(scene);
primaryStage.show();
ScrollBar verticalBar = (ScrollBar) table.lookup(".scroll-bar:vertical");
verticalBar.valueProperty().addListener((obs, oldValue, newValue) -> {
if (newValue.doubleValue() >= verticalBar.getMax()) {
addMoreData(table, 20);
}
});
}
private void addMoreData(TableView<Item> table, int numItems) {
Task<List<Item>> dataRetrieveTask = new Task<List<Item>>() {
#Override
public List<Item> call() throws Exception {
// mimic connect to db:
Thread.sleep(500);
List<Item> items = new ArrayList<>();
int nextItem = table.getItems().size() + 1 ;
for (int i = nextItem; i < nextItem + numItems; i++ ){
items.add(new Item("Item "+i, i));
}
return items ;
}
};
dataRetrieveTask.setOnSucceeded(e -> table.getItems().addAll(dataRetrieveTask.getValue()));
new Thread(dataRetrieveTask).start();
}
private <S,T> TableColumn<S,T> column(String title, Function<S, ObservableValue<T>> prop) {
TableColumn<S,T> col = new TableColumn<>(title);
col.setCellValueFactory(cellData -> prop.apply(cellData.getValue()));
return col ;
}
public static class Item {
private final StringProperty name = new SimpleStringProperty();
private final IntegerProperty value = new SimpleIntegerProperty();
public Item(String name, int value) {
setName(name);
setValue(value);
}
public final StringProperty nameProperty() {
return this.name;
}
public final String getName() {
return this.nameProperty().get();
}
public final void setName(final String name) {
this.nameProperty().set(name);
}
public final IntegerProperty valueProperty() {
return this.value;
}
public final int getValue() {
return this.valueProperty().get();
}
public final void setValue(final int value) {
this.valueProperty().set(value);
}
}
public static void main(String[] args) {
launch(args);
}
}
I'm using the following code from Oracle:
import java.util.Arrays;
import java.util.List;
import javafx.application.Application;
import javafx.event.Event;
import javafx.event.EventHandler;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.control.TextField;
import javafx.scene.control.TreeCell;
import javafx.scene.control.TreeItem;
import javafx.scene.control.TreeView;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;
import javafx.scene.paint.Color;
import javafx.stage.Stage;
import javafx.util.Callback;
import javafx.beans.property.SimpleStringProperty;
import javafx.scene.control.ContextMenu;
import javafx.scene.control.MenuItem;
import javafx.scene.layout.VBox;
public class TreeViewSample extends Application {
List<Employee> employees = Arrays.<Employee>asList(
new Employee("Ethan Williams", "Sales Department"),
new Employee("Emma Jones", "Sales Department"),
new Employee("Michael Brown", "Sales Department"),
new Employee("Anna Black", "Sales Department"),
new Employee("Rodger York", "Sales Department"),
new Employee("Susan Collins", "Sales Department"),
new Employee("Mike Graham", "IT Support"),
new Employee("Judy Mayer", "IT Support"),
new Employee("Gregory Smith", "IT Support"),
new Employee("Jacob Smith", "Accounts Department"),
new Employee("Isabella Johnson", "Accounts Department"));
TreeItem<String> rootNode =
new TreeItem<String>("MyCompany Human Resources");
public static void main(String[] args) {
Application.launch(args);
}
#Override
public void start(Stage stage) {
rootNode.setExpanded(true);
for (Employee employee : employees) {
TreeItem<String> empLeaf = new TreeItem<String>(employee.getName());
boolean found = false;
for (TreeItem<String> depNode : rootNode.getChildren()) {
if (depNode.getValue().contentEquals(employee.getDepartment())){
depNode.getChildren().add(empLeaf);
found = true;
break;
}
}
if (!found) {
TreeItem depNode = new TreeItem(employee.getDepartment());
rootNode.getChildren().add(depNode);
depNode.getChildren().add(empLeaf);
}
}
stage.setTitle("Tree View Sample");
VBox box = new VBox();
final Scene scene = new Scene(box, 400, 300);
scene.setFill(Color.LIGHTGRAY);
TreeView<String> treeView = new TreeView<String>(rootNode);
treeView.setEditable(true);
treeView.setCellFactory(new Callback<TreeView<String>,TreeCell<String>>(){
#Override
public TreeCell<String> call(TreeView<String> p) {
return new TextFieldTreeCellImpl();
}
});
box.getChildren().add(treeView);
stage.setScene(scene);
stage.show();
}
private final class TextFieldTreeCellImpl extends TreeCell<String> {
private TextField textField;
private ContextMenu addMenu = new ContextMenu();
public TextFieldTreeCellImpl() {
MenuItem addMenuItem = new MenuItem("Add Employee");
addMenu.getItems().add(addMenuItem);
addMenuItem.setOnAction(new EventHandler() {
public void handle(Event t) {
TreeItem newEmployee =
new TreeItem<String>("New Employee");
getTreeItem().getChildren().add(newEmployee);
}
});
}
#Override
public void startEdit() {
super.startEdit();
if (textField == null) {
createTextField();
}
setText(null);
setGraphic(textField);
textField.selectAll();
}
#Override
public void cancelEdit() {
super.cancelEdit();
setText((String) getItem());
setGraphic(getTreeItem().getGraphic());
}
#Override
public void updateItem(String item, boolean empty) {
super.updateItem(item, empty);
if (empty) {
setText(null);
setGraphic(null);
} else {
if (isEditing()) {
if (textField != null) {
textField.setText(getString());
}
setText(null);
setGraphic(textField);
} else {
setText(getString());
setGraphic(getTreeItem().getGraphic());
if (
!getTreeItem().isLeaf()&&getTreeItem().getParent()!= null
){
setContextMenu(addMenu);
}
}
}
}
private void createTextField() {
textField = new TextField(getString());
textField.setOnKeyReleased(new EventHandler<KeyEvent>() {
#Override
public void handle(KeyEvent t) {
if (t.getCode() == KeyCode.ENTER) {
commitEdit(textField.getText());
} else if (t.getCode() == KeyCode.ESCAPE) {
cancelEdit();
}
}
});
}
private String getString() {
return getItem() == null ? "" : getItem().toString();
}
}
public static class Employee {
private final SimpleStringProperty name;
private final SimpleStringProperty department;
private Employee(String name, String department) {
this.name = new SimpleStringProperty(name);
this.department = new SimpleStringProperty(department);
}
public String getName() {
return name.get();
}
public void setName(String fName) {
name.set(fName);
}
public String getDepartment() {
return department.get();
}
public void setDepartment(String fName) {
department.set(fName);
}
}
}
This code produces a GUI with a very basic, editable TreeView. However, when clicking around the cells that populate the tree, eventually, the text fields used for editing will begin to display incorrect information (information that is contained within the tree, but that is not represented by the cell being edited). I do not understand why this is happening, and I haven't found any reference of this happening anywhere on Google or elsewhere on StackOverflow. If anyone could help me understand why this is occuring, I'd be very happy.
Thanks!
I checked your application and I have found only one weird thing. When you create the edit textfield once, the unsaved information will be visible in that textfield later.
This modification will solve it:
# Override
public void cancelEdit() {
super.cancelEdit();
this.setText(this.getItem());
this.textField.setText(this.getItem());
this.setGraphic(this.getTreeItem().getGraphic());
}
I have this simple example of a table.
import javafx.application.Application;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.event.EventHandler;
import javafx.geometry.Insets;
import javafx.scene.Group;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.control.TableCell;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableColumn.CellEditEvent;
import javafx.scene.control.TableView;
import javafx.scene.control.TextField;
import javafx.scene.control.cell.PropertyValueFactory;
import javafx.scene.layout.Priority;
import javafx.scene.layout.VBox;
import javafx.scene.text.Font;
import javafx.stage.Stage;
import javafx.util.Callback;
public class MainApp extends Application
{
private TableView<Person> table = new TableView<>();
private final ObservableList<Person> data
= FXCollections.observableArrayList(
new Person("Processor", "72"),
new Person("RAM", "78"),
new Person("HDD Free Space", "890"),
new Person("Lan Adapter NIC 1", "36"),
new Person("Lan Adapter NIC 2", "67"));
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(850);
//stage.setHeight(550);
final Label label = new Label("Address Book");
label.setFont(new Font("Arial", 20));
table.setEditable(false);
Callback<TableColumn, TableCell> cellFactory
= new Callback<TableColumn, TableCell>()
{
#Override
public TableCell call(TableColumn p)
{
return new EditingCell();
}
};
table.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY);
TableColumn firstNameCol = new TableColumn("Credentials");
//firstNameCol.setPrefWidth(300);
firstNameCol.setMinWidth(50);
//firstNameCol.prefWidthProperty().bind(table.widthProperty().divide(2)); // w * 1/2
firstNameCol.setCellValueFactory(new PropertyValueFactory<Person, String>("firstName"));
firstNameCol.setCellFactory(cellFactory);
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("Value");
//lastNameCol.setPrefWidth(300);
lastNameCol.setMinWidth(50);
//lastNameCol.prefWidthProperty().bind(table.widthProperty().divide(2)); // w * 1/2
lastNameCol.setCellValueFactory(new PropertyValueFactory<Person, String>("lastName"));
lastNameCol.setCellFactory(cellFactory);
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());
}
}
);
table.setItems(data);
table.getColumns().addAll(firstNameCol, lastNameCol);
final VBox vbox = new VBox();
VBox.setVgrow(table, Priority.ALWAYS);
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 SimpleStringProperty firstName;
private final SimpleStringProperty lastName;
private Person(String fName, String lName)
{
this.firstName = new SimpleStringProperty(fName);
this.lastName = new SimpleStringProperty(lName);
}
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);
}
}
class EditingCell extends TableCell<Person, String>
{
private TextField textField;
public EditingCell()
{
}
#Override
public void startEdit()
{
if (!isEmpty())
{
super.startEdit();
createTextField();
setText(null);
setGraphic(textField);
textField.selectAll();
}
}
#Override
public void cancelEdit()
{
super.cancelEdit();
setText((String) getItem());
setGraphic(null);
}
#Override
public void updateItem(String item, boolean empty)
{
super.updateItem(item, empty);
if (empty)
{
setText(null);
setGraphic(null);
}
else
{
if (isEditing())
{
if (textField != null)
{
textField.setText(getString());
}
setText(null);
setGraphic(textField);
}
else
{
setText(getString());
setGraphic(null);
}
}
}
private void createTextField()
{
textField = new TextField(getString());
textField.setMinWidth(this.getWidth() - this.getGraphicTextGap() * 2);
textField.focusedProperty().addListener(new ChangeListener<Boolean>()
{
#Override
public void changed(ObservableValue<? extends Boolean> arg0,
Boolean arg1, Boolean arg2)
{
if (!arg2)
{
commitEdit(textField.getText());
}
}
});
}
private String getString()
{
return getItem() == null ? "" : getItem().toString();
}
}
}
I want resize the table and fit the main stage when I resize the window. How I can do this?
P.S I tested to add table.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY); but nothing is changed.
use vbox as root for scene and set vgrow constraint for table to always.
Sample Code :
Scene scene = new Scene(vbox);
vbox.setVgrow(table, Priority.ALWAYS);
If you are using Scenebuilder, then you can easily anchor the anchorpane (where your tableview is residing) to the sides. Or you can set it programatically in the controller.
Select the sides whichever direction you want the table to grow.
AnchorPane.setTopAnchor(<anchorpane>,0.0);
AnchorPane.setBottomAnchor(<anchorpane>,0.0);
AnchorPane.setLeftAnchor(<anchorpane>,0.0);
AnchorPane.setRightAnchor(<anchorpane>,0.0);
ColumnResizePolicy will only resize the tablecolumn according to your tableview size.