Maven & JavaFX für Werner

Malen mit der Maus

Das Malen mit der Maus ist unter JavaFX ebenso kein Hexenwerk. Dazu benötigt man nur die passenden Event-Handler. Diese richten wir einfach auf dem Canvas ein. Einen, wenn wir die linke Maustaste drücken, einen, wenn wir die Maus mit gedrückter Maustaste bewegen, und einen, wenn wir die Maustaste wieder loslassen.
Beim Drücken der Maustaste, initieren wir einen neuen Pfad. Beim Bewegen der Maus mit gedrückter Maustaste, zeichnen wir eine Linie. Beim Loslassen der Maustaste beenden wir den Pfad und füllen die gezeichnete Fläche mit eine Farbe.
        // mouse event listener when pressing mouse button
        canvas.addEventHandler(MouseEvent.MOUSE_PRESSED, (pEvent) -> {
            // if left mouse button pressed, start a new draw
            if (pEvent.getButton() == MouseButton.PRIMARY) {
                gc.beginPath();
                gc.setStroke(strokeColor);
                gc.setFill(fillColor);
                gc.moveTo(pEvent.getSceneX(), pEvent.getSceneY());
            }
        });
Sieht doch sehr selbsterklärend aus. Auf dem Canvas fügen wir einen Event-Handler hinzu, der angesprungen wird, wenn wir eine Maustaste drücken. Ist es die primäre Maustaste, initieren wir einen neuen Pfad, setzen die Pfad-Farbe und bewegen die aktuelle Position dort hin.
        // mouse event listener when holding mouse button and move the mouse
        canvas.addEventHandler(MouseEvent.MOUSE_DRAGGED, (pEvent) -> {
            // if left mouse button is down while moving
            if (pEvent.getButton() == MouseButton.PRIMARY) {
                gc.lineTo(pEvent.getSceneX(), pEvent.getSceneY());
                gc.stroke();
            }
        });
Das gleiche Spiel in Grün. Auf dem Canvas fügen wir einen Event-Handler hinzu, der angesprungen wird, wenn wir bei gedrückter Maustaste die Maus bewegen. Ist die Maustaste die primäre Taste, zeichnen wir eine Linie von der bisherigen Position zur neuen Position.
        // mouse event listener when release mouse button
        canvas.addEventHandler(MouseEvent.MOUSE_RELEASED, (pEvent) -> {
            // if left mouse button is released, fill drawn graphics
            if (pEvent.getButton() == MouseButton.PRIMARY) {
                gc.lineTo(pEvent.getSceneX(), pEvent.getSceneY());
                gc.stroke();
                gc.fill();
            }
        });
Und auch hier noch mal das gleiche in Grün. Nur malen wir noch die letzte Linie, und füllen dann die gezeichnete Fläche mit unserer Füllfarbe.
Der komplette Code könnte dann so aussehen:
package de.wh.javafx;

import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.canvas.Canvas;
import javafx.scene.canvas.GraphicsContext;
import javafx.scene.control.ContextMenu;
import javafx.scene.control.Menu;
import javafx.scene.control.MenuItem;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;
import javafx.scene.input.MouseButton;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.Pane;
import javafx.scene.paint.Color;
import javafx.stage.Stage;

/**
 * Hello world!
 *
 */
public class App extends Application {

    public static void main(String[] args) {
        System.out.println("Hello World!");
        launch(args);
    }

