Chapter 14. First Swing Application, Layout, and Menus

CONTENTS

Chapter 14. First Swing Application, Layout, and Menus

  •  Putting Things Together with Basic Java GUIs
  •  Adding an Input Form for an Address Entry: The Prototype
  •  Adding an Input Form for an Address Entry: First Cut
  •  Adding a Main Window for the Address Book Application: Prototype
  •  Adding a Main Window: First Cut
  •  Adding a Toolbar and a Dialog for the Address Book Application
  •  Menus
  •  Layout Managers
  •  A GridBagLayout Example
  •  Putting Things Together: Adding GridBagLayout to the Address Application
  •  Summary

Terms in This Chapter

  • Absolute positioning

  • Dialog

  • Helper function

  • Index

  • Layout manager

  • Main window

  • Modal

  • Separator

In the last chapter, we covered Swing with short code snippets; in this chapter, we'll build an example Swing application. Snippets are great for learning how to use an API, but to see how things really come together you need a more involved example.

We'll continue with the address book application, but we'll start off with minimal features and add more as we put the application together, gradually showing more of Java GUI programming like menus and layout managers. Before we add any features, though, we'll do a prototype in an interactive session to give you hands-on, instant gratification.

Putting Things Together with Basic Java GUIs

As promised, we're going to extend the address book application with a GUI front end. For this extension we'll use Address5.py ,which is based on Address4.py from Chapter 8 with a few changes, all within the readAddresses() and writeAddresses() functions. One change is the use of ObjectOutputStream instead of the Python pickle module; another is the introduction of the __repr__ method for the Address class, which allows us to create address objects with eval and makes the printing of dictionaries more meaningful.

As an exercise, run the text_read_write() function in readAddresses() and writeAddresses() in both files. What differences do you notice in the output? How are these differences related to __repr__? Also try creating one instance of Address using the eval function and the return value from __repr__ of another instance.

Adding an Input Form for an Address Entry: The Prototype

In an interactive session, let's create a GUI form that can edit an address entry. We want a frame that holds text field components, which will be used to gather the address information. We also want an OK button, which signifies that the user is done entering the current address.

To get the layout for the frame components, we'll use a technique that layers components in a container. For this we'll put the text fields in one panel and the OK button in another. The panels themselves are components that we'll put in the frame.

The first thing we need to do is import the JFrame, JButton, and JTextField classes from javax.swing.

>>> from javax.swing import JFrame >>> from javax.swing import JButton, JTextField

Next we create a frame with the titlebar set to "Address Entry," create an OK button and a panel to hold it, set the frame's layout to BorderLayout, and add the panel to the south region of the frame. Then we pack the frame and look at the results.

>>> frame = JFrame("Address Entry", visible=1) >>> okay = JButton("OK") >>> from javax.swing import JPanel >>> buttonbar = JPanel() >>> buttonbar.add(okay) >>> from java.awt import BorderLayout >>> frame.contentPane.add(buttonbar, BorderLayout.SOUTH) >>> frame.pack()

Adding Text Fields

Now we want to add text fields to the frame. There may be many of them, and the frame is using BorderLayout, which can hold only five components (EAST, WEST, NORTH, SOUTH, and CENTER). Therefore, we add the fields to another panel, which we place in the CENTER region of the frame. We'll use GridLayout for the layout manager of the panel, which will lay the components out in a grid. As we add fields, we'll label them so that the user knows what each one represents. We'll do this by adding instances of the Label class near the corresponding text field.

Import GridLayout, and create an instance of it that lays out components in a 2-by-2 grid. Create a panel to hold the text fields called contentPane. Set the layout of contentPane to the GridLayout instance.

>>> from java.awt import GridLayout >>> grid = GridLayout(2,2) >>> contentPane = JPanel() >>> contentPane.setLayout(grid)

Create a JTextField instance called name. Import the Label class, create an instance of it, and add it to contentPane. Add the name JTextField to the panel. Then pack the frame, and look at the results.

>>> name = JTextField() >>> from java.swing import JLabel >>> contentPane.add(JLabel("Name")) >>> contentPane.add(name) >>> frame.pack() >>> frame.contentPane.add(contentPane, BorderLayout.CENTER) >>> frame.pack()

Now use the same technique to add phone number and email text fields.

Add the phone number field.

>>> phoneNumber = JTextField() >>> contentPane.add(JLabel("Phone Number")) >>> contentPane.add(phoneNumber)

Add the email field.

>>> email = JTextField()      >>> contentPane.add(JLabel("eMail"))      >>> contentPane.add(email)      >>> frame.pack()

Adding Event Handlers

We could improve our form by setting the labels' alignment to the right, but pretty interfaces are only the start. For the form to be useful, we need to set up the event handlers. This is just the prototype, so the event handlers will access the text field component data but only print it to the console.

To set up the event handler for the OK button, we need to define an event handler that takes one argument to hold the event.

Create a function that acts as an event handler for the OK button.

>>> def okayHandler(event): ...     global name, email, phoneNumber ...     print (name.text, email.text, phoneNumber.text) ...

Register the okayHandler() function with the OK button's actionPerformed event property.

>>> okay.actionPerformed = okayHandler

Now we have a working prototype. Try it out by entering a name, a phone number, and an email address, and then hit the OK button.

Adding an Input Form for an Address Entry: First Cut

At this point, you probably feel comfortable with the concepts the prototype introduces. To incorporate these concepts into the address book program, let's organize our code into a class called AddressForm. This class extends the javax.swing.JFrame class, which makes it a JFrame.

AddressForm is passed an instance of the Address class in its constructor. okayHandler() populates the fields of the instance with values extracted from AddressForm's text fields. Look at Address6.py below and make sure to read all of the comments.

from javax.swing import JFrame, JButton, JTextField, JLabel, JPanel from java.awt import BorderLayout, GridLayout       # AddressForm Class.       # This class is used to edit an instance of the Address class class AddressForm(JFrame):       def __init__(self, address):             """Constructor for the AddressForm class \n """                    # Call constuctor             JFrame.__init__(self, "Address Entry", visible=1)                    # Declare private variables             self.__address = address #hold address to be edited             self.__name = NONE   #hold the private name textfield             self.__email = NONE   #hold the private email textfield             self.__phoneNumber = NONE #hold the private phone# field             self.__init__CreateButtonBar()             self.__init__CreateTextFieldPanel()       def __init__CreateButtonBar(self):             """ Create okay button, a panel for the button. \n """             """   Add the button panel to this frame.             """             okay = JButton("OK")             buttonbar = JPanel()             buttonbar.add(okay)             self.contentPane.add(buttonbar, BorderLayout.SOUTH)                    # Set the event handler for the okay button.             okay.actionPerformed = self.__okayHandler       def __init__CreateTextFieldPanel(self):             """ Set up the email, phoneNumber and name text fields """                    # Create the panel and set the grid layout.                    # The grid has a layout of a                    # 3 row by 2 column grid.             editPane = JPanel()             editPane.setLayout(GridLayout(3,2))             self.contentPane.add(editPane, BorderLayout.CENTER)                    # Create the name textfield and add it and its                    # associated label to the contentPane.                    # Note that name is a member of AddressForm             self.__name = JTextField()             editPane.add(JLabel("Name"))             editPane.add(self.__name)                    # Create the phoneNumber textfield and add it and its                    # associated label to the contentPane.                    # Note that phoneNumber is a member of AddressForm.             self.__phoneNumber = JTextField()             editPane.add(JLabel("Phone Number"))             editPane.add(self.__phoneNumber)                    # Create the email textfield and add it and its                    # associated label to the contentPane.                    # Note that email is a member of AddressForm.             self.__email = JTextField()             editPane.add(JLabel("eMail"))             editPane.add(self.__email)             self.pack()             #Defines the event handler for the okay button       def __okayHandler(self, event):             """Event handler for the okay button"""             print (self.__name.text, self.__email.text,                    self.__phoneNumber.text)             Address.__init__(self.__address, self.__name.text,                              self.__phoneNumber.text, self.__email.text)             print self.__address

Try adding the ability to edit the address lines of the Address class and to right-align the labels that describe the text fields.

Adding a Main Window for the Address Book Application: Prototype

The main window will have a list of names from the address book. As you select a name from the list, an AddressForm instance pops up in which you edit the Address instance corresponding to that name.

As a rule, code written to access data shouldn't be in the same module with code written to build GUIs, so first we have to break our code into separate modules:

  • AddressForm.py holds the AWT version of AddressForm

  • Address7.py holds the address data code

  • AddressMain.py holds the main window of the code

  • AddressFormPane.py holds the AWT panel version of AddressForm

The first step is creating a frame that acts as the main window and then adding a list to it. Then we pack the frame and make it visible.

Import JFrame and JList from javax.swing.

>>> from javax.swing import JFrame, JList

Create instances of JFrame. Import DefaultListModel, and create a JFC list using an instance of it. Add the instance to the frame container.

>>> frame = JFrame("Main Frame") >>> from javax.swing import DefaultListModel >>> addresses = JList(DefaultListModel()) >>> frame.contentPane.add(addresses)

Pack the frame, and make it visible.

>>> frame.pack() >>> frame.visible=1

Adding Names

A list with no items is like a pizza with no toppings. For this next part, we're going to add names to our list from the dictionary that holds a collection of address entries.

