I want a draw a line in a Barchart. the line reflects a certain value to which other values are compared. I applied the solution for a LineChart to my situation (see code). However, no line is drawn.
public XYChart buildBarChart ()
{
Line valueMarker = new Line ();
CategoryAxis xAxis = new CategoryAxis();
NumberAxis yAxis = new NumberAxis();
BarChart<String,Number> barChart = new BarChart<String,Number> (xAxis,yAxis);
barChart.setBarGap (-10);
barChart.setCategoryGap (0);
barChart.getYAxis().setTickLabelsVisible(false);
barChart.getYAxis().setOpacity(0);
yAxis.setLabel ("Score");
// update marker
Node chartArea = barChart.lookup (".chart-plot-background");
Bounds chartAreaBounds = chartArea.localToScene (chartArea.getBoundsInLocal ());
// remember scene position of chart area
yShift = chartAreaBounds.getMinY();
// set x parameters of the valueMarker to chart area bounds
valueMarker.setStartX (chartAreaBounds.getMinX());
valueMarker.setEndX (chartAreaBounds.getMaxX());
// find pixel position of that value
double displayPosition = yAxis.getDisplayPosition (valNederland);
// update marker
valueMarker.setStartY (yShift + displayPosition);
valueMarker.setEndY (yShift + displayPosition);
BarChart.Series<String,Number> series1 = new BarChart.Series<String,Number> ();
BarChart.Series<String,Number> series2 = new BarChart.Series<String,Number> ();
BarChart.Series<String,Number> series3 = new BarChart.Series<String,Number> ();
BarChart.Series<String,Number> series4 = new BarChart.Series<String,Number> ();
series1.getData ().add (new XYChart.Data<String,Number> (sRegio, valRegio_2015));
series2.getData ().add (new XYChart.Data<String,Number> (sOmgeving, valOmgeving));
series3.getData ().add (new XYChart.Data<String,Number> (sRest, valRest));
series4.getData ().add (new XYChart.Data<String,Number> (sNl, valNederland));
barChart.getData().addAll (series1, series2, series3, series4);
return barChart;
} /*** buildBarChart ***/
Essential for this situation is to have the coordinates of the ChartArea. When I inspect the properties of ChartArea in the debugger I find that all values of _geomBounds are 'unlogic', i.e. maxX/Y = -1 and minX/Y = -1. I inspect just one line after chartASreaBounds is assigned a value.
To me it seems that the barChart lookup fails as I had a similar problem before. Does anyone have a suggestion of how to correct this situation?
Edit
The BarChart is created in the constructor as follows:
public Charter (int nTimes, int nCategories, float [] years, String [] titles, String label, Indicator ind, float [] past)
{
this ();
// all kind of code to do somethong with the parameters omitted
// ...
// Setting up the BarChart. Note that past == null in my experiments
HBox hBox = new HBox ();
XYChart bar = buildBarChart ();
bar.setPrefHeight (height);
bar.setPrefWidth (width);
VBox buttons = buildButtonView ();
if (past != null)
{
XYChart line = buildLineChart ();
line.setPrefHeight (height);
line.setPrefWidth (width);
hBox.getChildren ().addAll (bar, line);
} else
{
hBox.getChildren ().addAll (bar);
} // if
Label dsecription = new Label (label);
this.getProperties ().put ("--Indicator", ind.getName ());
this.getChildren ().addAll (dsecription, hBox);
} /*** Charter ***/
After a Charter has been created it is added to the VBox and therewith shows on the screen as VBox is already added to the scene.
It looks like you want to calculate the position of the bar chart on the screen before it is added to the scene. Since at that point it is not yet on the screen that position is -1 or not defined.
I tend to make my layouts in fxml, therefore such problems don't appear. If you need to do it in code, I think first adding it to the children of hBox before drawing the line could solve the issue.
Related
I'm working on a JavaFX program that can display different grocery items as a BarChart. My code structure uses an initial loading class, GroceryGrapher.java, a controller, GroceryGrapherController.java, and an .fxml file, GroceryGrapher.fxml.
I'm relatively new to JavaFX and I've tried different approaches to creating and displaying my BarChart. For the most part I've resolved issues and can load the chart with its data and components, but my issue is that the chart won't display / update after the program has started. The BarChart is part of an AnchorPane (which all comes under a StackPane) in my FXML file, and I've tried to update it after initialization since it starts out blank but it remains blank.
The BarChart is part of the AnchorPane, which also has a MenuBar. My goal was to be able to update different parts of the BarChart via the MenuBar. When I load the program, the MenuBar and empty BarChart are loaded:
This is how I initialize my BarChart and its data:
public class GroceryGrapherController implements Initializable {
#FXML
private AnchorPane anchorPane = new AnchorPane();
#FXML
private NumberAxis xAxis = new NumberAxis();
#FXML
private CategoryAxis yAxis = new CategoryAxis();
#FXML
private BarChart<Number, String> groceryBarChart;
#FXML
//private Stage stage;
Parent root;
#Override
public void initialize(URL location, ResourceBundle resources) {
groceryBarChart = new BarChart<Number, String>(xAxis, yAxis);
groceryBarChart.setTitle("Horizontal Grocery List");
xAxis.setLabel("Number");
xAxis.setTickLabelRotation(90);
yAxis.setLabel("Grocery Type");
// Populate BarChart
XYChart.Series<Number, String> series1 = new XYChart.Series<>();
series1.setName("Chocolates");
series1.getData().add(new XYChart.Data<Number, String>(25601.34, "Reese"));
series1.getData().add(new XYChart.Data<Number, String>(20148.82, "Cadbury"));
series1.getData().add(new XYChart.Data<Number, String>(10000, "Mars"));
series1.getData().add(new XYChart.Data<Number, String>(35407.15, "Snickers"));
series1.getData().add(new XYChart.Data<Number, String>(12000, "Lindt"));
XYChart.Series<Number, String> series2 = new XYChart.Series<>();
series2.setName("Vegetables");
series2.getData().add(new XYChart.Data<Number, String>(57401.85, "Cabbage"));
series2.getData().add(new XYChart.Data<Number, String>(41941.19, "Lettuce"));
series2.getData().add(new XYChart.Data<Number, String>(45263.37, "Tomato"));
series2.getData().add(new XYChart.Data<Number, String>(117320.16, "Eggplant"));
series2.getData().add(new XYChart.Data<Number, String>(14845.27, "Beetroot"));
XYChart.Series<Number, String> series3 = new XYChart.Series<>();
series3.setName("Drinks");
series3.getData().add(new XYChart.Data<Number, String>(45000.65, "Water"));
series3.getData().add(new XYChart.Data<Number, String>(44835.76, "Coke"));
series3.getData().add(new XYChart.Data<Number, String>(18722.18, "Sprite"));
series3.getData().add(new XYChart.Data<Number, String>(17557.31, "Mountain Dew"));
series3.getData().add(new XYChart.Data<Number, String>(92633.68, "Beer"));
// Need to do this because of a bug with CategoryAxis... manually set categories
yAxis.setCategories(FXCollections.observableArrayList("Reese", "Cadbury", "Mars", "Snickers", "Lindt", "Cabbage", "Lettuce",
"Tomato", "Eggplant", "Beetroot", "Water", "Coke", "Sprite", "Mountain Dew", "Beer"));
groceryBarChart.getData().addAll(series1, series2, series3);
}
Now, I have an item:
In my MenuBar to load the BarChart (since it initially appears empty), which utilizes the a method I created, loadGraph. I've tried different methods to get my updated BarChart to show up with no major success. My initial idea was to use setScene in the loadGraph method to set the scene to the BarChart, as shown in this example.
Stage stage = (Stage) anchorPane.getScene().getWindow();
stage.setScene(new Scene(groceryBarChart, 800, 600));
While that did work, it naturally replaced the scene to solely consist of the BarChart, removing my MenuBar and previous GUI:
I saw this question which had a similar issue (I think) but I wasn't really sure how to apply it to my program. I got this code:
try {
FXMLLoader fxmlLoader = new FXMLLoader(getClass().getResource("GroceryGrapher.fxml"));
root = (Parent) fxmlLoader.load();
GroceryGrapherController ggController = fxmlLoader.getController();
} catch (Exception e) {
e.printStackTrace();
}
I copied over my initialization from the initialize method to a new method and tried calling it with ggController but it didn't change anything. I also tried creating a new method, updateData:
#FXML
private void updateData() {
Platform.runLater(() -> {
groceryBarChart.setTitle("Horizontal Grocery List");
});
}
I tried calling it with ggController in the loadGraph method but my BarChart still didn't change.
I know I'm doing something wrong, but I'm not very familiar with the BarChart class and JavaFX in too much depth, so I figured I would come here to ask for help. I need to update the existing BarChart in my GUI so that my data can load without replacing the entire scene, in order for the MenuBar to remain visible in the scene able to perform functions. How would I go about altering the BarChart component part of my AnchorPane, updating the data as necessary, without completely replacing the scene as I did previously?
Thank you very much and sorry if this question is bad or my coding practices are wrong. All feedback would be much appreciated!
EDIT
Just in case it might help, here is my FXML code for the BarChart.
<BarChart fx:id="groceryBarChart" layoutY="31.0" prefHeight="566.0" prefWidth="802.0">
<xAxis>
<CategoryAxis fx:id="yAxis" side="BOTTOM" />
</xAxis>
<yAxis>
<NumberAxis side="LEFT" fx:id="xAxis" />
</yAxis>
</BarChart>
Most of the problems seem to be resolved, so I'll comment on this part only:
Ohh, I see. I increased the height and it seems to be thicker, thank you for your comment, that's very interesting behavior from the library. So I shouldn't use the approach I'm using?
Well, it depends. There is no other way to treat each data series as a separate list of categories in BarChart (at least, I'm not aware of any), so if you want to have groups of categories, each group with its own color, you'll have to use the same method as in your example.
The alternatives are to write your own implementation of bar chart layoutPlotChildren() method which will show only one bar per category (the only one if there are no two data series that contain the same category), or to find another chart library with those features. Of couse, you can always choose to show your data in different format (one data series for each month).
If you plan to use your original method, you'll have to adjust vertical position of bars (to center them again), and you'll have to fix bar height problem too:
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.Node;
import javafx.scene.chart.*;
import java.net.URL;
import java.util.ResourceBundle;
public class GroceryGrapherController implements Initializable {
#FXML
private NumberAxis xAxis;
#FXML
private CategoryAxis yAxis;
#FXML
private BarChart<Number, String> groceryBarChart;
//the height of one bar in the chart (if it can fit category area); you can pick another value
private final double barHeight = 10;
#Override
public void initialize(URL location, ResourceBundle resources) {
groceryBarChart.setTitle("Horizontal Grocery List");
//there's only one bar per category, so remove the gap (don't edit this, it's used in calculations)
groceryBarChart.setBarGap(0);
//gap between categories; you can pick another value
groceryBarChart.setCategoryGap(5);
//there're some bugs with the chart layout not updating when this is enabled, so disable it
groceryBarChart.setAnimated(false);
xAxis.setLabel("Number");
xAxis.setTickLabelRotation(90);
yAxis.setLabel("Grocery Type");
//if category height changes (ex. when you resize the chart), bar positions and height need to be recalculated
yAxis.categorySpacingProperty().addListener((observable, oldValue, newValue) ->
recalculateBarPositionsAndHeight()
);
//your original data
// Populate BarChart
final XYChart.Series<Number, String> series1 = new XYChart.Series<>();
series1.setName("Chocolates");
series1.getData().addAll(
new XYChart.Data<>(25601.34, "Reese"),
new XYChart.Data<>(20148.82, "Cadbury"),
new XYChart.Data<>(10000, "Mars"),
new XYChart.Data<>(35407.15, "Snickers"),
new XYChart.Data<>(12000, "Lindt")
);
final XYChart.Series<Number, String> series2 = new XYChart.Series<>();
series2.setName("Vegetables");
series2.getData().addAll(
new XYChart.Data<>(57401.85, "Cabbage"),
new XYChart.Data<>(41941.19, "Lettuce"),
new XYChart.Data<>(45263.37, "Tomato"),
new XYChart.Data<>(117320.16, "Eggplant"),
new XYChart.Data<>(14845.27, "Beetroot")
);
final XYChart.Series<Number, String> series3 = new XYChart.Series<>();
series3.setName("Drinks");
series3.getData().addAll(
new XYChart.Data<>(45000.65, "Water"),
new XYChart.Data<>(44835.76, "Coke"),
new XYChart.Data<>(18722.18, "Sprite"),
new XYChart.Data<>(17557.31, "Mountain Dew"),
new XYChart.Data<>(92633.68, "Beer")
);
groceryBarChart.getData().addAll(series1, series2, series3);
}
private void recalculateBarPositionsAndHeight() {
final int seriesCount = groceryBarChart.getData().size();
final double categoryHeight = yAxis.getCategorySpacing() - groceryBarChart.getCategoryGap();
final double originalBarHeight = categoryHeight / seriesCount;
final double barTranslateY = originalBarHeight * (seriesCount - 1) / 2;
//find the (bar) node associated with each series data and adjust its size and position
groceryBarChart.getData().forEach(numberStringSeries ->
numberStringSeries.getData().forEach(numberStringData -> {
Node node = numberStringData.getNode();
//calculate the vertical scaling of the node, so that its height matches "barHeight"
//or maximum height available if it's too big
double scaleY = Math.min(barHeight, categoryHeight) / originalBarHeight;
node.setScaleY(scaleY);
//translate the node vertically to center it around the category label
node.setTranslateY(barTranslateY);
})
);
}
}
I chose fixed bar height, but you don't need to (in which case you'll have to edit recalculateBarPositionsAndHeight method to use originalBarHeight).
I have a sample 3D application (built by taking reference from the Javafx sample 3DViewer) which has a table created by laying out Boxes and Panes:
The table is centered wrt (0,0,0) coordinates and camera is at -z position initially.
It has the zoom-in/out based on the camera z position from the object.
On zooming in/out the object's boundsInParent increases/decreases i.e. area of the face increases/decreases. So the idea is to put more text when we have more area (always confining within the face) and lesser text or no text when the face area is too less. I am able to to do that using this node hierarchy:
and resizing the Pane (and managing the vBox and number of texts in it) as per Box on each zoom-in/out.
Now the issue is that table boundsInParent is giving incorrect results (table image showing the boundingBox off at the top) whenever a text is added to the vBox for the first time only. On further zooming-in/out gives correct boundingBox and does not go off.
Below is the UIpane3D class:
public class UIPane3D extends Pane
{
VBox textPane;
ArrayList<String> infoTextKeys = new ArrayList<>();
ArrayList<Text> infoTextValues = new ArrayList<>();
Rectangle bgCanvasRect = null;
final double fontSize = 16.0;
public UIPane3D() {
setMouseTransparent(true);
textPane = new VBox(2.0)
}
public void updateContent() {
textPane.getChildren().clear();
getChildren().clear();
for (Text textNode : infoTextValues) {
textPane.getChildren().add(textNode);
textPane.autosize();
if (textPane.getHeight() > getHeight()) {
textPane.getChildren().remove(textNode);
textPane.autosize();
break;
}
}
textPane.setTranslateY(getHeight() / 2 - textPane.getHeight() / 2.0);
bgCanvasRect = new Rectangle(getWidth(), getHeight());
bgCanvasRect.setFill(Color.web(Color.BURLYWOOD.toString(), 0.10));
bgCanvasRect.setVisible(true);
getChildren().addAll(bgCanvasRect, textPane);
}
public void resetInfoTextMap()
{
if (infoTextKeys != null || infoTextValues != null)
{
try
{
infoTextKeys.clear();
infoTextValues.clear();
} catch (Exception e){e.printStackTrace();}
}
}
public void updateInfoTextMap(String pKey, String pValue)
{
int index = -1;
boolean objectFound = false;
for (String string : infoTextKeys)
{
index++;
if(string.equals(pKey))
{
objectFound = true;
break;
}
}
if(objectFound)
{
infoTextValues.get(index).setText(pValue.toUpperCase());
}
else
{
if (pValue != null)
{
Text textNode = new Text(pValue.toUpperCase());
textNode.setFont(Font.font("Consolas", FontWeight.BLACK, FontPosture.REGULAR, fontSize));
textNode.wrappingWidthProperty().bind(widthProperty());
textNode.setTextAlignment(TextAlignment.CENTER);
infoTextKeys.add(pKey);
infoTextValues.add(textNode);
}
}
}
}
The code which get called at the last after all the manipulations:
public void refreshBoundingBox()
{
if(boundingBox != null)
{
root3D.getChildren().remove(boundingBox);
}
PhongMaterial blueMaterial = new PhongMaterial();
blueMaterial.setDiffuseColor(Color.web(Color.CRIMSON.toString(), 0.25));
Bounds tableBounds = table.getBoundsInParent();
boundingBox = new Box(tableBounds.getWidth(), tableBounds.getHeight(), tableBounds.getDepth());
boundingBox.setMaterial(blueMaterial);
boundingBox.setTranslateX(tableBounds.getMinX() + tableBounds.getWidth()/2.0);
boundingBox.setTranslateY(tableBounds.getMinY() + tableBounds.getHeight()/2.0);
boundingBox.setTranslateZ(tableBounds.getMinZ() + tableBounds.getDepth()/2.0);
boundingBox.setMouseTransparent(true);
root3D.getChildren().add(boundingBox);
}
Two things:
The table3D's boundsInParent is not updated properly when texts are added for the first time.
What would be the right way of putting texts on 3D nodes? I am having to manipulate a whole lot to bring the texts as required.
Sharing code here.
For the first question, about the "jump" that can be noticed just when after scrolling a new text item is laid out:
After digging into the code, I noticed that the UIPane3D has a VBox textPane that contains the different Text nodes. Every time updateContent is called, it tries to add a text node, but it checks that the vbox's height is always lower than the pane's height, or else the node will be removed:
for (Text textNode : infoTextValues) {
textPane.getChildren().add(textNode);
textPane.autosize();
if (textPane.getHeight() > getHeight()) {
textPane.getChildren().remove(textNode);
textPane.autosize();
break;
}
}
While this is basically correct, when you add a node to the scene, you can't get textPane.getHeight() immediately, as it hasn't been laid out yet, and you have to wait until the next pulse. This is why the next time you scroll, the height is correct and the bounding box is well placed.
One way to force the layout and get the correct height of the textNode is by forcing css and a layout pass:
for (Text textNode : infoTextValues) {
textPane.getChildren().add(textNode);
// force css and layout
textPane.applyCss();
textPane.layout();
textPane.autosize();
if (textPane.getHeight() > getHeight()) {
textPane.getChildren().remove(textNode);
textPane.autosize();
break;
}
}
Note that:
This method [applyCss] does not normally need to be invoked directly but may be used in conjunction with Parent.layout() to size a Node before the next pulse, or if the Scene is not in a Stage.
For the second question, about a different solution to add Text to 3D Shape.
Indeed, placing a (2D) text on top of a 3D shape is quite difficult, and requires complex maths (that are done quite nicely in the project, by the way).
There is an alternative avoiding the use of 2D nodes directly.
Precisely in a previous question, I "wrote" into an image, that later on I used as the material diffuse map of a 3D shape.
The built-in 3D Box places the same image into every face, so that wouldn't work. We can implement a 3D prism, or we can make use of the CuboidMesh node from the FXyz3D library.
Replacing the Box in UIPaneBoxGroup:
final CuboidMesh contentShape;
UIPane3D displaypane = null;
PhongMaterial shader = new PhongMaterial();
final Color pColor;
public UIPaneBoxGroup(final double pWidth, final double pHeight, final double pDepth, final Color pColor) {
contentShape = new CuboidMesh(pWidth, pHeight, pDepth);
this.pColor = pColor;
contentShape.setMaterial(shader);
getChildren().add(contentShape);
addInfoUIPane();
}
and adding the generateNet method:
private Image generateNet(String string) {
GridPane grid = new GridPane();
grid.setAlignment(Pos.CENTER);
Label label5 = new Label(string);
label5.setFont(Font.font("Consolas", FontWeight.BLACK, FontPosture.REGULAR, 40));
GridPane.setHalignment(label5, HPos.CENTER);
grid.add(label5, 3, 1);
double w = contentShape.getWidth() * 10; // more resolution
double h = contentShape.getHeight() * 10;
double d = contentShape.getDepth() * 10;
final double W = 2 * d + 2 * w;
final double H = 2 * d + h;
ColumnConstraints col1 = new ColumnConstraints();
col1.setPercentWidth(d * 100 / W);
ColumnConstraints col2 = new ColumnConstraints();
col2.setPercentWidth(w * 100 / W);
ColumnConstraints col3 = new ColumnConstraints();
col3.setPercentWidth(d * 100 / W);
ColumnConstraints col4 = new ColumnConstraints();
col4.setPercentWidth(w * 100 / W);
grid.getColumnConstraints().addAll(col1, col2, col3, col4);
RowConstraints row1 = new RowConstraints();
row1.setPercentHeight(d * 100 / H);
RowConstraints row2 = new RowConstraints();
row2.setPercentHeight(h * 100 / H);
RowConstraints row3 = new RowConstraints();
row3.setPercentHeight(d * 100 / H);
grid.getRowConstraints().addAll(row1, row2, row3);
grid.setPrefSize(W, H);
grid.setBackground(new Background(new BackgroundFill(pColor, CornerRadii.EMPTY, Insets.EMPTY)));
new Scene(grid);
return grid.snapshot(null, null);
}
Now all the 2D related code can be removed (including displaypane), and after a scrolling event get the image:
public void refreshBomUIPane() {
Image net = generateNet(displaypane.getText());
shader.setDiffuseMap(net);
}
where in UIPane3D:
public String getText() {
return infoTextKeys.stream().collect(Collectors.joining("\n"));
}
I've also removed the bounding box to get this picture:
I haven't played around with the number of text nodes that can be added to the VBox, the font size nor with an strategy to avoid generating images on every scroll: only when the text changes this should be done. So with the current approach is quite slow, but it can be improved notably as there are only three possible images for each box.
I want create a app using javafx. It looks like this:
I want to add the zoom function for the chart. When I click the button "Zoom in", the app will become fig2. However, I have no idea to achieve it. When I change the size of pane included the chart, it will change grid pane size, looks like this:
You do not want the zoom to be considered for the gridpane layout. In this can be achieved by applying transforms to the child of the gridpane you want to modify.
The following example demonstrates how to zoom a node while the mouse is hovering over it:
private static Region createRegion(String background) {
Region region = new Region();
region.setStyle("-fx-background-color:"+background);
region.setPrefSize(300, 100);
return region;
}
#Override
public void start(Stage primaryStage) {
GridPane gp = new GridPane();
// create background
gp.add(createRegion("green"), 0, 0);
gp.add(createRegion("dodgerblue"), 0, 1);
// create region to be zoomed
Region zoomRegion = createRegion("red");
GridPane.setFillWidth(zoomRegion, Boolean.FALSE);
GridPane.setFillHeight(zoomRegion, Boolean.FALSE);
zoomRegion.setPrefWidth(100);
Scale scale = new Scale();
zoomRegion.getTransforms().add(scale);
// keep pivot at bottom left corner
scale.pivotYProperty().bind(zoomRegion.heightProperty());
zoomRegion.hoverProperty().addListener((observable, oldValue, newValue) -> {
// adjust scale when hover state is changed
double scaleFactor = newValue ? 1.5 : 1;
scale.setX(scaleFactor);
scale.setY(scaleFactor);
});
gp.add(zoomRegion, 0, 1);
Scene scene = new Scene(gp);
primaryStage.setScene(scene);
primaryStage.show();
}
I have a Pane that contains some VBoxs and Lines. I want to bind the endpoints of the Lines to the "anchors" inside the VBoxs (basically I want to bind to the position of a Node nested arbitrarily deep in the VBox), but I can't figure out what value represents the Nodes position relative to the top Pane. I've tried layout properties, translate properties, as well as bounds in local and bounds in parent, and none of them seem to work. What am I missing?
(I can provide a code sample if needed, but I don't think it helps explain my problem any better since I can't get it to work.)
EDIT: I forgot to mention the VBox can be moved around the pane freely, which is why I need to bind the lines.
EDIT: Here's some source showing my progress. I can get the correct location, but it's not binding
public class Graph extends Application {
private double startX;
private double startY;
private ObjectBinding<Bounds> bounds;
private DoubleBinding tx;
private DoubleBinding ty;
#Override
public void start(Stage primaryStage) throws Exception {
Pane pane = new Pane();
Circle target = new Circle(5, Color.RED);
VBox node = wrap(target);
Line connector = new Line();
bounds = Bindings.createObjectBinding(() -> {
Bounds nodeLocal = target.getBoundsInLocal();
Bounds nodeScene = target.localToScene(nodeLocal);
Bounds nodePane = pane.sceneToLocal(nodeScene);
return nodePane;
},
target.boundsInLocalProperty(),
target.localToSceneTransformProperty(),
pane.localToSceneTransformProperty()
);
connector.setStartX(0);
connector.setStartY(0);
tx = Bindings.createDoubleBinding(() -> bounds.get().getMinX(), bounds);
ty = Bindings.createDoubleBinding(() -> bounds.get().getMinY(), bounds);
connector.endXProperty().bind(tx);
connector.endYProperty().bind(ty);
connector.setStroke(Color.BLACK);
pane.getChildren().add(node);
pane.getChildren().add(connector);
node.relocate(100, 100);
primaryStage.setScene(new Scene(pane, 300, 300));
primaryStage.show();
}
private VBox wrap(Circle target) {
VBox node = new VBox(new Label("Node"), new StackPane(new Rectangle(50, 50, Color.GRAY), target));
node.setOnMousePressed(event -> {
Node source = (Node) event.getSource();
startX = source.getBoundsInParent().getMinX() - event.getScreenX();
startY = source.getBoundsInParent().getMinY() - event.getScreenY();
});
node.setOnMouseDragged(event -> {
Node source = (Node) event.getSource();
double offsetX = event.getScreenX() + startX;
double offsetY = event.getScreenY() + startY;
source.relocate(offsetX, offsetY);
});
return node;
}
}
Given any Node node in a (meaning it has a parent or indirect ancestor) Pane pane, the basic idea is to do
ObjectBinding<Bounds> boundsInPaneBinding = Bindings.createObjectBinding(() -> {
Bounds nodeLocal = node.getBoundsInLocal();
Bounds nodeScene = node.localToScene(nodeLocal);
Bounds nodePane = pane.sceneToLocal(nodeScene);
return nodePane ;
}, node.boundsInLocalProperty(), node.localToSceneTransformProperty(),
pane.localToSceneTransformProperty());
Then boundsInPaneBinding is an ObservableValue<Bounds> that always contains the bounds of the Node in the Pane's coordinate system. So you can then do things like
line.startXProperty().bind(Bindings.createDoubleBinding(
() -> boundsInPaneBinding.get().getMinX(),
boundsInPaneBinding));
The tricky part here is making sure that the bindings don't get garbage collected prematurely. (See here for a discussion.) First, you need to retain a reference to the binding:
private ObjectBinding<Bounds> boundsInPaneBinding ;
and then (for reasons I can't quite figure out), the binding must actually evaluate the bound property directly:
boundsInPaneBinding = Bindings.createObjectBinding(() -> {
Bounds nodeLocal = node.getBoundsInLocal();
// note how this actually gets the value of localToSceneTransformProperty():
Bounds nodeScene = node.getLocalToSceneTransform().apply(nodeLocal);
Bounds nodePane = pane.sceneToLocal(nodeScene);
return nodePane ;
}, node.boundsInLocalProperty(), node.localToSceneTransformProperty(),
pane.localToSceneTransformProperty());
I'm trying to combine 3 features into a single JavaFX class. My first feature displays "WELCOME TO JAVA" around in a circle. The second displays a 10x10 matrix of random 1's and 0's. The third displays a smiley face. They should be displayed one after the other in a single pane. I have the first and third features but the matrix one is throwing me off. Although everything is supposed to be in a single pane to display on the same GUI window (per professor), I don't see how else I could've done the matrix other than creating the gridPane. It displays fine without the size constraints, but then it takes up the entire screen and my other 2 features aren't visible. When I add the constraints, it gets small and the numbers aren't visible. I'm not sure how I can fix this. Can someone please help?
Pane pane = new Pane();
// Create a circle and set its properties
Circle circle = new Circle();
circle.setCenterX(100);
circle.setCenterY(100);
circle.setRadius(50);
circle.setStroke(null);
circle.setFill(null);
pane.getChildren().add(circle); // Add circle to the pane
//Display WELCOME TO JAVA with the text forming a circle
int i = 0;
String phrase = "WELCOME TO JAVA ";
double degree = 360 / phrase.length();
for (double degrees = 0; i < phrase.length(); i++, degrees += degree) {
double pointX = circle.getCenterX() + circle.getRadius() *
Math.cos(Math.toRadians(degrees));
double pointY = circle.getCenterY() + circle.getRadius() *
Math.sin(Math.toRadians(degrees));
Text letter = new Text(pointX, pointY, phrase.charAt(i) + "");
letter.setFill(Color.BLACK);
letter.setFont(Font.font("Times New Roman", FontWeight.BOLD, 20));
letter.setRotate(degrees + 90);
pane.getChildren().add(letter); }
//Create a 10x10 matrix of 1s and 0s
GridPane pane2 = new GridPane();
pane2.setHgap(1);
pane2.setVgap(1);
Button[][] matrix;
int length = 10;
int width = 10;
ArrayList<TextField> textFields = new ArrayList<>();
for (int y = 0; y < length; y++) {
ColumnConstraints colConst = new ColumnConstraints();
colConst.setPercentWidth(10);
pane2.getColumnConstraints().add(colConst);
for (int x = 0; x < width; x++) {
RowConstraints rowConst = new RowConstraints();
rowConst.setPercentHeight(10);
pane2.getRowConstraints().add(rowConst);
Random rand = new Random();
int random1 = rand.nextInt(2);
TextField textf = new TextField();
textf.setText("" + random1);
textf.setPrefSize(15, 15);
pane2.setRowIndex(textf, x);
pane2.setColumnIndex(textf, y);
pane2.getChildren().add(textf);
}}
//Create a smiley face
Circle circle2 = new Circle();
circle2.setCenterX(600.0f);
circle2.setCenterY(100.0f);
circle2.setRadius(50.0f);
circle2.setStroke(Color.BLACK);
circle2.setFill(null);
pane.getChildren().add(circle2);
Circle leftInnerEye = new Circle();
leftInnerEye.setCenterX(580.0f);
leftInnerEye.setCenterY(85.0f);
leftInnerEye.setRadius(5);
leftInnerEye.setStroke(Color.BLACK);
pane.getChildren().add(leftInnerEye);
Ellipse leftOutterEye = new Ellipse();
leftOutterEye.setCenterX(580.0f);
leftOutterEye.setCenterY(85.0f);
leftOutterEye.setRadiusX(11.0f);
leftOutterEye.setRadiusY(8.0f);
leftOutterEye.setStroke(Color.BLACK);
leftOutterEye.setFill(null);
pane.getChildren().add(leftOutterEye);
Circle rightEye = new Circle();
rightEye.setCenterX(620.0f);
rightEye.setCenterY(85.0f);
rightEye.setRadius(5);
rightEye.setStroke(Color.BLACK);
pane.getChildren().add(rightEye);
Ellipse rightOutterEye = new Ellipse();
rightOutterEye.setCenterX(620.0f);
rightOutterEye.setCenterY(85.0f);
rightOutterEye.setRadiusX(11.0f);
rightOutterEye.setRadiusY(8.0f);
rightOutterEye.setStroke(Color.BLACK);
rightOutterEye.setFill(null);
pane.getChildren().add(rightOutterEye);
Polygon nose = new Polygon();
nose.getPoints().setAll(
600d, 90d,
588d, 115d,
612d, 115d );
nose.setStroke(Color.BLACK);
nose.setFill(null);
pane.getChildren().add(nose);
Arc mouth = new Arc(600, 115, 30, 16, 180, 180);
mouth.setFill(null);
mouth.setType(ArcType.OPEN);
mouth.setStroke(Color.BLACK);
pane.getChildren().add(mouth);
HBox hbox = new HBox(pane, pane2);
hbox.autosize();
hbox.setAlignment(Pos.BASELINE_LEFT);
hbox.setPadding(new Insets(20));
// Create a scene and place it in the stage
Scene scene = new Scene(hbox, 1000, 500);
primaryStage.setTitle("Laura's Chapter 14"); // Set the stage title
primaryStage.setScene(scene); // Place the scene in the stage
primaryStage.show(); // Display the stage
}
catch(Exception e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
launch(args);
}
Create a pane for each feature and then add the features to either a VBox or HBox, that will root pane of the scene. That way you can have a GridPane for your second feature. There are different layouts available in JavaFX and they behave differently. Take a look at this documentation and its sub documentations.