HelloFX

Preliminaries

Install Scene Builder

We use JavaFX8 Scene Builder to edit the FMXL files generated by these applications. Go to the site
http://gluonhq.com/open-source/scene-builder/
Download the correct package for your operating system and install it.

You'll know whether Scene Builder is installed if attempting to open an FXML file (*.fxml) in NetBeans invokes this application. If attempting to open simply brings up the editor, it is not installed.

Running a NetBeans Project

You can use Run ⇾ Run Main Project from the menu or choose the green run button or F6. When you save a file, it is compiled and when you run the application, NetBeans automatically saves the files, thereby compiling them and storing the compiled class files in the build project folder.

If an application is loaded from sources and/or there are more than one possible main class, you can set the desired main class to run from the default config ⇾ Customize drop-down selection.

Clean-Build

An issue in complex projects is that they use auxiliary non-java files which must be added to the compilation. Editing changes made to these auxiliary files can often be overlooked if you simply re-run the project.

A safer compilation mechanism for complex projects is to run the "Clean and Build" feature from menu prior to running the project per se. From the menu, use Run ⇾ Clean and Build Main Project, or else the button. Doing so compiles all the files in the project, integrates the non-java files into the compilation build folder and and builds the project JAR file within the dist directory.

JavaFX8 Documentation

Its is very useful to select a class in the editor, right-click, and then choose
Show JavaDoc
At issue is that the JavaDoc for JavaFX may be for the older JavaFX2. Double-check and fix if necessary by choosing from the menu:
Tools ⇾ Java Platforms
Choose the JavaDoc tab. The URL you want is:
https://docs.oracle.com/javase/8/javafx/api/
If you have the URL for the older version-2 API, remove it and add this one.

Create the Project

  1. Select File ⇾ New Project.
  2. In the New Project window, select the JavaFX category, and choose JavaFX FXML Applicationnot the related JavaFX Application choice. Click Next.
  3. In the New JavaFX Application window, set this and then click Finish.
    Project Name: HelloFX
    The initial auto-generated features could be modified at this point, but we'll do that afterwards.
  4. Test run the project to see the initial behavior.
The three files are generated:
FXMLDocument.fxml             = the FXML "view" file
FXMLDocumentController.java   = the Java Controller
HelloFX.java                  = the Main class
NetBeans initially displays all 3 files for editing, but we rarely ever want to edit the .fxml documents directly; instead, we want to edit these files through the GUI Scene Builder application. If you right-click on FXMLDocument.fxml you should see two choices:
Open =  edit in Scene Builder
Edit =  edit directly in NetBeans
In a JavaFX-FXML application, there will be a view.fxml/controller.java file pair for the main window, plus a pair for each popup dialog. The main view is connected to the file HelloFX.java through the line which loads the view:
Parent root = FXMLLoader.load(getClass().getResource("FXMLDocument.fxml"));
The controller is connected to the view file through a FXML attribute setting:
fx:controller="hellofx.FXMLDocumentController"

Explore the starter application

Double-click FXMLDocument.fxml, thereby editing it through the Scene Builder application. On the left side is the Library, consisting of a palette components at the top, separated by sections, and Document information, including component Hierarchy and Controller details at the bottom. On the right side is the Inspector consisting of three sections: Properties, Layout, and Code. In the middle is a form presenting the view component under construction.

Choose the Document ⇾ Controller section. Within it you will see:
Controller class
hellofx.FXMLDocumentController
You must have this Controller class established for anything at all to work. The point is that if you change the "root node," you have to reset this.

Look in the Document ⇾ Heirarchy section.
AnchorPane
  Button  Click Me!
  Label
Selecting any one of these "reveals" it in the visual form (to some degree) and, more importantly, shows the programming features employed in the Inspector portion within 3 sections:
Properties : Type
    Layout : Type
      Code : Type
Select the Button and choose the Code:Button section in the Inspector. Look for:
On Action
#handleButtonAction
Select the Label — note its position relative to the button — and choose the Code:Label pane. Look for:
fx:id     label
Then observe the controller code:

