1
$\begingroup$

I'm running into some annoying technical problems in trying to incorporate JavaFX into my CS1 class, mainly arising from the constraints of avoiding/minimizing material they haven't yet learned. Specifically. the class is an objects-late approach, and by this point, they've seen the standard imperative control flow constructs, static method definitions, and object usage, though not yet class definitions. They're about to learn arrays, and they'll see class definitions and the basic notions of inheritance and encapsulation behind interfaces after that.

Up to this point, they've learned to write console-based applications, with input taken from command line arguments or else interactively, using Scanners.

At this very moment, I want to detour in to graphics, because algorithmic art is wonderful, and students love it. In the past, I've used Swing for this, starting from the templates in David Eck's textbook (Introduction to Programming Using Java). Here, for example, is a simple program that repeatedly prompts the user for (x,y) coordinates and a color choice, then displays a dot at that location and color (as you can see, this one was shown after introducing arrays, but before class definitions):

import java.awt.*;
import java.awt.event.*;
import javax.swing.*;

import java.util.Scanner;
import java.util.InputMismatchException;

public class Drawing extends JPanel { 

    static final int MAX_DOTS  = 100;

    static int[] xs = new int[MAX_DOTS];
    static int[] ys = new int[MAX_DOTS];
    static Color[] cs = new Color[MAX_DOTS];
    static int numDots = 0;  // the number of array cells actually used

    public static void main (String[] args) {

        // 1. Set up the drawing area
        Drawing d = new Drawing();
        d.setBackground(Color.WHITE);
        d.setPreferredSize(new Dimension(700,450));

        // 2. Set up the window
        JFrame window = new JFrame("Drawing window"); 
        window.setContentPane(d);  
        window.pack();   
        window.setLocation(150,100);  
        window.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        window.setResizable(true);
        window.setVisible(true);  // show the window on the screen

        Scanner inp = new Scanner (System.in);

        while (numDots < MAX_DOTS) {
            System.out.print("x y c (0:red, 1:green, 2:blue, 3:yellow) --> ");
            int x = inp.nextInt();
            int y = inp.nextInt();
            Color c = colorOf(inp.nextInt());
            xs[numDots] = x;
            ys[numDots] = y;
            cs[numDots] = c;
            numDots = numDots + 1;

            d.repaint();     // schedule a re-drawing of the image

            // flush the input stream to get ready for the next input:
            inp.nextLine();  
        } // while

        System.out.println("No more dots to draw.  Press ENTER to exit the program");
        inp.nextLine();
        window.dispose();  
    } // main

    // Quick and dirty integer to color map.
    static Color colorOf(int c) {
        Color[] colors = {Color.red, Color.green, Color.blue, Color.yellow};

        if (0 <= c && c < colors.length) {
            return colors[c];
        } else {
            return Color.black;
        }
    } // colorOf

    protected void paintComponent(Graphics g) {
        super.paintComponent(g);

        for (int i = 0; i < numDots; i++) {
            // We use the assumption that a single dot is represented in the
            // array elements xs[i], ys[i], and cs[i]
            Color savedColor = g.getColor(); 
            g.setColor(cs[i]);
            g.fillOval(xs[i],ys[i],15,15);
            g.setColor(savedColor);
        } //for
    } //paintComponent

} // Drawing class

The primary interaction is the loop in main that prompts for coordinates, adds entries to the parallel arrays, and calls the JPanel's repaint() method. No class definitions, no lambdas or ActionListeners, no invocation of separate threads via a run() method. It is as simple as I know how to make graphics at this level, while still using a real-world Java framework.

I cannot figure out how to do this kind of interaction in JavaFX, within these constraints. Here's where I'm stuck:

import javafx.application.Application;
import javafx.scene.layout.BorderPane;
import javafx.scene.Scene;
import javafx.stage.Stage;
import javafx.scene.canvas.Canvas;
import javafx.scene.canvas.GraphicsContext;
import javafx.scene.paint.Color;

import java.util.List;
import java.util.Scanner;


/**
 *  This file can be used to draw simple pictures.  Just fill in
 *  the definition of drawPicture with the code that draws your picture.
 */
public class Circles extends Application {

    private static Canvas canvas;

    public static void main(String[] args) {
        //        launch();  // Boilerplate:  essentially, this starts the window
        launch(args);

        if (Circles.canvas != null ){
            Scanner inp = new Scanner(System.in);

            while (true) {
                System.out.print("x y : ");
                int x = inp.nextInt();
                int y = inp.nextInt();
                drawCircle(canvas.getGraphicsContext2D(),x,y);
            }
        }  else {
            System.err.println("Fatal:  no canvas created.  Bailing out.");
            System.exit(1);
        }
    } // main

    public static void drawCircle(GraphicsContext g, int x, int y) {

        int colorChoice = (int)(4*Math.random());
        switch (colorChoice) {
        case 0:
            g.setFill(Color.RED);
            break;
        case 1:
            g.setFill(Color.GREEN);
            break;
        case 2:
            g.setFill(Color.BLUE);
            break;
        case 3:
            g.setFill(Color.YELLOW);
            break;
        }

        g.fillOval(x - 50, y - 50, 100, 100 );
        g.setStroke(Color.BLACK);
        g.strokeOval( x - 50, y - 50, 100, 100 );
    }


