본문 바로가기
Data Structure

TreeModel 만들기

by SuldenLion 2024. 1. 6.
반응형

GUI 작업에서 중요한 파트인 Tree. 직접 만들어 보았다. 

작업 과정을 소개해 보겠다.

TreeFrame이라는 View와 TreeModel의 Model 그리고 View에 포함된 Controller로 MVC 패러다임을 따랐다.

 

import java.util.*;

import javax.swing.event.*;
import javax.swing.tree.*;

public class MyTreeModel implements TreeModel {

	private TreeNode root;

	MyTreeModel() {
		root = null;
	}

	void setRoot(String s) {
		root = new TreeNode(s);
	}

	boolean addNewChild(String parent, String child) {
		if (root == null) return false;
		TreeNode pNode = root.find(parent);
		if (pNode == null) return false;
		pNode.addChild(child);
		return true;
	}

	boolean deleteChild(String parent, String child) {
		if (root == null) return false;
		TreeNode pNode = root.find(parent);
		if (pNode == null) return false;
		TreeNode cNode = root.find(child);
		if (cNode == null) return false;

		pNode.deleteChild(pNode.getIndexOfChild(cNode));
		return true;
	}

	public boolean isLeaf(Object obj) {
		TreeNode node = (TreeNode)obj;

		return node.isLeaf();
	}

	public int getChildCount(Object obj) {
		TreeNode node = (TreeNode)obj;

		return node.getChildCount();
	}

	public Object getChild(Object obj, int index) {
		TreeNode node = (TreeNode)obj;

		return node.getChildAt(index);
	}

	public int getIndexOfChild(Object parent, Object child) {
		TreeNode node = (TreeNode)parent;

		return node.getIndexOfChild((TreeNode)child);
	}

	public Object getRoot() {
		return root;
	}

	public void addTreeModelListener(TreeModelListener l) {

	}

	public void removeTreeModelListener(TreeModelListener l) {

	}

	public void valueForPathChanged(TreePath path, Object newValue) {

	}

}

 

MyTreeModel은 TreeModel 인터페이스를 상속받는다. isLeaf, getChild와 같은 반드시 구현해야 하는 메서드 8가지를 추가하지만 TreeModelListener와 valueForPathChanged 메서드 등은 사용하지 않을 것이라 비워둔다.

TreeModel은 구체적인 노드의 정보를 알수 없을뿐만 아니라 알 필요도 없이 root의 정보만으로 모든 정보를 알아낸다는 것이 특징이다. 예를 들어, 어떤 노드가 leaf 노드인가를 알아낸다거나 add, delete작업을 위하여 특정 위치의 노드를 알아내고자 할때 root를 통해서 traverse 한다.

 

import java.awt.*;

import javax.swing.*;
import javax.swing.tree.*;

public class TreeFrame extends JFrame {

	JTree tree;
	JPanel panel;
	MyTreeModel model;
	JButton setRootBtn;
	JTextField rootField;
	JLabel parentLabel;
	JTextField parentField;
	JLabel childLabel;
	JTextField childField;
	JButton addBtn;
	JButton deleteBtn;
	JScrollPane sp;
	JMenuBar menuBar;
	JMenu menu;
	JSplitPane splitPane;
	JPanel funcPanel;

	TreeFrame() {
		menuBar = new JMenuBar();
		setJMenuBar(menuBar);

		menu = new JMenu("File");
		menuBar.add(menu);

		//panel = new JPanel();
		funcPanel = new JPanel();

		model = new MyTreeModel();
		tree = new JTree();
		sp = new JScrollPane(tree);
		//getContentPane().add(panel);
		//panel.add(sp);
		//panel.add(funcPanel);

		splitPane = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, sp, funcPanel);

		rootField = new JTextField();

		setRootBtn = new JButton("Root 설정");
		setRootBtn.addActionListener((e) -> {
			if (rootField.getText() == null) return;
			model.setRoot(rootField.getText());
			tree.setModel(model);

			rootField.setEditable(false);
			setRootBtn.setEnabled(false);	
		});

		funcPanel.add(rootField);
		funcPanel.add(setRootBtn);

		parentField = new JTextField();

		parentLabel = new JLabel("Parent");

		childField = new JTextField();
        
		childLabel = new JLabel("Child");
        
		funcPanel.add(parentLabel);
		funcPanel.add(parentField);
		funcPanel.add(childLabel);
		funcPanel.add(childField);

		addBtn = new JButton("Add");
		addBtn.addActionListener((e) -> {
			String parent = parentField.getText();
			String child = childField.getText();

			model.addNewChild(parent, child);
			tree.updateUI();
		});

		deleteBtn = new JButton("Delete");
		deleteBtn.addActionListener((e) -> {
			String parent = parentField.getText();
			String child = childField.getText();

			model.deleteChild(parent, child);
			tree.updateUI();
		});

		funcPanel.add(addBtn);
		funcPanel.add(deleteBtn);

		getContentPane().add(splitPane);

		setSize(800,600);
		setDefaultCloseOperation(EXIT_ON_CLOSE);
		setLocationRelativeTo(null);
	}

}

 

Tree의 View역할을 해줄 TreeFrame이다. Root를 설정해줄 텍스트 필드와 버튼, parent와 child를 각각 입력받아 add해주기 위한 component들을 배치해 주었다. JSplitPane으로 Tree와 Control UI부분을 나눠서 배치해주고 Tree는 ScrollPane에 담아주었다.

 

import java.util.*;

public class TreeNode {

	private String data;
	private LinkedList<TreeNode> children;

	TreeNode(String s) {
		data = s;
		children = new LinkedList<TreeNode>();
	}

	String getData() {
		return data;
	}

	public String toString() {
		return data;
	}

	public boolean isLeaf() {
		if (children.size() == 0) return true;
		return false;
	}

	public int getChildCount() {
		return children.size();
	}

	public Object getChildAt(int i) {
		return children.get(i);
	}

	public int getIndexOfChild(TreeNode child) {
		return children.indexOf(child);
	}

	void depthFirstTraverse() {
		System.out.println(data);

		ListIterator<TreeNode> i = children.listIterator();
		while (i.hasNext()) {
			TreeNode child = i.next();
			child.depthFirstTraverse();
		}
	}

	void depthFirstEnumeration(LinkedList<String> pEnumeration) {
		pEnumeration.addLast(data);
		ListIterator<TreeNode> i = children.listIterator();
		while(i.hasNext()) {
			TreeNode child = i.next();
			child.depthFirstEnumeration(pEnumeration);
		}
	}

	TreeNode find(String s) {
		if (data.equals(s)) return this;

		ListIterator<TreeNode> i = children.listIterator();
		while (i.hasNext()) {
			TreeNode child = i.next();
			TreeNode foundNode = child.find(s);
			if (foundNode != null) return foundNode;
		}
		return null;
	}

	void addChild(String s) {
		TreeNode pNewNode = new TreeNode(s);
		children.addLast(pNewNode);
	}

	void deleteChild(int i) {
		children.remove(i);
	}

}

 

Model을 통해 실질적으로 작업이 수행될 TreeNode이다. 노드는 String으로 된 Data값을 가지며 자식 노드들을 보관해줄 children list를 가진다. dfs나 enumeration, find 등으로 필요한 데이터를 recursion하게 찾아낸다. 그리고 해당 노드의 특정 child를 찾아줄 childAt, getIndexOfChild, getChildCount 등의 메소드가 있다.

 

 

여기까지가 version 1

============================================================================================

 

import java.awt.*;

import javax.swing.*;
import javax.swing.tree.*;

