The Counter & Music Player app

Here’s the final app you will end up making:

Edit

It has two parts to it:

  1. Counter: Displays a number, with the following buttons:
    • Increment the number by 1,
    • Decrement the number by 1,
    • Reset the number to zero,
    • Switch to the Music Player.
  2. Music Player: Simple music player with the following buttons:
    • Play the first song,
    • Play the second song,
    • Switch to the Counter.


Starter code given

The starter code provided to you is a blank Maven project with all the necessary dependencies you need (specified in the pom.xml file) to create this app:

  • javafx-controls
  • javafx-fxml
  • javafx-media


The key steps

More detailed instructions are in the Get Your Hands Dirty section below. But in a nutshell, the steps include:

  1. Importing the Maven project into your favourite IDE.
  2. Create a counter.fxml file.
  3. Use Scene Builder to add the controls.
  4. Create a CounterController.java file and connect it with the counter.fxml file.
  5. Specify an fx:id for the controls to the counter.fxml file.
  6. Add the controls as fields to CounterController.java, annotated with @FXML.
  7. Add a field to represent the state of the number.
  8. Specify the handler methods (declared in CounterController.java) to each button’s On Action via Scene Builder.
  9. Create musicplayer.fxml and MusicPlayerController.java files, and repeat the above steps for the music interface.
  10. Create a central controller to control switching of the Parent root node for the app’s main Scene.



Common issues you might encounter

  • The counter.fxml file isn’t created (or it’s not in the correct place).
  • FXML files (from Scene Builder) aren’t properly saving after an edit. You are encouraged to keep the FXML file open in the IDE to actually see it is being updated as you change things in Scene Builder.
  • When you connect the FXML to the Java controller file, check the correct class name is used, with the full package name also. Again, looking at the FXML is a good help.
  • Did you use the correct handler name (from Scene Builder) of the method you are trying to call in the controller?
  • Incorrect names of the @FMXL fields (in the controller) with respect to what’s actually written as the fx:id in Scene Builder.
  • Using the incorrect type of the @FXML field. For example, you have added 3x Button and 1x Label in Scene Builder, but in the controller you declare all of them as Button type.




