JavaFX CheckboxTreeItem: unselectable leaves - java

I'm trying to set up a checkbox tree in JavaFX where the leaves are bound to their parents:
they are selected when the parent is selected
cannot be changed individually.
I did it using a binding with the selectedProperty.
It works just fine, but it throws an exception each time I select or deselect the parent.
Is there an easy way around this? I hate to abandon an approach that works except for those nasty exceptions.
The relevant code is below. Please forgive any java convention errors - I'm still new to the language.
Controller:
package application;
import java.util.ArrayList;
import java.util.List;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.fxml.FXML;
import javafx.scene.control.CheckBoxTreeItem;
import javafx.scene.control.SelectionMode;
import javafx.scene.control.TreeView;
import javafx.scene.control.cell.CheckBoxTreeCell;
public class BindingTestController {
#FXML
private TreeView<String> MainTree;
static List<CheckBoxTreeItem<String>> treeItems = new ArrayList<CheckBoxTreeItem<String>>();
CheckBoxTreeItem<String> root = new CheckBoxTreeItem<String>("Root");
CheckBoxTreeItem<String> parent1 = new CheckBoxTreeItem<String>("Parent1");
CheckBoxTreeItem<String> parent2 = new CheckBoxTreeItem<String>("Parent2");
private void AddColumns(CheckBoxTreeItem<String> item){
for (int i = 1 ; i < 5 ; i++){
CheckBoxTreeItem<String> itemColumn = new CheckBoxTreeItem<String>(item.getValue()+"_Column_"+i);
treeItems.add(itemColumn);
item.getChildren().add(itemColumn);
itemColumn.selectedProperty().bind(item.selectedProperty());
}
}
#FXML // This method is called by the FXMLLoader when initialization is complete
private void initialize() {
treeItems.add(root);
treeItems.add(parent1);
root.getChildren().add(parent1);
treeItems.add(parent2);
root.getChildren().add(parent2);
AddColumns(parent1);
AddColumns(parent2);
MainTree.setRoot(root);
MainTree.setShowRoot(true);
root.setExpanded(true);
MainTree.setCellFactory(p-> new CheckBoxTreeCell());
MainTree.getSelectionModel().setSelectionMode(SelectionMode.SINGLE);
MainTree.getSelectionModel().selectedItemProperty().addListener(new ChangeListener() {
#Override
public void changed(ObservableValue observable, Object oldValue, Object newValue) {
CheckBoxTreeItem<String> treeItem = (CheckBoxTreeItem)newValue;
}
});
}
}
Here is the FXML:
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.control.TreeView?>
<?import javafx.scene.layout.BorderPane?>
<?import javafx.scene.layout.VBox?>
<BorderPane xmlns="http://javafx.com/javafx/8.0.65" xmlns:fx="http://javafx.com/fxml/1" fx:controller="application.BindingTestController">
<center>
<VBox prefHeight="1049.0" prefWidth="714.0"BorderPane.alignment="CENTER">
<children>
<TreeView fx:id="MainTree" prefHeight="624.0" prefWidth="714.0"/>
</children>
</VBox>
</center>
</BorderPane>
and here is the main:
package application;
import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.stage.Stage;
import javafx.scene.Scene;
import javafx.scene.layout.BorderPane;
public class Main extends Application {
#Override
public void start(Stage primaryStage) {
try {
// BorderPane root = new BorderPane();
BorderPane root = (BorderPane) FXMLLoader.load(Main.class.getResource("BindingTest.fxml"));
Scene scene = new Scene(root,400,400);
scene.getStylesheets().add(getClass().getResource("application.css").toExternalForm());
primaryStage.setScene(scene);
primaryStage.show();
} catch(Exception e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
launch(args);
}
}
Any advice would be appreciated.

The problem is coming from this line
itemColumn.selectedProperty().bind(item.selectedProperty());
To understand why do you get an exception whatever node you select, you should take a look on the independentProperty of CheckBoxTreeItem:
A CheckBoxTreeItem can be independent or dependent. By default,
CheckBoxTreeItem instances are dependent, which means that any changes
to the selection state of a TreeItem will have an impact on parent and
children CheckBoxTreeItem instances. If a CheckBoxTreeItem is set to
be independent, this means that any changes to that CheckBoxTreeItem
will not directly impact the state of parent and children
CheckBoxTreeItem instances.
This means, when you select/deselect a node in the tree, which has children, it will select/deselect all of it's children by default (and also sets the checked, unchecked, indeterminate state of its parent, but this is not important in your case). This is one of the functionality what you need and you get it out of the box.
Now you can understand why your binding is problematic:
You bind the selectedProperty() of the child nodes to the selectedProperty() of the parent node, then:
if you check one of the leaf nodes, you will get an exception, because the value is bound, and bound values cannot be set.
if you check one of the parent nodes, it will try to select all of its children (because of being dependent), then you will get an exception again.
Solution
You have two requirements:
select all of the children nodes, when the parent is selected, which is already done
do not allow to select child nodes manually
For the second requirement, you can disable the checkbox of these items. For this you can use the setCellFactory method of your TreeView:
MainTree.setCellFactory((item) -> {
return new CheckBoxTreeCell<String>(){
#Override
public void updateItem(String item, boolean empty) {
super.updateItem(item, empty);
if(item != null) {
this.disableProperty().unbind();
CheckBoxTreeItem<String> value = (CheckBoxTreeItem<String>) treeItemProperty().getValue();
this.disableProperty().bind(value.leafProperty());
}
}
};
});
This snippet will bind the disableProperty of the displayed CheckBoxTreeCell objects to the leafProperty of the corresponding CheckBoxTreeItem.
public final ReadOnlyBooleanProperty leafProperty()
Represents the TreeItem leaf property, which is true if the TreeItem
has no children.
Which results in a tree where all the nodes that has no children are disabled.
To make this working you should remove this line from your code:
itemColumn.selectedProperty().bind(item.selectedProperty());
and you should replace this line
MainTree.setCellFactory(p-> new CheckBoxTreeCell());
with the code posted above.

Related

JavaFX: Right click on TableColumn disables resizing

Reproduced in OpenJFX 11.0.2 & 12.0.1 SDK (Windows 10, x64), not reproducible in JavaFX 8
Right-click on a table-column, then try to resize the column. No resize cursor is shown and column can't be resized until you manually click on the column again.
Any ideas for a workaround? I need to usecontextMenu for TableColumns, so potential workarounds that make the header ignore right mouse click aren't possible.
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.stage.Stage;
public class Foo extends Application {
#Override
public void start(Stage stage) throws Exception {
TableView<Object> testView = new TableView<>();
testView.getColumns().addAll(new TableColumn<Object, Object>("C1"), new TableColumn<Object, Object>("C2"), new TableColumn<Object, Object>("C3"));
stage.setScene(new Scene(testView));
stage.show();
}
public static void main(String[] args) {
launch(args);
}
}
Ok I found the following (very, very dirty) workaround. I never tried this before because I assumend it would prevent the context menu from showing (as I noted in my original question), but apprently simply consuming the mouse event of every TableColumnHeader works and the context menu is still shown correctly (also works with TableColumns without context menus).
Not sure if anything internal could go wrong with this, but as the right click doesn't seem to be doing anything useful by default, I hope not.
Of course lookupAll needs to be called after it has been rendered.
Note 1: If you have TableMenuButtonVisible set to true, you need to do this every time a column is set to visible.
Note 2: Its getting dirtier and dirtier. Simply calling this again after a column has been set to visible (see note 1) doesn't always suffice (also not with a Platform.runLater call). I assume that's because the column header hasn't been rendered at that point. You either
need to wait until the Set<Node> is fully filled, i.e. the size of
it must be amountOfVisibleColumns + 1. If its equal to the amount
of visible columns, it won't work for the newly shown column.
or call layout() on the TableView before lookupAll
or if you have a class that extends TableView, override layoutChildren and execute the lookup if the amount of visible columns has changed
Note 3: You need to keep track of the old onMousePressed and execute it if the button isn't SECONDARY, otherwise the reordering of columns won't work.
Please let me know if you can think of any cleaner way.
import java.util.Set;
import javafx.application.Application;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.control.ContextMenu;
import javafx.scene.control.MenuItem;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.control.skin.TableColumnHeader;
import javafx.scene.input.MouseButton;
import javafx.stage.Stage;
public class Foo extends Application {
#Override
public void start(Stage stage) throws Exception {
TableView<Object> testView = new TableView<>();
testView.getColumns().addAll(createColumn("C1"), createColumn("C2"), createColumn("C3"));
stage.setOnShown(ev -> {
Set<Node> headers = testView.lookupAll("TableColumnHeader");
for (Node header : headers) {
if (header != null) {
((TableColumnHeader) header).setOnMousePressed(e -> {
if (e.getButton() == MouseButton.SECONDARY) {
e.consume();
}
});
}
}
});
stage.setScene(new Scene(testView));
stage.show();
}
private TableColumn<Object, Object> createColumn(String text) {
MenuItem item = new MenuItem("Context");
item.setOnAction(e -> {
System.out.println("Action");
});
ContextMenu contextMenu = new ContextMenu();
contextMenu.getItems().add(item);
TableColumn<Object, Object> column = new TableColumn<>(text);
column.setContextMenu(contextMenu);
return column;
}
public static void main(String[] args) {
launch(args);
}
}
EDIT: Found the described bug in the Java bug tracker and filed a PR with the fix:
https://github.com/openjdk/jfx/pull/483
EDIT 2: My PR was accepted and merged back. The bug is fixed now, you can test it by using 17-ea+11. :-)
I have the same problem. This bug is caused by the mousePressedHandler added in TableColumnHeader. This class has even more problems, for example if I close a PopupControl with setConsumeAutoHidingEvents(true) by a click on a column, the sorting will be triggered. Those methods needs to be changed, maybe the addEventHandler methods should be used instead of the convenience setOn... methods.
I fixed it by consuming the event when I'm about to show my PopupControl:
public class MyTableColumnHeader extends TableColumnHeader {
public MyTableColumnHeader(TableColumnBase tc) {
super(tc);
addEventHandler(MouseEvent.MOUSE_PRESSED, this::onMousePressed);
}
private void onMousePressed(MouseEvent mouseEvent) {
if (mouseEvent.getButton() == MouseButton.SECONDARY) {
showPopup();
// Consume here, so the column won't get 'stuck'.
mouseEvent.consume();
}
}
private void showPopup() {
...
}
}
Eventually, someone should open at least a bug. I may will also have a look in the not too distant future.

