OOP-Lab05-GUIProgramming
OOP-Lab05-GUIProgramming
Lecturer:
Teaching Assistant:
0. Assignment Submission
For this lab class, you will have to turn in your work twice, specifically:
§ Right after the class: for this deadline, you should include any work you have done within the lab
class.
§ 10PM five days after the class: for this deadline, you should include the source code of all sections
of this lab, into a branch namely “release/lab05” of the valid repository.
After completing all the exercises in the lab, you have to update the use case diagram and the class
diagram of AIMS project.
Each student is expected to turn in his or her own work and not give or receive unpermitted aid. Otherwise,
we would apply extreme methods for measurement to prevent cheating. Please write down answers for all
questions into a text file named “answers.txt” and submit it within your repository.
1. Swing components
Note: For this exercise, you will create a new Java project named GUIProject, and put all your source
code in a package called “hust.soict.globalict.swing” (for ICT) or “hust.soict.dsai.swing” (for DS & AI).
In this exercise, we revisit the elements of the Swing API and compare them with those of AWT through
implementing the same mini-application using the two libraries. The application is an accumulator which
accumulates the values entered by the user and display the sum.
Figure 1. SwingAccumulator
Page 1 of 34
1.1. AWTAccumulator
1.1.1. Create class AWTAccumulator with the source code as below
1.1.2. Explanation
- In AWT, the top-level container is Frame, which is inherited by the application class.
- In the constructor, we set up the GUI components in the Frame object and the event-handling:
Page 2 of 34
• In line 13, the layout of the frame is set as GridLayout
• In line 15, we add the first component to our Frame, an anonymous Label
• In line 17-19, we add a TextField component to our Frame, where the user will enter
values. We add a listener which takes this TextField component as the source, using a
named inner class.
• In line 21, we add another anonymous Label to our Frame
• In line 23 – 25, we add a TextField component to our Frame, where the accumulated sum
of entered values will be displayed. The component is set to read-only in line 24.
• In line 27 – 29, the title & size of the Frame is set, and the Frame visibility is set to true,
which shows the Frame to us.
- In the listener class (line 36 - 44), the actionPerformed() method is implemented, which
handles the event when the user hit “Enter” on the source TextField.
• In line 39-42, the entered number is parsed, added to the sum, and the output TextField’s
text is changed to reflect the new sum.
- In the main() method, we invoke the AWTAccumulator constructor to set up the GUI
Page 3 of 34
1.2. SwingAccumulator
1.2.1. Create class SwingAccumulator with the source code as below:
1.2.2. Explanation
- In Swing, the top-level container is JFrame which is inherited by the application class.
Page 4 of 34
- In the constructor, we set up the GUI components in the JFrame object and the event-handling:
• Unlike AWT, the JComponents shall not be added onto the top-level container (e.g.,
JFrame, JApplet) directly because they are lightweight components. The JComponents
must be added onto the so-called content-pane of the top-level container. Content-pane is in
fact a java.awt.Container that can be used to group and layout components.
• In line 15, we get the content-pane of the top-level container.
• In line 16, the layout of the content-pane is set as GridLayout
• In line 18, we add the first component to our content-pane, an anonymous JLabel
• In line 20-22, we add a JTextField component to our content-pane, where the user will
enter values. We add a listener which takes this JTextField component as the source.
• In line 24, we add another anonymous JLabel to our content-pane
• In line 26 – 28, we add a JTextField component to our content-pane, where the
accumulated sum of entered values will be displayed. The component is set to read-only in line
27.
• In line 30 – 32, the title & size of the JFrame is set, and the Frame visibility is set to true,
which shows the JFrame to us.
- In the listener class (line 39 - 47), the code for event-handling is exactly like the
AWTAccumulator.
- In the main() method, we invoke the SwingAccumulator constructor to set up the GUI
1.3. Compare Swing and AWT elements
Programming with AWT and Swing is quite similar (similar elements including container/components,
event-handling). However, there are some differences that you need to note:
o The top-level containers in Swing and AWT
o The class name of components in AWT and corresponding class’s name in Swing
Figure 5. NumberGrid
This class allows us to input a number digit-by-digit from a number grid into a text field display. We can
also delete the latest digit or delete the entire number and start over.
Page 6 of 34
Figure 6. NumberGrid source code(1)
Page 7 of 34
Figure 7. NumberGrid source code(2)
The buttons share the same listener of the ButtonListener class, which is a named inner class.
2.2.3. Complete inner class ButtonListener
Your task is to complete the implementation of the ButtonListener class below:
In the actionPerformed() method, we will handle the button pressed event. Since we have many
sources, we need to determine which source is firing the event (which button is pressed) and handle each
case accordingly (change the text of the display text field). Here, we have three cases:
- A digit button: a digit is appended to the end
- DEL button: delete the last digit
- C button: clears all digits
Page 8 of 34
The code for the first case is there for reference, you need to implement by yourself the remaining two cases.
Page 9 of 34
This will be our View Store Screen.
Declare one attribute in the StoreScreen class: Store store. This is because we need information
on the items in the store to display them
3.1.2. The NORTH component
Create the method createNorth(), which will create our NORTH component:
Page 10 of 34
Create the method createHeader():
Here, we see that each cell is an object of class MediaStore, which represents the GUI element for a
Media in the Store Screen.
3.1.4. The MediaStore class
Here, since the MediaStore is a GUI element, it extends the JPanel class. It has one attribute: Media
media.
Page 11 of 34
Figure 16. MediaStore source code
Note how the code checks if the Media implements the Playable interface to create a “Play” button.
3.1.5. Putting it all together
Finally, we have all the component methods to use in the constructor of StoreScreen:
Page 12 of 34
3.2. Adding more user interaction
We have successfully set up all the components for our store, but they are just static – buttons and menu
items don’t respond when being clicked. Now, it’s your task to implement the handling of the event when
the user interacts with:
• The buttons on MediaHome:
o When user clicks on the “Play” button, the Media should be played in a dialog window. You
can use JDialog here.
o When user clicks on the “Add to cart” button, the Media should be added to the cart.
Note: The GUI of the “View cart” & “Update Store” functionality will be implemented in the next exercises.
For now, it should suffice to still use the console interface for them.
4. JavaFX API
Note: For this exercise, you will continue to use the GUIProject, and put all your source code in a
package called “hust.soict.globalict.javafx” (for ICT) or “hust.soict.dsai.javafx” (for DS & AI). You
might need to add the JavaFX library to this project if you are using JDK version after 1.8.
In this exercise, we revisit the components of a JavaFX application by implementing a simple Painter
app which allows user to draw on a white canvas with their mouse.
Page 13 of 34
Recall the basic structure of a JavaFX application: It uses the metaphor of a theater to model the graphics
application. A stage (defined by the javafx.stage.Stage class) represents the top-level container
(window). The individual controls (or components) are contained in a scene (defined by the
javafx.scene.Scene class). An application can have more than one scene, but only one of the scenes
can be displayed on the stage at any given time. The contents of a scene is represented in a hierarchical
scene graph of nodes (defined by javafx.scene.Node).
Like any other JavaFX applications, there are 3 steps for creating this Painter app as follows:
- Create the FXML file “Painter.fxml” (we will be using Scene Builder)
- Create the controller class PainterController
- Create the application class Painter
The FXML file lays out the UI components in the scene graph. The controller adds the interactivity to these
components by providing even-handling methods. Together, they complete the construction of the scene
graph. Finally, the application class creates a scene with the scene graph and add it to the stage.
Page 14 of 34
Figure 20. Create a new FXML in Eclipse(1)
Name the file “Painter” and choose BorderPane as the root element as in Figure 21
A new file is created. Right-click on it in Project Explorer and select Open with SceneBuilder
Page 15 of 34
Figure 22. Open FXML with SceneBuilder from Eclipse
For the right-side section, we use a regular Pane. On the other hand, for the left-side section, since we want
to arrange subsequent items below the previous ones vertically, we use a VBox layout.
Step 1. Configuring the BorderPane – the root element of the scene
- We set the GridPane’s Pref Width and Pref Height properties to 640 and 480 respectively. Recall
that the stage’s size is determined based on the size of the root node in the FXML document
- Set the BorderPane’s Padding property to 8 to inset it from the stage’s edges
Page 16 of 34
Figure 24. Configuring the BorderPane
Page 17 of 34
Figure 26. Configuring the VBox
Page 18 of 34
Figure 28. Configuring the Button
Now that all the elements are set, you can preview them by selecting Preview > Show Preview in Window
4.2. Create the controller class
In the same package as the FXML, create a Java class call PainterController. You can also utilize
Scene Builder for coding the controller as follows: Select View > Show Sample Controller Skeleton. A
window like in XXX will appear:
Page 19 of 34
You can choose to copy the skeleton and paste it in your PainterController.java file. Remember
to replace the class name in the skeleton code with your actual class name (PainterController). The
results look roughly like this:
For the clearButtonPressed() method, we simply need to clear all the Circle objects on the Pane.
Again, we have to access the Pane’s children list through Pane.getChildren().
The source code for the controller is complete, however, to ensure that an object of the controller class is
created when the app loads the FXML file at runtime, you must specify the controller class’s name in the
FXML file using Scene Builder, in the lower right corner under Document menu > Controller.
Page 20 of 34
Figure 33. Specify the controller for the FXML file in Scene Builder
Page 21 of 34
Figure 35. Painter with Eraser
Hint:
• For the interface design: use TitledPane and RadioButton. Using Scene Builder, set the
Toggle Group properties of the RadioButtons as identical, so only one of them can be selected at a
time.
• For the implementation of Eraser: One approach is to implement an eraser just like a pen above,
but use white ink color (canvas color) instead.
Page 22 of 34
Figure 36. View Cart Screen
Like the previous exercise, we start by creating an FXML file named “cart.fxml” with BorderPane being
the root node. This layout is the most appropriate for our screen, which clearly has three distinct areas
(bounded in red borders) corresponding to TOP, CENTER and RIGHT areas of BorderPane.
5.1. Setting up the BorderPane
- Layout: Pref Width: 1024
- Layout: Pref Height: 768
5.2. Setting up the TOP area
Since the TOP area contains only the MenuBar on top of the header, we will use the Vbox layout.
Step 1. Drag a VBox into the BorderPane’s NORTH area.
- Layout: Pref HEIGHT: USE_COMPUTED_SIZE
Step 2. Add a MenuBar into the VBox
- Set up the MenuBar as in Figure 37:
Page 23 of 34
Figure 37. Set up of the MenuBar
Page 24 of 34
5.4. Setting up the RIGHT area
Step 1. Drag a VBox into the RIGHT area
- Properties: Alignment: TOP_CENTER
- Layout: Pref Width: USE_COMPUTED_SIZE
- Layout: Padding: 50 top
Step 2. Add an HBox into the VBox
- Properties: Alignment: CENTER
- Layout: Pref Width: USE_COMPUTED_SIZE
- Layout: Pref Height: USE_COMPUTED_SIZE
Step 3. Add a Label into the HBox
- Properties: Text: Total:
- Properties: Font: 24px
- Layout: Spacing: 10
Step 4. Add another Label into the HBox
- Properties: Text: 0 $
- Properties: Font: 24px
- Properties: Text Fill: #00ffff (AQUA)
Step 5. Add a Button into the VBox
- Properties: Text: Place Order
- Properties: Font: 24px
- Properties: Text Fill: #ffffff (WHITE)
- Properties: Style: -fx-background-color: red
Page 25 of 34
Figure 38. Source code for CartScreen
We create a CartScreen class that extends the JFrame, just like the StoreScreen class. Inside its
constructor, we do the following steps:
• In line 23 – 24, we set up a JFXPanel in our JFrame.
• In line 32 – 37, we load the root Node from the FXML file and create its controller object (we will
come to the actual implementation of the controller in the next section).
• In line 38, we create a new Scene with the root Node and add the Scene to the JFXPanel.
Page 26 of 34
Figure 39. Source code for CartScreenController
Recall earlier in the CartScreen constructor method, the constructor for CartScreenController
is invoked, then the load() method of the FXMLLoader. In the load() method, any @FXML annotated
fields are populated, then initialize() is called. That means we use can use the initialize()
method to perform any post-processing on the content of the JFrame after it is loaded from FXML and
before it is shown.
In line 42, we set the cart’s list of items to the items of the TableView. Note that this will initially cause
an error, because we cannot set a regular List as the items of a TableView. Instead, we have to use an
ObservableList, so that any change on the data can be observed and reflected by the TableView.
Please open the source code of Cart and change the itemsOrdered from List<Media> to
ObservableList<Media>
After setting the items of the TableView, the data still isn’t showing up in the TableView yet, because
we still have to set up the way the columns can retrieve data. This is done by setting the columns’
cellValueFactory.
In line 36 – 41, we set the columns’ cellValueFactory using the class
PropertyValueFactory<S, T> (Read the Javadocs for more details). This class is a callback that
will take in a String <property> and look for the method get<property>() in the Source S
Page 27 of 34
class. If a method matching this pattern exists, the value returned from this method is returned to the
TableCell.
You can now test the View Cart Screen with some media in your cart, the results will look roughly like in
Figure 41
Page 28 of 34
Figure 42. Modified initialize() method
Put some code at the end of the initialize() method to add a ChangeListener to the
TableView’s selectedItem property (line 76 – 86 in Figure 42). Here, we create an anonymous inner
class for the ChangeListener. All ChangeListeners must implement the changed() method.
Whenever a selected item in the TableView is changed, the method changed() is called. Here, we
check to make sure the newValue is not null (the user didn’t just unselect) and call the
updateButtonBar() method (Figure 43)
9. Deleting a media
Next, we will implement the event-handling for the “Remove” button. Please add a method name to the
onAction property of the button in Scene Builder. You can refer to the event-handling code as below (change
the method name to match the one used by your FXML file):
Note that we don’t need to invoke an update for the TableView because the it can already observe the
changes though the ObservableList and update its display.
Page 29 of 34
10. Filter items in cart – FilteredList
This exercise is optional (full credit can still be given for this lab without doing this exercise), but you can
do it for extra credit.
We will implement a filter that is re-applied every time the user makes a change in the filter text field. To
do this, again, we need references to the text field where the user inputs the filter string, and the two radio
buttons (to determine what criteria is being used to filter).
Similar to the above, please add the fx:id property for the components in SceneBuilder and create three
corresponding attributes in the controller:
- The TextField: tfFilter
- The RadioButton “By ID”: radioBtnFilterId
- The RadioButton “By Title”: radioBtnFilterTitle
At the end of the initialize() method, put some code to add a ChangeListener to the
TextField’s text property (illustrated in Figure 45):
Please implement by yourself the showFilteredMedia() method. Hint: You might need to change the
source code in previous exercises. Wrap the ObservableList in a FilteredList and set a new
Predicate for the FilteredList each time you need to apply a new filter.
Page 31 of 34
13.2. Raise the PlayerException in the play() method
- Update play() method in DigitalVideoDisc and Track
• For each of DigitalVideoDisc and Track, update the play() method to first check the
object's length using getLength() method. If the length of the Media is less than or equal to
zero, the Media object cannot be played.
• At this point, you should output an error message using System.err.println() method and
the PlayerException should be raised.
- The example of codes and results for the play() of DigitalVideoDisc are illustrated in the following
figures.
public void play() throws PlayerException {
if (this.getLength() > 0) {
// TODO Play DVD as you have implemented
} else {
throw new PlayerException("ERROR: DVD length is non-positive!");
}
}
- Save your changes and make the same with the play() method of Track.
13.3. Update play() in the Playable interface
- Change the method signature for the Playable interface's play() method to include the throws
PlayerException keywords.
13.4. Update play() in CompactDisc
- The play() method in the CompactDisc is more interesting because not only it is possible for the
CompactDisc to have an invalid length of 0 or less, but it is also possible that as it iterates through the
tracks to play each one, there may have a track of length 0 or less
- First update the play() method in CompactDisc class to check the length using getLength() method
as you did with DigitalVideoDisc
- Raise the PlayerException. Be sure to change the method signature to include throws
PlayerException keywords.
- Update the play() method to catch a PlayerException raised by each Track using block try-
catch.
The code example is shown as follows.
Page 32 of 34
public void play() throws PlayerException{
if(this.getLength() > 0) {
// TODO Play all tracks in the CD as you have implemented
java.util.Iterator iter = tracks.iterator();
Track nextTrack;
while(iter.hasNext()) {
nextTrack = (Track) iter.next();
try {
nextTrack.play();
}catch(PlayerException e) {
throw e;
}
}
}else {
throw new PlayerException("ERROR: CD length is non-positive!");
}
}
- You should modify the above source code so that if any track in a CD can’t play, it throws a
PlayerException exception.
Page 33 of 34
15. Modify the equals() method of Media class
- Two medias are equal if they have the same title
- Please remember to check for NullPointerException and ClassCastException if applicable.
You may use instanceof operator to check if an object is an instance of a ClassType.
Page 34 of 34