JavaFX - First App with Modern Graphical User Interface

in #utopian-io7 years ago (edited)

Untitled 01.png

What Will I Learn?

In this tutorial, I will guide you how to build beautiful apps using UI Controls of JavaFX. Many developers just know Java Swing for building apps with graphical user interface, but JavaFX libraries is more complete, powerful and awesome as the best GUI of Java libraries (APIs) ever. By using JavaFX, developers can build various apps with the modern user interface completely with many features, such as audio, video, 2D graphics, 3D graphics and even animations.

  • JavaFX UI Controls
  • JavaFX Behaviour (Event Handler)
  • Java™ Map Object, which Maps Keys to Values

Requirements


Difficulty

  • Intermediate

Tutorial Contents

Overview
  1. Create New Project
  2. Create Entity Class of Student
  3. Set up of Grid Layout Pane
  4. Set up All the Required Components
  5. Create Submit Handler
  6. Test!


1) Create New Project

  • Open your IntelliJ IDEA.
  • Select: FileNewProject...
    Untitled 03.png
  • On the field of Project name, type Test. Or you can enter another name as you wish. Finally, click Finish.

After this project is successfully created, it will look like the following project structure,

⇾ .idea
⇾ out
⇾ srcsample
         ↳ Controller
         ↳ Main
         ↳ sample.fxml

Next, we will only focus on the src/sample directory to write the source codes. For the moment we will only render all components programmatically, so we do not need the sample.fxml file yet.


2) Create Entity Class of Student

  • Right click on src/sample directory. Choose: NewJava Class.

  • Then on the pop up dialog, type entities.Entity right on field of Name, then click OK.

  • As defined earlier, we'll provide five fields, such as:

    1. The fields of name, degree and address as java.lang.String.
    2. A field of birthDate as java.util.Date.
    3. A field of female as boolean.

    And these are all the Setters that always return the current object, and the validators inside them,

    public Student setName(String value) {
        if(value != null && value.length() > 0) {
            this.name = value;
            return this;
        }
        throw new IllegalArgumentException("Name shouldn't be empty");
    }
    
    public Student setBirthDate(String value) {
        try {
            birthDate = FORMATTER.parse(value);
            return this;
        } catch(NullPointerException e) {
            throw new IllegalArgumentException("Date of Birth shouldn't be empty");
        } catch(Throwable e) {
            throw new IllegalArgumentException(String.format("Incorrect Date of Birth (%s)", e.getMessage()));
        }
    }
    
    public Student setDegree(Object value) {
        String degree;
        if(value != null && (degree = (String) value).length() > 0) {
            this.degree = degree;
            return this;
        }
        throw new IllegalArgumentException("Please, select your current degree");
    }
    
    public Student setAddress(String value) {
        if(value != null && value.length() > 0) {
            this.address = value;
            return this;
        }
        throw new IllegalArgumentException("Address shouldn't be empty");
    }
    /**
     * Set gender of Student.
     * @param value {@code True} for Male, otherwise Female.
     * @return This current object.
     */
    public Student setGender(boolean value) {
        this.female = !value;
        return this;
    }
    

    Slightly different from the Getters as common, we will also provide a global static variables that stores the name of each getters. They will be needed later when defining columns in the table. Here are all the getters:

    public static final String NAME = "name";
    public String getName() { return name; }
    
    public static final String BIRTH_DATE = "birth";
    public String getBirth() {
        LocalDate date = LocalDate.ofInstant(birthDate.toInstant(), java.time.ZoneId.systemDefault());
        /**
         * Or, for JDK < 9
         * java.time.ZonedDateTime zdt = birthDate.toInstant().atZone(java.time.ZoneId.systemDefault());
         * LocalDate date = zdt.toLocalDate();
         */
        return String.format("%d, %d %s", date.getYear(), date.getDayOfMonth(), date.getMonth().getDisplayName(TextStyle.FULL, Locale.UK));
    }
    
    public static final String GENDER = "gender";
    public String getGender() { return isFemale()? "Female" : "Male"; }
    
    public static final String DEGREE = "degree";
    public String getDegree() { return degree; }
    
    public static final String ADDRESS = "address";
    public String getAddress() { return address; }
    
    public Date getBirthDate() { return (Date) birthDate.clone(); }
    
    public boolean isFemale() { return female; }
    

    There are some issues when converting java.util.Date object to java.time.LocalDate. In JDK 9, the following statement works well:

    LocalDate date = LocalDate.ofInstant(birthDate.toInstant(), java.time.ZoneId.systemDefault());
    

    Fortunately, for you who are working using JDK with earlier version of the current one (JDK < 9), there are another alternative way to convert, which are like the following:

    java.time.ZonedDateTime zdt = birthDate.toInstant().atZone(java.time.ZoneId.systemDefault());
    LocalDate date = zdt.toLocalDate();
    

    Untitled 04.png


