티스토리 뷰

JavaFX TreeView 다루는 법
 
 이번 시간에는 JavaFX에서 TreeView를 다루는 방법에 대해서 알아보도록 하겠습니다. 


 프로젝트를 진행하면서 쓸 일이 있었는데 굉장히 많이 헤맸었습니다. 

 
 그래서 그 과정과 해결방법을 이 글을 통해서 나누고자 합니다.  일단 구현하고자 하는 모습은 다음과 같습니다.







 1. Root 와 Child Item의 아이콘은 각각 다르다.


 2. 각 Item 의 아이콘은 hover, focus 시에 하얀색 아이콘으로 바뀌어야 한다.

  
 이제 시작해보겠습니다. 



TreeItem

 우선 TreeItem을 이용해서 아이템들을 추가 해보겠습니다.


 소스코드는 아래와 같습니다. 이미지는.. 알아서 구하시면 될 것 같습니다.


  MyTreeView.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
public class MyTreeView extends TreeView<String> {
  private String rootValue = "Root";
  private String subRootValue = "Sub Root";
  private String DEFAULT_STYLE_CLASS = "my-tree-view";
  private String CSS_PATH = "treeview.css";
  private String[] rootItem = {"1""2""3"};
  private Image normalRootIcon;
  private Image focusedRootIcon;    
  private Image normalChildIcon;
  private Image focusedChildIcon; 
 
  public MyTreeView() {
    Image normalRootIcon = new Image(getClass().getResourceAsStream("normalRoot.png"));
    Image focusedRootIcon = new Image(getClass().getResourceAsStream("focusedRoot.png"));;    
    Image normalChildIcon = new Image(getClass().getResourceAsStream("normalChild.png"));;
    Image focusedChildIcon = new Image(getClass().getResourceAsStream("focusedChild.png"));;
 
    getStylesheets().add(CSS_PATH);
    getStyleClass().add(DEFAULT_STYLE_CLASS);
    setPrefWidth(200);
    setPrefHeight(200);
 
 
    TreeItem<String> root = new TreeItem(rootValue, new ImageView(normalRootIcon));
    TreeItem<String> subRoot = new TreeItem(subRootValue, new ImageView(normalRootIcon));
    root.setExpanded(true);
    subRoot.setExpanded(true);
 
    for (String itemString : rootItem) {
      subRoot.getChildren().add(new TreeItem(itemString, new ImageView(normalChildIcon));
    }
    root.getChildren().add(subRoot);
    //this.setCellFactory(treeView -> new MyTreeCell());
    this.setRoot(root);
  }
}
 
cs

 

 CSS 파일을 파일로 다운받으시기 바랍니다.      my-tree-view.css




 이렇게 만들게 되면 효과는 제외하고 원하는 결과를 얻을 수 있습니다. 아래와 같이 말이죠.








 그런데 문제는 이제 효과 입니다. 효과를 어떻게 넣어줄까요..


 그 전에 TreeItem 이 도대체 뭔지 알아보죠.



 TreeItem 이란?

  1. TreeView와 같은 컨트롤에 값의 계층 구조를 제공하는 단일 노드 모델이다.
  2. 이 모델에서는 항목 수가 변경되거나 위치 자체가 변경됬을 때 알림을 받을 수 있는 리스너 등록을 허용한다. 
  3. 명심해야 할 것은 TreeItem은 노드가 아니다. 그렇기 때문에 시각적인 이벤트가 발생하지는 않는다. 
  4. 그래서 이벤트를 얻으려면 리스너를 TreeCell 인스턴스의 이벤트에 추가를 해줘야 한다.

 


 3, 4 번을 주목해 보시면 TreeItem은 노드가 아니기 때문에 이벤트가 발생하지 않습니다. 


 

 그렇기 때문에 TreeCell 이라는 것을 사용해야 합니다.




TreeCell


 위 코드에서 33번 Line에 주석 처리가 되있는 부분이 보이시나요? 이 부분이 바로 TreeItem들을 TreeCell 의 인스턴스로 추가하는 것입니다.



  MyTreeCell 이라는 클래스를 별도로 만들겠습니다. 



  MyTreeCell.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class MyTreeCell extends TreeCell<String> {
 
  public MyTreeCell() {
        //이벤트 처리          
  }
  @Override
  protected void updateItem(String item, boolean empty) {
    super.updateItem(item, empty);
    if(!empty){
      setText(item);
      TreeItem treeItem = getTreeItem();
      setGraphic(new ImageView(treeItem.getGraphic()));
    }else{
      setText(null);
      setGraphic(null);
    }
  }
}
 
cs



 위 코드 에서는 updateItem 이라는 메소드를 통해서 TreeView의 모든 Item들을 TreeCell 로 초기화 해주는 작업을 진행합니다.



 empty에는 해당 item이 비어있는지 여부를 나타내는데, 이를 통해서는 TreeView의 비어있는 Cell들 까지도 체크를 하는 것을 알 수 있죠.



 그래서 만들어진 Item 하나하나의 String 값과 ImageView들을 가져와서 설정을 해줍니다. 

 


 그런데 사실 생각해보면 item이 root인지 아닌지 검사할 수 있는 방법은 지금은 존재 하지 않습니다. cell로 바뀌면서 말이죠.


  

 그래서 이 과정을 좀 더 편하게 하기 위해서 TreeItem을 직접 구현해 보도록 하겠습니다.





 

TreeItem 구현



