How to add nodes and create a multilevel hierarchy for an AgcJTree control.
You can run this technique code from:
NOTE First make sure that database is running on your localhost SilverStream Server | |
See the chapter on programming forms in the Programmer's Guide |
The frmJTreeLoadingData example has three AgcJTree controls that each load data using different techniques:
To work with the classes of the tree data model, include this import statement in your form:
import javax.swing.tree.*;
The treeLoaded control has a list of first level values created by specifying a static list in the Property Inspector. Then code called from the formLoaded event adds child values to each first-level node from a static array. The sample values are types of animals (the first-level values) and specific animals (the second-level values).
The sample code produces a tree control whose values look like this:
Mammals Birds Reptiles Amphibians Insects
The array of data for the child nodes is declares as an instance variable:
// 2D array of data, the subarrays correspond to the animal types // specified in Designer String value[] [] = { { "cat", "dog", "agouti" }, // mammals { "duck", "goose", "egret" }, // birds { "iguana", "basilisk", "skink" }, // reptiles { "treefrog", "toad", "peeper" }, // amphibians { "mosquito", "greenhead fly", "moose fly" } // Insects };
This code in the user-defined method setup_treeLoaded()
called from the formLoaded event adds child values to each of the first-level values:
int count; DefaultMutableTreeNode root, parentNode; DefaultTreeModel model; model = (DefaultTreeModel) tree.getModel(); root = (DefaultMutableTreeNode) model.getRoot(); // For each 1st-level node, specify that it allows children; // then add the children from the sTreeDetail array. count = root.getChildCount(); for (int i = 0; i < count; i++) { parentNode = (DefaultMutableTreeNode) root.getChildAt(i); parentNode.setAllowsChildren(true); for (int j = 0; j < sTreeDetail[i].length; j++) { parentNode.add( new DefaultMutableTreeNode( sTreeDetail[i][j], true) ); } }
tree
argument passed to setup_TreeLoaded()
is the treeLoaded control.
tree.getModel()
gets a reference to the model and model.getRoot()
gets a reference to the root node. root.getChildCount()
finds out how many children the root node has.
setAllowsChildren(true)
so that you can add the second level of values.
parentNode.add( new DefaultMutableTreeNode( value[i][j], false) )
constructs a new node and adds it as a child of parentNode. The value false
specifies that the new second-level node does not allow children.
In the treeExpandable control, the same data values populate the tree, but the child nodes are not added until the user tries to expand one of the first-level nodes. This is accomplished by implementing a TreeWillExpandListener and writing code in the treeWillExpand event that adds child values when necessary.
The treeWillExpand event occurs after a user requests that the node expand and before the expanded nodes are visible. It is not automatically included on the form. To add it, you implement the TreeWillExpandListener interface. SilverStream can generate stubs for interface methods when you use this procedure:
javax.swing.event.TreeWillExpandListener
. Select the check box Create stubs for interface methods.
setup_treeExpandable()
method, called from formLoaded.
// Add the TreeWillExpandListener to the form. tree.addTreeWillExpandListener(this);
The example doesn't add child nodes during the setup phase, but it does need to specify that each parent node allows children so that a treeWillExpand event will occur. This code in setup_treeExpandable()
loops over the nodes added in the Designer and calls setAllowsChildren(true)
for each.
int count; DefaultMutableTreeNode root, parentNode; DefaultTreeModel model; model = (DefaultTreeModel) tree.getModel(); root = (DefaultMutableTreeNode) model.getRoot(); // For each 1st-level node, specify that it allows children. count = root.getChildCount(); for (int i = 0; i < count; i++) { parentNode = (DefaultMutableTreeNode) root.getChildAt(i); parentNode.setAllowsChildren(true); }
The instance variable sTreeDetail is a 2D array that holds the data for the child nodes. For the treeExpandable control, when the user clicks on a parent node, the value of that node is the hashtable key.
This is the declaration of the 2D array of Strings (also used for the treeLoaded control):
// Array of data for child nodes; // order of subarrays matches order of 1st-level // values specified in Designer. String sTreeDetail[] [] = { { "cat", "dog", "agouti" }, // mammals { "duck", "goose", "egret" }, // birds { "iguana", "basilisk", "skink" }, // reptiles { "treefrog", "toad", "peeper" }, // amphibians { "mosquito", "greenhead fly", "moose fly" } // insects };
This code in setup_treeExpandable()
, called from formLoaded, populates the hashtable:
hshTreeDetail.put("Mammals", sTreeDetail[0] ); hshTreeDetail.put("Birds", sTreeDetail[1] ); hshTreeDetail.put("Reptiles", sTreeDetail[2] ); hshTreeDetail.put("Amphibians", sTreeDetail[3] ); hshTreeDetail.put("Insects", sTreeDetail[4] );
The TreeExpansionEvent object provides information about the node being expanded in a TreePath object. This code analyzes the TreePath to determine whether the node needs child nodes, and if so, adds them:
public void treeWillExpand(javax.swing.event.TreeExpansionEvent evt) throws javax.swing.tree.ExpandVetoException { if (evt.getSource() != treeExpandable) return; TreePath tp = evt.getPath(); // Only add child nodes to 1st-level nodes if (tp.getPathCount() != 2) return; // Get the node the user clicked. DefaultMutableTreeNode parentNode = (DefaultMutableTreeNode) tp.getLastPathComponent(); // if it already has children (has been expanded), return. if (parentNode.getChildCount() > 0) return; // If this node has no children (never expanded), add children. // Get array of child node strings from hashtable // using parent node value as key. String value[] = (String[]) hshTreeDetail.get( parentNode.toString() ); for (int j = 0; j < value.length; j++) { // add each child node so that it does not allow children parentNode.add(new DefaultMutableTreeNode(value[j], false)); } }
evt.getSource()
returns one of the other tree controls, the event handler returns.
getPath()
and checks if the path is two nodes long (root node plus first-level node). If not, it returns.
getLastPathComponent()
and finds out if it has children with getChildCount()
.
get()
. The parent node value is the key for the hashtable lookup. The values stored in the hashtable are String arrays.
add()
method creates child nodes using the values in the returned array.
You can populate the first level of a tree from any row cursor. Other controls, such as views, can share and display the same data as the tree control. Changes to the data are also shared between controls using the same row cursor.
In this example, the form includes an AgcData object called dataDepts, which is associated with a table called departments. It has two column expressions: DeptID and DeptName. The node's display value is the name (expression 1) and the storage value is the ID (expression 0). The following code is in user-defined method setup_treeRowCursor()
, called from the formLoaded event:
try {
tree.loadFromRowCursor((AgiRowCursor)dataDepts, 1, 0);
}
catch (Exception e) {
agDialog.showMessage("Failed to load tree from row cursor. "
+ e.toString() );
}
// Add a second level of nodes
addLevel
(tree);
loadFromRowCursor()
, you must catch exceptions in a try/catch block.
loadFromRowCursor()
method are the column expressions for display and storage values. Internally, these are combined into an AgoDisplayValue object.
The root node is visible after you call loadFromRowCursor()
. You can give it a value with the setUserObject()
method and call nodeChanged()
to notify the model of the change.
// Get the root node and specify meaningful text. DefaultTreeModel model = (DefaultTreeModel) tree.getModel(); DefaultMutableTreeNode root = (DefaultMutableTreeNode) model.getRoot(); root.setUserObject("Departments"); model.nodeChanged(root);
loadFromRowCursor()
because the loading creates a new data model object. You need to make the change in the new model and root node.
toString()
method that can be displayed in the tree.
nodeChanged()
to notify the model to update the node's display.
loadFromRowCursor()
it is visible. Instead of setting the root node's text, you could choose to hide the node:
tree.setRootVisible(false);
setRootVisible()
is a method of JTree, not of the model.
The treeRowCursor control calls two user-defined methods to populate the second level of data: addLevel()
and populate()
. After populating the first level from the row cursor, you can add additional levels from another AgcData control.
Looping over the first-level nodes
The addLevel()
method is called in the formLoaded event after loadFromRowCursor()
. addLevel()
gets each parent node value (a department ID) and calls populate()
to create the child nodes for the department.
public void addLevel
(AgcJTree tree)
{
DefaultTreeModel model;
DefaultMutableTreeNode root, parentNode;
Integer value;
int count;
// get model
model = (DefaultTreeModel) tree.getModel();
// get root
root = (DefaultMutableTreeNode) model.getRoot();
// get the number of categories at the first level
count = root.getChildCount();
for (int i=0; i < count; i++)
{
// get a department node and extract the ID value
parentNode = (DefaultMutableTreeNode) root.getChildAt(i);
parentNode.setAllowsChildren(true);
value = (Integer) ((AgoDisplayValue)
parentNode.getUserObject()).getUserData();
populate(parentNode, value);
}
}
loadFromRowCursor()
, the nodes it creates already allow children; setAllowsChildren(true)
is not necessary.
getUserData()
method gets the storage value; in this case, an Integer.
value = (Integer) ((AgoDisplayValue)parentNode.getUserObject()).getUserData();
AgoDisplayValue has a toString()
method that returns the display value.
Adding the child nodes
The AgcData control that retrieves the second-level data is called dataKeywords and has these column expressions: EmpID, EmpFirstName, EmpLastName, and DeptID. The DeptID value has a foreign key relationship with the Department data at the first level.
The user-defined method populateDept()
is called from addLevel()
:
public void populateDept(DefaultMutableTreeNode parentNode, Integer value) { DefaultMutableTreeNode childNode; try { String where; dataEmployees.query("employees.deptid=" + value); // if no employees found for dept, end process if (!dataEmployees.gotoFirst()) return; // loop over keywords and add as child nodes of category while (dataEmployees.gotoNext()) { childNode = new DefaultMutableTreeNode ( new AgoDisplayValue( (String)dataEmployees.getProperty(1) + " " + dataEmployees.getProperty(2), dataEmployees.getProperty(0) ) ); parentNode.add(childNode); } } catch (Exception e) { System.out.println("populateDept: " + e); } }
parentNode
argument for the populateDept()
method is a department ID, which is used in a Where clause.
query()
, gotoFirst()
, and gotoNext()
are in a try/catch block.
new AgoDisplayValue( (String)dataEmployees.getProperty(1) + (String)dataEmployees.getProperty(2), dataEmployees.getProperty(0) )
add()
method puts the child node at the end of the parent's child array. The DefaultMutableTreeNode class has other methods that let you control the position of new nodes.