Swing, Top-Down (With GridBagLayout)
For this second stage I am exploring, and discussing, a number of different features of, and approaches to, Swing. I have kept the term top-down in the title because each section still provides a broad overview. My target audience remains an experienced programmer, perhaps with experience of building GUIs in another language. This topic list suggests what will be covered:
- Labels and Association
- Listening at the Window
- Using an Inner Class to Listen
- Handling Secondary Objects
- Using Helper Methods
- To Help, Overload or Extend
- JFormattedTextFields and InputVerifiers
Here is how the final frame will look:
Rather than starting a new GUI/frame completely from scratch I've taken the one from my previous tutorial, removed a couple of components and moved things around a little. I don't want to be distracted with concerns about the design, and appearance, of the frame; I want to concentrate on new features and functionality. (Design and appearance are, obviously, very important, but I've already discussed these in part 1.)
Copy and paste this code to a new class MyGui2:
The driver is the same as before but using MyGui2():
There are only a few changes:
This includes the code that shows/hides the "US" label.
firstName and lastName have been replaced with a single fullName field.
genderGroup is now a member-variable.
There are a couple more countries.
JComboBox is now JComboBox<String>. Without this change JDK 5+ produces a warning message:
JComboBox is a raw type. References to generic type JComboBox<E> should be parameterized
This is discussed further here at SO.
Build and run this code as it is the starting point for what follows. Read through it to make sure you are comfortable before we add extra features to it. It looks like this initially:
As we add new features to our application, and run and test it, don't worry if the frame is shorter than you expect, or components don't appear exactly where we will want them. (You could, if you really wanted to, use NORTHWEST instead of just WEST to push a component higher up on the frame, but it is not necessary, and I don't advise it.) When the tutorial is completed everything will be in the right place.
Remember that you should use pencil and paper (or software tools) to design your GUI first, as discussed in the first tutorial.
As you progress through the tutorial, if you are not sure where to place new code, refer to the full code at the end.
Labels and Association
Labels are usually paired with another component, and we should make this association clear using setLabelFor():
Often, a label describes another component. When this occurs, you can improve your program's accessibility by using the setLabelFor method to identify the component that the label describes. For example:
We cannot use this method with anonymous inner classes, such as:
addGridItem(panel, new JLabel("Full Name:"), 0, 0, 1, 1, GridBagConstraints.EAST);
This label, and the fullName text-field, should be associated, so we need to remove the anonymity:
JLabel fullNameLbl = new JLabel("Full Name:"); addGridItem(panel, fullNameLbl, 0, 0, 1, 1, GridBagConstraints.EAST); fullName = new JTextField(20); fullNameLbl.setLabelFor(fullName);
fullNameLbl doesn't need to be a member-variable though, as we won't be doing anything else with it.
I haven't done this elsewhere for this tutorial but you should certainly do so for your own projects. It would be good practice to modify my code to use this method consistently, once you've completed the tutorial. I'll remind you about this at the end.
Listening at the Window
We have only used ActionListener so far, responding to button-clicks and combo-box selection, but there is also WindowListener. This, of course, responds to Window events which, in effect, means frame (or application) level events. If our frame-class were to implement WindowListener then an IDE such as eclipse will offer to add unimplemented methods for us, producing the following code:
It is very helpful that modern IDEs can generate these code-stubs for us. However, for most applications, we will only need one or two of the events. Most often windowClosing and/or windowOpened. (windowActivated will fire every time the frame is activated, including when it is first opened.)
Instead of implementing WindowListener we can use a WindowAdapter. This provides default method-implementations for us, so we only need to override the one or two we need.
The following code resolves the issue of the combo-box possibly showing "US" when first opening the frame/window, even though "United States" may not be selected in the list.
I also ensure that exiting the application by any means will exhibit the same behaviour as clicking our Exit button. This is achieved by calling doClick() on this button. In order for this to work we need to change EXIT_ON_CLOSE to DO_NOTHING_ON_CLOSE. The Exit button only produces a goodbye message, if it did any more than this then I would prefer to create and call a separate method, with a name such as beforeExit.
Using an Inner Class to Listen
Rather than the JFrame implementing ActionListener we can use an inner class. One advantage is that we can use more than one of these. This way we could, for example, have one listener that responds to certain button clicks and another that responds to changing the selected-item of combo- or list-boxes. (This was discussed in a few comments at the end of my first tutorial.)
Modify the current actionPerformed as follows, as it will now only respond to the combo-box:
MultiListener Example :tutoracles
Handling Secondary Objects
By secondary objects I mean, for example, Border, Font. That is, objects that work with or tailor components. I will re-examine the GridBagConstraints object for this discussion.
Currently our helper-method addGridItem() creates, and then discards, a GridBagConstraints object. We pass significant arguments for it: x, y, width, height and alignment (anchor). Other values are set consistently in the method, or are left as their default-values. This is a good approach because:
- For a specific panel (and specific frame) we are likely to use the same constraint-values consistently.
- We can modify or add constraint-values within the method to change the overall appearance of the panel(s).
The disadvantage is that if we need to tweak a constraint-value for a particular component we're in trouble. We can:
- Modify the method to account for a specific component. This is a terrible approach.
- Ignore the method and create a constraints-object just for this particular component. This grates. If we do this more than once then it might tempt us to abandon the method altogether.
- Having used the method it may be possible to reach inside the panel to locate and modify the constraints for this component.
(To be honest, I haven't investigated this third approach. I'm assuming it is possible, but it is not something that excites me.)
The other approaches are to create a new constraints-object whenever we add a component to the panel. This leads to huge, illegible, code. Or we can create and modify a single constraints-object. This is better but we would still need to modify a number of values each time, and it is very easy to forget one; finding this particular value could be tricky (there won't be any errors to help us).
I will take the following approach:
- Create and maintain a single GridBagConstraints instance.
- Set the default, common, values that we want.
- Continue to use out addGridItem() method when adding components.
The advantage is that we can change any constraint-values that we need to before calling the method.
This still isn't perfect. If we change some constraint-values before calling the method then we need to remember to set them back to the previous values (or some other values) before calling the method again. Nevertheless I like this approach, particularly bearing in mind that we shouldn't need to change constraint-values very often.
Also refer to the section below on overloading and extending.
Using Helper Methods
I want to add some radio-buttons for "Marital Status". These will be styled in the same way as the "Gender" buttons.
Have a look at the current code:
We could just copy and paste this code but instead will create a helper-method. The advantages are:
- The code will be neater and easier to read.
- If we decide to add a third radio-group it will be straight-forward.
- We can change the styling for all the radio-groups in one place.
- We could possibly copy and paste the method into other projects.
We can create a method that returns a Box, passing it the list of choices and a title. Then we can just add this Box to the panel using addGridItem(). However, there is a complication. We might need to refer to the ButtonGroup later, as we do for the genderGroup. This does make it slightly messier, but we can just pass a ButtonGroup to the method.
Before you modify the code though, there is another complication. Because the radio buttons are created in the method, we no longer have individual references to them. The line:
in the actionPerformed method can no longer be used and needs to be deleted. If we still want to set the focus we can use:
//genderBox.requestFocus(); // or genderBox.getComponents().requestFocus();
The second method is preferable because, with the first, it is not clear that the radio-group has the focus. Also, with the second approach, we don't actually care which radio has the focus, so it is sensible just to highlight the first one in the list.
This discussion, and the code amendments that follow, are worthwhile. However, for something as fixed, and brief, as gender and marital status, I would probably stick with the copy/paste approach. If values were being obtained from a database (or other source) I would create the helper-method, because the individual items would not be known in advance anyway.
Here is the revised section of member-variables:
Here's the helper-method:
and here is the revised and new code in the constructor:
What about aligning things horizontally rather than vertically? We could add an additional argument to the method. Alternatively, we could create a second method, copying and pasting the first. Aligning things horizontally may require some different styling to vertically.
A Conclusion About Helper Methods
We are building an application, and a GUI for this application. If helper methods help us to keep the code neat, and give us some flexibility to add other, similar, components, then we should use them.
We are not building a Components-Library for use in other applications. Our helper-methods do not have to be completely flexible and portable. (It is not possible to make them completely flexible anyway, as we could only achieve this by completely reproducing the Swing Object Model.) However, if our helper-methods are well thought out, it is likely that we will be able to copy and paste them for use in future projects.
To Help, Overload or Extend
This brief discussion is relevant to, and follows on from, the previous sections on secondary objects and helper methods.
Referring back to our discussion of the addGridItem() helper-method, and the need to tweak it, there is another approach which is more Java-esque. That is, to overload the method by providing different versions of it with different parameters. This is certainly an option that we should be aware of.
However, in my opinion, we are only building a form/frame, not a rocket-ship. These extra methods add complexity. Besides, the methods are likely to be very similar and I have already demonstrated an alternative approach to tweaking the method; that is, to maintain an instance of the object (or objects) that the method uses.
Bottom-line: Be aware of the option to overload methods, and make use of it if it suits your purpose.
Almost every Swing Component can be extended. We have already extended JFrame to provide our GUI. We could have:
- Created a class which constructs and displays our JFrame without actually extending it. The construction and display of the form could be from a static method as in the example here.
- This class could instead include a getFrame() method to return the frame to be displayed by the caller.
- This class could include inner classes which extend, typically, JPanels, and maybe other components, which are then added to the JFrame.
- Even with our current approach we could extend JPanel, and perhaps other components, using an inner class.
I will state clearly that I am not suggesting anything definitive about these approaches. They continue to be discussed and debated by wiser heads than mine. I only have some, hopefully, common-sense advice:
- The first approach is a very common alternative to our current one. I am happy to take either approach.
- The second approach concerns me a little, and we would need to be careful to avoid any potential cross-threading issues.
The issue with extending components is, firstly, that of complexity. It would need to be a fairly complex frame to warrant extending anything more than a JPanel or two. Secondly, we are not, actually, extending the class(es). That is, we are not adding extra functionality. (Well, let's say that "most of time" we won't be adding extra functionality.)
Anyway, I will leave the discussion at this point. You might do some research if it interests, or concerns, you.
JFormattedTextFields and InputVerifiers
JFormattedTextField extends JTextField to enable formatting of the text to represent, for example, a valid date, number or phone number. If an invalid value is entered it will, on losing focus, simply revert to its original, default, value. This behaviour isn't particularly useful.
We can attach an InputVerifier to this component, to verify the value and take appropriate action. I am briefly introducing this component and a verifier, but please note that InputVerifiers can be used with many of the Swing components, including the simpler JTextField.
There is much more to a JFormattedTextField than I am discussing here (as well as to all the other Swing components!).
Validating/ verifying user-input is a big subject and I am just providing a brief introduction. There is an interesting tutorial here at Javalobby (although it is from 2005), and a library discussed here at dzone.
Let's add a 'date of birth' field. We will use a SimpleDateFormat() to specify the formatting for display of a date, and default it to today's date (new Date()). If we are not able to parse the value the user enter's to a Date-value we'll beep and select the text they have just entered.
I used NORTHWEST/NORTHEAST (temporarily) rather than just WEST/EAST so that it might be less distracting for you, seeing components apparently in the wrong position, and perhaps jumping around. Nevertheless, I recommend that you DON'T do this. Just build the frame, and the panel/GridBagLayout, in accordance with your original pencil-and-paper design. Once the frame is fully built, and the components tested, then, at the end of the whole process, you can correct any alignment issues.
I want to emphasize this: it is inevitable that you will build and test the frame, and its components, in stages. You WILL encounter alignment issues. There is little point in trying to fix these until the frame is complete. If you do take this approach then you will probably find that you have to reverse most of the adjustments that you made! Have faith in your pencil-and-paper design.
I was briefly tempted to include code that would verify that the person was at least than 18 years old. This is not as straight-forward as it should be. Nevertheless, I recommend that you investigate this for yourself.
Rather than just selecting the date of birth I want to display an error message. We will add a JLabel and use setVisible() to show or hide it.
Adding a JLabel directly to a cell, and hiding and showing it, won't work. (That is, not without considerable effort.) The other components on the panel will jump around. Using another component (a container) will resolve this. I'm using a Box, which might seem a little odd as it only contains a single component; you could use a different container if you prefer.
A better approach could be to put both the date-of-birth field and the error-label in the same container. If using a Box you could use a strut to separate them, and the Box could span two rows if appropriate.
Please note that I wouldn't use a JFormattedTextField for a date-value. I would probably either use three separate combo-boxes or find a calendar (a date chooser) plug-in. I'm just using it for illustration. Here's an example for a phone number:
Don't neglect the Java Tutorials and documentation:
Trail: Creating a GUI With JFC/Swing
A reminder about the setLabelFor() method mentioned much earlier in this tutorial. I believe it would be a good exercise for you to modify the final code to use this method wherever appropriate.
Here is the complete code: