Comfortable mappings - javafx

I have a grid in which I show different images. When the user clicks on an image, an event should be fired. My code now looks like this:
<GridPane alignment="center" hgap="10" vgap="10">
<ImageView
fx:id="cell1" fitHeight="90" fitWidth="70"
GridPane.columnIndex="0" GridPane.rowIndex="0"
onMouseClicked="#previewClicked1" />
<ImageView
fx:id="cell2" fitHeight="90" fitWidth="
GridPane.columnIndex="1" GridPane.rowIndex="0"
onMouseClicked="#previewClicked2"/>
[...]
</GridPane>
I have like 20 of these cells. Do I really need to create an ImageView object and an event method for every one of these objects? E.g. isn't there a way to object has been clicked?

The best option is simply to do this in Java, rather than in FXML. So your FXML will just be
<GridPane fx:id="imagePane" alignment="center" ... >
</GridPane>
Now in your controller's initialize() method define the ImageViews:
public class MyController {
#FXML
private GridPane imagePane ;
public void initialize() {
int numColumns = ... ;
int numRows = ... ;
for (int col = 0; col < numColumns; col++) {
for (int row = 0; row < numRows; row++) {
ImageView imageView = new ImageView();
imagePane.add(imageView, col, row);
imageView.setOnMouseClicked(e -> {
// handle click for this image...
});
}
}
}
}
This is likely way less code in total anyway (since the FXML will be much smaller), and you can readily organize the image views into an array if you need, etc.
If you really want to define all the image views in FXML, one by one, then you can get the source of the event in the event handler. I don't really like this approach because of the necessary downcast, which can potentially make it brittle if you change things at a later stage. However:
#FXML
private void previewClicked(MouseEvent event) {
ImageView clickedImage = (ImageView) event.getSource();
// ...
}
and then just set onMouseClicked="#previewClicked" for all the ImageViews. You still have to laboriously define all the ImageViews in FXML, one by one, though.

Related

Add a node into a split pane divider

I'm trying to find a solution of how to add a label into a split pane divider or at least create the illusion of that. My approach was to add three panes into the split pane and drag the middle pane along with the two divider. The problem is that on rapid mouse movements the middle pane gets bigger, but if I define a max height then the dragging doesn't work at all.
My question is if there is a way of directly inserting a label onto the divider or if somebody has a better solution as mine to accomplish something like this.
MWE:
Class
public class MainSplit extends Application {
#FXML
private SplitPane splitPane;
#FXML
private Pane dividerPane;
public static void main(String[] args) {
launch(args);
}
#Override
public void start(Stage primaryStage) throws IOException {
FXMLLoader loader = new FXMLLoader(getClass().getResource("/splitpane.fxml"));
loader.setController(this);
Parent root = loader.load();
AtomicReference<Double> start = new AtomicReference<>((double) 0);
dividerPane.setOnMousePressed(event -> {
start.set((1.0 / splitPane.getScene().getHeight()) * event.getSceneY());
});
dividerPane.setOnMouseDragged(event -> {
double p = (1.0 / splitPane.getScene().getHeight()) * event.getSceneY();
double diff = p - start.get();
start.set(p);
double[] d = splitPane.getDividerPositions();
splitPane.setDividerPosition(0, d[0] + diff);
splitPane.setDividerPosition(1, d[1] + diff);
});
primaryStage.setScene(new Scene(root));
primaryStage.show();
}
}
FXML
<SplitPane fx:id="splitPane" dividerPositions="0.36, 0.43" orientation="VERTICAL" prefHeight="400.0" prefWidth="400.0"
xmlns="http://javafx.com/javafx/11.0.1" xmlns:fx="http://javafx.com/fxml/1">
<AnchorPane/>
<Pane fx:id="dividerPane" minHeight="-Infinity" prefHeight="20.0" style="-fx-background-color: #4545;">
<cursor>
<Cursor fx:constant="V_RESIZE"/>
</cursor>
</Pane>
<AnchorPane/>
</SplitPane>

