CONTENTS |
Terms in This Chapter
|
|
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.
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.
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.
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.
>>> 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.
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 ReviewWe'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 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.
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)
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.
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 JFCThe 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. |
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.
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.
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()
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 MoreSometimes 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 MethodsMost 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. |
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.
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)
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)
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.
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.
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.
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.
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.
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.
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.
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.
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.)
If you're feeling pretty sure of yourself, try reimplementing the complete address book application in Java.
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).
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.
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
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
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 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.
Sharing a Data Model among JTree ViewsIf 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. |
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.
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.
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.
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 DefaultTreeModelAnother 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. |
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.
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.
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.
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.
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.
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.
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 |