I can't seem to find any material on the subject. To give a more concrete example, let's say I want to create a simple component that combines a checkbox and a label. Then, populate a ListView with instances of this custom component.
UPDATE:
see my answer for complete code
UPDATE 2:
For an up-to-date tutorial, please, consult the official documentation. There was a lot of new stuff that was added in 2.2. Finally, the Introduction to FXML covers pretty much everything you need to know about FXML.
UPDATE 3:
Hendrik Ebbers made an extremely helpful blog post about custom UI controls.
Update: For an up-to-date tutorial, please, consult the official documentation. There was a lot of new stuff that was added in 2.2. Also, the Introduction to FXML covers pretty much everything you need to know about FXML. Finally, Hendrik Ebbers made an extremely helpful blog post about custom UI controls.
After a few days of looking around the API and reading through some docs (Intro to FXML, Getting started with FXML Property binding, Future of FXML) I've come up with a fairly sensible solution.
The least straight-forward piece of information I learned from this little experiment was that the instance of a controller (declared with fx:controller in FXML) is held by the FXMLLoader that loaded the FXML file... Worst of all, this important fact is only mentioned in one place in all the docs I saw:
a controller is generally only visible to the FXML loader that creates it
So, remember, in order to programmatically (from Java code) obtain a reference to the instance of a controller that was declared in FXML with fx:controller use FXMLLoader.getController() (refer to the implementation of the ChoiceCell class below for a complete example).
Another thing to note is that Property.bindBiderctional() will set the value of the calling property to the value of the property passed in as the argument. Given two boolean properties target (originally set to false) and source (initially set to true) calling target.bindBidirectional(source) will set the value of target to true. Obviously, any subsequent changes to either property will change the other property's value (target.set(false) will cause the value of source to be set to false):
BooleanProperty target = new SimpleBooleanProperty();//value is false
BooleanProperty source = new SimpleBooleanProperty(true);//value is true
target.bindBidirectional(source);//target.get() will now return true
target.set(false);//both values are now false
source.set(true);//both values are now true
Anyway, here is the complete code that demonstrates how FXML and Java can work together (as well as a few other useful things)
Package structure:
com.example.javafx.choice
ChoiceCell.java
ChoiceController.java
ChoiceModel.java
ChoiceView.fxml
com.example.javafx.mvc
FxmlMvcPatternDemo.java
MainController.java
MainView.fxml
MainView.properties
FxmlMvcPatternDemo.java
package com.example.javafx.mvc;
import java.util.ResourceBundle;
import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.stage.Stage;
public class FxmlMvcPatternDemo extends Application
{
public static void main(String[] args) throws ClassNotFoundException
{
Application.launch(FxmlMvcPatternDemo.class, args);
}
#Override
public void start(Stage stage) throws Exception
{
Parent root = FXMLLoader.load
(
FxmlMvcPatternDemo.class.getResource("MainView.fxml"),
ResourceBundle.getBundle(FxmlMvcPatternDemo.class.getPackage().getName()+".MainView")/*properties file*/
);
stage.setScene(new Scene(root));
stage.show();
}
}
MainView.fxml
<?xml version="1.0" encoding="UTF-8"?>
<?import java.lang.*?>
<?import javafx.scene.*?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>
<VBox
xmlns:fx="http://javafx.com/fxml"
fx:controller="com.example.javafx.mvc.MainController"
prefWidth="300"
prefHeight="400"
fillWidth="false"
>
<children>
<Label text="%title" />
<ListView fx:id="choicesView" />
<Button text="Force Change" onAction="#handleForceChange" />
</children>
</VBox>
MainView.properties
title=JavaFX 2.0 FXML MVC demo
MainController.java
package com.example.javafx.mvc;
import com.example.javafx.choice.ChoiceCell;
import com.example.javafx.choice.ChoiceModel;
import java.net.URL;
import java.util.ResourceBundle;
import javafx.collections.FXCollections;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.control.ListCell;
import javafx.scene.control.ListView;
import javafx.util.Callback;
public class MainController implements Initializable
{
#FXML
private ListView<ChoiceModel> choicesView;
#Override
public void initialize(URL url, ResourceBundle rb)
{
choicesView.setCellFactory(new Callback<ListView<ChoiceModel>, ListCell<ChoiceModel>>()
{
public ListCell<ChoiceModel> call(ListView<ChoiceModel> p)
{
return new ChoiceCell();
}
});
choicesView.setItems(FXCollections.observableArrayList
(
new ChoiceModel("Tiger", true),
new ChoiceModel("Shark", false),
new ChoiceModel("Bear", false),
new ChoiceModel("Wolf", true)
));
}
#FXML
private void handleForceChange(ActionEvent event)
{
if(choicesView != null && choicesView.getItems().size() > 0)
{
boolean isSelected = choicesView.getItems().get(0).isSelected();
choicesView.getItems().get(0).setSelected(!isSelected);
}
}
}
ChoiceView.fxml
<?xml version="1.0" encoding="UTF-8"?>
<?import java.lang.*?>
<?import javafx.scene.*?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>
<HBox
xmlns:fx="http://javafx.com/fxml"
fx:controller="com.example.javafx.choice.ChoiceController"
>
<children>
<CheckBox fx:id="isSelectedView" />
<Label fx:id="labelView" />
</children>
</HBox>
ChoiceController.java
package com.example.javafx.choice;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.control.CheckBox;
import javafx.scene.control.Label;
public class ChoiceController
{
private final ChangeListener<String> LABEL_CHANGE_LISTENER = new ChangeListener<String>()
{
public void changed(ObservableValue<? extends String> property, String oldValue, String newValue)
{
updateLabelView(newValue);
}
};
private final ChangeListener<Boolean> IS_SELECTED_CHANGE_LISTENER = new ChangeListener<Boolean>()
{
public void changed(ObservableValue<? extends Boolean> property, Boolean oldValue, Boolean newValue)
{
updateIsSelectedView(newValue);
}
};
#FXML
private Label labelView;
#FXML
private CheckBox isSelectedView;
private ChoiceModel model;
public ChoiceModel getModel()
{
return model;
}
public void setModel(ChoiceModel model)
{
if(this.model != null)
removeModelListeners();
this.model = model;
setupModelListeners();
updateView();
}
private void removeModelListeners()
{
model.labelProperty().removeListener(LABEL_CHANGE_LISTENER);
model.isSelectedProperty().removeListener(IS_SELECTED_CHANGE_LISTENER);
isSelectedView.selectedProperty().unbindBidirectional(model.isSelectedProperty())
}
private void setupModelListeners()
{
model.labelProperty().addListener(LABEL_CHANGE_LISTENER);
model.isSelectedProperty().addListener(IS_SELECTED_CHANGE_LISTENER);
isSelectedView.selectedProperty().bindBidirectional(model.isSelectedProperty());
}
private void updateView()
{
updateLabelView();
updateIsSelectedView();
}
private void updateLabelView(){ updateLabelView(model.getLabel()); }
private void updateLabelView(String newValue)
{
labelView.setText(newValue);
}
private void updateIsSelectedView(){ updateIsSelectedView(model.isSelected()); }
private void updateIsSelectedView(boolean newValue)
{
isSelectedView.setSelected(newValue);
}
}
ChoiceModel.java
package com.example.javafx.choice;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
public class ChoiceModel
{
private final StringProperty label;
private final BooleanProperty isSelected;
public ChoiceModel()
{
this(null, false);
}
public ChoiceModel(String label)
{
this(label, false);
}
public ChoiceModel(String label, boolean isSelected)
{
this.label = new SimpleStringProperty(label);
this.isSelected = new SimpleBooleanProperty(isSelected);
}
public String getLabel(){ return label.get(); }
public void setLabel(String label){ this.label.set(label); }
public StringProperty labelProperty(){ return label; }
public boolean isSelected(){ return isSelected.get(); }
public void setSelected(boolean isSelected){ this.isSelected.set(isSelected); }
public BooleanProperty isSelectedProperty(){ return isSelected; }
}
ChoiceCell.java
package com.example.javafx.choice;
import java.io.IOException;
import java.net.URL;
import javafx.fxml.FXMLLoader;
import javafx.fxml.JavaFXBuilderFactory;
import javafx.scene.Node;
import javafx.scene.control.ListCell;
public class ChoiceCell extends ListCell<ChoiceModel>
{
#Override
protected void updateItem(ChoiceModel model, boolean bln)
{
super.updateItem(model, bln);
if(model != null)
{
URL location = ChoiceController.class.getResource("ChoiceView.fxml");
FXMLLoader fxmlLoader = new FXMLLoader();
fxmlLoader.setLocation(location);
fxmlLoader.setBuilderFactory(new JavaFXBuilderFactory());
try
{
Node root = (Node)fxmlLoader.load(location.openStream());
ChoiceController controller = (ChoiceController)fxmlLoader.getController();
controller.setModel(model);
setGraphic(root);
}
catch(IOException ioe)
{
throw new IllegalStateException(ioe);
}
}
}
}
For JavaFx 2.1, You can create a custom FXML control component by this way:
<?xml version="1.0" encoding="UTF-8"?>
<?import java.lang.*?>
<?import java.util.*?>
<?import javafx.scene.*?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>
<?import customcontrolexample.myCommponent.*?>
<VBox xmlns:fx="http://javafx.com/fxml" fx:controller="customcontrolexample.FXML1Controller">
<children>
<MyComponent welcome="1234"/>
</children>
</VBox>
Component code:
MyComponent.java
package customcontrolexample.myCommponent;
import java.io.IOException;
import javafx.beans.property.StringProperty;
import javafx.fxml.FXMLLoader;
import javafx.scene.Node;
import javafx.scene.layout.Pane;
import javafx.util.Callback;
public class MyComponent extends Pane {
private Node view;
private MyComponentController controller;
public MyComponent() {
FXMLLoader fxmlLoader = new FXMLLoader(getClass().getResource("myComponent.fxml"));
fxmlLoader.setControllerFactory(new Callback<Class<?>, Object>() {
#Override
public Object call(Class<?> param) {
return controller = new MyComponentController();
}
});
try {
view = (Node) fxmlLoader.load();
} catch (IOException ex) {
}
getChildren().add(view);
}
public void setWelcome(String str) {
controller.textField.setText(str);
}
public String getWelcome() {
return controller.textField.getText();
}
public StringProperty welcomeProperty() {
return controller.textField.textProperty();
}
}
MyComponentController.java
package customcontrolexample.myCommponent;
import java.net.URL;
import java.util.ResourceBundle;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.control.TextField;
public class MyComponentController implements Initializable {
int i = 0;
#FXML
TextField textField;
#FXML
protected void doSomething() {
textField.setText("The button was clicked #" + ++i);
}
#Override
public void initialize(URL location, ResourceBundle resources) {
textField.setText("Just click the button!");
}
}
myComponent.fxml
<?xml version="1.0" encoding="UTF-8"?>
<?import java.lang.*?>
<?import java.util.*?>
<?import javafx.scene.*?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>
<VBox xmlns:fx="http://javafx.com/fxml" fx:controller="customcontrolexample.myCommponent.MyComponentController">
<children>
<TextField fx:id="textField" prefWidth="200.0" />
<Button mnemonicParsing="false" onAction="#doSomething" text="B" />
</children>
</VBox>
This code needs to check if there is no memory leak.
Quick answer is <fx:include> tag, however, you would need to set the ChoiceModel in the Controller class.
<VBox
xmlns:fx="http://javafx.com/fxml"
fx:controller="fxmltestinclude.ChoiceDemo"
>
<children>
**<fx:include source="Choice.fxml" />**
<ListView fx:id="choices" />
</children>
</VBox>
The introduction to fxml chapter on custom components
gave me the right hint. My intention was to combine a label a slider and a textfield to one custom LabeledValueSlider component.
Usage example:
see resources/fx of rc-dukes Self-Driving RC Car Java FX App
<LabeledValueSlider fx:id='cannyThreshold1' text="Canny threshold 1" blockIncrement="1" max="2000" min="0" value="20" format="\%.0f"/>
<LabeledValueSlider fx:id="cannyThreshold2" text="Canny threshold 2" blockIncrement="1" max="2000" min="0" value="50" format="\%.0f"/>
<LabeledValueSlider fx:id="lineDetectRho" text="LineDetect rho" blockIncrement="0.01" max="20" min="0" value="0.5" />
<LabeledValueSlider fx:id="lineDetectTheta" text="LineDetect theta" blockIncrement="0.01" max="5" min="-5" value="0.5" />
<LabeledValueSlider fx:id="lineDetectThreshold" text="LineDetect threshold" blockIncrement="1" max="200" min="0" value="20" format="\%.0f" />
<LabeledValueSlider fx:id="lineDetectMinLineLength" text="LineDetect minLineLength" blockIncrement="1" max="200" min="0" value="50" format="\%.0f"/>
<LabeledValueSlider fx:id="lineDetectMaxLineGap" text="LineDetect maxLineGap" blockIncrement="1" max="500" min="0" value="50" format="\%.0f"/>
FXML file
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.geometry.Insets?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.control.Slider?>
<?import javafx.scene.control.TextField?>
<?import javafx.scene.layout.HBox?>
<fx:root type="javafx.scene.layout.HBox" xmlns:fx="http://javafx.com/fxml">
<padding>
<Insets left="10" right="10" />
</padding>
<Label fx:id='label' text="Label for Slider" minWidth="180"/>
<Slider fx:id='slider' blockIncrement="1" max="100" min="0" value="50" />
<TextField fx:id="textField" maxWidth="75"/>
</fx:root>
Component Source code
see LabeledValueSlider.java
package org.rcdukes.app;
import java.io.IOException;
import java.net.URL;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javafx.fxml.FXML;
import javafx.fxml.FXMLLoader;
import javafx.geometry.Pos;
import javafx.scene.control.Label;
import javafx.scene.control.Slider;
import javafx.scene.control.TextField;
import javafx.scene.layout.HBox;
/**
* a Slider with a Label and a value
*
* #author wf
*
*/
public class LabeledValueSlider extends HBox {
public static boolean debug=true;
protected static final Logger LOG = LoggerFactory
.getLogger(LabeledValueSlider.class);
#FXML
private Label label;
#FXML
private Slider slider;
#FXML
private TextField textField;
String format;
public String getFormat() {
return format;
}
public void setFormat(String format) {
textField.textProperty().bind(slider.valueProperty().asString(format));
this.format = format;
}
public double getBlockIncrement() {
return slider.getBlockIncrement();
}
public void setBlockIncrement(double value) {
slider.setBlockIncrement(value);
}
public double getMax() {
return slider.getMax();
}
public void setMax(double value) {
slider.setMax(value);
}
public double getMin() {
return slider.getMin();
}
public void setMin(double value) {
slider.setMin(value);
}
public double getValue() {
return slider.getValue();
}
public void setValue(double value) {
slider.setValue(value);
}
public String getText() {
return label.getText();
}
public void setText(String pLabelText) {
label.setText(pLabelText);
}
public URL getResource(String path) {
return getClass().getClassLoader().getResource(path);
}
/**
* construct me
* see https://docs.oracle.com/javase/9/docs/api/javafx/fxml/doc-files/introduction_to_fxml.html#custom_components
*/
public LabeledValueSlider() {
FXMLLoader fxmlLoader = new FXMLLoader(
getResource("fx/labeledvalueslider.fxml"));
try {
// let's load the HBox - fxmlLoader doesn't know anything about us yet
fxmlLoader.setController(this);
fxmlLoader.setRoot(this);
Object loaded = fxmlLoader.load();
Object root=fxmlLoader.getRoot();
if (debug) {
String msg=String.format("%s loaded for root %s", loaded.getClass().getName(),root.getClass().getName());
LOG.info(msg);
}
textField.setAlignment(Pos.CENTER_RIGHT);
if (format == null)
setFormat("%.2f");
} catch (IOException exception) {
throw new RuntimeException(exception);
}
}
}
Related
I am fairly new to JavaFX and am not very experienced in threading either. The project I am working on uses a WebView for all UI functions but freezes when processing data/running methods in Java. I have read about Platform.runlater() but I am unsure how to apply it to my particular situation.
Here is the code that I currently have (it is an example but replicates the issue that I am having).
Main.java
package application;
import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.stage.Stage;
public class Main extends Application {
#Override
public void start(Stage stage) throws Exception {
Parent root = FXMLLoader.load(Main.class.getResource("Main.fxml"));
Scene scene = new Scene(root);
stage.setScene(scene);
stage.show();
}
public static void main(String[] args) {
launch(args);
}
}
Controller.java
import java.net.URL;
import java.util.ResourceBundle;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.concurrent.Worker.State;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.web.WebEngine;
import javafx.scene.web.WebView;
import netscape.javascript.JSObject;
public class Controller implements Initializable {
#FXML
private WebView webView;
private WebEngine webEngine;
private AppInterface appInterface;
private void loadPage(String fileName) {
URL url = this.getClass().getResource(fileName);
webEngine = webView.getEngine();
webEngine.load(url.toExternalForm());
}
#Override
public void initialize(URL location, ResourceBundle resources) {
loadPage("/resources/index.html");
appInterface = new AppInterface(webEngine);
webEngine.getLoadWorker().stateProperty().addListener(new ChangeListener<State>() {
#Override
public void changed(ObservableValue<? extends State> observableValue, State oldState, State newState) {
if (newState == State.SUCCEEDED) {
JSObject window = (JSObject) webEngine.executeScript("window");
window.setMember("javaApp", appInterface);
}
}
});
}
}
AppInterface.java
import javafx.scene.web.WebEngine;
public class AppInterface {
WebEngine webEngine;
public AppInterface(WebEngine inputWebEngine) {
webEngine = inputWebEngine;
}
public void getFibN(int digit) {
System.out.println("Calculating digit " + digit + " of Fibonacci Sequence.");
int value = Fibonacci.getDigit(digit);
System.out.println("Fibonacci Sequence, digit " + digit + ": " + value);
webEngine.executeScript("displayWebView('" + value + "')");
}
}
Fibonacci.java
class Fibonacci {
static int getDigit(int n)
{
if (n <= 1)
return n;
return getDigit(n - 1) + getDigit(n - 2);
}
}
index.html
<!DOCTYPE html>
<html>
<head>
<script>
function getMessage(){
javaApp.getFibN(47);
}
function displayWebView(message){
document.body.append(message);
}
</script>
</head>
<body onload="javascript:setTimeout(getMessage, 1000)">
</body>
</html>
Main.fxml
<?xml version="1.0" encoding="UTF-8"?>
<?import java.lang.*?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>
<?import javafx.scene.web.*?>
<?import javafx.scene.web.WebView?>
<?import javafx.scene.layout.AnchorPane?>
<AnchorPane prefHeight="600.0" prefWidth="1000.0" xmlns="http://javafx.com/javafx/8" xmlns:fx="http://javafx.com/fxml/1" fx:controller="application.Controller">
<children>
<!-- Given the webView ID to initiate the web page -->
<WebView fx:id="webView" prefWidth="1000.0" AnchorPane.bottomAnchor="0.0" AnchorPane.leftAnchor="0.0" AnchorPane.rightAnchor="0.0" AnchorPane.topAnchor="0.0" />
</children>
</AnchorPane>
I have tried creating a new thread in the Fibonacci class but get an error stating that it is not the JavaFX thread. What am I doing wrong to keep the UI from freezing while the task is running?
I have a background thread which updates a list with incoming buy sell orders, prices etc. I get the data from my broker asynchronously. It’s important that the updates in the list happens in the background thread sequentially.
I want to display the mainPortfolioList in my javafx table without the risk of a “Not on FX application thread IllegalStateException”. The nearest solution I found was JavaFX refresh TableView thread. But this will not work if the list is in another thread, as I understand it.
I’m quite new in java and tried to solve my problem with an addlistener. I made a simplified example to show what I want and what I’ve done so far.
How can I display updates from my mainPortfolioList in the JavaFX table?
PortfolioController
import application.Test;
import javafx.application.Platform;
import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.scene.control.Button;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.control.cell.TextFieldTableCell;
import javafx.util.converter.NumberStringConverter;
public class PortfolioController {
#FXML private Button btnTest;
#FXML private TableView<Portfolio> tblPortfolio;
#FXML private TableColumn<Portfolio, String> colSymbol;
#FXML private TableColumn<Portfolio, Number> colQuantity;
#FXML private TableColumn<Portfolio, Number> colPrice;
private ObservableList<Portfolio> fxPortfolioList;
#FXML
private void initialize() {
tblPortfolio.setEditable(true);
colSymbol.setCellValueFactory(data -> data.getValue().colSymbolProperty());
colQuantity.setCellValueFactory(data -> data.getValue().colQuantityProperty());
colPrice.setCellValueFactory(data -> data.getValue().colPriceProperty());
colSymbol.setCellFactory(TextFieldTableCell.forTableColumn());
colQuantity.setCellFactory(TextFieldTableCell.<Portfolio, Number>forTableColumn(
new NumberStringConverter("#,##0.00")));
colQuantity.setOnEditCommit(event -> {
int newValue = event.getNewValue().intValue();
event.getRowValue().setColQuantity(newValue);});
colPrice.setCellFactory(TextFieldTableCell.<Portfolio, Number>forTableColumn(
new NumberStringConverter("#,##0.00")));
fxPortfolioList = FXCollections.observableArrayList();
tblPortfolio.setItems(fxPortfolioList);
Test.mainPortfolioList.addListener((ListChangeListener.Change<? extends Portfolio> c) -> {
while (c.next()) {
if (c.wasAdded()) {
Platform.runLater(() -> {
for (Portfolio asset : c.getAddedSubList()) {
fxPortfolioList.add(asset);
}
});
} else if (c.wasRemoved()) {
Platform.runLater(() -> {
for (Portfolio asset : c.getRemoved()) {
fxPortfolioList.remove(asset);
}
});
} else if (c.wasUpdated()) {
Platform.runLater(() -> {
for (int i = c.getFrom(); i < c.getTo(); ++i) {
fxPortfolioList.set(i, c.getList().get(i));
}
});
}
}
});
}
#FXML
void btnTestClicked(ActionEvent event) {
Test test = new Test();
test.dataStream(this);
}
}
Portfolio
import javafx.beans.Observable;
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.SimpleDoubleProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.util.Callback;
public class Portfolio {
private final StringProperty colSymbol;
private final IntegerProperty colQuantity;
private final DoubleProperty colPrice;
public Portfolio(String symbol, int quantity, double price) {
this.colSymbol = new SimpleStringProperty(symbol);
this.colQuantity = new SimpleIntegerProperty(quantity);
this.colPrice = new SimpleDoubleProperty(price);
}
// extractor
public static Callback<Portfolio, Observable[]> extractor() {
return (Portfolio p) -> new Observable[] {
p.colSymbolProperty(),
p.colQuantityProperty(),
p.colPriceProperty(),
};
}
// property
public StringProperty colSymbolProperty() {
return colSymbol;
}
public IntegerProperty colQuantityProperty() {
return colQuantity;
}
public DoubleProperty colPriceProperty() {
return colPrice;
}
// getter
public String getColSymbol() {
return colSymbol.get();
}
public int getColQuantity() {
return colQuantity.get();
}
public double getColPrice() {
return colPrice.get();
}
// setter
public void setColSymbol(String newValue) {
colSymbol.set(newValue);
}
public void setColQuantity(int newValue) {
colQuantity.set(newValue);
}
public void setColPrice(double newValue) {
colPrice.set(newValue);
}
}
Test simulation
import controller.portfolio.Portfolio;
import controller.portfolio.PortfolioController;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
public class Test {
public static ObservableList<Portfolio> mainPortfolioList =
FXCollections.observableArrayList(Portfolio.extractor());
public void dataStream(PortfolioController portfolioController) {
// Need to be sequentially
// The task only simulates simplified operations
Runnable task = () -> {
// add stock
mainPortfolioList.add(new Portfolio("AAPL", 13, 153.03));
mainPortfolioList.add(new Portfolio("MSFT", 31, 67.51));
// Change the quantity
for (Portfolio asset : mainPortfolioList) {
if (asset.getColSymbol().equals("AAPL")) {
asset.setColQuantity(55);
}
}
// run price updates
for (int k = 0; k < 15; k++) {
for (int m = 0; m < mainPortfolioList.size(); m++) {
double random = Math.random() * 50 + 1;
String symbol = mainPortfolioList.get(m).getColSymbol();
setTickPrice(symbol, 4, random);
randomSleep();
}
}
// remove stock
for (Portfolio asset : mainPortfolioList) {
if (asset.getColSymbol().equals("AAPL")) {
mainPortfolioList.remove(asset);
}
}
};
Thread t = new Thread(task, "Simulation");
t.setDaemon(true);
t.start();
}
public void setTickPrice(String symbol, int tickType, double price) {
for (Portfolio asset : mainPortfolioList) {
if (asset.getColSymbol().equals(symbol)) {
switch(tickType){
case 4: // Last Price
asset.setColPrice(price);
break;
}
}
}
}
private void randomSleep() {
try {
Thread.sleep((int)(Math.random() * 300));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
Main
import java.io.IOException;
import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.stage.Stage;
public class Main extends Application {
#Override
public void start(Stage primaryStage) throws IOException {
Parent root = FXMLLoader.load(getClass().getResource("/view/Portfolio.fxml"));
Scene scene = new Scene(root, 500, 300);
primaryStage.setScene(scene);
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
}
view
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.geometry.Insets?>
<?import javafx.scene.control.Button?>
<?import javafx.scene.control.TableColumn?>
<?import javafx.scene.control.TableView?>
<?import javafx.scene.layout.BorderPane?>
<?import javafx.scene.layout.HBox?>
<BorderPane prefHeight="600.0" prefWidth="800.0" xmlns="http://javafx.com/javafx/8.0.111" xmlns:fx="http://javafx.com/fxml/1" fx:controller="controller.portfolio.PortfolioController">
<top>
</top>
<center>
<TableView fx:id="tblPortfolio" prefHeight="200.0" prefWidth="200.0" tableMenuButtonVisible="true" BorderPane.alignment="CENTER">
<columns>
<TableColumn fx:id="colSymbol" maxWidth="80.0" minWidth="60.0" prefWidth="-1.0" text="Symbol" />
<TableColumn fx:id="colQuantity" maxWidth="60.0" minWidth="40.0" prefWidth="-1.0" text="Quantity" />
<TableColumn fx:id="colPrice" maxWidth="69.0" minWidth="49.0" prefWidth="-1.0" text="Price" />
</columns>
<columnResizePolicy>
<TableView fx:constant="CONSTRAINED_RESIZE_POLICY" />
</columnResizePolicy>
</TableView>
</center>
<bottom>
</bottom>
<top>
<HBox BorderPane.alignment="CENTER">
<children>
<Button fx:id="btnTest" mnemonicParsing="false" onAction="#btnTestClicked" text="Test">
<HBox.margin>
<Insets bottom="10.0" left="10.0" right="10.0" top="10.0" />
</HBox.margin>
</Button>
</children>
</HBox>
</top>
</BorderPane>
Everything coming from the reader thread(assuming IB) that will change something on the screen has to be wrapped in Platform.runLater.
eg.
Platform.runLater(() -> asset.setColPrice(price));
The same for all other calls, like add remove etc.
What would be easier is if in the wrapper for updatePortfolio you just wrap a new call to your data model updater with Platform.runLater.
//in wrapper implementation, this call happens on EReader thread.
void updatePortfolio(Contract contract, int position, double marketPrice, double marketValue,
double averageCost, double unrealizedPNL, double realizedPNL, String accountName){
//this moves it to FXApplication thread.
Platform.runLater(() ->
updateMyPortfolio(contract, position, marketPrice));//etc.. more flds
}
That way you can use all the new data in the scene with no worries.
I am building a multiscreen JavaFX app with data being pulled from a SQL database to ObservableLists and displayed in the interface via Tableview. Because of the multiscreen nature of the app, I am trying to initialize the data from the ObservableList to the Tableview via the controller. The SQL pull to ObservableList is done via a Task on a new thread. When I include the sqlCSEditTbl.itemsProperty().setValue((ObservableList) csSQLList) in the Task method nothing is displayed in the TableView when I run the program. If I place the code outside the Task method,the particular screen will not display. I don't know what I am missing here to be able to get the data to display on the particular screen. I've debugged the SQL to ObservableList and the data is being properly stored in the ObservableList. The problem is getting it from the ObservableList to the Tableview interface. Any help would be greatly appreciated. Thank you!
App Edit Screen Screenshot
SQLCalcScript Array Model
package model.calcs;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
/**
* Created by jdsmith on 2/17/2016.
*/
public class SQLCalcScripts {
private final IntegerProperty calcScriptID;
private final IntegerProperty calcScriptIndex;
private final StringProperty calcScriptName;
private final StringProperty calcScriptServer;
private final StringProperty calcScriptApp;
private final StringProperty calcScriptGroup;
public SQLCalcScripts() {
this(null, null, null, null, null, null);
}
public SQLCalcScripts(Integer calcScriptID, Integer calcScriptIndex, String calcScriptName, String calcScriptServer, String calcScriptApp, String calcScriptGroup) {
this.calcScriptID = new SimpleIntegerProperty(calcScriptID);
this.calcScriptIndex = new SimpleIntegerProperty(calcScriptIndex);
this.calcScriptName = new SimpleStringProperty(calcScriptName);
this.calcScriptServer = new SimpleStringProperty(calcScriptServer);
this.calcScriptApp = new SimpleStringProperty(calcScriptApp);
this.calcScriptGroup = new SimpleStringProperty(calcScriptGroup);
this.calcScriptID.addListener((e) -> System.out.println("ID changed to " + this.calcScriptID.get()));
this.calcScriptIndex.addListener((e) -> System.out.println("ID changed to " + this.calcScriptIndex.get()));
this.calcScriptName.addListener((e) -> System.out.println("ID changed to " + this.calcScriptName.get()));
this.calcScriptServer.addListener((e) -> System.out.println("ID changed to " + this.calcScriptServer.get()));
this.calcScriptApp.addListener((e) -> System.out.println("ID changed to " + this.calcScriptApp.get()));
this.calcScriptGroup.addListener((e) -> System.out.println("ID changed to " + this.calcScriptGroup.get()));
}
// Getters and Setters for Calc Script
public Integer getCalcScriptID() {
return calcScriptID.get();
}
public void setCalcScriptID(Integer calcScriptID) {
this.calcScriptID.set(calcScriptID);
}
public IntegerProperty calcScriptIDProperty() {
return calcScriptID;
}
public Integer getCalcScriptIndex() {
return calcScriptIndex.get();
}
public void setCalcScriptIndex(Integer calcScriptIndex) {
this.calcScriptIndex.set(calcScriptIndex);
}
public IntegerProperty calcScriptIndexProperty() {
return calcScriptIndex;
}
public String getCalcScriptName() {
return calcScriptName.get();
}
public void setCalcScriptName(String calcScriptName) {
this.calcScriptName.set(calcScriptName);
}
public StringProperty calcScriptNameProperty() {
return calcScriptName;
}
public String getCalcScriptServer() {
return calcScriptServer.get();
}
public void setCalcScriptServer(String calcScriptServer) {
this.calcScriptServer.set(calcScriptServer);
}
public StringProperty calcScriptServerProperty() {
return calcScriptServer;
}
public String getCalcScriptApp() {
return calcScriptApp.get();
}
public void setCalcScriptApp(String calcScriptApp) {
this.calcScriptApp.set(calcScriptApp);
}
public StringProperty calcScriptAppProperty() {
return calcScriptApp;
}
public String getCalcScriptGroup() {
return calcScriptGroup.get();
}
public void setCalcScriptGroup(String calcScriptGroup) {
this.calcScriptGroup.set(calcScriptGroup);
}
public StringProperty calcScriptGroupProperty() {
return calcScriptGroup;
}
}
csEditInterface.fxml
<?xml version="1.0" encoding="UTF-8"?>
<?import java.lang.*?>
<?import javafx.geometry.*?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>
<?import javafx.scene.text.*?>
<AnchorPane maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" prefHeight="1080.0" prefWidth="1920.0" xmlns="http://javafx.com/javafx/8" xmlns:fx="http://javafx.com/fxml/1" fx:controller="essapp.csEditController">
<children>
<VBox alignment="TOP_CENTER" prefHeight="200.0" prefWidth="100.0" AnchorPane.bottomAnchor="0.0" AnchorPane.leftAnchor="0.0" AnchorPane.rightAnchor="0.0" AnchorPane.topAnchor="0.0">
<children>
<Label alignment="CENTER" contentDisplay="CENTER" text="Calculation Script Editor">
<font>
<Font name="System Bold" size="36.0" />
</font>
</Label>
<ScrollPane fitToHeight="true" fitToWidth="true" pannable="true" prefHeight="800.0" prefWidth="717.0">
<content>
<AnchorPane prefHeight="200.0" prefWidth="717.0">
<children>
<TableView editable="true" prefHeight="200.0" prefWidth="200.0" AnchorPane.bottomAnchor="0.0" AnchorPane.leftAnchor="0.0" AnchorPane.rightAnchor="0.0" AnchorPane.topAnchor="0.0">
<columns>
<TableColumn fx:id="csEditID" editable="false" prefWidth="100.0" sortable="false" text="ID" />
<TableColumn fx:id="csEditIndex" prefWidth="100.0" text="Index" />
<TableColumn fx:id="csEditName" prefWidth="250.0" sortable="false" text="Name" />
<TableColumn fx:id="csEditServer" editable="false" prefWidth="300.0" sortable="false" text="Server" />
<TableColumn fx:id="csEditApp" editable="false" prefWidth="250.0" sortable="false" text="Application" />
<TableColumn fx:id="csEditGroup" prefWidth="400.0" sortable="false" text="Calc Group" />
</columns>
</TableView>
</children>
</AnchorPane>
</content>
<VBox.margin>
<Insets left="50.0" right="50.0" top="50.0" />
</VBox.margin>
</ScrollPane>
<Button fx:id="csEditOkBtn" defaultButton="true" mnemonicParsing="false" onAction="#createTbl" prefHeight="25.0" prefWidth="151.0" text="Commit Changes">
<VBox.margin>
<Insets top="50.0" />
</VBox.margin>
</Button>
<Button fx:id="csEditExitBtn" cancelButton="true" mnemonicParsing="false" onAction="#goToCSInt" prefHeight="25.0" prefWidth="150.0" text="Cancel">
<VBox.margin>
<Insets top="25.0" />
</VBox.margin>
</Button>
</children>
</VBox>
</children>
</AnchorPane>
csEditController Code:
package essapp;
import com.sun.javafx.tk.Toolkit;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.concurrent.Task;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.control.*;
import javafx.scene.control.cell.ComboBoxTableCell;
import javafx.scene.control.cell.PropertyValueFactory;
import javafx.util.StringConverter;
import javafx.util.converter.DefaultStringConverter;
import model.calcs.*;
import java.net.URL;
import java.util.Map;
import java.util.ResourceBundle;
import static javafx.scene.input.KeyCode.T;
#SuppressWarnings("Duplicates")
public class csEditController implements Initializable, ControlledScreen {
ScreensController myController;
ObservableList<SQLCalcScripts> csEditCSList = FXCollections.observableArrayList();
ObservableList<CalcScripts> csEditEssSQL = FXCollections.observableArrayList();
/**
* Initializes the controller class.
*/
#Override
public void initialize(URL url, ResourceBundle rb) {
//Add SQL data to TableView
Task task = new Task<Void>() {
#Override
public Void call() throws Exception {
SQL2CalcScripts csSQLList = new SQL2CalcScripts();
csSQLList.sqlCalc("http://TEST-HYPRPT01:13080/aps/JAPI","GNLESB",csEditCSList);
sqlCSEditTbl.itemsProperty().setValue((ObservableList<SQLCalcScripts>) csSQLList);
return null;
}
};
// sqlCSEditTbl.itemsProperty().bind(task.valueProperty());
// sqlCSEditTbl.setItems(csEditCSList);
new Thread(task).start();
// Initialize table with columns
csEditID.setCellValueFactory(cellData -> cellData.getValue().calcScriptIDProperty().asObject());
csEditIndex.setCellValueFactory(cellData -> cellData.getValue().calcScriptIndexProperty().asObject());
csEditName.setCellValueFactory(cellData -> cellData.getValue().calcScriptNameProperty());
csEditServer.setCellValueFactory(cellData -> cellData.getValue().calcScriptServerProperty());
csEditApp.setCellValueFactory(cellData -> cellData.getValue().calcScriptAppProperty());
csEditGroup.setCellValueFactory(cellData -> cellData.getValue().calcScriptGroupProperty());
// TableView Calc Group ComboBox
ObservableList<String> csGroupList = FXCollections.observableArrayList("Supplement", "Wrapper", "Board Book");
csEditGroup.setCellFactory(ComboBoxTableCell.forTableColumn(new DefaultStringConverter(), csGroupList));
csEditGroup.setOnEditCommit(
(TableColumn.CellEditEvent<SQLCalcScripts,String> cb) -> {
((SQLCalcScripts) cb.getTableView().getItems().get(
cb.getTablePosition().getRow()
)).setCalcScriptGroup(cb.getNewValue());
}
);
}
public void setScreenParent(ScreensController screenParent){
myController = screenParent;
}
#FXML // ResourceBundle that was given to the FXMLLoader
private ResourceBundle resources;
#FXML // URL location of the FXML file that was given to the FXMLLoader
private URL location;
#FXML // fx:id="sqlCSEditTbl"
private TableView<SQLCalcScripts> sqlCSEditTbl; // Value injected by FXMLLoader
#FXML // fx:id="csEditGroup"
private TableColumn<SQLCalcScripts, String> csEditGroup; // Value injected by FXMLLoader
#FXML // fx:id="csEditExitBtn"
private Button csEditExitBtn; // Value injected by FXMLLoader
#FXML // fx:id="csEditServer"
private TableColumn<SQLCalcScripts, String> csEditServer; // Value injected by FXMLLoader
#FXML // fx:id="csEditID"
private TableColumn<SQLCalcScripts, Integer> csEditID; // Value injected by FXMLLoader
#FXML // fx:id="csEditIndex"
private TableColumn<SQLCalcScripts, Integer> csEditIndex; // Value injected by FXMLLoader
#FXML // fx:id="csEditOkBtn"
private Button csEditOkBtn; // Value injected by FXMLLoader
#FXML // fx:id="csEditApp"
private TableColumn<SQLCalcScripts, String> csEditApp; // Value injected by FXMLLoader
#FXML // fx:id="csEditName"
private TableColumn<SQLCalcScripts, String> csEditName; // Value injected by FXMLLoader
#FXML
private void goToCSInt(ActionEvent event){
myController.setScreen(ScreensFramework.calcScriptInterfaceID);
}
// #FXML
// private void createTbl(ActionEvent event) {
// sqlCSEditTbl.setItems(csEditCSList);
// }
}
ControlledScreen class Code:
package sample;
/**
* Created by jdsmith on 4/21/2016.
*/
public interface ControlledScreen {
//This method will allow the injection of the Parent ScreenPane
public void setScreenParent(ScreensController screenPage);
}
ScreensController class Code:
package essapp;
import java.util.HashMap;
import javafx.animation.KeyFrame;
import javafx.animation.KeyValue;
import javafx.animation.Timeline;
import javafx.beans.property.DoubleProperty;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.fxml.FXMLLoader;
import javafx.scene.Node;
import javafx.scene.Parent;
import javafx.scene.layout.StackPane;
import javafx.util.Duration;
/**
* Created by jdsmith on 1/7/2016.
*/
public class ScreensController extends StackPane {
//Holds the screens to be displayed
private HashMap<String, Node> screens = new HashMap<>();
public ScreensController() {
super();
}
//Add the screen to the collection
public void addScreen(String name, Node screen) {
screens.put(name, screen);
}
//Returns the Node with the appropriate name
public Node getScreen(String name) {
return screens.get(name);
}
//Loads the fxml file, add the screen to the screens collection and
//finally injects the screenPane to the controller.
public boolean loadScreen(String name, String resource) {
try {
FXMLLoader myLoader = new FXMLLoader(getClass().getResource(resource));
Parent loadScreen = (Parent) myLoader.load();
ControlledScreen myScreenControler = ((ControlledScreen) myLoader.getController());
myScreenControler.setScreenParent(this);
addScreen(name, loadScreen);
return true;
} catch (Exception e) {
System.out.println(e.getMessage());
return false;
}
}
//This method tries to displayed the screen with a predefined name.
//First it makes sure the screen has been already loaded. Then if there is more than
//one screen the new screen is been added second, and then the current screen is removed.
// If there isn't any screen being displayed, the new screen is just added to the root.
public boolean setScreen(final String name) {
if (screens.get(name) != null) { //screen loaded
final DoubleProperty opacity = opacityProperty();
if (!getChildren().isEmpty()) { //if there is more than one screen
Timeline fade = new Timeline(
new KeyFrame(Duration.ZERO, new KeyValue(opacity, 1.0)),
new KeyFrame(new Duration(1000), new EventHandler<ActionEvent>() {
#Override
public void handle(ActionEvent t) {
getChildren().remove(0); //remove the displayed screen
getChildren().add(0, screens.get(name)); //add the screen
Timeline fadeIn = new Timeline(
new KeyFrame(Duration.ZERO, new KeyValue(opacity, 0.0)),
new KeyFrame(new Duration(800), new KeyValue(opacity, 1.0)));
fadeIn.play();
}
}, new KeyValue(opacity, 0.0)));
fade.play();
} else {
setOpacity(0.0);
getChildren().add(screens.get(name)); //no one else been displayed, then just show
Timeline fadeIn = new Timeline(
new KeyFrame(Duration.ZERO, new KeyValue(opacity, 0.0)),
new KeyFrame(new Duration(2500), new KeyValue(opacity, 1.0)));
fadeIn.play();
}
return true;
} else {
System.out.println("screen hasn't been loaded!!! \n");
return false;
}
/*Node screenToRemove;
if(screens.get(name) != null){ //screen loaded
if(!getChildren().isEmpty()){ //if there is more than one screen
getChildren().add(0, screens.get(name)); //add the screen
screenToRemove = getChildren().get(1);
getChildren().remove(1); //remove the displayed screen
}else{
getChildren().add(screens.get(name)); //no one else been displayed, then just show
}
return true;
}else {
System.out.println("screen hasn't been loaded!!! \n");
return false;
}*/
}
//This method will remove the screen with the given name from the collection of screens
public boolean unloadScreen(String name) {
if (screens.remove(name) == null) {
System.out.println("Screen didn't exist");
return false;
} else {
return true;
}
}
}
ScreensFramework Main App Code:
package essapp;
import javafx.application.Application;
import javafx.geometry.Rectangle2D;
import javafx.scene.Group;
import javafx.scene.Scene;
import javafx.stage.Screen;
import javafx.stage.Stage;
public class ScreensFramework extends Application {
public static String mainInterfaceID = "main";
public static String mainInterfaceFile = "mainInterface.fxml";
public static String msaInterfaceID = "msa";
public static String msaInterfaceFile = "msaInterface.fxml";
public static String creditRatingInterfaceID = "credit";
public static String creditRatingInterfaceFile = "creditRatingInterface.fxml";
public static String calcScriptEditID = "csEdit";
public static String calcScriptEditFile = "csEditInterface.fxml";
public static String calcScriptInterfaceID = "calc";
public static String calcScriptInterfaceFile = "calcScriptInterface.fxml";
public static String subVarInterfaceID = "subvar";
public static String subVarInterfaceFile = "subVarInterface.fxml";
#Override
public void start(Stage primaryStage) {
ScreensController mainContainer = new ScreensController();
mainContainer.loadScreen(ScreensFramework.mainInterfaceID, ScreensFramework.mainInterfaceFile);
mainContainer.loadScreen(ScreensFramework.calcScriptInterfaceID, ScreensFramework.calcScriptInterfaceFile);
mainContainer.loadScreen(ScreensFramework.calcScriptEditID, ScreensFramework.calcScriptEditFile);
mainContainer.loadScreen(ScreensFramework.subVarInterfaceID, ScreensFramework.subVarInterfaceFile);
mainContainer.loadScreen(ScreensFramework.msaInterfaceID, ScreensFramework.msaInterfaceFile);
mainContainer.loadScreen(ScreensFramework.creditRatingInterfaceID, ScreensFramework.creditRatingInterfaceFile);
mainContainer.setScreen(ScreensFramework.mainInterfaceID);
mainContainer.prefHeightProperty().bind(primaryStage.heightProperty());
mainContainer.prefWidthProperty().bind(primaryStage.widthProperty());
mainContainer.setCenterShape(true);
mainContainer.setScaleShape(true);
Group root = new Group();
root.getChildren().addAll(mainContainer);
Scene scene = new Scene(root);
Screen screen = Screen.getPrimary();
Rectangle2D bounds = screen.getVisualBounds();
primaryStage.setWidth(bounds.getWidth());
primaryStage.setHeight(bounds.getHeight());
primaryStage.setScene(scene);
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
}
When you call
sqlCSEditTbl.itemsProperty().setValue(...);
you are updating the UI (by updating the items displayed in the table). Like (almost?) all UI toolkits, JavaFX is single-threaded: updates to the UI can only happen on the FX Application Thread. Doing this in the Task, which is being executed on a background thread, will throw a IllegalStateException; so your call method never completes and you never see the update to the table.
The usual way to do this is to return the data from your call method:
Task<List<SQLCalcScripts>> task = new Task<List<SQLCalcScripts>>() {
#Override
public List<SQLCalcScripts> call() throws Exception {
List<SQLCalcScripts> data = /* get data.... */ ;
return data;
}
};
when the task completes, the value property is set to the value returned from the call method, so you can now do:
task.setOnSucceeded(e -> sqlCSEditTbl.getItems().setAll(task.getValue()));
It's probably a good idea to log any exceptions that occur with
task.setOnFailed(e -> task.getException().printStackTrace());
Then as before launch the task with
new Thread(task).start();
The JavaFX2 tutorial has an Addressbook example which is a table view.
tutorial:
http://docs.oracle.com/javafx/2/fxml_get_started/fxml_tutorial_intermediate.htm#CACFEHBI
enhancement to edit cells:
http://docs.oracle.com/javafx/2/ui_controls/table-view.htm
modification to add checkbox:
http://download.oracle.com/otndocs/products/javafx/2.2/samples/Ensemble/index.html#SAMPLES/Controls/Table/Table Cell Factory
Entering data in the text boxes and Clicking the Add button adds a row to the table.
However, rows cannot be deleted.
I was trying to see if I can add a Delete button in each cell - clicking it will delete the row.
I modified the existing sample code.
Somehow, the buttons are not visible. Any pointers appreciated.
//FXML_tableview.fxml file
<?import javafx.collections.*?>
<?import javafx.geometry.Insets?>
<?import java.lang.*?>
<?import javafx.scene.*?>
<?import javafx.scene.control.*?>
<?import javafx.scene.control.cell.*?>
<?import javafx.scene.layout.*?>
<?import fxmltableview.*?>
<Scene fx:id="root" width="550" height="550"
fx:controller="fxmltableview.FXMLTableViewController"
xmlns:fx="http://javafx.com/fxml">
<GridPane alignment="center" hgap="10" vgap="10">
<padding>
<Insets top="10" right="10" bottom="10" left="10"/>
</padding>
<TableView fx:id="tableView" GridPane.columnIndex="0"
GridPane.rowIndex="1">
</TableView>
<HBox spacing="10" alignment="bottom_right" GridPane.columnIndex="0"
GridPane.rowIndex="2">
<TextField fx:id="firstNameField" promptText="First Name"
prefWidth="90"/>
<TextField fx:id="lastNameField" promptText="Last Name"
prefWidth="90"/>
<TextField fx:id="emailField" promptText="Email"
prefWidth="150"/>
<Button text="Add" onAction="#addPerson"/>
</HBox>
</GridPane>
</Scene>
// FXMLTableView.java
package fxmltableview;
import javafx.application.Application;
import javafx.event.EventHandler;
import javafx.fxml.FXMLLoader;
import javafx.scene.Scene;
import javafx.scene.control.TableColumn;
import javafx.stage.Stage;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.StringProperty;
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.ActionEvent;
import javafx.event.EventHandler;
import javafx.fxml.FXML;
import javafx.geometry.Pos;
import javafx.scene.control.Button;
import javafx.scene.control.CheckBox;
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.input.KeyCode;
import javafx.scene.input.KeyEvent;
import javafx.scene.layout.AnchorPane;
import javafx.util.Callback;
public class FXMLTableView extends Application {
static public ObservableList<Person> data;
#FXML Scene root;
#FXML TableView tableView;
#Override
public void start(Stage primaryStage) throws Exception {
primaryStage.setTitle("FXML TableView Example");
root = (Scene)FXMLLoader.load(getClass().getResource("fxml_tableview.fxml"));
primaryStage.setScene(root);
data = FXCollections.observableArrayList(
new Person(true, "Jacob", "Smith", "jacob.smith#example.com"),
new Person(false, "Isabella", "Johnson", "isabella.johnson#example.com"),
new Person(true, "Ethan", "Williams", "ethan.williams#example.com"),
new Person(true, "Emma", "Jones", "emma.jones#example.com"),
new Person(false, "Michael", "Brown", "michael.brown#example.com"));
tableView = (TableView) root.lookup("#tableView");
TableColumn delCol = new TableColumn<Person, Boolean>();
delCol.setMinWidth(50);
delCol.setCellFactory(new Callback<TableColumn<Person, Boolean>, TableCell<Person, Boolean>>() {
public TableCell<Person, Boolean> call(TableColumn<Person, Boolean> p) {
return new DeleteTableCell<Person, Boolean>();
}
});
//"Invited" column
TableColumn invitedCol = new TableColumn<Person, Boolean>();
invitedCol.setText("Invited");
invitedCol.setMinWidth(50);
invitedCol.setCellValueFactory(new PropertyValueFactory("invited"));
invitedCol.setCellFactory(new Callback<TableColumn<Person, Boolean>, TableCell<Person, Boolean>>() {
public TableCell<Person, Boolean> call(TableColumn<Person, Boolean> p) {
return new CheckBoxTableCell<Person, Boolean>();
}
});
//"First Name" column
TableColumn firstNameCol = new TableColumn();
firstNameCol.setText("First");
firstNameCol.setCellValueFactory(new PropertyValueFactory("firstName"));
//"Last Name" column
TableColumn lastNameCol = new TableColumn();
lastNameCol.setText("Last");
lastNameCol.setCellValueFactory(new PropertyValueFactory("lastName"));
//"Email" column
TableColumn emailCol = new TableColumn();
emailCol.setText("Email");
emailCol.setMinWidth(200);
emailCol.setCellValueFactory(new PropertyValueFactory("email"));
//Set cell factory for cells that allow editing
Callback<TableColumn, TableCell> cellFactory =
new Callback<TableColumn, TableCell>() {
public TableCell call(TableColumn p) {
return new EditingCell();
}
};
emailCol.setCellFactory(cellFactory);
firstNameCol.setCellFactory(cellFactory);
lastNameCol.setCellFactory(cellFactory);
//Set handler to update ObservableList properties. Applicable if cell is edited
updateObservableListProperties(emailCol, firstNameCol, lastNameCol);
tableView.setItems(data);
//Enabling editing
tableView.setEditable(true);
tableView.getColumns().addAll(invitedCol, firstNameCol, lastNameCol, emailCol, delCol);
// root.getChildren().add(tableView);
primaryStage.show();
}
private void updateObservableListProperties(TableColumn emailCol, TableColumn firstNameCol,
TableColumn lastNameCol) {
//Modifying the email property in the ObservableList
emailCol.setOnEditCommit(new EventHandler<TableColumn.CellEditEvent<Person, String>>() {
#Override public void handle(TableColumn.CellEditEvent<Person, String> t) {
((Person) t.getTableView().getItems().get(
t.getTablePosition().getRow())).setEmail(t.getNewValue());
}
});
//Modifying the firstName property in the ObservableList
firstNameCol.setOnEditCommit(new EventHandler<TableColumn.CellEditEvent<Person, String>>() {
#Override public void handle(TableColumn.CellEditEvent<Person, String> t) {
((Person) t.getTableView().getItems().get(
t.getTablePosition().getRow())).setFirstName(t.getNewValue());
}
});
//Modifying the lastName property in the ObservableList
lastNameCol.setOnEditCommit(new EventHandler<TableColumn.CellEditEvent<Person, String>>() {
#Override public void handle(TableColumn.CellEditEvent<Person, String> t) {
((Person) t.getTableView().getItems().get(
t.getTablePosition().getRow())).setLastName(t.getNewValue());
}
});
}
//CheckBoxTableCell for creating a CheckBox in a table cell
public static class CheckBoxTableCell<S, T> extends TableCell<S, T> {
private final CheckBox checkBox;
private ObservableValue<T> ov;
public CheckBoxTableCell() {
this.checkBox = new CheckBox();
this.checkBox.setAlignment(Pos.CENTER);
setAlignment(Pos.CENTER);
setGraphic(checkBox);
}
#Override public void updateItem(T item, boolean empty) {
super.updateItem(item, empty);
if (empty) {
setText(null);
setGraphic(null);
} else {
setGraphic(checkBox);
if (ov instanceof BooleanProperty) {
checkBox.selectedProperty().unbindBidirectional((BooleanProperty) ov);
}
ov = getTableColumn().getCellObservableValue(getIndex());
if (ov instanceof BooleanProperty) {
checkBox.selectedProperty().bindBidirectional((BooleanProperty) ov);
}
}
}
}
public static class DeleteTableCell<S, T> extends TableCell<S, T> {
private final Button del;
private ObservableValue<T> ov;
public DeleteTableCell() {
this.del = new Button("X");
del.setStyle("-fx-base: red;");
this.del.setAlignment(Pos.CENTER);
setAlignment(Pos.CENTER);
setGraphic(del);
del.setOnAction(new EventHandler<ActionEvent> () {
#Override
public void handle(ActionEvent t) {
int i= getIndex();
FXMLTableView.data.remove(i);
}
});
}
#Override public void updateItem(T item, boolean empty) {
super.updateItem(item, empty);
if (empty) {
setText(null);
setGraphic(null);
} else {
setGraphic(del);
}
}
}
// EditingCell - for editing capability in a TableCell
public static class EditingCell extends TableCell<Person, String> {
private TextField textField;
public EditingCell() {
}
#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(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.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 void main(String[] args) {
launch(args);
}
}
// FXMLTableViewController.java
package fxmltableview;
import javafx.collections.ObservableList;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.fxml.FXML;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.control.TextField;
public class FXMLTableViewController {
#FXML private TableView<Person> tableView;
#FXML private TextField firstNameField;
#FXML private TextField lastNameField;
#FXML private TextField emailField;
#FXML
protected void addPerson(ActionEvent event) {
ObservableList<Person> data = tableView.getItems();
data.add(new Person(false, firstNameField.getText(),
lastNameField.getText(),
emailField.getText()
));
firstNameField.setText("");
lastNameField.setText("");
emailField.setText("");
}
}
// Person.java
package fxmltableview;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
//Person object
public class Person {
private BooleanProperty invited;
private StringProperty firstName;
private StringProperty lastName;
private StringProperty email;
Person(boolean invited, String fName, String lName, String email) {
this.invited = new SimpleBooleanProperty(invited);
this.firstName = new SimpleStringProperty(fName);
this.lastName = new SimpleStringProperty(lName);
this.email = new SimpleStringProperty(email);
this.invited = new SimpleBooleanProperty(invited);
this.invited.addListener(new ChangeListener<Boolean>() {
public void changed(ObservableValue<? extends Boolean> ov, Boolean t, Boolean t1) {
System.out.println(firstNameProperty().get() + " invited: " + t1);
}
});
}
public BooleanProperty invitedProperty() { return invited; }
public StringProperty firstNameProperty() { return firstName; }
public StringProperty lastNameProperty() { return lastName; }
public StringProperty emailProperty() { return email; }
public void setLastName(String lastName) { this.lastName.set(lastName); }
public void setFirstName(String firstName) { this.firstName.set(firstName); }
public void setEmail(String email) { this.email.set(email); }
}
The delCol's cell value factory is missing. Add
delCol.setCellValueFactory(new PropertyValueFactory("invited"));
PropertyValueFactory's parameter is not important here.
I am busy working through a tutorial and am stuck on one point.
Basically launch new scene and display values from ObservableList of type Person in a table view.
The person class consists of 3 SimpleStringProperty’s firsName, lastName and email.
But when I run the app it seems to display the values of the SimpleStringProperty [StringProperty [value:actualvalue]] in the table and not the value of the SimpleStringProperty. If I change the Person’s class getFirstName method to return a String firstName.get() then it display the value correctly in the table.
Am I supposed to use the SimpleStringProperty or rather plain objects like String?
The person class
package co.za.chrispie.addressBook;
import javafx.beans.property.SimpleStringProperty;
public class Person {
private SimpleStringProperty firstName;
private SimpleStringProperty lastName;
private SimpleStringProperty email;
public Person(final String firstName, final String lastName, final String email) {
this.firstName = new SimpleStringProperty(firstName);
this.lastName = new SimpleStringProperty(lastName);
this.email = new SimpleStringProperty(email);
}
public String getFirstName() {
return firstName.get(); //Changes this method to return a String
}
public SimpleStringProperty getLastName() {
return lastName;
}
public SimpleStringProperty getEmail() {
return email;
}
}
The Controller class
/*
* To change this template, choose Tools | Templates
* and open the template in the editor.
*/
package co.za.chrispie.addressBook;
import java.net.URL;
import java.util.ResourceBundle;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.control.cell.PropertyValueFactory;
public class OracleAddressBookController implements Initializable {
#FXML
TableView<Person> table;
#FXML
TableColumn<Person, String> colFirstName;
#FXML
TableColumn<Person, String> colLastName;
#FXML
TableColumn<Person, String> colEmail;
final ObservableList<Person> data = FXCollections.observableArrayList(
new Person("Jaco", "Pieters", "jp#me.co.za"),
new Person("Chrispie", "Pieters", "chrispie#me.co.za")
);
#Override
public void initialize(URL url, ResourceBundle rb) {
assert colFirstName != null : "fx:id=\"colFirstName\" was not injected: check your FXML file 'OracleAddressBook.fxml'.";
assert colLastName != null : "fx:id=\"colLastName\" was not injected: check your FXML file 'OracleAddressBook.fxml'.";
assert colEmail != null : "fx:id=\"colEmail\" was not injected: check your FXML file 'OracleAddressBook.fxml'.";
configureTable();
}
private void configureTable() {
colFirstName.setCellValueFactory(new PropertyValueFactory<Person, String>("firstName"));
colLastName.setCellValueFactory(new PropertyValueFactory<Person, String>("lastName"));
colEmail.setCellValueFactory(new PropertyValueFactory<Person, String>("email"));
table.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY);
table.setItems(data);
assert table.getItems() == data;
}
}
and the front end FXML
<?xml version="1.0" encoding="UTF-8"?>
<?import java.lang.*?>
<?import java.net.*?>
<?import java.util.*?>
<?import javafx.scene.*?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>
<AnchorPane id="AnchorPane" prefHeight="500.0" prefWidth="600.0" styleClass="mainFxmlClass" xmlns:fx="http://javafx.com/fxml" fx:controller="co.za.chrispie.addressBook.OracleAddressBookController">
<children>
<Label layoutX="14.0" layoutY="11.0" text="Address Book" />
<TableView fx:id="table" layoutX="14.0" layoutY="35.0" prefHeight="451.0" prefWidth="572.0">
<columns>
<TableColumn prefWidth="75.0" text="First Name" fx:id="colFirstName" />
<TableColumn prefWidth="75.0" text="Last Name" fx:id="colLastName" />
<TableColumn prefWidth="75.0" text="Email" fx:id="colEmail" />
</columns>
</TableView>
</children>
<stylesheets>
<URL value="#oracleaddressbook.css" />
</stylesheets>
</AnchorPane>
You need to change the name of your property getters to nameProperty(), where name is your property. So you would have firstNameProperty() and emailProperty().
getName() works, but the table won't observe the property's changes.
The doc for PropertyValueFactory explains this subject:
In this example, the "firstName" string is used as a reference to an assumed firstNameProperty() method in the Person class type (which is the class type of the TableView items list). Additionally, this method must return a Property instance. If a method meeting these requirements is found, then the TableCell is populated with this ObservableValue . In addition, the TableView will automatically add an observer to the returned value, such that any changes fired will be observed by the TableView, resulting in the cell immediately updating.
If no method matching this pattern exists, there is fall-through support for attempting to call get() or is() (that is, getFirstName() or isFirstName() in the example above). If a method matching this pattern exists, the value returned from this method is wrapped in a ReadOnlyObjectWrapper and returned to the TableCell. However, in this situation, this means that the TableCell will not be able to observe the ObservableValue for changes (as is the case in the first approach above).