Assigning FXML elements in controller from a newly added pane - JavaFX

Let's assume, for simplicity's sake, I have an FXML with a SplitPane. On one side I have a Button; the other has an empty AnchorPane (for now).
The button simply places a pane on the other side of the SplitPane from another FXML file. The newly added pane contains a Button and a Label. Both FXMLs have the same controller for two reasons: 1. The Added Pane FXML is not used elsewhere, 2. The Added Pane FXML has so little functions that it's not really worth it to have a separate controller for it.
Pressing the button on the newly added pane sets the text in its label to "Hello World".
Split Pane
<SplitPane fx:controller="myController".... >
<items>
<VBox .... >
<Button fx:id="mainButton" onAction="#handleFirstButton".... />
</VBox>
<AnchorPane fx:id="secondPane" ....>
</items>
</SplitPane>
Added Pane
<VBox fx:controller="myController" ....>
<Button fx:id="btn" onAction="#changeText" ... />
<Label fx:id="lbl" .... />
</VBox>
Controller Class
// Regular stuff here
#FXML
AnchorPane secondPane;
#FXML
Button mainButton;
#FXML
Button btn;
#FXML
Label lbl;
public void initialize(URL arg0, ResourceBundle arg1) {
}
private void loadSecondPane {
FXMLLoader loader = FXMLLoader.load(getClass().getResource("AddedPane.fxml"));
VBox box = loader.load();
secondPane.setTopAnchor(box);
}
#FXML
private void handleFirstButton(ActionEvent event) {
loadSecondPane();
}
#FXML
private void changeText(ActionEvent event) {
lbl.setText("Hello World");
}
The problem is, when the Added Pane is loaded, its components will NOT be defined in the controller, and trying to perform any method on them will produce a NullPointerException.
Is there a way around this or is having a separate controller mandatory in this case? And assuming I have 2+ buttons on the first side of the pane, and each button produces a different pane on the other side, does the solution remain the same?
NOTE: I'm fairly new with javafx, so excuse any mistypes.

Moving an image using mouse drag events