    private GraphicsContext gc;                 // used graphics context
    private Color strokeColor = Color.GREEN;    // used line color
    private Color fillColor = Color.YELLOW;     // used fill color

    
    @Override
    public void start(Stage primaryStage) throws Exception {
        Pane root = new Pane();

        Canvas canvas = new Canvas(900, 900);
        root.getChildren().add(canvas);

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

        primaryStage.setTitle("Hallo Werner");
        primaryStage.setScene(scene);
        primaryStage.show();

        // get the graphics context for use in event handlers
        gc = canvas.getGraphicsContext2D();

        // mouse event listener when pressing mouse button
        canvas.addEventHandler(MouseEvent.MOUSE_PRESSED, (pEvent) -> {
            // if left mouse button pressed, start a new draw
            if (pEvent.getButton() == MouseButton.PRIMARY) {
                gc.beginPath();
                gc.setStroke(strokeColor);
                gc.moveTo(pEvent.getSceneX(), pEvent.getSceneY());
            }
        });
        // mouse event listener when holding mouse button and move the mouse
        canvas.addEventHandler(MouseEvent.MOUSE_DRAGGED, (pEvent) -> {
            // if left mouse button is down while moving
            if (pEvent.getButton() == MouseButton.PRIMARY) {
                gc.lineTo(pEvent.getSceneX(), pEvent.getSceneY());
                gc.stroke();
            }
        });
        // mouse event listener when release mouse button
        canvas.addEventHandler(MouseEvent.MOUSE_RELEASED, (pEvent) -> {
            // if left mouse button is released, fill drawn graphics
            if (pEvent.getButton() == MouseButton.PRIMARY) {
                gc.lineTo(pEvent.getSceneX(), pEvent.getSceneY());
                gc.stroke();
                gc.setFill(fillColor);
                gc.fill();
            }
        });

    }

}
Startet man nun dieses Programm, kann man mit der Maus eine grüne Kurve zeichnen, die beim Loslassen Gelb eingefärbt wird.
Ein Löschen der Zeichenfläche, könnte man einfach durch ein Tastaturkürzel auslösen. Z.b. mit Strg-C. Ein passenden Event-Handler könnte so aussehen:
        // ctrl-c clears the area
        scene.addEventHandler(KeyEvent.KEY_RELEASED, (pEvent) -> {
            if (pEvent.getCode() == KeyCode.C && pEvent.isControlDown()) {
                gc.setFill(Color.WHITE);
                gc.fillRect(0, 0, 900, 900);
            }
        });
Man beachte, dass dieser Event-Handler nicht auf dem Canvas, sondern auf der Scene eingehängt wird. Das liegt daran, dass ein Canvas zum Malen da ist, und daher keine Tastatur-Events erhält.
Jetzt wäre es noch schön, wenn man die Linie- und Flächenfarbe ändern könnte. Eine einfache Idee wäre, diese Änderungen über ein Kontextmenü zu machen. Man drückt die rechte Maustaste, bekommt ein Menü mit zwei Punkten ("Linie", "Fläche") angezeigt, und dadrunter gibt es die Farben zum Auswählen. Das geht in JavaFX - natürlich - auch ganz einfach. Man muss nur ein neues Kontextmenü anlegen, die Menüeinträge und Farbeinträge hinzufügen, und dem Canvas sagen, dass er dieses Kontextmenü anzeigen soll, wenn wir die rechte Maustaste drücken:
        // a new context menu to choose the colors
        ContextMenu contextMenu = new ContextMenu();
        
        // sub menu for stroke colors
        Menu stroke = new Menu("stroke");
        // stroke color red
        MenuItem strokeRed = new MenuItem("red");
        strokeRed.setOnAction((pEvent) -> {
            strokeColor = Color.RED;
        });
        // stroke color green
        MenuItem strokeGreen = new MenuItem("green");
        strokeGreen.setOnAction((pEvent) -> {
            strokeColor = Color.GREEN;
        });
        // stroke color blue
        MenuItem strokeBlue = new MenuItem("blue");
        strokeBlue.setOnAction((pEvent) -> {
            strokeColor = Color.BLUE;
        });
        // add red, green blue to stroke color sub menu
        stroke.getItems().addAll(strokeRed, strokeGreen, strokeBlue);
        
        // sub menu for fill colors
        Menu fill = new Menu("fill");
        // fill color cyan
        MenuItem fillCyan = new MenuItem("cyan");
        fillCyan.setOnAction((pEvent) -> {
            fillColor = Color.CYAN;
        });
        // fill color magenta
        MenuItem fillMagenta = new MenuItem("magenta");
        fillMagenta.setOnAction((pEvent) -> {
            fillColor = Color.MAGENTA;
        });
        // fill color yellow
        MenuItem fillYellow = new MenuItem("yellow");
        fillYellow.setOnAction((pEvent) -> {
            fillColor = Color.YELLOW;
        });
        // fill color black
        MenuItem fillKey = new MenuItem("key");
        fillKey.setOnAction((pEvent) -> {
            fillColor = Color.BLACK;
        });
        // fill color none
        MenuItem fillNone = new MenuItem("none");
        fillNone.setOnAction((pEvent) -> {
            fillColor = Color.TRANSPARENT;
        });
        // add cyan, magenta, yellow, key and none to fill color sub menu
        fill.getItems().addAll(fillCyan, fillMagenta, fillYellow, fillKey, fillNone);
        
        // add stroke and fill color to context menu
        contextMenu.getItems().addAll(stroke, fill);
        
        // if right click on canvas, show context menu
        canvas.setOnContextMenuRequested((pEvent) -> {
            contextMenu.show(scene.getWindow(), pEvent.getScreenX(), pEvent.getScreenY());
        });