3) Set up of Grid Layout Pane

Do some change in the void start(javafx.stage.Stage) method at sample.Main as JavaFX Application. Next, create a static method: javafx.scene.layoutPane initialize() and then type this code:

@Override
public void start(Stage primaryStage) {
    Scene scene = new Scene(initialize(), 800, 550);
    primaryStage.setScene(scene);
    primaryStage.setTitle("Student Form");
    primaryStage.setMinWidth(350);
    primaryStage.setMinHeight(200);
    primaryStage.show();
}

private static Pane initialize() {
    GridPane grid = new GridPane();
    grid.setVgap(10);
    grid.setHgap(10);
    grid.setAlignment(Pos.CENTER);
    grid.setPadding(new Insets(15, 25, 15, 25));
    {
        //Register all components here
    }
    return grid;
}

Here we use javafx.scene.layout.GridPane as the main layout, because it's easy to use. Our perspective to arrange the components based on position of rows and columns.


4) Set up All the Required Components

We need to register all components in a map object, CONTROLS as instance of java.util.HashMap. Insert all of these codes within initialize method as we have previously defined. And here step by step:

  • Main Title and Field of Name

    final Map<String, Control> CONTROLS = new java.util.HashMap<>();
    Control control;
    Text label;
    (label = new Text("Student")).setId("mainTitle");
    grid.add(label, 0, 0, 2, 1);
    {
         (label = new Text("Name")).getStyleClass().add("baseLabel");
         grid.add(label, 0, 1);
         grid.add(control = new TextField(), 1, 1, 2, 1);
         ((TextInputControl) control).setPromptText("Enter your full name");
         CONTROLS.put(Student.NAME, control);
     }
    

    Result

    Untitled 05.png


  • Field of Birthday

    {
        (label = new Text("Date of Birth")).getStyleClass().add("baseLabel");
        grid.add(label, 0, 2);
        DatePicker picker = new DatePicker();
        picker.setPromptText("Enter your birthday");
        grid.add(picker, 1, 2, 2, 1);
        CONTROLS.put(Student.BIRTH_DATE, picker);
    }
    

    Result

    Untitled 06.png


  • Field of Gender

    {
        ToggleGroup gender = new ToggleGroup();
        RadioButton[] options = {
            new RadioButton("Male"),
            new RadioButton("Female")
        };
        for(RadioButton option : options)
            option.setToggleGroup(gender);
        (label = new Text("Gender")).getStyleClass().add("baseLabel");
        grid.add(label, 0, 3);
        grid.add(options[0], 1, 3);
        grid.add(options[1], 2, 3);
        CONTROLS.put("male", options[0]);
        CONTROLS.put("female", options[1]);
    }
    

    Result

    Untitled 07.png


  • Field of Degree
    {
        ChoiceBox<String> degrees = new ChoiceBox<>();
        degrees.getItems().addAll("Computer Science", "Mathematics ", "Biomedical Engineering", "Nursing", "Psychology and Counseling");
        (label = new Text("Degree")).getStyleClass().add("baseLabel");
        grid.add(label, 0, 4);
        grid.add(control = degrees, 1, 4, 2, 1);
        CONTROLS.put(Student.DEGREE, control);
    }
    
    Untitled 08.png

  • Fields of Address

    {
        (label = new Text("Address")).getStyleClass().add("baseLabel");
        grid.add(label, 0, 5);
        grid.add(control = new TextField(), 1, 5, 2, 1);
        ((TextInputControl) control).setPromptText("Enter your current address");
        CONTROLS.put(Student.ADDRESS, control);
    }
    

    Result

    Untitled 09.png


  • Button of Submit

    Button submit = new Button("Submit");
    {
       submit.setStyle("-fx-background-color:gold;-fx-text-fill:black;-fx-font:normal bold 20px 'Calibri'");
       HBox container = new HBox(15);
       container.setAlignment(Pos.BOTTOM_RIGHT);
       container.getChildren().add(submit);
       grid.add(container, 2, 6);
    }
    

    Result

    Untitled 10.png


  • Main Table

    TableView<Student> table = new TableView<>();
    table.setPrefWidth(800);
    TableColumn<Student, String> column;
    {//Register column of Name
        table.getColumns().add(column = new TableColumn<>("Name"));
        column.setMinWidth(180);
        column.setCellValueFactory(new PropertyValueFactory<>(Student.NAME));
    }
    {//Register column of Birth date
        table.getColumns().add(column = new TableColumn<>("Date of Birth"));
        column.setMinWidth(120);
        column.setCellValueFactory(new PropertyValueFactory<>(Student.BIRTH_DATE));
    }
    {//Register column of Gender
        table.getColumns().add(column = new TableColumn<>("Gender"));
        column.setMinWidth(80);
        column.setCellValueFactory(new PropertyValueFactory<>(Student.GENDER));
    }
    {//Register column of Degree
        table.getColumns().add(column = new TableColumn<>("Degree"));
        column.setMinWidth(125);
        column.setCellValueFactory(new PropertyValueFactory<>(Student.DEGREE));
    }
    {//Register column of Address
        table.getColumns().add(column = new TableColumn<>("Address"));
        column.setMinWidth(200);
        column.setCellValueFactory(new PropertyValueFactory<>(Student.ADDRESS));
    }
    grid.add(table, 0, 7, 5, 1);
    

    Result

    Untitled 11.png


