先看渲染的例子:
我們爲我們的樹增加了可用與否、可見與否和節點圖片的屬性,當然你也可以自己添加想要的屬性,比如順序等.
整個實現的過程很清晰,也不復雜,我們首先實現我們自己的TreeNode,它繼承於DefaultMutableTreeNode,我們在裏面添加自己的屬性;然後是實現樹的節點的Renderer和Editor,在Renderer裏我們設置可用也否,選擇狀態,節點圖片等;在Editor裏我們設置樹的容器佈局.最後構造樹時使用setRenderer和setEditor就可以了,需要注意的是如果僅僅是呈現,設置Renderer就可以了,如果除了呈現還有操作的話,必須都要設置.
先看我們自己的TreeNode類, 繼承於DefaultMutableTreeNode, 我們添加了屬性: 節點的選擇狀態 節點使用與否 節點可見與否 節點圖片和圖片名字
然後是構造函數,我們初始化屬性,或使用默認的: 然後就是一些設置和取得屬性的方法,這個類就相當於一個Bean.
然後就是Rnederer了,我們這裏繼承於DefaultTreeCellRenderer 然後複寫它的getTreeCellRendererComponent方法: 然後設置節點的屬性:
首先是顯示文本: 然後是可用與否: 然後是是圖片 還有一些基本的屬性,比如背景色等: 再來需要看的類就是Editor了,它繼承於DefaultTreeCellEditor: 因爲我們使用的是JLabel作爲顯示控件,所以我們的主要實現是佈局處理,對於事件可以不需要考慮.我們複習了DefaultTreeCellEditor的默認佈局: 複寫它的doLayout方法: 取得節點的邊緣和大小: 設置節點的位置和大小: 最後就是使用了,很簡單和以前一樣,直接使用JTree的設置方法就可以了:
這次我們用樹實現一個比較綜合的例子,做一個類似Windows的資源管理器,先看Windows的,如下圖:
接着就是我們的實現了,路是一步一步走的,先看最基礎的實現,這個例子是我在網上看到的,雖然簡單,起碼是一種思路:
效果如下圖:
先說說這個實現的思路吧,首先創建樹,再創建一個根節點,然後取得盤符,把盤符放置在根節點下面,然後增加樹的監聽,當樹的節點展開時,取得當前節點下的所有文件和文件夾,加入到節點下,刷新樹,展開到指定位置就可以了.
先是創建根節點的:
public DefaultMutableTreeNode createRootNode(){
File dir = new File(".");
DefaultMutableTreeNode rootNode = new DefaultMutableTreeNode(ROOT_NAME);
for(int i = 0; i < dir.listRoots().length; i++){
if(dir.listRoots()[i].isDirectory()){
String rootPath = dir.listRoots()[i].getPath();
this.treeNode = new DefaultMutableTreeNode(rootPath);
rootNode.add(this.treeNode);
this.treeNode = null;
}
}
return rootNode;
}
然後是增加監聽,這裏我們監聽展開事件:
this.tree.addTreeExpansionListener(this);
然後是處理事件:
@Override
publicvoid treeExpanded(TreeExpansionEvent event) {
先取得選擇的節點對象:
this.selectNode
=(DefaultMutableTreeNode)event.getPath().getLastPathComponent();
然後取得節點的絕對路徑:
String path = event.getPath().toString();
然後根據路徑在節點下添加子節點:
publicvoid addTreeNode(DefaultMutableTreeNode node, File dir) {
if (node == null || dir == null) {
return;
}
if (!dir.isDirectory()) {
return;
}
if (!node.isRoot()) {
// get all files in node
File file[] = dir.listFiles();
for (int i = 0; i < file.length; i++) {
// hidden is not show
if (file[i].isDirectory() && !file[i].isHidden()) {
// create node
this.treeNode = new DefaultMutableTreeNode(dir.list()[i]);
// add to tree
((DefaultTreeModel) this.jt.getModel()).insertNodeInto(
treeNode, node, node.getChildCount());
this.treeNode = null;
}
}
}
}
同樣的收起事件也要處理:
@Override
publicvoid treeCollapsed(TreeExpansionEvent event) {
最後把樹放置在JScrollPane上就可以了.
this.jscroolpane.setViewportView(this.tree);
這樣一個簡單的資源管理樹就完成了,下面我們說說它的問題:
① 圖片和外觀和Windows相差太大
這個我們可以通過設置L&F和通過前面寫的Renderer那樣設置新的圖片解決,不是大問題.
② 文件夾裏文件多時展開會很慢,會導致界面假死
這個我們可以自己寫一個緩加載的TreeNode,讓它繼承於DefaultMutableTreeNode,在它裏面定義加載標示,然後使用SwingWorker或者多線程方式使Tree平穩加載,雖然麻煩,但是也可以解決.
③ Tree點擊假死時,用戶會以爲出現問題,胡亂點擊會加載多個事件
這個問題其實是Swing事件機制的問題,其實是沒辦法解決的,因爲總會存在耗時的操作的,不等待是不可能的.但我們可以做更好的用戶體驗來避免這個問題,這裏我想到的解決辦法是在Tree上繪製一層GlassPane,屏蔽所有事件,提示用戶,等加載完成後,取消GlassPane界面.
④ 只有我的電腦的基本文件,沒有網上鄰居之類的
這個問題很難解決,涉及到網上鄰居就存在網絡的問題了,還需要網絡連接和掃描,開始我的思路是使用Apache的commons-client做,後來發現有人給出了更好的辦法,使用Java的JFileChooser類,Java已經實現了很多我們需要實現的.
⑤ 取得的資源管理樹的子目錄是亂序的
這個很好解決,使我們的TreeNode實現Comparable接口就可以了.
爲了解決這五個問題我們做的改進版:
首先我們解決問題一,看看我們的代碼:
節點的圖片的樣式問題我們可以設置Renderer,又因爲這些圖片可以在JFileChooser的UI中取得,我們先參照JFileChooser的UI做一個FileView類:
// ***********************
// * FileView operations *
// ***********************
protectedclass BasicFileView extends FileView {
複寫它的方法:
@Override
public String getName(File f) {
// Note: Returns display name rather than file name
String fileName = null;
if (f != null) {
fileName = chooser.getFileSystemView().getSystemDisplayName(f);
}
return fileName;
}
這個是顯示名字.
@Override
public String getDescription(File f) {
return f.getName();
}
這個是描述
@Override
public String getTypeDescription(File f) {
String type = chooser.getFileSystemView().getSystemTypeDescription(
f);
if (type == null) {
if (f.isDirectory()) {
type = directoryDescriptionText;
} else {
type = fileDescriptionText;
}
}
return type;
}
這個是文件類別
@Override
public Icon getIcon(File f) {
這個是圖片表示.
這樣我們構建這個FileView之後我們需要的圖片和名字就都可以取得了.
然後是我們的Renderer了:
privateclass FileSystemTreeRenderer extends DefaultTreeCellRenderer {
複寫它的方法,設置我們從FileView取得圖片和名字:
@Override
public Component getTreeCellRendererComponent(JTree tree, Object value, boolean sel, booleanexpanded, boolean leaf, int row,
boolean hasFocus) {
setText(getFileView(chooser).getName(node.getFile()));
setIcon(getFileView(chooser).getIcon(node.getFile()));
然後設置到樹上:
tree.setCellRenderer(new FileSystemTreeRenderer());
看看效果:
是不是和Windows的很接近了,設置L&F,如下圖:
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
然後解決問題二,我們不能用樹的原始節點了,用我們自己構造的,繼承於它:
publicabstractclass LazyMutableTreeNode extends DefaultMutableTreeNode {
增加一個屬性:
/** is node load. */
privatebooleanloaded = false;
提供一個虛方法給子類實現:
protectedabstractvoid loadChildren();
然後是我們的實現:
privateclass FileTreeNode extends LazyMutableTreeNode {
複寫它的方法,非load不允許加載:
@Override
publicboolean isLeaf() {
if (!isLoaded()) {
returnfalse;
} else {
returnsuper.isLeaf();
}
}
還有它的現實名字:
@Override
public String toString() {
returnchooser.getFileSystemView().getSystemDisplayName(
(File) getUserObject());
}
實現虛方法:
@Override
protectedvoid loadChildren() {
FileTreeNode[] nodes = getChildren();
for (int i = 0, c = nodes.length; i < c; i++) {
add(nodes[i]);
}
}
這樣問題二就解決了,同時也可以在這裏解決我們的問題五,使我們的TreeNode實現Comparable接口:
privateclass FileTreeNode extends LazyMutableTreeNode implements
Comparable<Object> {
然後實現方法:
@Override
publicint compareTo(Object o) {
if (!(o instanceof FileTreeNode)) {
return 1;
}
return getFile().compareTo(((FileTreeNode) o).getFile());
}
最後在我們使用時:
// sort directories, FileTreeNode implements Comparable
FileTreeNode[] result = (FileTreeNode[]) nodes
.toArray(new FileTreeNode[0]);
Arrays.sort(result);
nodes.add(new FileTreeNode(result[i]));
這樣我們加入的節點文件夾就都是排序的了.
然後我們解決問題四,三比較麻煩留在最後:
構建這個組件時,我們先構建JFileChooser
JFileChooser chooser = new JFileChooser();
增加監聽:
protectedvoid installListeners() {
tree.addTreeSelectionListener(new SelectionListener());
chooser.getActionMap().put("refreshTree", new UpdateAction());
chooser.getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT).put(
KeyStroke.getKeyStroke("F5"), "refreshTree");
chooser.addPropertyChangeListener(new ChangeListener());
}
在監聽中展開樹時,使用JFileChooser的方法:
/**
* tree node select change.
*/
privateclass SelectionListener implements TreeSelectionListener {
@Override
publicvoid valueChanged(TreeSelectionEvent e) {
getApproveSelectionAction()
.setEnabled(tree.getSelectionCount() > 0);
setSelectedFiles();
// the current directory is the one currently selected
TreePath currentDirectoryPath = tree.getSelectionPath();
if (currentDirectoryPath != null) {
File currentDirectory = ((FileTreeNode) currentDirectoryPath
.getLastPathComponent()).getFile();
chooser.setCurrentDirectory(currentDirectory);
}
}
}
這樣我們所有的目錄結構就不需要自己去循環構建了,使用JFileChooser爲我們提供好的就可以了,如下圖,網上鄰居也有了,問題四完成了:
最後我們來解決問題三,爲什麼會假死,是因爲文件夾多或者網速慢導致的,解決辦法當然是多線程,但是多線程在Swing裏容易出現線程不安全,因爲它不在ADT上,這裏我們使用SwingWorker,監聽樹的展開事件:
tree.addTreeExpansionListener(new TreeExpansion());
處理它:
privateclass TreeExpansion implements TreeExpansionListener {
@Override
publicvoid treeCollapsed(TreeExpansionEvent event) {
}
@Override
publicvoid treeExpanded(TreeExpansionEvent event) {
// ensure children gets expanded later
if (event.getPath() != null) {
Object lastElement = event.getPath().getLastPathComponent();
if (lastElement instanceof FileTreeNode && useNodeQueue)
if (((FileTreeNode) lastElement).isLoaded()) {
慢主要是在這裏的處理,我們把它放在SwingWorker裏面:
new WorkerQueue(node, tree, glassPane).execute();
然後看這個類:
privatestaticfinalclass WorkerQueue extends
SwingWorker<Void, FileTreeNode> {
複寫它的方法,處理我們的TreeNode添加事件:
@Override
protected Void doInBackground() throws Exception {
glassPanel.setVisible(true);
for (Enumeration<?> e = node.children(); e.hasMoreElements();) {
publish((FileTreeNode) e.nextElement());
}
returnnull;
}
@Override
protectedvoid process(List<FileTreeNode> chunks) {
for (FileTreeNode fileTreeNode : chunks) {
fileTreeNode.getChildCount();
}
}
@Override
protectedvoid done() {
glassPanel.setVisible(false);
tree.repaint();
}
然後是處理我們在展開節點時屏蔽所有的鼠標點擊並給以用戶提示,這裏我們自己繪製一個Component,把它設置爲GlassPane,屏蔽所有事件:
/**
*/
publicclass GlassPane extends JComponent {
屏蔽所有事件,只能獲得焦點:
// blocks all user input
addMouseListener(new MouseAdapter() {
});
addMouseMotionListener(new MouseMotionAdapter() {
});
addKeyListener(new KeyAdapter() {
});
setFocusTraversalKeysEnabled(false);
addComponentListener(new ComponentAdapter() {
publicvoid componentShown(ComponentEvent evt) {
requestFocusInWindow();
}
});
然後是繪製:
@Override
protectedvoid paintComponent(Graphics g) {
先繪製整體背景:
// gets the current clipping area
Rectangle clip = g.getClipBounds();
// sets a 65% translucent composite
AlphaComposite alpha = AlphaComposite.SrcOver.derive(0.65f);
Composite composite = g2.getComposite();
g2.setComposite(alpha);
// fills the background
g2.setColor(getBackground());
g2.fillRect(clip.x, clip.y, clip.width, clip.height);
g2.setComposite(composite);
然後繪製一張提示圖片,本來想繪製一個滾動的等待圖標的,實在是沒心情寫了,隨便Google了張圖片放上去了.
if (image == null) {
try {
image = ImageIO.read(getClass().getResource("wait2.jpg"));
} catch (IOException ex) {
ex.printStackTrace();
}
}
g.drawImage(image, getWidth() / 2 - 40, getHeight() / 2
- 80, 120, 120, null);
通過設置畫面的GlassPane就可以了
Component glassPane = new GlassPane();
frame.getRootPane().setGlassPane(glassPane);
最終效果如下圖:
到此爲止,關於樹的操作基本就完成了,下面再開個專題講下有CheckBox的JTree,至於JTree的拖拽,因爲和其它Component基本是一致的,就留在以後拖拽專題一起寫了,總之,JTree還是不算複雜的一個組件.