Edit Get your hands dirty!

  1. Fork the JavaFX Introduction to Fundamentals repository to your GitHub account, then clone your fork.
  2. Import the project into your preferred IDE (see the FAQ for help).
  3. Run the code using the Maven wrapper:
    • Windows: .\mvnw.cmd clean javafx:run
    • Linux/Mac: ./mvnw clean javafx:run
  4. You will see a BUILD FAILURE.
  5. Scroll up slowly, and try to look in the stack trace for file names that might appear to be leading to your project code. For example, the red section:
    Edit

  6. Spend some time trying to analyse and looking for hints. All the evidence appears to be pointing to something to do with loading the FXML file. What FXML file? Go to the line relevant to your project, shown in the yellow underlines above (it might be different line numbers in your code). Here, it’s lines 19 and 24 of the App.java file.
  7. Go to App.java, and have a look at those lines. You can see that line 24 is calling line 17, which then goes on to line 18, and then line 19. After that, the program digs into JavaFX’s libraries and eventually complains. Line 18 is making reference to a resource called /fxml/counter.fxml. Does this exist? No!
    Edit

  8. Create a counter.fxml file, and place it in the resources/fmxl/ folder:
    Edit

  9. The counter.fxml should have at least a root node of some sort (e.g. a StackPane). If you created the file yourself, then it will be empty—in which case you can place the following content in it:
     <?xml version="1.0" encoding="UTF-8"?>
     <?import javafx.scene.layout.StackPane?>
     <StackPane xmlns="http://javafx.com/javafx" xmlns:fx="http://javafx.com/fxml">
     </StackPane>
    

    WARNING: If you get an error when copy-pasting this into your FXML file, make sure there isn’t any extra spaces at the front/top of your file. Make sure your FXML file starts with the <?xml...> as the very first thing.

  10. Run the code again, and you should see a blank JavaFX window appear:
    Edit

  11. Create a corresponding controller called CounterController. You might want to organise it into a controllers sub-package:
    Edit

  12. Open the counter.fxml file with Scene Builder, and add a Pane (Containers) with a Label (Controls) and four Buttons (Controls):
    Edit

  13. Change the displayed Text for the label and buttons so they look like this:
    Edit

  14. Specify the controller class for the FXML file. You need the full package name, and without the .java extension:
    Edit

  15. Give an ID to your label and buttons. This will later correspond to fields inside the controller class:
    Edit

  16. Specify a handler method name in the On Action of each of the buttons:
    Edit

  17. In your controller class, you will want to declare:
    • Fields corresponding to the control IDs (e.g., private Button incrementButton;).
    • Methods corresponding to the On Action handlers (e.g., private void increment() { ... }).
  18. You might see the following warning in your FXML file (e.g., The controller 'CounterController' has no field 'numberLabel', etc):
    Edit

  19. This is due to the fields and methods being private in the controller class, which is usually the right thing to have. Go to your controller class and add the @FXML annotation for all fields and methods that need to be accessed by FXML file. The warnings should go away.
    Edit

  20. Implement the logic of the increment(), decrement(), and reset() handlers so that these buttons change the label accordingly:
    Edit

  21. Now it’s time to understand how the “switch” button will work:
    Edit

  22. The handler for it will work similar to the others. However, we can also specify a parameter to the handler of type ActionEvent. Add the following print statements to learn more about the source of this event:
    Edit

  23. Run your program again and click the button so we see what it prints. You will see that the source of the event is a Button, namely the one you clicked:
    Edit

  24. Now that we know it’s a Button, we can cast to it. We can then get the Scene that this button is in by using the getScene() method. Once you have a reference to the Scene, you can set its root node using the setRoot() method. This will overwrite the root you initially set in the App’s start() method. You can reuse the loadFXML() method in the App class, but you might need to make it public:
    Edit

  25. Create a new FXML file called musicplayer.fxml that will correspond to the music player’s UI that you want to load when the switch button is clicked. You will also need to create a MusicPlayerController.java and link it to the FXML file:
    Edit

  26. You should Test it out. Once you click the switch button, it should load up the (currently blank) UI for the music player.
  27. Repeat the steps above to create a UI for the music player, so it looks like this. Don’t forget all the IDs and handlers also:
    Edit

  28. Go to dig.ccmixter.org or pixabay.com/music/, scroll down to one of the Dig! buttons, and find yourself a couple of nice songs. Download them, and save them to the resources/sounds/ folder:
    Edit

    Alternatively, we have cached a couple from Pixelbay (Looking Forward and Risk)

    Download Looking Forward (mp3)

    Download Risk (mp3)

  29. Add a method called @FXML private void initialize() in both your controllers, with the following code (notice we are also printing the this instance):
    Edit

  30. Have a look at the output that gets printed to the terminal when you switch between the UIs. See how it’s a different instance (hence new initialisation) every time we switch between the UIs? You probably noticed also that the state isn’t saved for the counter UI, since it’s a new controller every time you go into it! This most likely isn’t the effect we want:
    Edit

  31. Let’s tidy things up, by defining a centralised place to manage the content of the scene better. Create a new class and call it whatever you want. Here, we call it SceneManager. Sorry, no copy-pasting here as it’s good practice to understand what’s happening by coding it yourself:
    Edit
    You don’t have to implement it this way. The main point here is that:
    • We store the roots of the UIs in a HashMap, and associate then with an enum value.
    • This will allow us to look up the same root corresponding to each UI.
    • So, how do we get the Parent roots? That’s the next step!
  32. Go to your start() method in the App class, and reuse the loadFXML() method to get the Parent node of each UI, and connect it with the corresponding enum value through the addUi() method. You can then set the initial UI using the getUiRoot() method:
    Edit

  33. Make use of the same getUiRoot() method in your controller’s handers when you want to switch the UI:
    Edit

  34. Run your program again, and notice how each controller is only ever created once (the output of the initialize() methods):
    Edit

  35. Now it’s time to get your songs playing! This is left for you to figure out. But in a nutshell, here’s how you load a resource file (the audio file), place it in a Media object, create a MediaPlayer with it, and play() it:
    Edit

  36. You will need a new MediaPlayer instance for each Media. You might want to make the MediaPlayer instance accessible by other handlers so you can stop() it before playing another song, and before switching back to the counter UI.
  37. Extra: Make the UI look a little nicer. Read up on applying CSS for JavaFX (store the CSS files in the resources/css folder).