 아래의 코드는 처럼 TreeItem을 상속받은 클래스를 만들겠습니다.



 MyTreeItem.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class MyTreeItem extends TreeItem<String> {
  private Image normalIcon;
  private Image focusedIcon;
 
  public MyTreeItem(String text, String normalIconPath, String focusedIconPath) {
    super(text);
    this.normalIcon = new Image(getClass().getResourceAsStream(normalIconPath));
    this.focusedIcon = new Image(getClass().getResourceAsStream(focusedIconPath));
    setGraphic(new ImageView(this.normalIcon));
  }
 
  public Image getNormalIcon() {
    return normalIcon;
  }
 
  public Image getFocusedIcon() {
    return focusedIcon;
  }
}
 
cs



 위 코드를 보면 Treeitem은 Item 에 들어갈 String 값과 이미지를 path 형태로 받습니다. 그런데 여기는 이미지가 두 개밖에 없습니다.



 총 4개가 필요한데 말이죠. 그 이유는 Root, Child 일 때를 TreeItem을 상속받은 각각 다른 클래스로 구현하기 위함입니다.



 그래서 아래처럼 두 개의 클래스를 만듭니다.



 MyDirTreeIte.java

1
2
3
4
5
6
public class MyDirTreeItem extends Gk2TreeItem {
 
  public MyDirTreeItem(String text) {
    super(text, "normalRoot.png""focusedRoot.png");
  }
}
cs




 MyFileTreeItem.java

1
2
3
4
5
6
public class MyFileTreeItem extends Gk2TreeItem {
 
  public MyFileTreeItem(String text) {
    super(text, "normalChild.png""focusedChild.png");
  }
}
cs



 그럼 이제 위에서 처음에 TreeItem 을 구성 할 때도 바꿔줘야 겠죠? 




 MyTreeView.java 수정

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
public class MyTreeView extends TreeView<String> {
  private String rootValue = "Root";
  private String subRootValue = "Sub Root";
  private String DEFAULT_STYLE_CLASS = "gk2-tree-view";
  private String CSS_PATH = "/commons/ui/control/treeview/treeview.css";
  private String[] rootItem = {"1""2""3"};
 
 
  public MyTreeView() {
    getStylesheets().add(CSS_PATH);
    getStyleClass().add(DEFAULT_STYLE_CLASS);
    setPrefWidth(200);
    setPrefHeight(200);
 
    TreeItem<String> root = new MyRootTreeItem(rootValue);
    TreeItem<String> subRoot = new MyRootTreeItem(subRootValue);
    root.setExpanded(true);
    subRoot.setExpanded(true);
 
    for (String itemString : rootItem) {
      subRoot.getChildren().add(new MyChildTreeItem(itemString));
    }
    root.getChildren().add(subRoot);
 
    //TreeCell 인스턴스 생성
    this.setCellFactory(treeView -> new MyTreeCell());
    this.setRoot(root);
 
    // 처음 실행 시 root의 focus를 통해 이상 현상 해결
    this.getSelectionModel().select(0);
  }
}
cs





 그럼 이제는 TreeCell 도 바꿔줘야 합니다.




 MyTreeCell.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class MyTreeCell extends TreeCell<String> {
 
  public MyTreeCell(){
 
          //이벤트 처리..
 
  }
  @Override
  protected void updateItem(String item, boolean empty) {
    super.updateItem(item, empty);
    if(!empty){
      setText(item);
      MyTreeItem treeItem = (MyTreeItem)getTreeItem();
      setGraphic(new ImageView(treeItem.getNormalIcon()));
    }else{
      setText(null);
      setGraphic(null);
    }
  }
}
 
cs






 이제 남은 것은 이벤트를 처리하는 일 밖에 없습니다. 이벤트 처리는 각 Cell 마다 Listener를 달아주는 방식입니다. 




 위에 주석 부분에 들어갈 코드는 아래와 같습니다.





 이벤트 처리

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
hoverProperty().addListener(new ChangeListener<Boolean>() {
      @Override
      public void changed(ObservableValue<extends Boolean> observable, Boolean oldValue, Boolean newValue) {
        if (getItem() != null && !isFocused()) {
          if (newValue) {
            MyTreeItem item = (MyTreeItem) getTreeItem();
            setGraphic(new ImageView(item.getFocusedIcon()));
          } else {
            MyTreeItem item = (MyTreeItem) getTreeItem();
            setGraphic(new ImageView(item.getNormalIcon()));
          }
        }
      }
    });
 
    focusedProperty().addListener(new ChangeListener<Boolean>() {
      @Override
      public void changed(ObservableValue<extends Boolean> observable, Boolean oldValue, Boolean newValue) {
        if (getItem() != null ) {
          if (newValue) {
            MyTreeItem item = (MyTreeItem) getTreeItem();
            setGraphic(new ImageView(item.getFocusedIcon()));
          }else{
            MyTreeItem item = (MyTreeItem) getTreeItem();
            setGraphic(new ImageView(item.getNormalIcon()));
          }
        }
      }
    });
 
    selectedProperty().addListener(new ChangeListener<Boolean>() {
      @Override
      public void changed(ObservableValue<extends Boolean> observable, Boolean oldValue, Boolean newValue) {
        if (newValue) {
          MyTreeItem item = (MyTreeItem) getTreeItem();
          setGraphic(new ImageView(item.getFocusedIcon()));
        }
      }
    });
  }
cs





 그래서 결과화면은 아래와 같습니다.






반응형
댓글