1

Introduction:

I have created a canvas and my objective is creating circles at the place you click on the canvas. But I also want to be able to delete the circles I have drawn or at least the last drawn one. Redrawing at the same place where the circle is with background color is not a good solution. Because if there are other things under the circle they'll be deleted as well. For instance in this application 2 circles might have intersection and when you try to delete the 2nd circle those intersection points will be empty which would mean erasing some part of the first circle too. So I thought of creating 2nd canvas. And every time I click somewhere on the canvas before drawing the circle I would assign main canvas to backup canvas. And when I want to delete the last drawn object I could load the backup canvas.

Now I tried to do what I explained on the above and couldn't make it work as I intend it to do. In fact I even saw some strange (at least for me) behaviors on other things.

What I have done is basically creating 2 canvas at the same size.

public class Main extends Application {
    double ldx, ldy, diameter;
    Canvas lastCanvas = new Canvas(800,600);
    Canvas pane1 = new Canvas(800,600);

    @Override
    public void start(Stage primaryStage) throws Exception{
        System.out.println("Hello");
        HBox root = new HBox();

        GraphicsContext gc = pane1.getGraphicsContext2D();
        pane1.setOnMouseClicked(new EventHandler<MouseEvent>() {
            @Override
            public void handle(MouseEvent event) {
                // Draw circle around x,y
                lastCanvas = pane1;
                diameter = 25.0;
                ldx = event.getSceneX() - ( diameter/2 );
                ldy = event.getSceneY() - ( diameter/2 );
                gc.strokeOval(ldx, ldy, diameter, diameter);
                System.out.println("Clicked");
            }
        });
        VBox pane2 = new VBox(10);
        pane2.setPrefSize(224,600);
        Button button1 = new Button("Clear Last Drawn");
        button1.setOnMouseClicked(new EventHandler<MouseEvent>() {
            @Override
            public void handle(MouseEvent event) {
                pane1 = lastCanvas;
                System.out.println("Last canvas loaded");
            }
        });
        pane2.getChildren().addAll(button1);
        pane1.setStyle("-fx-background-color: #224488");
        pane2.setStyle("-fx-background-color: #224488");
        root.getChildren().addAll(pane1, pane2);
        Scene scene1 = new Scene(root, 1024, 600);
        primaryStage.setScene(scene1);
        primaryStage.setTitle("Mouse Clicker");
        primaryStage.show();

    }
}

Expected behavior is that every time I click on the button it'll load the lastCanvas which is the canvas before I drawn the last object. But it doesn't. I tried something like this inside button1's mouseevent

root.getChildren().removeAll();
root.getChildren().addAll(lastCanvas, pane2);

Which generates error on the console every time you click button. Another problem is that I tried to create 2nd scene.

HBox root2 = new HBox();
root2.getChildren().addAll(lastCanvas, pane2);

Doing this suprisingly deletes everything on the pane2 although I don't use root2 anywhere. All I do here is creating a new HBox and adding pane2 into that hbox too. Why doing this removes pane2 from my primaryStage instead leaves white space?

Additionally if you have other recommendation for removing the last object drawn on the canvas -once or until canvas is fully empty- you can also tell me that instead of trying to solve what I did wrong on the code above.

