lördag 19 november 2011

JavaFX 2 - Mastering the TreeView

The TreeView is a rely good component for presenting a tree structure, but it can be a bit tricky to get the different part worked out.

In this post we will create a demo which let us:
  • Lazy loading the children of the nodes in the tree.
  • Using the a ChangeListener to print the of the selected nodes text.
  • Adding new nodes to the tree.

The simple demo which we will create:


First out, creating the components which is used in the demo

/** Create and setup all the components used in this demo. */
 private void initComponents() {
  LazyTreeItem rootItem = new LazyTreeItem("Root", 0);
  rootItem.setExpanded(true);
  treeView = new TreeView(rootItem);
  treeView.getSelectionModel().setSelectionMode(SelectionMode.SINGLE);
  treeView.getSelectionModel().selectedItemProperty().addListener(this);
  
  labelSelectedItem = new Label("Selected:");
  
  labelErrorText = new Label("Error: ");
  
  inputNewItem = new TextField();
  
  buttonNewItem = new Button("Insert item");
  buttonNewItem.setOnAction(this);
 }

Row 3: we start by creating a root node which we will be using as the root in our tree. The node is a class which we will be creating to be able to load the children of the node on request (lazy).

Row 4: Making the root node expanded will render the root expanded with all children visible.

Row 5: Creating the TreeView with our root node as the root for the tree.

Row 6: By using the trees selection model we set the tree to only support one selected node at each time. This can be configured to be multiple nodes, but in this demo we are only interested in one node at time.

Row 7: By adding a ChangeListener to the selection model we can track when we change the selected node in the tree. More on the ChangeListener later. For now just notice how we add the listener to the tree.

Row 9 and on is the other components which are used in the demo. Nothing fancy about them.

Laying out the demo

@Override
         public void start(Stage stage) throws Exception {
  initComponents();
  
  stage.setTitle("Demo of TreeView");
  
  BorderPane mainPane = new BorderPane();
  
  mainPane.setLeft(treeView);
  
  VBox centerPane = new VBox();
  centerPane.setSpacing(3);
  centerPane.setPadding(new Insets(3));
  centerPane.getChildren().addAll(labelSelectedItem, labelErrorText, inputNewItem, buttonNewItem);
  mainPane.setCenter(centerPane);
  
  Scene scene = new Scene(mainPane, 600, 400);
  
  stage.setScene(scene);
  stage.sizeToScene();
  stage.show();  
 }

We are using a simple BorderPane where we place the TreeView to the left, and the rest of the components within a VBox in the center section.

Creating lazy loading of the children in the tree

So far things has been straight forward and without any fancy stuff. However to solve the lazy loading of the child nodes we need to dig into the TreeItem a bit. The idea of the lazy loading is to only load the child nodes when we need them and this is made by creating a class which we extend with TreeItem and overload some functions.

private class LazyTreeItem extends TreeItem<String> {
  /** The depth of this tree item in the {@link TreeView}. */
  private final int depth;
  /** Control if the children of this tree item has been loaded. */
  private boolean hasLoadedChildren = false;
  
  public LazyTreeItem(String itemText, int depth) {
   super(itemText);
   this.depth = depth;
  }
  
  @Override
  public ObservableList<TreeItem<String>> getChildren() {
   if (hasLoadedChildren == false) {
    loadChildren();
   }
   return super.getChildren();
  }
  
  @Override
  public boolean isLeaf() {
   if (hasLoadedChildren == false) {
    loadChildren();
   }
   return super.getChildren().isEmpty();
  }
  
  /** Create some dummy children for this item. */
  @SuppressWarnings("unchecked") // Safe to ignore since we know that the types are correct.
  private void loadChildren() {
   hasLoadedChildren = true;
   int localDeepth = depth + 1;
   LazyTreeItem child1 = new LazyTreeItem("Child 1 (deepth = " + localDeepth + ")",
localDeepth);
   LazyTreeItem child2 = new LazyTreeItem("Child 2 (deepth = " + localDeepth + ")",
localDeepth);
   super.getChildren().setAll(child1, child2);
  }
  
  /** Return the depth of this item within the {@link TreeView}.*/
  public int getDepth() {
   return depth;
  }
 }

Row 1: Extending the TreeItem otherwise we will not be able to put our item in the TreeView.

Row 12-18: Overriding the getChildren method will provide us the ability to load the children lazy since the method is first called when the parent node is expanded. Since we can not populate the children in the constructor of the node we need to populate the children now. Populating the children in the constructor would not make the node lazy since all children would be loaded when a new instance of the node is created.

Row 20-26: Overriding the isLeaf method is necessary since it is called when the node is rendered. If we would not load the children when the function is called it would always return true, and the node would be rendered as a leaf without the ability to show any potential children.

Row 28-37: Here is where we would load our real node if this was something else then a demo. For now we are satisfied by creating two dummy children for this node.

Row 36: Using the setAll method will remove all existing children of the node and add the new children instead. If we would like to add more children to the node we instead should use addAll. More on this later when we implement a function to add new nodes to the tree.

Showing the selected item's text

When a user click on a node in the tree we like to show the text in the 'Selected' label. This is achived by using the ChangeListener which we atached to the selection model when we created the TreeView.

... implements EventHandler<ActionEvent>, ChangeListener<TreeItem<String>>

...

treeView.getSelectionModel().selectedItemProperty().addListener(this);

...

/** Handles change event from the {@link TreeView}. */
 @Override
 public void changed(ObservableValue<? extends TreeItem<String>> observableValue, TreeItem<String> oldItem, TreeItem<String> newItem) {
  labelSelectedItem.setText("Selected: " + newItem.getValue());
 }

Row 1: On the class we implement the ChangeListener interface.

Row 5: We assign our listener to the selection model of the tree.

Row 11-13: This is where we implement the action we like to do when a change to the selection in the tree has happen. For this demo we are pleased with just printing the text of the node in our label.

Adding a new node to the tree


In the last part of this demo we add a function for adding a new node to the tree. By using a text input field and a button this is achieved together with the selected node in the tree. When the user click the button we take the text from the text field and create a new node in the tree.

/** Handles the action which is triggered by the button. */
 @Override
 public void handle(ActionEvent event) {
  LazyTreeItem selectedItem = (LazyTreeItem) treeView.getSelectionModel().getSelectedItem();
selectedItem.getChildren().add(new LazyTreeItem(inputNewItem.getText(), selectedItem.getDepth()));
 }

Row 3: By implementing the interface EventHandler and connect the button to our handler.

Row 4: Using the selection model of the TreeView to get the selected item.

Row 5: Adding a new node to the tree is simple. Get the children list and insert the new node by using the add function. Remember that using the set function will remove all old children from the node.

Summary

In this demo we have learn how to use the TreeView to load the children of the nodes lazy, how we can get the information from the selected node, and how to add new nodes to the tree.

The complete source code can be fetched from: http://files.loop81.com/rest/download/6643f32b-c6d8-4816-9a3d-d062ee8ce57e.

5 kommentarer:

  1. Den här kommentaren har tagits bort av skribenten.

    SvaraRadera
  2. The post is now updated with a working link to the source code.

    SvaraRadera
  3. Your program throws a StackOverflow exception every time you try to insert anything.

    SvaraRadera
  4. Great post! But the link is not working.

    SvaraRadera
  5. When loading of the children is takeing some time, the interface is not responsive, especially when a tree node with a lot of children is expanded, so I want to show a progress indicator. After a lot of trials with background tasks, events and so on without success I am stuck without an idea.

    What is an elegant way to achieve progress indicator during TreeItem.getChildren()?

    SvaraRadera