5) Create Submit Handler

When the Submit button is clicked, then:

  • Make sure all fields are filled correctly, and give the user focus on the unfilled field.
  • Save all the Student data into the table.
  • Reset all controls.

Then, create a static class of: SubmitHandler that implements the javafx.event.EventHandler<ActionEvent> interface, and type in the following code:

private static final class SubmitHandler implements EventHandler<ActionEvent> {
    private final Map<String, Control> CONTROLS;
    private final TableView<Student> TABLE;

    private SubmitHandler(Map<String, Control> controls, TableView<Student> table) {
        CONTROLS = controls;
        TABLE = table;
    }

    @Override
    public void handle(ActionEvent event) {
        Control control = null;
        String value = null;
        RadioButton[] options;
        try {
            Student student = new Student()
                .setName(((TextInputControl)
                    (control = CONTROLS.get(Student.NAME))
                ).getText());
            {//Set Date of Birth
                DatePicker datePicker = (DatePicker) CONTROLS.get(Student.BIRTH_DATE);
                value = ((TextInputControl) (control = datePicker.getEditor())).getText();
                student.setBirthDate(value);
                value = null;
            }
            {//Set Gender
                options = new RadioButton[] {
                    (RadioButton) CONTROLS.get("male"),
                    (RadioButton) CONTROLS.get("female")
                };
                boolean b = options[0].isSelected();
                if(!b && !options[1].isSelected()) {
                    control = options[0];
                    throw new UnsupportedOperationException("Please, select your gender");
                }
                student.setGender(b);
            }
            student.setDegree(
                    ((ChoiceBox<?>) (control = CONTROLS.get(Student.DEGREE))).getValue()
                ).setAddress(
                    ((TextInputControl) (control = CONTROLS.get(Student.ADDRESS))).getText()
                );
            TABLE.getItems().add(student);
        } catch(Throwable e) {
            if(control != null) {
                control.requestFocus();
                if(value != null)
                    ((TextInputControl) control).selectPositionCaret(value.length());
            }
            return;
        }
        {//Reset all controls
            ((TextInputControl) CONTROLS.get(Student.NAME)).setText(null);
            ((DatePicker) CONTROLS.get(Student.BIRTH_DATE)).setValue(null);
            for(RadioButton option : options)
                option.setSelected(false);
            ((ChoiceBox<?>) CONTROLS.get(Student.DEGREE)).setValue(null);
            ((TextInputControl) CONTROLS.get(Student.ADDRESS)).setText(null);
            NOTIFIER.setText(null);
        }
    }
}