Import everything from address7.py. (Run it as a main module to create a clean set of data files.)

>>> from address7 import *

Use the readAddresses() function to open the testaddr.dat file, which is full of address entries.

>>> fname="c:\\dat\\testaddr.dat" >>> dict = readAddresses(fname)

Iterate through the keys of the dictionary of address entries returned by readAddresses(), and add the keys as names in the list.

>>> for name in dict.keys(): ... addresses.model.addElement(name) ... >>> frame.pack()

Now our list has the names from our address application. The frame should look like Figure 14-1.

Figure 14-1. Address Book Names List

graphics/14fig01.gif

Adding Event Handlers

Next we need to add some functionality to our list. First we'll make the AddressForm instance pop up when we select an item, so we need to define a list event handler. How do we know which one? Remember that you can use the jinfo module to inspect a component's event properties. Or, if you remember which event listener a component handles, you can inspect it.

For example, you may remember that JList handles ListSelectionListener, or you may haved looked up JList in the Java API documentation and seen the addListSelectionListener() method, which is a dead giveaway. Let's say that we remembered, so we can do the following:

Import ListSelectionListener from the javax.swing.event package.

>>> from javax.swing.event import ListSelectionListener

Use the dir() function to list ListSelectionListener's methods.

>>> dir(ListSelectionListener) ['valueChanged']

Let's also say that we remember that all methods of a given XListener interface take an XEvent argument, so we can inspect the interface of the event object that will be passed to our event handler.

As an exercise look up the ListSelectionListener interface and the ListSelectionEvent class in the Java API documentation.

Import ListSelectionEvent from javax.swing.event, and inspect its attributes with the dir() function.