Fügt man diesen Code zum Programm hinzu, so kann man die Linie- und Flächenfarbe ändern. Die Logik ist leicht zu erkennen. Wir legen ein ContextMenu an, hängen zwei Menu darunter (stroke, fill), unter jedem Menu ein paar MenuItem (die Farben) und den MenuItem sagen wir noch, was passieren soll, wenn wir das MenuItem auswählen (setzen der strokeColor bzw. fillColor).
Nur noch zwei Bonbon. Das Kontextmenü können wir natürlich auch zum Setzen der Liniestärke verwenden, und es wäre schön, wenn man einen bereits begonnenen Pfad fortführen könnte. Beides super einfach. Die Linienstärke wird genauso wie die Farben ins Kontextmenü eingehängt:
    private Color strokeColor = Color.GREEN;
    private double lineWidth = 1;
    private Color fillColor = Color.YELLOW;
...
        // sub menu for line width
        Menu width = new Menu("width");
        // one point
        MenuItem one = new MenuItem("1px");
        one.setOnAction((pEvent) -> {
            lineWidth = 1;
        });
        // two point
        MenuItem two = new MenuItem("2px");
        two.setOnAction((pEvent) -> {
            lineWidth = 2;
        });
        // four point
        MenuItem four = new MenuItem("4px");
        four.setOnAction((pEvent) -> {
            lineWidth = 4;
        });
        // eight point
        MenuItem eight = new MenuItem("8px");
        eight.setOnAction((pEvent) -> {
            lineWidth = 8;
        });
        width.getItems().addAll(one, two, four, eight);
...
        contextMenu.getItems().addAll(stroke, width, fill);
...
        // mouse event listener when pressing mouse button
        canvas.addEventHandler(MouseEvent.MOUSE_PRESSED, (pEvent) -> {
            // if left mouse button pressed, start a new draw
            if (pEvent.getButton() == MouseButton.PRIMARY) {
                gc.beginPath();
                gc.setStroke(strokeColor);
                gc.setLineWidth(lineWidth);
                gc.moveTo(pEvent.getSceneX(), pEvent.getSceneY());
            }
        });
...
        