Finally, register this action to Submit button,

submit.setOnAction(new SubmitHandler(CONTROLS, table));

Here, we already know why we need to list all the components into the map object. The goal is the Handler can references and update to the existing components directly.


6) Test!

Here , I try to submit the form by leaving the Degree empty, and the result is:
Untitled 01.png

Final Touches
  • Notifier

    Create a static label as the Notifier of user carelessness,

    private static final Text NOTIFIER = new Text();
    

    Then, add it to the layout,

    grid.add(NOTIFIER, 3, 6);
    

    Finally, catch every error at the SubmitHandler made by the user and display its message .

    catch(Throwable e) {
                 NOTIFIER.setText(e.getMessage());
                 . . .
    }
    
  • Styles

    Add a CSS file as src/sample/main.css, and type this following:

    .baseLabel {
        -fx-font-size: 15px;
        -fx-font-weight: bold;
        -fx-fill: #888;
    }
    .redLabel {
        -fx-font-size: 15px;
        -fx-font-weight: bold;
        -fx-fill: #b22222;
    }
    #mainTitle {
       -fx-font-size: 42px;
       -fx-font-family: "Cambria";
       -fx-font-weight: bold;
       -fx-fill: #818181;
       -fx-effect: innershadow( three-pass-box , rgba(0, 0, 0, 0.7) , 6, 0.0 , 0 , 2 );
    }
    

    Apply this styles using this following statement on start(javafx.stage.Stage) method:

    scene.getStylesheets().add(Main.class.getResource("main.css").toExternalForm());
    
Thank you!

Share with Heart.


Appendix

src/sample/Main

package sample;

import javafx.application.Application;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.fxml.FXMLLoader;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.scene.control.*;
import javafx.scene.control.cell.PropertyValueFactory;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Pane;
import javafx.scene.text.Text;
import javafx.stage.Stage;
import sample.entities.Student;
import java.util.Map;

public class Main extends Application {
    private static final Text NOTIFIER = new Text();

    static {
        NOTIFIER.getStyleClass().add("redLabel");
        NOTIFIER.setWrappingWidth(350);
    }

    @Override
    public void start(Stage primaryStage) {
        Scene scene = new Scene(initialize(), 800, 550);
        scene.getStylesheets().add(Main.class.getResource("main.css").toExternalForm());
        primaryStage.setScene(scene);
        primaryStage.setTitle("Student Form");
        primaryStage.setMinWidth(350);
        primaryStage.setMinHeight(200);
        primaryStage.show();
    }