Problem with extending SimpleStringProperty

I am just trying to extend a SimpleStringProperty in OpenJFX 11.0.1 to add some extra functionality. But ist seems not so easy, I experienced strange behavior of my extended Property and I don't know why. I think it should work.
My in this sample code simplified SimpleStringProperty extension contains another readonly string property which should be updated every time the the user types into a bound TextField. In this case remove all not allowed characters and convert the prefix. (I know this is not perfect but short enough to show)
After starting the sample code you will get a window with a rows of Controls. Typing in a String like "001 (242) 555666" the label should show the normalized phone number like "+1242555666".
The initial conversion works correcty.
I never get any exceptions.
The conversion is called when I type in new digits.
But if you play around with typing and deleting after a few seconds the set() method of my property isn't longer triggered by the bidirectional binding to the TextField.
To simplify the example I didn't use a TextFormatter. If I use one the problem doesn't change.
Can anyone help me figure out the problem?
Windows and OS X show the same behavior with OpenJFX 11 and OpenJFX 11.0.1
I tried the same code with JDK 1.8 and there it works fine.
package testproperty;
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.stage.Stage;
import javafx.beans.property.ReadOnlyStringProperty;
import javafx.beans.property.ReadOnlyStringWrapper;
import javafx.beans.property.SimpleStringProperty;
import javafx.geometry.Insets;
import javafx.scene.control.Label;
import javafx.scene.control.TextField;
import javafx.scene.layout.GridPane;
public class TestProperty extends Application {
// attempt to create an own property
public class myPhoneNumberProperty extends SimpleStringProperty {
private final ReadOnlyStringWrapper normalizedNumber = new ReadOnlyStringWrapper("");
public ReadOnlyStringProperty normalizedNumberProperty() { return normalizedNumber.getReadOnlyProperty(); }
public String getNormalizedNumber() { return normalizedNumber.get(); }
public myPhoneNumberProperty() {
super();
}
public myPhoneNumberProperty(String s) {
super(s);
calculate();
}
#Override
public void set(String s) {
super.set(s);
calculate();
}
private void calculate() {
// some calculations (only for test purposes)
String original = this.get();
String result = original.replaceAll("[^0123456789]","");
if (result.startsWith("00")) result = result.replaceFirst("00", "+");
if (original.startsWith("+")) result = "+".concat(result);
normalizedNumber.set(result);
}
}
#Override
public void start(Stage primaryStage) {
// create my property
myPhoneNumberProperty phoneNumberA = new myPhoneNumberProperty("+34 952 111 222");
// set up grid pane
GridPane grid = new GridPane();
grid.setPadding(new Insets(5,5,5,5));
grid.setVgap(20);
grid.setHgap(20);
// set up the row
Label labelA = new Label("Enter phone number");
TextField textFieldA = new TextField();
textFieldA.textProperty().bindBidirectional(phoneNumberA);
Label labelB = new Label("Normalized number");
Label labelN = new Label();
labelN.textProperty().bind(phoneNumberA.normalizedNumberProperty());
grid.addRow(0, labelA, textFieldA, labelB, labelN);
// complete scene
Scene scene = new Scene(grid, 1000, 100);
primaryStage.setTitle("PhoneNumberProperty TestProg");
primaryStage.setScene(scene);
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
}
Your phoneNumberA property object is being garbage collected. To fix this you must keep a strong reference to the object. One option is to make it an instance field.
JavaFX implements bindings using weak listeners/references. Bidirectional bindings have no strong references to the other property. This is different from unidirectional bindings where a reference to the observable value must be kept in order to unbind from it later.

JavaFX Sync Duplicate Views to the Same Controller (FXML & MVC)

Below is a small Application that illustrates the problem:
ButtonPanel.fxml
<ScrollPane fx:controller="ButtonPanelController">
<VBox>
<Button fx:id="myButton" text="Click Me" onAction="#buttonClickedAction" />
</VBox>
</ScrollPane>
ButtonPanelController.java
public class ButtonPanelController {
#FXML
Button myButton;
boolean isRed = false;
public void buttonClickedAction(ActionEvent event) {
if(isRed) {
myButton.setStyle("");
} else {
myButton.setStyle("-fx-background-color: red");
}
isRed = !isRed;
}
}
TestApp.java
public class TestApp extends Application {
ButtonPanelController buttonController;
#Override
public void start(Stage stage) throws Exception {
// 1st Stage
stage.setTitle("1st Stage");
stage.setWidth(200);
stage.setHeight(200);
stage.setResizable(false);
// Load FXML
FXMLLoader loader = new FXMLLoader(
ButtonPanelController.class.getResource("ButtonPanel.fxml"));
Parent root = (Parent) loader.load();
// Grab the instance of ButtonPanelController
buttonController = loader.getController();
// Show 1st Scene
Scene scene = new Scene(root);
stage.setScene(scene);
stage.show();
// 2nd Stage
Stage stage2 = new Stage();
stage2.setTitle("2nd Stage");
stage2.setWidth(200);
stage2.setHeight(200);
stage2.setResizable(false);
/* Override the ControllerFactory callback to use
* the stored instance of ButtonPanelController
* instead of creating a new one.
*/
Callback<Class<?>, Object> controllerFactory = type -> {
if(type == ButtonPanelController.class) {
return buttonController;
} else {
try {
return type.newInstance();
} catch(Exception e) {
throw new RuntimeException(e);
}
}
};
// Load FXML
FXMLLoader loader2 = new FXMLLoader(
ButtonPanelController.class.getResource("ButtonPanel.fxml"));
// Set the ControllerFactory before the load takes place
loader2.setControllerFactory(controllerFactory);
Parent root2 = (Parent) loader2.load();
// Show 2nd Scene
Scene scene2 = new Scene(root2);
stage2.setScene(scene2);
stage2.show();
}
public static void main(String[] args) {
launch(args);
}
}
Basically, I have a single FXML that I am using for two separate scenes that may or may not be active on the screen at the same time. A practical example of this would be having content docked to a side panel plus a button that opens the same content in a separate window that can be dragged/resized/etc.
The goal I am trying to achieve is to keep the views in sync (changes to one of the views affects the other one).
I am able to point both views to the same controller via a callback however the issue I am running into now is that the UI changes are only reflected on the 2nd scene. Both views talk to the controller but the controller only talks back to the 2nd scene. I'm assuming something with JavaFX's implementation of MVC or IOC is linking the controller to the view in some 1:1 relationship when it is loaded via the FXMLLoader.
I am well aware that trying to link two views to 1 controller is bad MVC practice, however I would like to avoid having to implement a separate FXML and Controller that are practically identical.
Is it possible to achieve this kind of synchronization that I listed above?
If I need to create a separate Controller, what's the best way to ensure that both UI's are in sync (even down to sidebar movements)?
Thanks in Advance!
-Steve
The reason your code doesn't work is that the FXMLLoader injects references to elements in the FXML with fx:id attributes into fields in the controller with matching names. So when you load the FXML file the first time, the FXMLLoader sets the field myButton to be a reference to the button it creates when it loads the FXML. Since you use the exact same controller instance the second time you load the FXML, the FXMLLoader now sets that same field (in the same controller instance) to be a reference to the button it creates when the FXML file is loaded again. In other words, buttonController.myButton now refers to the second button created, not the first. So when you call myButton.setStyle(...) it updates the style of the second button.
Basically, you always want one controller instance per view instance. What you need is for both controllers to access the same shared state.
Create a model class that stores the data. In a MVC architecture, the View observes the model and updates when the data in the model changes. The controller reacts to user interaction with the view and updates the model.
(Arguably, FXML gives you more of a MVP architecture, which is similar. There are variants of this too, but generally the presenter will observe the model and update the view when data in the model changes, as well as update the model in response to user interaction.)
So your model might look like:
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
public class Model {
private final BooleanProperty red = new SimpleBooleanProperty();
public final BooleanProperty redProperty() {
return this.red;
}
public final boolean isRed() {
return this.redProperty().get();
}
public final void setRed(final boolean red) {
this.redProperty().set(red);
}
public void toggleRed() {
setRed(! isRed() );
}
}
Your ButtonPanel.fxml doesn't change:
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.control.ScrollPane?>
<?import javafx.scene.layout.VBox?>
<?import javafx.scene.control.Button?>
<ScrollPane xmlns:fx="http://javafx.com/fxml/1" fx:controller="ButtonPanelController">
<VBox >
<Button fx:id="myButton" text="Click Me" onAction="#buttonClickedAction" />
</VBox>
</ScrollPane>
Your controller has a reference to the model. It can use bindings or listeners on the model properties to update the UI, and the handler methods just update the model:
import javafx.beans.binding.Bindings;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.scene.control.Button;
public class ButtonPanelController {
#FXML
Button myButton;
boolean isRed = false;
private Model model ;
public ButtonPanelController(Model model) {
this.model = model ;
}
public void initialize() {
myButton.styleProperty().bind(Bindings.
when(model.redProperty()).
then("-fx-background-color: red;").
otherwise("")
);
}
public void buttonClickedAction(ActionEvent event) {
model.toggleRed();
}
}
Finally, you keep everything synchronized because the views are views of the same model. In other words you just create one model and hand its reference to both controllers. Since I made the model a constructor parameter in the controller (which is nice, because you know you have a model as soon as the instance is created), we need a controller factory to create the controller instances:
import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.stage.Stage;
import javafx.util.Callback;
public class TestApp extends Application {
#Override
public void start(Stage stage) throws Exception {
// 1st Stage
stage.setTitle("1st Stage");
stage.setWidth(200);
stage.setHeight(200);
stage.setResizable(false);
// The one and only model we will use for both views and controllers:
Model model = new Model();
/* Override the ControllerFactory callback to create
* the controller using the model:
*/
Callback<Class<?>, Object> controllerFactory = type -> {
if(type == ButtonPanelController.class) {
return new ButtonPanelController(model);
} else {
try {
return type.newInstance();
} catch(Exception e) {
throw new RuntimeException(e);
}
}
};
// Load FXML
FXMLLoader loader = new FXMLLoader(
ButtonPanelController.class.getResource("ButtonPanel.fxml"));
loader.setControllerFactory(controllerFactory);
Parent root = (Parent) loader.load();
// Show 1st Scene
Scene scene = new Scene(root);
stage.setScene(scene);
stage.show();
// 2nd Stage
Stage stage2 = new Stage();
stage2.setTitle("2nd Stage");
stage2.setWidth(200);
stage2.setHeight(200);
stage2.setResizable(false);
// Load FXML
FXMLLoader loader2 = new FXMLLoader(
ButtonPanelController.class.getResource("ButtonPanel.fxml"));
// Set the ControllerFactory before the load takes place
loader2.setControllerFactory(controllerFactory);
Parent root2 = (Parent) loader2.load();
// Show 2nd Scene
Scene scene2 = new Scene(root2);
stage2.setScene(scene2);
stage2.show();
}
public static void main(String[] args) {
launch(args);
}
}

JavaFX Choiceox change not updating graphics

When I change the underlying observable array list the graphics choice box doesn't update. There must be a newer solution than what I have seen suggested here for example:
JavaFX: Update of ListView if an element of ObservableList changes
int selected = productsChoiceBox.getSelectionModel().getSelectedIndex();
Product prod = products.get(selected);
prod.setName(productName.getText());
prod.setUrl(productUrl.getText());
Any thoughts? I would like to avoid removing and adding.
The "standard" answer is to use an ObservableList with an extractor. However, when I tested this out, it didn't behave as advertised, and it seems like there is a bug (my guess is that ChoiceBox is not correctly handling wasUpdated type changes fired in its ListChangedListener) which I will report at JIRA. Update: filed report at https://javafx-jira.kenai.com/browse/RT-38394
The factory method FXCollections.observableArrayList(Callback) creates an (empty) observable array list. The provided Callback is a function that maps each element in the list to an array of Observables. The list registers listeners with those observables, and if those properties change, the list fires update notifications to its listeners.
This produces strange results with a ChoiceBox, however; one possible workaround would be to use a ComboBox which seems to work fine.
Here's some sample code. Select an item: then type in the text field and press enter to change the name of the selected item. Change ChoiceBox to ComboBox to see the correct behavior:
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import javafx.application.Application;
import javafx.beans.Observable;
import javafx.beans.binding.Bindings;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.scene.Scene;
import javafx.scene.control.ChoiceBox;
import javafx.scene.control.TextField;
import javafx.scene.layout.BorderPane;
import javafx.stage.Stage;
public class ChoiceBoxUpdateExample extends Application {
#Override
public void start(Stage primaryStage) {
ChoiceBox<Item> choiceBox = new ChoiceBox<>();
ObservableList<Item> items = FXCollections.observableArrayList(
item -> new Observable[] {item.nameProperty()}); // the extractor
items.addAll(
IntStream.rangeClosed(1, 10)
.mapToObj(i -> new Item("Item "+i))
.collect(Collectors.toList()));
choiceBox.setItems(items);
TextField changeSelectedField = new TextField();
changeSelectedField.disableProperty()
.bind(Bindings.isNull(choiceBox.getSelectionModel().selectedItemProperty()));
changeSelectedField.setOnAction(event ->
choiceBox.getSelectionModel().getSelectedItem().setName(changeSelectedField.getText()));
BorderPane root = new BorderPane();
root.setTop(choiceBox);
root.setBottom(changeSelectedField);
Scene scene = new Scene(root, 250, 150);
primaryStage.setScene(scene);
primaryStage.show();
}
public static class Item {
public final StringProperty name = new SimpleStringProperty();
public StringProperty nameProperty() {
return name ;
}
public final String getName() {
return nameProperty().get();
}
public final void setName(String name) {
nameProperty().set(name);
}
public Item(String name) {
setName(name);
}
#Override
public String toString() {
return getName();
}
}
public static void main(String[] args) {
launch(args);
}
}
The correct and proper answer is from James_D, but if you REALLY want to use ChoiceBox, then try adding and removing:
int selected = productsChoiceBox.getSelectionModel().getSelectedIndex();
products.remove(selected);
products.add(selected, prod);
I do NOT believe this is the right way, but I tested it, and it does work. The ChoiceBox stays on the the removed and selected index and looks like it updates.

Javafx Donut Chart in FXML

I want to do a Doughnut/Donut chart on JavaFX and searching I came to this example: Can PieChart from JavaFX be displayed as a doughnut?
I Works really nice, but since I'm using FXML to make my GUI, I can't use this example. First, I tried to add the DoughtnutChart.java class as a #FXML var in the controller class of the panel where I want to insert it, but launched errors.
Then, searched in Google to make the DoughnutChart a custom component, but all the examples are based on Panes. Also, If I try to import my donu.jar to SceneBuilder, the window to select a component is empty.
So, my question is: How do I implement this Doughnut Chart on JavaFX when my GUI is made on FXML?
Thanks a lot.
It's hard to tell what the cause of your error is without seeing the FXML and the error message.
I got this to work pretty easily: the one thing to be aware of is that the FXMLLoader instantiates classes by invoking the no-argument constructor. If it can't find one, it tries to use a builder class as a back-up plan. So the one modification you need to make to #jewelsea's DoughnutChart implementation is to add a no-argument constructor. (You could also define a DoughnutClassBuilder, but that's a lot more work, and doesn't get you any extra benefit.) So I did this:
package doughnut ;
// imports as before...
public class DoughnutChart extends PieChart {
private final Circle innerCircle;
public DoughnutChart() {
this(FXCollections.observableArrayList());
}
// everything else as before...
}
Then the following FXML:
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.layout.StackPane?>
<?import doughnut.DoughnutChart?>
<StackPane xmlns:fx="http://javafx.com/fxml" fx:controller="doughnut.SampleController">
<DoughnutChart fx:id="doughnutChart" />
</StackPane>
with the controller SampleController.java:
package doughnut;
import javafx.fxml.FXML;
import javafx.scene.chart.PieChart;
public class SampleController {
#FXML
private PieChart doughnutChart ;
public void initialize() {
doughnutChart.getData().addAll(
new PieChart.Data("Grapefruit", 13),
new PieChart.Data("Oranges", 25),
new PieChart.Data("Plums", 10),
new PieChart.Data("Pears", 22),
new PieChart.Data("Apples", 30));
}
}
and the application class
package doughnut;
import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Scene;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;
public class Main extends Application {
#Override
public void start(Stage primaryStage) throws Exception {
StackPane root = (StackPane)FXMLLoader.load(getClass().getResource("DoughnutChartDemo.fxml"));
Scene scene = new Scene(root,400,400);
primaryStage.setScene(scene);
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
}
work exactly as expected.
I had to replace this with super in James_D answer to make it work.
For me the constructor looked like:
public DoughnutChart()
{
super(FXCollections.observableArrayList());
innerCircle = new Circle();
// just styled in code for demo purposes,
// use a style class instead to style via css.
innerCircle.setFill(Color.WHITESMOKE);
innerCircle.setStroke(Color.WHITE);
innerCircle.setStrokeWidth(3);
}

Categories