2013年8月22日 星期四

使用 SWT 開發視窗程式(一):實作檔案總管的樹狀目錄

SWT 本身有提供 Tree 元件,可以用來產生樹狀結構的容器
因此想實作簡單的檔案總管的樹狀目錄時,可以透過 Tree 元件來實作。

首先,第一步要先產生一個 Tree,並且設定 Tree 這個容器的尺寸
Tree dirTree = new Tree(parent, SWT.BORDER | SWT.MULTI | SWT.V_SCROLL);
dirTree.setSize(width, height);

接著,因為要做樹狀目錄,因此程式剛開啟時,要顯示出系統的根目錄們。可以利用 File 的相關方法來取得系統的根目錄。
File[] roots = File.listRoots();

想要在 Tree 上面加入子項目時,必須初始化一個 TreeItem,並且透過建構子 TreeItem(Tree parent, int style) 讓產生的 TreeItem 變成 Tree 的子項目。
TreeItem dirItem = dirItem = new TreeItem(dirTree, 0);
dirItem.setText(data.getAbsolutePath());

而因為預設 TreeItem 是沒有子項目的,因此不會有展開的選項可以讓使用者點選。
為了要有一個展開的選項(即使程式實際上還沒讀到那層子目錄),要預先在 TreeItem 裡放一個假的子項目
因此上述的初始化 TreeItem 的程式碼就會變成這樣:
TreeItem dirItem = dirItem = new TreeItem(dirTree, 0);
dirItem.setText(data.getAbsolutePath());
dirItem.setData(TREE_DATA_FILE, data);
TreeItem fakeChild = new TreeItem(dirItem, 0);

第三行是把 TreeItem 所代表的 Path 存在 TreeItem 的 Data 欄位上,方便日後有事件觸發時,可以隨時得知觸發的 TreeItem 表示的是哪層路徑。
第四行就是產生一個假的 TreeItem 作為子項目。

有了 TreeItem 之後,要設定當 Tree 被展開時,要動態地讀取 TreeItem 所屬的路徑當中的所有目錄,因此要對 Tree 加上 Listener,如下所示。
TreeItem item = (TreeItem) event.item;
File path = (File) item.getData(TREE_DATA_FILE);
// Remove the fake (or the existing) children.
item.removeAll();
        
// Traverse the path and get all of the sub directories.
File[] files = path.listFiles();
for(File file : files) {
  if(file.isDirectory()) {
    addChildToDirectoryTree(item, file);
  }
}

其中 addChildToDirectoryTree() 這個函式是把上一個步驟中,產生子項目以及設定初始狀態的程式碼集中起來寫成一個函式。

基本來說,目前為止就已經可以產生一個能夠展開的樹狀目錄了。
在額外加上一些簡單的 error handling 之後,組合起來就變成以下的程式碼了。

import java.io.File;
import java.nio.file.Files;

import org.apache.log4j.Logger;
import org.eclipse.swt.SWT;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Event;
import org.eclipse.swt.widgets.Listener;
import org.eclipse.swt.widgets.Tree;
import org.eclipse.swt.widgets.TreeItem;
import org.eclipse.swt.widgets.Widget;

public class DirectoryTree {
  private static Logger log = Logger.getLogger("Component.DTree");
  private static String TREE_DATA_FILE = "FILE";
  
  public static void createDirectoryTree (Composite parent, int width, int height) {
    Tree dirTree = new Tree(parent, SWT.BORDER | SWT.MULTI | SWT.V_SCROLL);
    dirTree.setSize(width, height);
    
    dirTree.addListener(SWT.Expand, new Listener() {
      @Override
      public void handleEvent(Event event) {
        log.debug("Expand event is triggered.");
        TreeItem item = (TreeItem) event.item;
        File path = (File) item.getData(TREE_DATA_FILE);
        log.debug("Get the path " + path.getAbsolutePath());
        // Remove the fake (or the existing) children.
        item.removeAll();
        
        // Traverse the path and get all of the sub directories.
        File[] files = path.listFiles();
        log.debug("List " + files.length);
        for(File file : files) {
          if(file.isDirectory()
              && Files.isReadable(file.toPath())        // From Java SE 7's API
              && !Files.isSymbolicLink(file.toPath())) {   // From Java SE 7's API
            addChildToDirectoryTree(item, file);
          }
        }
      }
    });
    
    // Get the root directories.
    File[] roots = File.listRoots();
    for(File root : roots) {
      addChildToDirectoryTree(dirTree, root);
    }
  }
  
  private static void addChildToDirectoryTree (Widget parent, File data) {
    TreeItem dirItem = null;
    
    if (parent instanceof Tree) {
      dirItem = new TreeItem((Tree) parent, 0);
      dirItem.setText(data.getAbsolutePath());
    }
    else {
      dirItem = new TreeItem((TreeItem) parent, 0);
      dirItem.setText(data.getName());
    }
    
    dirItem.setData(TREE_DATA_FILE, data);
    TreeItem fakeChild = new TreeItem(dirItem, 0);
    
    log.debug("Add directory " + ((File)dirItem.getData(TREE_DATA_FILE)).getAbsolutePath());
  }
}

比較特別的地方是 36、37 行,在增加子項目時,除了檢查 File 物件是不是表示一個資料夾之外,又額外檢查了兩個奇怪的條件
這個部分可以參考 [2] 的討論,實際上如果不加這個檢查的話,大部分的目錄操作都沒有問題
但遇到像是 C:\Document and Settings 時,程式就會意外地拋出 NullPointerException
認真去追蹤的話,結果會發現 Exception 的來源會是 32 行的 File[] files = path.listFiles();
原因在於 C:\Document and Settings 這個目錄,在 Vista 以後的 Windows 作業系統上,是一個實際上並不存在的資料夾
(也就是一個 Symbolic Link)
同時這個目錄並不具備足夠的讀取權限,因為 JVM 無法讀取內容,也代表著無從得知這個目錄是不是一個 Symbolic Link
(因為無權限讀取,所以系統也不會真的告訴你它是什麼東西)
所以必須把這些無法讀取內容的路徑去除掉,才能避免使用者點一點就把程式點壞了 XD

執行結果如下圖,當然在執行前要自己產生 main(),還有要呼叫 createDirectoryTree() 之前要先做出 Display 和 Shell
這些如果不會的話,都可以在網路上輕易找到其他的入門教學或範例程式。

參考資料:
1、17.60.5.Tree Exapand listener
2、Java finds a “Documents and Settings” folder on Win 7 HP. I can't see one

沒有留言: