1

The program I am working on simply paints over text or images by using a canvas layer over a StackPane. What I want to accomplish is that when I release the mouse the MouseEvent.MOUSE_RELEASED handler it will automatically get a snapshot of the Canvas, add the image to an ImageView Cover and display it on top of the TextArea, but it cannot add the changes to the class StackPane, namely the ImageView.

What I have here is a program that I will add to another one I'm working on, and I plan to take everything from the main class, TextCanvas, into the menu controller class from the main project.

Main class TextCanvas.java:

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.util.logging.Level;
import java.util.logging.Logger;
import javafx.application.Application;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.embed.swing.SwingFXUtils;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.scene.Scene;
import javafx.scene.SnapshotParameters;
import javafx.scene.canvas.Canvas;
import javafx.scene.canvas.GraphicsContext;
import javafx.scene.control.Button;
import javafx.scene.control.ScrollPane;
import javafx.scene.control.TextArea;
import javafx.scene.control.Toggle;
import javafx.scene.control.ToggleButton;
import javafx.scene.control.ToggleGroup;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.image.PixelReader;
import javafx.scene.image.PixelWriter;
import javafx.scene.image.WritableImage;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import javafx.scene.paint.Color;
import javafx.stage.Screen;
import javafx.stage.Stage;
import javax.imageio.ImageIO;

public class TextCanvas extends Application
{
    private ScrollPane Scroll = new ScrollPane();

    Canvas Can = new Canvas(800, 400);
    GraphicsContext GG = Can.getGraphicsContext2D();

    TextArea TA = new TextArea();

    ImageView Cover = new ImageView();

    VBox ButtonBox = new VBox();

    WordCanvas WC = new WordCanvas(Can, TA, GG);

    @Override
    public void start(Stage PrimaryStage)
    {
        ToggleGroup DrawErase = new ToggleGroup();

        ToggleButton Draw = new ToggleButton("Draw");
        ToggleButton Erase = new ToggleButton("Erase");
        Button Clear = new Button("Clear");
        Draw.setToggleGroup(DrawErase);
        Erase.setToggleGroup(DrawErase);

        double DotsPerInch = Screen.getPrimary().getDpi();
        double W = DotsPerInch * 8.5;
        double H = DotsPerInch * 11.0;

        StackPane Stack = new WordCanvas(W, H);

        WC.GetArea().setMaxWidth(W);
        WC.GetArea().setMaxHeight(H);
        WC.GetCan().setWidth(W);
        WC.GetCan().setHeight(H);

        DrawErase.selectedToggleProperty().addListener(new ChangeListener<Toggle>()
        {
            public void changed(ObservableValue<? extends Toggle> OV, Toggle TOld, Toggle TNew)
            {
                if(TNew == null)
                {
                    GG.setStroke(Color.TRANSPARENT);

                    Stack.getChildren().remove(WC.GetCan());
                }
                else if(DrawErase.getSelectedToggle().equals(Draw))
                {
                    GG.setStroke(Color.BLACK);

                    if(!Stack.getChildren().contains(WC.GetCan()))
                    {
                        Stack.getChildren().add(WC.GetCan());
                    }
                }
                else if(DrawErase.getSelectedToggle().equals(Erase))
                {
                    GG.setStroke(Color.WHITE);

                    if(!Stack.getChildren().contains(WC.GetCan()))
                    {
                        Stack.getChildren().add(WC.GetCan());
                    }
                }
            }
        });

        Clear.setOnAction(new EventHandler<ActionEvent>()
        {
            @Override
            public void handle(ActionEvent e)
            {
                WC.GetGC().clearRect(0, 0, W, H);

                Stack.getChildren().remove(WC.GetCover());
            }
        });

        Button Snap = new Button("Snap");

        Snap.setOnAction(new EventHandler<ActionEvent>()
        {
            @Override
            public void handle(ActionEvent e)
            {
                Cover.setMouseTransparent(true);

                WritableImage WImage = Can.snapshot(new SnapshotParameters(), null);

                ByteArrayOutputStream Bos = new ByteArrayOutputStream();

                try
                {
                    ImageIO.write(SwingFXUtils.fromFXImage(WImage, null), "png", Bos);
                }
                catch(IOException ex)
                {
                    Logger.getLogger(WordCanvas.class.getName()).log(Level.SEVERE, null, ex);
                }

                InputStream Fis = new ByteArrayInputStream(Bos.toByteArray());
                Image Imos = new Image(Fis);

                int IW = (int) Imos.getWidth();
                int IH = (int) Imos.getHeight();

                WritableImage OutputImage = new WritableImage(IW, IH);
                PixelReader PReader = Imos.getPixelReader();
                PixelWriter PWriter = OutputImage.getPixelWriter();

                for (int y = 0; y < IH; y++)
                {
                    for (int x = 0; x < IW; x++)
                    {
                        int argb = PReader.getArgb(x, y);

                        int r = (argb >> 16) & 0xFF;
                        int g = (argb >> 8) & 0xFF;
                        int b = argb & 0xFF;

                        if(r >= 0xCF && g >= 0xCF && b >= 0xCF)
                        {
                            argb &= 0x00FFFFFF;
                        }

                        PWriter.setArgb(x, y, argb);
                    }
                }

                if(!Stack.getChildren().contains(WC.GetCover()))
                {
                    WC.GetCover().setImage(OutputImage);

                    Stack.getChildren().add(WC.GetCover());
                }
                else
                {
                    WC.GetCover().setImage(OutputImage);
                }
            }
        });

        ButtonBox.getChildren().addAll(Draw, Erase, Clear, Snap);

        BorderPane Border = new BorderPane();
        Border.setCenter(Stack);
        Border.setBottom(ButtonBox);

        Scroll.setContent(Border);
        Scroll.setFitToWidth(true);
        Scroll.setFitToHeight(true);

        Scene MainScene = new Scene(Scroll);

        PrimaryStage.setMaximized(true);
        PrimaryStage.setTitle("Practice Canvas");
        PrimaryStage.setScene(MainScene);
        PrimaryStage.show();
    }

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

Secondary class that contains the TextArea and Canvas:

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.util.logging.Level;
import java.util.logging.Logger;
import javafx.embed.swing.SwingFXUtils;
import javafx.event.EventHandler;
import javafx.geometry.Insets;
import javafx.scene.Cursor;
import javafx.scene.SnapshotParameters;
import javafx.scene.canvas.Canvas;
import javafx.scene.canvas.GraphicsContext;
import javafx.scene.control.TextArea;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.image.PixelReader;
import javafx.scene.image.PixelWriter;
import javafx.scene.image.WritableImage;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.Background;
import javafx.scene.layout.BackgroundFill;
import javafx.scene.layout.CornerRadii;
import javafx.scene.layout.StackPane;
import javafx.scene.paint.Color;
import javax.imageio.ImageIO;

public class WordCanvas extends StackPane
{
    private Canvas Can = new Canvas();

