I am going to school for programming in java. I received a program where i have to use a scrollbar to change the width of an imageView. My question is it even possible with the scrollbar API in JavaFX?
Alright Here is my code.
sb = new ScrollBar();
sb.setMax(100);
sb.setMin(0);
lastValue = 500;
sb.setValue(lastValue);
sb.setUnitIncrement(1);
sb.blockIncrementProperty();
sb.setOnScroll(e -> FacePart.getPart().scrollAction(lastValue));
this is where i am having the issue. communicating back and forth between the class that this code is in and my method that is in another class.
here is the method in the other class.
Other method
#Override
public void scrollAction(double j) {
/*Global variable*/ lastScrollValue = j;
iv.setFitWidth(300 + 2 * lastScrollValue);
}
This can be done, but it shouldn't be done using the onScroll event.
Add a ChangeListener to the value property instead:
sb.valueProperty().addListener((observable, oldValue, newValue) -> {
FacePart.getPart().scrollAction(newValue.doubleValue());
});
Or simply use bindings:
iv.fitWidthProperty().bind(sb.valueProperty().multiply(2).add(300));
At least I guess that's what you want to do. I'm not sure why you'd use a "global" there.
Furthermore:
sb.blockIncrementProperty();
is effectively a NOP here. If you need to set (or get) the value, you should do it using the provided setter (or the getter):
sb.setBlockIncrement(someValue);
Doing this via the property is less readable and does the same.
Also usually you'd use a Slider for this (since the handle size doesn't seem to have a meaning in this case).
I agree with Fabian, just use a Slider for this task, rather than a ScrollBar. A Slider is a more suitable control.
import javafx.application.Application;
import javafx.geometry.Insets;
import javafx.scene.Scene;
import javafx.scene.control.Slider;
import javafx.scene.image.*;
import javafx.scene.layout.*;
import javafx.stage.Stage;
public class SmurfObesityMeter extends Application {
private static final double DEFAULT_SIZE = 128;
private static final double MIN_WIDTH = DEFAULT_SIZE / 2;
private static final double MAX_WIDTH = DEFAULT_SIZE * 2;
#Override
public void start(final Stage stage) throws Exception {
final Image image = new Image(IMAGE_LOC);
final ImageView imageView = new ImageView(image);
StackPane imagePane = new StackPane(imageView);
imagePane.setMinSize(StackPane.USE_PREF_SIZE, StackPane.USE_PREF_SIZE);
imagePane.setPrefSize(MAX_WIDTH, DEFAULT_SIZE);
imagePane.setMaxSize(StackPane.USE_PREF_SIZE, StackPane.USE_PREF_SIZE);
final Slider slider = new Slider(MIN_WIDTH, MAX_WIDTH, DEFAULT_SIZE);
imageView.fitWidthProperty().bind(slider.valueProperty());
final VBox layout = new VBox(10, imagePane, slider);
layout.setPadding(new Insets(10));
stage.setScene(new Scene(layout));
stage.show();
}
public static void main(String[] args) {
launch(args);
}
private static final String IMAGE_LOC =
"http://icons.iconarchive.com/icons/designbolts/smurfs-movie/128/smurfette-icon.png";
}
Related
I'm trying to draw 10,000 circles in JavaFX but it seems like its not working and I'm not even able to draw a single circle. Actually it trows me an error:
This is the code that I currently have:
public class RandomCircles extends Application {
private Random randomNumbers;
private int count;
private final double MAX_X = 600;
private final double MAX_Y = 300;
private final int FINAL_CIRCLES = 10000;
public void start(Stage primaryStage){
Circle initCircle = new Circle();
initCircle.setStroke(Color.BLACK);
initCircle.setStrokeWidth(3);
initCircle.setRadius(1);
for(count = 0; count <= FINAL_CIRCLES; count++){
initCircle.setCenterX(randomNumbers.nextInt((int) MAX_X));
initCircle.setCenterY(randomNumbers.nextInt((int) MAX_Y));
}
Group baseDemo = new Group(initCircle);
// Scene scene = new Scene(baseDemo, MAX_X, MAX_Y);
Scene scene = new Scene(baseDemo);
scene.setFill(Color.WHITE);
scene.getWidth();
primaryStage.setTitle("10,000");
primaryStage.setScene(scene);
primaryStage.setResizable(true);
primaryStage.show();
}
/**
* #param args the command line arguments
*/
public static void main(String[] args) {
// TODO code application logic here
launch(args);
}
}
Can somebody also tell me if using the setCenterX/Y is the right approach to create the circles in random locations?
Thanks.
UPDATE: To the person who though of my post as a duplicate, it is not. My problem comes from my logic that I implemented in my code not from a NullPointerException(not really) error. , which was wrong. Some guy already helped me to solve it.
After fixing the runtime error you are getting, your code only draws one circle. That's because you only add one circle to your scene graph. The for loop basically does nothing. The last X and Y coordinates for the circle's center are used to draw the single, solitary circle. You need to add ten thousand circles.
In the code below, I changed 10_000 to 100 (one hundred) since 10_000 has too many overlapping circles in the stage dimensions that you set. I also increased each circle's radius.
import java.util.Random;
import javafx.application.Application;
import javafx.scene.Group;
import javafx.scene.Scene;
import javafx.scene.paint.Color;
import javafx.scene.shape.Circle;
import javafx.stage.Stage;
public class RandomCircles extends Application {
private Random randomNumbers = new Random();
private int count;
private final double MAX_X = 600;
private final double MAX_Y = 300;
private final int FINAL_CIRCLES = 100;
public void start(Stage primaryStage){
Group baseDemo = new Group();
for(count = 0; count <= FINAL_CIRCLES; count++){
Circle initCircle = new Circle();
initCircle.setStroke(Color.BLACK);
initCircle.setStrokeWidth(3);
initCircle.setRadius(5);
initCircle.setCenterX(randomNumbers.nextInt((int) MAX_X));
initCircle.setCenterY(randomNumbers.nextInt((int) MAX_Y));
baseDemo.getChildren().add(initCircle);
}
Scene scene = new Scene(baseDemo);
scene.setFill(Color.WHITE);
scene.getWidth();
primaryStage.setTitle("100");
primaryStage.setScene(scene);
primaryStage.setResizable(true);
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
}
Change this line:
private Random randomNumbers;
to this:
private Random randomNumbers = new Random();
Your code is assuming the the Random object will be allocated like the other member variables, but it is an object and must be created with new.
I am tryng to develop a naviagtion map system using ArcGIS Runtime for Java, and FXML files for the view part. I am currently facing zoomButtons disabling: in some zoomlevel examples, the zoomIn button should be disabled at zoomlevel = 18 and zoomout should be disabled at zoomlevel = 0. Now I am stuck while trying to disable those buttons at several zoom levels. Can anyone help me to solve this problem? You can find the attached code below.
I have already developped the zoomIn and zoomOut methods and they are working properly.
//ZoomIn Function is created
public void zoomInFunction() {
Viewpoint current = mapView.getCurrentViewpoint(Viewpoint.Type.CENTER_AND_SCALE);
Viewpoint zoomedIn = new Viewpoint((Point) current.getTargetGeometry(), current.getTargetScale() / 2.0);
mapView.setViewpointAsync(zoomedIn);
}
//ZoomOut Function is created
public void zoomOutFunction() {
Viewpoint current = mapView.getCurrentViewpoint(Viewpoint.Type.CENTER_AND_SCALE);
Viewpoint zoomedOut = new Viewpoint((Point) current.getTargetGeometry(), current.getTargetScale() * 2.0);
mapView.setViewpointAsync(zoomedOut);
}
// Create action event for ZoomIn Function
public void zoomInAction(ActionEvent event) {
map.zoomInFunction();
}
// Create action event for ZoomOut Function
public void zoomOutAction(ActionEvent event) {
map.zoomOutFunction();
}
Define a property (zoomLevel) and bind the disableProperty of the buttons to the zoomLevel property when it goes above or below certain maximum and minimum values.
Zoom between min and max
Zoomed out
Zoomed in
Implementation is pretty straightforward. A sample app is provided below to demonstrate the concepts involved.
import javafx.application.Application;
import javafx.beans.property.*;
import javafx.geometry.Insets;
import javafx.scene.Scene;
import javafx.scene.control.*;
import javafx.scene.layout.*;
import javafx.stage.Stage;
public class BoundedZoom extends Application {
private static final int MIN_ZOOM_LEVEL = 1;
private static final int MAX_ZOOM_LEVEL = 5;
private static final int DEFAULT_ZOOM_LEVEL = 3;
private IntegerProperty zoomLevel = new SimpleIntegerProperty(DEFAULT_ZOOM_LEVEL);
public int getZoomLevel() {
return zoomLevel.get();
}
public IntegerProperty zoomLevelProperty() {
return zoomLevel;
}
public void setZoomLevel(int zoomLevel) {
this.zoomLevel.set(zoomLevel);
}
#Override
public void start(Stage stage) throws Exception {
Button zoomIn = new Button("Zoom In");
zoomIn.setOnAction(event -> setZoomLevel(getZoomLevel() + 1));
zoomIn.setDisable(getZoomLevel() >= MAX_ZOOM_LEVEL);
zoomIn.disableProperty().bind(zoomLevel.greaterThanOrEqualTo(MAX_ZOOM_LEVEL));
Button zoomOut = new Button("Zoom Out");
zoomOut.setOnAction(event -> setZoomLevel(getZoomLevel() - 1));
zoomOut.setDisable(getZoomLevel() <= MIN_ZOOM_LEVEL);
zoomOut.disableProperty().bind(zoomLevel.lessThanOrEqualTo(MIN_ZOOM_LEVEL));
Label zoomLevelDescLabel = new Label("Zoom level (min " + MIN_ZOOM_LEVEL + ", max " + MAX_ZOOM_LEVEL + "): ");
Label zoomLevelValueLabel = new Label("" + getZoomLevel());
zoomLevelValueLabel.textProperty().bind(zoomLevel.asString());
Pane zoomLevelDisplay = new HBox(10, zoomLevelDescLabel, zoomLevelValueLabel);
Pane controlPane = new HBox(10, zoomIn, zoomOut);
Pane layout = new VBox(10, zoomLevelDisplay, controlPane);
layout.setPadding(new Insets(10));
stage.setScene(new Scene(layout));
stage.show();
}
public static void main(String[] args) {
launch(args);
}
}
I have a pane that layouts its child nodes customly by overriding the layoutChildren method. It simplified looks like this:
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.layout.Pane;
import javafx.scene.text.Font;
public class MyPane extends Pane {
private Label lblTitel;
private Button btnStart;
public MyPane(Runnable sceneChanger) {
this.lblTitel = new Label("Title");
lblTitel.setFont(Font.font(16));
this.btnStart = new Button("Change Scene");
btnStart.setOnAction(e -> sceneChanger.run());
getChildren().addAll(lblTitel, btnStart);
}
#Override
protected void layoutChildren() {
lblTitel.setLayoutX(getWidth() / 2 - lblTitel.getWidth() / 2);
lblTitel.setLayoutY(4);
btnStart.setLayoutX(getWidth() / 2 - btnStart.getWidth() / 2);
btnStart.setLayoutY(getHeight() - 4 - btnStart.getHeight());
super.layoutChildren();
}
}
Displaying that Pane as root node of a scene of a stage works fine. But when I change the scene and then change it back again, the panes size doesn't fit the stages viewport anymore.
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.layout.Pane;
import javafx.stage.Stage;
public class App extends Application {
private Stage window;
private Scene firstScene;
private Scene secondScene;
public static void main(String[] args) {
launch(args);
}
#Override
public void start(Stage window) {
this.window = window;
this.firstScene = new Scene(new MyPane(() -> window.setScene(secondScene)));
this.secondScene = new Scene(new SecondPane());
window.setScene(firstScene);
window.setWidth(800);
window.setHeight(600);
window.show();
}
private class SecondPane extends Pane {
public SecondPane() {
Button btnBack = new Button("back");
btnBack.setOnAction(e -> window.setScene(firstScene));
getChildren().add(btnBack);
}
}
}
After changing back to the first scene the pane is far to large, you can see that by the title being more on the right side than in the horizontal center. As soon as I change the size of the window, the panes size perfectly fits the stages viewport again and everything is fine. So I added a custom increase of size to the window right after I switch scenes.
Platform.runLater(() -> window.setWidth(window.getWidth() + 1));
It works in most of the cases, but its a ridiculous solution and doesn't work when the stage is e.g. maximized.
Is there any way to make a customly layouted pane fit into the stages viewport after setting its scene to the stage, that works in all cases?
First, note that the layout you give in the example code can readily be accomplished using existing layout panes. Something like
BorderPane myPane = new BorderPane();
myPane.setTop(lblTitel);
myPane.setBottom(btnStart);
BorderPane.setAlignment(lblTitel, Pos.CENTER);
BorderPane.setAlignment(btnStart, Pos.CENTER);
BorderPane.setMargin(lblTitle, new Insets(4, 0, 0, 0));
BorderPane.setMargin(btnStart, new Insets(0, 0, 4, 0));
should give you the layout you want.
In general, you should always prefer the built-in layouts to a custom layout.
If you find you really do need a custom pane, then there is a lot more work to do than simply laying out the child nodes. Your pane needs to override the methods to determine its resizable range, which means overriding the methods computeMin/Pref/MaxWidth() and computeMin/Pref/MaxHeight(). You should also determine the content bias by overriding getContentBias(). The layoutChildren() method should determine the size of the child nodes as well as their positions. It should do this by determining their preferred sizes and using those if possible, and using a size in the min-max range for each otherwise. You can determine those sizes by calling prefWidth(), prefHeight() etc on the child nodes.
Finally, to ensure the custom layout properly respects borders and padding, you should account for any insets in the layout calculations. The utility methods snappedTop/Right/Bottom/LeftInsets() give you the values for the insets coming from borders or padding.
Here's an example which works for me: this assumes both child nodes have HORIZONTAL content bias (which makes sense here), so I pass -1 to the methods computing the width, and the computed width to the methods computing the height.
import javafx.geometry.Orientation;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.layout.Pane;
import javafx.scene.text.Font;
import javafx.scene.text.TextAlignment;
public class MyPane extends Pane {
private Label lblTitel;
private Button btnStart;
public MyPane(Runnable sceneChanger) {
// updated to test for long text, wrapping, etc.
this.lblTitel = new Label("This is a really long title that might end up having to span multiple lines of text");
this.lblTitel.setWrapText(true);
this.lblTitel.setTextAlignment(TextAlignment.CENTER);
lblTitel.setFont(Font.font(16));
this.btnStart = new Button("Change Scene");
btnStart.setOnAction(e -> sceneChanger.run());
getChildren().addAll(lblTitel, btnStart);
}
#Override
public Orientation getContentBias() {
return Orientation.HORIZONTAL ;
}
#Override
public double computePrefWidth(double height) {
return Math.max(lblTitel.prefWidth(height), btnStart.prefWidth(height));
}
#Override
public double computeMinWidth(double height) {
return Math.max(lblTitel.minWidth(height), btnStart.minWidth(height));
}
#Override
public double computeMaxWidth(double height) {
return Math.max(lblTitel.maxWidth(height), btnStart.maxWidth(height));
}
#Override
public double computePrefHeight(double width) {
return lblTitel.prefHeight(width) + btnStart.prefHeight(width) + 8 ;
}
#Override
public double computeMinHeight(double width) {
return lblTitel.minHeight(width) + btnStart.minHeight(width) + 8 ;
}
#Override
public double computeMaxHeight(double width) {
return lblTitel.maxHeight(width) + btnStart.maxHeight(width) + 8 ;
}
#Override
protected void layoutChildren() {
double usableWidth = getWidth() - snappedLeftInset() - snappedRightInset() ;
double usableHeight = getHeight() - snappedTopInset() - snappedBottomInset() ;
double lblTitleWidth = lblTitel.prefWidth(-1);
if (lblTitleWidth > usableWidth) {
lblTitleWidth = Math.max(lblTitel.minWidth(-1), usableWidth);
}
double lblTitleHeight = lblTitel.prefHeight(lblTitleWidth);
if (lblTitleHeight > usableHeight) {
lblTitleHeight = Math.max(lblTitel.prefHeight(lblTitleWidth), usableHeight);
}
double lblTitleX = snappedLeftInset() + usableWidth / 2 - lblTitleWidth / 2 ;
lblTitel.resizeRelocate(lblTitleX, 4 + snappedTopInset(), lblTitleWidth, lblTitleHeight);
double btnStartWidth = btnStart.prefWidth(-1);
if (btnStartWidth > usableWidth) {
btnStartWidth = Math.max(btnStart.minWidth(-1), usableWidth);
}
double btnStartHeight = btnStart.prefHeight(btnStartWidth);
if (btnStartHeight > usableHeight) {
btnStartHeight = Math.max(btnStart.minHeight(btnStartWidth), usableHeight);
}
double btnStartX = snappedLeftInset() + usableWidth / 2 - btnStartWidth / 2 ;
double btnStartY = snappedTopInset() + getHeight() - 4 - btnStartHeight ;
btnStart.resizeRelocate(btnStartX, btnStartY, btnStartWidth, btnStartHeight);
}
}
I'm using JavaFX to create a Java application which is able to apply a TranslateTransition to a generic node and recall it continuously.
I retrieved a simple right arrow from this url https://www.google.it/search?q=arrow.png&espv=2&source=lnms&tbm=isch&sa=X&ved=0ahUKEwiGheeJvYrTAhWMB5oKHU3-DxgQ_AUIBigB&biw=1600&bih=764#imgrc=rH0TbMkQY2kUaM:
and used it to create the node to translate.
This is my AnimatedNode class:
package application.model.utils.addon;
import javafx.animation.TranslateTransition;
import javafx.scene.Node;
import javafx.util.Duration;
public class AnimatedNode {
private Node node;
private double positionY;
private TranslateTransition translateTransition;
private boolean animated;
private int reverse = 1;
public AnimatedNode(Node node, double animationTime) {
setPositionY(0.0);
setNode(node);
setTranslateTransition(animationTime);
}
public void play() {
if(translateTransition != null && !isAnimated()) {
setAnimated(true);
new Thread() {
#Override
public void run() {
while(isAnimated()) {
translateTransition.setToY(positionY + 50 * reverse);
translateTransition.play();
reverse = -reverse;
setPositionY(translateTransition.getToY());
}
}
}.start();
}
}
public void stop() {
setAnimated(false);
}
public Node getNode() {
return node;
}
private void setNode(Node node) {
this.node = node;
}
public TranslateTransition getTranslateTransition() {
return translateTransition;
}
private void setTranslateTransition(double animationTime) {
translateTransition = new TranslateTransition();
if(node != null) {
translateTransition.setDuration(Duration.seconds(animationTime));
translateTransition.setNode(node);
}
}
public double getPositionY() {
return positionY;
}
private void setPositionY(double positionY) {
this.positionY = positionY;
}
public boolean isAnimated() {
return animated;
}
private void setAnimated(boolean animated) {
this.animated = animated;
}
}
and this is the Application class
package test;
import application.model.utils.addon.AnimatedNode;
import javafx.application.Application;
import javafx.event.EventHandler;
import javafx.scene.Scene;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.input.MouseButton;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.Pane;
import javafx.stage.Stage;
public class Test extends Application {
private final String TITLE = "Test application";
private final double WIDTH = 600;
private final double HEIGHT = 400;
private final String ARROW_PATH = "file:resources/png/arrow.png";
private BorderPane rootPane;
#Override
public void start(Stage primaryStage) {
primaryStage.setTitle(TITLE);
rootPane = new BorderPane();
rootPane.setPrefSize(WIDTH, HEIGHT);
Image image = new Image(ARROW_PATH);
ImageView imageView = new ImageView(image);
imageView.setFitWidth(WIDTH);
imageView.setFitHeight(HEIGHT);
imageView.setPreserveRatio(true);
AnimatedNode animatedNode = new AnimatedNode(imageView, 0.7);
Pane pane = new Pane();
pane.getChildren().add(animatedNode.getNode());
pane.setOnMouseClicked(new EventHandler<MouseEvent>() {
#Override
public void handle(MouseEvent arg0) {
if(arg0.getButton().equals(MouseButton.PRIMARY))
animatedNode.play();
if(arg0.getButton().equals(MouseButton.SECONDARY))
animatedNode.stop();
}
});
rootPane.setCenter(pane);
Scene scene = new Scene(rootPane, WIDTH, HEIGHT);
primaryStage.setScene(scene);
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
}
The node is added to a generic pane; the pane has a MouseListener. I can start the TranslateTransition by using the primary button of the mouse and stop it with the secondary one.
I used a Thread in the play() method of AnimatedNode but I still have a continuous delay in the transition.
Is this the best way to perform the transition? Can I improve my code?
Thanks a lot for your support.
Sample
This is a simplified example which demonstrates a continuous animation started and stopped by left and right mouse clicks.
import javafx.animation.*;
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.image.*;
import javafx.scene.layout.Pane;
import javafx.stage.Stage;
import javafx.util.Duration;
public class BouncingCat extends Application {
private static final double WIDTH = 100;
private static final double HEIGHT = 100;
private final String ARROW_PATH =
"http://icons.iconarchive.com/icons/iconka/meow-2/64/cat-rascal-icon.png";
// image source: http://www.iconka.com
#Override
public void start(Stage stage) {
Image image = new Image(ARROW_PATH);
ImageView imageView = new ImageView(image);
TranslateTransition animation = new TranslateTransition(
Duration.seconds(0.7), imageView
);
animation.setCycleCount(Animation.INDEFINITE);
animation.setFromY(0);
animation.setToY(50);
animation.setAutoReverse(true);
Pane pane = new Pane(imageView);
Scene scene = new Scene(pane, WIDTH, HEIGHT);
scene.setOnMouseClicked(e -> {
switch (e.getButton()) {
case PRIMARY:
animation.play();
break;
case SECONDARY:
animation.pause();
break;
}
});
stage.setScene(scene);
stage.show();
}
public static void main(String[] args) {
launch(args);
}
}
Advice
You don't need a Thread when you have a Transition. JavaFX will render updated transition frames automatically each pulse.
I don't advise keeping track of properties in a class, when those same values are already represented in the underlying tools you use.
For example:
replace int reverse = 1; with transition.setAutoReverse(true) or transition.setRate(1) (or -1).
replace animated with transition.getStatus().
instead of double positionY, set the toY of the transition.
I wouldn't advise calling your class AnimatedNode unless it extended node, otherwise it is confusing, instead call it something like AnimationControl.
import javafx.animation.TranslateTransition;
import javafx.scene.Node;
import javafx.util.Duration;
public class AnimationControl {
private final TranslateTransition translateTransition;
public AnimationControl(Duration duration, Node node) {
translateTransition = new TranslateTransition(duration, node);
}
public TranslateTransition getTranslateTransition() {
return translateTransition;
}
}
You only need to encapsulate the node and the transition in the AnimationControl and not other fields unless you need further functionality not apparent in your question and not already provided by Node or Transition. If you have that extra functionality then you can enhance the AnimationControl class above to add it.
Exposing the node and the translate transition is enough, as if the user wants to manage the animation, such as starting and stopping it, then the user can just get it from the AnimationControl class. Depending on your use case, the entire AnimationControl class might be unnecessary as you might not need the encapsulation it provides and might instead prefer to just work directly with the node and the transition (as demoed in the sample).
thanks for reading my question.
I'm currently working with JavaFX-8, SceneBuilder and Eclipse.
I want to do a scatter chart with four quadrants, that has two fixed number axis (the data position is not relevant, I only need to display the dots on each quadrant... only matters in which quadrant a dot is). Each quadrant must have a background with a specific color.
I found this question, so I tried to extend ScatterChart with the aim of overriding the method layoutPlotChildren(). I tried a minimum implementation to see if it will run with my FXML (I did import the new component to the FXML). This was my minimum implementation:
public class ScatterQuadrantChart<X,Y> extends ScatterChart<X,Y> {
public ScatterQuadrantChart(Axis<X> xAxis, Axis<Y> yAxis) {
super(xAxis, yAxis);
} }
And then, I get the NotSuchMethodError init error. I found a similar error but from someone extending LineChart here, but I'm not quite sure of what I need to do on my own class.
I tried adding a no-parameters constructor, but I need to call super and cant because I can't call the "getXAxis()" method either. What should I do here?
Plus, the other issue that remains is, once I solve this, what should the layoutPlotChildren() method do?
Thanks for reading.
The problem you are seeing is arising because the default mechanism for the FXMLLoader to instantiate a class is to call the no-argument constructor. Your ScatterQuadrantChart has no no-argument constructor, hence the NoSuchMethodError.
Prior to Java 8, the only way to fix this was to create a builder class for your class, as in the post you linked. JavaFX 8 introduced (but failed to document) a mechanism to specify values for constructor parameters that would be recognized by the FXMLLoader, using the #NamedArg annotation).
So, in Java 8, you can modify your ScatterQuadrantChart:
public class ScatterQuadrantChart<X,Y> extends ScatterChart<X,Y> {
public ScatterQuadrantChart(#NamedArg("xAxis")Axis<X> xAxis,
#NamedArg("yAxis)Axis<Y> yAxis) {
super(xAxis, yAxis);
}
}
and then your FXML will look like
<ScatterQuadrantChart>
<xAxis>
<NumberAxis ... />
</xAxis>
<yAxis>
<NumberAxis ... />
</yAxis>
</ScatterQuadrantChart>
I have no idea if or how SceneBuilder will interact with this, but the FXML will work.
As for the implementation, you will need to add some nodes to the plot to represent your quadrants. I would probably just use plain regions for these. Create them in the constructor and call getPlotChildren().add(...) to add them. Then in the layoutPlotChildren() method, first call the superclass method (which will lay out the scatter chart nodes), and then resize and reposition the quadrants. You can use getXAxis().getDisplayPosition(...) to figure out the location from the actual divider value.
In real life, you should add style classes to the quadrants so you can style them externally with css, etc, but a very basic implementation might look like
import javafx.beans.NamedArg;
import javafx.beans.property.Property;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.value.ChangeListener;
import javafx.scene.chart.Axis;
import javafx.scene.chart.ScatterChart;
import javafx.scene.layout.Region;
public class ScatterQuadrantChart<X,Y> extends ScatterChart<X,Y> {
private final Property<X> xQuadrantDivider = new SimpleObjectProperty<>();
private final Property<Y> yQuadrantDivider = new SimpleObjectProperty<>();
private final Region nwQuad ;
private final Region neQuad ;
private final Region swQuad ;
private final Region seQuad ;
public ScatterQuadrantChart(#NamedArg("xAxis") Axis<X> xAxis,
#NamedArg("yAxis") Axis<Y> yAxis) {
super(xAxis, yAxis);
nwQuad = new Region();
neQuad = new Region();
swQuad = new Region();
seQuad = new Region();
nwQuad.setStyle("-fx-background-color: lightsalmon ;");
neQuad.setStyle("-fx-background-color: antiquewhite ;");
swQuad.setStyle("-fx-background-color: aqua ;");
seQuad.setStyle("-fx-background-color: lightskyblue ;");
getPlotChildren().addAll(nwQuad, neQuad, swQuad, seQuad);
ChangeListener<Object> quadListener = (obs, oldValue, newValue) -> layoutPlotChildren();
xQuadrantDivider.addListener(quadListener);
yQuadrantDivider.addListener(quadListener);
}
#Override
public void layoutPlotChildren() {
super.layoutPlotChildren();
X x = xQuadrantDivider.getValue();
Y y = yQuadrantDivider.getValue();
if (x != null && y != null) {
Axis<X> xAxis = getXAxis();
Axis<Y> yAxis = getYAxis();
double xPixels = xAxis.getDisplayPosition(x);
double yPixels = yAxis.getDisplayPosition(y);
double totalWidth = xAxis.getWidth();
double totalHeight = yAxis.getHeight();
nwQuad.resizeRelocate(0, 0, xPixels, yPixels);
swQuad.resizeRelocate(0, yPixels, xPixels, totalHeight - yPixels);
neQuad.resizeRelocate(xPixels, 0, totalWidth - xPixels, yPixels);
seQuad.resizeRelocate(xPixels, yPixels, totalWidth - xPixels, totalHeight - yPixels);
}
}
public final Property<X> xQuadrantDividerProperty() {
return this.xQuadrantDivider;
}
public final X getXQuadrantDivider() {
return this.xQuadrantDividerProperty().getValue();
}
public final void setXQuadrantDivider(final X xQuadrantDivider) {
this.xQuadrantDividerProperty().setValue(xQuadrantDivider);
}
public final Property<Y> yQuadrantDividerProperty() {
return this.yQuadrantDivider;
}
public final Y getYQuadrantDivider() {
return this.yQuadrantDividerProperty().getValue();
}
public final void setYQuadrantDivider(final Y yQuadrantDivider) {
this.yQuadrantDividerProperty().setValue(yQuadrantDivider);
}
}
Test code:
import java.util.Random;
import java.util.stream.Stream;
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.chart.NumberAxis;
import javafx.scene.chart.XYChart.Data;
import javafx.scene.chart.XYChart.Series;
import javafx.scene.layout.BorderPane;
import javafx.stage.Stage;
public class ScatterQuadrantChartTest extends Application {
#Override
public void start(Stage primaryStage) {
final Random rng = new Random();
ScatterQuadrantChart<Number, Number> chart = new ScatterQuadrantChart<>(new NumberAxis(), new NumberAxis());
Series<Number, Number> series = new Series<>();
for (int i=0; i<20; i++) {
series.getData().add(new Data<>(rng.nextDouble() * 100, rng.nextDouble() * 100));
}
chart.getData().add(series);
chart.setXQuadrantDivider(50);
chart.setYQuadrantDivider(50);
BorderPane root = new BorderPane(chart);
Scene scene = new Scene(root, 600, 600);
primaryStage.setScene(scene);
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
}