>>> from javax.swing.event import ListSelectionEvent >>> dir(ListSelectionEvent) ['firstIndex', 'getFirstIndex', 'getLastIndex', getValueIsAdjusting',     'valueIsAdjusting']

Now that we understand the event properties for List, we can set up our event handler to pop up an AddressForm instance when an item in the list is clicked.

Import AddressForm from the AddressForm module.

>>> from AddressForm import AddressForm

Create an event handler that gets the selected item from the list, looks up the corresponding Address instance in the dictionary, and passes it to the AddressForm instance, where it can be edited.

>>> def itemSelected(event): ...     list = event.source ...     name = list.selectedValue ...     address = dict[name] ...     form = AddressForm(address) ...

Register the event handler with the event property of the list.

>>> addresses.valueChanged = itemSelected

Now, every time you click on the listbox, a new window pops up that lets you edit the address. So far, so good, but we've left out a few things, like reading and writing files and closing the main window.

Adding a Main Window: First Cut

We're going to create the main window for the address book application, which will have the list on the left side of the frame when the user selects an item, it appears in the AddressForm instance on the right side. To do this we'll convert AddressForm so that it extends java.awt.Panel instead of java.awt.Frame. We'll also need to add some code to initialize the text fields in the AddressForm class (in AddressForm.py) so that every time an address is passed to AddressForm (that is, when the user selects a name in the list), AddressForm shows that Address instance.

Here is a listing of AddressForm in AddressFormPane.py:

from Address7 import Address ...       # AddressForm Class.       # This class is used to edit an instance of the Address class class AddressForm(JPanel):       ...       ...       def set(self, address):             """Used to put a new address in this form                Initializes all of the text fields with                the attributes from self.__address.             """             self.__address = address             self.__name.text = address.name()             self.__email.text = address.email()             self.__phoneNumber.text = address.phoneNumber()        ...        ... def __init__CreateTextFieldPanel(self):             """ Set up the email, phoneNumber and name text fields """ ... ...                   # Create the name textfield and add it and its                   # associated label to the contentPane.                   # Note that name is a member of AddressForm             self.__name = JTextField(self.__address.name())             editPane.add(JLabel("Name"))             editPane.add(self.__name)                  # Create the phoneNumber textfield and add it and its                  # associated label to the contentPane.                  # Note that phoneNumber is a member of AddressForm.             self.__phoneNumber=JTextField(self.__address.phoneNumber())             editPane.add(JLabel("Phone Number"))             editPane.add(self.__phoneNumber)                  # Create the email textfield and add it and its                  # associated label to the contentPane.                  # Note that email is a member of AddressForm.             print self.__address.email()             self.__email = JTextField(self.__address.email())             editPane.add(JLabel("eMail"))             editPane.add(self.__email)

Notice that I've added extra functions to the Address class email(), name(), and phoneNumber() to allow access to the private variables of the Address instance.

Moving Things Around

Now let's change the application so that the list shows up on the left side of the screen and the information shows up on the right side. The next example, from AddressMain.py, shows the AddressMain class. Much of its code was covered in the prototype from the last section.

Don't Overdo the Comments

I don't recommend using comments as I have here when you're writing real code. For more on writing comments and maintainable code, read Enough Rope to Shoot Yourself in the Foot (Holub, 1995).

from javax.swing import JFrame, JList, DefaultListModel from java.awt import BorderLayout from address7 import * from AddressFormPane import AddressForm class AddressMain(JFrame):        def __init__(self):                     # Call the base class constructor              JFrame.__init__(self, "Main Frame")                     # Create a list.              self.addresses = JList(DefaultListModel())                     # Add the addresses list to the                     # container, on the left side              self.contentPane.add(self.addresses, BorderLayout.WEST)                     # Open up the file.              self.fname="c:\\dat\\testaddr.dat"              self.dict = readAddresses(self.fname)                     # Populate the address list.              for name in self.dict.keys():                     self.addresses.model.addElement(name)                     # Set the event handler for the addresses.              self.addresses.valueChanged = self.__itemSelected                     # Create the AddressForm and add it to the EAST.              self.form = AddressForm()              self.contentPane.add(self.form, BorderLayout.EAST)                     # Pack the frame and make it visible.              self.pack()              self.visible=1              self.windowClosing = self.__windowClosing        def __itemSelected(self, event):              name = event.source.selectedValue              address = self.dict[name]                     #Notify the panel that the address has changed              self.form.set(address)        def __windowClosing(self,event):              writeAddresses(self.fname, self.dict)              from java.lang import System              System.exit(0) if __name__ == "__main__":        mainWindow = AddressMain()

If you run AddressMain.py, you should get a frame that looks like Figure 14-2. Notice that, when you click on a name in the list, its fields are displayed in the form. You can edit the fields and then hit the Apply button. Also notice that, when you change the names and then reload the application, the changed values are displayed.

Figure 14-2. Address Main Frame

graphics/14fig02.gif

Changes in AddressMain.py

As for what's different in AddressMain.py, the most obvious thing is that all of the code is contained in a class. We created an instance of AddressForm and added it to the main frame.

     # Create the AddressForm and add it to the EAST. self.form = AddressForm() self.container.add(self.form, BorderLayout.EAST)

Another difference appears in the list event handler. In the prototype, we created a new instance of AddressForm and passed its constructor an address instance from the dictionary. Here we passed the address instance from the dictionary to AddressForm's set() function. The set() function changes the address attribute of the AddressForm instance and initializes the text fields.

class AddressMain(Frame): ...        def __itemSelected(self, event):               if (event.stateChange == event.SELECTED):                     name = event.itemSelectable.getItem(event.item)                     address = self.dict[name]                            # Notify the panel that the address has changed                     self.form.set(address) class AddressForm(Panel):        ...        ...        def set(self, address):               """Used to put a new address in this form               Initializes all of the text fields with               the attributes from self.__address.               """               self.__address = address               self.__name.text = address.name()               self.__email.text = address.email()               self.__phoneNumber.text = address.phoneNumber()

Where to Find the Example

Throughout this chapter and into the next, we'll return to this AddressMain.py and make improvements to it. We'll put each improvement in a new directory within the current chapter directory, so the next improvement will be in the One directory within the Chap14 directory of the Web site's Examples directory (i.e., C\JYTHONBook\Examples\Chap14\One).

We've created a GUI application that wins no prizes for beauty and grace but gets the job done. Now, as all good programmers do, let's muck with it.

Adding a Toolbar and a Dialog for the Address Book Application

We want to be able to add new addresses to the address list. Essentially, that means two new buttons, Add and Remove, which we'll put in a new panel in the north region of the main frame. (See Figure 14-3.) When the user presses the Add button a dialog pops up. A dialog is like a frame, except that it's modal (i.e., you have to close it before you can continue using your application).

Figure 14-3. The Add and Remove Buttons

graphics/14fig03.gif

The Dialog Class

The dialog class, AddAddressDialog, is in the same module as AddressMain and contains an instance of the AddressForm panel. It extends java.awt.Dialog.

from java.awt import Dialog class AddAddressDialog(Dialog):       ...       ...

AddAddressDialog gets most of its functionality from AddressFormPane, which it contains, and adds two buttons, OK and Cancel, which appear on a panel that acts as a toolbar. (See Figure 14-4.) The toolbar is placed in the south region of the panel. The AddressFormPane instance is placed in the center region.

Figure 14-4. The Okay and Cancel Buttons

graphics/14fig04.gif

class AddAddressDialog(JDialog):       ...       ...       def __init__(self, frame):             ...             ...                    # Create a dialog box and add the AddressForm to it.             self.contentPane.add(self.__form, BorderLayout.CENTER)                    # Create a toolbar with two buttons and                    # add it to the south part of the dialog             toolbar = Panel()             ok = Button("OK")             cancel = Button("Cancel")             toolbar.add(ok)             toolbar.add(cancel)             ok.actionPerformed = self.__okEventHandler             cancel.actionPerformed = self.__cancelEventHandler             self.contentPane.add(toolbar, BorderLayout.SOUTH)

Pressing the OK button hides the dialog, saves its state, and invokes a commit() method that commits the changes to the AddressForm instance.

class AddAddressDialog(JDialog):       OK = 0       CANCEL = 1             ...             ...       def __okEventHandler(self,event):             self.visible = 0             self.__state = AddAddressDialog.OK             self.__form.commit() class AddressForm(Panel):       ...       ...       def commit(self):             self.__address.__init__(self.__name.text,             self.__phoneNumber.text, self.__email.text, [])

Pressing the Cancel button hides the dialog and saves its state.

class AddAddressDialog(JDialog):        OK = 0        CANCEL = 1              ...              ...        def __cancelEventHandler(self,event):              self.visible = 0              self.__state = AddAddressDialog.CANCEL

AddAddressDialog has a method called GetState(), which returns whether the dialog was closed with OK or Cancel, and a method called getAddress(), which returns the new address instance.

class AddAddressDialog(JDialog):        OK = 0        CANCEL = 1              ...        def GetState()(self):              return self.__state        def getAddress()(self):              return self.__address

Here is the complete listing for AddAddressDialog (from One\AddressMain.py):

from java.awt import BorderLayout from javax.swing import JFrame, JList, DefaultListModel, JPanel, JDialog, JButton from address import * from AddressFormPane import AddressForm class AddAddressDialog(JDialog):        OK = 0        CANCEL = 1        def __init__(self, frame):              JDialog.__init__(self, frame, "Add a new address")              self.__address = Address() #hold the address to be added.              self.__form = AddressForm(self.__address)                     # Create a dialog box and add the AddressForm to it.              self.contentPane.add(self.__form, BorderLayout.CENTER)                     # Create a toolbar with two buttons and                     # add it to the south part of the dialog              toolbar = JPanel()              ok = JButton("OK")              cancel = JButton("Cancel")              toolbar.add(ok)              toolbar.add(cancel)              ok.actionPerformed = self.__okEventHandler              cancel.actionPerformed = self.__cancelEventHandler              self.contentPane.add(toolbar, BorderLayout.SOUTH)                     # Pack it, make it modal, and show it              self.setModal(1)              self.pack()              self.show()        def __okEventHandler(self,event):              self.__state = AddAddressDialog.OK              self.__form.commit()        def __cancelEventHandler(self,event):              self.visible = 0              self.__state = AddAddressDialog.CANCEL        def GetState(self):              return self.__state        def getAddress()(self):              return self.__address

Adding the Toolbar

Remember that the dialog is opened in response to a click of the Add button. Let's take a look at Add in a partial listing of AddressMain with the toolbar added.

class AddressMain(JFrame):        def __init__(self):                     # Call the base class constructor              JFrame.__init__(self, "Main Frame")              ... ...              self.__init__toolbar()              ...              ...        def __init__toolbar(self):                     # Create the toolbar panel and                     # add it to the NORTH border of the container.              toolbar = JPanel()              self.contentPane.add(toolbar, BorderLayout.NORTH)                     # Create two buttons              addAddress = JButton("Add")              removeAddress = JButton ("Remove")                     # Add the buttons to the toolbar              toolbar.add(addAddress)              toolbar.add(removeAddress)                     # Registers the buttons event handler              addAddress.actionPerformed=self.__addAddress_Clicked              removeAddress.actionPerformed=self.__RemoveAddress_Clicked        def __addAddress_Clicked(self,event):              dialog = AddAddressDialog(self)              if dialog.GetState() == AddAddressDialog.OK:                     addr = dialog.getAddress()                     self.dict[addr.name()] = addr                     self.addresses.add(addr.name())              else:                     print "Cancel Add: " + `dialog.getAddress()`        def __RemoveAddress_Clicked(self,event):              index = self.addresses.selectedIndex              key = self.addresses.selectedItem              del self.dict[key]              self.addresses.remove(key)                     # Get the index of the item before this one.                     # Unless the index of the item we removed is 0.              if index-1 < 0: index = 0              else: index = index -1                     # Select the index of the adjacent item.              if len(self.dict.keys()) > index:                     print self.dict.keys()[index]                     self.addresses.select(index)                          # set the form to the index                          # that we selected in the list                     address = self.dict[self.dict.keys()[index]]                     self.form.set(address)

The __init__toolbar method creates a toolbar and adds it to the north region of the main frame. Then it instantiates two buttons and adds them to the toolbar. The Add and Remove buttons are registered with the event handler methods. __addAddress_Clicked and __RemoveAddress_Clicked.

      def __init__toolbar(self):                    # Create the toolbar panel and                    # add it to the NORTH border of the container.             toolbar = Panel()             self.container.add(toolbar, BorderLayout.NORTH)                    # Create two buttons             addAddress = Button("Add")             removeAddress = Button ("Remove")                    # Add the buttons to the toolbar             toolbar.add(addAddress)             toolbar.add(removeAddress)                    # Registers the buttons event handler             addAddress.actionPerformed=self.__addAddress_Clicked             removeAddress.actionPerformed=self.__RemoveAddress_Clicked

Adding and Removing Addresses

The __addAddress_Clicked event handler is where the AddAddressDialog class is used. It creates an instance of AddAddressDialog called dialog. After the user opens the dialog to enter an address (or not), the method checks to see if the OK button was pressed by calling dialog's GetState() method. If the OK state is set, the address is added to the address dictionary (dict) and the address list (addresses).

def __addAddress_Clicked(self,event):        dialog = AddAddressDialog(self)        if dialog.GetState() == AddAddressDialog.OK:               addr = dialog.getAddress()               self.dict[addr.name()] = addr               self.addresses.add(addr.name())        else:               print "Cancel Add: " + `dialog.getAddress()`

Clicking the Remove button calls __RemoveAddress_Clicked(). This method is a little long, so we'll go through it step by step.

First, we get the index of the selected item in the address list (addresses). Then we get the value of the string at that index, which is the name of the person in the address entry.

index = self.addresses.selectedIndex key = self.addresses.selectedItem

Now we can remove the person from the address dictionary (dict) and from the address list.

del self.dict[key] self.addresses.remove(key)

Next we need to select another person in the list and reinitialize the address form so that both are synchronized. This means that we have to get the index of the item before this one, unless the index of the item we removed is 0.

if index-1 < 0: index = 0 else: index = index -1

Then we select the index of the adjacent item in the list and the form. We also do some sanity checking to see if the dictionary has enough items in it that is, that the index value is no greater than the number of items in the dictionary.

if len(self.dict.keys()) > index:

Select the address in the list.

self.addresses.select(index)

Get the address instance from the dictionary; then select the address in the form.

address = self.dict[self.dict.keys()[index]] self.form.set(address)

The following line of code does a lot and may be hard to understand:

address = self.dict[self.dict.keys()[index]]

Essentially, it's equivalent to writing these three lines of code:

listAddressNames = self.dict.keys() currentAddressKey = listAddressNames[index] address = self.dict[currentAddressKey]

Here's what it means step by step:

Get the dictionary keys, which act as a list of names from address instances.

listAddressNames = self.dict.keys()

Get the name corresponding to the value of the index, which is the address closest to the address just removed.

currentAddressKey = listAddressNames[index]

Use that name as a key into the dictionary to get the address instances corresponding to the address that was next to the address just removed.

address = self.dict[currentAddressKey]

Set the AddressForm instance, form, to the new address.

self.form.set(address)

Now that you have a good understanding of how to add and remove addresses, run AddressMain.py, and try these exercises:

  • Add and remove a couple of address entries. Get the feel for how the program works because you'll need it for the rest of the exercises.

  • Add a company field and a title field to the Address class. You'll need an extra text field in the AddressForm class for this.

  • Add a flag argument called isDialog in the constructor of AddressForm that allows you to forgo adding the Apply button to the form. This flag will be used when the form acts as a dialog. Also, change the background color of the dialog to match the background color of the address form.

  • Add two buttons to the toolbar of MainAddressForm. The Copy button (copyAddress) should copy the current selected address. The Insert button (insertAddress) should insert the address into the list.

Menus

In Java there are two types of menus: menubar and popup. The following section shows how to add a menubar to a frame.

JMenu Bar

Import the JMenuBar class from java.swing.

>>> from javax.swing import JJMenuBar

Create an instance of it.

>>> bar = JJMenuBar()

Import and create an instance of JFrame.

>>> from javax.swing import JFrame >>> frame = JFrame()

Set the JMenuBar property of the frame to the JMenuBar instance.

>>> frame.JJMenuBar = bar >>> frame.visible=1

Now that we have a menubar added in the frame, we need to add a menu to it. Create a menu (JMenu), and add it to the menubar.

>>> from javax.swing import JMenu >>> fruitMenu = JMenu("Fruit") >>> bar.add(fruitMenu) >>> frame.pack()

Add menu items to the menu.

>>> from javax.swing import JMenuItem >>> fruitMenu.add(MenuItem("Apple")) >>> fruitMenu.add(JMenuItem("Pear")) >>> fruitMenu.add(JMenuItem("Peaches"))

Menus Event Handling

The menu just created should look like the one in Figure 14-5 (if not, try manually resizing the frame). It's pretty, but it doesn't do anything. We need to handle events from the menu or menu items.

Figure 14-5. The Fruit Menu

graphics/14fig05.gif

Define an event handler.

>>> def actionPerformed(event): ...     print event.source ...     print event.paramString() ...     print event.actionCommand ...

Get the Apple menu from the Fruit menu. It's the first one, so it's at index 0.

>>> appleMenu = fruitMenu.getMenuComponent(0)

Set the actionPerformed event property to actionPerformed.

>>> appleMenu.actionPerformed = actionPerformed

Try selecting the Apple menu item in the Fruit menu again. You should get the following output from the event handler we just defined:

javax.swing.JMenuItem[...] ACTION_PERFORMED,cmd=Apple Apple

Notice that actionCommand is a string set to Apple. Now try selecting Pear and Peaches. Nothing happens because we didn't register event handlers for them.

Set the event handlers for all menu items.

>>> for menuItem in fruitMenu.menuComponents: ...     menuItem.actionPerformed = actionPerformed ...

Select Pear and Peaches again.

In this next example we'll add another menu to JMenuBar called Veggies. Then we'll add menu items to it and handle events from specific items instead of from the menu as a whole.

Create the Veggies menu.

>>> veggiesMenu = JMenu("Veggies")

Create three menu items.

>>> spinach = JMenuItem("Spinach") >>> carrots = JMenuItem("Carrots") >>> peas = JMenuItem("Peas")

Define event handlers for each menu item and register them.

>>> def spinachEvent(event): ...   print "You Picked Spinach" >>> spinach.actionPerformed = spinachEvent >>> def carrotEvent(event): ...   print "What's up DOC?" >>> carrots.actionPerformed = carrotEvent >>> def peaEvent(event): ...   print "Yuck!" >>> peas.actionPerformed = peaEvent

Add the menu items to the Veggies menu.

>>> veggiesMenu.add(spinach) >>> veggiesMenu.add(carrots) >>> veggiesMenu.add(peas)

Add the Veggies menu to the menubar.

>>> bar.add(veggiesMenu) >>> frame.pack()

Select each menu item; notice that its handler is called.

You Picked Spinach What's up DOC? Yuck!

Now we'll add cooking options to the Veggies menu so that, once the user selects an option (Fry, Broil, Boil, Roast), he can select the Cook menu item to prepare the veggies accordingly. The exercise that follows will use separators and checkboxes.

Add a separator to the end of the Veggies menu using the addSeparator() method.

>>> veggiesMenu.addSeparator()

Add a submenu to Veggies by creating cookMenu and adding it to veggiesMenu.

>>> cookMenu = JMenu("Cooking") >>> veggiesMenu.add(cookMenu)

Add a menu item to cookMenu called Cook.

>>> cookMenuItem = JMenuItem("Cook") >>> cookMenu.add(cookMenuItem) >>> cookMenu.insertSeparator(cookMenu.itemCount)

Now we'll demonstrate creating and using checkboxes.

Import JCheckboxMenuItem from javax.swing.

>>> from javax.swing import JCheckBoxMenuItem

Create a list of item names, and create a dictionary to hold mappings from the items to the names.

>>> cookItems = ('Fry', 'Broil', 'Boil', 'Roast') >>> dict ={}

Add each item in the above list to the Cook menu (cookMenu). In addition, add the item to the dictionary so it can be found easily.

>>> for item in cookItems: ...    cb = JCheckBoxMenuItem(item) ...    dict[item]=cb ...    cookMenu.add(cb)

Now let's set up the event handler, cookOptionEvent, to handle these checkbox menu items. For clarity, let's go through it step by step. The currently selected menu item's caption is equal to event.item. If it's Broil, event.item is Broil.

...    if event.item in cookItems:

Iterate through each item in cookItems, which you'll recall holds all of the possible cooking options.

...    for item in cookItems:

If the item in cookItems isn't the current item, make sure its checkbox isn't selected.

...    if (item != event.item): ...           cb = dict[item] ...           cb.state = 0

The statement cb = dict[item] looks up the JCheckboxMenuItem instance in the dictionary that contains mappings of strings to menu items. The statement cb.state = 0 sets the state of the JCheckBoxMenuItem instance (cb) to false, which removes its check.

Now we need to set up the event handler for cookMenuItem.

>>> def cookEvent(event): ...   for item in cookItems: ...          if dict[item].state: ...                print "I will " + item + " your veggies" >>> cookMenuItem.actionPerformed = cookEvent

At this point, the menu in your frame should look like the one in Figure 14-6. If it doesn't, try resizing it.

Figure 14-6. The Veggies Menu with the Cooking Submenu

graphics/14fig06.gif

Now try selecting Fry and then Cook. You should get the following output:

I will Fry your veggies

Then try Broil and Boil.

I will Broil your veggies I will Boil your veggies

Here are some more things to try:

  • Convert the previous interactive session into a class that contains all of the functionality derived from Frame. Remember how we converted an earlier interactive session that worked with Frame into a class.

  • Change the code to use JRadioButtonMenuItem. Remember from Chapter 13 that JMenuItem and JRadioButtonMenuItem inherit functionality from AbstractButton. Hint: Add all of the JRadioButtonMenuItems to ButtonGroup.

  • Add icons to the various menu items.

  • Set the mnemonics of the JFC menu items.

JPopupMenus

Popup menus are context-sensitive. Typically, they appear when you right-click an icon or component. The following example demonstrates popup menus. Using JFC creates a frame with two buttons: Switch and Pop.

Import JPopupMenu and all of its support classes from javax.swing.

>>> from javax.swing import JPopupMenu >>> from javax.swing import JFrame, JButton, JPanel, JLabel

Create instances of the popup menu.

>>> popup = JPopupMenu("JFC Popup")

Put some menu items in it.

>>> fruits = ("Apple", "Oranges", "GrapeFruit", "Grapes") >>> for fruit in fruits: ...   popup.add(fruit)

Create a frame, a pane, and a label. The label holds a string, which shows the state of the current popup menu JFC.

>>> frame = JFrame(size=(200,200)) >>> pane = JPanel() >>> label = JLabel("JFC Popup Menu") >>> pop = JButton("Pop")

Add the popup menu to the pane.

>>> pane.add(popup)

Add the label and pop components to the pane. Add the pane to the frame.

>>> pane.add(pop) >>> pane.add(label) >>> frame.contentPane.add(pane)

Create an event handler for pop that pops up the current menu at the x, y location 20,10 in the pane.

>>> def popEvent(event): ...    global popup, pane ...    popup.show(pane, 20, 10)

Register the event handlers with the corresponding buttons.

>>> pop.actionPerformed = popEvent

Pack the frame, and show it off.

>>> frame.pack() >>> frame.visible=1 Putting Things Together with Menubar Menus and Popup Menus

Adding Menus to the Address Application

Let's add a menubar menu and a popup menu to our address application. Essentially, we'll set the menubar property of AddressMain to a JMenuBar instance. Then we'll add the Address menu to the menubar and give it two items, Add and Remove. The actionPerformed event properties of these items are set to the same handler as for the Add and Remove buttons.

All of this is done from the constructor of AddressMain. It's rather long, so we'll break out the functionality of adding the menu to a method called __init__menu(). Here's the code (from class AddressMain in Two\AddressMain.py):

def __init__(self)      ... self.__init__menu()      ... def __init__menu(self):                     # Create the JMenuBar and                     # add it to the addressMenu.             jmb = JMenuBar()             self.JMenuBar = JMenuBar             addressMenu = JMenu("Address")                     # Create two menu items.             addAddress = JMenuItem("Add")             removeAddress = JMenuItem("Remove")                     # Add the menu items to the menu.                     # then add the menu to the menu bar             addressMenu.add(addAddress)             addressMenu.add(removeAddress)             JMenuBar.add(addressMenu)                     # Register the menu items event handler             addAddress.actionPerformed = self.__addAddress_Clicked             removeAddress.actionPerformed= self.__RemoveAddress_Clicked

We also want to add a popup menu to our application that appears when the user clicks the address list. The __init__popup method is similar to __init__menu, the only difference being that the mousePressed event is handled for addresses. Notice that, if event.metaDown (in the event handler for mousePressed) is true (1), the right mouse button was clicked.

def __init__(self)       ... self.popup = NONE    #to hold the popup menu self.__init__popup() self.addresses.mousePressed = self.__popupEvent ... def __popupEvent(self, event):              if event.metaDown == 1:                     self.popup.show(self.addresses, event.x, event.y) def __init__popup(self):              self.popup = JPopupMenu("Address")              self.addresses.add(self.popup)                     # Create two menu items.              addAddress = JMenuItem("Add")              removeAddress = JMenuItem("Remove")                     # Add the menu items to the menu.              self.popup.add(addAddress)              self.popup.add(removeAddress)                     # Register the menu item's event handler              addAddress.actionPerformed = self.__addAddress_Clicked              removeAddress.actionPerformed= self.__RemoveAddress_Clicked

As an exercise, add and then remove an address entry with the Add and Remove menu items on JMenuBar. Do it again with JPopupMenu. Also create a duplicate menu item for both the menubar menu and the popup menu that allows an address to be duplicated, that is, copied and pasted into the list.

Layout Managers

If you've worked with Visual Basic or any other tools that support GUI code generation, you may be accustomed to laying out components on a form and specifying their size and position in pixels. (A pixel is 1/20 inch.) The problem is, of course, that you may develop your application on a 25-inch monitor, and then someone will try to use it on a 12-inch monitor, leaving half the GUI off the screen. This problem becomes even more pronounced if you have no control over the type of display your GUI will be used on. For these reasons, Java needs a flexible way to display user interfaces on a variety of platforms.

Layout managers provide a way to lay out components in a container with great flexibility, but with flexibility comes greater complexity, and sometimes there's just no substitute for absolute positioning. Even so, in most cases you should use layout managers, and if you do use absolute positioning, make sure it's in a container whose container is a layout manager.

The positioning of components on the screen is decided by the order in which they're added to the container, the constraints passed to the container's add() method, and the layout manager properties the container is using.

Each type of container has its own default layout manager. For example, java.awt.Frame and javax.swing.JFrame use BorderLayout whereas Panel and JPanel use FlowLayout. However, you can change the default so that a frame can use FlowLayout and a panel can use BorderLayout.

In one GUI, you'll usually have many layout managers. Nested containers with different layout managers will give you the desired result, as we saw with the address book application. There AddressMain used BorderLayout; ToolBar, which contains the Add and Remove buttons, used FlowLayout; and editPane used GridLayout (to arrange the labels and text fields used to edit the addresses).

The key to understanding layout managers is using them, so we'll be doing plenty of interactive examples and exercises. I've also provided a tutor application (Layout3.py) that works with FlowLayout, BorderLayout, GridLayout, and GridBagLayout. Feel free to fire up Layout3.py and experiment with layout managers at any time.

Strategy Design Pattern

The layout manager approach uses the Strategy design pattern developed by Gamma and his colleagues (Design Patterns, 1995). This is another reminder to read Gamma, but first read Object-Oriented Analysis and Design with Applications (Booch, 1994).

FlowLayout

The FlowLayout class is the simplest of the layout managers. (Actually, it's the second simplest; I'll explain later.) As components are added one at a time to the container, they're positioned from left to right and from top to bottom. When one row fills up, the components begin filling up the next row. You can set the alignment of each row so that the components can be centered or left- or right-aligned.

Here's an interactive FlowLayout example. Import the FlowLayout class and some container and component support classes.

>>> from java.awt import FlowLayout >>> from javax.swing import JFrame, JButton, JTextField, JLabel

Create an instance of Frame, and set its layout to FlowLayout, that is, an instance of the FlowLayout class. Remember that the frame is a container.

>>> frame = JFrame("FlowLayout", size = (200, 200), visible=1) >>> frame.contentPane.layout = FlowLayout()

Add a label to the frame, and validate the frame. (validate() forces the frame to redraw itself, which makes the label just added visible.)

>>> frame.contentPane.add(JLabel("Name")) javax.swing.JLabel[...text=Name,...verticalTextPosition=CENTER] >>> frame.validate()

Add a text field to the frame, and validate the frame.

>>> frame.contentPane.add(JTextField(20)) >>> frame.validate()

Remember, if the component doesn't fit on the current row, it's added to the next row. If we eyeball the components, they look like they almost fit. To be sure, we have to check their width.

>>> frame.contentPane.components[0].width + frame.contentPane.components[1].width 253

The first component's width (frame.contentPane.components[0]) plus the second's width (frame.contentPane.components[1]) is 253. Take a look at the call to the constructor to see what the frame width is.

>>> frame.width 200

Clearly both components can't fit on the same row. (We'll make them fit later.)