public class MyTreeCellRenderer implements TreeCellRenderer {	
	public Component getTreeCellRendererComponent(JTree tree, Object value,
			boolean selected, boolean expanded, boolean leaf, int row, boolean hasFocus) {
		return new JPanel() {
			public void paintComponent(Graphics g) {
				//setSize(new Dimension(50,10));
				if (!(value instanceof TreeNode)) return;
				String s = ((TreeNode)value).getData();
				g.drawString(s, 15, 10);
				if (leaf) {
					//g.setColor(Color.red);
					g.drawImage((new ImageIcon("red-ball.gif")).getImage(), 1, 1, tree);
				} else {
					//g.setColor(Color.blue);
					g.drawImage((new ImageIcon("blue-ball.gif")).getImage(), 1, 1, tree);	
				}
				/*if (expanded) {
					tree.expandRow(row);
					System.out.println(expanded);
				}*/
				//Dimension d = getSize();
				//System.out.println("Size = " + d);
				//g.fillRect(0, 0, d.width, d.height);
				//g.fillRect(0, 0, 2, 10);
			}
			public Dimension getPreferredSize() {
				return new Dimension(100,20);
			}
		};
	}	
}

 

Tree의 Cell단위로 작업을 하기 위한 TreeCellRenderer이다. 셀 마다 Panel을 주고 그 Panel안의 String이나 Icon을 따로 작업해 주었다. 트리에 노드를 추가할 때 자동으로 트리를 펼쳐주는 expand 기능을 구현하려 시도, 하지만 이 작업은 Renderer에서 하는것이 아니었다.

 

import java.io.*;
import java.util.*;

import javax.swing.event.*;
import javax.swing.tree.*;

public class MyTreeModel implements TreeModel, Serializable {

	private TreeNode root;
	
	MyTreeModel() {
		root = null;
	}
	
	void setRoot(String s) {
		root = new TreeNode(s);
	}

	TreePath addNewChild(String parent, String child) {
		if (root == null) return null;
		TreeNode pNode = root.find(parent);
		if (pNode == null) return null;
		TreeNode pChildNode = pNode.addChild(child);
		
		TreePath path = pChildNode.constructTreePath();
		return path;
	}
	
	boolean deleteChild(String parent, String child) {
		if (root == null) return false;
		TreeNode pNode = root.find(parent);
		if (pNode == null) return false;
		TreeNode cNode = root.find(child);
		if (cNode == null) return false;
		
		pNode.deleteChild(pNode.getIndexOfChild(cNode));
		return true;
	}
	
	public boolean isLeaf(Object obj) {
		TreeNode node = (TreeNode)obj;
		
		return node.isLeaf();
	}
	
	public int getChildCount(Object obj) {
		TreeNode node = (TreeNode)obj;
		
		return node.getChildCount();
	}
	
	public Object getChild(Object obj, int index) {
		TreeNode node = (TreeNode)obj;
		
		return node.getChildAt(index);
	}
	
	public int getIndexOfChild(Object parent, Object child) {
		TreeNode node = (TreeNode)parent;
		
		return node.getIndexOfChild((TreeNode)child);
	}
	
	public Object getRoot() {
		return root;
	}
	
	public void serialLoad(String fileName) {
		try {
			FileInputStream fis = new FileInputStream(fileName);
			ObjectInputStream ois = new ObjectInputStream(fis);
			root = (TreeNode)ois.readObject();
			ois.close();
			fis.close();
		} catch (Exception e) {
			e.printStackTrace();
		}
	}
	
	public void serialSave(String fileName) {
		try {
			FileOutputStream fos = new FileOutputStream(fileName);
			ObjectOutputStream oos = new ObjectOutputStream(fos);
			oos.writeObject(root);
			oos.flush();
			oos.close();
			fos.close();
		} catch (IOException e) {
			e.printStackTrace();
		}
	}
	
	public void txtLoad(String fileName) {
		try {
			BufferedReader br = new BufferedReader(new FileReader(new File(fileName)));
			
			String s = br.readLine();
			System.out.println(s);
			setRoot(s);
			while ((s = br.readLine()) != null) {
				if (s.length() == 0) break;
				String[] token = s.split(" ");
				addNewChild(token[0], token[1]);
			}
			
			br.close();
			
		} catch (IOException e) {
			e.printStackTrace();
		}
	}
	
	public void txtSave(String fileName) {
		try {
			FileWriter fw = new FileWriter(new File(fileName));
			
			String s = root.getData() + "\n";
			s += root.dfsSave();
			fw.write(s);
			
			fw.flush();			
			fw.close();
		} catch (IOException e) {
			e.printStackTrace();
		}
	}
	
	public int getDepth() {
		if (root == null) return 0;
		return root.getDepth();
	}
	
	public int getNumberOfLeafNodes() {
		if (root == null) return 0;
		return root.getNumberOfLeafNodes();
	}
	
	public int getNoneLeafNodesNumber() {
		if (root == null) return 0;
		return root.getNoneLeafNodesNumber();
	}
	
	public int getTotalNodesNumber() {
		if (root == null) return 0;
		return root.getTotalNodesNumber();
	}
	
	public void addTreeModelListener(TreeModelListener l) {
		
	}
	
	public void removeTreeModelListener(TreeModelListener l) {
		
	}
	
	public void valueForPathChanged(TreePath path, Object newValue) {
		
	}
	
}

 

트리 모델에서 Serialize하게 트리 구조를 저장하고 불러오는 기능, data값을 text 파일로 만들어서 저장하고 불러오는 기능을 추가하였다. serialize기능을 사용하기 위해서 model과 node 쪽에 각각 serializable을 implement 해준다. linked list 같은 모든 컬렉션 객체는 Serializable 되어있다. serial save는 stream을 불러와 root의 정보를 넘기면 다 저장 해준다. load도 마찬가지. text save같은 경우는 root 정보, 각각의 parent child를 pair형태로 저장해준다. text load는 그렇게 저장된 파일을 root에다가 split된 parent child 쌍의 값을 addNewChild 해가면서 load해준다.

 

import java.awt.*;

import javax.swing.*;
import javax.swing.filechooser.FileNameExtensionFilter;
import javax.swing.tree.*;

public class TreeFrame extends JFrame {
	static int WIDTH = 600;
	static int HEIGHT = 800;
	JTree tree;
	JPanel panel;
	MyTreeModel model;
	JButton setRootBtn;
	JTextField rootField;
	JLabel parentLabel;
	JTextField parentField;
	JLabel childLabel;
	JTextField childField;
	JButton addBtn;
	JButton deleteBtn;
	JScrollPane sp;
	JMenuBar menuBar;
	JMenu menu;
	JSplitPane splitPane;
	JPanel funcPanel;
	JMenu serMenu;
	JMenu txtMenu;
	JMenuItem serSave;
	JMenuItem serLoad;
	JMenuItem txtSave;
	JMenuItem txtLoad;
	
	TreeFrame() {
		menuBar = new JMenuBar();
		setJMenuBar(menuBar);
		
		menu = new JMenu("File");
		menuBar.add(menu);
		
		serMenu = new JMenu("Serialize");
		txtMenu = new JMenu("Text");
		
		serLoad = new JMenuItem("Load");
		serSave = new JMenuItem("Save");
		txtLoad = new JMenuItem("Load");
		txtSave = new JMenuItem("Save");

		serMenu.add(serLoad);
		serMenu.add(serSave);
		txtMenu.add(txtLoad);
		txtMenu.add(txtSave);
		serLoad.addActionListener((e) -> serialLoad());
		serSave.addActionListener((e) -> serialSave());
		txtLoad.addActionListener((e) -> textLoad());
		txtSave.addActionListener((e) -> textSave());
		
		menu.add(serMenu);
		menu.add(txtMenu);
		
		//panel = new JPanel();
		funcPanel = new JPanel();
		funcPanel.setLayout(new BoxLayout(funcPanel, BoxLayout.PAGE_AXIS));
		
		model = new MyTreeModel();
		tree = new JTree();
		sp = new JScrollPane(tree);
		//getContentPane().add(panel);
        
		splitPane = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, sp, funcPanel);
		Dimension sz = getSize();
		sp.setPreferredSize(new Dimension(WIDTH/2,HEIGHT));
		//panel.add(splitPane);
		
		rootField = new JTextField(10);
		