    private GraphicsContext GC = Can.getGraphicsContext2D();

    private TextArea Area = new TextArea();

    private ImageView Cover = new ImageView();

    double Width;
    double Height;

    public WordCanvas()
    {
        CreateUI();
    }

    public WordCanvas(double W, double H)
    {
        this.Width = W;
        this.Height = H;

        CreateUI();
    }

    public WordCanvas(ImageView IV)
    {
        this.Cover = IV;
    }

    public WordCanvas(Canvas C, TextArea TA, GraphicsContext GG)
    {
        this.Can = C;
        this.Area = TA;
        this.GC = GG;

        CreateUI();
    }

    public void CreateUI()
    {
        Caligraphy();

        Imagination();

        Color C = Color.STEELBLUE;

        BackgroundFill BFill = new BackgroundFill(C, CornerRadii.EMPTY, Insets.EMPTY);

        Background BGround = new Background(BFill);

        this.getChildren().addAll(Area);

        this.setBackground(BGround);
    }

    public void Caligraphy()
    {
        Area.setMaxWidth(Width);
        Area.setMaxHeight(Height);
    }

    public void Imagination()
    { 
        double CanvasWidth = GC.getCanvas().getWidth();
        double CanvasHeight = GC.getCanvas().getHeight();

        GC.setFill(Color.TRANSPARENT);
        GC.fillRect(0, 0, Can.getWidth(), Can.getHeight());
        GC.rect(0, 0, CanvasWidth, CanvasHeight);
        GC.setLineWidth(3);

        Can.addEventHandler(MouseEvent.MOUSE_ENTERED, new EventHandler<MouseEvent>()
        {
            @Override
            public void handle(MouseEvent event)
            {
                Can.setCursor(Cursor.CROSSHAIR);
            }
        });

        Can.addEventHandler(MouseEvent.MOUSE_EXITED, new EventHandler<MouseEvent>()
        {
            @Override
            public void handle(MouseEvent event)
            {
                Can.setCursor(Cursor.DEFAULT);
            }
        });

        Can.addEventHandler(MouseEvent.MOUSE_PRESSED, new EventHandler<MouseEvent>()
        {
            @Override
            public void handle(MouseEvent event)
            {
                GC.beginPath();
                GC.lineTo(event.getX(), event.getY());
                GC.moveTo(event.getX(), event.getY());
                GC.stroke();
            }
        });

        Can.addEventHandler(MouseEvent.MOUSE_DRAGGED, new EventHandler<MouseEvent>()
        {
            @Override
            public void handle(MouseEvent event)
            {
                GC.lineTo(event.getX(), event.getY());
                GC.stroke();
            }
        });

        Can.addEventHandler(MouseEvent.MOUSE_RELEASED, new EventHandler<MouseEvent>()
        {
            @Override
            public void handle(MouseEvent event)
            {
                ImageView IV = new ImageView();

                WordCanvas Stack = new WordCanvas(IV);

                Cover.setMouseTransparent(true);

                WritableImage WImage = Can.snapshot(new SnapshotParameters(), null);

                ByteArrayOutputStream Bos = new ByteArrayOutputStream();

                try
                {
                    ImageIO.write(SwingFXUtils.fromFXImage(WImage, null), "png", Bos);
                }
                catch(IOException ex)
                {
                    Logger.getLogger(WordCanvas.class.getName()).log(Level.SEVERE, null, ex);
                }

                InputStream Fis = new ByteArrayInputStream(Bos.toByteArray());
                Image Imos = new Image(Fis);

                int IW = (int) Imos.getWidth();
                int IH = (int) Imos.getHeight();

                WritableImage OutputImage = new WritableImage(IW, IH);
                PixelReader PReader = Imos.getPixelReader();
                PixelWriter PWriter = OutputImage.getPixelWriter();

                for (int y = 0; y < IH; y++)
                {
                    for (int x = 0; x < IW; x++)
                    {
                        int argb = PReader.getArgb(x, y);

                        int r = (argb >> 16) & 0xFF;
                        int g = (argb >> 8) & 0xFF;
                        int b = argb & 0xFF;

                        if(r >= 0xCF && g >= 0xCF && b >= 0xCF)
                        {
                            argb &= 0x00FFFFFF;
                        }

                        PWriter.setArgb(x, y, argb);
                    }
                }

                if(!Stack.getChildren().contains(Cover))
                {
                    Cover.setImage(OutputImage);

                    Stack.getChildren().add(Cover);
                }
                else
                {
                    Cover.setImage(OutputImage);
                }
            }
        });
    }