Let's make this example more interesting and add eight more components to the container (frame). Create a tuple of strings and iterate through it, creating buttons with the strings and adding them to the frame.

>>> blist = ("one", "two", "three", "four", "five", "six", "seven", "eight") >>> for label in blist: ...     frame.contentPane.add(Button(label)) ... >>> frame.validate()

The frame should now look like Figure 14-7. Notice that the components in each row are centered, which is the default row alignment with FlowLayout (FlowLayout.CENTER). We'll change the alignment to the left and then to the right. Then we'll change the vertical and horizontal spacing between the components.

Figure 14-7. Center Alignment of Components (FlowLayout's Default)

graphics/14fig07.gif

Set the layout of the frame to the left.

>>> frame.contentPane.layout.alignment = FlowLayout.LEFT >>> frame.contentPane.doLayout()

It should look like Figure 14-8. Notice that the components are aligned along the left border. If we change the layout to right alignment, the frame should look like Figure 14-9.

Figure 14-8. Left Alignment of Components with FlowLayout

graphics/14fig08.gif

Figure 14-9. Right Alignment of Components with FlowLayout

graphics/14fig09.gif

>>> frame.contentPane.layout.alignment = FlowLayout.RIGHT >>> frame.contentPane.doLayout()

Recall that the first two components didn't fit on the first row. What would happen if we made the frame bigger than the 253 pixels that make up the width of the label plus the width of the text field? Change the width of the frame to 275 pixels.

>>> frame.size = 275, 200 >>> frame.contentPane.doLayout() >>> frame.validate()

The frame should now look like Figure 14-10.

Figure 14-10. Effect of Resizing on Component Layout

graphics/14fig10.gif

Let's set the spacing between the components with the vgap property, which controls vertical spacing, and the hgap property, which controls horizontal spacing. Set vgap and hgap to 0 so that your frame will look like the one in Figure 14-11.

Figure 14-11. Vertical and Horizontal Gap Set to 0

graphics/14fig11.gif

>>> frame.contentPane.layout.vgap=0 >>> frame.contentPane.layout.hgap=0 >>> frame.contentPane.doLayout()

Now set vgap and hgap to 10 pixels.

>>> frame.contentPane.layout.vgap=10 >>> frame.contentPane.layout.hgap=10 >>> frame.contentPane.doLayout()

Finally, we want to pack the frame with FlowLayout if the container is an instance of JFrame, JDialog, or Window. FlowLayout lays out the components' preferredSize property (if you try to resize a component yourself, FlowLayout will change it back) and adjusts the width of the frame to the cumulative width of all of its components. If you pack the frame (frame.pack()), FlowLayout will try to fit all of the components on the top row, as in Figure 14-12.

Figure 14-12. Result of Packing the Frame

graphics/14fig12.gif

Surprise

All of you who weren't following along (and you know who you are), fire up layout4.py, which lets you adjust properties for all of the layout managers that we discuss in this chapter. It looks like Figure 14-13. Notice, in the frame at the left, that you can adjust the properties of the current layout and those of the components (or those of the constraint objects associated with a component). Play around with it:

  1. Set the layout manager to FlowLayout.

  2. Align the components on the left by selecting the LEFT item in the alignment list and then clicking the Apply button.

  3. Align the components on the right.

  4. Change the vertical gap to 0 by editing the vgap text field to 0 and then hitting the Apply button.

  5. Change the horizontal gap to 0.

  6. Change the vertical and horizontal gaps to 20.

  7. Size the frame with the mouse so that it's half its original size.

  8. Hit the Resize button on the control frame.

  9. Hit the Pack button to resize the frame.

  10. Try all of the available layout managers.

Figure 14-13. Layout4.py after Playing with It

graphics/14fig13.gif

There are four versions of the layout program, Layout.py, Layout2.py, Layout3.py, and Layout4.py, which increase in complexity. To understand what's going on, examine Layout.py without looking at Layout4.py, which, since it supports all of the layout managers, is pretty long.

BoxLayout

You may remember that I referred to FlowLayout as the second simplest layout manager. It used to be the easiest, but that was before BoxLayout, which was added as part of JFC/Swing. BoxLayout can be used with AWT components, but it works better with the JFC variety.

BoxLayout has only one parameter, AXIS, and there are only two choices: X and Y. Here's a short interactive example.

Import BoxLayout, JFrame, and JButton from javax.swing.

>>> from javax.swing import BoxLayout, JFrame, JButton

Create a frame, and add some buttons to it.

>>> frame = JFrame("BoxLayout Example", size = (300,200), visible=1) >>> list = ["one","two","three", "four"] >>> for button in list: ...     frame.contentPane.add(JButton(button)) ...

Change the layout of the frame to BoxLayout, which uses X_AXIS. (invalidate() tells the frame that something has changed and that the window needs to be redrawn; validate() tells the frame that it's time to fix any problems. Calling invalidate() and then validate() forces the frame to call doLayout.)

>>> frame.contentPane.layout=BoxLayout(frame.contentPane, BoxLayout.X_AXIS) >>> frame.invalidate() >>> frame.validate()

Look at Figure 14-14, and notice that BoxLayout has arranged things from left to right along the x axis. Now arrange the layout along the y axis (Figure 14-15).

Figure 14-14. Components Laid Out Left to Right on the x Axis

graphics/14fig14.gif

Figure 14-15. Components Laid Out Vertically on the y Axis

graphics/14fig15.gif

>>> frame.contentPane.layout=BoxLayout(frame.contentPane, BoxLayout.Y_AXIS) >>> frame.invalidate() >>> frame.validate()

Like FlowLayout, BoxLayout uses the preferredSize property. It simply spaces the components evenly against a given axis. Try this exercise using BoxLayout from Layout4.py. Explain what happens in each step.

  1. Set the axis to X_AXIS. (Remember to hit the Apply button on the control frame.)

  2. Reduce the size of the frame to half and then half again along the x axis.

  3. Hit the Resize button on the control frame.

  4. Hit the Pack button on the control frame. (This is the same as calling frame.pack().)

  5. Set the axis to Y_AXIS.

  6. Hit the Resize button again to set the frame to the default starting size.

  7. Reduce the size of the layout frame by half and then by half again along the y axis.

  8. Hit the Pack button again.

GridLayout

The GridLayout manager lays out components in a grid. We used it in the address application for editPane. Like FlowLayout, GridLayout has the vgap and hgap properties that set the vertical and horizontal spacing between components.

GridLayout also has properties that specify the rows and columns of the grid. The row property is dominant. The only component property that GridLayout pays any attention to is minimumSize. If you pack the container (frame.pack()), it uses minimumSize to create the smallest components that are all the same size. It ignores preferredSize and stretches the width and height of the components so that they all have the same area and fill the container (if possible). Here's an example.

Import the classes needed.

>>> from java.awt import GridLayout >>> from javax.swing import JFrame, JButton

Create a frame and add some components to it.

>>> frame = JFrame("GridLayout Example", size=(300,300), visible=1) >>> blist = ("One", "Two", "Three", "Four", "Five", "Six", "Seven", "Eight") >>> for label in blist: ...     frame.contentPane.add(JButton(label)) ...

Create a grid layout that lays out components in a 2-by-4 grid.

>>> frame.contentPane.layout = GridLayout(2,4) >>> frame.contentPane.doLayout()

Your frame should look like Figure 14-16. Notice that neither the width nor the height is observed from preferredSize. With the mouse, drag the right corner of the frame so that it's twice its original size, which doubles the width and height of the components as well. Now return the frame to its original size.

Figure 14-16. A 2-by-4 Component Layout with GridLayout

graphics/14fig16.gif

Set the row property of the GridLayout instance to 3.

>>> frame.contentPane.layout.rows=3

Your frame should now look like Figure 14-17. Although we didn't change the columns to 3, only three columns show up. That's because GridLayout adjusts the number of columns to fit the desired number of rows. Also, the grid is 3 by 3 and, since we only have eight components, the last cell is left blank.

Figure 14-17. A 3-by-3 Layout (with the Last Cell Blank)

graphics/14fig17.gif

Now set the rows to four. Then print out the row and column properties (layout.rows and layout.columns).

>>> frame.contentPane.layout.rows=4 >>> frame.contentPane.doLayout() >>> layout=frame.contentPane.layout >>> print layout.rows, layout.columns 4 4

Your frame should look like Figure 14-18. Again, the number of columns has changed to accommodate the number of rows; however, the column property is still set to 4 because, as I said earlier, the row property is dominant.

Figure 14-18. A 4-Row Layout

graphics/14fig18.gif

Set vgap and hgap of frame.layout to 3. Look at the frame, and then set these properties to 7.

>>> layout.vgap, layout.hgap = 3,3 >>> frame.contentPane.doLayout()

Look at the frame.

>>> layout.vgap, layout.hgap = 7,7 >>> frame.invalidate();frame.validate()

It should look like Figure 14-19. Notice that the spacing is between the components and not between the components and the edges. (If you want to set the area between the components and the edges, you need to set the insets of the frame.)

Figure 14-19. Horizontal and Vertical Gap Set to 3

graphics/14fig19.gif

Set the layout back to the way it was.

>>> layout.vgap, layout.hgap = 0,0 >>> frame.contentPane.layout = GridLayout(2,4)

As we discussed, GridLayout uses minimumSize when you pack the frame. It lays out all components with the same size by checking the minimum size for each one and then setting all of them to the largest size it finds. Pack the frame. It should look like Figure 14-20.

Figure 14-20. Packed Frame

graphics/14fig20.gif

>>> frame.pack()

Now let's go through the components and find the maximum minimumSize. First we'll create two lists to collect the widths and heights from the components.

>>> widths, heights = [], []

Iterate through the components in the frame, and collect the widths and heights in the widths and heights lists.

>>> for component in frame.contentPane.components ...     widths.append(component.minimumSize.width) ...     heights.append(component.minimumSize.height)

Print the maximum minimumSize width.

>>> print "Minimum width " + `max(widths)` Minimum width 48

Print the maximum minimumSize height.

>>> print "Minimum height " + `max(heights)` Minimum height 23

Print the actual size of the components. (Remember that with GridLayout all components are the same size.) Notice that the width and height match the maximum minimumSize.

>>> print "Size of all of the components " + `frame.components[0].size` Size of all of the components java.awt.Dimension[width=48,height=23]

Try this exercise with layout4.py:

  1. Select GridLayout from the layouts list in the control pane.

  2. Hit the Pack button. You should have a frame that looks like Figure 14-21.

    Figure 14-21. Packed GridLayout Frame

    graphics/14fig21.gif

  3. Change hgap and vgap to 5 pixels. Then hit the Apply button. The frame should look like Figure 14-22.

    Figure 14-22. Frame with Horizontal and Vertical Gaps of 5

    graphics/14fig22.gif

  4. Hit the Pack button again.

  5. Change the number of columns to 100. What happens?

  6. Set the number of columns to 0. What happens?

  7. Set the number of columns back to 5. What happens?

  8. Set the rows from 4 to 6. Then hit Pack. Notice that the last two rows are empty (Figure 14-23).

    Figure 14-23. Six-Row Packed Frame with Last Two Rows Empty

    graphics/14fig23.gif

  9. Increment the rows by 1 until you reach 10. Be sure to hit Apply each time, and then repack the frame with Pack.

BorderLayout

BorderLayout works well with top-level containers. It can have at most five components, so the components added to it are usually containers themselves that is, panels.

The five regions of BorderLayout are north, south, east, west, and center. What you put in them is up to you and is application-specific. North is used mainly for toolbars (panels with buttons or similar components); south, usually for status windows (panels with labels that show the status of an application). West can be used for tree views, lists, and the like; east, for toolboxes (panels with buttons and property text fields). Center is usually the main view of the application, perhaps where the text fields for editing an object are located.

The north and south regions span the full width of the parent container. Their height is the preferred height of the component. The east and west regions span the full height of the container minus the height of the north and/or south regions (if any). Their width is the preferred width of the component. The center region gets whatever is left over, usually the largest portion of the frame. Now for our BorderLayout example.

Set up a frame.

>>> from java.awt import BorderLayout >>> from javax.swing import JFrame, JButton >>> f = JFrame("BorderLayout", size = (300,300), visible=1) >>> f.contentPane.layout = BorderLayout()

Add a button to the north region, and force the frame to lay out the components (Figure 14-24).

Figure 14-24. BorderLayout Frame with a Button in the North Region

graphics/14fig24.gif

>>> f.contentPane.add(JButton("NORTH"),BorderLayout.NORTH) >>> f.contentPane.doLayout()

Notice that the button's width is stretched to the full width of the container. Add a button to the east region (Figure 14-25).

Figure 14-25. BorderLayout Frame with a Button Added in the East Region

graphics/14fig25.gif

>>> f.contentPane.add(JButton("EAST"), BorderLayout.EAST) >>> f.contentPane.doLayout()

Notice that its height is the full height of the container minus the height of the button added to the north region. Add a button to the west region (Figure 14-26).

Figure 14-26. BorderLayout Frame with a Button Added in the West Region

graphics/14fig26.gif

>>> f.contentPane.add(JButton("WEST"), BorderLayout.WEST) >>> f.contentPane.doLayout()

Add a button to the south region (Figure 14-27).

Figure 14-27. BorderLayout Frame with a Button Added in the South Region

graphics/14fig27.gif

>>> f.contentPane.add(JButton("SOUTH"), BorderLayout.SOUTH) >>> f.contentPane.doLayout()

Notice that the height of the west and south buttons decreases by the height of the south button. Add a button to the center region (Figure 14-28).

Figure 14-28. BorderLayout Frame with a Button Added in the Center Region

graphics/14fig28.gif

>>> f.contentPane.add(JButton("CENTER"), BorderLayout.CENTER) >>> f.contentPane.doLayout()

Notice that it gets all the leftovers.

Let's verify that the width of the east and west regions is equal to the preferred size of the components added to them. Set up some constants to extract the components from the container.

>>> NORTH,EAST,WEST,south=0,1,2,3

Get the preferred size and the actual size of the component added to the east region.

>>> f.contentPane.components[EAST].preferredSize java.awt.Dimension[width=65,height=27] >>> f.contentPane.components[EAST].size java.awt.Dimension[width=65,height=219]

Get the preferred size and the actual size of the component added to the west region.

>>> f.contentPane.components[WEST].preferredSize java.awt.Dimension[width=69,height=27] >>> f.contentPane.components[WEST].size java.awt.Dimension[width=69,height=219]

Notice that the preferred width and the actual width of the button added to the east region are both 65. Also notice that the preferred width and the actual width of the button added to the west region are both 69.

As an exercise, use the same technique just demonstrated to prove that the north and south regions stretch the width of the component but use the preferred height. Then, using Layout4.py, try this:

  1. Change the layout manager in the layouts list to BorderLayout.

  2. Select the north component in the components list.

  3. Select height in the growth direction list. The Grow button sets the preferred height of the button that was added to the north region.

  4. Hit the Grow button several times to make the north region expand.

  5. Make the frame half the width and height it was before. Do this in small increments with the mouse. Notice that the height of the north component doesn't change.

  6. Hit the Resize button to set the size back to normal.

  7. Make the frame twice as big (use the mouse to drag the corner of the frame). Notice that the north component doesn't change.

  8. Hit the Resize button.

  9. Select the west component in the components list.

  10. Select the width item in the growth direction list.

  11. Hit the Grow button several times. Notice that the width of the west button grows.

  12. Make the frame larger and smaller by dragging a corner.

  13. Change the horizontal and vertical gap properties to 5, and hit the Apply button.

If you've followed along the layout frame should look like Figure 14-29.

Figure 14-29. Layout4.py Result

graphics/14fig29.gif

GridBagLayout

GridBagLayout is the most complex and most feared layout manager. It's also the most hated and yet the most loved. Most important, it's the most powerful and, in my opinion, the most useful.

This layout manager is somewhat like GridLayout in that you lay out components in cells in a grid. However, its components can span multiple cells, and the cells aren't always the same size. Instead, you set the ratio of container space the cells will use. For example, one column of cells can span half the width of the container; the other two columns can span a quarter of the width.

Unlike in GridLayout, in GridBagLayout components don't have to span the width and height of the cells, so their preferred size can be maintained. You can specify that the component width and/or height span the cell width or height. You can also specify that the component be anchored in the center, north, south, east, or west region of a cell.

GridBagConstraints

To create a GridBagLayout component, you need to master the GridBagConstraints class, which specifies all constraints for a given cell. The instances of this class are associated with components added to the container that has GridBagLayout as its layout manager. GridBagConstraints determines the placement, cell dimensions, alignment, ratio, and so forth. The association among the GridBagLayout instance, GridBagConstraints, and each component defines the overall layout of components in the container.

Here are GridBagConstraint's properties:

  • gridx, gridy defines the cell location of the constraint

  • gridheight, gridwidth defines the cell width and cell height

  • weightx, weighty defines the ratio between the cells

  • anchor defines the justification of a component within the cell (CENTER, NORTH, WEST, etc.)

  • fill holds the fill type, i.e., whether or not the component spans the width or height of the cell (possible values are BOTH, VERTICAL, NONE, HORIZONTAL)

  • insets sets the insets

  • ipadx, ipady sets additional padding needed for a cell (width or height in addition to the ratio defined by weightx or weighty for a particular cell)

The number of rows and columns in the container is a function of the largest gridx and gridy specified in a constraint if no rows or columns are blank. Say you specify a particular cell location as gridx=5, gridy=5. If there are no cells in the fourth row or column but there are cells in the third row or column, cell 5,5 actually equates to cell 4,4.

GridBagLayout lays out cells left to right and then top to bottom, so gridx=0 and gridy=0, respectively, indicate the first row and first column at the top left corner of the component.

gridheight and gridwidth specify the cell width. If we have a constraint that has a gridx and a gridy of 1 and a gridheight and a gridwidth of 2, that region spans multiple cells (1,1 to 3,3).

weightx and weighty specify the ratio given to a certain cell. If the weightx is 0, there's another cell in the same column setting the ratio of width space for it. If weighty is 0, there's another cell in that row setting the ratio of height space for the column. If three cells in the row have weightx of 33, each cell gets a horizontal width equal to 33 percent of the width of the container, in other words, each cell width = 33/(33+33+33) = 33%.

If three cells in the same column have a weight of 33, each cell gets a vertical height equal to 33 percent of the height of the container, in other words, each cell height = 33/(33+33+33) = 33%.

fill holds the fill type. The possible values in this situation are BOTH, VERTICAL, NONE, and HORIZONTAL. BOTH specifies that the component span both the vertical and horizontal space of the cell (this is similar to the GridLayout cells). VERTICAL specifies that the component stretch across the vertical area of the cell. HORIZONTAL specifies that the component stretch across the horizontal area of the cell. NONE specifies that the component not stretch in either direction, so that it remains its preferred size in the cell.

A GridBagLayout Example

Theory is good, but we need to put some code behind these abstract concepts. The following example, from GridBag.py, takes us step by step through the layout of components with GridBagLayout. At first we'll use only buttons; then we'll graduate to other components such as text fields and labels. To start, import the classes needed.

>>> from javax.swing import JButton, JTextField, JFrame, JButton, JPanel, JLabel >>> from java.awt import GridBagLayout, GridBagConstraints

Constraints

Define a helper function for creating GridBagConstraints. If you're following along in the interactive interpreter, enter from GridBag import makeConstraint.

def makeConstraint() gridx=0, gridy=0,      # Holds the x, y cell location in the grid gridheight=1, gridwidth=1, # Holds the cell width and cell height weightx=0,weighty=0,   # Determines ratio relative to another. anchor= GridBagConstraints.CENTER,  # Holds the justification fill=GridBagConstraints.NONE,     # Holds the fill type BOTH, NONE,... insets=Insets(0,0,0,0), ipadx=0, ipady=0        ): return GridBagConstraints(gridx,gridy,gridwidth,gridheight,weightx,weighty,anchor, fill,insets, ipadx, ipady)

Import the helper method to create GridBagConstraints without specifying all of its parameters.

>>> from GridBag import makeConstraint

Create a GridBagLayout instance. Then create a panel that has its layout manager property set to the GridBagLayout instance.

>>> gbag = GridBagLayout() >>> contentPane = JPanel(layout=gbag) >>> frame = JFrame(contentPane=contentPane)

Create a 2-row by 2-column layout, first defining the top left region.

>>> top_left = makeConstraint(0,0,1,1, weightx=20, weighty=50, fill=GridBagConstraints.BOTH)

Notice that gridx and gridy are both set to 0, so this component will be in the top left, or the first row, first column of the grid.

Define the top right region.

>>> top_right = makeConstraint(1,0,1,1, weightx=80, fill=GridBagConstraints.BOTH)

Notice that gridx is set to 1 and gridy is set to 0, which means that this region equates to the cell located at the first row, second column of the grid.

Define the bottom left region.

>>> bottom_left = makeConstraint(0,1,1,1, weighty=50, fill=GridBagConstraints.BOTH)

gridx is set to 0, so this is the first column. gridy is set to 1, so this is the second row.

>>> bottom_right = makeConstraint(1,1,1,1,fill=GridBagConstraints.BOTH)

Notice that weightx of cell top_left (0,0) is set to 20 and weightx of cell top_right (1,0) is set to 80. This means that the top left cell will get 20/(20+80), or 20 percent, of the width of the container and that the top right cell will get 80/(20+80), or 80 percent, of the width. Also notice that both cells, top_left and top_right, have weighty of 50, so each gets 50/(50+50), or 50 percent, of the height of the container, The fill is set so that the components fill the full height and width of the cell.

Components

Now that we've set the dimensions of the cells, let's create some components and add them to the containers.

Define some sample components buttons whose labels correspond to where they'll be added in the grid layout.

>>> tl = JButton("Top Left") >>> tr = JButton("Top Right") >>> bl = JButton("Bottom Left") >>> br = JButton("Bottom Right")

Add the components to the contentPane using their corresponding GridBag Constraints.

>>> contentPane.add(tl, top_left) >>> contentPane.add(tr, top_right) >>> contentPane.add(bl, bottom_left) >>> contentPane.add(br, bottom_right)

Size the frame and show it.

>>> frame.size=(400,200) >>> frame.validate() >>> frame.visible=1

If you've been following along, your frame should look like the one in Figure 14-30. Now change the size of the container to 600,100.

Figure 14-30. GridBag.py Example

graphics/14fig30.gif

>>> frame.size=(600,100) >>> frame.validate()

Notice that the 80:20 ratio between the top left and top right widths remains consistent. So does the 50:50 ratio between the top left and bottom left heights. Your frame should look like the one in Figure 14-31.

Figure 14-31. GridBag.py Example with a Container Size of 600,100

graphics/14fig31.gif

Let's change the size to 500,500 so you can get the true feel of things.

>>> frame.size=(500,500) >>> frame.invalidate() >>> frame.validate()

Try packing the frame. What do you think will happen?

>>> frame.pack()

Adding and Removing Components

For this next set of exercises, we're going to remove and add components after each step. We'll define a set of helper functions to do this. (Import them from GridBag.py.)

def addComponents(contentPane, comps):      for (component, constraint) in comps:           contentPane.add(component, constraint) def removeComponents(contentPane):           #Remove the components      for x in range (0,contentPane.componentCount):           contentPane.remove(0)

Import the addComponents() and removeComponents() functions.

>>> from GridBag import addComponents, removeComponents

Group component/constraint pairs into a list.

>>> comps = [(tl, top_left),(tr, top_right),(bl, bottom_left),(br,             bottom_right)]

We've essentially defined a list of tuples, each of which contains a component and a constraint. We can use them to iterate through the component/constraint pairs and add them to the panel.

for (component, constraint) in comps:      contentPane.add(component, constraint)

Set the ipadx and ipady properties for the top left and top right cells, and set the fill to NONE for both. Then resize the frame to 600,100, and add and remove the components from the container. (See Figure 14-32.)

Figure 14-32. Modified GridBag.py Example

graphics/14fig32.gif

>>> top_left.ipadx, top_left.ipady, top_left.fill = 10, 10, GridBagConstraints.NONE >>> top_right.ipadx, top_right.ipady, top_right.fill = 10, 10, GridBagConstraints.NONE >>> removeComponents(contentPane) >>> addComponents(contentPane, comps) >>> frame.size=(600,100); frame.validate()

Notice that the top left and top right components are centered in their cells. That's because the default anchor is CENTER. Remember that the anchor property moves the component to a particular anchor or justification within a cell.

Set the anchor to EAST for the top left and top right constraints.

>>> top_left.anchor = GridBagConstraints.EAST >>> top_right.anchor = GridBagConstraints.EAST >>> removeComponents();addComponents() >>> frame.invalidate() >>> frame.validate()

Notice that the top left and top right components are lined up along the east borders of their cells (Figure 14-33).

Figure 14-33. Modified GridBag.py Example with anchor Set to EAST

graphics/14fig33.gif

The Prototype

Now let's prototype a real application that subsets to the components the address form needs. First, though, we need to clean house.

>>> frame.visible=0 >>> frame.dispose() >>> frame = NONE >>> contentPane = NONE

Define some convenience variables for CENTER, WEST, and EAST.

>>> CENTER=GridBagConstraints.CENTER >>> WEST=GridBagConstraints.WEST >>> EAST=GridBagConstraints.EAST

Create a 2-column by 3-row grid that's almost identical to the one created before except for the addition of an extra row at the bottom called last (last goes underneath bottom).

>>>                             # x,y,w,h >>> top_left     = makeConstraint(0,0,1,1, weightx=20,weighty=33, anchor=EAST) >>> top_right    = makeConstraint(1,0,1,1, weightx=80,            anchor=WEST) >>> bottom_left  = makeConstraint(0,1,1,1,            weighty=33, anchor=EAST) >>> bottom_right = makeConstraint(1,1,1,1,                       anchor=WEST) >>> last         = makeConstraint(1,2,1,1,            weighty=33)

Define some sample components: labels, text fields, and a button.

>>> tl = JLabel("Name", JLabel.RIGHT)  #Top Left component >>> tr = JTextField(20)                #Top Right component >>> bl = JLabel("eMail", JLabel.RIGHT) #Bottom Left component >>> br = JTextField(30)                #Bottom Right component >>> okay = JButton("Okay")

Notice that we're using the same nomenclature as in the last example. Only the components are different.

Set up the frame with a panel that uses GridBagLayout.

>>> gbag = GridBagLayout() >>> contentPane = JPanel(layout=gbag) >>> frame = JFrame(contentPane=contentPane)

Add the sample components to contentPane. They have the same name so we can use the addComponents() method. Group the components with the constraints (the OK button and the last constraint are now on the end).

>>> comps = [(tl, top_left),(tr, top_right),(bl, bottom_left),(br, bottom_right),(okay,last)] >>> addComponents(contentPane, comps)

Size the frame, and make it visible. Your frame should look like the one in Figure 14-34.

Figure 14-34. Prototype of Name/Email Address Book Entry Form

graphics/14fig34.gif

>>> frame.size=(400,200) >>> frame.visible=1

Now let's try resizing the frame to see what happens to the components. Change the frame size to 500,500.

>>> frame.size=(500,500) >>> frame.invalidate() >>> frame.validate()

Change the size to 600,100.

>>> frame.size=(600,100) >>> frame.invalidate() >>> frame.validate()

Now pack the frame, and see how good it looks. (It should look like Figure 14-35.)

Figure 14-35. A Better-Looking Version of the Name/Email Address Book Entry Form

graphics/14fig35.gif

>>> frame.pack()

Change ipadx and ipady for top_left. Space is added only to the first column, and there's more space between the components.

>>> top_left.ipadx, top_left.ipady = 20,20 >>> removeComponents() >>> addComponents() >>> contentPane.add(okay, last) >>> frame.pack()

Experimenting

GridBagLayout takes some up-front planning and a lot of messing around with constraints to get what you want, but the reward is a good-looking GUI that can be resized and still display a reasonable layout.

In Layout4.py, try this exercise:

  1. From layouts select GridBagLayout. Notice that a 3-by-3 grid is defined. You should get two frames that look like Figure 14-36.

    Figure 14-36. Layout4.py after the Exercise

    graphics/14fig36.gif

  2. Select the top right component in the components list.

  3. Change the anchor to WEST from the anchor list. Then hit the Apply button. What happens?

  4. Select the center right component from the components list. Then change the anchor to EAST, and hit the Apply button. What happens?

  5. Try all possible combinations with anchor on several components. Then exit, and restart Layout4.py.

  6. Hit the Pack button. Select All from the components list. Change the fill from NONE to BOTH, and hit the Apply button. Notice that the anchors for center right and top right no longer have any effect. What happens?

  7. Hit Pack, and resize a few times.

  8. Change the fill type to HORIZONTAL; then hit the Apply button. What happens?

  9. Change the anchor to SOUTH; then hit the Apply button. What happens?

  10. Select the top left component, and set its gridx to 3. What happens? Hit the Apply button. What happens? Why?

  11. Select the top right component, and set its gridx to 4. What happens? Hit the Apply button. What happens? Why?

  12. Select the top middle component, and set its gridx to 5. What happens? Hit the Apply button. What happens? Why?

  13. Set the fill to BOTH, and then play with the insets of all of the components.

  14. Create a constraint that spans more than one cell.

  15. Try several combinations until you get the hang of things. Don't be afraid to experiment.

Putting Things Together: Adding GridBagLayout to the Address Application

In our last iteration of the address book application we used GridLayout for the address form pane so that all of the text fields were the same size. That's okay for the name field, which should be about 20 characters, but what about the phone number field, which should be only 13 characters? Left alone, it's about seven characters wider than it needs to be, which is sloppy, not to mention ugly.

To fix the address form pane so that the text fields vary in length but are still flexible across displays, we'll use GridBagLayout to create a 3-by-3 grid. The code to do this is nearly identical to the interactive session explaining GridBagLayout, so you should be able to follow along without much trouble. In case of confusion, I've added numerous comments.

Review the code (from three\AddressFormPane.py), and then look at the new address book application in Figure 14-37. Also run this version of the application, and try resizing the frame to see what happens to the components.

Figure 14-37. Address Book Entry Form with Varying-length Fields

graphics/14fig37.gif

def __init__CreateTextFieldPanel(self):            """ Set up the email, phoneNumber and name text fields """                  # Create the panel and set the grid layout.                  # The grid has a layout of a 3 rows by 2 columns.            editPane = JPanel()            self.cadd(editPane, BorderLayout.CENTER)            editPane.setLayout(GridBagLayout())                  # Set up some constants for the anchor.            CENTER=GridBagConstraints.CENTER            WEST=GridBagConstraints.WEST            EAST=GridBagConstraints.EAST                  # Now let's create a 2 column by 3 row grid.                  # This will be almost identical to the one we created                  # before.                                # x,y,w,h            top_left=makeConstraint(0,0,1,1,weightx=20,weighty=33,anchor=EAST)            top_right = makeConstraint(1,0,1,1,weightx=80,anchor=WEST)            CENTER_left = makeConstraint(0,1,1,1,weighty=33,anchor=EAST)            CENTER_right = makeConstraint(1,1,1,1,anchor=WEST)            bottom_left = makeConstraint(0,2,1,1,weighty=33, anchor=EAST)            bottom_right = makeConstraint(1,2,1,1,anchor=WEST)                  # Create the name textfield. Then add it and its                  # associated label to the contentPane.                  # Add the label to the top_left and the __name                  # textField to the top_right.            self.__name = JTextField(22, text=self.__address.name())            editPane.add(JLabel("Name ", JLabel.RIGHT), top_left)            editPane.add(self.__name, top_right)                  # Create the phoneNumber textfield. Then add it and                  # its corresponding label to the contentPane.                  # Add the label to the CENTER_left. Add the                  # __phoneNumber textfield to the CENTER_right.            self.__phoneNumber=                JTextField(13,text=self.__address.phoneNumber())            editPane.add(JLabel("Phone Number ", JLabel.RIGHT), CENTER_left)            editPane.add(self.__phoneNumber, CENTER_right)                  # Create the email textfield. Then add it and its                  # associated label to the contentPane.                  # Add the label to the bottom_left. Add the __email                  # textfield to the bottom_right.            self.__email = JTextField(30, text=self.__address.email())            editPane.add(JLabel("eMail ", JLabel.RIGHT), bottom_left)            editPane.add(self.__email, bottom_right)

Compare Figure 14-37 to Figure 14-17, which uses GridLayout for the address form instead of GridBagLayout. Which do you think looks better?

Summary

In this chapter, we used the things we've learned about Java GUIs to add a GUI to our address book application. Along the way, we added other things: a list to select address entries based on a person's name, a toolbar, menubar, and popup menus.

With the address book application, we worked with GridLayout, FlowLayout, GridBagLayout, and BorderLayout. We also covered the ins and outs of Java menuing systems and layout managers.

CONTENTS


Python Programming with the JavaT Class Libraries. A Tutorial for Building Web and Enterprise Applications with Jython
Python Programming with the Javaв„ў Class Libraries: A Tutorial for Building Web and Enterprise Applications with Jython
ISBN: 0201616165
EAN: 2147483647
Year: 2001
Pages: 25

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net