How to bind nested Task progress property to TableView in JavaFX? - javafx

Enironment:
OpenJDK12, JavaFX 11
Context: I'm trying to show the Task progress to a TableView, for that, when my code was less complex, my Task object included the bean properties, and the TableView datamodel was my Task object.
public class MyTask extends Task<Void>{
private String name;
//other properties
public Void call() {
//"progress" property is inherited from Task.
//do something and updateProgress()
}
}
public class MyController {
...
#FXML
private TableView<MyTask> dataTable;
#FXML
private TableColumn<MyTask,Double> progressCol;
...
progressCol.setCellValueFactory(new PropertyValueFactory<MyTask, Double>("progress"));
progressCol.setCellFactory(ProgressCell.<Double>forTableColumn());
...
}
That worked fine. But I wanted to separate the Task from the bean properties, so I decided to make a kind of wrapper, but I'm unable to retrieve the progress property anymore.
EDIT
Sample Code:
MyApp
public class MyApp extends Application {
#Override
public void start(Stage stage) throws IOException {
stage.setMinWidth(800);
stage.setMinHeight(500);
FXMLLoader sceneLoader = new FXMLLoader(MyApp.class.getResource("MyScene.fxml"));
Parent parent = sceneLoader.load();
Scene scene = new Scene(parent);
stage.setScene(scene);
stage.show();
}
public static void main(String[] args) {
launch();
}
}
MyController
public class MyController implements Initializable{
#FXML
private TableView<MyWrapper> dataTable;
#FXML
private TableColumn<MyWrapper, String> nameColumn;
#FXML
private TableColumn<MyWrapper, Double> progressColumn;
public MyController() {
}
#Override
public void initialize(URL location, ResourceBundle resources) {
nameColumn.setCellValueFactory((TableColumn.CellDataFeatures<MyWrapper, String> download) -> download.getValue()
.getMyBean().nameProperty());
//This line only works when MyWrapper has progressPropery() method
//progressColumn.setCellValueFactory(new PropertyValueFactory<>("progress"));
progressColumn.setCellFactory(ProgressCell.<Double>forTableColumn());
MyWrapper w1 = new MyWrapper("qqqqqqq");
MyWrapper w2 = new MyWrapper("wwwwww");
MyWrapper w3 = new MyWrapper("eeeeeee");
ObservableList<MyWrapper> obsList = FXCollections.observableArrayList();
obsList.addAll(w1,w2,w3);
dataTable.setItems(obsList);
Thread t1 = new Thread(w1.getMyTask());
t1.start();
}
MyWrapper
public class MyWrapper {
private SimpleObjectProperty<MyBean> myBean;
private SimpleObjectProperty<MyTask> myTask;
public MyWrapper(String name) {
myBean = new SimpleObjectProperty<MyBean>();
myBean.setValue(new MyBean());
myBean.getValue().setName(name);
myTask = new SimpleObjectProperty<MyTask>();
myTask.setValue(new MyTask());
}
public MyBean getMyBean() {
return myBean.getValue();
}
public MyTask getMyTask() {
return myTask.getValue();
}
}
MyBean
public class MyBean {
private SimpleStringProperty name;
public MyBean() {
name = new SimpleStringProperty("--");
}
public SimpleStringProperty nameProperty() {
return name;
}
public void setName(String name) {
this.name.setValue(name);
}
}
MyTask
public class MyTask extends Task<Void>{
#Override
protected Void call() throws Exception {
// Set the total number of steps in our process
double steps = 1000;
// Simulate a long running task
for (int i = 0; i < steps; i++) {
Thread.sleep(10); // Pause briefly
// Update our progress and message properties
updateProgress(i, steps);
updateMessage(String.valueOf(i));
} return null;
}
}
ProgressCell
public class ProgressCell extends TableCell<MyWrapper, Double> {
private ProgressBar bar;
private ObservableValue<Double> observable;
private StringProperty colorProperty = new SimpleStringProperty();
public ProgressCell() {
bar = new ProgressBar();
bar.setMaxWidth(Double.MAX_VALUE);
bar.setProgress(0f);
bar.styleProperty().bind(colorProperty);
}
public static <S> Callback<TableColumn<MyWrapper, Double>, TableCell<MyWrapper, Double>> forTableColumn() {
return param -> new ProgressCell();
}
#Override
protected void updateItem(Double item, boolean empty) {
super.updateItem(item, empty);
if (empty || item == null) {
setGraphic(null);
} else {
final TableColumn<MyWrapper, Double> column = getTableColumn();
observable = column == null ? null : column.getCellObservableValue(getIndex());
if (observable != null) {
bar.progressProperty().bind(observable);
} else if (item != null) {
bar.setProgress(item);
}
setGraphic(bar);
}
}
}
MyScene.fxml
<?import javafx.scene.control.TableColumn?>
<?import javafx.scene.control.TableView?>
<?import javafx.scene.effect.Blend?>
<?import javafx.scene.layout.AnchorPane?>
<?import javafx.scene.layout.BorderPane?>
<?import javafx.scene.layout.StackPane?>
<AnchorPane xmlns="http://javafx.com/javafx/11.0.1" xmlns:fx="http://javafx.com/fxml/1" fx:controller="main.java.MyController">
<StackPane BorderPane.alignment="CENTER">
<children>
<TableView id="dataTable" fx:id="dataTable" prefHeight="193.0" prefWidth="678.0" snapToPixel="false">
<columns>
<TableColumn fx:id="nameColumn" editable="false" prefWidth="88.0" text="Name" />
<TableColumn fx:id="progressColumn" editable="false" prefWidth="75.0" text="Progress" />
</columns>
<effect>
<Blend />
</effect>
<columnResizePolicy>
<TableView fx:constant="CONSTRAINED_RESIZE_POLICY" />
</columnResizePolicy>
</TableView>
</children>
</StackPane>
</AnchorPane>
I don't know how to get the progress bar working, without adding the progressProperty() method in MyWrapper. I was expecting to access the progress property like the name property. Is there some way ? How do you think it would be better?
Any help appreciated.

There is no support for nested properties (as you noticed and I confirmed in a comment that mysteriously disappeared .. ) - providing the property in a custom cellValueFactory that walks down the tree is the way to go: just do the same for the progress of the task as you do for the name of the bean.
A working code snippet:
// column setup
nameColumn.setCellValueFactory(cc -> cc.getValue().getMyBean().nameProperty());
progressColumn.setCellValueFactory(cc -> cc.getValue().getMyTask().progressProperty().asObject());
progressColumn.setCellFactory(ProgressBarTableCell.forTableColumn());
new Thread(w1.getMyTask()).start();
Note the conversion of DoubleProperty to ObjectProperty<Double> (as Slaw noted in a comment that disappeared as well ;)
Whether or not such deep diving is a good idea depends on your context: it's okay as long as the data is read-only and doesn't change over its lifetime. Otherwise, you would need to take precautions to guard against such change. Which will require additonal logic in the wrapper anyway, so exposing the properties of interest in that layer probably would be the cleaner approach.

The first error is thrown because your MyObject class doesn't have a progressProperty function.
If you add this function to your wrapper class it will work.
public ReadOnlyDoubleProperty progressProperty() {
return task.progressProperty();
}
.
progressCol.setCellValueFactory(new PropertyValueFactory<>("progress"));

Related

selectFirst() on javafx ListView dont select the element

i have small application to manage my tasks. I created DB(sqlite), and i store there a data.
After i log in i got dashboard scene. On dashboard scene i have implemented ObservableList which store taskList and their details. This list is updated always when i add/modify/delete tasks.
I have problem when i add/modify/delete task, when i do it the list is correctly updated but the item witch i add/modify is not selected after operation although i have that implemented.
The Main code:
public class Main extends Application {
private static Main instance;
public BorderPane mainBorderPane;
private Stage primaryStage;
public static Main getInstance() {
return instance;
}
#Override
public void start(Stage primaryStage) throws Exception{
instance = this;
this.primaryStage = primaryStage;
this.primaryStage.setTitle("Hello World");
showDashboardScene();
}
private void showDashboardScene() throws IOException {
FXMLLoader loader = new FXMLLoader();
loader.setLocation(getClass().getResource("/sample/dashboard.fxml"));
mainBorderPane = loader.load();
Scene scene = new Scene(mainBorderPane);
primaryStage.setScene(scene);
primaryStage.initStyle(StageStyle.UTILITY);
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
}
DashboardController:
public class DashboardController {
private ObservableList<Task> tasks = FXCollections.observableArrayList();
#FXML
private ListView<Task> taskListView;
public void initialize() throws SQLException {
tasks = TaskData.getInstance().getTaskList();
findListChange();
taskListView.setItems(tasks);
taskListView.getSelectionModel().setSelectionMode(SelectionMode.SINGLE);
taskListView.getSelectionModel().selectFirst();
}
public void refreshView() {
tasks = TaskData.getInstance().getTaskList();
findListChange();
taskListView.setItems(tasks);
taskListView.getSelectionModel().selectedItemProperty();
taskListView.getSelectionModel().selectLast();
}
private void findListChange() {
this.taskListView.getSelectionModel().selectedItemProperty().addListener(new ChangeListener<Task>() {
#Override
public void changed(ObservableValue<? extends Task> observable, Task oldValue, Task newValue) {
if (newValue != null) {
Task item = taskListView.getSelectionModel().getSelectedItem();
}
}
});
}
}
dashboard.fxml
<?import javafx.scene.layout.BorderPane?>
<?import javafx.scene.control.ListView?>
<BorderPane fx:controller="sample.DashboardController" xmlns:fx="http://javafx.com/fxml" >
<top>
<fx:include fx:id="mainMenu" source="/sample/menu.fxml"/>
</top>
<left>
<ListView fx:id="taskListView"/>
</left>
</BorderPane>
MenuController
public class MenuController {
public void showDialog() throws SQLException, IOException {
FXMLLoader loader = new FXMLLoader();
loader.setLocation(getClass().getResource("/sample/addTask.fxml"));
loader.load();
AddTaskController controller = loader.getController();
controller.showAddTaskDialog();
}
}
menu.fxml
<?import javafx.scene.control.MenuBar?>
<?import javafx.scene.control.Menu?>
<?import javafx.scene.control.MenuItem?>
<MenuBar fx:controller="sample.MenuController" xmlns="http://javafx.com/javafx"
xmlns:fx="http://javafx.com/fxml ">
<Menu text="Tasks">
<MenuItem text="Add" onAction="#showDialog"/>
</Menu>
</MenuBar>
addTask.fxml
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.GridPane?>
<DialogPane xmlns="http://javafx.com/javafx" xmlns:fx="http://javafx.com/fxml"
fx:controller="sample.AddTaskController">
<content>
<GridPane vgap="10" hgap="10" >
<Label text="Name" GridPane.rowIndex="0" GridPane.columnIndex="0"/>
<TextField fx:id="taskNameTF" GridPane.rowIndex="0" GridPane.columnIndex="1"/>
</GridPane>
</content>
</DialogPane>
AddTaskController:
public class AddTaskController {
#FXML
private TextField taskNameTF;
#FXML
public void showAddTaskDialog()throws IOException, SQLException {
Dialog<ButtonType> dialog = new Dialog<>();
dialog.initOwner(Main.getInstance().mainBorderPane.getScene().getWindow());
FXMLLoader loader = new FXMLLoader();
loader.setLocation(getClass().getResource("/sample/addTask.fxml"));
try {
dialog.getDialogPane().setContent(loader.load());
} catch (IOException e) {e.printStackTrace();
System.out.println("Nie udało sie wyświetlić panelu, prosimy spróbować później");
e.printStackTrace();
return;
}
dialog.getDialogPane().getButtonTypes().add(ButtonType.OK);
dialog.getDialogPane().getButtonTypes().add(ButtonType.CANCEL);
Optional<ButtonType> result = dialog.showAndWait();
if(result.isPresent() && result.get().equals(ButtonType.OK)) {
AddTaskController controller = loader.getController();
controller.addTaskToList();
FXMLLoader fxmlLoader = new FXMLLoader();
fxmlLoader.setLocation(getClass().getResource("/sample/dashboard.fxml"));
fxmlLoader.load();
DashboardController dashboardController = fxmlLoader.getController();
dashboardController.refreshView();
System.out.println("OK, pressed");
} else {
System.out.println("CANCEL, pressed");
}
}
private void addTaskToList() {
String taskName = taskNameTF.getText().trim();
TaskData.getInstance().addTaskToList(new Task(taskName));
}
}
Task
public class Task {
private String taskName;
public Task(String taskName) {
this.taskName = taskName;
}
#Override
public String toString() {
return taskName.toString();
}
}
TaskData
public class TaskData {
private ObservableList<Task> tasks = FXCollections.observableArrayList();
private static TaskData instance = new TaskData();
public static TaskData getInstance() {
return instance;
}
public ObservableList<Task> getTaskList() {
return tasks;
}
public void addTaskToList(Task task) {
instance.tasks.add(task);
}
}
i'm not sure but when i initialize a application, reference to ListView is different than reference of list when i add/modify item. I think this is the problem but i dont know how to fix it ;/

FXML Dynamically initialize ObservableList for ComboBox and TableView

I am trying to make a custom builder proposed in Dan Nicks's comment to this question.
The idea is to set combo's data before constructing it.
combo.fxml:
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.control.ComboBox?>
<ComboBox fx:id="combo1" items="${itemLoader.items}" prefWidth="150.0"
xmlns:fx="http://javafx.com/fxml/1">
</ComboBox>
The class that provides the data:
public class ComboLoader {
public ObservableList<Item> items;
public ComboLoader() {
items = FXCollections.observableArrayList(createItems());
}
private List<Item> createItems() {
return IntStream.rangeClosed(0, 5)
.mapToObj(i -> "Item "+i)
.map(Item::new)
.collect(Collectors.toList());
}
public ObservableList<Item> getItems(){
return items;
}
public static class Item {
private final StringProperty name = new SimpleStringProperty();
public Item(String name) {
this.name.set(name);
}
public final StringProperty nameProperty() {
return name;
}
}
}
And the test:
public class ComboTest extends Application {
#Override
public void start(Stage primaryStage) throws IOException {
primaryStage.setTitle("Populate combo from custom builder");
Group group = new Group();
GridPane grid = new GridPane();
grid.setPadding(new Insets(25, 25, 25, 25));
group.getChildren().add(grid);
FXMLLoader loader = new FXMLLoader();
ComboBox combo = loader.load(getClass().getResource("combo.fxml"));
loader.getNamespace().put("itemLoader", new ComboLoader());
grid.add(combo, 0, 0);
Scene scene = new Scene(group, 450, 175);
primaryStage.setScene(scene);
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
}
No errors produced, but combo is not populated.
What is missing ?
BTW: a similar solution for TableView works fine:
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.control.TableView?>
<?import javafx.scene.control.TableColumn?>
<?import javafx.scene.control.cell.PropertyValueFactory?>
<TableView items="${itemLoader.items}" xmlns:fx="http://javafx.com/fxml/1">
<columns>
<TableColumn text="Item">
<cellValueFactory><PropertyValueFactory property="name" /></cellValueFactory>
</TableColumn>
</columns>
</TableView>
Starting from a nit-pick, I did some experiments on how to actually implement what I tried to outline in my comments to c0der's answer.
The basic idea is to follow the same approach for the listCell as for the data, that is configure both content and appearance via namespace (my learn item of the day). The ingredients:
a generic custom listCell configurable with a function to convert an item to text
a generic "cellFactory factory" class for providing a cellFactory creating that cell
The cell/factory:
public class ListCellFactory<T> {
private Function<T, String> textProvider;
public ListCellFactory(Function<T, String> provider) {
this.textProvider = provider;
}
public Callback<ListView<T>, ListCell<T>> getCellFactory() {
return cc -> new CListCell<>(textProvider);
}
public ListCell<T> getButtonCell() {
return getCellFactory().call(null);
}
public static class CListCell<T> extends ListCell<T> {
private Function<T, String> converter;
public CListCell(Function<T, String> converter) {
this.converter = Objects.requireNonNull(converter, "converter must not be null");
}
#Override
protected void updateItem(T item, boolean empty) {
super.updateItem(item, empty);
if (empty) {
setText(null);
} else {
setText(converter.apply(item));
}
}
}
}
The fxml to create and configure the combo:
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.control.ComboBox?>
<ComboBox fx:id="combo1" items="${itemLoader.items}"
cellFactory="${cellFactoryProvider.cellFactory}"
buttonCell = "${cellFactoryProvider.buttonCell}"
prefWidth="150.0"
xmlns:fx="http://javafx.com/fxml/1">
</ComboBox>
An example to use it:
public class LocaleLoaderApp extends Application {
private ComboBox<Locale> loadCombo(Object itemLoader, Function<Locale, String> extractor) throws IOException {
FXMLLoader loader = new FXMLLoader(getClass().getResource("comboloader.fxml"));
loader.getNamespace().put("itemLoader", itemLoader);
loader.getNamespace().put("cellFactoryProvider", new ListCellFactory<Locale>(extractor));
ComboBox<Locale> combo = loader.load();
return combo;
}
#Override
public void start(Stage primaryStage) throws IOException {
primaryStage.setTitle("Populate combo from custom builder");
Group group = new Group();
GridPane grid = new GridPane();
grid.setPadding(new Insets(25, 25, 25, 25));
group.getChildren().add(grid);
LocaleProvider provider = new LocaleProvider();
grid.add(loadCombo(provider, Locale::getDisplayName), 0, 0);
grid.add(loadCombo(provider, Locale::getLanguage), 1, 0);
Scene scene = new Scene(group, 450, 175);
primaryStage.setScene(scene);
primaryStage.show();
}
public static class LocaleProvider {
ObservableList<Locale> locales = FXCollections.observableArrayList(Locale.getAvailableLocales());
public ObservableList<Locale> getItems() {
return locales;
}
}
public static void main(String[] args) {
launch(args);
}
}
Edited following comments by kleopatra:
Loading combo.fxml given in the question with Strings can be done using the following loader:
//load observable list with strings
public class ComboStringLoader {
private final ObservableList<String> items;
public ComboStringLoader() {
items = FXCollections.observableArrayList(createStrings());
}
private List<String> createStrings() {
return IntStream.rangeClosed(0, 5)
.mapToObj(i -> "String "+i)
.map(String::new)
.collect(Collectors.toList());
}
//name of this method corresponds to itemLoader.items in xml.
//if xml name was itemLoader.a this method should have been getA().
public ObservableList<String> getItems(){
return items;
}
}
Loading combo with Item instances in a similar fashion simply means that Item#toString for the text in the combo:
//load observable list with Item#toString
public class ComboObjectLoader1 {
public ObservableList<Item> items;
public ComboObjectLoader1() {
items = FXCollections.observableArrayList(createItems());
}
private List<Item> createItems() {
return IntStream.rangeClosed(0, 5)
.mapToObj(i -> "Item "+i)
.map(Item::new)
.collect(Collectors.toList());
}
public ObservableList<Item> getItems(){
return items;
}
}
Where Item is defined as:
class Item {
private final StringProperty name = new SimpleStringProperty();
public Item(String name) {
this.name.set(name);
}
public final StringProperty nameProperty() {
return name;
}
#Override
public String toString() {
return name.getValue();
}
}
A better approach is to load combo with custom ListCell<item>:
//load observable list with custom ListCell
public class ComboObjectLoader2 {
private final ObservableList<ItemListCell> items;
public ComboObjectLoader2() {
items =FXCollections.observableArrayList (createCells());
}
private List<ItemListCell> createCells() {
return IntStream.rangeClosed(0, 5)
.mapToObj(i -> "Item "+i)
.map(Item::new)
.map(ItemListCell::new)
.collect(Collectors.toList());
}
public ObservableList<ItemListCell> getItems(){
return items;
}
}
class ItemListCell extends ListCell<Item> {
private final Label text;
public ItemListCell(Item item) {
text = new Label(item.nameProperty().get());
setGraphic(new Pane(text));
}
#Override
public void updateItem(Item item, boolean empty) {
super.updateItem(item, empty);
if (empty) {
setText(null);
setGraphic(null);
} else {
text.setText(item.nameProperty().get());
}
}
}
The last but not least alternative is to set a custom ListCell<Item> as a cell factory for the combo.
This can be done by adding a controller to the fxml file:
combo2.fxml:
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.control.ComboBox?>
<ComboBox fx:id="combo1" items="${itemLoader.items}" prefWidth="150.0" xmlns:fx="http://javafx.com/fxml/1"
xmlns="http://javafx.com/javafx/10.0.1" fx:controller="test.ComboObjectLoaderAndController">
</ComboBox>
Where ComboObjectLoaderAndController is both a loader and a controller:
//loads observable list with Items and serves as controller to set cell factory
public class ComboObjectLoaderAndController {
public ObservableList<Item> items;
#FXML ComboBox<Item> combo1;
public ComboObjectLoaderAndController() {
items = FXCollections.observableArrayList(createItems());
}
#FXML
public void initialize() {
combo1.setCellFactory(l->new ItemListCell());
}
private List<Item> createItems() {
return IntStream.rangeClosed(0, 5)
.mapToObj(i -> "Item "+i)
.map(Item::new)
.collect(Collectors.toList());
}
public ObservableList<Item> getItems(){
return items;
}
class ItemListCell extends ListCell<Item>{
#Override
public void updateItem(Item item, boolean empty) {
super.updateItem(item, empty);
if (empty) {
setText(null);
setGraphic(null);
} else {
setText(item.nameProperty().get());
}
}
}
}
Edit:
following kleopatra's answer I added a generic custom ListCell
public class ObjectListCell<T> extends ListCell<T> {
Function<T,String> textSupplier;
public ObjectListCell(Function<T,String> textSupplier) {
this.textSupplier = textSupplier;
}
public Callback<ListView<T>, ListCell<T>> getFactory() {
return cc -> new ObjectListCell<>(textSupplier);
}
public ListCell<T> getButtonCell() {
return getFactory().call(null);
}
#Override
public void updateItem(T t, boolean empty) {
super.updateItem(t, empty);
if (t== null || empty) {
setText(null);
setGraphic(null);
} else {
setText(textSupplier.apply(t));
}
}
}
The factory is set in the fxml file:
combo3.fxml:
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.control.ComboBox?>
<ComboBox fx:id="combo1" items="${itemLoader.items}" cellFactory="${cellFactoryProvider.factory}"
buttonCell = "${cellFactoryProvider.buttonCell}"
prefWidth="150.0" xmlns:fx="http://javafx.com/fxml/1" xmlns="http://javafx.com/javafx/10.0.1">
</ComboBox>
A test class:
public class ComboTest extends Application {
#Override
public void start(Stage primaryStage) throws IOException {
primaryStage.setTitle("Populate combo from custom builder");
//Combo of Strings
FXMLLoader loader = new FXMLLoader(getClass().getResource("combo.fxml"));
loader.getNamespace().put("itemLoader", new ComboStringLoader());
ComboBox<String>stringCombo = loader.load();
//Combo of Item
loader = new FXMLLoader(getClass().getResource("combo.fxml"));
loader.getNamespace().put("itemLoader", new ComboObjectLoader1());
ComboBox<Item>objectsCombo1 = loader.load();
//Combo of custom ListCell
loader = new FXMLLoader(getClass().getResource("combo.fxml"));
loader.getNamespace().put("itemLoader", new ComboObjectLoader2());
ComboBox<ItemListCell>objectsCombo2 = loader.load();
//Combo of Item with custom ListCell factory
loader = new FXMLLoader(getClass().getResource("combo2.fxml"));
loader.getNamespace().put("itemLoader", new ComboObjectLoaderAndController());
ComboBox<Item>objectsCombo3 = loader.load();
//Combo of Item with custom ListCell factory. Factory is set in FXML
loader = new FXMLLoader(getClass().getResource("combo3.fxml"));
loader.getNamespace().put("itemLoader", new ComboObjectLoader1());
loader.getNamespace().put("cellFactoryProvider", new ObjectListCell<Item>(t -> t.nameProperty().get()));
ComboBox<Item>objectsCombo4 = loader.load();
HBox pane = new HBox(25, stringCombo, objectsCombo1,objectsCombo2, objectsCombo3, objectsCombo4);
pane.setPadding(new Insets(25, 25, 25, 25));
Scene scene = new Scene(pane, 550, 175);
primaryStage.setScene(scene);
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
}

JavaFX : data isn't displayed in tableview's column when trying to use cellfactory

I want to display data in tableview's column with custom rendering. I tried to adapt this tuto to my need.
Problem :
In the code example below, when I use the setCellFactory() method, the data in the corresponding column isn't displayed.
You can comment or uncomment the delimited section to see what happen in controller class.
Main class
public class CellFactory extends Application {
#Override
public void start(Stage primaryStage) throws Exception {
AnchorPane root = FXMLLoader.load(CellFactory.class.getResource("CellFactory_Layout.fxml"));
Scene scene = new Scene(root);
primaryStage.setScene(scene);
primaryStage.setTitle("CellFactory EXAMPLE");
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
}
Model class
public class Fruit {
private final SimpleStringProperty name;
private final SimpleIntegerProperty weight;
public Fruit(String name, int weight){
this.name = new SimpleStringProperty(name);
this.weight = new SimpleIntegerProperty(weight);
}
public String getName() {return this.name.get();}
public void setName(String v) {this.name.set(v);}
public SimpleStringProperty nameProperty() {return this.name;}
public int getWeight() {return this.weight.get();}
public void setWeight(int v) {this.weight.set(v);}
public SimpleIntegerProperty weightProperty() {return this.weight;}
}
Controller class
public class CellFactory_Controller implements Initializable {
#FXML private TableView<Fruit> fruit_tbl;
#FXML private TableColumn<Fruit, String> name_cln;
#FXML private TableColumn<Fruit, Integer> weight_cln;
// array with table data
final ObservableList<Fruit> data = FXCollections.observableArrayList();
public CellFactory_Controller() {
// some data
this.data.add(new Fruit("banana", 120));
this.data.add(new Fruit("apple", 150));
this.data.add(new Fruit("coconut", 500));
this.data.add(new Fruit("orange", 200));
}
#Override
public void initialize(URL location, ResourceBundle resources) {
this.name_cln.setCellValueFactory(new PropertyValueFactory<>("name"));
this.weight_cln.setCellValueFactory(new PropertyValueFactory<>("weight"));
this.weight_cln.setCellValueFactory(cellData -> cellData.getValue().weightProperty().asObject());
// comment or uncomment to see what happen
///////////////////////////////////////////////////////////////////////
this.weight_cln.setCellFactory(column -> new TableCell<Fruit, Integer>() {
#Override
protected void updateItem(Integer item, boolean empty) {
super.updateItem(item, empty);
if (item == null || empty) {
setText(null);
setStyle("");
} else {
if (item < 10) {
setTextFill(Color.CHOCOLATE);
} else {
setTextFill(Color.BLACK);
setStyle("");
}
}
}
});
///////////////////////////////////////////////////////////////////////
this.fruit_tbl.setItems(this.data);
}
}
FXML
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.control.*?>
<?import java.lang.*?>
<?import javafx.scene.layout.*?>
<AnchorPane maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" prefHeight="400.0" prefWidth="600.0" xmlns="http://javafx.com/javafx/8.0.40" xmlns:fx="http://javafx.com/fxml/1" fx:controller="CellFactory.CellFactory_Controller">
<children>
<TableView fx:id="fruit_tbl" layoutX="189.0" layoutY="93.0" prefHeight="400.0" prefWidth="600.0" AnchorPane.bottomAnchor="0.0" AnchorPane.leftAnchor="0.0" AnchorPane.rightAnchor="0.0" AnchorPane.topAnchor="0.0">
<columns>
<TableColumn fx:id="name_cln" prefWidth="471.0" text="FRUIT" />
<TableColumn fx:id="weight_cln" prefWidth="75.0" text="WEIGHT" />
</columns>
<columnResizePolicy>
<TableView fx:constant="CONSTRAINED_RESIZE_POLICY" />
</columnResizePolicy>
</TableView>
</children>
</AnchorPane>
Question :
From my code example, how can I use custom cell renderer properly (with int data type) to display my data ?
you forgot to add the following statement:
setText(String.valueOf(item));
So, your setCellFactory() method should look like the following:
this.weight_cln.setCellFactory(column -> new TableCell<Fruit, Integer>() {
#Override
protected void updateItem(Integer item, boolean empty) {
super.updateItem(item, empty);
if (item == null || empty) {
setText(null);
setStyle("");
} else {
setText(String.valueOf(item));
if (item < 200) {
setTextFill(Color.CHOCOLATE);
} else {
setTextFill(Color.BLACK);
}
}
}
});

How to handle Null Pointer Exception of AnchorPane object in javafx

I am building a javafx application using afterburner. I have main window with a AnchorPane. In this AnchorPane i have MenuBar and an another AnchorPane with fx:id=contentPane. In this contentPane i am trying to load another scene which have a different Presenter which gives me an NPE while it is fine if i am doing so with a menuAction. See the code below and more details..
This is my Main class which start the stage.
public class App extends Application
{
#Override
public void start(Stage stage) throws Exception {
MainView mainView = new MainView();
Scene scene = new Scene(mainView.getView());
stage.setTitle("Main");
stage.setScene(scene);
stage.show();
}
#Override
public void stop() throws Exception {
Injector.forgetAll();
}
public static void main(String[] args) {
launch(args);
}
}
Here is main presenter
public class MainPresenter implements Initializable {
#FXML
AnchorPane contentBox;
private ObjectProperty<FormOpener> newFormProperty ;
ReturnsInputView returnsView ;
ReturnsInputPresenter returnsPresenter;
InwardsInputView inputView;
InwardsInputPresenter inwardsPresenter;
#PostConstruct
public void init(){
this.newFormProperty = new SimpleObjectProperty();
this.newFormProperty.addListener(new ChangeListener<FormOpener>() {
#Override
public void changed(ObservableValue<? extends FormOpener> observable, FormOpener oldValue, FormOpener newValue) {
if(newValue!=null){
InwardsInputView inputView = new InwardsInputView();
inwardsPresenter = (InwardsInputPresenter) inputView.getPresenter();
contentBox.getChildren().add(inputView.getView());
}
}
});
}
#Override
public void initialize(URL location, ResourceBundle resources) {
this.returnsView = new ReturnsInputView();
this.returnsPresenter = (ReturnsInputPresenter) returnsView.getPresenter();
this.contentBox.getChildren().add(returnsView.getView());
}
public void showIncomingForm(){
this.returnsView = new ReturnsInputView();
this.returnsPresenter = (ReturnsInputPresenter) returnsView.getPresenter();
contentBox.getChildren().add(returnsView.getView());
}
public ObjectProperty<FormOpener> newFormProperty(){
return newFormProperty;
}
}
Here is main.fxml
<AnchorPane id="AnchorPane" minHeight="180.0" prefHeight="362.0" prefWidth="503.0" styleClass="airpad" xmlns="http://javafx.com/javafx/8" xmlns:fx="http://javafx.com/fxml/1" fx:controller="com.qaf.MainPresenter">
<children>
<AnchorPane fx:id="contentBox" layoutX="1.0" layoutY="25.0" minHeight="0.0" minWidth="0.0" prefHeight="336.0" prefWidth="503.0" style="-fx-background-color: aqua;" />
<MenuBar fx:id="menuBar" prefHeight="25.0" prefWidth="504.0">
<menus>
<Menu mnemonicParsing="false" text="Stock">
<items>
<MenuItem fx:id="returned" mnemonicParsing="false" onAction="#showReturnedForm" text="Returned" />
</items>
</Menu>
</menus>
</MenuBar>
</children>
</AnchorPane>
Now here is ReturnsInputPresenter
public class ReturnsInputPresenter implements Initializable {
#FXML
Button saveButton;
#FXML
TextField orderNo;
#Inject
MainPresenter main;
private ObjectProperty<FormOpener> newFormProperty ;
#Override
public void initialize(URL location, ResourceBundle resources) {
this.newFormProperty = new SimpleObjectProperty();
this.newFormProperty.addListener(new ChangeListener<FormOpener>() {
#Override
public void changed(ObservableValue<? extends FormOpener> observable, FormOpener oldValue, FormOpener newValue) {
if(newValue!=null){
main.newFormProperty().set(newValue);
}
}
});
}
public void save() {
FormOpener fOpener = new FormOpener();
fOpener.setInwards(false);
this.newFormProperty.set(fOpener);
}
public ObjectProperty<FormOpener> newFormProperty(){
return newFormProperty;
}
Here in returnsinput.fxml i have a text box and a button. On button's action i am setting the newFormProperty() to new Value which have a listener that changes the value of newFormProperty() belongs to mainPresenter and here when i try to access contentPanethen it gives me a NPE while with menu Action performing the same process have no issue.
Why it's happening and what is the solution. Please help me with this.
Thank you.
The instance of MainPresenter that is injected into ReturnsInputPresenter is not the same instance as the one created for you when MainView is instantiated. (Imagine you had two instances of MainView, which is perfectly plausible, and consequently two instances of MainPresenter. How would the framework know which instance it should inject into the ReturnsInputPresenter instance?)
The problem here is really that you are trying to use the framework in a way that it is not intended to be used. Afterburnerfx is a framework for implementing MVP and the injection mechanism is really designed as a convenience for injecting elements of the model so that model instances can be shared by multiple presenters. It is not designed to pass around references to the presenters from one presenter to another.
But actually all you are trying to do is something that fits very naturally into MVP: you are trying to share a piece of data (specifically, an ObjectProperty<FormOpener>) among different presenters. So just define a class to hold that data (and other data you might need to share):
public class FormOpenerModel /* you can probably think of a better name */ {
private final ObjectProperty<FormOpener> formOpener = new SimpleObjectProperty<>();
public ObjectProperty<FormOpener> formOpenerProperty() {
return formOpener ;
}
public final FormOpener getFormOpener() {
return formOpenerProperty().get();
}
public final void setFormOpener(FormOpener formOpener) {
formOpenerProperty().set(formOpener);
}
// other properties as needed...
}
Now your MainPresenter should look something like
public class MainPresenter implements Initializable {
#FXML
AnchorPane contentBox;
#Inject
private FormOpenerModel formOpenerModel ;
// you probably don't need any of these references any more:
ReturnsInputView returnsView ;
ReturnsInputPresenter returnsPresenter;
InwardsInputView inputView;
InwardsInputPresenter inwardsPresenter;
#Override
public void initialize(URL location, ResourceBundle resources) {
this.returnsView = new ReturnsInputView();
this.returnsPresenter = (ReturnsInputPresenter) returnsView.getPresenter();
this.contentBox.getChildren().add(returnsView.getView());
formOpenerModel.formOpenerProperty().addListener(new ChangeListener<FormOpener>() {
#Override
public void changed(ObservableValue<? extends FormOpener> observable, FormOpener oldValue, FormOpener newValue) {
if(newValue!=null){
InwardsInputView inputView = new InwardsInputView();
inwardsPresenter = (InwardsInputPresenter) inputView.getPresenter();
contentBox.getChildren().add(inputView.getView());
}
}
});
}
public void showIncomingForm(){
this.returnsView = new ReturnsInputView();
this.returnsPresenter = (ReturnsInputPresenter) returnsView.getPresenter();
contentBox.getChildren().add(returnsView.getView());
}
}
You probably don't need all those references to the other views and presenters, etc, but I left them in. You should remove them if you don't need them.
Your ReturnsInputPresenter:
public class ReturnsInputPresenter implements Initializable {
#FXML
Button saveButton;
#FXML
TextField orderNo;
#Inject
private FormOpenerModel formOpenerModel;
#Override
public void initialize(URL location, ResourceBundle resources) {
}
public void save() {
FormOpener fOpener = new FormOpener();
fOpener.setInwards(false);
formOpenerModel.formOpenerProperty().set(fOpener);
}
}
(By the way, FXML controller classes haven't needed to implement Initializable since version 2.0, so you can get rid of implements Initializable, and if the initialize(...) method is a no-op as above, you can just remove it entirely.)
The injector is smart enough to cache instances when it injects them and reuse them (i.e. it uses the equivalent of "singleton scope" from CDI or Spring), so both presenters will get a reference to the same FormOpenerModel instance.

javaFX - how to use function or object of a controller from another controller

I have seen a lot of question similar to mine, but I didn't figure it out how to solve my problem, so there is my code:
I have a simple main:
MainClassFXGui.java
public class MainClassFXGui extends Application {
#Override
public void start(Stage stage) throws Exception {
Parent root = FXMLLoader.load(getClass().getResource("MyProgramMainGuiTab.fxml"));
Scene scene = new Scene(root);
stage.setScene(scene);
stage.setTitle("Assicurazione");
stage.show();
}
/**
* #param args the command line arguments
*/
public static void main(String[] args) {
launch(args);
}
}
and three fxml files, one of them that contains the other two, by the tag
<fx:include source="MyTab1.fxml" />
MyProgramMainGuiTab.fxml
...
<TabPane maxHeight="1.7976931348623157E308" maxWidth="1.7976931348623157E308" prefHeight="5000.0" prefWidth="5000.0" tabClosingPolicy="UNAVAILABLE">
<tabs>
<Tab closable="false" text="MyTab1">
<content>
<fx:include source="MyTab1.fxml" />
</content>
</Tab>
<Tab closable="false" text="MyTab2">
<content>
<fx:include source="MyTab2.fxml" />
</content>
</Tab>
</tabs>
</TabPane>
...
and three controller:
MyProgramMainGuiTabController.java
....
#FXML
private Label LeftSt;
/**
* Initializes the controller class.
*/
#Override
public void initialize(URL url, ResourceBundle rb) {
LeftSt.setText("");
}
public void setLeftSt(String st){
LeftSt.setText(st);
}
...
MyTab1Controller.java
#FXML
private void handleButtonAction(ActionEvent event) {
System.out.println("You clicked me!");
setLeftSt("Can I change the label of the MyProgramMainGuiTabController?");
}
I tried in this way:
MyTab1Controller.java
private MyProgramMainGuiTabController controller;
#FXML
private void handleButtonAction(ActionEvent event) {
System.out.println("You clicked me!");
controller.setLeftSt("Can I change the label of the MyProgramMainGuiTabController?");
}
#Override
public void initialize(URL url, ResourceBundle rb) {
FXMLLoader fxmlLoader = new
FXMLLoader(getClass().getResource("MyProgramMainGuiTab.fxml"));
try {
fxmlLoader.load();
} catch (IOException exception) {
throw new RuntimeException(exception);
}
controller = (MyProgramMainGuiTabController)fxmlLoader.getController();
}
But I have a null pointer exception or if I move the code for the instanzialization of the object 'controller' in the handleButtonAction function, I don't have any error, but the label does not change.
Maybe I can create a static Scene and a static Stage in the MainClassFXGui.java?
but then I don't know how I could use them...
MainClassFXGui.java
public class MainClassFXGui extends Application {
static private Scene scene;
static private Stage stage;
#Override
public void start(Stage stage) throws Exception {
Parent root = FXMLLoader.load(getClass().getResource("MyProgramMainGuiTab.fxml"));
this.scene = new Scene(root);
stage.setScene(scene);
stage.setTitle("Assicurazione");
stage.show();
this.stage = stage;
}
/**
* #param args the command line arguments
*/
public static void main(String[] args) {
launch(args);
}
public static Scene getScene(){
return AssicurazioneFXGui.scene;
}
public static Stage getStage(){
return AssicurazioneFXGui.stage;
}
}
can I do something with getScene() or getStage()? any idea or suggest?
thanks to everyone
Define a StringProperty in MyTab1Controller, with the usual methods:
public class MyTab1Controller {
private final StringProperty leftSt = new SimpleStringProperty();
public StringProperty leftStProperty() {
return leftSt ;
}
public final String getLeftSt() {
return leftStProperty().get();
}
public final void setLeftSt(String leftSt) {
leftStProperty().set(leftSt);
}
// ...
#FXML
public void handleButtonAction() {
setLeftSt(...);
}
// ...
}
Now assign an fx:id attribute to the fx:include tag:
<Tab closable="false" text="MyTab1">
<content>
<fx:include fx:id="myTab1" source="MyTab1.fxml" />
</content>
</Tab>
This means you can inject the controller for MyTab1.fxml into the controller for the FXML where it is included (MyProgramMainGuiTabController):
public class MyProgramMainGuiTabController {
#FXML
private MyTab1Controller myTab1Controller ;
}
and then you can just observe the property and update the label when it changes:
public class MyProgramMainGuiTabController {
#FXML
private MyTab1Controller myTab1Controller ;
#FXML
private Label leftSt;
public void initialize() {
leftSt.setText("");
myTab1Controller.leftStProperty().addListener((obs, oldValue, newValue) ->
leftSt.setText(newValue));
}
}
Note the rule here is that the field name for the controller is the value of the fx:id attribute with the word "Controller" appended: myTab1 in the fx:id resolves to myTab1Controller for the field where the controller is injected. See NestedControllers for more information.
At the moment I have solved in this way. If anyway someone has a better solution, please write. Also because this does not solve the problem if I need to execute a function.. thanks
MyProgramMainGuiTabController.fxml
...
#FXML
private Label LeftSt;
/**
* Initializes the controller class.
*/
#Override
public void initialize(URL url, ResourceBundle rb) {
LeftSt.setText("");
}
...
MyTab1Controller.fxml
#FXML
private Button myObject;
private Scene scene;
private Label lblDataLeft;
#FXML
private void handleButtonAction(ActionEvent event) {
System.out.println("You clicked me!");
setLeftSt("this works!");
}
public void setLeftSt(String st){
if(this.scene == null)
this.scene = myObject.getScene();
if(this.lblDataLeft==null)
this.lblDataLeft = (Label) scene.lookup("#LeftSt");
if (this.lblDataLeft!=null)
this.lblDataLeft.setText(st);
}

Resources