Hiding Legend for Zero Value Chart Items javaFX - java

I'm new to the forums, so I hope I'm not asking a question that has been answered in the past. I've tried to be thorough looking for answer before posting.
I am currently working on a pie chart that will eventually be used for tracking financial expenses. Right now I have several categories that make up each slice. I am trying to hide the legend for the zero value slices.
I am doing this in javaFX. I'm still very green when it comes to programming and don't have experience outside of Java. Any help as explained to dummies would be appreciated. Thanks.
Added a picture and complete code to illustrate the problem at hand. Restaurants & Dining, and Shopping & entertainment both have zero values. I want to hide the legend for those items in this example.
package Example;
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.chart.PieChart;
public class PieExampleController implements Initializable {
#FXML
private PieChart pieChart;
#Override
public void initialize(URL arg0, ResourceBundle arg1) {
// TODO Auto-generated method stub
ObservableList<PieChart.Data> pieChartData = FXCollections.observableArrayList(
new PieChart.Data("Groceries", 1),
new PieChart.Data("Transportation", 1),
new PieChart.Data("Restaurants & Dining", 0),
new PieChart.Data("Shopping & Entertainment", 0));
pieChart.setData(pieChartData);
}
}

Thats how i do it:
List<PieChart.Data> dataArrayList = new LinkedList<Data>();
if (value1>0) {
Data data = new PieChart.Data("my label", value1);
dataArrayList.add(data);
}
...
ObservableList<PieChart.Data> pieChartData =
FXCollections.observableArrayList(dataArrayList);

Adding only not empty data entries (or removing empty entries) manually once at startup is just fine if the data is immutable and unmodifiable. On the other hand, if it can change during the lifetime of the chart, we need a mechanism that handles the add/remove automagically: FilteredList to the rescue.
Below is an example that
configures a source list with an extractor (on the pieValueProperty): doing so will notify any listChangeListener on change of that value with a change of type update
wraps a FilteredList around the source list
configures the pieChart with the filteredList
With that in place, we can install a predicate on the filteredList that hides items as needed: the example uses a Slider to update the lower threshhold of which data values should be included in the chart.
Unfortunately, PieChart has a couple of bugs (sigh... whatever I touch in FX, they always boil up ...) that interfere with such a simple setup
due to a freaky mixture of node/value plus "optimized" internal data structure plus incorrect implementation of syncing the internal (linked) data structure with changes to the list the chart can't be animated
the sync can't handle changes of type replaced at all (which is what FilteredList fires on resetting the predicate)
In an example both issues can be avoided by disabling animation and clearing out the list (set a predicate that blocks all) before setting the real condition. In producation code such tweaking may or may not be possible.
The example:
public class FilteredPieChartExample extends Application {
#Override
public void start(Stage primaryStage) {
FilteredList<Data> filtered = getChartData();
//ListChangeReport report = new ListChangeReport(filtered);
PieChart pieChart = new PieChart(filtered);
// bug in pieChart: can't handle data modification with animation on
pieChart.setAnimated(false);
// use slider to set lower threshhold for value of data to show in pie
Slider slider = new Slider(-1., 100., -1.);
slider.valueProperty().addListener((src, ov, nv) -> {
// actually, cannot handle data modification at all ... need to clear out first ...
// bug in pieChart.dataChangeListener: doesn't handle replaced correctly
filtered.setPredicate(data -> false);
filtered.setPredicate(data -> data.getPieValue() > nv.doubleValue());
//report.prettyPrint();
});
primaryStage.setTitle("PieChart");
Pane root = new VBox(pieChart, slider);
Scene scene = new Scene(root);
primaryStage.setScene(scene);
primaryStage.show();
}
private FilteredList<Data> getChartData() {
// use ObservableList with extractor on pieValueProperty
ObservableList<Data> answer = FXCollections.observableArrayList(
e -> new Observable[] {e.pieValueProperty()}
);
answer.addAll(
new Data("java", 17.56),
new Data("C", 17.06),
new Data("C++", 8.25),
new Data("C#", 8.20),
new Data("ObjectiveC", 6.8),
new Data("PHP", 6.0),
new Data("(Visual)Basic", 4.76),
new Data("Other", 31.37),
new Data("empty", 0)
);
return new FilteredList<>(answer);
}
/**
* #param args the command line arguments
*/
public static void main(String[] args) {
launch(args);
}
#SuppressWarnings("unused")
private static final Logger LOG = Logger.getLogger(FilteredPieChartExample.class
.getName());
}