		setRootBtn = new JButton("Root 설정");
		setRootBtn.addActionListener((e) -> {
			if (rootField.getText() == null) return;
			model.setRoot(rootField.getText());
			tree.setModel(model);
			
			rootField.setEditable(false);
			setRootBtn.setEnabled(false);	
		});
		
		JPanel p1 = new JPanel();
		p1.add(rootField);
		p1.add(setRootBtn);
		funcPanel.add(p1);
		
		parentField = new JTextField(5);
		
		parentLabel = new JLabel("Parent");
		
		childField = new JTextField(5);
		
		childLabel = new JLabel("Child");
		
		JPanel p2 = new JPanel();
		p2.add(parentLabel);
		p2.add(parentField);
		p2.add(childLabel);
		p2.add(childField);
		funcPanel.add(p2);
		
		addBtn = new JButton("Add");
		addBtn.addActionListener((e) -> {
			String parent = parentField.getText();
			String child = childField.getText();
			
			TreePath path = model.addNewChild(parent, child);
			tree.expandPath(path);
			System.out.println(path);
			tree.updateUI();
			
			System.out.println(model.getTotalNodesNumber());
		});
		
		deleteBtn = new JButton("Delete");
		deleteBtn.addActionListener((e) -> {
			String parent = parentField.getText();
			String child = childField.getText();
			
			model.deleteChild(parent, child);
			tree.updateUI();
		});
		
		JPanel p3 = new JPanel();
		p3.add(addBtn);
		p3.add(deleteBtn);
		funcPanel.add(p3);
		
		MyTreeCellRenderer renderer = new MyTreeCellRenderer();
		tree.setCellRenderer(renderer);
		
		getContentPane().add(splitPane);
		
		setSize(WIDTH,HEIGHT);
		setDefaultCloseOperation(EXIT_ON_CLOSE);
		setLocationRelativeTo(null);
	}
	
	public void serialLoad() {
		JFileChooser chooser = new JFileChooser(System.getProperty("user.dir"));
		chooser.setFileSelectionMode(JFileChooser.FILES_AND_DIRECTORIES);
		chooser.setDialogType(JFileChooser.OPEN_DIALOG);
		int returnVal = chooser.showOpenDialog(null);
		if (returnVal != JFileChooser.APPROVE_OPTION) return;
		String fileName = chooser.getSelectedFile().getPath();
		if (fileName == null) return;
		if (fileName.length() == 0) return;
		
		model.serialLoad(fileName);
		tree.setModel(model);
		rootField.setEditable(false);
		setRootBtn.setEnabled(false);	
		tree.updateUI();
	}
	
	public void serialSave() {
		JFileChooser chooser = new JFileChooser(System.getProperty("user.dir"));
		chooser.setFileSelectionMode(JFileChooser.FILES_AND_DIRECTORIES);
		chooser.setDialogType(JFileChooser.SAVE_DIALOG);
		int returnVal = chooser.showSaveDialog(null);
		if (returnVal != JFileChooser.APPROVE_OPTION) return;
		String fileName = chooser.getSelectedFile().getPath();
		if (fileName == null) return;
		if (fileName.length() == 0) return;
		
		model.serialSave(fileName);
	}
	
	public void textLoad() {
		JFileChooser chooser = new JFileChooser(System.getProperty("user.dir"));
		chooser.setFileSelectionMode(JFileChooser.FILES_AND_DIRECTORIES);
		chooser.setDialogType(JFileChooser.OPEN_DIALOG);
		int returnVal = chooser.showOpenDialog(null);
		if (returnVal != JFileChooser.APPROVE_OPTION) return;
		String fileName = chooser.getSelectedFile().getPath();
		if (fileName == null) return;
		if (fileName.length() == 0) return;
		
		model.txtLoad(fileName);
		tree.setModel(model);
		rootField.setEditable(false);
		setRootBtn.setEnabled(false);	
		tree.updateUI();
	}
	
	public void textSave() {
		JFileChooser chooser = new JFileChooser(System.getProperty("user.dir"));
		FileNameExtensionFilter filter = new FileNameExtensionFilter(".txt", "txt");
		chooser.setFileFilter(filter);
		chooser.setFileSelectionMode(JFileChooser.FILES_AND_DIRECTORIES);
		chooser.setDialogType(JFileChooser.SAVE_DIALOG);
		int returnVal = chooser.showSaveDialog(null);
		if (returnVal != JFileChooser.APPROVE_OPTION) return;
		String fileName = chooser.getSelectedFile().getPath();
		if (fileName == null) return;
		if (fileName.length() == 0) return;
		
		model.txtSave(fileName);
	}
	
}

 

TreeFrame은 GUI를 약간 고쳐주고 save, load 작업을 수행하기 위한 dialog를 불러오게 해줄 menuBar을 달아주었다. renderer도 tree에다 set 해주었다. 

 

import java.io.*;
import java.util.*;

import javax.swing.tree.*;

public class TreeNode implements Serializable {

	private String data;
	private LinkedList<TreeNode> children;
	private TreeNode parent;
	
	TreeNode(String s) {
		data = s;
		children = new LinkedList<TreeNode>();
		parent = null;
	}
	
	String getData() {
		return data;
	}
	
	public String toString() {
		return data;
	}
	
	public boolean isLeaf() {
		if (children.size() == 0) return true;
		return false;
	}
	
	public int getChildCount() {
		return children.size();
	}
	
	public Object getChildAt(int i) {
		return children.get(i);
	}
	
	public int getIndexOfChild(TreeNode child) {
		return children.indexOf(child);
	}
	
	void depthFirstTraverse() {
		System.out.println(data);
		
		ListIterator<TreeNode> i = children.listIterator();
		while (i.hasNext()) {
			TreeNode child = i.next();
			child.depthFirstTraverse();
		}
	}
	
	String dfsSave() {
		String s = "";
		
		ListIterator<TreeNode> i = children.listIterator();
		while (i.hasNext()) {
			TreeNode child = i.next();
			s += data + " " + child.data + "\n";
			child.dfsSave();
		}
		return s;
	}
	
	public int getDepth() {
		int depth = 0;
		int maxDepth = 0;
		ListIterator<TreeNode> i = children.listIterator();
		while (i.hasNext()) {
			TreeNode child = i.next();
			depth = child.getDepth();
			if (maxDepth < depth) maxDepth = depth;
		}
		return maxDepth + 1;
	}
	
	public int getNumberOfLeafNodes() {
		if (children.size() == 0) return 1;
		
		int total = 0;
		ListIterator<TreeNode> i = children.listIterator();
		while (i.hasNext()) {
			TreeNode child = i.next();
			total += child.getNumberOfLeafNodes();			
		}
		return total;
	}
	
	public int getNoneLeafNodesNumber() {
		if (children.size() == 0) return 0;
		
		int total = 0;
		ListIterator<TreeNode> i = children.listIterator();
		while (i.hasNext()) {
			TreeNode child = i.next();
			total += child.getNoneLeafNodesNumber();	
		}
		return total + 1;
	}
	
	public int getTotalNodesNumber() {
		if (children.size() == 0) return 1;
		
		int total = 0;
		ListIterator<TreeNode> i = children.listIterator();
		while (i.hasNext()) {
			TreeNode child = i.next();
			total += child.getTotalNodesNumber();
		}
		return total + 1;
	}
	
	void depthFirstEnumeration(LinkedList<String> pEnumeration) {
		pEnumeration.addLast(data);
		ListIterator<TreeNode> i = children.listIterator();
		while(i.hasNext()) {
			TreeNode child = i.next();
			child.depthFirstEnumeration(pEnumeration);
		}
	}
	
	TreeNode find(String s) {
		if (data.equals(s)) return this;
		
		ListIterator<TreeNode> i = children.listIterator();
		while (i.hasNext()) {
			TreeNode child = i.next();
			TreeNode foundNode = child.find(s);
			if (foundNode != null) return foundNode;
		}
		return null;
	}
	
