这是一篇不错的文章,细细读来还是很有帮助!等有时间了翻译一下与大家共享!
How to use the JFace Tree Viewer
Summary
The goal of this article is to teach you how to use TreeViewers in your Eclipse plug-ins or stand-alone JFace/SWT applications. We’ll start with a simple example and progressively add functionality.By Chris Grindstaff, Applied Reasoning (chrisg at appliedReasoning.com)
May 5, 2002
The TreeViewer is used throughout Eclipse. In fact, trees are pervasive in most applications. This article will explain the following information about tree viewers:
- how the tree viewer fits into the JFace UI toolkit
- how to populate a tree viewer
- how to listen for events generated by the tree viewer
- how to select items in the tree viewer
- how to filter out items
- how to control the order of items in the tree viewer.
The tree viewer can be used in Eclipse plug-ins or in a stand alone JFace/SWT application. We’ve chosen to implement a plug-in that demonstrates the tree viewer functionality.
Here’s an example of what the plugin provides:
As you work through the article you may notice the occasional . These icons indicate points where you should start up Eclipse and try the example.
Source Code
To run the examples or view the source for this article, unzip cbg.article.treeviewer.zip into your eclipse_root directory and restart Eclipse. In Windows the eclipse_root directory will look something like d:/apps/eclipse/ . Once Eclipse is restarted, select the Perspective | Show View | Other… menu option. In the “Other” category select “Moving Box.”
This article will be most effective if you are able to run the plugin we’ve provided as you read it.
Big Picture
To understand how to use the TreeViewer, it is important to understand where the TreeViewer fits into JFace as a whole, and how JFace fits into Eclipse.
JFace is a UI toolkit that helps solve common UI programming tasks. JFace also acts as a bridge between low-level SWT widgets and your domain objects. SWT widgets interact with the host operating system and as such have no knowledge of your domain objects.
One of the ways that JFace bridges the gap between SWT widgets and domain models is through viewers. JFace viewers consist of an SWT widget (e.g. Tree, Table, etc), plus your domain objects. You provide the viewers with the information they need in order to populate the underlying SWT widget. The viewer is able to sort and filter your domain objects, as well as update the widget when your domain objects change.
More information about JFace and SWT can be found in the Javadoc for org.eclipse.jface.viewers, org.eclipse.swt.widgets, org.eclipse.swt.custom and JFace viewers .
Setting the Stage
Because the TreeViewer uses domain objects to populate its Tree, we will create a simple model to be used throughout this article.
Suppose you've bought a new house and it's time to pack up your stuff for the move. To keep all of your books and board games organized you decide to employ a TreeViewer to help you maintain order. (Yes, you're a geek.)
Here are our domain objects:
- MovingBox
A moving box can contain other moving boxes, books, and board games. A moving box may or may not have a name.
- Book
- Board game
Each book and board game has both a title and an author.
Build a Simple TreeViewer
We’ll start with a simple TreeViewer example and build on it throughout the article.
Here’s the code that creates this view depicted earlier.
treeViewer = new TreeViewer(parent);
treeViewer.setContentProvider(new MovingBoxContentProvider());
treeViewer.setLabelProvider(new MovingBoxLabelProvider());
treeViewer.setInput(getInitalInput());
treeViewer.expandAll();
TreeViewer Domain Model Interactions
It is crucial to understand the model/view relationship as utilized by JFace viewers. Conceptually, all viewers perform two primary tasks:
- they help adapt your domain objects into viewable entities
- they provide notifications when the viewable entities are selected or changed through the UI
More specifically, when working with a tree viewer, you use your domain objects as arguments in the API methods. For example, you can add a book to a moving box by calling the TreeViewer.add(aMovingBox, aBook) method. You don’t need to translate your domain objects into UI elements; the tree viewer does that for you. You make your "root" domain object avaliable to the tree viewer by invoking the TreeViewer’s setInput method. Your domain object then becomes the tree viewer’s input.
Likewise, when you ask the tree viewer for the selected objects, it will answer with the domain objects - not the underlying UI resources.
Content Provider
When working with a tree viewer, you need to provide the viewer with information on how to transform your domain object into an item in the UI tree. That is the purpose of an ITreeContentProvider
. One of your domain classes could implement this interface. Instead of "polluting" your domain objects with user interface code, you may want to create another object to fulfill the tree content provider requirements.
Let’s take a look at some of this interface’s methods.
-
public Object[] getElements(Object inputElement)
This is the method invoked by calling the setInput method on the tree viewer. In fact, the getElements method is called only in response to the tree viewer's setInput method and should answer with the appropriate domain objects of the inputElement. The getElements and getChildren methods operate in a similar way. Depending on your domain objects, you may have the getElements simply return the result of calling getChildren . The two methods are kept distinct because it provides a clean way to differentiate between the root domain object and all other domain objects.
-
public Object[] getChildren(Object parent)
The tree viewer calls its content provider’s getChildren method when it needs to create or display the child elements of the domain object, parent . This method should answer an array of domain objects that represent the unfiltered children of parent (more on filtering later).
-
public Object getParent(Object element)
The tree viewer calls its content provider’s getParent method when it needs to reveal collapsed domain objects programmatically and to set the expanded state of domain objects. This method should answer the parent of the domain object element.
-
public boolean hasChildren(Object element)
The tree viewer asks its content provider if the domain object represented by element has any children. This method is used by the tree viewer to determine whether or not a plus or minus should appear on the tree widget.
-
public void inputChanged
(Viewer viewer, Object oldInput, Object newInput)
This method really serves two slightly different purposes. This method is invoked when you set the tree viewer's input. In other words, anytime you change the tree viewer's input via the setInput method, the inputChanged method will be called. This will often be used to register the content provider as a recipient of domain changes and to de-register itself from the old domain object.
In big picture terms, input objects for the content provider are managed through the viewer.
The method is also called when the tree viewer wants to inform the content provider that it's no longer the tree viewer's content provider. This happens when you set the tree viewer's content provider.
For example, our moving box notifies its listeners whenever a book, board game or box is added to or removed from it. The content provider will register as a listener for these domain model changes so it can update its UI when the domain changes. Likewise, it will want to remove itself as a listener from domain objects that are no longer being viewed.
Enough discussion about the abstract APIs. Let’s take a look at the actual code used in this example. In the code provided above the tree viewer’s content provider was set to an instance of MovingBoxContentProvider
. Let’s take a look at that class in detail.
public Object[] getChildren(Object parentElement) {
if(parentElement instanceof MovingBox) {
MovingBox box = (MovingBox)parentElement;
return concat(box.getBoxes().toArray(),
box.getBooks().toArray(), box.getGames().toArray());
}
return EMPTY_ARRAY;
}
Only MovingBox domain objects can have children, so an empty array is returned for any other object. A moving box’s children are a concatenation of its moving boxes, plus its books, plus its board games.
public Object[] getElements(Object inputElement) {
return getChildren(inputElement);
}
The getElements method is used to obtain the root elements for the tree viewer. In our case, the tree viewer's root object is a moving box so we can simply call the getChildren method since it handles MovingBoxes. If the root domain object was special in some way, the getElement method would likely not delegate to the getChildren method.
public Object getParent(Object element) {
if(element instanceof Model) {
return ((Model)element).getParent();
}
return null;
}
The getParent method is used to obtain the parent of the given element. In our example, the Model class is the common superclass of moving boxes, books and board games and defines a child/parent relationship.
public boolean hasChildren(Object element) {
return getChildren(element).length > 0;
}
The hasChildren method is invoked by the tree viewer when it needs to know whether a given domain object has children.
public void inputChanged(Viewer viewer, Object oldInput, Object newInput) {
this.viewer = (TreeViewer)viewer;
if(oldInput != null) {
removeListenerFrom((MovingBox)oldInput);
}
if(newInput != null) {
addListenerTo((MovingBox)newInput);
}
}
The input changed method caches the viewer argument for later use when responding to events. Recall that the inputChanged method will be called when the tree viewer's setInput method is invoked. When the tree viewer's input is changed, we need to make sure that we remove any listeners we have associated with the old input so that we no longer receive updates from the stale input. Likewise, we need to listen for changes in the new input. Typically the content provider adds itself as a listener to domain object changes. This way when the domain object changes the content provider is notified and can in turn, notify the tree viewer. Here is the addListenerTo method. (The removeListenerFrom method is almost exactly the same so we'll ignore it.)
/** Because the domain model does not have a richer
* listener model, recursively add this listener
* to each child box of the given box. */
protected void addListenerTo(MovingBox box) {
box.addListener(this);
for (Iterator iterator = box.getBoxes().iterator(); iterator.hasNext();) {
MovingBox aBox = (MovingBox) iterator.next();
addListenerTo(aBox);
}
}
This method simply finds all the moving boxes and adds the content viewer as a listener; therefore, if any of the moving boxes change, the content viewer will be notified. Later in the article we'll show how the content viewer responds to these domain changes.
Label Provider
The label provider is responsible for providing an image and text for each item contained in the tree viewer. As with the content provider, the label provider accepts domain objects as its arguments. It is important that instances of label providers are not shared between tree viewers because the label provider will be disposed when the viewer is disposed.
The tree viewer makes use of the ILabelProvider interface to provide these services. Let’s take a look at the interface’s two methods.
public Image getImage(Object element)
This method answers an SWT Image
to be used when displaying the domain object, element . If the domain object does not have a corresponding image, you can answer null. Because images use OS resources you need to be careful to dispose of them when you are no longer using them. This is often accomplished by caching the images in the label provider or the plug-in class and disposing of them when the viewer is disposed.
Another important point to keep in mind is that the tree viewer will scale your images if they are different sizes. The first image returned from this method will be the "standard" tree viewer image size. All other images will be scaled up or down to match the size of this first image. These plus and minus images used in the tree viewer to show expanded and collapsed items are also scaled to the "standard" size.
A safe size to use for your images is 16x16.
public String getText(Object element)
The getText method answers a string that represents the label for the domain object, element. If there is no label for the element, answer null.
public void dispose()
The dispose method is called when the tree viewer that contains the label provider is disposed. This method is often used to dispose of the cached images managed by the receiver.
In the code listed above , the tree viewer’s label provider was set to an instance of MovingBoxLabelProvider
. Let’s take a look at this class in detail.
public Image getImage(Object element) {
ImageDescriptor descriptor = null;
if (element instanceof MovingBox) {
descriptor = TreeViewerPlugin.getImageDescriptor("movingBox.gif");
} else if (element instanceof Book) {
descriptor = TreeViewerPlugin.getImageDescriptor("book.gif");
} else if (element instanceof BoardGame) {
descriptor = TreeViewerPlugin.getImageDescriptor("gameboard.gif");
} else {
throw unknownElement(element);
}
//obtain the cached image corresponding to the descriptor
Image image = (Image)imageCache.get(descriptor);
if (image == null) {
image = descriptor.createImage();
imageCache.put(descriptor, image);
}
return image;
}
This method answers the correct image for the given domain object, element . An ImageDescriptor is used to load the image for the corresponding domain object. If an image has not yet been loaded, the image descriptor is asked to create the image. The getImageDescriptor
(String)
method is shown below. This method will often be implemented as a static method on the plug-in class since it is a generic utility method and makes use of the plug-in’s installURL.
public static ImageDescriptor getImageDescriptor(String name) {
String iconPath = "icons/";
try {
URL installURL = getDefault().getDescriptor().getInstallURL();
URL url = new URL(installURL, iconPath + name);
return ImageDescriptor.createFromURL(url);
} catch (MalformedURLException e) {
// should not happen
return ImageDescriptor.getMissingImageDescriptor();
}
}
public void dispose() {
for (Iterator i = imageCache.values().iterator(); i.hasNext();) {
((Image) i.next()).dispose();
}
imageCache.clear();
}
This dispose method makes sure the label provider correctly disposes of the images it has created. This is very important to insure that operating system resources are not “leaked.”
Setting the Initial Input
Now that we’ve discussed the content provider and the label provider, we are nearly finished with the code required to create the tree viewer. The one step missing from the example code is setting the initial input for the tree viewer. As it stands now, our tree viewer does not contain any domain objects. Here’s the code used to set the initial input of the tree viewer.
treeViewer.setInput(getInitalInput());
When you set the tree viewer’s input, the tree viewer works in conjunction with the content and label providers to display the tree. Also, remember our content provider’s inputChanged(…)
method will be invoked whenever the tree viewer’s setInput method is called.
The getInitalInput() method simply creates our sample set of domain objects.
public MovingBox getInitalInput() {
MovingBox root = new MovingBox();
MovingBox books = new MovingBox("Books");
MovingBox games = new MovingBox("Games");
MovingBox books2 = new MovingBox("More books");
MovingBox games2 = new MovingBox("More games");
root.addBox(books);
root.addBox(games);
root.addBox(new MovingBox());
books.addBox(books2);
games.addBox(games2);
books.addBook(new Book("The Lord of the Rings", "J.R.R.", "Tolkien"));
books.addBoardGame(new BoardGame("Taj Mahal", "Reiner", "Knizia"));
books.addBook(new Book("Cryptonomicon", "Neal", "Stephenson"));
books.addBook(new Book("Smalltalk, Objects, and Design", "Chamond", "Liu"));
books.addBook(new Book("A Game of Thrones", "George R. R.", " Martin"));
books.addBook(new Book("The Hacker Ethic", "Pekka", "Himanen"));
//books.addBox(new MovingBox());
books2.addBook(new Book("The Code Book", "Simon", "Singh"));
books2.addBook(new Book("The Chronicles of Narnia", "C. S.", "Lewis"));
books2.addBook(new Book("The Screwtape Letters", "C. S.", "Lewis"));
books2.addBook(new Book("Mere Christianity ", "C. S.", "Lewis"));
games.addBoardGame(new BoardGame("Tigris & Euphrates", "Reiner", "Knizia"));
games.addBoardGame(new BoardGame("La Citta", "Gerd", "Fenchel"));
games.addBoardGame(new BoardGame("El Grande", "Wolfgang", "Kramer"));
games.addBoardGame(new BoardGame("The Princes of Florence", "Richard", "Ulrich"));
games.addBoardGame(new BoardGame("The Traders of Genoa", "Rudiger", "Dorn"));
games2.addBoardGame(new BoardGame("Tikal", "M.", "Kiesling"));
games2.addBoardGame(new BoardGame("Modern Art", "Reiner", "Knizia"));
return root;
}
Selection
In most applications, the user selects an item from the tree viewer in order to perform some specific action. You can be notified of these selections by adding a selection change listener to the tree viewer. When a selection occurs in the tree viewer, it will notify each of its selection change listeners, passing along a selection event describing what has been selected. Note that these events flow in the opposite direction of the model-generated events we discussed earlier.
Here’s an example of handling selection in the tree viewer.
treeViewer.addSelectionChangedListener(new ISelectionChangedListener() {
public void selectionChanged(SelectionChangedEvent event) {
// if the selection is empty clear the label
if(event.getSelection().isEmpty()) {
text.setText("");
return;
}
if(event.getSelection() instanceof IStructuredSelection) {
IStructuredSelection selection = (IStructuredSelection)event.getSelection();
StringBuffer toShow = new StringBuffer();
for (Iterator iterator = selection.iterator(); iterator.hasNext();) {
Object domain = (Model) iterator.next();
String value = labelProvider.getText(domain);
toShow.append(value);
toShow.append(", ");
}
// remove the trailing comma space pair
if(toShow.length() > 0) {
toShow.setLength(toShow.length() - 2);
}
text.setText(toShow.toString());
}
}
});
Here an anonymous class selection changed listener is created. The selection changed method is implemented to display the currently selected items in a label or to clear the label if no items are selected. A few points to note:
- Because the selection changed listener method is defined in a very generic way, some casting is required. For example, a tree viewer’s selection will always be an
IStructuredSelection
even though theSelectionChangedEvent
’s getSelection method will answer the more genericISelection
. - We are using the label provider’s getText() method to assist us in converting the domain objects to a textual representation to display in the label.
- In most UI frameworks, the selection-changed listener will be called by the application’s UI thread. SWT is no exception; this means you should be careful to return from listener methods in a timely fashion.
Open the “Moving Box” view and select one or more of the items in the tree. You should see the status label update with the names of the selected items like this:
Responding to Change
Applications aren’t very useful if they don’t handle change. Conceptually, change can be described in a couple of ways. When domain objects are changed, the UI usually reflects these changes. Likewise, user actions in the UI may require updates in the domain objects.
Responding to domain model changes
When a change occurs in the domain model, the UI needs to reflect that change. For example, if a new book is added to one of our moving boxes programmatically, we want the UI to show the newly added book.
While we want the domain to notify the UI through some means, we do not want to “pollute” the domain objects with knowledge about the UI. If the model and the view are too strongly coupled, each becomes brittle and fragile to change. We employ an Observer or Event-Notification style pattern to break this strong coupling.
In our example, we achieve this by creating a listener interface that our domain objects notify when an interesting change occurs. Now we need to provide an object to listen for the changes.
Typically that object will be the tree viewer’s content provider. Remember the inputChanged () method? Our inputChanged method will register itself as a listener to the domain object changes so it can notify the tree viewer of any changes.
Typically the tree viewer will be notified of domain object changes by calling one of the update methods. These are important methods, so let’s take a look at each of them in more detail.
What’s the difference between refresh and update?
The tree viewer provides both a refresh and an update method. What is the difference between these two and when should you use one or the other?
Refresh method
The refresh method refreshes the domain object and all of the domain object’s children. The tree viewer updates the label for the object passed to the refresh method. The tree viewer also asks its content viewer for the children of the object passed to the refresh method and recursively updates each of them by collaborating with the label and content providers.
In addition to this version of refresh, there is also a version that allows you to specify whether or not the labels of existing elements should be updated.
Update method
The update method simply updates the given domain object’s label. In other words, if the domain object passed to the update method contained new children, those new children would not appear in the tree viewer by invoking the update method. Invoking update will only update the domain object’s label or image.
Update properties
In addition to this behavior, the update method also provides a means to specify which “parts” of the domain object changed. If you choose to specify these sub-parts, the tree viewer may be able to optimize the update. If no sub-properties are specified, a full update of the element is performed.
In particular, when you call the update method the following decisions are made:
- Does the updated domain object need a new label and image?
The tree viewer collaborates with your label provider in order to answer this question. By default, all label providers answer that they should be updated when a given property of the domain object changes. You can override the isLabelProperty(Object element, String property)
method to specify otherwise.
- If the tree viewer contains any filters, it asks each filter if the updated domain object needs to be filtered out of the tree.
The same rules apply here. Each viewer filter can specify whether or not it is affected by a change in the given property. By default, viewer filters answer false but you can override the isFilterProperty(Object element, String property)
method to specify otherwise.
- If the tree viewer contains a sorter, it asks the sorter if the updated domain object needs to be re-sorted.
The same rules apply again. The sorter can specify whether or not it is affected by a change in the given property. By default, viewer sorters answer false but you can override the isSorterProperty(Object element, String property)
method to specify otherwise.
Example of Responding to Domain Object Additions and Removals
Let's take a look at how to respond to domain object changes. Let's add a new book to one of the moving boxes. We will add this book in response to a button push. It is important to understand, however, that this domain change could come from "anywhere" - a database trigger, a background thread or some other program. We use a button press to keep it simple.
When you press the "Add New Book" button on the view's toolbar, a book will be added to the selected moving box. What happens when the button is pressed? When the SWT button is presed the OS generates an event that is forwarded to the button's selection listener. The selection listener method adds a new book to the selected moving box. When a new book is added, the moving box will notify its listeners that a domain change has occurred. Since we added the content provider as a domain listener, it will be notified of the change. The content provider then updates the tree viewer.
If nothing is selected, the book will be added to the topmost moving box.
Let's take a look at the code.
protected void addNewBook() {
MovingBox receivingBox;
if (treeViewer.getSelection().isEmpty()) {
receivingBox = root;
} else {
IStructuredSelection selection = (IStructuredSelection) treeViewer.getSelection();
Model selectedDomainObject = (Model) selection.getFirstElement();
if (!(selectedDomainObject instanceof MovingBox)) {
receivingBox = selectedDomainObject.getParent();
} else {
receivingBox = (MovingBox) selectedDomainObject;
}
}
receivingBox.add(Book.newBook());
}
The addNewBook method is called when the "Add a New Book" toolbar button is pressed. Notice that if multiple items are selected, we ignore all of them except for the first one. Also notice that if the selected item is not a moving box, we ask the selected item for its parent, which will be a moving box. Once we have the moving box domain object, we tell it to add a new book.
The moving box's add method is not very interesting. It simply adds the book and notifies its listeners that a book has been added. When this notification occurs, the content viewer will be informed since it registered itself as a listener for these domain changes. Let's take a look at how the content provider services the addition event notification. Here's the content provider's listener method that is called on additions.
public void add(DeltaEvent event) {
Object movingBox = ((Model)event.receiver()).getParent();
viewer.refresh(movingBox, false);
}
Pretty simple, right? The content provider asks the event for the receiver, which is the newly added book. Next, the content provider asks the tree viewer to refresh the moving box that contains the newly added book. Remember that refresh will cause the tree viewer to consult with its content provider to supply a list of children for the refresh object. The false argument is passed along to the refresh method to let it know that we do not need the labels of the other objects in the tree refreshed.
Removing something from the tree works in exactly the same way. We remove the domain object from the moving box and ask the tree viewer to acquire its model objects from the content provider again.
Responding to UI changes
We also need to respond to changes made in the UI. Often these UI changes cause a domain object to change. For example, if we edit the author of one of the books in the tree viewer, we need to make sure this change is correctly propagated to the book domain object.
Typically, you will create listeners and add them to the tree viewer. Your listener methods will retrieve the necessary domain objects from the tree viewer or passed event object.
An example of this technique was shown in the selectionChanged method. Here's part of that method again.
public void selectionChanged(SelectionChangedEvent event) {
// if the selection is empty clear the label
if(event.getSelection().isEmpty()) {
text.setText("");
return;
}
if(event.getSelection() instanceof IStructuredSelection) {
IStructuredSelection selection = (IStructuredSelection)event.getSelection();
StringBuffer toShow = new StringBuffer();
for (Iterator iterator = selection.iterator(); iterator.hasNext();) {
Object domain = (Model) iterator.next();
String value = labelProvider.getText(domain);
toShow.append(value);
toShow.append(", ");
}
// remove the trailing comma space pair
if(toShow.length() > 0) {
toShow.setLength(toShow.length() - 2);
}
text.setText(toShow.toString());
}
In this example, we're retrieving the domain object from the event object's selection. We could have just as easily asked the tree viewer for the currently selected objects.
Here is a list of listeners you can add to a tree viewer along with a description of each of them.
Listener Type |
Description |
|
Responds to help requests |
|
Responds to selection changes |
|
Responds to double clicks |
|
Sets up a listener to support dragging items out of the tree viewer |
|
Sets up a listener to support dropping objects in tree viewer |
|
Responds to expand and collapse events |
Viewer Filters
Viewer filters are used by a tree viewer to extract a subset of the domain objects provided by the content provider. You add and remove viewer filters from the tree viewer.
Viewer filters are additive, which means that the output of one filter is passed in as the input of the next filter. The final result has been filtered through each view filter.
ViewerFilter is an abstract class. In order to implement a viewer filter you subclass this class and override a single method, select(Viewer, Object, Object)
. The method should answer true if the domain object makes it through the filter. The select method also passes the domain object’s parent.
If you need more sophisticated filtering, you may override other methods in ViewFilter to refine how the filtering is accomplished. In particular the isFilterProperty(Object domain, String property)
method can be used to answer whether or not the particular filter is interested in a property change to the domain object.
The isFilterProperty
method is used in conjunction with the tree viewer’s update method. More information about the update method can be found here .
Let’s build a view filter that shows only the board games contained in our moving boxes.
When building filters, it’s important to keep in mind the content providers and the input element for the viewer. For example, if we use the following filter, we’ll have problems.
public boolean select(Viewer viewer, Object parentElement, Object element) {
return element instanceof BoardGame;
}
The problem with this example is that we will filter out our parent. In other words, this filter rejects moving boxes, but remember that moving boxes are what contain board games. So if we filter out moving boxes, the content provider will not have anything left to ask for content.
We can solve this by changing our filter to this:
public boolean select(Viewer viewer, Object parentElement, Object element) {
return element instanceof BoardGame || element instanceof MovingBox;
}
This filter works as intended, leaving only board games visible in the tree viewer.
Let’s build another filter that both augments and works independently from the board game filter. The second filter will show only the contents of moving boxes that contain at least 3 items. Here’s the code for our ThreeItemFilter:
public boolean select(Viewer viewer, Object parentElement, Object element) {
return parentElement instanceof MovingBox && ((MovingBox)parentElement).size() >= 3;
}
Coupled with the first filter, this filter can be used to show all of the board games contained in moving boxes with three or more items in them.
Another important point to remember with filters is that they also apply to the root of the tree viewer. In other words, if the root does not contain at least three items, then you will never get around to filtering the children.
Open the “Moving Box” view and select the triangle near the edge of the window. Then choose the filters and verify that they are working.
Viewer Sorters
The tree viewer collaborates with a viewer sorter to order the domain objects provided by the content provider.
In contrast to our discussion of viewer filters, a tree viewer may utilize only one viewer sorter at a given time. You can always set a different view sorter whenever you like, but at any specific moment, there is either exactly one viewer sorter or none at all.
ViewerSorter is an abstract class that defines a default sorting behavior. Conceptually, the only method that needs to be implemented is the public int compare(Viewer viewer, Object e1, Object e2)
method. The ViewerSorter class provides a default implementation of this method that defines a default sorting behavior.
This default sorting behavior consists of grouping each domain object into categories [by invoking the public int category(Object element) method] and then sorting each domain object within a category by using the tree viewer’s label content provider’s getText method.
By default the viewer sorter considers all domain objects to be in the same category.
If this two-phase default sorting behavior makes sense for your application, then you likely only need to override the category method in order to place your domain objects into their respective categories. Otherwise you’re probably better off implementing the compare method to sort in the appropriate manner for your application. We’ll take a look at both approaches.
As with the view filter, the view sorter contains an isSorterProperty
method that can be used to answer whether or not the particular sorter would be affected by a change to the given property for the given domain object.
The isSorterProperty
method is used in conjunction with the tree viewer’s update method. More information about the update method can be found here .
Our first sorter will rely on the default sorting behavior and implement a sorter that orders the items in such a way that books appear before moving boxes, which appear before board games. To achieve this we can simply override the category method like this:
/** Orders the items in such a way that books appear
* before moving boxes, which appear before board games. */
public int category(Object element) {
if(element instanceof Book) return 1;
if(element instanceof MovingBox) return 2;
return 3;
}
The default sorting behavior uses the category method to place objects into bins, and the bins are arranged in ascending order.
Our second sorter will use the text labels to order the items in an article independent fashion. This means that the literals (e.g. “the”, “a”, “El” and “La” will be ignored in terms of sorting. For example “A Zoo” will sort after “The Car.” This was not true for the first sorter.
The default sort method uses a simple case insensitive sort for items within the same category. We will override the compare method to implement an article-ignoring sort.
Here’s our compare method (which is mostly a copy of ViewerSorter’s method).
public int compare(Viewer viewer, Object e1, Object e2) {
int cat1 = category(e1);
int cat2 = category(e2);
if (cat1 != cat2) return cat1 - cat2;
String name1, name2;
if (viewer == null || !(viewer instanceof ContentViewer)) {
name1 = e1.toString();
name2 = e2.toString();
} else {
IBaseLabelProvider prov = ((ContentViewer)viewer).getLabelProvider();
if (prov instanceof ILabelProvider) {
ILabelProvider lprov = (ILabelProvider)prov;
name1 = lprov.getText(e1);
name2 = lprov.getText(e2);
} else {
name1 = e1.toString();
name2 = e2.toString();
}
}
if(name1 == null) name1 = "";
if(name2 == null) name2 = "";
name1 = stripArticles(name1);
name2 = stripArticles(name2);
return collator.compare(name1, name2);
}
This is a verbatim copy of the superclass’s method, except for the addition of stripArticles near the bottom of the method. The stripArticles method looks like this:
protected String stripArticles(String name) {
String test = name.toLowerCase();
if(test.startsWith("the ") && test.length() > 3) {
return name.substring(4);
} else if(test.startsWith("a ") && test.length() > 1) {
return name.substring(2);
} else if(test.startsWith("el ") && test.length() > 2) {
return name.substring(3);
} else if(test.startsWith("la ") && test.length() > 2) {
return name.substring(3);
}
return name;
}
This sorter does exactly what we want and shows an example of overriding the compare method to implement the search.
Open the “Moving Box” view and select the triangle near the edge of the window. Then choose the sorters and verify that they work as you expected.
Conclusion
This article has demonstrated the basic usage of tree viewers. Hopefully, you now understand how to use the tree viewer and understand where the tree viewer fits within the larger JFace framework.
There are a number of topics planned for an “advanced” tree viewer article, including drag and drop, auto expand nodes within the tree, in-place editing and making use of domain object properties.
Frequently Asked Questions
Here is a list of frequently asked questions about using the tree viewer.
How do I add or remove an item from the tree?
Addition and removal of items is essentially the same.
To remove an item from the tree viewer you should remove the domain object from your model. Then ask the tree viewer to refresh the parent of the domain model. When the update method is called on the tree viewer, it will ask its content provider for the children of the parent of the domain model. However, since you removed it from the parent, it will not be there and subsequently will be removed from the view.
If your domain objects trigger events, you will add the content provider as a listener so that whenever removals occur in your domain object, the content provider can be notified and perform the steps above.
What about the tree viewer's remove methods? If you read the Javadoc for these methods, they will tell you the remove methods should be called by the content provider . This makes sense after all, because the content provider is responsible for supplying the content. If you removed the item from the tree viewer without “going through” the content provider, the item would likely reappear the next time the parent of the item is refreshed.
How do I programmatically select an item in the tree?
Call the tree viewer’s setSelection(new StructuredSelection(<domain model>), true).
What is the difference between the tree viewer’s update and refresh methods?
See this discussion .
How do I reveal a newly added model object that is hidden?
Let’s assume you’ve added the domain object book to your moving box. The moving box is already in the tree but isn’t expanded when you add the book to it. You want the book to be revealed when you add it to the moving box. You would do this:
treeViewer.setExpandedState(movingBox, true);
treeViewer.refresh(movingBox, false);
My tree viewer hangs or isn’t very responsive because I’m hitting a database in my content provider. What can I do?
In most UI frameworks, the listeners are called by the application’s UI thread. SWT is no exception. This means you should be careful to return from listener or provider methods in a timely fashion. If you don’t, the GUI will not receive events, which makes for a sluggish and potentially useless UI.
If you have a long running operation in your listener or provider methods, you should consider forking another thread to do the actual work. For example, if you are populating your tree from a database, you can’t have your content provider waiting for the database to return the domain objects. Instead you could have the content provider return a stub object that shows something like “Loading Data…” while the background thread loads the real data.
Once the background thread has loaded the data, it would need to refresh the parent of the stub object so that the parent would request its domain objects again. Now when the parent requests the domain objects, they already exist since they’ve been loaded from the database.
How do I add a viewer filter to a tree viewer?
See this discussion .
How do I change the order of the items in a tree viewer?
See this discussion .
What does the setUseHashlookup() method do?
The setUseHashlookup(boolean) method informs the tree viewer that it should maintain a hash mapping of your domain object to the corresponding SWT TreeItem. Whenever the tree viewer needs to manipulate the SWT tree items, it uses the hash lookup or performs a recursive search starting at the root looking for the item.
The tree viewer manipulates SWT tree items whenever you add, remove, collapse, expand, reveal, refresh or update a domain object. In other words, the tree viewer is manipulating tree items all the time.
You should probably always setUseHashlookup to true. The only downside to doing so is that the tree viewer will require more memory since it’s maintaining the mapping, but it should perform much faster with a large set of domain objects.
Another important point about tree viewers - in general, they don’t handle domain objects that map to the same value. This is because the tree viewer uses the equals() method and potentially the hashCode() method if you use the setUseHashlookup method.