Vsky
  • 59
  • 1
  • 9
  • "Doing this suprisingly deletes everything on the pane2 although I don't use root2 anywhere. All I do here is creating a new HBox and adding pane2 into that hbox too. " => From [Node doc](https://docs.oracle.com/javase/8/javafx/api/javafx/scene/Node.html): "If a program adds a child node to a Parent (including Group, Region, etc) and that node is already a child of a different Parent or the root of a Scene, the node is automatically (and silently) removed from its former parent." One question per question is good :-) – jewelsea Apr 04 '17 at 17:34

3 Answers3

3

Without analyzing the problem, I propose the following solution using your code as a base

    private final double diameter = 25;
    private final LinkedList<Point2D> centers = new LinkedList<>();
    private final Canvas pane1 = new Canvas(800, 600);

    @Override
    public void start(Stage primaryStage) throws Exception
    {
        System.out.println("Hello");
        HBox root = new HBox();

        pane1.setOnMouseClicked((MouseEvent event) ->
        {
            centers.add(new Point2D(event.getSceneX(), event.getSceneY()));
            render();
            System.out.println("Clicked");
        });
        VBox pane2 = new VBox(10);
        pane2.setPrefSize(224, 600);
        Button button1 = new Button("Clear Last Drawn");
        button1.setOnMouseClicked((MouseEvent event) ->
        {
            if (!centers.isEmpty())
            {
                centers.removeLast();
                render();
                System.out.println("Last canvas loaded");
            }
        });
        pane2.getChildren().addAll(button1);
        pane1.setStyle("-fx-background-color: #224488");
        pane2.setStyle("-fx-background-color: #224488");
        root.getChildren().addAll(pane1, pane2);
        Scene scene1 = new Scene(root, 1024, 600);
        primaryStage.setScene(scene1);
        primaryStage.setTitle("Mouse Clicker Project (Berk YATKIN)");
        primaryStage.show();

    }

    private void render()
    {
        pane1.getGraphicsContext2D().clearRect(0, 0, pane1.getWidth(), pane1.getHeight());
        centers.forEach((p) ->
        {
            pane1.getGraphicsContext2D().strokeOval(
                    p.getX() - (diameter / 2),
                    p.getY() - (diameter / 2),
                    diameter,
                    diameter);
        });
    }
geh
  • 71
  • 11
  • I see your implementation stores each circle and every time it draws a circle it clears the whole canvas then draws each circle again. I'm not sure if it's a good solution or not but at least it works as I want it so thanks. By the way I casted get.SceneX and get.SceneY to floats because Point2D function expects float parameter hope it's okay to do this. Also in the render function it should be "p.x" and "p.y" instead of "p.getX()" and "p.getY". Again thanks for the reply this should be enough for me. – Vsky Apr 04 '17 at 19:43
  • Additionally is there any difference between your and my way of creating MouseClick events? I might also say IntelliJ's way since whole onMouseClick event was auto generated. – Vsky Apr 04 '17 at 19:53
  • Regarding Point2D, I use 'javafx.geometry.Point2D', and I think you are using 'com.sun.javafx.geom.Point2D'. As for the listeners, I used lamdas (Java 8) instead anonymous classes. [(Differences)](http://stackoverflow.com/questions/22637900/java8-lambdas-vs-anonymous-classes). And finally, you should take this example as a proof of concept, scene graph may be a better solution, but with canvas should be able to redraw hundreds of thousands of circles several times per second. – geh Apr 05 '17 at 07:06
1

Also without analyzing the problem I would first ask the question why you insist on using a canvas. The scene graph is much more suited for this kind of problems. You could add (almost) any number of circles to it and remove as many of them as you like in any order you like. What could be easier than that?

mipa
  • 10,369
  • 2
  • 16
  • 35
  • Thanks for the reply. I didn't insist on anything. I don't have knowledge about this stuff and when I first searched about Java GUI programming saw two things: Swing and JavaFX. After looking briefly I saw that JavaFX is newer and swing is old so wanted to go with new one. In the documantation of JavaFX I also saw that it shows how to draw objects on canvas and thought that I can use it. Didn't see "scene graph" that you mention. But I'd be glad to check out your implementation if you can write me one using scene graph. Thanks. – Vsky Apr 04 '17 at 19:47
1

You asked for it so here it is:

import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.Pane;
import javafx.scene.layout.VBox;
import javafx.scene.paint.Color;
import javafx.scene.shape.Circle;
import javafx.stage.Stage;

public class Main extends Application {
    @Override
    public void start(Stage primaryStage) throws Exception{
        BorderPane root = new BorderPane();

        Pane graphicsPane = new Pane();
        root.setCenter(graphicsPane);

        graphicsPane.setOnMouseClicked(e -> {
            // Draw circle around x,y
            Circle circle = new Circle(e.getX(), e.getY(), 50.0);
            circle.setStroke(Color.RED);
            circle.setStrokeWidth(4.0);
            circle.setFill(null);
            graphicsPane.getChildren().add(circle);
        });

        VBox pane2 = new VBox(10);
        pane2.setPrefWidth(224);
        Button button1 = new Button("Clear Last Drawn");
        button1.setOnAction(e -> {
            int numCircles = graphicsPane.getChildren().size();
            if (numCircles > 0) {
                graphicsPane.getChildren().remove(numCircles - 1);
            }
        });

        pane2.getChildren().addAll(button1);
        root.setRight(pane2);
        graphicsPane.setStyle("-fx-background-color: #aaaaaa");
        pane2.setStyle("-fx-background-color: #224488");
        Scene scene1 = new Scene(root, 1024, 600);
        primaryStage.setScene(scene1);
        primaryStage.setTitle("Mouse Clicker");
        primaryStage.show();

    }

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

}

This is a solution using the scene graph directly. This example could also be extended easily to move the circles arround or deleting a circle via graphical selection. All things which are not easily possible with a canvas.

mipa
  • 10,369
  • 2
  • 16
  • 35
  • Thanks for the code. But is there a way to filter the nodes inside graphicsPane? Because this way it doesn't erase the last drawn circle but rather erases the last drawn object. So is there a way to get only Circle children inside graphicPane? – Vsky Apr 05 '17 at 11:11
  • Of course. You just have to iterate over the list and find the last Circle. – mipa Apr 05 '17 at 14:57
  • It wasn't me who downvoted even if I did it wouldn't be shown since I don't have enough reputation. I don't know the reason of that guy but this helped me thanks. I found the way to get the last circle I'm going to submit edit to your post according to that in case another beginner like me wonders how to do it. – Vsky Apr 05 '17 at 15:24