	TreeNode addChild(String s) {
		TreeNode pNewNode = new TreeNode(s);
		children.addLast(pNewNode);
		pNewNode.parent = this;		
		return pNewNode;
	}
	
	TreePath constructTreePath() {
		ArrayList<TreeNode> list = new ArrayList<TreeNode>();
		TreeNode tmp = parent;
		while (tmp != null) {
			list.add(tmp);
			tmp = tmp.parent;
		}
		
		TreeNode nodes[] = new TreeNode[list.size()];
		
		/*for (int i = 0; i < list.size(); i++) {
			nodes[i] = list.get(list.size()-i-1);
		}
		return new TreePath(nodes);*/
		int i = 0;
		ListIterator<TreeNode> li = list.listIterator(list.size()); 
		while (li.hasPrevious()) {
			TreeNode node = li.previous();
			nodes[i++] = node;
		}
		
		return new TreePath(nodes);
		/*if (parent == null) {
			return new TreePath(this);
		} else {
			TreePath path = parent.constructTreePath();
			
			if (isLeaf()) {
				return path;
			} else {
				return path.pathByAddingChild(this);
			}
		}*/
	}
	
	void deleteChild(int i) {
		children.remove(i);
	}	
}

 

TreePath를 통해 expand 작업을 하기 위해서 상위 노드를 가리키는 레퍼런스 노드 parent를 둔다. constructTreePath 메서드는 parent 노드들을 nodes라는 배열에 보관하여 TreePath 타입으로 만들어 돌려준다. 만드는 방법은 list에 담긴 parent 값들을 뒤에서부터 가져와서 nodes에 넣는다. for문을 쓰는 방법, iterator의 previous 메서드로 반대로 읽어와서 넣는 방법, constructTreePath를 계속 호출하여 recursion하게 path를 만드는 방법 등으로 표현하였다.

 

 

여기까지가 version 2

============================================================================================

 

import java.awt.*;

import javax.swing.*;
import javax.swing.tree.*;

public class MyTreeCellRenderer implements TreeCellRenderer {	
	public Component getTreeCellRendererComponent(JTree tree, Object value,
			boolean selected, boolean expanded, boolean leaf, int row, boolean hasFocus) {
		return new JPanel() {
			public void paintComponent(Graphics g) {
				if (!(value instanceof StringTreeNode)) return;
				if (selected) {
					g.setColor(new Color(150,150,255));
					g.fillRect(0, 0, 80, 20);
				}
				g.setColor(Color.black);
				String s = ((StringTreeNode)value).getData();
				g.drawString(s, 15, 10);
				if (leaf) {
					g.drawImage((new ImageIcon("red-ball.gif")).getImage(), 1, 1, tree);
				} else {
					g.drawImage((new ImageIcon("blue-ball.gif")).getImage(), 1, 1, tree);	
				}
			}
			public Dimension getPreferredSize() {
				return new Dimension(80,20);
			}
		};
	}	
}

 

 

CellRenderer이다. 셀 선택시 푸른색으로 셀을 채워주는 것 정도만 추가되었다.

 

import java.io.*;
import java.util.*;

import javax.swing.event.*;
import javax.swing.tree.*;

public class MyTreeModel extends DefaultTreeModel {

	private TreeNode root;
	
	MyTreeModel() {
		root = null;
	}
	
	void setRoot(String s) {
		root = new StringTreeNode(s);
	}

	TreePath addNewChild(String parent, String child) {
		if (root == null) return null;
		TreeNode pNode = ((StringTreeNode)root).find(parent);
		if (pNode == null) return null;
		TreeNode pChildNode = ((StringTreeNode)pNode).addChild(child);
		
		TreePath path = ((StringTreeNode)pChildNode).constructTreePath();
		return path;
	}
	
	boolean deleteChild(String parent, String child) {
		if (root == null) return false;
		TreeNode pNode = ((StringTreeNode)root).find(parent);
		if (pNode == null) return false;
		TreeNode cNode = ((StringTreeNode)root).find(child);
		if (cNode == null) return false;
		
		StringTreeNode tmp = (StringTreeNode)pNode;
		tmp.deleteChild(tmp.getIndexOfChild(cNode));
		return true;
	}
	
	public boolean isLeaf(Object obj) {
		TreeNode node = (TreeNode)obj;
		
		return node.isLeaf();
	}
	
	public int getChildCount(Object obj) {
		TreeNode node = (TreeNode)obj;
		
		return node.getChildCount();
	}
	
	public Object getChild(Object obj, int index) {
		TreeNode node = (TreeNode)obj;
		
		return node.getChildAt(index);
	}
	
	public int getIndexOfChild(Object parent, Object child) {
		TreeNode node = (TreeNode)parent;
		
		return node.getIndex((TreeNode)child);
	}
	
	public Object getRoot() {
		return root;
	}
	
	public void serialLoad(String fileName) {
		try {
			FileInputStream fis = new FileInputStream(fileName);
			ObjectInputStream ois = new ObjectInputStream(fis);
			root = (StringTreeNode)ois.readObject();
			ois.close();
			fis.close();
		} catch (Exception e) {
			e.printStackTrace();
		}
	}
	
	public void serialSave(String fileName) {
		try {
			FileOutputStream fos = new FileOutputStream(fileName);
			ObjectOutputStream oos = new ObjectOutputStream(fos);
			oos.writeObject(root);
			oos.flush();
			oos.close();
			fos.close();
		} catch (IOException e) {
			e.printStackTrace();
		}
	}
	
	public void txtLoad(String fileName) {
		try {
			BufferedReader br = new BufferedReader(new FileReader(new File(fileName)));
			
			String s = br.readLine();
			System.out.println(s);
			setRoot(s);
			while ((s = br.readLine()) != null) {
				if (s.length() == 0) break;
				String[] token = s.split(" ");
				addNewChild(token[0], token[1]);
			}
			
			br.close();
			
		} catch (IOException e) {
			e.printStackTrace();
		}
	}
	
	public void txtSave(String fileName) {
		try {
			FileWriter fw = new FileWriter(new File(fileName));
			
			String s = ((StringTreeNode)root).getData() + "\n";
			s += ((StringTreeNode)root).dfsSave();
			fw.write(s);
			
			fw.flush();			
			fw.close();
		} catch (IOException e) {
			e.printStackTrace();
		}
	}
	
	public int getDepth() {
		if (root == null) return 0;
		return ((StringTreeNode)root).getDepth();
	}
	
	public int getNumberOfLeafNodes() {
		if (root == null) return 0;
		return ((StringTreeNode)root).getNumberOfLeafNodes();
	}
	
	public int getNoneLeafNodesNumber() {
		if (root == null) return 0;
		return ((StringTreeNode)root).getNoneLeafNodesNumber();
	}
	
	public int getTotalNodesNumber() {
		if (root == null) return 0;
		return ((StringTreeNode)root).getTotalNodesNumber();
	}
	
	public void addTreeModelListener(TreeModelListener l) {
		
	}
	
	public void removeTreeModelListener(TreeModelListener l) {
		
	}
	
	public void valueForPathChanged(TreePath path, Object newValue) {
		
	}
	
}

 

TreeModel과 Serializable을 implements 받던 이전 버전과 달리 DefaultTreeModel을 상속받아 사용하였다. DefaultTreeModel은 Serializable과 TreeModel을 동시에 implements한다. 기존에 만들어놨던 TreeNode 클래스를 라이브러리에 있는 TreeNode와 구분하기 위해서 StringTreeNode 클래스로 바꿨다. StringTreeNode 클래스는 TreeNode 클래스를 implements 할 것이다. 그래서 TreeNode에 있는 메소드들은 호환이 될 것이나 TreeNode 라이브러리에 포함되어있지 않은 dfsSave, getTotalNodesNumber 등의 메서드들은 StringTreeNode로 다운 캐스팅을 해줬다.

 

import java.io.*;
import java.util.*;

import javax.swing.tree.*;

