Sunday, September 20, 2015

Displaying images from files in JavaFX

As I continue to both employ JavaFX in my own code and teach JavaFX in my courses, I am slowly learning how to perform tasks I used to do in Swing in new ways.  The latest example is the all-important task of opening image files.

The strategy employed in this example is to set the image file as a background for a JavaFX Pane object.  Opening the image is a straightforward use of the JavaFX file chooser dialog.  Error handling is done via a simple error dialog created as an Alert.  Displaying the image requires the following steps:

  • Transform the File object into a URL, and use this to create an Image object.
  • Create a BackgroundSize for the object.  
    • By setting the height and width to AUTO, the image will automatically resize as the window displaying it is resized. 
    • It will not deform the image; if the aspect ratio of the displaying window does not match the image, the image will be truncated.
  • Create a BackgroundImage object.
    • The first parameter passes the Image object to use as its basis.
    • The second and third parameters control whether the object is repeated in the X and Y directions.  For this example, I deemed this undesirable.
    • The fourth parameter centers the image in the background.
    • The final parameter is the BackgroundSize object created before. 
  • Finally, we designate the BackgroundImage as the Pane's background.

import java.io.File;
import java.net.MalformedURLException;

import javafx.fxml.FXML;
import javafx.scene.control.Alert;
import javafx.scene.control.Alert.AlertType;
import javafx.scene.control.Button;
import javafx.scene.image.Image;
import javafx.scene.layout.Background;
import javafx.scene.layout.BackgroundImage;
import javafx.scene.layout.BackgroundPosition;
import javafx.scene.layout.BackgroundRepeat;
import javafx.scene.layout.BackgroundSize;
import javafx.scene.layout.Pane;
import javafx.stage.FileChooser;
import javafx.stage.FileChooser.ExtensionFilter;

public class DemoController {
 @FXML
 Pane pane;
 
 @FXML
 Button open;
 
 @FXML
 void initialize() {
  open.setOnAction(event -> imageOpener());
 }
 
 void imageOpener() {
  File imgFile = openImage();
  try {
   if (imgFile != null) {
    displayImage(imgFile);
   } 
  } catch (MalformedURLException e) {
   showError(e);
  }
 }
 
 File openImage() {
  FileChooser fileChooser = new FileChooser();
  fileChooser.setTitle("Select an Image File");
  fileChooser.getExtensionFilters().add(new ExtensionFilter(
    "Image Files", "*.png", "*.jpg", "*.gif"));
  return fileChooser.showOpenDialog(null);
 }
 
 void displayImage(File imgFile) throws MalformedURLException {
  Image background = new Image(imgFile.toURI().toURL().toString());
  BackgroundSize size = new BackgroundSize(BackgroundSize.AUTO,
    BackgroundSize.AUTO, false, false, true, true);
  BackgroundImage bimg = new BackgroundImage(background,
    BackgroundRepeat.NO_REPEAT, BackgroundRepeat.NO_REPEAT,
    BackgroundPosition.CENTER, size);
  pane.setBackground(new Background(bimg));
 }
 
 void showError(Exception e) {
  Alert info = new Alert(AlertType.ERROR);
  info.setTitle("Image not found");
  info.setHeaderText("Image not found");
  info.setContentText(e.getMessage());
  info.showAndWait();
 }
}

Dragging objects with a mouse in JavaFX

Dragging objects is pretty straightforward in JavaFX as long as some key concepts are kept straight.  If we go with the strategy of attaching the event handler to the object to be dragged, then the very first time that the onMouseDragged event is invoked, the (x,y) coordinates given by the event parameter will be the location on the object that was clicked.

If we then move the object, on subsequent invocations of the event as a result of moving the mouse, the (x,y) coordinates will be the relative distance between the mouse position at the time of the preceding event and the current mouse position.  This works because the new position of the object is determined by the previous mouse location.

In either case, it works pretty well to use the (x,y) coordinate of the mouse as an offset by which to translate the object.  The code below demonstrates the concept nicely, provided that a suitable GUI has been created using SceneBuilder and a suitable main() has also been created.  The drag() method is set up to work properly with a draggable Node of any subtype.

import javafx.fxml.FXML;
import javafx.scene.Node;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.Pane;
import javafx.scene.shape.Circle;

public class DemoController {
 @FXML
 Pane pane;
 
 @FXML
 Circle ball1, ball2;
 
 @FXML
 void initialize() {
  ball1.setOnMouseDragged(event -> drag(event));
  ball2.setOnMouseDragged(event -> drag(event));
 }
 
 public void drag(MouseEvent event) {
  Node n = (Node)event.getSource();
  n.setTranslateX(n.getTranslateX() + event.getX());
  n.setTranslateY(n.getTranslateY() + event.getY());
 }
}

Tuesday, September 8, 2015

Be careful with operator precedence

I recently encountered the following bug while programming in Java. I've taken it out of context and created the program below for illustrative purposes.

The goal is to add two byte values while avoiding overflow. To this end, we cast them to integers, and we use bit-masking to avoid sign extension when casting.  The unsuccessful version is highlighted in red, while the successful version is green:

public class ConvertBytes {
 public static void main(String[] args) {
  byte a = 127;
  byte b = 126;
  int bad = ((int)a) & 0xff + ((int)b) & 0xff;
  System.out.println(bad);
  int good = ((int)a & 0xff) + ((int)b & 0xff);
  System.out.println(good);
 }
}

When this program is executed, bad is 125 and good is 253.

I can't believe that after 13 years as an active Java programmer (with 11 years before that as an active C++ programmer) I made so elementary a mistake, but I'm posting it here in hopes it will be a useful warning to others.  For whatever reason, I assumed that bitwise-and had a higher precedence than addition, perhaps because it is a traditionally "multiplicative" operator.  As it turns out, its precedence is actually much lower.

A further observation is that this is not what I thought the bug was originally.  At first, I thought I had somehow messed up the casting.  It was only after I rechecked it carefully that I realized I was killing the higher-order bits with the bitwise and.