hellofx.FXMLDocumentController
  ...
  @FXML
  private Label label;
 
  @FXML
  private void handleButtonAction(ActionEvent event) {
    System.out.println("You clicked me!");
    label.setText("Hello World!");
  }
  ...
The @FXML annotation is the magic "glue" which associates the names label and handleButtonAction defined in the FXML view file to actual members within the Java controller class.

Add a TextField

The AnchorPane container offers us an easy way to explore other components. It presents a positional layout from which we can place components by simple drag-and-drop operations.

Text Components

Three very common GUI control components used to hold text are TextField, TextArea, and Label. They all expose the text property through getter/setter access. The Label is limited in that the user cannot change the text directly, and so one only ever uses setText on a Label.

Add it

  1. First, delete the label within Scene Builder through the right-click menu. Edit FXMLDocumentController.java and remove the line:
    label.setText("Hello World!");
    
  2. Re-run to verify that the application still works. The code
    @FXML
    private Label label;
    
    can still be present in the controller even if it's no longer part of the view. The opposite is not true. Whenever you set an fx:id in the view, it must be present in the controller.
  3. Now from Scene Builder, open the Library ⇾ Controls section. Scroll down to select a TextField component and drag it onto the form in the middle where the Label used to be.
  4. In the controller, add the new FXML member:

    hellofx.FXMLDocumentController
      ...
      @FXML
      private TextField textf;
      ...
    The indicated error has to do with a missing import statement. Let NetBeans fix it for you by right-clicking on the document and choosing Fix Imports. This will bring up a selection popup:
    Import Statements:
    TextField
    In any such choice like this, always pick the one which starts with javafx.
  5. In the Inspector ⇾ Code:TextField section, set the fx:id to be textf:
    fx:id     textf
    
    If you click away, and then back to the TextField, you should be able to obtain this value through a selection list. Save the changes.
  6. Finally, change the handler to capture the user-typed text and echo it to standard output:

    hellofx.FXMLDocumentController
      ...
      @FXML
      private void handleButtonAction(ActionEvent event) {
        System.out.println(textf.getText());
      }
      ...
Run the application and test it by typing text into the field and clicking the button.

ListView and ComboBox

Both ListViews and ComboBoxes provide lists of elements which can be selected. The difference is that a ListView presents the elements through a scrollable view showing all elements, whereas a ComboBox is a drop-down presentation of elements with one of them selected.

Add them

From the Controls section of the Library, drag a ListView and a ComboBox element onto the form. Rearrange and resize to fit within the form in some way or another.

Choose Preview ⇾ Show Preview to see what the app will look like (without any data). I get something like this:
ListView
ComboBox
Edit the controller, adding content to make it this:

hellofx.FXMLDocumentController
...
public class FXMLDocumentController implements Initializable {
 
  @FXML
  private TextField textf;
 
  @FXML
  private ListView<String> namelist;
 
  @FXML
  private ComboBox<Integer> numlist;
 
  @FXML
  private void handleButtonAction(ActionEvent event) {
    String name = namelist.getSelectionModel().getSelectedItem();
    System.out.println("namelist selection: " + name);
 
    Integer num = numlist.getSelectionModel().getSelectedItem();
    System.out.println("numlist selection:  " + num);
  }
 
  @Override
  public void initialize(URL url, ResourceBundle rb) {
    String names[] = { "John", "Jim", "Ernie", "Mary", "Alicia", "Erin",  };
 
    // add names one at a time into the listview
    for(String name: names) {
      namelist.getItems().add(name);
    }
 
    List<Integer> nums = new ArrayList<>();
    nums.add(33);
    nums.add(55);
    nums.add(11);
    nums.add(77);
    nums.add(22);
 
    // add the entire collection in one step
    ObservableList<Integer> obsList = FXCollections.observableList(nums);
    numlist.setItems(obsList);
 
    //numlist.setValue(33); // create a preset value
  }  
}
Edit FXMLDocument.fxml in Scene Builder. In the Inspector ⇾ Code section, set the fx:id values of these newly-added elements. For the ListView object:
fx:id     namelist
and this for the ComboBox object:
fx:id     numlist
Again, you should be able to find these members through selection.