... und beim Event-Handler für das Drücken der Maustaste fügen wir hinzu, dass nur ein neuer Pfad anfängt, wenn wir die Shift-Taste nicht dazu drücken:
        // mouse event listener when pressing mouse button
        canvas.addEventHandler(MouseEvent.MOUSE_PRESSED, (pEvent) -> {
            // if left mouse button pressed and shift is not pressed, start a new draw
            if (pEvent.getButton() == MouseButton.PRIMARY && !pEvent.isShiftDown()) {
...
Halten wir also die Shift-Taste beim drücken der Maustaste und bewegen dann die Maus, malen wir den vorherigen Pfad weiter.
Der ganze Code könnte dann so aussehen:
package de.wh.javafx;

import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.canvas.Canvas;
import javafx.scene.canvas.GraphicsContext;
import javafx.scene.control.ContextMenu;
import javafx.scene.control.Menu;
import javafx.scene.control.MenuItem;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;
import javafx.scene.input.MouseButton;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.Pane;
import javafx.scene.paint.Color;
import javafx.stage.Stage;

/**
 * Hello world!
 *
 */
public class App extends Application {

    public static void main(String[] args) {
        System.out.println("Hello World!");
        launch(args);
    }

    private GraphicsContext gc;
    private Color strokeColor = Color.GREEN;
    private double lineWidth = 1;
    private Color fillColor = Color.TRANSPARENT;
    
    @Override
    public void start(Stage primaryStage) throws Exception {
        Pane root = new Pane();

        Canvas canvas = new Canvas(900, 900);
        root.getChildren().add(canvas);

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

        primaryStage.setTitle("Hallo Werner");
        primaryStage.setScene(scene);
        primaryStage.show();

        gc = canvas.getGraphicsContext2D();

        // a new context menu to choose the colors
        ContextMenu contextMenu = new ContextMenu();
        
        // sub menu for stroke colors
        Menu stroke = new Menu("stroke");
        // stroke color red
        MenuItem strokeRed = new MenuItem("red");
        strokeRed.setOnAction((pEvent) -> {
            strokeColor = Color.RED;
        });
        // stroke color green
        MenuItem strokeGreen = new MenuItem("green");
        strokeGreen.setOnAction((pEvent) -> {
            strokeColor = Color.GREEN;
        });
        // stroke color blue
        MenuItem strokeBlue = new MenuItem("blue");
        strokeBlue.setOnAction((pEvent) -> {
            strokeColor = Color.BLUE;
        });
        // add red, green blue to stroke color sub menu
        stroke.getItems().addAll(strokeRed, strokeGreen, strokeBlue);
        
        // sub menu for line width
        Menu width = new Menu("width");
        // one point
        MenuItem one = new MenuItem("1px");
        one.setOnAction((pEvent) -> {
            lineWidth = 1;
        });
        // two point
        MenuItem two = new MenuItem("2px");
        two.setOnAction((pEvent) -> {
            lineWidth = 2;
        });
        // four point
        MenuItem four = new MenuItem("4px");
        four.setOnAction((pEvent) -> {
            lineWidth = 4;
        });
        // eight point
        MenuItem eight = new MenuItem("8px");
        eight.setOnAction((pEvent) -> {
            lineWidth = 8;
        });
        width.getItems().addAll(one, two, four, eight);
        
        // sub menu for fill colors
        Menu fill = new Menu("fill");
        // fill color cyan
        MenuItem fillCyan = new MenuItem("cyan");
        fillCyan.setOnAction((pEvent) -> {
            fillColor = Color.CYAN;
        });
        // fill color magenta
        MenuItem fillMagenta = new MenuItem("magenta");
        fillMagenta.setOnAction((pEvent) -> {
            fillColor = Color.MAGENTA;
        });
        // fill color yellow
        MenuItem fillYellow = new MenuItem("yellow");
        fillYellow.setOnAction((pEvent) -> {
            fillColor = Color.YELLOW;
        });
        // fill color black
        MenuItem fillKey = new MenuItem("key");
        fillKey.setOnAction((pEvent) -> {
            fillColor = Color.BLACK;
        });
        // fill color none
        MenuItem fillNone = new MenuItem("none");
        fillNone.setOnAction((pEvent) -> {
            fillColor = Color.TRANSPARENT;
        });
        // add cyan, magenta, yellow, key and none to fill color sub menu
        fill.getItems().addAll(fillCyan, fillMagenta, fillYellow, fillKey, fillNone);
        
        // add stroke and fill color to context menu
        contextMenu.getItems().addAll(stroke, width, fill);
        
        // if right click on canvas, show context menu
        canvas.setOnContextMenuRequested((pEvent) -> {
            contextMenu.show(scene.getWindow(), pEvent.getScreenX(), pEvent.getScreenY());
        });
        
        // mouse event listener when pressing mouse button
        canvas.addEventHandler(MouseEvent.MOUSE_PRESSED, (pEvent) -> {
            // if left mouse button pressed and shift is not pressed, start a new draw
            if (pEvent.getButton() == MouseButton.PRIMARY && !pEvent.isShiftDown()) {
                gc.beginPath();
                gc.setStroke(strokeColor);
                gc.setLineWidth(lineWidth);
                gc.setFill(fillColor);
                gc.moveTo(pEvent.getSceneX(), pEvent.getSceneY());
            }
        });
        // mouse event listener when holding mouse button and move the mouse
        canvas.addEventHandler(MouseEvent.MOUSE_DRAGGED, (pEvent) -> {
            // if left mouse button is down while moving
            if (pEvent.getButton() == MouseButton.PRIMARY) {
                gc.lineTo(pEvent.getSceneX(), pEvent.getSceneY());
                gc.stroke();
            }
        });
        // mouse event listener when release mouse button
        canvas.addEventHandler(MouseEvent.MOUSE_RELEASED, (pEvent) -> {
            // if left mouse button is released, fill drawn graphics
            if (pEvent.getButton() == MouseButton.PRIMARY) {
                gc.lineTo(pEvent.getSceneX(), pEvent.getSceneY());
                gc.stroke();
                gc.fill();
            }
        });

        // ctrl-c clears the area
        scene.addEventHandler(KeyEvent.KEY_RELEASED, (pEvent) -> {
            if (pEvent.getCode() == KeyCode.C && pEvent.isControlDown()) {
                gc.setFill(Color.WHITE);
                gc.fillRect(0, 0, 900, 900);
            }
        });
    }

}
Fertig. Weiter geht es mit Teil 5: Gierige Programmierer, oder zurück zu Teil 3: Jetzt wird gemalt