Related

Force relayout of Java FX Chart programatically

I have made programmatic changes to a Java FX Line Chart and I need a programmatic way to force a re-layout of the JavaFX Chart to occur. This question has been asked/answered before but not in my context.
I have tried the typical methods that have been presented as answers to this question (see complete, minimal example code below with in-line attempts at solving the problem). None of the typical solutions to this problem work.
Specifically (sp is a StackPane):
sp.requestLayout(); // does not work
and
sp.applyCss();
sp.layout(); // does not work
placing the above code in a .runLater() does not work.
I know that my changes are present in the chart because
(1) When I resize the chart by hand my changes suddenly appear
(2) When I use the "resize" method programmatically my changes appear BUT there is a different error (plus only parent nodes are supposed to use the "resize" method - not us programmers).
Below is a minimal complete set of code which reproduces the problem. When you run the code I programmatically change one of the data points to be larger when the chart is displayed. This resize works correctly. When you right-click on the chart a context menu appears with one choice ("Resize ALL the points"). When you select that single option my code resizes all the points - BUT - none of the data points are resized visually. If I resize the chart manually by dragging a side, the chart does a re-layout and all the data node sizes immediately visually change to the correct size (The size I programmatically set them to).
How can I force the re-layout to occur programmatically that I can force to occur manually? I would NOT like to do a hack (like programmatically set the stage size to be 1 pixel smaller and then set it one pixel larger).
Note: I have read that attempts to do a requestLayout() while a layout is in progress will be ignored so perhaps something like that is going on. I think a requestLayout() inside of a runLater() would fix the issue of an ongoing Layout() but that has not worked either.
Update: Scaling was suggested as an alternative to changing the StackPane size. This solution may be helpful to some but not to me. The Look and Feel of scaling a symbol is different than the look and feel of changing the regions size and allowing the "symbol" to grow into that size.
As a complete aside this is my first stackoverflow post. So thanks for all the previous examples a I have used from this forum in the past & thanks in advance for the answer to this problem.
import java.util.Random;
import javafx.application.Application;
import javafx.application.Platform;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.scene.Group;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.chart.LineChart;
import javafx.scene.chart.LineChart.SortingPolicy;
import javafx.scene.chart.NumberAxis;
import javafx.scene.chart.XYChart.Data;
import javafx.scene.chart.XYChart.Series;
import javafx.scene.control.ContextMenu;
import javafx.scene.control.MenuItem;
import javafx.scene.input.MouseButton;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;
public class dummy extends Application {
#Override
public void start(Stage primaryStage) {
Random random = new Random();
final NumberAxis xAxis = new NumberAxis();
final NumberAxis yAxis = new NumberAxis();
xAxis.setLabel("X");
yAxis.setLabel("Y");
final LineChart<Number,Number> lineChart = new LineChart<Number,Number>(xAxis,yAxis);
Series<Number,Number> series = new Series<Number,Number>();
series.setName("Dummy Data");
// Generate data
double x = 0.0;
double y = 0.0;
for (int i = 0; i < 10; i++) {
Data<Number,Number> data = new Data(x += random.nextDouble(), y+=random.nextDouble());
series.getData().add(data);
}
lineChart.getData().add(series);
lineChart.setTitle("Random Data");
lineChart.setAxisSortingPolicy(SortingPolicy.NONE);
Scene scene = new Scene(lineChart,1200,600);
Stage stage = new Stage();
stage.setScene(scene);
stage.show();
// This resizes the first data point directly (this resize is displayed correctly when program is run)
Node node = series.getData().get(0).getNode();
setSize((StackPane)node,20);
// The context menu is invoked by a right click on the line Chart. It will resize the data point based on a context menu pick
// this resize does not work....unless I resize the window manually which causes a refresh/re-layout of the chart).
lineChart.setOnMouseClicked(new EventHandler<MouseEvent>() {
#Override public void handle(MouseEvent mouseEvent) {
if (MouseButton.SECONDARY.equals(mouseEvent.getButton())) {
Scene scene = ((Node)mouseEvent.getSource()).getScene();
ContextMenu menu = createMenu(lineChart);
menu.show(scene.getWindow(), mouseEvent.getScreenX(), mouseEvent.getScreenY());
}
}
});
}
private void setSize(StackPane sp, int size) {
sp.setMinSize(size, size);
sp.setMaxSize(size, size);
sp.setPrefSize(size, size);
}
// this creates a context menu that will allow you to resize all the data point nodes
private ContextMenu createMenu(LineChart<Number,Number> lineChart) {
final ContextMenu contextMenu = new ContextMenu();
final MenuItem resize = new MenuItem("Resize ALL the points");
resize.setOnAction(new EventHandler<ActionEvent>() {
#Override public void handle(ActionEvent event) {
for (Series<Number, Number> series : lineChart.getData()) {
for (Data<Number, Number> data : series.getData()) {
StackPane sp = (StackPane)data.getNode();
setSize (sp, 20);
// The above resizes do not take effect unless/until I manually resize the chart.
// the following two calls do not do anything;
sp.applyCss();
sp.layout();
// The request to layout the node does nothing
sp.requestLayout();
// Doing both of the above as runLaters does nothing
Platform.runLater(()->{sp.applyCss();sp.layout();});
Platform.runLater(()->{sp.requestLayout();});
// Going after the parent does nothing either
Group group = (Group)sp.getParent();
group.applyCss();
group.layout();
group.requestLayout();
// Going after the parent in a run later does nothing
Platform.runLater(()->{
group.applyCss();
group.layout();
group.requestLayout();
});
// note... doing a resize [commented out below] will work-ish.
// The documentation says NOT to use it thought that as it is for internal use only.
// By work-ish, the data points are enlarged BUT they are no longer centered on the data point
// When I resize the stage they get centered again - so this "solves" my original problem but causes a different problem
////////////////////////////////////
// sp.resize(20, 20); // Uncomment this line to see how it mostly works but introduces a new issue
////////////////////////////////////
}
}
}
});
contextMenu.getItems().add(resize);
return contextMenu;
}
public static void main(String[] args) {
Application.launch(args);
}
}
You can force a relayout by using e.g. an inner class
class LineChartX<X, Y> extends LineChart<X, Y>
{
public LineChartX(#NamedArg("xAxis") Axis<X> xAxis, #NamedArg("yAxis") Axis<Y> yAxis)
{
super(xAxis, yAxis);
}
#Override
public void layoutPlotChildren()
{
super.layoutPlotChildren();
}
}
and calling
lineChart.layoutPlotChildren();
in your menu action.
Simple one-line Solution:
nodes in LineChart scene graph have these parent-child relationships:
Pane chartContent - Group plotArea - Group plotContent - Path seriesLine;
layout requests for Group plotArea, defined in class XYChart, are suppressed:
private final Group plotArea = new Group(){
#Override public void requestLayout() {} // suppress layout requests
};
but Pane chartContent accepts layout requests:
Node node = series.getNode();
if (node instanceof Path) {
Path seriesLine = (Path) node;
Parent parent = seriesLine.getParent();
if (parent instanceof Group) {
Group plotContent = (Group) parent;
parent = plotContent.getParent();
if (parent instanceof Group) {
Group plotArea = (Group) parent;
parent = plotArea.getParent();
if (parent instanceof Pane) {
Pane chartContent = (Pane) parent;
chartContent.requestLayout();
}
}
}
}
so relayout of your chart can be forced by addding this single line
series.getNode().getParent().getParent().getParent().requestLayout();
to the end of your menu action handler.
You don't need to cast that node into a StackPane and set sizes. You need to use the setScaleX() and setScaleY() Methods
Node node = series.getData().get(0).getNode();
node.setScaleX(20);
node.setScaleY(20);
#Override
public void handle(ActionEvent event) {
for (Series<Number, Number> series : lineChart.getData()) {
for (Data<Number, Number> data : series.getData()) {
Node node = data.getNode();
node.setScaleY(20);
node.setScaleX(20);
}
}
}
#c0der posted a solution in comment form to my original post which worked but produced a runtime warning (in Eclipse). His solution was to add a dummy style sheet at the lineChart level with lineChart.getStylesheets().add(""); after the for loops ended. This code produced the warning "Apr 28, 2020 9:01:12 AM com.sun.javafx.css.StyleManager loadStylesheetUnPrivileged WARNING: Resource "" not found."
What did work without causing a run-time warning was to load an empty .css file and add it as a StyleSheet:
lineChart.getStylesheets().add(CSS.class.getResource("Empty.css").toExternalForm());
// note: I keep my .css resource files at the same location as my CSS class
// which is why I have the code "CSS.class" above
This one line solution worked once but I doubted it would work multiple times. I tested it to by increasing the size of the StackPane by 5 for each successive time "Resize ALL the points" was selected (in my dummy code above). Sure enough, it only worked the first time.
However, I added the no-op code lineChart.getStylesheets().replaceAll((s)->s+" "); before that one line solution and now it works multiple times in a row.
No matter how many times I executed the two lines of code
lineChart.getStylesheets().replaceAll((s)->s+" ");
lineChart.getStylesheets().add(CSS.class.getResource("Empty.css").toExternalForm()); `
it (1) worked and (2) The size of the list of lineChart StyleSheets did not grow beyond a size of 1. So a solution with a mystery.
Note: if you have an existing style sheet (I did not in my dummy example above) lineChart.getStylesheets().replaceAll((s)->s+" "); by itself may work as well. For some reason lineChart.getStylesheets().replaceAll((s)->s); without adding the " " on the end did not work.
Note: I originally thought I would have to code up a toggle solution to add Empty.css and the remove Empty.css with alternate calls but that proved unnecessary.
Bottom Line: if you have an existing StyleSheet lineChart.getStylesheets().replaceAll((s)->s+" "); works. If you do not have an existing StyleSheet adding an empty .css file as a StyleSheet in conjunction with the above replaceAll works.
Thanks again to #c0der for his original approach.

JavaFX ComboBox becomes unclickable after removal and re-adding

I think I perhaps have found a bug in Java, or maybe I am doing something wrong.
I populate a container based on some received data. The container has one or more ComboBoxes. On ComboBox selection change I receive new data. I then clear the GridPane and re-add the nodes (that still exist in the new data, and/or add new nodes).
The ComboBox still has focus, but I am unable to activate it again on click. Anything which causes the ComboBox to lose focus (such as focusing another component) will cause it to work again.
This is an simplified example. Tried with jdk1.8.0_162 and jdk-9.0.4
import javafx.application.Application;
import javafx.application.Platform;
import javafx.scene.Scene;
import javafx.scene.control.ComboBox;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
public class ComboBoxTest extends Application {
public static void main(String[] args) {
ComboBoxTest.launch(args);
}
#Override
public void start(Stage stage) {
VBox root = new VBox();
final ComboBox<String> choices = new ComboBox<>();
choices.getItems().add("Test1");
choices.getItems().add("Test2");
root.getChildren().add(choices);
choices.getSelectionModel().selectedItemProperty().addListener(
(observable, oldValue, newValue) -> {
root.getChildren().clear();
root.getChildren().add(choices);
});
Platform.setImplicitExit(true);
stage.setScene(new Scene(root));
stage.show();
}
}
The design is dynamic. I have a list of values received from a server. This is used to create and place ComboBox on a grid. When the user changes a selection in a ComboBox it receive a new list of values from the server. This list may still contain values that corresponds to existing nodes in the grid. They are reused rather than re-created.
Just to not loose reason and solution posted as comment to the deleted answer by sillyfly (post your own and I'll delete this :)
A little guess as to the underlying cause/issue - the change causes the ComboBox to disappear while its list (which is technically a different stage) is showing. My guess is that leaves it in an indefinite state where it thinks the list is still showing, but it never hides so it doesn't reset. In this case, maybe calling ComboBox::hide will also work
This assumption is correct as you can see if you change the selection by keyboard (in which case the dropdown is not open): the combo is still accessible by keyboard and mouse. So hiding the dropdown before removing indeed is the solution.
In code (the simplified example in the Michael's edit)
public class ReaddFocusedCombo extends Application {
#Override
public void start(Stage stage) {
VBox root = new VBox();
final ComboBox<String> choices = new ComboBox<>();
choices.getItems().add("Test1");
choices.getItems().add("Test2");
root.getChildren().add(choices);
choices.getSelectionModel().selectedItemProperty().addListener((observable, oldValue, newValue) -> {
// guess by sillyfly: combo gets confused if popup still open
choices.hide();
root.getChildren().clear();
root.getChildren().add(choices);
// suggested in answer: working but then the choice isn't focused
//root.requestFocus();
// doesn't work
// choices.requestFocus();
});
stage.setScene(new Scene(root));
stage.show();
}
public static void main(String[] args) {
launch(args);
}
}
Update: a little search in the bug parade turned up a similar misbehaviour on adding a showing combo which was fixed on initial attaching to a scene, but missed the dynamic use case. Filed a new issue for the latter.

Trying to add Items to a Dialog, but they don't get displayed

First time asker, so please excuse any missing/wrong-placed Information and of course my bad english. Gotta say that as a german. ;)
Simple and short, I try to add Nodes into a Dialog by using
dialogPane.getChildren.add()
Code:
The Class signature:
public class VerteilDialog extends Dialog<Void>
Declaration of the needed items:
private Label _jlVersionNum;
private TextField _tfVersionNum;
private Label _jlSonstiges;
private TextArea _taSonstiges;
ButtonType btAbbrechen = new ButtonType("Abbrechen", ButtonData.CANCEL_CLOSE);
ButtonType btOk = new ButtonType("Ok", ButtonData.OK_DONE);
getDialogPane().getButtonTypes().add(btAbbrechen);
getDialogPane().getButtonTypes().add(btOk);``
And initialization of those in the contructor:
_jlVersionNum = new Label("Versionsnummer:");
_tfVersionNum = new TextField("4.10.x");
_jlSonstiges = new Label("Sonstiges:");
_taSonstiges = new TextArea();
_jlEinDatum = new Label("Einsatz am:");
And I add the Items like this:
getDialogPane().getChildren().addAll(_tfEinDatum, _jlVersionNum, _tfVersionNum, _jlSonstiges, _taSonstiges);
and Show the dialog
showAndWait();
Calling the Dialog happens here:
import javafx.application.Application;
import javafx.stage.Stage;
public class MainKlasse extends Application {
public static void main(String[] args) {
Application.launch(args);
}
#Override
public void start(Stage arg0) throws Exception {
new VerteilDialog();
}
}
I'd expect the items to be added properly without further Problems, but (and that's why I am actually here):
The Dialog is displayed without further Errors or any exceptions, but it's empty apart from the Buttons, "Ok" and "Abbrechen".
Edit: Added call of showAndWait(); that I forgot while writing the question.
After a bit of tinkering, I found a solution.
getDialogPane().getChildren.addAll() does not add Nodes so that they get visible. One should use a Pane to add Nodes to and set the Pane as Content by using getDialogPane().setContent

JavaFX Listview Observe Map

I have a HashMap. I want to display the Keys in a ListView.
The trouble is, ListView.setItems() wants an ObservableList, and all I have is a keySet().
How can I get a ListView to observe the keys in my Map, without doing something clunky like maintaining two matching data structures?
I know this is not the answer you want, but... my advice is to maintain two data structures.
Sample App (synching data structures)
Add items to a map using the UI on the right and, as the keys in the extension -> mimeType map change, you will see the list of keys shown in the ListView on the left automatically update.
The solution listens for changes to an ObservableMap which wraps the extension -> mimetype map and, when a key in the map changes, applies relevant updates to an ObservableList which backs the ListView.
In the sample screenshot below, png will be added to the left hand side list when the user presses the Add button.
import javafx.application.Application;
import javafx.collections.*;
import javafx.geometry.Insets;
import javafx.scene.Scene;
import javafx.scene.control.*;
import javafx.scene.layout.*;
import javafx.stage.Stage;
import java.util.Arrays;
import java.util.Map;
import java.util.stream.Collectors;
public class ObservableMapTest extends Application {
// map initializer based on http://stackoverflow.com/a/25829097/1155209
private static final Map<String, String> extensionToMimeMap =
Arrays.stream(new String[][]{
{"txt", "text/plain"},
{"html", "text/html"},
{"js", "application/javascript"},
{"css", "text/css"}
}).collect(Collectors.toMap(kv -> kv[0], kv -> kv[1]));
#Override
public void start(Stage stage) throws Exception {
// create an observable wrapper for our map data.
final ObservableMap<String, String> observableExtensionToMimeMap = FXCollections.observableMap(
extensionToMimeMap
);
// create an ListView based on key items in the map.
ListView<String> extensionListView = new ListView<>();
extensionListView.getItems().setAll(extensionToMimeMap.keySet());
extensionListView.setPrefWidth(100);
// have the ListView observe the underlying map and modify its items if the key set changes.
observableExtensionToMimeMap.addListener((MapChangeListener<String, String>) change -> {
extensionListView.getItems().removeAll(change.getKey());
if (change.wasAdded()) {
extensionListView.getItems().add(change.getKey());
}
});
// layout the app.
Pane layout = new HBox(
extensionListView,
createAddExtensionPane(
observableExtensionToMimeMap
)
);
layout.setPrefHeight(150);
// display the app.
stage.setScene(new Scene(layout));
stage.show();
}
/** Helper factory function to create a UI for adding an element to an map. */
private GridPane createAddExtensionPane(Map<String, String> map) {
GridPane addExtensionPane = new GridPane();
addExtensionPane.add(new Label("Extension:"), 0, 0);
TextField extensionField = new TextField();
addExtensionPane.add(extensionField, 1, 0);
addExtensionPane.add(new Label("Mime Type:"), 0, 1);
TextField mimeTypeField = new TextField();
addExtensionPane.add(mimeTypeField, 1, 1);
Button addButton = new Button("Add");
addButton.setOnAction(event ->
map.put(
extensionField.getText(),
mimeTypeField.getText()
)
);
addExtensionPane.add(addButton, 1, 2);
addExtensionPane.setPadding(new Insets(10));
addExtensionPane.setHgap(5);
addExtensionPane.setVgap(10);
return addExtensionPane;
}
public static void main(String[] args) {
launch(args);
}
}
A possible alternate implementation which did not copy data into an ObservableList would be to implement the ObservableList interface and in the implemented methods refer directly to the key data of the observable map. Such an approach would be very complex to implement and not worthwhile pursuing (IMO).

How to queue some part of a program

I apologize for the topic headline, it might not exactly express my thought but i'll give it a try. If someone knows what's the better headline, please suggest an edit.
So i'd like to create rectangles and give values for them after the button has been pressed. Everything's plain and simple if i know how many rectangles i want to create. Here's where thing gets complicated - i get the rectangle count after i've pressed the button.
I'll explain with an example, so it's a bit more clear:
final ArrayList rectList = new ArrayList();
btn.setOnAction(new EventHandler<ActionEvent>() {
public void handle(final ActionEvent event) {
ArrayList getFromMethodAnArrayList = methodWhichReturnsAnArrayList();
for (int i = 0; i<getFromMethodAnArrayList.size();i++){
rectList.add(new Rectangle(0,0,0,30));
}
}
});
HBox box1 = new HBox(1);
for (int i = 0; i<rectList.size();i++){
box1.getChildren().add(rectList.get(i));
}
This code gives an error because when first loaded the rectList is empty. How could i queue adding elements into HBox, so it would be performed after the rectList has been valued.
Recommendation
You don't need a queue here and you don't need to multi-thread either, at least as you have currently described your question - additional requirements on the implementation could imply that the use of both of those things are necessary.
Sample code
What the sample code does is define a source of items which are model data for something you want to display. When you click on the create button, it will generate a random number of new items with random data values for each item. These items will be placed in a queue and a subsequent routine will take the items from the queue, read their data values and create appropriate visual representations (rectangles) for the item data. It uses a queue data structure, but a simple array or list would have worked just fine.
import javafx.application.Application;
import javafx.scene.*;
import javafx.scene.control.*;
import javafx.scene.layout.*;
import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle;
import javafx.stage.Stage;
import java.util.ArrayDeque;
import java.util.Queue;
import java.util.Random;
// java 8 code
public class RectangleAddition extends Application {
private final Random random = new Random(42);
public static void main(String[] args) {
launch(args);
}
public void start(Stage stage) {
FlowPane flow = createItemContainer();
ScrollPane scroll = makeContainerScrollable(flow);
ItemSource itemSource = new ItemSource();
Button create = createItemControl(flow, scroll, itemSource);
VBox layout = createLayout(create, scroll);
Scene scene = new Scene(layout);
stage.setScene(scene);
stage.show();
}
private FlowPane createItemContainer() {
FlowPane flow = new FlowPane();
flow.setHgap(5);
flow.setVgap(5);
return flow;
}
/**
* The control will
* retrieve items from the source,
* add them to the scrollable pane,
* scroll the pane to the bottom on each addition.
*/
private Button createItemControl(Pane flow, ScrollPane scroll, ItemSource itemSource) {
Button create = new Button("Create Rectangles (keep pressing to create more)");
create.setOnAction(event -> {
addRectangles(flow, itemSource);
scroll.setVvalue(scroll.getVmax());
});
return create;
}
private VBox createLayout(Button create, ScrollPane scroll) {
VBox layout = new VBox(10, create, scroll);
layout.setStyle("-fx-padding: 10px;");
layout.setPrefSize(300, 300);
VBox.setVgrow(scroll, Priority.ALWAYS);
create.setMinHeight(Button.USE_PREF_SIZE);
return layout;
}
/**
* fetches some items from the source,
* creates rectangle nodes for them
* adds them to the container.
*/
private void addRectangles(Pane container, ItemSource itemSource) {
Queue<Item> items = itemSource.fetchNextItems();
while (!items.isEmpty()) {
Item item = items.remove();
Node rectangle = createRectangle(item);
container.getChildren().add(rectangle);
}
}
private Rectangle createRectangle(Item item) {
Rectangle rectangle = new Rectangle(item.size, item.size, item.color);
rectangle.setRotate(item.rotation);
return rectangle;
}
private ScrollPane makeContainerScrollable(FlowPane flow) {
ScrollPane scroll = new ScrollPane(flow);
scroll.setFitToWidth(true);
scroll.setMaxSize(Double.MAX_VALUE, Double.MAX_VALUE);
return scroll;
}
/** some model data for application items */
class Item {
// item will be colored according to rgb values from the (inclusive) range
// MIN_COLOR_VALUE to MIN_COLOR_VALUE + COLOR_RANGE - 1
private static final int MIN_COLOR_VALUE = 50;
private static final int COLOR_RANGE = 201;
// item will be sized from the (inclusive) range
// MIN_SIZE to MIN_SIZE + SIZE_RANGE - 1
private static final int MIN_SIZE = 5;
private static final int SIZE_RANGE = 21;
// item will be (z-axis) rotated from the (inclusive) range
// - ROTATE_SCOPE to + ROTATE_SCOPE
private static final int ROTATE_SCOPE = 10;
private Color color;
private int size;
private int rotation;
public Item() {
color = Color.rgb(
createColorComponent(),
createColorComponent(),
createColorComponent()
);
size = random.nextInt(SIZE_RANGE) + MIN_SIZE;
rotation = random.nextInt(ROTATE_SCOPE * 2 + 1) - ROTATE_SCOPE;
}
private int createColorComponent() {
return random.nextInt(COLOR_RANGE) + MIN_COLOR_VALUE;
}
}
/** a never-ending source of new items fetched in batches */
class ItemSource {
// will fetch between 1 and MAX_NUM_ITEMS_PER_FETCH (inclusive) items on each fetch call.
private static final int MAX_NUM_ITEMS_PER_FETCH = 5;
public Queue<Item> fetchNextItems() {
int numItems = random.nextInt(MAX_NUM_ITEMS_PER_FETCH) + 1;
Queue<Item> queue = new ArrayDeque<>(numItems);
for (int i = 0; i < numItems; i++) {
queue.add(new Item());
}
return queue;
}
}
}
Thoughts On Multithreading
Where you might want a different implementation which does actually use multi-threading is if the item creation or fetching from the item source takes a long time. For example you need to read the item data from a network, database or very large file. If you don't multi-thread such things, then you will end up freezing the UI while it waits for the I/O to complete. A general rule is if the operation you are performing will finish in less than a sixtieth of a millisecond, then you can do it on the JavaFX UI thread without any issue as there will be no visible lag and stuttering in the UI, but if it takes longer than that then you should use concurrency utilities (which are more tricky to use than single-threaded code).
Java has numerous threading mechanisms, which can be used, but you in many cases, using the JavaFX specific concurrency extensions is the best way to integrate multi-threaded code into your JavaFX application.
The appropriate concurrency utility to use usually would be the JavaFX Task or Service interfaces if you are doing this on demand from the UI. You can read the documentation for these facilities which demonstrates sample code for doing things like "a task which returns partial results" (which is a bit similar to your question).
If the thing which provides the items to be consumed is some background long running network task to which items are pushed, rather than pulled on demand, then running it in it's own thread and calling back into the JavaFX to signal a UI update via platform.runLater() is the way to go. Another data structure which can aid in such cases is a BlockingQueue as demonstrated in this multi-chart creation code - but that is quite a sophisticated solution.
I guess part of my point is that you may not need to use these concurrency utilities for your situation, you need to evaluate it on a case by case basis and use the most appropriate solution.
I think you can simplify your code quite a bit here by getting rid of the ArrayList and populating box1 when the button event happens:
final HBox box1 = new HBox(1);
btn.setOnAction(new EventHandler<ActionEvent>() {
public void handle(final ActionEvent event) {
ArrayList getFromMethodAnArrayList = methodWhichReturnsAnArrayList();
for (int i = 0; i<getFromMethodAnArrayList.size();i++){
box1.getChildren().add(new Rectangle(0,0,0,30));
}
}
});
If it is concurrency that you are interested in, it would be good to read Concurrency in JavaFX, although I don't think that is the right solution for the question you posted.

Categories