class EnumerationAdapter<E> implements Enumeration<E> {
	Iterator<E> it;
	public EnumerationAdapter(Iterator<E> it) {
		this.it = it;
	}
	public boolean hasMoreElements() {
		return it.hasNext();
	}
	public E nextElement() {
		return it.next();
	}
}

public class StringTreeNode implements TreeNode, Serializable {

	private String data;
	private LinkedList<TreeNode> children;
	private TreeNode parent;
	
	StringTreeNode(String s) {
		data = s;
		children = new LinkedList<TreeNode>();
		parent = null;
	}
	
	String getData() {
		return data;
	}
	
	public int getIndex(TreeNode node) {
		return children.indexOf(node);
	}
	
	public TreeNode getParent() {
		return parent;
	}
	
	public String toString() {
		return data;
	}
	
	public boolean isLeaf() {
		if (children.size() == 0) return true;
		return false;
	}
	
	public Enumeration children() {
		return new EnumerationAdapter(children.listIterator());
	}
	
	public boolean getAllowsChildren() {
		return true;
	}
	
	public int getChildCount() {
		return children.size();
	}
	
	public TreeNode getChildAt(int i) {
		return children.get(i);
	}
	
	public int getIndexOfChild(TreeNode child) {
		return children.indexOf(child);
	}
	
	void depthFirstTraverse() {
		System.out.println(data);
		
		ListIterator<TreeNode> i = children.listIterator();
		while (i.hasNext()) {
			StringTreeNode child = (StringTreeNode)i.next();
			child.depthFirstTraverse();
		}
	}
	
	String dfsSave() {
		String s = "";
		
		ListIterator<TreeNode> i = children.listIterator();
		while (i.hasNext()) {
			StringTreeNode child = (StringTreeNode)i.next();
			s += data + " " + child.data + "\n";
			child.dfsSave();
		}
		return s;
	}
	
	public int getDepth() {
		int depth = 0;
		int maxDepth = 0;
		ListIterator<TreeNode> i = children.listIterator();
		while (i.hasNext()) {
			StringTreeNode child = (StringTreeNode)i.next();
			depth = child.getDepth();
			if (maxDepth < depth) maxDepth = depth;
		}
		return maxDepth + 1;
	}
	
	public int getNumberOfLeafNodes() {
		if (children.size() == 0) return 1;
		
		int total = 0;
		ListIterator<TreeNode> i = children.listIterator();
		while (i.hasNext()) {
			StringTreeNode child = (StringTreeNode)i.next();
			total += child.getNumberOfLeafNodes();			
		}
		return total;
	}
	
	public int getNoneLeafNodesNumber() {
		if (children.size() == 0) return 0;
		
		int total = 0;
		ListIterator<TreeNode> i = children.listIterator();
		while (i.hasNext()) {
			StringTreeNode child = (StringTreeNode)i.next();
			total += child.getNoneLeafNodesNumber();	
		}
		return total + 1;
	}
	
	public int getTotalNodesNumber() {
		if (children.size() == 0) return 1;
		
		int total = 0;
		ListIterator<TreeNode> i = children.listIterator();
		while (i.hasNext()) {
			StringTreeNode child = (StringTreeNode)i.next();
			total += child.getTotalNodesNumber();
		}
		return total + 1;
	}
	
	void depthFirstEnumeration(LinkedList<String> pEnumeration) {
		pEnumeration.addLast(data);
		ListIterator<TreeNode> i = children.listIterator();
		while(i.hasNext()) {
			StringTreeNode child = (StringTreeNode)i.next();
			child.depthFirstEnumeration(pEnumeration);
		}
	}
	
	TreeNode find(String s) {
		if (data.equals(s)) return this;
		
		ListIterator<TreeNode> i = children.listIterator();
		while (i.hasNext()) {
			StringTreeNode child = (StringTreeNode)i.next();
			TreeNode foundNode = child.find(s);
			if (foundNode != null) return foundNode;
		}
		return null;
	}
	
	TreeNode addChild(String s) {
		StringTreeNode pNewNode = new StringTreeNode(s);
		children.addLast(pNewNode);
		pNewNode.parent = this;
		return pNewNode;
	}
	
	TreePath constructTreePath() {
		ArrayList<TreeNode> list = new ArrayList<TreeNode>();
		StringTreeNode tmp = (StringTreeNode)parent;
		while (tmp != null) {
			list.add(tmp);
			tmp = (StringTreeNode)tmp.parent;
		}
		
		TreeNode nodes[] = new TreeNode[list.size()];
		
		/*for (int i = 0; i < list.size(); i++) {
			nodes[i] = list.get(list.size()-i-1);
		}
		return new TreePath(nodes);*/
		int i = 0;
		ListIterator<TreeNode> li = list.listIterator(list.size()); 
		while (li.hasPrevious()) {
			TreeNode node = li.previous();
			nodes[i++] = node;
		}
		
		return new TreePath(nodes);
		/*if (parent == null) {
			return new TreePath(this);
		} else {
			TreePath path = parent.constructTreePath();
			
			if (isLeaf()) {
				return path;
			} else {
				return path.pathByAddingChild(this);
			}
		}*/
	}
	
	void deleteChild(int i) {
		children.remove(i);
	}	
}

 

기존 TreeNode를 StringTreeNode 클래스로 바꾸고 라이브러리의 TreeNode를 implements 해준다. TreeNode가 갖고 있는 메서드들 중 없던것들은 추가해주고 타입이 안맞던 메서드는 타입에 맞게 다시 수정해줬다. children 메서드 같은 경우는 리턴 타입이 Enumeration이라 iterator를 enumeration으로 바꿔서 쓰는것을 가능하게 해주는 EnumerationAdapter 클래스를 만들어 줬다. 하위 노드들을 가리키는 레퍼런스들의 객체 children이 linked list로 구현되어 있기 때문에 Enumeration을 사용할 수 없고 iterator를 사용하여야 한다. (children이 vector로 구현되었다면 enumeration 바로 사용가능함)

 

import java.awt.*;

import javax.swing.*;
import javax.swing.filechooser.FileNameExtensionFilter;
import javax.swing.tree.*;

public class TreeFrame extends JFrame {
	static int WIDTH = 600;
	static int HEIGHT = 800;
	JTree tree;
	JPanel panel;
	MyTreeModel model;
	JButton setRootBtn;
	JTextField rootField;
	JLabel parentLabel;
	JTextField parentField;
	JLabel childLabel;
	JTextField childField;
	JButton addBtn;
	JButton deleteBtn;
	JScrollPane sp;
	JMenuBar menuBar;
	JMenu menu;
	JSplitPane splitPane;
	JPanel funcPanel;
	JMenu serMenu;
	JMenu txtMenu;
	JMenuItem serSave;
	JMenuItem serLoad;
	JMenuItem txtSave;
	JMenuItem txtLoad;
	
