Chapter 16. Advanced Swing

CONTENTS
  •  JTable
  •  Working with Table Models
  •  Putting Things Together Adding a Table Model to the Address Book Application
  •  JTree
  •  JToolBar and Actions
  •  Summary

Terms in This Chapter

  • Abstract method

  • Array

  • Data model

  • Design pattern

  • Event listening

  • Functional decomposition

  • Hashtable

  • Helper method

  • Key/value pair

  • Main module

  • Object wrapper

  • Self-documentation

  • Tree model/node

Our coverage of Swing so far has focused on its differences from, and advantages over, AWT. But Swing is more than that. For example, it has some powerful GUI components, particularly JTable and JTree. JTable shows data in a table view, as in a spreadsheet; JTree displays data in a hierarchical view, as in Microsoft Windows Explorer.

In this chapter, we'll update the address book program, making it a JFC application that uses JTable. As we do this, we'll introduce other Swing components.

JTable

To start out, we'll create a prototype that introduces the key features of JTable. Then we'll add JTable support to the address application. This time the application will use the MVC architecture the AddressBook class instance will be a model for the table.

We'll begin by creating a 4-by-4 grid, but first let's learn to use the JTable component. As always, follow along in the interactive interpreter.

Import the JFrame and JTable classes from the javax.swing package.

>>> from javax.swing import JFrame, JTable

Create an instance of JFrame to hold the table.

>>> frame = JFrame('JTable example')

Create an instance of JTable, passing the number of rows and columns to its constructor. Then add the table to the frame.