    public void SetCan(Canvas C)
    {
        this.Can = C;
    }

    public Canvas GetCan()
    {
        return Can;
    }

    public void SetGC(GraphicsContext GG)
    {
        this.GC = GG;
    }

    public GraphicsContext GetGC()
    {
        return GC;
    }

    public void SetArea(TextArea TA)
    {
        this.Area = TA;
    }

    public TextArea GetArea()
    {
        return Area;
    }

    public ImageView GetCover()
    {
        return Cover;
    }
}

In the main class the Snap button handler does work as I intend to, but what I want is that in the secondary class the MouseEvent.MOUSE_RELEASED even handler automatically creates the snapshot and does what the Snap button in the main class does. However nothing I have tried works, and it won't even accept this.getChildren().add(Cover).

Another minor problem, I want the WritableImage to automatically become transparent with a more elegant solution. User @jewelsea gives a solution here which works perfectly but I would prefer something a bit shorter that doesn't have to read through every pixel. Existing png files do work as intended but when I make my own png files they are not transparent.

user1803551
  • 12,965
  • 5
  • 47
  • 74
Fe Chodes
  • 13
  • 3
  • Use Java naming conventions: fields, local variables, method arguments and method names use camelCase. Your IDE can help you with renaming and once you do that I suggest you [edit] your question with this change. – user1803551 Nov 02 '17 at 08:41
  • Why do you have 2 `WorldCanvas`s and in your release action handler you create a new one each time and then don't use it? – user1803551 Nov 02 '17 at 10:15
  • And why do you want to write the image every time? Do you just want to paint over the text area? – user1803551 Nov 02 '17 at 14:35
  • Yes I want to paint over it and keep it there. I've since fixed some of those problems, however I still don't know how to correctly change the GUI elements from a different class file. Sending for example the TextArea through the constructor allows me to add and get text, but not change its maxwidth or remove it from the StackPane. – Fe Chodes Nov 03 '17 at 15:25

1 Answers1

0

It's not clear why you're creating a canvas on every draw, play with the children of the parent, and why you need to capture the screen and create an image each time.

You simply want to draw over the text area, so have a single canvas over it on which you draw. The canvas's mouseTransparentProperty can be used to decide which layer gets the input.

public class TextCanvas extends Application {