	TreeFrame() {
		menuBar = new JMenuBar();
		setJMenuBar(menuBar);
		
		menu = new JMenu("File");
		menuBar.add(menu);
		
		serMenu = new JMenu("Serialize");
		txtMenu = new JMenu("Text");
		
		serLoad = new JMenuItem("Load");
		serSave = new JMenuItem("Save");
		txtLoad = new JMenuItem("Load");
		txtSave = new JMenuItem("Save");

		serMenu.add(serLoad);
		serMenu.add(serSave);
		txtMenu.add(txtLoad);
		txtMenu.add(txtSave);
		serLoad.addActionListener((e) -> serialLoad());
		serSave.addActionListener((e) -> serialSave());
		txtLoad.addActionListener((e) -> textLoad());
		txtSave.addActionListener((e) -> textSave());
		
		menu.add(serMenu);
		menu.add(txtMenu);
		
		panel = new JPanel();
		funcPanel = new JPanel();
		funcPanel.setLayout(new BoxLayout(funcPanel, BoxLayout.LINE_AXIS));
		
		model = new MyTreeModel();
		tree = new JTree();
		sp = new JScrollPane(tree);
		sp.setPreferredSize(new Dimension(WIDTH/3*2,HEIGHT/6*5));
		
		splitPane = new JSplitPane(JSplitPane.VERTICAL_SPLIT, sp, funcPanel);
		panel.add(splitPane);
		
		rootField = new JTextField(10);
		parentField = new JTextField(5);
		parentLabel = new JLabel("Parent");
		parentField.setEnabled(false);
		childField = new JTextField(5);		
		childLabel = new JLabel("Child");
		childField.setEnabled(false);
		
		setRootBtn = new JButton("Root 설정");
		setRootBtn.addActionListener((e) -> {
			if (rootField.getText() == null) return;
			model.setRoot(rootField.getText());
			tree.setModel(model);
			
			rootField.setEditable(false);
			setRootBtn.setEnabled(false);	
			parentField.setEnabled(true);
			childField.setEnabled(true);
		});
		
		JPanel p1 = new JPanel();
		p1.add(rootField);
		p1.add(setRootBtn);
		funcPanel.add(p1);
				
		JPanel p2 = new JPanel();
		p2.add(parentLabel);
		p2.add(parentField);
		p2.add(childLabel);
		p2.add(childField);
		funcPanel.add(p2);
		
		addBtn = new JButton("Add");
		addBtn.addActionListener((e) -> {
			String parent = parentField.getText();
			String child = childField.getText();
			
			TreePath path = model.addNewChild(parent, child);
			tree.expandPath(path);
			tree.updateUI();
		});
		
		deleteBtn = new JButton("Delete");
		deleteBtn.addActionListener((e) -> {
			String parent = parentField.getText();
			String child = childField.getText();
			
			model.deleteChild(parent, child);
			tree.updateUI();
		});
		
		JPanel p3 = new JPanel();
		p3.add(addBtn);
		p3.add(deleteBtn);
		funcPanel.add(p3);
		
		MyTreeCellRenderer renderer = new MyTreeCellRenderer();
		tree.setCellRenderer(renderer);
		
		getContentPane().add(panel);
		
		setSize(WIDTH,HEIGHT);
		setDefaultCloseOperation(EXIT_ON_CLOSE);
		setLocationRelativeTo(null);
	}
	
	public void serialLoad() {
		JFileChooser chooser = new JFileChooser(System.getProperty("user.dir"));
		chooser.setFileSelectionMode(JFileChooser.FILES_AND_DIRECTORIES);
		chooser.setDialogType(JFileChooser.OPEN_DIALOG);
		int returnVal = chooser.showOpenDialog(null);
		if (returnVal != JFileChooser.APPROVE_OPTION) return;
		String fileName = chooser.getSelectedFile().getPath();
		if (fileName == null) return;
		if (fileName.length() == 0) return;
		
		model.serialLoad(fileName);
		tree.setModel(model);
		rootField.setEditable(false);
		setRootBtn.setEnabled(false);	
		tree.updateUI();
	}
	
	public void serialSave() {
		JFileChooser chooser = new JFileChooser(System.getProperty("user.dir"));
		chooser.setFileSelectionMode(JFileChooser.FILES_AND_DIRECTORIES);
		chooser.setDialogType(JFileChooser.SAVE_DIALOG);
		int returnVal = chooser.showSaveDialog(null);
		if (returnVal != JFileChooser.APPROVE_OPTION) return;
		String fileName = chooser.getSelectedFile().getPath();
		if (fileName == null) return;
		if (fileName.length() == 0) return;
		
		model.serialSave(fileName);
	}
	
	public void textLoad() {
		JFileChooser chooser = new JFileChooser(System.getProperty("user.dir"));
		chooser.setFileSelectionMode(JFileChooser.FILES_AND_DIRECTORIES);
		chooser.setDialogType(JFileChooser.OPEN_DIALOG);
		int returnVal = chooser.showOpenDialog(null);
		if (returnVal != JFileChooser.APPROVE_OPTION) return;
		String fileName = chooser.getSelectedFile().getPath();
		if (fileName == null) return;
		if (fileName.length() == 0) return;
		
		model.txtLoad(fileName);
		tree.setModel(model);
		rootField.setEditable(false);
		setRootBtn.setEnabled(false);	
		tree.updateUI();
	}
	
	public void textSave() {
		JFileChooser chooser = new JFileChooser(System.getProperty("user.dir"));
		FileNameExtensionFilter filter = new FileNameExtensionFilter(".txt", "txt");
		chooser.setFileFilter(filter);
		chooser.setFileSelectionMode(JFileChooser.FILES_AND_DIRECTORIES);
		chooser.setDialogType(JFileChooser.SAVE_DIALOG);
		int returnVal = chooser.showSaveDialog(null);
		if (returnVal != JFileChooser.APPROVE_OPTION) return;
		String fileName = chooser.getSelectedFile().getPath();
		if (fileName == null) return;
		if (fileName.length() == 0) return;
		
		model.txtSave(fileName);
	}
	
}

 

frame 부분은 약간의 gui 수정만 해준다.

 

 

여기까지가 version 3

============================================================================================

 

import java.awt.*;

import javax.swing.*;
import javax.swing.tree.*;

public class MyTreeCellRenderer implements TreeCellRenderer {	
	public Component getTreeCellRendererComponent(JTree tree, Object value,
			boolean selected, boolean expanded, boolean leaf, int row, boolean hasFocus) {
		return new JPanel() {
			public void paintComponent(Graphics g) {
				if (!(value instanceof DefaultMutableTreeNode)) return;
				if (selected) {
					g.setColor(new Color(150,150,255));
					g.fillRect(0, 0, 600, 20);
				}
				g.setColor(Color.black);
				String s = "" + ((DefaultMutableTreeNode)value).getUserObject();
				g.drawString(s, 15, 10);
				if (leaf) {
					g.drawImage((new ImageIcon("red-ball.gif")).getImage(), 1, 1, tree);
				} else {
					g.drawImage((new ImageIcon("blue-ball.gif")).getImage(), 1, 1, tree);	
				}
			}
			public Dimension getPreferredSize() {
				return new Dimension(600,20);
			}
		};
	}	
}

 

StringTreeNode가 DefaultMutableTreeNode를 상속받도록 할 것이다. CellRenderer는 크게 수정사항이 없다.

 

import java.io.*;
import java.util.*;

import javax.swing.JOptionPane;
import javax.swing.event.*;
import javax.swing.tree.*;

public class MyTreeModel extends DefaultTreeModel {

	MyTreeModel() {
		super(null);
	}
	
	void setRoot(String s) {
		root = new StringTreeNode(s);
		super.setRoot(root);
	}

	void remove(TreePath path) {
		DefaultMutableTreeNode node = (DefaultMutableTreeNode)path.getLastPathComponent();
		if (!node.isLeaf()) {
			JOptionPane.showMessageDialog(null, "Please select terminal node");
			return;
		}
		DefaultMutableTreeNode parent = (DefaultMutableTreeNode)node.getParent();
		parent.remove(node);
	}
	
	TreePath addNewChild(String parent, String child) {
		if (root == null) return null;
		TreeNode pNode = ((StringTreeNode)root).find(parent);
		if (pNode == null) return null;
		TreeNode pChildNode = ((StringTreeNode)pNode).addChild(child);
		
		TreePath path = ((StringTreeNode)pChildNode).constructTreePath();
		return path;
	}
	
	boolean deleteChild(String parent, String child) {
		if (root == null) return false;
		TreeNode pNode = ((StringTreeNode)root).find(parent);
		if (pNode == null) return false;
		TreeNode cNode = ((StringTreeNode)root).find(child);
		if (cNode == null) return false;
		
		StringTreeNode tmp = (StringTreeNode)pNode;
		tmp.deleteChild(tmp.getIndex(cNode));
		return true;
	}
	
	public void serialLoad(String fileName) {
		try {
			FileInputStream fis = new FileInputStream(fileName);
			ObjectInputStream ois = new ObjectInputStream(fis);
			root = (StringTreeNode)ois.readObject();
			ois.close();
			fis.close();
		} catch (Exception e) {
			e.printStackTrace();
		}
	}
	