>>> table = JTable(4,4) >>> frame.contentPane.add(table) javax.swing.JTable[,0,0,0x0,invalid,alignmentX=null,alignmentY=null,border=,flag s=32,maximumSize=,minimumSize=,preferredSize=,autoCreateColumnsFromModel=true, autoResizeMode=AUTO_RESIZE_SUBSEQUENT_COLUMNS,cellSelectionEnabled=false, editing... ...

Pack the table in the frame, and show the frame.

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

Select a cell, and start typing if you want to. The typed text will show up in the cell, as illustrated in Figure 16-1.

Figure 16-1. Cells with Text Entered

graphics/16fig01.gif

The Default Table Model

We didn't specify a model, so a default was provided in which the actual table data is kept. Here's how to access that data.

Get the first row.

>>> table.model.dataVector[0] [Hello, World, Its, good]

Get the first row, second column.

>>> table.model.dataVector[0][1] 'World'

Get the first row, first column.

>>> table.model.dataVector[0][0] 'Hello'

This method is only guaranteed to work for the default. I use it to acclimate you to the model concepts first introduced in Chapter 14, on JList.

Other forms of the JTable constructor allow you to pass in the initial values, that is, the row and column data. Here's an example.

Create the row data.

>>> rowData = [ ['Marhta','Pena'],['Missy','Car'], ['Miguel','TorreAlto'] ]

Create the column headers.

>>> columnNames = ['First name', 'Last name']

Create a table with the row data and column headers.

>>> table = JTable(rowData,columnNames) >>> frame.contentPane.add(table) javax.swing.JTable[,0,0,0x0,invalid,alignmentX=null,alignmentY=null,border=, flags=32,maximumSize=,minimumSize=,preferredSize=,autoCreateColumnsFromModel=true, autoResizeMode=AUTO_RESIZE_SUBSEQUENT_COLUMNS,cellSelectionEnabled=false,editing Column=-1,editingRow=1,gridColor=javax.swing.plaf.ColorUIResource[r=153,g=153,b=1 ...

Pack the table, and display it.

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

There are other ways to set the initial data. Later we'll cover the ones that are substantially different, as well as a special method of populating a table with data using MVC.

Getting and Setting Table Values

With the interface provided by JTable, you can get and set table values. Once the data is set, you can modify it with the setValueAt()method.

>>> table.setValueAt('Hightower', 0, 1) >>> table.setValueAt('Hightower', 1, 1) >>> table.setValueAt('Pena', 2, 1)

However, the more correct way of accessing data is with getValueAt().

>>> table.getValueAt(0,0) 'Marhta'

Tables can work with object wrappers of primitive types Boolean, integer, float, and so forth. Here we set the second column's values to Boolean so that they look like Figure 16-2.

Figure 16-2. Example Table with the Second Column Set to Boolean Values

graphics/16fig02.gif

>>> from java.lang import Boolean >>> table.setValueAt(Boolean(1), 0, 1) >>> table.setValueAt(Boolean(0), 1, 1) >>> table.setValueAt(Boolean(1), 2, 1)

You may have noticed that our demo table doesn't show headers. I don't know why, but I suspect it's a bug in JDK v.1.2.1. As a workaround, we can put the table in a scroll pane (JScrollPane). You usually do this anyway, so there should be no problem. It wasn't necessary in earlier Swing versions.

JList makes much use of JScrollPane, which adds scrolling to a list or table via the Decorator design pattern (see Design Patterns, Gamma et al., 1995). Here's the last example (from JTable2.py) with the missing headers restored.

from javax.swing import JFrame, JTable, JScrollPane frame = JFrame('JTable example')       #Create row data. rowData = [ ['Marhta','Pena'],['Missy','Car'], ['Miguel','TorreAlto'] ]       #Create column headers. columnNames = ['First name', 'Last name']       #Create a table with the row data and the column headers. table = JTable(rowData,columnNames) frame.contentPane.add(JScrollPane(table)) frame.pack() frame.visible = 1

Now the table should look like Figure 16-3.

Figure 16-3. Table with the Column Headers Restored

graphics/16fig03.gif

Working with Table Models

The most common way to create tables is to supply the data model first. JTable works with any class instance that inherits from the TableModel interface (recall that a Java interface is like a Python class with all abstract methods). Let's first see what the TableModel methods do and then create a short example that uses our old address book application.

Data Model Review

We've covered TableModel and JList, so I won't go into the same level of detail because I'm assuming that you did your reading and your exercises and now have a basic understanding of them. If you don't, you may want to do a little review. You'll need it to understand the JTable and JTree controls, which will be covered later on.

The TableModel Interface

The TableModel interface is contained in javax.swing.table and specifies how table model data will be treated. Any class that implements TableModel can act as a data model for JTable.

Here are TableModel's methods:

  • getRowCount()

  • getColumnCount()

  • getColumnName(columnIndex)

  • getColumnClass(columnIndex)

  • isCellEditable(rowIndex, columnIndex)

  • getValueAt(rowIndex, columnIndex)

  • setValueAt(rowIndex, columnIndex)

  • addTableModelListener(l)

  • removeTableModelListener(l)

TableModel represents a tabular object just like a spreadsheet. Thus, getRowCount() and getColumnCount() return the number of rows and columns, from which the total number of cells can be calculated (row count * column count). You can access the row and column counts as the properties instance.columnCount and instance.rowCount, respectively.

setValueAt() and getValueAt() set and get cell data by specifying the cell's row and column indexes. The isCellEditable() method determines if the cell is read-only or read/write. Many times when using a table, you're likely showing a report or some other type of static data, in which case isCellEditable() returns false. If you're changing the cell data, it returns true.

getColumnName(), obviously, returns the name of a column. getColumnClass() returns the class that the column deals with, which helps in displaying and editing the column's cells, as we'll see later.

addTableModelListener() and removeTableModelListener() allow components like JTable instances to register for TableModel events.

The AbstractDataModel Class

Firing events and tracking event listeners can be real but necessary annoyances. Wouldn't it be nice, though, if all of their functionality was in a common, extensible class? This is where object-oriented programming comes in. Our friends at JavaSoft have combined event firing and listener tracking in a class called AbstractDataModel.

AbstractDataModel is, obviously, abstract, so you have to instantiate it in a subclass (i.e., extend it). It makes no assumption about how you supply or define your data, so you don't get the getRowCount(), getColumnCount(), or getValueAt() methods with it. That's okay, because DefaultDataModel, which does have them, extends AbstractDataModel.

Remember when we initialized a JTable with no model, just data? JTable took that data and passed it to an instance of DefaultDataModel. In other words, even when we don't pass JTable a model, it still uses one, or rather it uses an instance of a class that implements the TableModel interface.

To show how AbstractDataModel works, we'll go back to our pizza topping application, but this time we'll add editable columns and Boolean values. The first column will list the toppings; the second will hold checkboxes that are set when the customer makes a selection.

Here's the complete topping code (model.py):

from javax.swing.table import AbstractTableModel from java.lang import Boolean, String class PizzaTableModel(AbstractTableModel):       def __init__(self):             self.data = [ ['Pepperoni', Boolean(0)],                          ['Sausage', Boolean(0)],                          ['Onions', Boolean(0)],                          ['Olives', Boolean(0)],                          ['Mushrooms', Boolean(0)],                          ['Peppers', Boolean(0)] ]             self.columnNames = ['Topping', 'Add?']             self.columnClasses = [String, Boolean]             self.Add=1       def getRowCount(self):             return len(self.data)       def getColumnCount(self):             return len(self.data[0])       def getValueAt(self, rowIndex, columnIndex):             return self.data[rowIndex][columnIndex]       def getColumnName(self, columnIndex):             return self.columnNames[columnIndex]       def getColumnClass(self, columnIndex):             return self.columnClasses[columnIndex]       def isCellEditable(self, rowIndex, columnIndex):             return columnIndex == self.Add       def setValueAt (self, value, rowIndex, columnIndex):             if(columnIndex == self.Add):                    value = Boolean(value)                    self.data[rowIndex][columnIndex] = value                    self.fireTableCellUpdated(rowIndex, columnIndex)

A Table Model for the Pizza Topping Application Step by Step

First we import the classes we need: AbstractTableModel and PizzaTableModel (which extends AbstractTableModel). PizzaTableModel provides the user interface. It also imports the Boolean and String classes, which it uses for the getColumnClass() method (more on this later).

from javax.swing.table import AbstractTableModel from java.lang import Boolean, String class PizzaTableModel(AbstractTableModel):

Next the PizzaTableModel constructor defines three variables: data, which holds the cell data in a list of lists; columnNames, which holds the column headers in a list of strings; and columnClasses, which holds the classes in a list of classes. It also defines a variable a constant that defines the location of the Add? column. Add? consists of Boolean values that determine whether or not a customer selects a particular topping.

def __init__(self):       self.data = [ ['Pepperoni', Boolean(0)],                    ['Sausage', Boolean(0)],                    ['Onions', Boolean(0)],                    ['Olives', Boolean(0)],                    ['Mushrooms', Boolean(0)],                    ['Peppers', Boolean(0)] ]       self.columnNames = ['Topping', 'Add?']       self.columnClasses = [String, Boolean]       self.Add=1

PizzaTableModel implements getRowCount() by returning the length of the data list.

def getRowCount(self):       return len(self.data)

It also implements getColumnCount() by returning the length of the first list in the data variable. Recall that data is a list of lists and that all of the lists contain two items.

def getColumnCount(self):       return len(self.data[0])

getValueAt() indexes the rows and columns of data's list of lists and returns

def getValueAt(self, rowIndex, columnIndex):       return self.data[rowIndex][columnIndex]

getColumnNames() indexes the columnNames list with the columnIndex argument and returns

def getColumnName(self, columnIndex):       return self.columnNames[columnIndex]

getColumnClass() returns the value that results from indexing columnClasses. The class it returns determines the cell editor JTable will use. Setting the second column to Boolean values converts it to checkboxes.

def getColumnClass(self, columnIndex):       return self.columnClasses[columnIndex]

isCellEditable() helps JTable determine if the cell can be modified. Every cell in Add? is editable, so for that column a true value is returned. Remember, the Add attribute is a constant that denotes the Add? column.

def isCellEditable(self, rowIndex, columnIndex):       return columnIndex == self.Add

setValueAt() sets the value of the Add? column only. It should never be called for the Toppings column unless the JTable instance has determined, through isCellEditable(), that it can be modified. For this reason, setValueAt() checks if the column index is Add? If so, it converts the set value to Boolean.

Remember that Python treats Boolean primitives as integers, so we convert the set value to Boolean. Then we index data with the rowIndex and columnIndex arguments, set the data's value, and notify all event listeners of the change via fireTableCell().

AbstractTableModel implements fireTableCell() along with other helper methods like addTableModelListener() and removeTableModelListener().

def setValueAt (self, value, rowIndex, columnIndex):        if(columnIndex == self.Add):               value = Boolean(value)               self.data[rowIndex][columnIndex] = value               self.fireTableCellUpdated(rowIndex, columnIndex)

To test the model at this point we add the following code, but only if model.py is run as the main module.

if __name__ == '__main__':       from javax.swing import JTable, JFrame, JScrollPane       pizza_model = PizzaTableModel()       table = JTable(pizza_model)       frame = JFrame('Select your toppings')       frame.contentPane.add(JScrollPane(table))       frame.pack()       frame.visible = 1

The first part of the code imports JTable, JFrame, and JScrollPane. Then it creates a table and a frame and passes the table to a JScrollPane constructor, adding the JScrollPane instance to the frame's content pane. Finally it packs the frame and makes it visible.

Run model.py. What you get should look like Figure 16-4. Then try these exercises:

  • Add five more toppings.

  • Add a column called Extra for the customer to choose an extra amount of a particular topping. Make it so the cells can be edited only if the corresponding cell in the Add? column is selected.

Figure 16-4. The Topping Table

graphics/16fig04.gif

Putting Things Together Adding a Table Model to the Address Book Application

In this section, we'll add a table model to the address book application and "swingify" some of the application's AWT ways.

AWT to JFC

The first time I wrote the address book application, I used all of the old AWT components (java.awt.Frame, java.awt.Panel, etc.). When I modified it, only 12 lines out of 114 had to be changed, and 9 of those dealt with JList.

The point is that the move from List to JList changed a lot, although most of the changes weren't needed. Other JFC/AWT components map nicely even though they're not closely related on the class hierarchy. Luckily, JList is the exception, not the rule.

Up to now what we've seen of JFC/Swing is pretty much the same as AWT. Most developers have migrated to Swing by now, in spite of the fact that the first few Swing versions were well short of perfect. Since then, Swing has proven its worth.

Adding a Table and Moving the Address Data to a View

Now it's time to start tinkering with our "swingified" address book application, first adding support for a table. To do this we'll move the address data to a model, which will be a combination table and list. The purpose is to show how, with MVC, many views can be updated from a single data source.

Recall from prior examples that the address data was loaded into a dictionary (dict) from a file. We'll use this dictionary to hold the addresses, as always, but instead of making dict an aggregate member of AddressMain, we'll create a new class called AddressModel that acts as both a list model and a table model and put dict in there. Then we'll add a JFC table to the class so that both the JList and JTable instances reference the same model that is, an AddressMain instance.

The tricky part of AddressMain is mapping the address data (stored in a dictionary) to both the list and table models. We'll accomplish this by creating a Python list to contain the sorted dictionary keys, which we'll use to sort the addresses by name into table rows. This will make it easier to get the address instance at a given row index.

For AddressModel to act as a model for both a list and a table, it needs to implement the ListModel and TableModel interfaces. As I said earlier, JFC provides AbstractListModel and AbstractTableModel classes that hold a good part of the functionality needed to implement their respective models. We want to use them, but we can't just inherit their functionality. In Java you can't extend two base classes, but you can implement many interfaces. In Jython you can't extend two Java base classes, but you can extend two Python base classes. To overcome this obstacle, what we have to do is extend from AbstractTableModel and implement AbstractListModel's interface.

AddressModel

The following code (from Two/AddressModel.py) shows AddressModel's definition and constructor. Notice that AddressModel implements TableModel (through AbstractTableModel) and ListModel.

class AddressModel(AbstractTableModel, ListModel):        """The AddressModel is both a ListModel and a TableModel."""        def __init__(self):              """Initialize the Address model.              Read the dictionary from the file."""              self.dict = None    # holds the dictionary of addresses.              self.list = None    # holds the sorted list of names,                                  # which are keys                                  # into the dictionary.              self.listeners = [] #to hold list of ListModelListeners                    # Read the addresses from the file.              self.fname=".\\addr.dat"       # holds the file name                                             # that holds the addresses              self.dict = readAddresses(self.fname)                    # Store the sorted list of names.              self.list = self.dict.keys()              self.list.sort()                    # Define the column names and locations in the table.              self.columnNames=['Name', 'Phone #', 'Email']              self.NAME = 0   # To hold the location of the name column              self.PHONE = 1 # To hold the location of the phone number column              self.EMAIL = 2  # To hold the location of the email column

We can see that the list is sorted and that the rows in the table correspond to the index values of the sorted names. Thus, a given address is stored in the table based on the index of the name in the list. For the columns, we need to be more creative. The constants defined in the constructor correspond to the placement of the address instance fields in the rows relative to the columns.

AddressModel's Helper Methods

To map the address dictionary (self.dict) in the AddressModel interface to a table, we need to define some helper methods, as shown in the following code (Two\AddressModel.py). The method names are self-explanatory, and the comments should fill in any blanks. (Make sure to read the comments and document strings; think of them as integral to the concepts in this chapter.)

def __getAddressAtRow(self, rowIndex):          """Get the address at the specified row.               (Private methods begin with __)"""                   # Get the row name out of the list.                   # Use the name to index the dictionary of addresses.                   # Get the address associated with this row.          name = self.list[rowIndex]          address = self.dict[name]          return address def __getAddressAttributeAtColumn(self, address, columnIndex):          """Get the attribute of the address at the               specified column index. Maps column indexes to               attributes of the address class instances."""          value = None # Holds the value we are going to return                   # Set the value based on the column index.          if(columnIndex == self.NAME):                   value = address.name()
Mapping to ListModel and TableModel

The first two helper methods map the list and name attributes to ListModel and the dict and address instances to TableModel. getAddressAtRow() gets the address at the specified row. getAddressAttributeAtColumn() gets the address attribute at the specified column index; that is, it maps column indexes to attributes of the address instances. Since the name attribute is used to determine the row index, __changeDictKey() and __changeList() keep the list and dictionary in sync with each address whose name attribute is changed.

Comments: More Is More

Sometimes I comment the obvious in my code, but when it comes to comments I don't believe that less is more. In fact, when things aren't obvious, such as mapping a dictionary to a table model, I prefer to go overboard.

Don't be chintzy with your comments. For every minute saved by not writing them, you'll waste a hundred minutes in code maintenance.

 

Stupid Methods

Most of the time you break methods into many other methods so that the same code isn't repeated in different places. This is an idea borrowed from functional decomposition. In object-oriented programming, there's another reason to do this. It's called stupid, and that's a good thing.

Keep your methods short and stupid. Long methods are hard to understand and hard to change, so you have to divide to conquer. __setAddressAttributeAtColumn() is a stupid method because it's called by only one other method, leaving functional decomposition out of the picture.

Implementing the ListModel Interface

AddressModel's next four methods implement the ListModel interface.

def getSize(self):       """Returns the length of the items in the list."""       return len(self.list) def getElementAt(self, index):       """ Returns the value at index. """       return self.list[index] def addListDataListener(self, l):       """Add a listener that's notified when the model changes."""       self.listeners.append(l) def removeListDataListener(self, l):       """Remove a listener."""       self.listeners.remove(l)

getSize() returns the length of the list attribute. (Remember that list consists of the sorted keys of the dict attribute, and name corresponds to the address stored at the location indicated by the key.) getElementAt() returns the element at the index from the list. addListDataListener() and removeListDataListener() keep track of the listeners that subscribe to events from AddressModel instances.

ListModel Event Notification

Every time a ListModel event occurs, AddressModel notifies the listeners that have subscribed to it using the following methods:

def __fireContentsChanged(self, index, index1):        """Fire contents changed, notify viewers            that the list changed."""        event = ListDataEvent(self, ListDataEvent.CONTENTS_CHANGED, index,                index1)            for listener in self.listeners:                listener.contentsChanged(event) def __fireIntervalAdded(self, index, index1):        """Fire interval added, notify viewers           that the items were added to the list."""        event = ListDataEvent(self, ListDataEvent.INTERVAL_ADDED, index,                index1)            for listener in self.listeners:                listener.intervalAdded(event) def __fireIntervalRemoved(self, index, index1):        """Fire interval removed from the list,            notify viewers."""            event = ListDataEvent(self, ListDataEvent.INTERVAL_REMOVED, index,                    index1)            for listener in self.listeners:                listener.intervalRemoved(event)
Implementing the TableModel Interface

The next set of methods help implement the TableModel interface, partly by extending the AbstractTableModel class and partly by mapping the addresses stored in the dictionary to the rows and columns in the table model.

## The following methods implement the TableModel interface.   ##         def addTableModelListener(self, l):                """Add a listener that gets notified when the data model                  changes. Since all we are doing is calling the super,                  we don't need this method."""                AbstractTableModel.addTableModelListener(self, l)         def removeTableModelListener(self, l):                """Remove a listener. Since all we are doing is                    calling the super, we don't need this method.                    It's here for example."""                AbstractTableModel.removeTableModelListener(self, l)         def getColumnClass(self, columnIndex):                """Returns the common base Class for the column."""                return String         def getColumnCount(self):                """Returns the number of columns in the data model."""                return len(self.columnNames)         def getColumnName(self, columnIndex):                """Returns the name of the given column by columnIndex."""                return self.columnNames[columnIndex]         def getRowCount(self):                """Returns the number of rows in the table model."""                return len(self.list)         def getValueAt(self, rowIndex, columnIndex):                """Returns the cell value at location specified                    by columnIndex and rowIndex."""                        # Get the address object corresponding to this row.                address = self.__getAddressAtRow(rowIndex)                        # Get the address attribute corresponding                        # to this column.                value = self.__getAddressAttributeAtColumn(address, columnIndex)                return value         def isCellEditable(self, rowIndex, columnIndex):                """Returns if the cell is editable at the given                    rowIndex and columnIndex."""                    #All cells are editable                return 1         def setValueAt(self, aValue, rowIndex, columnIndex):                """Sets the value for the cell at the given                    columnIndex and rowIndex."""                        # Get the address object corresponding to this row.                address = self.__getAddressAtRow(rowIndex)                self.__setAddressAttributeAtColumn(aValue, address, columnIndex)                self.fireTableCellUpdated(rowIndex, columnIndex)

addTableModelListener() and removeTableModelListener() are in the code for illustration only, because AbstractTableModel already implements them. This means that the following code is unnecessary:

def addTableModelListener(self, l):      ...      ...      AbstractTableModel.addTableModelListener(self, l) def removeTableModelListener(self, l):      ...      ...      AbstractTableModel.removeTableModelListener(self, l)
Columns and Rows

AddressModel inherits functionality from AbstractTableModel, so we can leave out addTableModelListener() and removeTableModelListener() (that's what we'll do in the third iteration of this example), and it will still function.

Here are three methods that work with columns:

def getColumnClass(self, columnIndex):         """Returns the common base Class for the column."""         return String def getColumnCount(self):         """Returns the number of columns in the data model."""         return len(self.columnNames) def getColumnName(self, columnIndex):         """Returns the name of the given column             by columnIndex."""         return self.columnNames[columnIndex]

getColumnClass() always returns a string because every attribute we expose in the AddressModel class is a string. getColumnCount() uses the length of the columnNames attribute (defined in AddressModel's constructor), which is a list of strings that contain column names for the table model. It also uses the column index (columnIndex) as an index into columnNames to get the name that corresponds to a given column.

Only one method deals with rows specifically, getRowCount(), which returns the length of the items in columnNames. Every item in columnNames corresponds to a key that specifies a row in the table. This means that the length of columnNames is equal to the number of rows in TableModel.

Cell Values

The next three methods get and edit cells in the table.

def getValueAt(self, rowIndex, columnIndex):            """Returns the cell value at location specified by                 columnIndex and rowIndex."""                    # Get the address object corresponding to this row.            address = self.__getAddressAtRow(rowIndex)                    # Get the address attribute                    # corresponding to this column.            value = self.__getAddressAttributeAtColumn(address, columnIndex)            return value def isCellEditable(self, rowIndex, columnIndex):            """Returns if the cell is editable at the given                 rowIndex and columnIndex."""                    # All cells are editable            return 1 def setValueAt(self, aValue, rowIndex, columnIndex):            """Sets the value for the cell at the given                 columnIndex and rowIndex."""                    # Get the address object corresponding to this row.            address = self.__getAddressAtRow(rowIndex)            self.__setAddressAttributeAtColumn(aValue, address, columnIndex)            self.fireTableCellUpdated(rowIndex, columnIndex)

getValueAt() uses __getAddressAtRow() to get the address at the current row; then it uses __getAddressAttributeAtColumn() to get the value of the attribute corresponding to the given column index, and returns the value received. (__getAddressAttributeAtColumn() is an example of a stupid method: Its name describes what it does, which makes it almost completely self-documenting.)

IsCellEditable() assumes that all cells are editable and returns true (1) no matter what the row index and column index are equal to.

Like getValueAt(), setValueAt() uses __getAddressAtRow() to retrieve the address at the given row index (making getAddressAtRow() a good example of functional decomposition). Next it uses __setAddressAttribute() to set the value of the address attribute corresponding to the column index and, finally, notifies every JTable view of the change. This way, if multiple views are listening (or using the same model), they'll all be updated.

__setAddressAttribute()

Because it does so much, let's take a closer look at __setAddressAttribute(). Examine the code closely. If you understand it, you understand AddressModel.

def __setAddressAttributeAtColumn(self, value, address, columnIndex):          """Sets the address attribute at the corresponding              column index. Maps the Address instance attributes              to the column in the table(s) for editing"""              # Get the email, phone and name from the address object.          email, phone = address.email(), address.phoneNumber()          name = address.name()                 # Set the value based on the column                 # The columnIndex is the name so set the name                 # in the address object.                 # Since the name is used for the list and keys in the                 # dictionary, we must change both list item and the                 # dictionary key associated with name.          if(columnIndex == self.NAME):                 address.__init__(value, phone, email)                 self.__changeList(value, name)                 self.__changeDictKey(value, name)          elif(columnIndex == self.PHONE):                 address.__init__(name, value, email)          elif(columnIndex == self.EMAIL):                 address.__init__(name, phone, value)

columnIndex maps to a particular attribute in the class instance. It's compared against three constants (name, phone, and email) to determine the attribute to set.

__init__ and Its Stupid Methods

The __init__method of the address argument sets the attribute. However, if that attribute is name (i.e., columnIndex ==self.name), several things have to occur. This part of the code was tricky to write because the name of the address is a key into the dictionary and is used by the list to sort the keys. That means that if the name changes, so do the dictionary keys and list values. The best way I could think of to keep __init__ from becoming an unruly mess was to break it down into several stupid methods.

The first stupid method, __changeList(), removes the old value from the list and inserts the new value in its place.

def __changeList(self, newValue, oldValue):                 """Change the old value in the list to the new value.                      Keeps the JList views in sync with this                      Python list by firing an event."""                        # Get the index of the oldValue in the list.                        # Use the index to index the list,                        # and change the list location to the new value.                        # Notify the JList views subscribed to this model.                 index=self.list.index(oldValue)                 self.list[index]=newValue                 self.__fireContentsChanged(self, index, index)

Then it notifies the list views (JList instances) of the replacement.

The next stupid method, __changeDictKey(), removes the key/value pair in the dictionary corresponding to the name. (Recall that the name is the key into the dictionary and the value is the address instance with that name.) It's a lot like __changeList() in that it manages a collection of model data affected by a name change in an address instance.

def __changeDictKey(self, newKey, oldKey):      """Change the old key in the dictionary           to the new key."""              # Get the address stored at the oldKey location.              # Delete the old key value from the dictionary.              # Use the new key to create a new location              # for the address.      address = self.dict[oldKey]      del self.dict[oldKey]      self.dict[newKey]=address

Using the old key (oldKey), __changeDictKey() gets the address at the old location and deletes it. Then it uses the new key (newKey) to set a new location for the address, which was extracted on the first line.

Testing

We're not ready to put AddressModel in our address book application yet. Before we can integrate a fairly large piece of code like this, we have to test it with scaffolding code.

The AddressModel class has to fit well in the MVC architecture, so we want it to handle multiple views that work with ListModel and TableModel. To test for this we'll use JList and JTable, and we'll throw in JTabbedPane because it can hold several components, each in its own tab.

Here's our scaffolding code. Read the comments to get an idea of the flow.

if __name__ == '__main__':       from javax.swing import JTable, JList, JFrame, JTabbedPane, JScrollPane            # Create an instance of JFrame, JTabbedPane, AddressModel, JTable,            # and JList. Pass the same instance of AddressModel to the table            # and the list constructor.       frame = JFrame('Test AddressModel')       pane = JTabbedPane()       model = AddressModel()       list = JList(model)       table = JTable(model)            # Add the table and the list to the tabbed pane.       pane.addTab('table 1', JScrollPane(table))       pane.addTab('list 1', JScrollPane(list))            # Create another table and list.       table = JTable(model)       list = JList(model)            # Add the other table and list to the tabbed pane.            # Now we have four views that share the same model.       pane.addTab('table 2', JScrollPane(table))       pane.addTab('list 2', JScrollPane(list))            # Add the pane to the frame.            # Smack it, pack it, and show it off.       frame.contentPane.add(pane)       frame.pack()       frame.visible=1

Try these exercises:

  • Run AddressModel.py from the command line (jython AddressModel.py). You should get a frame that looks like Figure 16-5.

    Figure 16-5. The Initial Test AddressModel Frame

    graphics/16fig05.gif

  • Change "Andy Grove" to someone else, and change his phone number and email address. What happens to the other views (list1, list2, and table2)?

  • Use the code for AddressModel to create another column in the table called Cell Phone. Edit the code for Address and AddressModel to accommodate it.

Integrating AddressModel with AddressMain

Now that AddressModel has been defined and tested, let's put it in our address book application. To integrate it with AddressMain, we're going to need methods that will load the form with an address, add and remove an address from the model, and save an address to disk. Before this functionality was in AddressMain; now it's in the following methods in AddressModel:

  • getAddressAt() gets the address instance at a given row index

  • getAddressByName() gets the address by a given name

  • addAddress() adds an address to the model

  • removeAddressByName() removes an address from the model when given the name of the address

  • writeAddress() saves the address to disk

Here's the code for these methods:

def getAddressAt(self, rowIndex):          """Get an Address by rowIndex"""          return self.__getAddressAtRow(rowIndex) def getAddressByName(self, name):          """Get an Address by the name property of the address."""          return self.dict[name] def addAddress(self, address):          """Add an address to the model, and notify the              views of the change."""                  # Add the address to the dictionary,                  # and get the keys as the list.          self.dict[address.name()] = address          self.list = self.dict.keys()                  # Sort the addresses, and find the index of                  # the new address in the list.                  # Then notify the list and table views.          self.list.sort()          index = self.list.index(address.name())          self.__fireIntervalAdded(index, index)          self.fireTableDataChanged() def removeAddressByName(self, name):          """Removes the address from the model and             notifies views of change."""                  # Remove the address from the dictionary.          del self.dict[name]                  # Remove the name from the list.          index = self.list.index(name)          del self.list[index]                  # Notify the table and list views that the data changed.          self.fireTableDataChanged()          self.__fireIntervalRemoved(index, index) def writeAddresses(self):          """Write the address data to the file."""          writeAddresses(self.fname, self.dict)

Whenever addresses are added or removed from the model, all table and list views the listeners must be notified via the __fireIntervalAdded(), __fireIntervalRemoved(), and __fireTableDataChanged() methods.

The AddressModel Code

Now that we've covered each part of AddressModel, we can look at its complete code (from Two\AddressModel.py).

from address import * from javax.swing import AbstractListModel, ListModel from javax.swing.table import AbstractTableModel, TableModel from javax.swing.event import ListDataEvent from java.lang import String class AddressModel(AbstractTableModel, ListModel):        """The AddressModel is both a ListModel and a TableModel."""        def __init__(self):              """Initialize the Address model.              Read the dictionary from the file."""              self.dict = None    # holds the dictionary of addresses.              self.list = None    # holds the sorted list of names,                                  # which are keys into the dictionary.              self.listeners = [] #to hold list of ListModelListeners                     # Read the addresses from the file.              self.fname=".\\addr.dat" # holds the file name that                                       # holds the addresses              self.dict = readAddresses(self.fname)                     # Store the sorted list of names.              self.list = self.dict.keys()              self.list.sort()                    # Define the column names and locations in the table.              self.columnNames=['Name', 'Phone #', 'Email']              self.NAME = 0  # To hold the location of the name column              self.PHONE = 1 # To hold the location of the phone number column              self.EMAIL = 2     # To hold the location of the email column        def __getAddressAtRow(self, rowIndex):              """Get the address at the specifed row. (Private method)"""                    # Get the row name out of the list.                    # Use the name to index the dictionary of addresses.                    # Get the address associated with this row.              name = self.list[rowIndex]              address = self.dict[name]              return address        def __getAddressAttributeAtColumn(self, address, columnIndex):              """Get the attribute of the address at the                  specified column index."""              value = None # Holds the value we are going to return                    # Set the value based on the column index.              if(columnIndex == self.NAME):                    value = address.name()              elif(columnIndex == self.PHONE):                    value = address.phoneNumber()              elif(columnIndex == self.EMAIL):                    value = address.email()                    return value        def __changeList(self, newValue, oldValue):              """Change the old value in the list to the new value."""                    # Get the index of the oldValue in the list.                    # Use the index to index the list,                    # and change the list location to the new value.                    # Notify the world.              index=self.list.index(oldValue)              self.list[index]=newValue              self.__fireContentsChanged(index, index)        def __changeDictKey(self, newKey, oldKey):              """Change the old key in the dictionary to the new key."""                    # Get the address stored at the oldKey location.                    # Delete the old key value from the dictionary.                    # Use the new key to create a new location                    # for the address.              address = self.dict[oldKey]              del self.dict[oldKey]              self.dict[newKey]=address        def __setAddressAttributeAtColumn(self, value, address, columnIndex):              """Sets the address attribute at the                    corresponding column index."""                    # Get the email, phone and name from the address object.              email, phone = address.email(), address.phoneNumber()              name = address.name()                    # Set the value based on the column.                    # The columnIndex is the name so set the name                    # in the address object.                    # Since the name is used for the list and keys in the                    # dictionary, we must change both list item and the                    # dictionary key associated with name.              if(columnIndex == self.NAME):                    address.__init__(value, phone, email)                    self.__changeList(value, name)                    self.__changeDictKey(value, name)              elif(columnIndex == self.PHONE):                    address.__init__(name, value, email)              elif(columnIndex == self.EMAIL):                    address.__init__(name, phone, value) ## The following methods implement the ListModel interface.   ##        def getSize(self):              """Returns the length of the items in the list."""              return len(self.list)        def getElementAt(self, index):              """ Returns the value at index. """              return self.list[index]        def addListDataListener(self, l):              """Add a listener that's notified when the model changes."""              self.listeners.append(l)        def removeListDataListener(self, l):              """Remove a listener."""              self.listeners.remove(l)        def __fireContentsChanged(self, index, index1):              """Fire contents changed. Notify viewers                  that the list changed."""              event = ListDataEvent(self, ListDataEvent.CONTENTS_CHANGED,                      index, index1)              for listener in self.listeners:                      listener.contentsChanged(event)        def __fireIntervalAdded(self, index, index1):              """Fire interval added. Notify viewers that                  the items were added to the list."""              event = ListDataEvent(self, ListDataEvent.INTERVAL_ADDED, index,                      index1)              for listener in self.listeners:                      listener.intervalAdded(event)        def __fireIntervalRemoved(self, index, index1):              """Fire interval removed from the list. Notify viewers."""              event = ListDataEvent(self, ListDataEvent.INTERVAL_REMOVED,                      index, index1)              for listener in self.listeners:                      listener.intervalRemoved(event) ## The following methods implement the TableModel interface.   ##        def addTableModelListener(self, l):              """Add a listener that gets notified when the              data model changes. Since all we are doing is              calling the super, we don't need this method."""              AbstractTableModel.addTableModelListener(self, l)        def removeTableModelListener(self, l):              """Remove a listener. Since all we are doing is calling the                  super, we don't need this method. It's here for example."""              AbstractTableModel.removeTableModelListener(self, l)        def getColumnClass(self, columnIndex):              """Returns the common base Class for the column."""              return String        def getColumnCount(self):              """Returns the number of columns in the data model."""              return len(self.columnNames)        def getColumnName(self, columnIndex):              """Returns the name of the given column by columnIndex."""              return self.columnNames[columnIndex]        def getRowCount(self):              """Returns the number of rows in the table model."""              return len(self.list)        def getValueAt(self, rowIndex, columnIndex):              """Returns the cell value at location specified                  by columnIndex and rowIndex."""                      # Get the address object corresponding to this row.              address = self.__getAddressAtRow(rowIndex)                      # Get the address attribute                      # corresponding to this column.              value = self.__getAddressAttributeAtColumn(address, columnIndex)              return value        def isCellEditable(self, rowIndex, columnIndex):              """Returns if the cell is editable at the given                  rowIndex and columnIndex."""                      #All cells are editable              return 1        def setValueAt(self, aValue, rowIndex, columnIndex):              """Sets the value for the cell at the given                  columnIndex and rowIndex."""                      # Get the address object corresponding to this row.              address = self.__getAddressAtRow(rowIndex)              self.__getAddressAttributeAtColumn(aValue, address, columnIndex)                  # Notify that we changed this value so other                  # views can adjust.              self.fireTableCellUpdated(rowIndex, columnIndex)        def getAddressAt(self, rowIndex):              """Get an Address by rowIndex"""              return self.__getAddressAtRow(rowIndex)        def getAddressByName(self, name):              """Get an Address by the name property of the address."""              return self.dict[name]        def addAddress(self, address):              """Add an address to the model,              and notify the views of the change."""                      # Add the address to the dictionary,                      # and get the keys as the list.              self.dict[address.name()] = address              self.list = self.dict.keys()                      # Sort the addresses, and find the index of                      # the new address in the list.                      # Then notify the list and table views.              self.list.sort()              index = self.list.index(address.name())              self.__fireIntervalAdded(index, index)              self.fireTableDataChanged()        def removeAddressByName(self, name):              """Removes the address from the model                  and notifies views of change."""                      # Remove the address from the dictionary.              del self.dict[name]                      # Remove the name from the list.              index = self.list.index(name)              del self.list[index]                      # Notify the table and list views that the data changed.              self.fireTableDataChanged()              self.__fireIntervalRemoved(index, index)        def writeAddresses(self):              """Write the address data to the file."""              writeAddresses(self.fname, self.dict) if __name__ == '__main__':        from javax.swing import JTable, JList, JFrame, JTabbedPane, JScrollPane        from javax.swing.event import ListDataListener        class listenertest (ListDataListener):                def intervalAdded(self, e):                        print 'Interval Added: ' + `e`                def intervalRemoved(self, e):                        print 'Interval Removed: ' + `e`                def contentsChanged(self, e):                        print 'Content Changed: ' + `e`              # Create an instance of JFrame, JTabbedPane, AddressModel,              # JTable, and JList.              # Pass the same instance of AddressModel to the table              # and the list constructor.        frame = JFrame('Test AddressModel')        pane = JTabbedPane()        model = AddressModel()        model.addListDataListener(listenertest())        list = JList(model)        table = JTable(model)              # Add the table and the list to the tabbed pane.        pane.addTab('table 1', JScrollPane(table))        pane.addTab('list 1', JScrollPane(list))              # Create another table and list.        table = JTable(model)        list = JList(model)              # Add the other table and list to the tabbed pane.              # Now we have four views that share the same model.        pane.addTab('table 2', JScrollPane(table))        pane.addTab('list 2', JScrollPane(list))              # Add the pane to the frame.              # Smack it, pack it, and show it off.        frame.contentPane.add(pane)        frame.pack()        frame.visible=1        addr = Address('aaaaa','aaaaaa','aaaaaa')        model.addAddress(addr)        raw_input('Hit Enter to continue')        model.removeAddressByName(addr.name())

The actual code for implementing AddressModel is only about 100 lines, but the file is about 280 lines. Comments, document strings, and scaffolding account for the difference. Although it takes up 28 lines, scaffolding saves a lot of time in isolating and debugging problems. In a production environment, though, you should put any testing code in its own module.

Changes in AddressMain to Accommodate AddressModel

I had to change AddressMain to use AddressModel. In the code that follows the modifications are highlighted in bold:

from java.awt import BorderLayout, FlowLayout from AddressModel import AddressModel ... ... Frame=JFrame; List=JList; Panel=JPanel; Dialog=JDialog; Button=JButton Menu=JMenu; MenuItem=JMenuItem; MenuBar=JMenuBar; PopupMenu=JPopupMenu; from AddressFormPane import AddressForm ... ... class AddressMain(Frame):        def __init__(self):                     # Call the base class constructor.              Frame.__init__(self, "Main Frame")                     # Create a list.              self.addresses = List()                     # Keep forward-compatible with other containers like JFrame.              self.container = JPanel()              self.container.layout = BorderLayout()                     # Add the addresses list to the container, on the left side.              scrollpane = JScrollPane(self.addresses)              scrollpane.horizontalScrollBarPolicy =                  JScrollPane.HORIZONTAL_SCROLLBAR_ALWAYS              self.container.add(scrollpane, BorderLayout.WEST)                     # Create an instance of AddressModel,                     # and set the addresses and self model attributes.              model = AddressModel()              self.addresses.model = model              self.model = model                     # 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.container.add(self.form, BorderLayout.EAST)                     # Create a tabbed pane, and add the container and table                         in their own tabs.                     # Add the JTabbed instance to the contentPane.              tabbed_pane = JTabbedPane()              table = JTable(model)              tabbed_pane.addTab('Edit Address', self.container)              tabbed_pane.addTab('View Address Book', JScrollPane(table))              self.contentPane.add(tabbed_pane, BorderLayout.CENTER)                     # Set up toolbar and menubar.              self.__init__toolbar()              self.__init__menu()              self.popup = None     #to hold the popup menu              self.__init__popup()               self.addresses.mousePressed = self.__popupEvent              self.windowClosing = self.__windowClosing                     # Set the default address in the list and form.              if self.model.getSize() > 0:                     self.addresses.setSelectedIndex(0)                     address = self.model.getAddressAt(0)                     self.form.set(address)                     # Pack the frame and make it visible.              self.pack()              self.setSize(600, 300)              self.visible=1        ...        ...        ...        def __addAddress_Clicked(self,event):              dialog = AddAddressDialog(self)              if dialog.getState() == AddAddressDialog.OK:                     addr = dialog.getAddress()                     self.model.addAddress(addr)                     ...        def __removeAddress_Clicked(self,event):              index = self.addresses.selectedIndex              key = self.addresses.selectedValue              self.model.removeAddressByName(key)                     #Get the index of the item before this one                     #unless the index of the item we removed is 0.                     #Then select the index.              if index-1 < 0: index = 0              else: index = index -1              self.addresses.selectedIndex = index                     #Set the form to the current address.              key = self.addresses.selectedValue              address = self.model.getAddressByName(key)              self.form.set(address)              ...              ... if __name__ == "__main__":              mainWindow = AddressMain()

The modifications aren't extensive, which is a good thing. An important change is the addition of JTabbedPane to include multiple tabs. Run the code. You should get the frames shown in Figures 16-6 and 16-7. Try these exercises:

  • Add an address to the address book. Then check that the list and table views (JList and JTable) were updated.

  • Remove an address from the list.

  • Add an optional Web site address to the address book. (That is, make it so each entry has an optional Web page URL.)

Figure 16-6. Address Book Entry Form with JTabbedPane

graphics/16fig06.gif

Figure 16-7. Address List with JTabbedPane

graphics/16fig07.gif

If you're feeling pretty sure of yourself, try reimplementing the complete address book application in Java.

JTree

Breaking things into hierarchies is natural. Think of your computer's file system, which employs drives, directories, subdirectories, folders, and files to keep things organized. With JTree we can create hierarchical organizations for any sort of graphical display.

The best way to describe JTree is as a cross between a list and an organizational chart. We're going to illustrate it first with a simple example and then with examples that increase in complexity (and thus become more realistic).

JTree's Constructor

There are several versions of the JTree constructor one takes a hashtable as a parameter; others use vectors, tree models, tree nodes, and arrays. We're going to start off with the hashtable version and add some objects to it. Don't be scared. A hashtable is much like a Python built-in dictionary object in early versions of Jython; you can use the two interchangeably.

Create the hashtable.

>>> from java.util import Hashtable >>> dict = Hashtable()

Create some sample objects.

>>> from address import Address >>> rick = Address("Rick Hightower", "555-1212", "r@r.cos") >>> bob = "Bob DeAnna" >>> Kiley = "Kiley Hightower"

Add the sample objects to the dictionary.

>>> dict['Rick'] = rick >>> dict['Bob'] = bob >>> dict['Kiley'] = Kiley

Now that we have some data in a form that we can pass to a JTree constructor, we can create the actual tree.

Import JTree, and create an instance of it, passing the hashtable (dict) as a parameter.

>>> from javax.swing import JTree >>> tree = JTree(dict)

Add the tree to the frame, and show it.

>>> from javax.swing import JFrame >>> frame = JFrame("Tree Test") >>> frame.contentPane.add(tree) javax.swing.JTree[,0,0,0x0,invalid,... >>> frame.pack() >>> frame.visible=1

The frame should look like Figure 16-8. Seems more like a list than a tree, does it not? We'll fix this later when we define our own model.

Figure 16-8. A Simple JTree Tree

graphics/16fig08.gif

The Tree Model

Like JList and JTable, JTree uses JFC's MVC architecture, which means that every tree has a model. You don't have to specify a model because JTree does it for you with DefaultTreeModel, from javax.swing.tree.

>>> tree.model.class <jclass javax.swing.tree.DefaultTreeModel at 9727>

The tree model is made up of tree nodes. The TreeNode class implements the TreeNode interface. Get the root node, and see what class it is.

>>> root = tree.model.getRoot() >>> root.class <jclass javax.swing.tree.DefaultMutableTreeNode...>

Check that the root implements the TreeNode interface.

>>> root.class.interfaces array([<jclass java.lang.Cloneable at 95880612>, <jclass javax.swing.tree.MutableTreeNode at -509409883>, <jclass java.io.Serializable at 1612121508>], java.lang.Class)

The actual interface implemented is MutableTreeNode, which is the second element in the array. Here's how to see if MutableTreeNode implements TreeNode:

>>> root.class.interfaces[1].interfaces array([<jclass javax.swing.tree.TreeNode at -277936731>], java.lang.Class)

A faster way to do this is with the isinstance() built-in function. As we can see, isinstance() returns a 1 (true) value, so root is indeed an instance of TreeNode.

>>> isinstance(root, TreeNode) 1

A leaf is a node that can have no children. You can check if a node is a leaf like this:

>>> tree.model.isLeaf(root) 0

The 0 (false) value tells the story.

Now we'll iterate through the root node and print out the string representation of the child nodes (we'll also find out if they're actually leaves).

>>> count = tree.model.getChildCount(root) >>> for index in xrange(0,count): ...     child_node = tree.model.getChild(root, index) ...     print child_node ...     print 'isLeaf ' + `tree.model.isLeaf(child_node)` ... Kiley isLeaf 1 Rick isLeaf 1 Bob isLeaf 1

The JTree Model Interface

To create our tree model we have to implement the TreeModel interface, which has the following methods:

  • addTreeModelListener(listener) allows JTree to subscribe to tree model events

  • removeTreeModelListener(listener) removes the event listener

  • getChild(parent,index) returns the child of the parent at a given index

  • getChildCount(parent) returns the child count of the specified parent node

  • getIndexOfChild(parent, child) returns the index of the child in the parent

  • getRoot() returns the root of the tree

  • isLeaf(node) checks to see if the node is a leaf

  • ValueForPathChanged(path, newValue) an abstract method

getRoot() and getChild() return instances of java.lang.Object, which are often actual tree nodes.

TreeNode defines the interface for the nodes, which is one of the reasons that JTree is so complex it's like a model within a model. Here are its methods:

  • children() returns an enumeration of children

  • getAllowsChildren() returns if the node allows child nodes

  • getChildAt() gets the child at the specified index

  • getChildCount() returns the count of children in a specified node

  • getIndex(node) returns the index of the given child node

  • getParent() returns the parent of a specified child node

  • isLeaf() returns if a specified node is a leaf

The children() method requires that TreeNode return to it an instance of java.util.Enumeration, which must implement these methods:

  • hasMoreElements() returns true if there are more elements in the enumeration

  • nextElement() returns the next element in the collection

A short example will illustrate how tree nodes can contain other tree nodes. Our tree model contains instances of javax.swing.tree.TreeNode. It doesn't have to, but it's not a bad idea. We'll define three classes:

  • ListEnumeration implements the java.util.Enumeration interface

  • SimpleNode implements the javax.swing.tree.TreeNode interface

  • SimpleModel implements the javax.swing.tree.TreeModel interface

ListEnumeration

The ListEnumeration class helps develop a simple tree node. In its code, you'll notice that the children() method returns an enumeration more precisely, an instance of java.util.Enumeration which provides a standard way to enumerate over a collection of elements while abstracting the collection type.

Here's the code (from TreeModel1\ListEnumeration.py):

from java.util import Enumeration class ListEnumeration(Enumeration):        def __init__(self, the_list):               self.list = the_list[:]               self.count = len(self.list)               self.index = 0        def hasMoreElements(self):               return self.index < self.count        def nextElement(self):               object = self.list[self.index]               self.index = self.index + 1               return object

Let's break it down interactively. Import ListEnumeration.

>>> from ListEnumeration import ListEnumeration

Create a list containing three integers.

>>> mylist = [1,2,3]

Show that the list has been created.

>>> mylist.__class__ <jclass org.python.core.PyList at -992650171>

Create a ListEnumeration object, passing it mylist as an argument to the constructor.

>>> enum = ListEnumeration(mylist) >>> enum.__class__ <class ListEnumeration.ListEnumeration at -736797627>

Iterate through the list.

>>> while(enum.hasMoreElements()): ...     print enum.nextElement() ... 1 2 3
SimpleNode

SimpleNode implements TreeNode and can be used alone or with JTree. Like other classes that implement TreeNode, it's sort of a model without events. SimpleNode has a parent node, a list of child nodes (which are of type SimpleNode as well), and a property that determines whether a given node is a leaf. Its constructor optionally creates leaf nodes and adds them to its node list.

Here's the code (from TreeModel1\SimpleNode.py):

from javax.swing.tree import TreeNode from ListEnumeration import ListEnumeration from java.lang import Object class SimpleNode (Object, TreeNode):        def __init__(self, name, items=[], parent=None, leaf=0):              self.__nodes = []              self.__name = name              self.__parent = parent              self.__leaf=leaf              for name in items:                    node = SampleNode(name, parent=self, leaf=1)                    self.__nodes.append(node)        def getChildAt(self, index):              "Get the child at the given index"              return self.__nodes[index]        def children(self):              'get children nodes '              return ListEnumeration(self.__nodes)        def getAllowsChildren(self):              'Does this node allow children node?'              return not self.leaf        def getChildCount(self):              'child count of this node'              return len (self.__nodes)        def getIndex(self, node):              'get index of node in nodes list'              try:                    return self.__nodes.index(node)              except ValueError, e:                    return None        def getParent(self):              'get parent node'              return self.__parent        def isLeaf(self):              'is leaf node'              return self.__leaf

In addition to all of TreeNode's methods, SimpleNode implements its own:

  • __str__() displays the node as a Python string

  • toString() displays the node as a Java print string

  • __repr__() displays the node as a string for debugging

  • add() adds a SimpleNode child to the list of nodes

  • setParent(parent) sets the parent of a specified node

  • getName() gets the name of a specified node

  • setName() sets the name of a specified node

For toString() to function, SimpleNode has to extend java.lang.Object, which defines this method. I hope all this will take is defining __str__ in future Jython releases.

Here's part of the SimpleNode class showing its helper methods:

def __str__(self):        'str node'        return self.__name def toString(self):        return self.__str__() def __repr__(self):        nodes = []        for node in self.__nodes:              nodes.append(str(node))        if (self.__parent):              tpl=(self.__name, nodes, self.__parent, self.__leaf)              return 'SampleNode(name=%s,list=%s,parent=%s,leaf=%s)' % tpl        else:              tpl=(self.__name, nodes, self.__leaf)              return 'SampleNode(name=%s,list=%s,leaf=%s)' % tpl def add(self, node):        self.__nodes.append(node)        node.setParent(self) def setParent(self, parent):        self.__parent = parent def setName(self, name):        self.__name=name def getName(self, name):        return self.__name

The best way to define SimpleNode is to show it building a tree-like structure of nodes.

Create a parent node.

>>> parent = SimpleNode("Dick & Mary")

Create three child nodes: child1, child2, and child3 with their spouses. Pass a list of offspring to each node.

>>> child1 = SimpleNode("Rick & Kiley", ["Whitney"]) >>> child2 = SimpleNode("Martha & Miguel", ["Alex", "Nicholai", "Marcus"]) >>> child3 = SimpleNode("Missy & Adam", ["Mary", "Sarah"])

Add the children to the parent node.

>>> parent.add(child1) >>> parent.add(child2) >>> parent.add(child3)

Iterate through the children in the parent node showing their names.

>>> child = parent.children() >>> children = parent.children() >>> while(children.hasMoreElements()): ...     print children.nextElement() ... Rick & Kiley Martha & Miguel Missy & Adam

Here's an easier version of this example, using the for loop statement to handle the enumerators:

>>> child = parent.children() >>> children = parent.children() >>> for child in children: ...     print child ... Rick & Kiley Martha & Miguel Missy & Adam

Now let's make the parent tree node a tree model. Import the GUI components that are needed.

>>> from javax.swing import JTree, JScrollPane, JFrame

Create a JTree instance, passing the parent node as the argument to the JTree constructor.

>>> tree = JTree(parent)

Create a frame, and add the JTree instance to it. Pack the frame, and make it visible.

>>> frame = JFrame('SimpleNode test') >>> frame.contentPane.add(JScrollPane(tree)) >>> frame.pack() >>> frame.show()

Your end result should look like Figure 16-9.

Figure 16-9. A Family Tree

graphics/16fig09.gif

Sharing a Data Model among JTree Views

If the tree node works so well, why do we need a model? I can almost hear you asking this question. For the answer, think what will happen if child1, Kiley, has a second child.

>>> child1 SimpleNode(name=Rick & Kiley,list=['Whitney', 'Rick JR.'],parent=Dick & Mary,leaf=0)

To make this clearer, we can write this:

>>> kiley = child1

Create the baby node.

>>> baby = SimpleNode("Rick JR.") >>> kiley.add(baby)

Show Kiley's children.

>>> children = kiley.children() >>> for child in children: ...     print child ... Whitney Rick JR.

If you look at Kiley's children in the JTree node, you'll see that it's out of sync. Worse yet, it will be out of sync with any other Jtree you create. That's why we need models. Unlike nodes, they keep the data and the view synchronized.

SampleModel

Deriving your nodes from javax.swing.tree.TreeNode isn't mandatory if you define your own model. The TreeModel interface abstracts a node, which can therefore derive from java.lang.Object and implement no interfaces at all. TreeModel can have an ID of a database record as a member, so, instead of reading memory out of a Python list, you can get data out of the database via the associated tree model.

Models keep data and views in sync. The question is how. I'll show you, with a tree model I created called SampleModel.

Import SampleModel from the SampleModel.py module.

>>> from SampleModel import SampleModel

Create an instance of SampleModel by passing its constructor the name of the root node.

>>> tree_model = SampleModel("Dick & Mary")

Add the two branches (nodes) of the tree off of the root. The addNode() method returns the node created.

>>> tree_model.addNode("Martha & Miguel", ["Alex", "Nicholai", "Marcus"]) >>> tree_model.addNode("Missy & Adam", ["Mary", "Sarah"])

Create the last branch, and save it. Kiley is a handle we'll use later.

>>> Kiley=tree_model.addNode("Rick & Kiley", ["Whitney"])

Notice that we haven't called any methods on a node object. Instead, we've dealt directly with the model, which encapsulates node manipulation.

Now let's create some views for our model. Import all JFC components that are needed.

>>> from javax.swing import JFrame, JTree, JScrollPane

Create the first view.

>>> frame1 = JFrame("View 1 -No Scroll") >>> tree = JTree(tree_model) >>> frame1.contentPane.add(tree) >>> frame1.pack() >>> frame1.show()

Create the second view.

>>> frame2 = JFrame("View 2 -With Scroll") >>> tree2 = JTree(tree_model) >>> frame2.contentPane.add(JScrollPane(tree2)) >>> frame2.pack() >>> frame2.show()

Expand the Rick & Kiley node in both views. Your result should be the two views in Figure 16-10.

Figure 16-10. Two SampleModel Views

graphics/16fig10.gif

Now for the true litmus test. Will both views be changed if the family tree changes; that is, will the views be synchronized with the data? As before, we'll say that Kiley has a second child, so we need to add a node to Rick & Kiley.

>>> tree_model.addNode('Rick JR.', parent=Kiley)

Figure 16-11 shows that the new node was added to both views.

Figure 16-11. A Blessed Event for Views 1 and 2

graphics/16fig11.gif

Event Notification

Just like the table model, the tree model keeps a list of listeners, all of which are notified when the model data changes. Since TreeModel controls access to the nodes, we can add code to it, in the addNode() method, to fire a notification event. Here's the addNode() method with its helpers:

def addNode(self, name, children=[], parent=None):              # Set the value of the leaf.              # No children means the node is a leaf.        leaf = len(children)==0              # Create a SampleNode,              # and add the node to the given parent.        node = SampleNode(name, children, leaf=leaf)        self.__add(node, parent)        return node

addNode() creates an instance of SampleNode and passes its constructor the addNode() arguments. If the children argument is empty, the instance is set as a leaf. addNode() calls the __add() helper method and then returns the node so it can be used as a handle to other model methods. Here's the code for __add():

def __add(self, node, parent=None):              # If the parent is none,              # then set the parent to the root.        if not parent:              parent = self.getRoot()              # Add the node to the parent,              # and notify the world that the node changed.        parent.add(node)        self.fireStructureChanged(parent)

__add() adds the given node to the given parent. If the parent is None, the root node becomes the default parent. Adding the node to the parent invokes the fireStructureChanged event, which notifies all listeners (that is, views). Here's the code:

def fireStructureChanged(self, node):              # Get the path to the root node.              # Create a TreeModelEvent class instance.        path = self.getNodePathToRoot(node)        event = TreeModelEvent(self, path)              # Notify every tree model listener that              # this tree model changed at the tree path.        for listener in self.listeners:              listener.treeStructureChanged(event)

fireStuctureChanged iterates through the listener list and calls each listener's treeStructureChanged() method, passing a TreeModelEvent instance. To create the instance, TreeModelEvent's constructor needs a path a list of nodes from the root to the changed node. This is how the view notification is carried out and thus how views stay in sync with the data model.

Get the path with the getNodePathToRoot() method.

def getNodePathToRoot(self, node):        parent = node # Holds the current node.        path=[]       # To hold the path to root.                      # Get the path to the root        while not parent is None:                      # Add the parent to the path, and then get the                      # parent's parent.              path.append(parent)              parent = parent.getParent()              #Switch the order        path.reverse()        return path

This event does just what is says it does: it gets the node's path to the root. It should be obvious from the comments how it works.

Additional Tree Model Methods

The rest of the tree model is somewhat boring. Two methods add and remove listeners, and several other methods delegate responsibility for specific tasks to their corresponding SimpleNode instances.

class SampleModel(TreeModel):        def __init__(self, root_name):             root = SampleNode(root_name, [])             self._root = root             self.listeners = [] # to hold TreeModel listeners             # - The following methods implement the TreeModel interface.        def addTreeModelListener(self, listener):             self.listeners.append(listener)        def removeTreeModelListener(self, listener):             self.listeners.remove(listener)        def getChild(self, parent, index):             return parent.getChildAt(index)        def getChildCount(self, parent):             return parent.getChildCount()        def getIndexOfChild(self, parent, child):             return parent.getIndex(child)        def getRoot(self):             return self._root        def isLeaf(self, node):             return node.isLeaf()        def valueForPathChanged(self, path, newValue):             node = path.getLastPathComponent()             node.setName(newValue)        ...        ...

You can see that most of the work is done by the underlying tree nodes and the noninterface methods.

DefaultMutableTreeNode and DefaultTreeModel

Another way to use JTree is with DefaultMutableTreeNode and DefaultTreeModel. Once you understand TreeModel, these classes are easy. Look them up in the Java API documentation. As an exercise, use them to implement the last interactive session.

Handling JTree Events

JTree publishes the following event properties:

  • treeExpanded a node has expanded; associated with javax.swing.event.TreeExpansionListener and passed an instance of java.swing.event.TreeExpansionEvent

  • treeCollapsed a node has collapsed; associated with javax.swing.event.TreeExpansionListener and passed an instance of javax.swing.event.TreeExpansionEvent

  • treeWillExpand a node will expand (used to fetch data, as needed, into the tree model); associated with javax.swing.event.TreeWillExpandListener and passed an instance of javax.swing.eventTreeExpansionEvent

  • treeWillCollapse a node will collapse; associated with javax.swing.event.TreeWillExpandListener and passed an instance of javax.swing.event.TreeExpansionEvent

  • valueChanged a new node was selected; associated with javax.swing.event.TreeSelectionListener and passed an instance of javax.swing.event.TreeSelectionEvent

A little confusing? Let's look at an example that uses the event mechanism to put these properties to work. It's from TreeModel1\TreeEvents.py.

from SampleModel import SampleModel from javax.swing import JFrame, JTree, JScrollPane def handleTreeExpanded(event):       global g_event       print "Tree Expanded"       showPath(event.path)       g_event = event def handleTreeCollapsed(event):       global g_event       print "Tree Collapsed"       showPath(event.path)       g_event = event def handleTreeWillExpand(event):       global g_event       print "Tree Will Expand"       showPath(event.path)       g_event = event def handleTreeWillCollapse(event):       global g_event       print "Tree Will Collapse"       showPath(event.path)       g_event = event def handleValueChanged(event):       global g_event       print "Value Changed"       showPath(event.path)       g_event = event def showPath(treePath):       path = ""       count = treePath.pathCount       for index in range(0,count):             node = treePath.getPathComponent(index)             path = path + "-> [" + str(node) + "]"       print path tree_model = SampleModel("Dick & Mary") tree_model.addNode("Martha & Miguel", ["Alex", "Nicholai", "Marcus"]) tree_model.addNode("Missy & Adam", ["Mary", "Sarah"]) Kiley=tree_model.addNode("Rick & Kiley", ["Whitney"]) frame = JFrame("Tree Events") tree = JTree(tree_model) frame.contentPane.add(tree) frame.pack() frame.show()       # A node in the tree expanded. tree.treeExpanded = handleTreeExpanded       # A node in the tree collapsed. tree.treeCollapsed = handleTreeCollapsed       # A node in the tree will expand. tree.treeWillExpand = handleTreeWillExpand       # A node in the tree will collapse. tree.treeWillCollapse = handleTreeWillCollapse       # A new node was selected. tree.valueChanged = handleValueChanged

Notice that there's an event handler for every possible event. The handler prints out the path of the event using the tree path associated with it. It also copies the last handler to a global variable, g_event.

TreeEvents.py Example

Let's do an interactive session, first typing this at the system prompt:

C:\jython_book\scripts\chap16\TreeModel1>jython -i TreeEvents.py

Expand the first node to get the following output:

Tree Will Expand -> [Dick & Mary]-> [Martha & Miguel] Tree Expanded ->[Dick & Mary]-> [Martha & Miguel]

Select the first leaf in the first node to get this output:

Value Changed -> [Dick & Mary]-> [Martha & Miguel]-> [Alex]

Collapse the first node.

Tree Will Collapse -> [Dick & Mary]-> [Martha & Miguel] Tree Collapsed -> [Dick & Mary]-> [Martha & Miguel]

That was how to get the whole path, but our only interest is in the last node. We can get that by inspecting the last event, which is stored in g_event.

The event has a property called path, which is an instance of TreePath.

>>> g_event.path.class <jclass javax.swing.tree.TreePath at -1128707474>

TreePath has a method called getLastPathComponent().

>>> dir (g_event.path.class) ['getParentPath', 'path', 'getPathComponent', 'pathCount', 'pathByAddingChild', 'getPath', 'isDescendant', 'getPathCount', 'lastPathComponent', 'parentPath', 'getLastPathComponent']

getLastPathComponent() returns the node selected.

    >>> g_event.path.getLastPathComponent() SampleNode(name=Martha & Miguel,list=['Alex', 'Nicholai', 'Marcus'],     parent=Dick& Mary,leaf=0)

That about covers tree view events. To make sure you understand them, try these exercises:

  • Add a JTree component to the address book example that replaces the JList component. Group the addresses by category business, personal, and so forth. JTree will show the category and all of its addresses. Clicking on a particular category should show all of its items in the table. Clicking on a particular address should show that address in the entry form.

  • Modify the tree so that categories and subcategories can be added to it.

JToolBar and Actions

We've improved the address book application with the addition of JTabbedPane, JTable, and (if you did the exercises) JTree. Still, something's missing.

In the early days of Java there was no Swing, and AWT didn't have a toolbar. This meant that most developers used panels, which is what I did in the address book application. I had two reasons for this: to show how JPanel and the BorderLayout layout manager work and to avoid introducing actions.

Actions

We can avoid actions no longer. Any introduction to JToolBar has to include them. I held off until now because actions are a higher abstraction than components, and I thought you should have a good grip on components first.

Think of an action as a command. An item on a menu that says "remove address" is one example; another is a button on a toolbar that says the same thing. The main difference is that an action added to a toolbar usually becomes a button and an action added to a menu usually becomes an item.

The following code, from our third iteration of the address book application in this chapter, uses javax.swing.JToolBar and javax.swing.AbstractAction to show how actions work. Notice that the actions created are used by both a popup menu and a menubar menu, which makes the code much shorter.

First we define two classes that extend AbstractAction: one for adding and one for removing addresses.

class Add (AbstractAction):        def __init__(self, this):              self.this = this              AbstractAction.__init__(self, "Add")        def actionPerformed(self, event):              self.this.addAddress_Clicked() class Remove (AbstractAction):        def __init__(self, this):              self.this = this              AbstractAction.__init__(self, "Remove")        def actionPerformed(self, event):              self.this.removeAddress_Clicked()

As you can see, both classes extend AbstractAction, and the only method they have to override is actionPerformed(). The Remove and Add actions respectively delegate calls to the AddressMain classes removeAddress and addAddress.

Next we instantiate the actions and add them to the toolbar. (Later they'll be added to the menubar and popup menus.)

       #Instantiate the add and remove actions. addAction = Add(self) removeAction = Remove(self)        # Create the toolbar panel, and        # add it to the North border        # of the container. toolbar = JToolBar() self.contentPane.add(toolbar, BorderLayout.NORTH)        #Add the actions to the toolbar. toolbar.add(addAction) toolbar.add(removeAction)

This code is much smaller than the code for creating menu items and buttons. Since we're using JToolBar instead of JPanel, we get the additional capability of moving the toolbar at runtime, as illustrated in Figures 16-12, 16-13, and 16-14.

Figure 16-12. Main Frame with the Toolbar at the Top

graphics/16fig12.gif

Figure 16-13. Main Frame with the Toolbar at the Left

graphics/16fig13.gif

Figure 16-14. Main Frame with a Floating Toolbar

graphics/16fig14.gif

Icons

With Action in general and AbstractAction in particular, we can easily add support for icons. I did it with the following code:

class Add (AbstractAction):        def __init__(self, this):              self.this = this              icon = ImageIcon("./images/add.jpg")              AbstractAction.__init__(self, "Add", icon)        def actionPerformed(self, event):              self.this.addAddress_Clicked() class Remove (AbstractAction):        def __init__(self, this):              self.this = this              icon = ImageIcon("./images/remove.jpg")              AbstractAction.__init__(self, "Remove", icon)     def actionPerformed(self, event):              self.this.removeAddress_Clicked()

The result is shown in Figure 16-15.

Figure 16-15. Main Frame Toolbar with Icons

graphics/16fig15.gif

Try these exercises:

  • Change the paint program from the last chapter to use JToolBar for both the shape toolbar and the color/fill panel. For extra credit, add icons for the shape buttons.

  • Add mnemonics and tooltips to all of the components in the address book application.

Summary

In this chapter, we explored advanced Swing. We showed an example that employed the JFC components JTable, JTree, JToolBar, and JTabbedPane, and we discussed JTable and JTree.

We extended the address book application to use JTable, which led to a discussion of TableModel and TreeModel. Later we changed menus and toolbars to use actions instead of buttons and menu items.

With what you learned in this and the last three chapters, you should be able to create your own Swing-based applications.

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