Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to implement selection of Nodes in JavaFX

I am very new JavaFX, started learning it yesterday. Spent the whole day reading through the documentation, but learned nothing...

Here is what I want to do, make a simple JavaFX application that creates a circle. On click its stroke turns orange (some color). On unlicked (click on anything other than that circle), the stroke turns (some color) white.

Here is my pseudo code what I have so far. I want to make a separate class that creates a circle and handle the events (important).

public class Something extends Application {

    @Override
    public void start(Stage primaryStage) {
        MyCircle c1 = new MyCircle();
        c1.setCircle(20, 0, 0);

        TilePane root = new TilePane();
        root.getChildren().add(c1.getCircle());
        //Some kind of mouse event listener, not sure which one should be
        c1.getCircle().addEventListener(); //pseudo code

        Scene scene = new Scene(root, 400, 400);

        primaryStage.setTitle("Circle");
        primaryStage.setScene(scene);
        primaryStage.show();
    }

    public static void main(String[] args) {
        launch(args);
    }

}

This the class that should create a circle and handle all the events, such as, mouse click, mouse location, click and drag and stuff like that.

public class MyCircle implements EventHandler{
    Circle circle = new Circle();

    public void setCircle(int radius, int x, int y){
        circle.setRadius(radius);
        position(x,y);
        circle.setStrokeWidth(3);
        circle.setStroke(Color.valueOf("white"));
    }

    public Circle getCircle(){
        return circle;
    }

    public void position(int x, int y){
        circle.setTranslateX(x);
        circle.setTranslateY(y);
    }

    public void selected(){
        circle.setStroke(Color.valueOf("orange"));
    }

    public void unselected() {
        circle.setStroke(Color.valueOf("white"));
    }

    @Override
    public void handle(Event event) {
        if (event == MOUSE_CLICKED){ //pseudo code
            selected();
        }
        else if(event == MOUSE_UNCLICKED){ //pseudo code
            unselected();
        }
    }
}

Since I am very new to JavaFX, I'd much appreciate explanation as well. Thanks!


EDIT: This is my pseudo code, and I want to convert it into an actual working code. I am not sure how would I do that. Any help would be appreciated.


ANOTHER EDIT: Everything is a code except 3 marked places. Please look for my comment Psuedo Code within the code, where I need help to change the pseudo code into an actual code.

like image 682
Hafiz Temuri Avatar asked Dec 01 '16 17:12

Hafiz Temuri


People also ask

What are nodes in JavaFX?

Each item in the scene graph is called a Node . Branch nodes are of type Parent , whose concrete subclasses are Group , Region , and Control , or subclasses thereof. Leaf nodes are classes such as Rectangle , Text , ImageView , MediaView , or other such leaf classes which cannot have children.

Is a control a node JavaFX?

A "Control" is a node in the scene graph which can be manipulated by the user.

Can a JavaFX application have more than one stage object?

This primary stage object creates a window to display user created functionality in the output. If your requirement is more than one stage then we can create as many Stage objects as we want. JavaFX Stage class available in javafx.


3 Answers

You can use some of the in-built Java functions to help accomplish your task.

For example CSS PsuedoClasses and Toggles managed by a ToggleGroup.

It isn't necessary to do it this way, the solution you have which does not use these other JavaFX features, is just fine. It is just kind of neat to use some of the standard JavaFX functions.

lights

LightApp.java

import javafx.application.Application;
import javafx.scene.Scene;
import javafx.stage.Stage;

public class LightApp extends Application {

    @Override
    public void start(final Stage stage) throws Exception {
        final Bulb[] bulbs = {
                new Bulb(),
                new Bulb(),
                new Bulb()
        };

        Scene scene = new Scene(new LightArray(bulbs));
        stage.setScene(scene);
        stage.show();
    }

    public static void main(String[] args) {
        launch(args);
    }

}

LightArray.java

import javafx.geometry.Insets;
import javafx.scene.control.ToggleGroup;
import javafx.scene.layout.HBox;

public class LightArray extends HBox {
    public LightArray(Bulb... bulbs) {
        super(10, bulbs);
        setPadding(new Insets(10));

        ToggleGroup toggleGroup = new ToggleGroup();
        for (Bulb bulb: bulbs) {
            bulb.setToggleGroup(toggleGroup);
        }

        setOnMouseClicked(event -> {
            if (event.getTarget() instanceof Bulb) {
                toggleGroup.selectToggle((Bulb) event.getTarget());
            } else {
                toggleGroup.selectToggle(null);
            }
        });

        getStylesheets().add(
                this.getClass().getResource("bulb.css").toExternalForm()
        );
    }
}

