摘要: 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
The TreeViewer is used throughout Eclipse. In fact, trees are pervasive in most applications. This article will explain the following information about tree viewers:
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.
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.
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.
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:
A moving box can contain other moving boxes, books, and board games. A moving box may or may not have a name.
Each book and board game has both a title and an author.
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.
It is crucial to understand the model/view relationship as utilized by JFace viewers. Conceptually, all viewers perform two primary tasks:
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.
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)
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
Let’s take a look at that class in detail.
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.
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.
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.
The hasChildren method is invoked by the tree viewer when it needs to know whether a given domain object has children.
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.)
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.
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.
the code listed above,
the tree viewer’s label provider was set to an
Let’s take a look at this class in detail.
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
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
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.”
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.
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
will be invoked whenever the tree viewer’s setInput
method is called.
The getInitalInput() method simply creates our sample set of domain objects.
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.
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:
IStructuredSelectioneven though the
SelectionChangedEvent’s getSelection method will answer the more generic
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:
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.
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 changesby calling one of the update methods. These are important methods, so let’s take a look at each of them in more detail.
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?
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.
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.
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:
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.
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
String property) method to specify otherwise.
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
isSorterProperty(Object element, String
property) method to specify otherwise.
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.
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.
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.
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.
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.<table border="1" cellspacing="3" cellpadding="0" width="595"> <tr> <td valign="top">
Listener Type</td> <td valign="top">
Description</td> </tr> <tr> <td valign="top">
Responds to help requests</td> </tr> <tr> <td valign="top">
Responds to selection changes</td> </tr> <tr> <td valign="top">
Responds to double clicks</td> </tr> <tr> <td valign="top">
Sets up a listener to support dragging items out of the tree viewer</td> </tr> <tr> <td valign="top">
Sets up a listener to support dropping objects in tree viewer</td> </tr> <tr> <td valign="top">
Responds to expand and collapse events</td> </tr> </table>
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
Object). The method should answer true if
the domain object makes it through the filter. The select
method also passes the domain object’s
If you need more sophisticated filtering, you may override
other methods in ViewFilter to refine how the filtering
is accomplished. In particular the
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.
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.
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:
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:
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.
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
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:
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).
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:
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.
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.
Here is a list of frequently asked questions about using the tree viewer.
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.
Call the tree viewer’s setSelection(new
See this discussion.
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:
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.
See this discussion.
See this discussion.
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.