Tuesday, February 20, 2018

The JavaFX canvas and mouse clicks

The Canvas class in JavaFX is a very flexible platform for drawing whatever one would like. It is not that difficult to use, but there are a few elements that are a bit confusing.

To take advantage of this example, start by creating an FXML file containing a Canvas and a Button. Then connect it to the CanvasDemoController class below:

package canvas;

import javafx.fxml.FXML;
import javafx.scene.canvas.Canvas;

public class CanvasDemoController {
  @FXML
  Canvas canvas;
 
  @FXML
  void move() {
    d.move(10, 10);
    refresh();
  }
 
  void refresh() {
    canvas.getGraphicsContext2D()
          .clearRect(0, 0, canvas.getWidth(), canvas.getHeight());
    d.draw(canvas);
  }
 
  Drawing d = new Drawing();
 
  @FXML
  void initialize() {
    canvas.setOnMouseClicked(event -> {
      d.teleport(event.getX(), event.getY());
      refresh();
    });
  }
}

The class above references the Drawing class, shown below:

package canvas;

import javafx.scene.canvas.Canvas;

public class Drawing {
  private double x, y, radius;
 
  public Drawing() {
    x = y = 0.0;
    radius = 10.0;
  }
 
  public void move(double dx, double dy) {
    x += dx;
    y += dy;
  }
 
  public void teleport(double x, double y) {
    this.x = x;
    this.y = y;
  }
 
  public void draw(Canvas canvas) {
    canvas.getGraphicsContext2D()
          .fillOval(x - radius, y - radius, radius*2, radius*2);
  }
}


The combined example illustrates several useful ideas:
  • Creating graphics on a Canvas is not that difficult, but the Graphics2DContext object must be acquired from it in order to draw anything. 
  • Drawings on a Canvas persist until erased. Erase and redraw as necessary to animate.
  • Mouse click events are pretty easy to handle, as the coordinates of the mouse click are easily obtained from the event parameter. However, we need to code the event handler explicitly in the controller class.

Thursday, February 1, 2018

JavaFX and TableView

The TableView control in JavaFX is a flexible and powerful interface. Every TableView object is parameterized by a data type that represents a row of the table. The documentation encourages developers to create row objects that represent each column using ObservableValue types. Following the documentation, it is necessary to create three methods (a setter and two different getters) for each of these values.

I find this approach exceedingly verbose, and in this blog post I will demonstrate an alternative that is hinted at in the documentation. I will assume that the reader is comfortable creating a JavaFX interface using SceneBuilder. I will just present the controller to which the FXML file can be attached.

This is a simple application that lets the user enter names and ages, which are then displayed in the table. The table is not editable. First, we define a data type for the table rows:

public class NameAgeRow {
  private String name;
  private int age;
 
  public NameAgeRow(String name, int age) {
    this.name = name;
    this.age = age;
  }
 
  public NameAgeRow(String name, String age) {
    this(name, Integer.parseInt(age));
  }
 
  public String getName() {return name;}
  public int getAge() {return age;}
}

Next, we define the Controller for the application:

import javafx.beans.property.SimpleIntegerProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.fxml.FXML;
import javafx.scene.control.Button;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.control.TextField;

public class Controller {
  @FXML
  TableView<NameAgeRow> table;
  @FXML
  TableColumn<NameAgeRow,String> names;
  @FXML
  TableColumn<NameAgeRow,Integer> ages;
 
  @FXML
  TextField name;
  @FXML
  TextField age;
  @FXML
  Button add;
 
  @FXML
  void initialize() {
    add.setOnAction(e -> {
      table.getItems().add(new NameAgeRow(name.getText(), 
                                          age.getText()));
      name.setText("");
      age.setText("");
    });
  
    names.setCellValueFactory(cdf -> 
      new SimpleStringProperty(cdf.getValue().getName()));
    ages.setCellValueFactory(cdf -> 
      new SimpleIntegerProperty(cdf.getValue().getAge()).asObject());
  }
}

The overall structure is straightforward. For each column, we define a lambda that obtains the value from the NameAgeRow object for the current table row. A real convenience of this approach is that it can be made to work with any user-defined class that represents the row data. I find this more convenient and flexible than what is described in the documentation.