Bulb.java

import javafx.beans.property.*;
import javafx.css.PseudoClass;
import javafx.scene.control.*;
import javafx.scene.shape.Circle;

class Bulb extends Circle implements Toggle {
    private ObjectProperty<ToggleGroup> toggleGroup = new SimpleObjectProperty<>();

    Bulb() {
        super(30);
        getStyleClass().add("bulb");
    }

    @Override
    public void setSelected(boolean selected) {
        this.selected.set(selected);
    }

    @Override
    public boolean isSelected() {
        return selected.get();
    }

    @Override
    public BooleanProperty selectedProperty() {
        return selected;
    }

    public BooleanProperty selected =
            new BooleanPropertyBase(false) {
                @Override protected void invalidated() {
                    pseudoClassStateChanged(ON_PSEUDO_CLASS, get());
                }

                @Override public Object getBean() {
                    return Bulb.this;
                }

                @Override public String getName() {
                    return "on";
                }
            };

    private static final PseudoClass
            ON_PSEUDO_CLASS = PseudoClass.getPseudoClass("on");

    @Override
    public ToggleGroup getToggleGroup() {
        return toggleGroup.get();
    }

    @Override
    public void setToggleGroup(ToggleGroup toggleGroup) {
        this.toggleGroup.set(toggleGroup);
    }

    @Override
    public ObjectProperty<ToggleGroup> toggleGroupProperty() {
        return toggleGroup;
    }
}

bulb.css

.bulb {
    -fx-fill: lightslategray;
}

.bulb:on {
    -fx-fill: gold;
}

