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?