    private static Pane initialize() {
        GridPane grid = new GridPane();
        grid.setVgap(10);
        grid.setHgap(10);
        grid.setAlignment(Pos.CENTER);
        grid.setPadding(new Insets(15, 25, 15, 25));
        final Map<String, Control> CONTROLS = new java.util.HashMap<>();
        Control control;
        Text label;
        {//Main Title
            (label = new Text("Student")).setId("mainTitle");
            grid.add(label, 0, 0, 2, 1);
        }
        {//Field of Name
            (label = new Text("Name")).getStyleClass().add("baseLabel");
            grid.add(label, 0, 1);
            grid.add(control = new TextField(), 1, 1, 2, 1);
            ((TextInputControl) control).setPromptText("Enter your full name");
            CONTROLS.put(Student.NAME, control);
        }
        {//Field of Birthday
            (label = new Text("Date of Birth")).getStyleClass().add("baseLabel");
            grid.add(label, 0, 2);
            DatePicker picker = new DatePicker();
            picker.setPromptText("Enter your birthday");
            grid.add(picker, 1, 2, 2, 1);
            CONTROLS.put(Student.BIRTH_DATE, picker);
        }
        {//Field of Gender
            ToggleGroup gender = new ToggleGroup();
            RadioButton[] options = {
                new RadioButton("Male"),
                new RadioButton("Female")
            };
            for(RadioButton option : options)
                option.setToggleGroup(gender);
            (label = new Text("Gender")).getStyleClass().add("baseLabel");
            grid.add(label, 0, 3);
            grid.add(options[0], 1, 3);
            grid.add(options[1], 2, 3);
            CONTROLS.put("male", options[0]);
            CONTROLS.put("female", options[1]);
        }
        {//Field of Degrees
            ChoiceBox<String> degrees = new ChoiceBox<>();
            degrees.getItems().addAll("Computer Science", "Mathematics ", "Biomedical Engineering", "Nursing", "Psychology and Counseling");
            (label = new Text("Degree")).getStyleClass().add("baseLabel");
            grid.add(label, 0, 4);
            grid.add(control = degrees, 1, 4, 2, 1);
            CONTROLS.put(Student.DEGREE, control);
        }
        {//Field of Address
            (label = new Text("Address")).getStyleClass().add("baseLabel");
            grid.add(label, 0, 5);
            grid.add(control = new TextField(), 1, 5, 2, 1);
            ((TextInputControl) control).setPromptText("Enter your current address");
            CONTROLS.put(Student.ADDRESS, control);
        }
        Button submit = new Button("Submit");
        {//Submit
            submit.setStyle("-fx-background-color:gold;-fx-text-fill:black;-fx-font:normal bold 20px 'Calibri'");
            HBox container = new HBox(15);
            container.setAlignment(Pos.BOTTOM_RIGHT);
            container.getChildren().add(submit);
            grid.add(container, 2, 6);
            grid.add(NOTIFIER, 3, 6);
        }
        {
            TableView<Student> table = new TableView<>();
            submit.setOnAction(new SubmitHandler(CONTROLS, table));
            table.setPrefWidth(800);
            TableColumn<Student, String> column;
            {//Register column of Name
                table.getColumns().add(column = new TableColumn<>("Name"));
                column.setMinWidth(180);
                column.setCellValueFactory(new PropertyValueFactory<>(Student.NAME));
            }
            {//Register column of Birth date
                table.getColumns().add(column = new TableColumn<>("Date of Birth"));
                column.setMinWidth(120);
                column.setCellValueFactory(new PropertyValueFactory<>(Student.BIRTH_DATE));
            }
            {//Register column of Gender
                table.getColumns().add(column = new TableColumn<>("Gender"));
                column.setMinWidth(80);
                column.setCellValueFactory(new PropertyValueFactory<>(Student.GENDER));
            }
            {//Register column of Degree
                table.getColumns().add(column = new TableColumn<>("Degree"));
                column.setMinWidth(125);
                column.setCellValueFactory(new PropertyValueFactory<>(Student.DEGREE));
            }
            {//Register column of Address
                table.getColumns().add(column = new TableColumn<>("Address"));
                column.setMinWidth(200);
                column.setCellValueFactory(new PropertyValueFactory<>(Student.ADDRESS));
            }
            grid.add(table, 0, 7, 5, 1);
        }
        return grid;
    }

    private static final class SubmitHandler implements EventHandler<ActionEvent> {
        private final Map<String, Control> CONTROLS;
        private final TableView<Student> TABLE;

        private SubmitHandler(Map<String, Control> controls, TableView<Student> table) {
            CONTROLS = controls;
            TABLE = table;
        }

        @Override
        public void handle(ActionEvent event) {
            Control control = null;
            String value = null;
            RadioButton[] options;
            try {
                Student student = new Student()
                    .setName(((TextInputControl)
                        (control = CONTROLS.get(Student.NAME))
                    ).getText());
                {//Set Date of Birth
                    DatePicker datePicker = (DatePicker) CONTROLS.get(Student.BIRTH_DATE);
                    value = ((TextInputControl) (control = datePicker.getEditor())).getText();
                    student.setBirthDate(value);
                    value = null;
                }
                {//Set Gender
                    options = new RadioButton[] {
                        (RadioButton) CONTROLS.get("male"),
                        (RadioButton) CONTROLS.get("female")
                    };
                    boolean b = options[0].isSelected();
                    if(!b && !options[1].isSelected()) {
                        control = options[0];
                        throw new UnsupportedOperationException("Please, select your gender");
                    }
                    student.setGender(b);
                }
                student.setDegree(
                        ((ChoiceBox<?>) (control = CONTROLS.get(Student.DEGREE))).getValue()
                    ).setAddress(
                        ((TextInputControl) (control = CONTROLS.get(Student.ADDRESS))).getText()
                    );
                TABLE.getItems().add(student);
            } catch(Throwable e) {
                NOTIFIER.setText(e.getMessage());
                if(control != null) {
                    control.requestFocus();
                    if(value != null)
                        ((TextInputControl) control).selectPositionCaret(value.length());
                }
                return;
            }
            {//Reset all controls
                ((TextInputControl) CONTROLS.get(Student.NAME)).setText(null);
                ((DatePicker) CONTROLS.get(Student.BIRTH_DATE)).setValue(null);
                for(RadioButton option : options)
                    option.setSelected(false);
                ((ChoiceBox<?>) CONTROLS.get(Student.DEGREE)).setValue(null);
                ((TextInputControl) CONTROLS.get(Student.ADDRESS)).setText(null);
                NOTIFIER.setText(null);
            }
        }
    }

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

    private void show(Stage primaryStage) throws Throwable {
        Parent root = FXMLLoader.load(getClass().getResource("sample.fxml"));
        primaryStage.setTitle("Hello World");
        primaryStage.setScene(new Scene(root, 300, 275));
    }
}



Posted on Utopian.io - Rewarding Open Source Contributors

Sort:  

Thank you for the contribution. It has been approved.

You can contact us on Discord.
[utopian-moderator]

Hey @shreyasgune, I just gave you a tip for your hard work on moderation. Upvote this comment to support the utopian moderators and increase your future rewards!

Nice post .....your distribution is perfect

Hey @murez-nst I am @utopian-io. I have just upvoted you!

Achievements

  • You have less than 500 followers. Just gave you a gift to help you succeed!
  • Seems like you contribute quite often. AMAZING!

Suggestions

  • Contribute more often to get higher and higher rewards. I wish to see you often!
  • Work on your followers to increase the votes/rewards. I follow what humans do and my vote is mainly based on that. Good luck!

Get Noticed!

  • Did you know project owners can manually vote with their own voting power or by voting power delegated to their projects? Ask the project owner to review your contributions!

Community-Driven Witness!

I am the first and only Steem Community-Driven Witness. Participate on Discord. Lets GROW TOGETHER!

mooncryption-utopian-witness-gif

Up-vote this comment to grow my power and help Open Source contributions like this one. Want to chat? Join me on Discord https://discord.gg/Pc8HG9x