Save the changes and run the application. By default, the ComboBox has an empty (null) selection. Uncomment the last line in the initialize function to avoid this empty selection.

Selection Events

Often we want to register the selection of an element from one of these lists by calling a handler function. Edit the controller, adding these two functions:

hellofx.FXMLDocumentController
...
public class FXMLDocumentController implements Initializable {
 
  @FXML
  private void namelistSelect(Event event) {
    String name = namelist.getSelectionModel().getSelectedItem();
    System.out.println("namelist selection: " + name);
  }
 
  @FXML
  private void numlistSelect(Event event) {
    Integer num = numlist.getSelectionModel().getSelectedItem();
    System.out.println("numlist selection:  " + num);
  }
 
  @Override
  public void initialize(URL url, ResourceBundle rb) {
    ...
  }  
}
Edit FXMLDocument.fxml in Scene Builder. In the Inspector ⇾ Code section, set these handlers: Run the application and make list selections.

Radio Button Group

Expand the form and drag 3 RadioButtons from the Controls palette onto the form. Change the labels to
AAA
BBB
CCC
Select the AAA RadioButton and make it appear selected by opening Inspector ⇾ Properties and checking:
Selected   
It doesn't matter where you add these RadioButtons, for example, it could be horizontally at the bottom, appearing now like this:
AAA BBB CCC

Run a Preview to see the initial behavior. What we're missing is the "mutual exclusion" of selection. To fix this, select all 3 RadioButtons and from Inspector ⇾ Properties, set
Toggle Group   anything
Setting this toggle group value to any string as suggested makes the choice mutually exclusive.

Now establish the selection handler. Edit the controller, adding these two functions:

hellofx.FXMLDocumentController
...
public class FXMLDocumentController implements Initializable {
 
  @FXML
  private void radioGroupSelect(Event event) {
    String choice = ((RadioButton) event.getSource()).getText();
    System.out.println("radio choice: " + choice);
  }
 
  @Override
  public void initialize(URL url, ResourceBundle rb) {
    ...
  }  
}
Edit FXMLDocument.fxml in Scene Builder. Un-select and then re-select all 3 RadioButtons. In the Inspector ⇾ Code section, set the handler for all three:
On Action
radioGroupSelect
Run the application and make radio button selections. You'll note that it has the desired behavior in that the handler is only called when the selection changes.

Observations and Swing Comparisons

We have made a point of using the default (i.e. none) access specifier on all of our added data members, in particular not private which was set for the auto-created class members. The reason is that we want to allow subsequent controllers for dialogs (within the same package) to have access to these data members. The FXML event handler function could always be private since they're only used by the view files.

In contrast to the original handleButtonAction function, all the handler function which we have introduced type the event by the Event superclass since the actual event type is, for the most part, not needed.

One great features of JavaFX is that it treats the ListView and ComboBox very much alike, in contrast to Java Swing which regards the analogous JList and JComboBox more different than alike. One big difference between the ListView and ComboBox is that the former supports multiple selection, whereas the latter only single selection; however in the "out of the box" usage they are basically the same.

Here are some points about the code, contrasting differences with JavaFX and Swing:
  1. The instances of these classes are generic, reflecting the correct idea of "lists of elements of some type." In normal usage, the elements obtained from these lists do not have to be casted as must be done often in Swing.
  2. The content of the list is managed entirely through the items property, not separate "model" objects as is necessary in Swing. As you see, the getItems() member function retrieves a generic ObservableList object, through which one can effectively do all the work with standard Collection and List-style members:
    add, remove, clear, contains, get, set, ...
  3. Selection within the list is managed through the getSelectionModel() member function which retrieves a selection management object with member functions:
    getSelectedItem, clearSelection, select, ...
  4. Swing has CheckBoxes and RadioButtons, but regards these equally. These components in JavaFX behave more like the way they behave in HTML, with only radio buttons being exclusive by virtue of being part of a group.


© Robert M. Kline