Skip to content

Commit

Permalink
Merge pull request #3 from j-lum/inline-javafx-tutorials
Browse files Browse the repository at this point in the history
Inline JavaFX tutorials
j-lum authored Aug 14, 2019
2 parents 5c47c23 + bad66fc commit 10c48a8
Showing 8 changed files with 244 additions and 89 deletions.
Binary file modified tutorials/assets/DialogBoxesIteration2.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified tutorials/assets/MainWindowController.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 2 additions & 2 deletions tutorials/gradleTutorial.md
Original file line number Diff line number Diff line change
@@ -164,6 +164,6 @@ Now that you have a general idea of how to accomplish basic tasks with Gradle, h

* [Official Gradle Documentation](https://docs.gradle.org/current/userguide/userguide.html)

----------------------------------------------------------------------------------------
--------------------------------------------------------------------------------
**Authors:**
* Initial Version: Jeffry Lum
* Initial Version: Jeffry Lum
34 changes: 24 additions & 10 deletions tutorials/javaFxTutorialPart1.md
Original file line number Diff line number Diff line change
@@ -10,7 +10,7 @@ A JavaFX application is like a play you are directing. Instead of creating props

## Setting up Java FX

#### If you are not using Gradle
### If you are not using Gradle

1. Download the [JavaFX 11 SDK](https://gluonhq.com/products/javafx/) and unzip it.

@@ -23,7 +23,7 @@ A JavaFX application is like a play you are directing. Instead of creating props
`--module-path {JAVAFX_HOME}/lib --add-modules javafx.controls,javafx.fxml`<br>
e.g., `--module-path C:/javafx-sdk-11.0.2/lib --add-modules javafx.controls,javafx.fxml`

#### If you are using Gradle
### If you are using Gradle

Update your `build.gradle` to include the following lines:
```groovy
@@ -52,11 +52,9 @@ import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.stage.Stage;

public class HelloWorld extends Application {

public static void main(String[] args) {
launch();
}
public class Duke extends Application {

// ...

@Override
public void start(Stage stage) {
@@ -71,7 +69,23 @@ public class HelloWorld extends Application {

Note how we have created a `Label` to contain the text that we want to show. We then create the `Scene` and set its content. Finally, we set the stage and show it.

Run the program and you should see something like this:
Next, we create another Java class, `Launcher`, as an entry point to our application.
The `Launcher` class is reproduced below in its entirety.

```java
import javafx.application.Application;

/**
* A launcher class to workaround classpath issues.
*/
public class Launcher {
public static void main(String[] args) {
Application.launch(Duke.class, args);
}
}
```

Run `Launcher` and you should see something like this:

![Hello World](assets/HelloWorld.png)

@@ -91,6 +105,6 @@ Congratulations! You have created your first GUI application!
1. Can you have more than one `Stage` an application?
1. Try creating another stage and showing it! What happens?

----------------------------------------------------------------------------------------
--------------------------------------------------------------------------------
**Authors:**
* Initial Version: Jeffry Lum
* Initial Version: Jeffry Lum
65 changes: 33 additions & 32 deletions tutorials/javaFxTutorialPart2.md
Original file line number Diff line number Diff line change
@@ -49,7 +49,7 @@ import javafx.scene.layout.VBox;
import javafx.stage.Stage;


public class Main extends Application {
public class Duke extends Application {

private ScrollPane scrollPane;
private VBox dialogContainer;
@@ -58,7 +58,7 @@ public class Main extends Application {
private Scene scene;

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

@Override
@@ -105,34 +105,35 @@ Add the following code to the bottom of the `start` method. You'll have to add `

//...

//Step 2. Formatting the window to look as expected
stage.setTitle("Duke");
stage.setResizable(false);
stage.setMinHeight(600.0);
stage.setMinWidth(400.0);

mainLayout.setPrefSize(400.0, 600.0);

scrollPane.setPrefSize(385, 535);
scrollPane.setHbarPolicy(ScrollPane.ScrollBarPolicy.NEVER);
scrollPane.setVbarPolicy(ScrollPane.ScrollBarPolicy.ALWAYS);

scrollPane.setVvalue(1.0);
scrollPane.setFitToWidth(true);

dialogContainer.setPrefHeight(Region.USE_COMPUTED_SIZE);

userInput.setPrefWidth(325.0);

sendButton.setPrefWidth(55.0);

AnchorPane.setTopAnchor(scrollPane, 1.0);

AnchorPane.setBottomAnchor(sendButton, 1.0);
AnchorPane.setRightAnchor(sendButton, 1.0);

AnchorPane.setLeftAnchor(userInput , 1.0);
AnchorPane.setBottomAnchor(userInput, 1.0);
//Step 2. Formatting the window to look as expected
stage.setTitle("Duke");
stage.setResizable(false);
stage.setMinHeight(600.0);
stage.setMinWidth(400.0);

mainLayout.setPrefSize(400.0, 600.0);

scrollPane.setPrefSize(385, 535);
scrollPane.setHbarPolicy(ScrollPane.ScrollBarPolicy.NEVER);
scrollPane.setVbarPolicy(ScrollPane.ScrollBarPolicy.ALWAYS);

scrollPane.setVvalue(1.0);
scrollPane.setFitToWidth(true);

// You will need to import `javafx.scene.layout.Region` for this.
dialogContainer.setPrefHeight(Region.USE_COMPUTED_SIZE);

userInput.setPrefWidth(325.0);

sendButton.setPrefWidth(55.0);

AnchorPane.setTopAnchor(scrollPane, 1.0);

AnchorPane.setBottomAnchor(sendButton, 1.0);
AnchorPane.setRightAnchor(sendButton, 1.0);

AnchorPane.setLeftAnchor(userInput , 1.0);
AnchorPane.setBottomAnchor(userInput, 1.0);

// more code to be added here later
}
@@ -152,6 +153,6 @@ Run the application again. It should now look like this:
1. What happens when you press the `Enter` key or left-click the send button?
1. Why?

----------------------------------------------------------------------------------------
--------------------------------------------------------------------------------
**Authors:**
* Initial Version: Jeffry Lum
* Initial Version: Jeffry Lum
34 changes: 21 additions & 13 deletions tutorials/javaFxTutorialPart3.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
# JavaFX Tutorial 3 – Interacting with the user

Picking up from where we left off last tutorial, we have successfully achieved the desired layout. Now let’s make the application respond to user input.

:info: You should substitute the provided `DukeStub` class with your completed Duke.
Picking up from where we left off last tutorial, we have successfully achieved the desired layout. Now let’s make the application respond to user input.

Rather than to do everything in one try, let’s iterate and build up towards our final goal.

@@ -39,6 +37,7 @@ public void start(Stage stage) {
* @return a label with the specified text that has word wrap enabled.
*/
private Label getDialogLabel(String text) {
// You will need to import `javafx.scene.control.Label`.
Label textToAdd = new Label(text);
textToAdd.setWrapText(true);

@@ -103,10 +102,16 @@ import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
```

Next, add two images:
Next, add two images to the `main/resources/images` folder.
For this tutorial, we have two images `DaUser.png` and `DaDuke.png` to represent the user avatar and Duke's avatar respectively.

```java
private Image user = new Image(this.getClass().getResourceAsStream("/images/DaUser.png"));
private Image duke = new Image(this.getClass().getResourceAsStream("/images/DaDuke.png"));
public class Duke extends Application {
// ...
private Image user = new Image(this.getClass().getResourceAsStream("/images/DaUser.png"));
private Image duke = new Image(this.getClass().getResourceAsStream("/images/DaDuke.png"));
// ...
}
```

Add a new method to handle user input:
@@ -125,6 +130,14 @@ private void handleUserInput() {
);
userInput.clear();
}

/**
* You should have your own function to generate a response to user input.
* Replace this stub with your completed method.
*/
private String getResponse(String input) {
return "Duke heard: " + input;
}
```

Update the event handler code in the `start` method to use the new `handleUserInput` method:
@@ -154,18 +167,13 @@ Run the program and see how it works.
One additional benefit of defining a custom control is that we can add behavior specific to our `DialogBox`. Let’s add a method to flip a dialog box such that the image on the left to differentiate between user input and Duke’s output.

```java
// Make the constructor private
private DialogBox(Label l, ImageView iv) {
//...
}

/**
* Flips the dialog box such that the ImageView is on the left and text on the right.
*/
private void flip() {
this.setAlignment(Pos.TOP_LEFT);
ObservableList<Node> tmp = FXCollections.observableArrayList(this.getChildren());
Collections.reverse(tmp);
FXCollections.reverse(tmp);
this.getChildren().setAll(tmp);
}

@@ -224,6 +232,6 @@ You have successfully implemented a fully functional GUI for Duke!
* What was the development workflow like?
* Is the code base well-organized?

----------------------------------------------------------------------------------------
--------------------------------------------------------------------------------
**Authors:**
* Initial Version: Jeffry Lum
190 changes: 159 additions & 31 deletions tutorials/javaFxTutorialPart4.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
# JavaFX Tutorial 4 – FXML

You will need to clone the code sample for this tutorial.

While we have produced a fully functional prototype, there are a few major problems with our application.

1. The process of visually enhancing the GUI is long and painful:
@@ -35,10 +33,57 @@ Let's return to Duke and convert it to use FXML instead.

# Rebuilding the Scene using FXML

1. Scene Builder is a tool developed by Oracle and currently maintained by Gluon. It is a What-You-See-Is-What-You-Get GUI creation tool. [Download](https://gluonhq.com/products/scene-builder/#download) the appropriate version for your OS and install it.
Scene Builder is a tool developed by Oracle and currently maintained by Gluon. It is a What-You-See-Is-What-You-Get GUI creation tool. [Download](https://gluonhq.com/products/scene-builder/#download) the appropriate version for your OS and install it.

Create the following files in `src/main/resources/view`:

**MainWindow.fxml**
```xml
<?xml version="1.0" encoding="UTF-8"?>

<?import javafx.scene.control.Button?>
<?import javafx.scene.control.ScrollPane?>
<?import javafx.scene.control.TextField?>
<?import javafx.scene.layout.AnchorPane?>
<?import javafx.scene.layout.VBox?>

<AnchorPane maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" prefHeight="600.0" prefWidth="400.0" xmlns="http://javafx.com/javafx/8.0.171" xmlns:fx="http://javafx.com/fxml/1" fx:controller="MainWindow">
<children>
<TextField fx:id="userInput" layoutY="558.0" onAction="#handleUserInput" prefHeight="41.0" prefWidth="324.0" AnchorPane.bottomAnchor="1.0" />
<Button fx:id="sendButton" layoutX="324.0" layoutY="558.0" mnemonicParsing="false" onAction="#handleUserInput" prefHeight="41.0" prefWidth="76.0" text="Send" />
<ScrollPane fx:id="scrollPane" hbarPolicy="NEVER" hvalue="1.0" prefHeight="557.0" prefWidth="400.0" vvalue="1.0">
<content>
<VBox fx:id="dialogContainer" prefHeight="552.0" prefWidth="388.0" />
</content>
</ScrollPane>
</children>
</AnchorPane>
```

**DialogBox.fxml**
```xml
<?xml version="1.0" encoding="UTF-8"?>

<?import javafx.geometry.Insets?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.image.ImageView?>
<?import javafx.scene.layout.HBox?>

<fx:root alignment="TOP_RIGHT" maxHeight="1.7976931348623157E308" maxWidth="1.7976931348623157E308" prefWidth="400.0" type="javafx.scene.layout.HBox" xmlns="http://javafx.com/javafx/8.0.171" xmlns:fx="http://javafx.com/fxml/1">
<children>
<Label fx:id="dialog" text="Label" wrapText="true" />
<ImageView fx:id="displayPicture" fitHeight="99.0" fitWidth="99.0" pickOnBounds="true" preserveRatio="true" />
</children>
<padding>
<Insets bottom="15.0" left="5.0" right="5.0" top="15.0" />
</padding>
</fx:root>
```

1. Let’s explore the provided FXML files in Scene Builder. Running the tool brings up the main screen.
Select `Open Project` > `src/main/resources/view/MainWindow.fxml`. Inspect each control and its properties.
1. Let’s explore the provided FXML files in Scene Builder.

Running the tool brings up the main screen.
Select `Open Project` > `src/main/resources/view/MainWindow.fxml`. Inspect each control and its properties.

![SceneBuilder opening MainWindow.fxml](assets/SceneBuilder.png)

@@ -56,9 +101,20 @@ We will get to that later.

## Using Controllers

Let's take a closer look at the `MainWindow` class that we specified as the controller in the FXML file:
As part of the effort to separate the code handling Duke's logic and UI, let's _refactor_ the UI-related code to its own class.
We call these UI classes _controllers_.

Let's implement the `MainWindow` controller class that we specified in `MainWindow.fxml`.

**MainWindow.java**
```java
import javafx.fxml.FXML;
import javafx.scene.control.Button;
import javafx.scene.control.ScrollPane;
import javafx.scene.control.TextField;
import javafx.scene.image.Image;
import javafx.scene.layout.AnchorPane;
import javafx.scene.layout.VBox;
/**
* Controller for MainWindow. Provides the layout for the other controls.
*/
@@ -72,61 +128,107 @@ public class MainWindow extends AnchorPane {
@FXML
private Button sendButton;

private Image user = new Image(this.getClass().getResourceAsStream("/images/DaUser.png"));
private Image duke = new Image(this.getClass().getResourceAsStream("/images/DaDuke.png"));
private Duke duke;

private Image userImage = new Image(this.getClass().getResourceAsStream("/images/DaUser.png"));
private Image dukeImage = new Image(this.getClass().getResourceAsStream("/images/DaDuke.png"));

@FXML
public void initialize() {
scrollPane.vvalueProperty().bind(dialogContainer.heightProperty());
}

/** Creates two dialog boxes, one echoing user input and the other containing Duke's reply and then appends them to
public void setDuke(Duke d) {
duke = d;
}

/**
* Creates two dialog boxes, one echoing user input and the other containing Duke's reply and then appends them to
* the dialog container. Clears the user input after processing.
*/
@FXML
private void handleUserInput() {
String input = userInput.getText();
String response = DukeStub.getResponse(input);
String response = duke.getResponse(input);
dialogContainer.getChildren().addAll(
DialogBox.getUserDialog(input, user),
DialogBox.getDukeDialog(response, duke)
DialogBox.getUserDialog(input, userImage),
DialogBox.getDukeDialog(response, dukeImage)
);
userInput.clear();
}

}
```

As the controller, `MainWindow` is solely responsible for the logic behind the application.

The `@FXML` annotation marks a `private` or `protected` member and makes it accessible to FXML despite its modifier. Without the annotation, we will have to make everything `public` and expose our UI to unwanted changes.
The `@FXML` annotation marks a `private` or `protected` member and makes it accessible to FXML despite its modifier.
Without the annotation, we will have to make everything `public` and expose our UI to unwanted changes.

The `FXMLLoader` will map the a control with a `fx:id` defined in FXML to a variable with the same name in its controller. Notice how in `MainWindow`, we can invoke `TextField#clear()` on `userInput` and access its content just as we did in the previous example. Similarly, methods like private methods like `handleUserInput` can be used in FXML when annotated by `@FXML`.
The `FXMLLoader` will map the a control with a `fx:id` defined in FXML to a variable with the same name in its controller.
Notice how in `MainWindow`, we can invoke `TextField#clear()` on `userInput` and access its content just as we did in the previous example.
Similarly, methods like private methods like `handleUserInput` can be used in FXML when annotated by `@FXML`.

## Using FXML in our application

We load a FXML file using a `FXMLLoader`. Note how we use `getResource` instead of using `InputStreams` from `java.io` as our resources will no longer be accessible through file IO when we eventually package our application into an executable `jar`.
Let's create a new `Main` class as the bridge between the existing logic in `Duke` and the UI in `MainWindow`.

**Main.java**
```java
@Override
public void start(Stage stage) {
try {
FXMLLoader fxmlLoader = new FXMLLoader(Main.class.getResource("/view/MainWindow.fxml"));
AnchorPane ap = (AnchorPane) fxmlLoader.load();
Scene scene = new Scene(ap);
stage.setScene(scene);
stage.show();
} catch (IOException e) {
e.printStackTrace();
import java.io.IOException;

import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Scene;
import javafx.scene.layout.AnchorPane;
import javafx.stage.Stage;

/**
* A GUI for Duke using FXML.
*/
public class Main extends Application {

private Duke duke = new Duke();

@Override
public void start(Stage stage) {
try {
FXMLLoader fxmlLoader = new FXMLLoader(Main.class.getResource("/view/MainWindow.fxml"));
AnchorPane ap = fxmlLoader.load();
Scene scene = new Scene(ap);
stage.setScene(scene);
fxmlLoader.<MainWindow>getController().setDuke(duke);
stage.show();
} catch (IOException e) {
e.printStackTrace();
}
}
}
```

Again, we can interact with the `AnchorPane` defined in the FXML as we would have if we created the `AnchorPane` ourselves.

For our custom `DialogBox`, we did not define a controller. Looking at the code for the `DialogBox` class:
For our custom `DialogBox`, we did not define a controller so let's create a controller for it.

**DialogBox.java**
```java
import java.io.IOException;
import java.util.Collections;

import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.fxml.FXML;
import javafx.fxml.FXMLLoader;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.control.Label;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.layout.HBox;

/**
* An example of a custom control using FXML.
* This control represents a dialog box consisting of an ImageView to represent the speaker's face and a label
* containing text from the speaker.
*/
public class DialogBox extends HBox {
@FXML
private Label dialog;
@@ -146,13 +248,39 @@ public class DialogBox extends HBox {
dialog.setText(text);
displayPicture.setImage(img);
}
// ...

/**
* Flips the dialog box such that the ImageView is on the left and text on the right.
*/
private void flip() {
ObservableList<Node> tmp = FXCollections.observableArrayList(this.getChildren());
Collections.reverse(tmp);
getChildren().setAll(tmp);
setAlignment(Pos.TOP_LEFT);
}

public static DialogBox getUserDialog(String text, Image img) {
return new DialogBox(text, img);
}

public static DialogBox getDukeDialog(String text, Image img) {
var db = new DialogBox(text, img);
db.flip();
return db;
}
}
```

When we create a new instance of `DialogBox`, we set both the controller and root Node to `DialogBox`.
From this point onwards we can interact with `DialogBox` as we have in the previous tutorials.

The last change that we have to make is to point our `Launcher` class in the right direction:
In `Launcher.java`
```java
//...
Application.launch(Main.class, args);
//...
```
[todo]: # (Discussion on the fx:root pattern.)

## Exercises
@@ -161,6 +289,6 @@ From this point onwards we can interact with `DialogBox` as we have in the previ
1. Extend `MainWindow` to have a `Stage` as a root Node.
1. Customize the appearance of the application further with CSS.

----------------------------------------------------------------------------------------
--------------------------------------------------------------------------------
**Authors:**
* Initial Version: Jeffry Lum
* Initial Version: Jeffry Lum
6 changes: 5 additions & 1 deletion tutorials/textUiTestingTutorial.md
Original file line number Diff line number Diff line change
@@ -67,4 +67,8 @@
1. Update the `javac` and `java` commands in the script to match the name/location of your main class.
1. Add an `EXPECTED.txt` to the same folder, containing the expected output.
1. Add an `input.txt` containing the input commands.
1. Run the `.bat`/`.sh` file to execute the test.
1. Run the `.bat`/`.sh` file to execute the test.

--------------------------------------------------------------------------------
**Authors:**
* Initial Version: based on se-edu/addressbook-level2, adapted by Jeffry Lum

0 comments on commit 10c48a8

Please sign in to comment.