Before we leave the image-viewer project behind us, we want to add a few last improvements, and in the process look at two more GUI components: buttons and borders.
11.7.1 Buttons
We now want to add functionality to the image viewer to change the size of the image. We do this by providing two functions: larger, which doubles the image size, and smaller, which halves the size. (To be exact, we double or halve both the width and the height, not the area.) One way to do this is to implement filters for these tasks. But we decide against it. So far, filters never change the image size, and we want to leave it at that. Instead, we introduce a toolbar on the left side of our frame with two buttons in it labeled Larger and Smaller (Figure 11.12). This also gives us a chance to experiment a bit with buttons, containers, and layout managers.
Exercise 11.43 Implement an edge detection filter. Do this by analyzing the nine pixels in a three-by-three square around each pixel (similar to the smooth filter), and then set the value of the middle pixel to the difference between the highest and the lowest value found. Do this for each color component (red, green, blue). This also looks good if you invert the image at the same time.
Exercise 11.44 Experiment with your filters on different pictures. Try applying multiple filters, one after another.
Figure 11.12 Image viewer with toolbar buttons
11.7 ImageViewer 3.0: more interface components | 403
So far, our frame uses a BorderLayout, where the WEST area is empty. We can use this area to add our toolbar buttons. There is one small problem, though. The WEST area of a BorderLayout can hold only one component, but we have two buttons.
The solution is simple. We add a JPanel to the frame’s WEST area (as we know, a JPanel is a container), and then stick the two buttons into the JPanel. Code 11.14 shows the code to do this.
// Create the toolbar with the buttons JPanel toolbar = new JPanel();
smallerButton = new JButton("Smaller");
toolbar.add(smallerButton);
largerButton = new JButton("Larger");
toolbar.add(largerButton);
contentPane.add(toolbar, BorderLayout.WEST);
Code 11.14 Adding a toolbar panel with two buttons
When we try this out, we see that it works partially but does not look as expected. The reason is that a JPanel uses, by default, a FlowLayout, and a FlowLayout arranges its components horizontally. We would like them arranged vertically.
We can achieve this by using another layout manager. A GridLayout does what we want. When creating a GridLayout, constructor parameters determine how many rows and columns we wish to have. A value of zero has a special meaning here, standing for “as many as necessary.”
Thus, we can create a single column GridLayout by using 0 as the number of rows and 1 as the number of columns. We can then use this GridLayout for our JPanel by using the panel’s setLayout method immediately after creating it:
JPanel toolbar = new JPanel();
toolbar.setLayout(new GridLayout(0, 1));
Alternatively, the layout manager can also be specified as a constructor parameter of the container:
JPanel toolbar = new JPanel(new GridLayout(0, 1));
Exercise 11.45 Add two buttons labeled Larger and Smaller to your latest version of the project, using code similar to the one above. Test it. What do you observe?
Exercise 11.46 Change your code so that your toolbar panel uses a GridLayout, as discussed above. Test. What do you observe?
If we try this out, we can see that we are getting closer, but we still do not have what we want. Our buttons now are much larger than we intended. The reason is that a container in a BorderLayout (our toolbar JPanel in this case) always covers its whole area (the WEST area in our frame). And a GridLayout always resizes its components to fill the whole container.
AFlowLayout does not do this; it is quite happy to leave some empty space around the components. Our solution is therefore to use both: the GridLayout to arrange the buttons in a column and a FlowLayout around it to allow some space. We end up with a GridLayout panel inside a FlowLayout panel inside a BorderLayout. Code 11.15 shows this solution.
Constructions like this are very common. You will often nest various containers inside other containers to create exactly the look you want.
// Create the toolbar with the buttons.
JPanel toolbar = new JPanel();
toolbar.setLayout(new GridLayout(0, 1));
smallerButton = new JButton("Smaller");
toolbar.add(smallerButton);
largerButton = new JButton("Larger");
toolbar.add(largerButton);
// Add toolbar into panel with flow layout for spacing.
JPanel flow = new JPanel();
flow.add(toolbar);
contentPane.add(flow, BorderLayout.WEST);
Code 11.15 Using a nested GridLayout container inside a FlowLayout container
Our buttons now look quite close to what we were aiming for. Before adding the finishing polish, we can first work on making the buttons work.
We need to add two methods, named, for instance, makeLarger and makeSmaller, to do the actual work, and we need to add action listeners to the buttons that invoke these methods.
Exercise 11.47 In your project, add two method stubs named makeLarger and make Smaller. Initially, put just a single println statement into these method bodies to see when they have been called. The methods can be private.
Exercise 11.48 Add action listeners to the two buttons that invoke the two new methods.
Adding action listeners to buttons is identical to adding action listeners to menu items. You can essentially copy the code pattern from there. Test it. Make sure your makeSmaller and makeLarger methods get called by activating the buttons.
11.7 ImageViewer 3.0: more interface components | 405
11.7.2 Borders
The last polish we want to add to our interface is some internal borders. Borders can be used to group components or just to add some space between them. Every Swing component can have a border.
Some layout managers also accept constructor parameters that define their spacing, and the lay- out manager will then create the requested space between components.
The most used borders are BevelBorder,CompoundBorder,EmptyBorder,EtchedBorder, andTitledBorder. You should familiarize yourself with these.
We shall do three things to improve the look of our GUI:
■ add some empty space around the outside of the frame
■ add spacing between the components of the frame
■ add a line around the image
The code to do this is shown in Code 11.16. The setBorder call on the content pane with an EmptyBorder as a parameter adds empty space around the outside of the frame. Note that we now cast the contentPane to a JPanel, as the supertype Container does not have the set- Border method.6
Exercise 11.49 Properly implement the makeSmaller and makeLarger methods. To do this, you have to create a new OFImage with a different size, copy the pixels from the current image across (while scaling it up or down), and then set the new image as the current image.
At the end of your method, you should call the frame’s pack method to rearrange the compo- nents with the changed size.
Exercise 11.50 All Swing components have a setEnabled(boolean) method that can enable and disable the component. Disabled components are usually displayed in light gray and do not react to input. Change your image viewer so that the two toolbar buttons are ini- tially disabled. When an image is opened, they should be enabled, and when it is closed, they should be disabled again.
6Using a cast in this way only works because the dynamic type of the content pane is already JPanel.
The cast does not transform the content-pane object into a JPanel in any sense.
JPanel contentPane = (JPanel) frame.getContentPane();
contentPane.setBorder(new EmptyBorder(12, 12, 12, 12));
// Specify the layout manager with nice spacing.
contentPane.setLayout(new BorderLayout(6, 6));
imagePanel = new ImagePanel();
imagePanel.setBorder(new EtchedBorder());
contentPane.add(imagePanel, BorderLayout.CENTER);
Code 11.16 Adding spacing with gaps and borders
Creating the BorderLayout with two int parameters adds spacing between the components that it lays out. And finally, setting an EtchedBorder for the imagePanel adds a line with an “etched” look around the image. (Borders are defined in the package javax.swing.
border—we have to add an import statement for this package.)
All the improvements discussed in the section have been implemented in the last version of this application in the book projects: imageviewer3-0. In that version, we have also added a Save As function to the file menu so that images can be saved back to disk.
And we have added one more filter, called Fish Eye, to give you some more ideas about what you can do. Try it out. It works especially well on portraits.