JTree節點渲染和資源管理器加載

前面已經把樹的基本使用、樹的事件和樹的數據取得寫完了,並看了一個Sun提供的最簡單的樹的例子,這一專題我們講兩個方面,一個是使用樹的Renderer和Editor構造一棵我們自己的樹;一個樹使用SwingWorker完成一棵資源管理器樹的加載。(文/ zeyuphoenix

先看渲染的例子:

JTree-- 樹(節點渲染和資源管理器加載)(一)_25540

JTree-- 樹(節點渲染和資源管理器加載)(一)_25541

我們爲我們的樹增加了可用與否、可見與否和節點圖片的屬性,當然你也可以自己添加想要的屬性,比如順序等.

整個實現的過程很清晰,也不復雜,我們首先實現我們自己的TreeNode,它繼承於DefaultMutableTreeNode,我們在裏面添加自己的屬性;然後是實現樹的節點的Renderer和Editor,在Renderer裏我們設置可用也否,選擇狀態,節點圖片等;在Editor裏我們設置樹的容器佈局.最後構造樹時使用setRenderer和setEditor就可以了,需要注意的是如果僅僅是呈現,設置Renderer就可以了,如果除了呈現還有操作的話,必須都要設置.

先看我們自己的TreeNode類, 繼承於DefaultMutableTreeNode,
  1. /**

  2. * the tree node that I rewrite it.

  3. */

  4. publicclass MyTreeNode extends DefaultMutableTreeNode {
複製代碼
我們添加了屬性:
  1.     /** is select or not. */

  2.     privatebooleanisSelected = false;
複製代碼
節點的選擇狀態
  1.     /** is enable. */

  2.     privatebooleanenabled = false;
複製代碼
節點使用與否
  1.     /** is visible or not. */

  2.     privatebooleanisVisible = false;
複製代碼
節點可見與否
  1.     /** it's icon. */

  2.     private Icon icon = null;

  3.     /** icon name. */

  4.     private String iconName = null;
複製代碼
節點圖片和圖片名字

然後是構造函數,我們初始化屬性,或使用默認的:
  1. public MyTreeNode() {

  2.     this(null, true, false, true, true, null);

  3. }

  4. public MyTreeNode(Object userObject, boolean allowsChildren,

  5.     boolean isSelected, boolean enabled, boolean isVisible, Icon icon) {

  6.     super(userObject, allowsChildren);

  7.     this.isSelected = isSelected;

  8.     this.enabled = enabled;

  9.     this.isVisible = isVisible;

  10.     this.icon = icon;

  11.       setSelectionMode(TreeSelectionModel.DISCONTIGUOUS_TREE_SELECTI

  12. ON);

  13. }
複製代碼
然後就是一些設置和取得屬性的方法,這個類就相當於一個Bean.

然後就是Rnederer了,我們這裏繼承於DefaultTreeCellRenderer
  1. publicclass MyTreeRenderer extends DefaultTreeCellRenderer {
複製代碼
然後複寫它的getTreeCellRendererComponent方法:
  1. @Override

  2. public Component getTreeCellRendererComponent(JTree tree, Object value, boolean sel, boolean expanded, boolean leaf, int row,

  3.     boolean hasFocus) {
複製代碼
然後設置節點的屬性:

首先是顯示文本:
  1. String stringValue = tree.convertValueToText(value, sel, expanded, leaf, row, hasFocus);

  2. setText(stringValue);
複製代碼
然後是可用與否:
  1.       boolean nodeIsEnabled = ((MyTreeNode) value).isEnabled();

  2.       boolean isEnabled = (treeIsEnabled && nodeIsEnabled);

  3.       setEnabled(isEnabled);
複製代碼
然後是是圖片
  1. Icon icon = ((MyTreeNode) value).getIcon();

  2. setIcon(icon);
複製代碼
還有一些基本的屬性,比如背景色等:
  1. setForeground(getTextSelectionColor());
複製代碼
再來需要看的類就是Editor了,它繼承於DefaultTreeCellEditor:
  1. publicclass MyCellEditor extends DefaultTreeCellEditor {
複製代碼
因爲我們使用的是JLabel作爲顯示控件,所以我們的主要實現是佈局處理,對於事件可以不需要考慮.我們複習了DefaultTreeCellEditor的默認佈局:
  1.     /**

  2.     * Container responsible for placing the editingComponent.

  3.     */

  4.     privateclass MyEditorContainer extends

  5.           DefaultTreeCellEditor.EditorContainer {
複製代碼
複寫它的doLayout方法:
  1. @Override

  2. publicvoid doLayout() {
複製代碼
取得節點的邊緣和大小:
  1. r = tree.getBounds(r);

  2. eSize.width = r.width - (offset * n);

  3. editingComponent.setLocation(offset, 0);
複製代碼
設置節點的位置和大小:
  1. setSize(new Dimension(eSize.width + offset, cSize.height));
複製代碼
最後就是使用了,很簡單和以前一樣,直接使用JTree的設置方法就可以了:
  1.       MyTreeRenderer renderer = new MyTreeRenderer();

  2.       tree.setCellRenderer(renderer);

  3.       tree.setEditable(true);

  4.       tree.setCellEditor(new MyCellEditor(tree,

  5.           (DefaultTreeCellRenderer) tree.getCellRenderer()));
複製代碼


這次我們用樹實現一個比較綜合的例子,做一個類似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界面.

     只有我的電腦的基本文件,沒有網上鄰居之類的

這個問題很難解決,涉及到網上鄰居就存在網絡的問題了,還需要網絡連接和掃描,開始我的思路是使用Apachecommons-client,後來發現有人給出了更好的辦法,使用JavaJFileChooser,Java已經實現了很多我們需要實現的.

     取得的資源管理樹的子目錄是亂序的

這個很好解決,使我們的TreeNode實現Comparable接口就可以了.

爲了解決這五個問題我們做的改進版:

首先我們解決問題一,看看我們的代碼:

節點的圖片的樣式問題我們可以設置Renderer,又因爲這些圖片可以在JFileChooserUI中取得,我們先參照JFileChooserUI做一個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, treeglassPane).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);

最終效果如下圖:

到此爲止,關於樹的操作基本就完成了,下面再開個專題講下有CheckBoxJTree,至於JTree的拖拽,因爲和其它Component基本是一致的,就留在以後拖拽專題一起寫了,總之,JTree還是不算複雜的一個組件.










發佈了30 篇原創文章 · 獲贊 13 · 訪問量 15萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章