I am currently making a game in javafx, and what i need is to be able to make an image move left and right using mouse drag events, while the image is moving down. I have figured the latter part(the moving down part), and require assistance in adding the drag event to my image view object, so that on dragging it to the right is one event and dragging it to left is another event.
Thank you in advance.
This is my fxml file
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.control.Button?>
<?import javafx.scene.image.Image?>
<?import javafx.scene.image.ImageView?>
<?import javafx.scene.layout.AnchorPane?>
<AnchorPane maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" prefHeight="400.0" prefWidth="600.0" xmlns="http://javafx.com/javafx/9.0.1" xmlns:fx="http://javafx.com/fxml/1" fx:controller="application.Controller">
<children>
<ImageView fx:id="player" fitHeight="42.0" fitWidth="98.0" layoutX="47.0" layoutY="27.0" pickOnBounds="true" preserveRatio="true">
<image>
<Image url="#freefall.png" />
</image>
</ImageView>
<Button fx:id="button" layoutX="44.0" layoutY="21.0" mnemonicParsing="false" prefHeight="54.0" prefWidth="81.0" style="-fx-background-color: transparent; -fx-text-fill: transparent; -fx-border-fill: transparent;" text="Button" />
</children>
</AnchorPane>
This is my Controller Class
package application;
import java.net.URL;
import java.util.ResourceBundle;
import javafx.animation.TranslateTransition;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.image.ImageView;
import javafx.util.Duration;
public class Controller implements Initializable {
#FXML
ImageView player;
TranslateTransition transition = new TranslateTransition();
private double startDragX;
private double startDragY;
#Override
public void initialize(URL arg0, ResourceBundle arg1) {
player.setOnMousePressed(e -> {
startDragX = e.getSceneX();
startDragY = e.getSceneY();
});
player.setOnMouseDragged(e -> {
double X = e.getSceneX();
if(X - startDragX > 0)
player.setTranslateX(5);
else if (X - startDragX < 0)
player.setTranslateX(-5);
});
//This code makes the player move downwards continuously
transition.setDuration(Duration.seconds(15));
transition.setNode(player);
transition.setToY(800);
transition.play();
}
}
The problem i am facing after changing the code is that the player moves, when you drag it once to the right. But when u again drag it to the right nothing happens, all I can do is drag it back to its original position (by dragging it back to the left).
I think you already have the basics in your example.
There is something that I don't understand though: why is there a transition? If you want the ImageView to follow your mouse cursor as you drag, then it would be weird to mix transition in (i.e. it would appear like a lag to users).
This is the basic way to move your ImageView with drag event.
public class Controller implements Initializable {
#FXML
ImageView player;
private double startDragX;
private double startDragY;
#Override
public void initialize(URL arg0, ResourceBundle arg1) {
button.setMousePressed(e -> {
startDragX = e.getSceneX();
startDragY = e.getSceneY();
});
button.setMouseDragged(e -> {
button.setTranslateX(e.getSceneX() - startDragX);
button.setTranslateY(e.getSceneY() - startDragY);
});
}
}
Of course, you can choose to add the events in FXML and inject the event methods with #FXML annotation.
Edit
After having understood the question, I think this is what you want (or at least almost). I assumed that you want to achieve an effect similar to "flick" on a touch mobile device, except that this is done with mouse.
public class Controller implements Initializable {
#FXML
ImageView player;
private static final double MIN_FLICK_PIXELS = 10;
private static final double FLICK_MOVEMENT = 5;
private enum Direction {
LEFT, RIGHT
}
private double lastXPosition;
private Direction lastFlickDirection = null;
TranslateTransition transition = new TranslateTransition();
#Override
public void initialize(URL arg0, ResourceBundle arg1) {
player.setOnMousePressed(e -> {
lastXPosition = e.getSceneX();
});
player.setOnMouseDragged(e -> {
double currentX = e.getSceneX();
// Detect as "right flick" if the previous flick direction is not right, and the dragged pixels is more than 10
if (lastFlickDirection != Direction.RIGHT && currentX - lastXPosition > MIN_FLICK_PIXELS) {
direction = Direction.RIGHT;
player.setTranslateX(player.getTranslateX() + FLICK_MOVEMENT);
lastXPosition = currentX;
}
// Detect as "left flick" if the previous flick direction is not left, and the dragged pixels is more than -10
else if (lastFlickDirection != Direction.LEFT && currentX - lastXPosition < -MIN_FLICK_PIXELS) {
direction = Direction.LEFT;
player.setTranslateX(player.getTranslateX() -FLICK_MOVEMENT);
lastXPosition = currentX;
}
});
player.setOnMouseReleased(e -> {
lastFlickDirection = null;
});
//This code makes the player move downwards continuously
transition.setDuration(Duration.seconds(15));
transition.setNode(player);
transition.setToY(800);
transition.play();
}
}
Doing this will:
Shift the image 5 pixels to the right or left when user drag at least 10 pixels to the corresponding direction.
If the user drags 20 pixels continuously to the same direction, it only shift once.
If the user, within a single action, dragged 10 pixels to the right, then drag 10 pixels back to the left, the image will shift 5 pixels right, then 5 pixels left (which is original position).
One possible obvious flaw here is that the events are set on the ImageView, so the events may be lost if the cursor goes outside the bounds of the ImageView (it might not be lost, I didn't test this). If a problem occurs, shift the events out to perhaps its parent.

How to change parent of child fxml into other parent?

Can I change a Node's parent to another parent in FXML?
Suppose a parent named stackcon with a child node.
I now want to move this child node to different parent, e.g., named stackmain.
Is this possible? If yes, please give me a link or a example code.
This is just one of many ways how to do this.
MainView.fxml, just a simple view containing a button, and on button click the label should be moved from right to left and vice-versa (see the onMousePressed declaration):
<fx:root type="BorderPane" xmlns="http://javafx.com/javafx/8.0.111" xmlns:fx="http://javafx.com/fxml/1">
<left>
<VBox fx:id="lefty">
<Label fx:id="switchy" text="Switching text"></Label>
</VBox>
</left>
<center>
<Button fx:id="switchBtn" text="Switch" onMousePressed="#switchButtonPressed"></Button>
</center>
<right>
<VBox fx:id="righty">
</VBox>
</right>
</fx:root>
The controller MainView.java with its switchButtonPressed event handler:
public class MainView extends BorderPane {
#FXML private VBox lefty;
#FXML private VBox righty;
#FXML private Label switchy;
public MainView() {
URL fxmlFile = MainView.class.getResource("MainView.fxml");
FXMLLoader fxmlLoader = new FXMLLoader(fxmlFile);
fxmlLoader.setRoot(this);
fxmlLoader.setController(this);
try {
fxmlLoader.load();
} catch (IOException e) {
e.printStackTrace();
}
}
public void switchButtonPressed(MouseEvent mouseEvent) {
if(lefty.getChildren().contains(switchy)) {
lefty.getChildren().remove(switchy);
righty.getChildren().add(switchy);
} else {
righty.getChildren().remove(switchy);
lefty.getChildren().add(switchy);
}
}
}
You see, I just check on button click if the label is on the left side. If so, remove it on the left side and add it on the right side.
Analogous, if it is on the right side, remove it there and add it on the left side.
it may vary depending on type of the pane of stackmain & stackcon , i'm assuming that you use AnchorPane
but it is something like this
Node childNode = stackcon.getChildren().get(indexOfChild);
stackcon.getChildren().remove(indexOfChild);
stackmain.getChildren().add(childNode);

javaFX cannot access Scene elements

I am new to javafX, have run through various tutorials, and googled extensively. I am attempting to write a multiscreen javaFX program whose second screen has a split pane, whose right pane should display a grid and a button. The framework I am using for multiple screens is copied form Angela Caiedo's MultipleScreens Framework tutorial ( https://github.com/acaicedo/JFX-MultiScreen/tree/master/ScreensFramework) .
What works: The screens framework is successful in terms of flipping between my multiple screens.
My problem: I click the button on screen 1 to move to screen 2. Screen 2 successfully shows. On screen 2 I click a button to populate data in my vBox. The code in my controller class for screen 2 is unable to access the vbox (or any other defined containers on this screen).
My main class sets up the controller screens.
public class FieldMapCreator extends Application {
public static String mainID = "main";
public static String mainFile = "MainMapFXMLDocument.fxml";
public static String initialMapID = "initialMap";
public static String initialMapFile = "FXMLMapPicture.fxml";
#Override
public void start(Stage stage) throws Exception {
ScreensController mainContainer = new ScreensController();
mainContainer.loadScreen(FieldMapCreator.mainID, FieldMapCreator.mainFile);
mainContainer.loadScreen(FieldMapCreator.initialMapID, FieldMapCreator.initialMapFile);
mainContainer.setScreen(FieldMapCreator.mainID);
Group root = new Group();
root.getChildren().addAll(mainContainer);
Scene scene = new Scene(root);
stage.setScene(scene);
stage.show();
}
}
I get to the second screen by clicking a button on screen 1. Button1 code looks like:
#FXML
private void initialMapButtonAction(ActionEvent event) {
myController.setScreen(FieldMapCreator.initialMapID);
}
Screen 2 has a button that when clicked executes a method that creates data for the vbox. The fxml for screen 2 is this:
<AnchorPane id="AnchorPane" fx:id="mainAnchorPane" prefHeight="400.0" prefWidth="600.0" xmlns="http://javafx.com/javafx/8.0.40" xmlns:fx="http://javafx.com/fxml/1" fx:controller="FieldMapCreator.FXMLMapPictureController">
<children>
<SplitPane fx:id="mapSplitPane" dividerPositions="0.29797979797979796" layoutX="3.0" layoutY="7.0" prefHeight="400.0" prefWidth="600.0">
<items>
<AnchorPane fx:id="anchorPaneLeft" minHeight="0.0" minWidth="0.0" prefHeight="160.0" prefWidth="100.0">
<children>
<Button fx:id="backButton" layoutX="14.0" layoutY="348.0" mnemonicParsing="false" onAction="#goToMainScreen" text="Back" />
</children>
</AnchorPane>
<AnchorPane fx:id="anchorPaneRight" minHeight="0.0" minWidth="0.0" prefHeight="364.0" prefWidth="401.0">
<children>
<Button fx:id="vBoxShowMap" layoutX="24.0" layoutY="346.0" mnemonicParsing="false" onAction="#buildMapFromNurseries" prefHeight="26.0" prefWidth="91.0" text="show map" />
<VBox fx:id="mapVBox" layoutX="24.0" layoutY="24.0" onMouseClicked="#vBoxMouseClicked" prefHeight="200.0" prefWidth="309.0" />
</children></AnchorPane>
</items>
</SplitPane>
</children>
</AnchorPane>
The code that fails is in the "showInitialMap()" method of the controller class for screen 2. When executed, the last line of this method "mapVBox.getChildren().add(root)", results in a null pointer exception. I expected this to be resolved via the fxml injection. I also tried the commented out code to retrieve "mapVBox" using scene lookup, but it also results in a null pointer exception. The controller code is below:
public class FXMLMapPictureController implements Initializable, ControlledScreen {
ScreensController myController;
static MyNode[][] mainField ;
#FXML
private AnchorPane mainAnchorPane;
#FXML
private static SplitPane mapSplitPane;
#FXML
private AnchorPane anchorPaneLeft;
#FXML
private static AnchorPane anchorPaneRight;
#FXML
private static VBox mapVBox;
#FXML
private Button vBoxShowMap;
/**
* Initializes the controller class.
*/
#Override
public void initialize(URL url, ResourceBundle rb) {
// Should something be in here ??
}
public void goToMainScreen() {
myController.setScreen(FieldMapCreator.mainID);
}
public void showInitialMap() {
int row = userFieldMap.getNumRows();
int col = userFieldMap.getNumCols();
double gridWidth = 309/col;
double gridHeight = 200/row;
Group root = new Group();
// initialize the field
for (int idx = 0; idx < col; idx++) {
for (int jdx = 0; jdx < row; jdx++) {
// create plot
Plot plot = new Plot(idx, jdx, "Plot" + idx + "/" + jdx);
// Create node to hold the Plot
MyNode node = new MyNode(idx*gridWidth, jdx*gridHeight, gridWidth, gridHeight, plot);
// add plot to group
root.getChildren().add(node);
}
}
//Scene myScene = myController.getScene();
//VBox mapVBox = (VBox)myScene.lookup("#mapVBox");
mapVBox.getChildren().add(root);
}
#Override
public void setScreenParent(ScreensController screenParent) {
myController = screenParent;
}
Is there something missing from the initialize() method? I'm wondering if I am not setting something correctly when I change screens. I am able to access the initial screen's containers without problem from all screen 1 methods including the initialize class. When I attempt the same in screen 2, I get the message "screen hasn't been loaded !!!"
Thanks for any help you can provide.
You are using static fields as FXML injection targets which don't work in Java 8 and should not have been implemented to work in any other version, as static UI elements just don't make sense. Here's a bit more detailed answer. Having resorted to a static field most of the time means the software design of the application needs to be reconsidered.

Resources