    private GraphicsContext gc;

    @Override
    public void start(Stage primaryStage) {
        TextArea textArea = new TextArea();
        Canvas canvas = createCanvas();

        ToggleButton draw = new ToggleButton("Draw");
        ToggleButton erase = new ToggleButton("Erase");
        ToggleGroup drawErase = new ToggleGroup();
        draw.setToggleGroup(drawErase);
        erase.setToggleGroup(drawErase);
        drawErase.selectedToggleProperty().addListener((ov, oldV, newV) -> {
            if (newV == null) {
                gc.setStroke(Color.TRANSPARENT);
                canvas.setMouseTransparent(true);
            } else if (drawErase.getSelectedToggle().equals(draw)) {
                System.out.println("Fd");
                gc.setStroke(Color.BLACK);
                canvas.setMouseTransparent(false);
            } else if (drawErase.getSelectedToggle().equals(erase)) {
                gc.setStroke(Color.WHITE);
                canvas.setMouseTransparent(false);
            }
        });

        Button clear = new Button("Clear");
        clear.setOnAction(e -> gc.clearRect(0, 0, canvas.getWidth(), canvas.getHeight()));

        CheckBox cb = new CheckBox("Show canvas");
        cb.setSelected(true);
        canvas.visibleProperty().bind(cb.selectedProperty());

        VBox buttonBox = new VBox(draw, erase, clear, cb);

        StackPane stack = new StackPane(textArea, canvas);
        ScrollPane scrollPane = new ScrollPane(stack);
        scrollPane.setFitToHeight(true);

        canvas.widthProperty().bind(scrollPane.widthProperty());
        canvas.heightProperty().bind(scrollPane.heightProperty());

        stack.setBackground(new Background(new BackgroundFill(Color.STEELBLUE, CornerRadii.EMPTY, Insets.EMPTY)));

        BorderPane border = new BorderPane();
        border.setCenter(scrollPane);
        border.setBottom(buttonBox);

        primaryStage.setMaximized(true);
        primaryStage.setTitle("Practice Canvas");
        primaryStage.setScene(new Scene(border));
        primaryStage.show();
    }

    private Canvas createCanvas() {
        Canvas canvas = new Canvas();
        gc = canvas.getGraphicsContext2D();
        gc.setLineWidth(3);

        canvas.setOnMouseEntered(event -> canvas.setCursor(Cursor.CROSSHAIR));
        canvas.setOnMouseExited(event -> canvas.setCursor(Cursor.DEFAULT));

        canvas.setOnMousePressed(event -> {
            gc.beginPath();
            gc.lineTo(event.getX(), event.getY());
            gc.moveTo(event.getX(), event.getY());
            gc.stroke();
        });
        canvas.setOnMouseDragged(event -> {
            gc.lineTo(event.getX(), event.getY());
            gc.stroke();
        });

        canvas.setMouseTransparent(true);
        return canvas;
    }

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

Edit: I added a check box to toggle the canvas's visibility. You will have to clear up your usage requirements as to what each user operation does with each mode. In any case, this should be enough to play with.

Also, use proper Java naming conventions: local variables, (non-constant) field names and method argument should start with a lower case.

user1803551
  • 12,965
  • 5
  • 47
  • 74
  • Thank you and this looks very nice, good way to shorten it, but it wasn't exactly what I wanted though. I'm making something like a Wordpad application that streamlines the free drawing process and the canvas can go or stay as the user wishes, and personally when I'm drawing I prefer the brush function stays selected until I'm finished as well as what I've drawn. This certainly seems better than making every single white-light grey pixel transparent, although I will still need to do that for when I implement the option to cut and paste images with transparent background like in paint. – Fe Chodes Nov 06 '17 at 16:52
  • @FeChodes You are changing your specifications. See my edit to my answer. Even if it's not exactly what you wanted (you need to be very precise with that) it should give you all the tools to get it to the state you want. – user1803551 Nov 06 '17 at 23:06
  • I can't upvote yet but I accepted it. It wasn't precisely what I wanted but it was such a nice answer and way to shorten it. – Fe Chodes Nov 10 '17 at 16:35