	public void serialSave(String fileName) {
		try {
			FileOutputStream fos = new FileOutputStream(fileName);
			ObjectOutputStream oos = new ObjectOutputStream(fos);
			oos.writeObject(root);
			oos.flush();
			oos.close();
			fos.close();
		} catch (IOException e) {
			e.printStackTrace();
		}
	}
	
	public void txtLoad(String fileName) {
		try {
			BufferedReader br = new BufferedReader(new FileReader(new File(fileName)));
			
			String s = br.readLine();
			System.out.println(s);
			setRoot(s);
			while ((s = br.readLine()) != null) {
				if (s.length() == 0) break;
				String[] token = s.split(" ");
				addNewChild(token[0], token[1]);
			}
			
			br.close();
			
		} catch (IOException e) {
			e.printStackTrace();
		}
	}
	
	public void txtSave(String fileName) {
		try {
			FileWriter fw = new FileWriter(new File(fileName));
			
			String s = ((StringTreeNode)root).getData() + "\n";
			s += ((StringTreeNode)root).dfsSave();
			fw.write(s);
			
			fw.flush();			
			fw.close();
		} catch (IOException e) {
			e.printStackTrace();
		}
	}
	
	public int getDepth() {
		if (root == null) return 0;
		return ((StringTreeNode)root).getDepth();
	}
	
	public int getNumberOfLeafNodes() {
		if (root == null) return 0;
		return ((StringTreeNode)root).getNumberOfLeafNodes();
	}
	
	public int getNoneLeafNodesNumber() {
		if (root == null) return 0;
		return ((StringTreeNode)root).getNoneLeafNodesNumber();
	}
	
	public int getTotalNodesNumber() {
		if (root == null) return 0;
		return ((StringTreeNode)root).getTotalNodesNumber();
	}
	
	public void addTreeModelListener(TreeModelListener l) {
		
	}
	
	public void removeTreeModelListener(TreeModelListener l) {
		
	}
	
	public void valueForPathChanged(TreePath path, Object newValue) {
		
	}
	
}

 

TreeModel도 DefaultTreeModel을 상속받아 사용하기로 한다. 웬만한 메서드들은 super클래스에서 그대로 받아와서 사용한다. remove 메서드는 path값을 받아와서 해당 노드가 leaf 노드면 remove를 하도록 한다. 

 

import java.io.*;
import java.util.*;

import javax.swing.tree.*;

public class StringTreeNode extends DefaultMutableTreeNode {
	
	StringTreeNode(String s) {
		super(s);
	}
	
	String getData() {
		return (String)userObject;
	}
	
	public String toString() {
		return (String)userObject;
	}
	
	void depthFirstTraverse() {
		System.out.println(userObject);
		
		ListIterator<TreeNode> i = children.listIterator();
		while (i.hasNext()) {
			StringTreeNode child = (StringTreeNode)i.next();
			child.depthFirstTraverse();
		}
	}
	
	String dfsSave() {
		String s = "";
		
		ListIterator<TreeNode> i = children.listIterator();
		while (i.hasNext()) {
			StringTreeNode child = (StringTreeNode)i.next();
			s += userObject + " " + child.userObject + "\n";
			child.dfsSave();
		}
		return s;
	}
	
	public int getDepth() {
		int depth = 0;
		int maxDepth = 0;
		ListIterator<TreeNode> i = children.listIterator();
		while (i.hasNext()) {
			StringTreeNode child = (StringTreeNode)i.next();
			depth = child.getDepth();
			if (maxDepth < depth) maxDepth = depth;
		}
		return maxDepth + 1;
	}
	
	public int getNumberOfLeafNodes() {
		if (children.size() == 0) return 1;
		
		int total = 0;
		ListIterator<TreeNode> i = children.listIterator();
		while (i.hasNext()) {
			StringTreeNode child = (StringTreeNode)i.next();
			total += child.getNumberOfLeafNodes();			
		}
		return total;
	}
	
	public int getNoneLeafNodesNumber() {
		if (children.size() == 0) return 0;
		
		int total = 0;
		ListIterator<TreeNode> i = children.listIterator();
		while (i.hasNext()) {
			StringTreeNode child = (StringTreeNode)i.next();
			total += child.getNoneLeafNodesNumber();	
		}
		return total + 1;
	}
	
	public int getTotalNodesNumber() {
		if (children.size() == 0) return 1;
		
		int total = 0;
		ListIterator<TreeNode> i = children.listIterator();
		while (i.hasNext()) {
			StringTreeNode child = (StringTreeNode)i.next();
			total += child.getTotalNodesNumber();
		}
		return total + 1;
	}
	
	void depthFirstEnumeration(LinkedList<String> pEnumeration) {
		pEnumeration.addLast((String)userObject);
		ListIterator<TreeNode> i = children.listIterator();
		while(i.hasNext()) {
			StringTreeNode child = (StringTreeNode)i.next();
			child.depthFirstEnumeration(pEnumeration);
		}
	}
	
	TreeNode find(String s) {
		if (userObject.equals(s)) return this;
		
		ListIterator<TreeNode> i = children.listIterator();
		while (i.hasNext()) {
			StringTreeNode child = (StringTreeNode)i.next();
			TreeNode foundNode = child.find(s);
			if (foundNode != null) return foundNode;
		}
		return null;
	}
	
	TreeNode addChild(String s) {
		StringTreeNode pNewNode = new StringTreeNode(s);
		add(pNewNode);
		return pNewNode;
	}
	
	TreePath constructTreePath() {
		ArrayList<TreeNode> list = new ArrayList<TreeNode>();
		StringTreeNode tmp = (StringTreeNode)parent;
		while (tmp != null) {
			list.add(tmp);
			tmp = (StringTreeNode)tmp.parent;
		}
		
		TreeNode nodes[] = new TreeNode[list.size()];
		
		/*for (int i = 0; i < list.size(); i++) {
			nodes[i] = list.get(list.size()-i-1);
		}
		return new TreePath(nodes);*/
		int i = 0;
		ListIterator<TreeNode> li = list.listIterator(list.size()); 
		while (li.hasPrevious()) {
			TreeNode node = li.previous();
			nodes[i++] = node;
		}
		
		return new TreePath(nodes);
		/*if (parent == null) {
			return new TreePath(this);
		} else {
			TreePath path = parent.constructTreePath();
			
			if (isLeaf()) {
				return path;
			} else {
				return path.pathByAddingChild(this);
			}
		}*/
	}
	
	void deleteChild(int i) {
		children.remove(i);
	}	
}

 

StringTreeNode는 DefaultMutableTreeNode를 상속받고 구체적인 데이터값등은 슈퍼클래스에서 받아쓰도록 한다. 기존의 String 데이터는 상위클래스에 protected Object 타입의 userObject를 타입 캐스팅하여 저장한다. 기존의 TreeNode에서 implement 받던 메서드들은 전부 지워줬다. 개인적으로 이렇게 막연히 코드 수만 줄이는 방식보다는 이전 방식이 사용하기 더 좋은것 같다. 미리 만들어놨던 클래스들의 재사용성 부분에 있어서도 말이다.

 

import java.awt.*;
import java.awt.event.*;

import javax.swing.*;
import javax.swing.filechooser.FileNameExtensionFilter;
import javax.swing.tree.*;

class MainPanel extends JPanel {
	JTree tree;
	MyTreeModel model;
	JButton setRootBtn;
	JTextField rootField;
	JLabel parentLabel;
	JTextField parentField;
	JLabel childLabel;
	JTextField childField;
	JButton addBtn;
	JButton deleteBtn;
	JScrollPane sp;
	JSplitPane splitPane;
	JPanel funcPanel;
	MainPanel() {		
		funcPanel = new JPanel();
		funcPanel.setLayout(new BoxLayout(funcPanel, BoxLayout.LINE_AXIS));
		
		model = new MyTreeModel();
		DefaultTreeModel tmpModel = new DefaultTreeModel(new DefaultMutableTreeNode("Please set the root node"));
		tree = new JTree(tmpModel);
		sp = new JScrollPane(tree);
		sp.setPreferredSize(new Dimension(600/3*2,800/6*5));
		
		splitPane = new JSplitPane(JSplitPane.VERTICAL_SPLIT, sp, funcPanel);
		
		rootField = new JTextField(10);
		parentField = new JTextField(5);
		parentLabel = new JLabel("Parent");
		parentField.setEnabled(false);
		childField = new JTextField(5);		
		childLabel = new JLabel("Child");
		childField.setEnabled(false);
		
		setRootBtn = new JButton("Root 설정");
		setRootBtn.addActionListener((e) -> {
			if (rootField.getText() == null) return;
			model.setRoot(rootField.getText());
			tree.setModel(model);
			
			rootField.setEditable(false);
			setRootBtn.setEnabled(false);	
			parentField.setEnabled(true);
			childField.setEnabled(true);
		});
		
		JPanel p1 = new JPanel();
		p1.add(rootField);
		p1.add(setRootBtn);
		funcPanel.add(p1);
				
		JPanel p2 = new JPanel();
		p2.add(parentLabel);
		p2.add(parentField);
		p2.add(childLabel);
		p2.add(childField);
		funcPanel.add(p2);
		
		addBtn = new JButton("Add");
		addBtn.addActionListener((e) -> {
			String parent = parentField.getText();
			String child = childField.getText();
			
			addBtnAction(parent, child);
		});
		
		deleteBtn = new JButton("Delete");
		deleteBtn.addActionListener((e) -> {
			TreePath selectedPath = tree.getSelectionPath();
			model.remove(selectedPath);
			//tree.removeSelectionPath(selectedPath);
			/*
			String parent = parentField.getText();
			String child = childField.getText();
			
			model.deleteChild(parent, child);*/
			tree.updateUI();
		});
		
		childField.addKeyListener(new KeyAdapter() {
			public void keyPressed(KeyEvent e) {
				String parentText = parentField.getText();
				String childText = childField.getText();
				if (parentText.isEmpty() || childText.isEmpty()) return;
				if (e.getKeyCode() == KeyEvent.VK_ENTER) {
					addBtnAction(parentText, childText);
				}
			}
		});
		
		JPanel p3 = new JPanel();
		p3.add(addBtn);
		p3.add(deleteBtn);
		funcPanel.add(p3);
		
		MyTreeCellRenderer renderer = new MyTreeCellRenderer();
		tree.setCellRenderer(renderer);
		
		add(splitPane);
	}
	
	public void addBtnAction(String parent, String child) {
		TreePath path = model.addNewChild(parent,child);
		tree.expandPath(path);
		parentField.setText("");
		childField.setText("");
		tree.updateUI();
	}
	
	public void serialLoad(String fileName) {
		model.serialLoad(fileName);
		tree.setModel(model);
		rootField.setEditable(false);
		setRootBtn.setEnabled(false);	
		tree.updateUI();
	}
	
	public MyTreeModel getModel() {
		return model;
	}
	
	public void textLoad(String fileName) {
		model.txtLoad(fileName);
		tree.setModel(model);
		rootField.setEditable(false);
		setRootBtn.setEnabled(false);	
		tree.updateUI();
	}
	
	public Dimension getPreferredSize() {
		return new Dimension(600,800);
	}
}

public class TreeFrame extends JFrame {
	MainPanel mainPanel;
	static int WIDTH = 600;
	static int HEIGHT = 800;
	JMenuBar menuBar;
	JMenu menu;
	JMenu serMenu;
	JMenu txtMenu;
	JMenuItem serSave;
	JMenuItem serLoad;
	JMenuItem txtSave;
	JMenuItem txtLoad;

	TreeFrame() {
		setTitle("Tree Test");
		setSize(WIDTH,HEIGHT);
		setDefaultCloseOperation(EXIT_ON_CLOSE);
		setLocationRelativeTo(null);
		
		menuBar = new JMenuBar();
		setJMenuBar(menuBar);
		
		menu = new JMenu("File");
		menuBar.add(menu);
		
		serMenu = new JMenu("Serialize");
		txtMenu = new JMenu("Text");
		
		serLoad = new JMenuItem("Load");
		serSave = new JMenuItem("Save");
		txtLoad = new JMenuItem("Load");
		txtSave = new JMenuItem("Save");

		serMenu.add(serLoad);
		serMenu.add(serSave);
		txtMenu.add(txtLoad);
		txtMenu.add(txtSave);
		serLoad.addActionListener((e) -> serialLoad());
		serSave.addActionListener((e) -> serialSave());
		txtLoad.addActionListener((e) -> textLoad());
		txtSave.addActionListener((e) -> textSave());
		
		menu.add(serMenu);
		menu.add(txtMenu);
		
		mainPanel = new MainPanel();
		getContentPane().add(mainPanel);
	}
	
	
	
	public void serialLoad() {
		JFileChooser chooser = new JFileChooser(System.getProperty("user.dir"));
		chooser.setFileSelectionMode(JFileChooser.FILES_AND_DIRECTORIES);
		chooser.setDialogType(JFileChooser.OPEN_DIALOG);
		int returnVal = chooser.showOpenDialog(null);
		if (returnVal != JFileChooser.APPROVE_OPTION) return;
		String fileName = chooser.getSelectedFile().getPath();
		if (fileName == null) return;
		if (fileName.length() == 0) return;
		
		mainPanel.serialLoad(fileName);
	}
	
	public void serialSave() {
		JFileChooser chooser = new JFileChooser(System.getProperty("user.dir"));
		chooser.setFileSelectionMode(JFileChooser.FILES_AND_DIRECTORIES);
		chooser.setDialogType(JFileChooser.SAVE_DIALOG);
		int returnVal = chooser.showSaveDialog(null);
		if (returnVal != JFileChooser.APPROVE_OPTION) return;
		String fileName = chooser.getSelectedFile().getPath();
		if (fileName == null) return;
		if (fileName.length() == 0) return;
		
		mainPanel.getModel().serialSave(fileName);
	}
	
	public void textLoad() {
		JFileChooser chooser = new JFileChooser(System.getProperty("user.dir"));
		chooser.setFileSelectionMode(JFileChooser.FILES_AND_DIRECTORIES);
		chooser.setDialogType(JFileChooser.OPEN_DIALOG);
		int returnVal = chooser.showOpenDialog(null);
		if (returnVal != JFileChooser.APPROVE_OPTION) return;
		String fileName = chooser.getSelectedFile().getPath();
		if (fileName == null) return;
		if (fileName.length() == 0) return;
		
		mainPanel.textLoad(fileName);
	}
	
	public void textSave() {
		JFileChooser chooser = new JFileChooser(System.getProperty("user.dir"));
		FileNameExtensionFilter filter = new FileNameExtensionFilter(".txt", "txt");
		chooser.setFileFilter(filter);
		chooser.setFileSelectionMode(JFileChooser.FILES_AND_DIRECTORIES);
		chooser.setDialogType(JFileChooser.SAVE_DIALOG);
		int returnVal = chooser.showSaveDialog(null);
		if (returnVal != JFileChooser.APPROVE_OPTION) return;
		String fileName = chooser.getSelectedFile().getPath();
		if (fileName == null) return;
		if (fileName.length() == 0) return;
		
		mainPanel.getModel().txtSave(fileName);
	}
	
}

 

frame에는 save, load 같은 최소한의 menuBar 작업만 두고 나머지 controller 요소들과 view적 요소들은 MainPanel로 보내버린다.

처음 실행시 tree view에 아무것도 뜨지 않는 것을 방지하기 위해 임시모델을 만들어 세팅해준다. childField에는 더 자연스런 인터페이스 느낌을 주기위해 엔터키로 바로 add 기능을 해주는 키 이벤트를 넣어준다.

 

TreeModel의 코딩 과정을 네가지 version으로 다루어 보았다.

 

 

반응형

댓글