    /**/
    //------ Implementation details: DO NOT EXPECT TO UNDERSTAND THIS ------

    public void start(Stage stage) {
        List<String> argList = getParameters().getRaw();

        int width = Integer.parseInt(argList.get(0));
        // The width of the image.  You can modify this value!
        int height = Integer.parseInt(argList.get(1));
        // The height of the image. You can modify this value!

        Circles.canvas = new Canvas(width,height);
        drawPicture(canvas.getGraphicsContext2D(), width, height);

        BorderPane root = new BorderPane(canvas);
        root.setStyle("-fx-border-width: 4px; -fx-border-color: #444");
        Scene scene = new Scene(root);
        stage.setScene(scene);
        stage.setTitle("Simple Graphics"); // STRING APPEARS IN WINDOW TITLEBAR!
        stage.show();
        stage.setResizable(false);
    }

} // end SimpleGraphicsStarter

Yes, I realize that I can get the effect I want by adding a runLater() call, to dispatch a separate thread. I'm skating on thin enough ice with my students as it is, though: there is only so much boilerplate you can ask them to accept and work within.

Do any of you have suggestions for how I can pull off this kind of interaction within these constraints?

$\endgroup$
  • $\begingroup$ Seems you are trying to demonstrate JavaFx drawing primitives a very non-typical way: text-based loop interface, drawing to a non-interactive Scene, avoiding all event-driven stuff. $\endgroup$ – Michel Billaud Nov 13 '19 at 15:06
  • $\begingroup$ That's right. The usual approach to introducing graphics early is to make a library class with static methods that can all of the ugly stuff. In the past, when I used Swing, I could give them a template of "real" code. It had a lot of boilerplate, but working within it proved quite tractable for students. More importantly, it was something I could grow nicely into examples that worked with actual GUI design, a few weeks later. In fact, you can see the germ of that in the Swing example I posted, in the way I use three parallel arrays to manage the x,y, and color components of a Dot. $\endgroup$ – JLasseter Nov 13 '19 at 16:56
  • $\begingroup$ Managing the correct state of an entity all at once and abstraction of the necessary implementation details to reveal only the behavioral interface is the core insight of object-oriented design. I found that student were able to transition from what you see here to that insight very easily. $\endgroup$ – JLasseter Nov 13 '19 at 16:57
3
$\begingroup$

Instead of a System.in/out based dialog, you may consider a Pane containing

  • a Canvas for drawing
  • a TextField for user input.

Here is a sample program (hard-coded constants, to keep it short):

package application;

import javafx.stage.Stage;

import javafx.application.Application;
import javafx.application.Platform;
import javafx.event.ActionEvent;
import javafx.scene.Scene;
import javafx.scene.canvas.Canvas;
import javafx.scene.canvas.GraphicsContext;
import javafx.scene.control.Label;
import javafx.scene.control.TextField;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.BorderPane;
import javafx.scene.paint.Color;

public class Main extends Application {

    Canvas drawingArea = new Canvas(300, 300);
    TextField inputLine = new TextField();

    double[] x = new double[100];
    double[] y = new double[100];
    int nb = 0;

    @Override
    public void start(Stage primaryStage) {
        try {
            // Build the scene
            BorderPane root = new BorderPane();
            root.setTop(new Label("Type exit, or coordinates"));
            root.setCenter(drawingArea);
            root.setBottom(inputLine);
            Scene scene = new Scene(root, 400, 400);

            inputLine.setOnAction(this::processLine);

            updateDrawing();

            primaryStage.setScene(scene);
            primaryStage.show();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    void addPoint(double nx, double ny) {
        if (nb >= 100) {
            return;
        }
        x[nb] = nx;
        y[nb] = ny;
        nb++;
    }

    void processLine(ActionEvent event) {
        String line = inputLine.getText().trim();

        // should we stop it?
        if (line.equals("exit")) {
            Platform.exit();
        }
        // items separated by spaces
        String[] items = line.split("\\s+");
        if (items.length != 2) {
            return;
        }
        try {
            addPoint(Double.parseDouble(items[0]),
                    Double.parseDouble(items[1]));
            inputLine.clear();
        } catch (NumberFormatException e) {
            // ...
        }
        updateDrawing();
    }

    void updateDrawing() {
        GraphicsContext gc = drawingArea.getGraphicsContext2D();

        gc.setFill(Color.RED);
        gc.fillRect(0, 0, drawingArea.getWidth(), drawingArea.getHeight());

        gc.setFill(Color.YELLOW);
        for (int i = 0; i < nb; i++) {
            gc.fillOval(x[i]-10, y[i]-10, 20, 20);
        }
    }

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

The event-related stuff is kept to a minimal inputLine.setOnAction(this::processLine), and your students will immediately want you to explain to add points with the mouse.

Answer:

    drawingArea.setOnMouseClicked(this::processMouse);

and

    void processMouse(MouseEvent event) {
        addPoint(event.getX(), event.getY());
        updateDrawing();
    }

It also happens to be much simpler to code than

  • using the TextField (a dozen lines of code above)
  • or running a text-based dialog (nobody even wants to try to propose a solution :-))
| improve this answer | |
$\endgroup$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.