An additional common thing which is often done with JavaFX (and I haven't done here), is to make items which can be styled by CSS (e.g. Regions or Panes) and then apply the styling to them. For example, instead of the Bulb extending Circle, it could extend StackPane and then the bulb shape could be customized in css via multiple layered backgrounds and svg shapes (this is how other similar things such as radio buttons are implemented).

like image 145
jewelsea Avatar answered Nov 01 '22 10:11

jewelsea


Handling selection status of a Node requires some knowledge not available within the Node. You'll going to need to know whether the mouse event happened somewhere else (e.g. other Nodes or the root Pane), so you may have to pass questionable arguments to it.

In general, it's not a good idea to delegate mouse event handling to MyCircle. Instead it's better to specify the selection behavior in that class and delegate selection handling to a separate helper class which has enough knowledge to handle the problem. I created this gist to show how this task can be done.

public class SelectionDemo extends Application {
    @Override
    public void start(Stage primaryStage) {
        Scene scene = new Scene(createPane(), 400, 400);
        primaryStage.setScene(scene);
        primaryStage.show();
    }

    private Parent createPane() {
        BorderPane root = new BorderPane();
        SelectionHandler selectionHandler = new SelectionHandler(root);
        root.addEventHandler(MouseEvent.MOUSE_PRESSED, selectionHandler.getMousePressedEventHandler());

        MyCircle c1 = new MyCircle(40, 40, 20);
        MyCircle c2 = new MyCircle(40, 100, 20);
        MyCircle c3 = new MyCircle(40, 160, 20);
        root.getChildren().addAll(c1, c2, c3);

        return root;
    }

    public static void main(String[] args) {
        launch(args);
    }
}

I borrowed and modified an interface from jfxtras-labs to represent a selectable Node. It's good to have this interface to differentiate between selectable nodes and unselectable ones:

/**
 * This interface is based on jfxtras-labs <a href="https://github.com/JFXtras/jfxtras-labs/blob/8.0/src/main/java/jfxtras/labs/scene/control/window/SelectableNode.java">SelectableNode</a>
 */
public interface SelectableNode {
    public boolean requestSelection(boolean select);

    public void notifySelection(boolean select);
}

classes who wish to be selectable must implement this interface and specify their selection behavior in implementation of notifySelection method:

public class MyCircle extends Circle implements SelectableNode {
    public MyCircle(double centerX, double centerY, double radius) {
        super(centerX, centerY, radius);
    }

    @Override
    public boolean requestSelection(boolean select) {
        return true;
    }

    @Override
    public void notifySelection(boolean select) {
        if(select)
            this.setFill(Color.RED);
        else
            this.setFill(Color.BLACK);
    }
}

And finally the selection handler class:

public class SelectionHandler {
    private Clipboard clipboard;

    private EventHandler<MouseEvent> mousePressedEventHandler;

    public SelectionHandler(final Parent root) {
        this.clipboard = new Clipboard();
        this.mousePressedEventHandler = new EventHandler<MouseEvent>() {
            @Override
            public void handle(MouseEvent event) {
                SelectionHandler.this.doOnMousePressed(root, event);
                event.consume();
            }
        };
    }

    public EventHandler<MouseEvent> getMousePressedEventHandler() {
        return mousePressedEventHandler;
    }

    private void doOnMousePressed(Parent root, MouseEvent event) {
        Node target = (Node) event.getTarget();
        if(target.equals(root))
            clipboard.unselectAll();
        if(root.getChildrenUnmodifiable().contains(target) && target instanceof SelectableNode) {
            SelectableNode selectableTarget = (SelectableNode) target;
            if(!clipboard.getSelectedItems().contains(selectableTarget))
                clipboard.unselectAll();
            clipboard.select(selectableTarget, true);
        }
    }

    /**
     * This class is based on jfxtras-labs
     *  <a href="https://github.com/JFXtras/jfxtras-labs/blob/8.0/src/main/java/jfxtras/labs/scene/control/window/Clipboard.java">Clipboard</a>
     *  and 
     *  <a href="https://github.com/JFXtras/jfxtras-labs/blob/8.0/src/main/java/jfxtras/labs/util/WindowUtil.java">WindowUtil</a>
     */
    private class Clipboard {
        private ObservableList<SelectableNode> selectedItems = FXCollections.observableArrayList();

        public ObservableList<SelectableNode> getSelectedItems() {
            return selectedItems;
        }

        public boolean select(SelectableNode n, boolean selected) {
            if(n.requestSelection(selected)) {
                if (selected) {
                    selectedItems.add(n);
                } else {
                    selectedItems.remove(n);
                }
                n.notifySelection(selected);
                return true;
            } else {
                return false;
            }
        }

        public void unselectAll() {
            List<SelectableNode> unselectList = new ArrayList<>();
            unselectList.addAll(selectedItems);

            for (SelectableNode sN : unselectList) {
                select(sN, false);
            }
        }
    }
}

enter image description here

like image 45
Omid Avatar answered Nov 01 '22 11:11

Omid


This is how I did it... Simple and Easy

Main Class

public class Lab05 extends Application {

    @Override
    public void start(Stage primaryStage) {
        double width = 400;
        double height = 400;

        int num_of_circles = 5;
        int radius_of_circles = 20;

        BorderPane root = new BorderPane();

        for (int i = 0; i < num_of_circles; i++) {
            MyCircle temp = createCircle(radius_of_circles, (50 * i) + 1, 100);
            temp.setFrame(width, height);
            root.getChildren().add(temp.getCircle());
            temp.getCircle().addEventFilter(MouseEvent.ANY, temp);
        }

        Scene scene = new Scene(root, width, height);

        primaryStage.setTitle("Lab 05");
        primaryStage.setScene(scene);
        primaryStage.show();
    }

    public static void main(String[] args) {
        launch(args);
    }

    public static MyCircle createCircle(int radius, int x, int y){
        MyCircle circle = new MyCircle();
        circle.setCircle(radius, x, y);
        return circle;
    }
}

MyCircle Class

public class MyCircle implements EventHandler<MouseEvent>{
    private static double frameX = 0;
    private static double frameY = 0;
    private final Circle circle = new Circle();
    private static final List<Circle> CIRCLES = new ArrayList<>();

    public void setCircle(int radius, int x, int y){
        circle.setRadius(radius);
        position(x,y);
        circle.setStrokeWidth(3);
        circle.setStroke(Color.valueOf("white"));
        CIRCLES.add(circle);
    }

    public void setFrame(double x, double y){
        frameX = x;
        frameY = y;
    }

    public Circle getCircle(){
        return circle;
    }

    public void position(double x, double y){
        if ( x < frameX && x > 0)
            circle.setCenterX(x);
        if ( y < frameY && y > 0)
            circle.setCenterY(y);
    }

    public void selected(){
        CIRCLES.stream().forEach((c) -> {
            c.setStroke(Color.valueOf("white"));
        });
        circle.setStroke(Color.valueOf("orange"));
    }

    public void unselected() {
        circle.setStroke(Color.valueOf("white"));
    }

    @Override
    public void handle(MouseEvent event) {
        if (event.getEventType() == MouseEvent.MOUSE_PRESSED){
            selected();
        }
        else if (event.getEventType() == MouseEvent.MOUSE_DRAGGED){
            position(event.getX(), event.getY());
        }
    }
}
like image 26
Hafiz Temuri Avatar answered Nov 01 '22 12:11

Hafiz Temuri