首先我们先来构建UI代码。首先是文件条目的bean PathItem,这个树状图我打算显示文件条目的同时显示文件的大小:
package com.bluepoint.bean;
import javafx.beans.property.SimpleStringProperty;
import java.io.File;
public class PathItem {
private SimpleStringProperty mDirName;
private SimpleStringProperty mDirSize;
private File mFile;
public PathItem(String path) {
mFile = new File(path);
mDirName = new SimpleStringProperty(mFile.getName());
if (mFile.isDirectory()) {
mDirSize = new SimpleStringProperty(""); //todo 还没想到不用遍历的方式获取文件夹大小的方法,遍历太深的话时间要爆炸
} else {
mDirSize = new SimpleStringProperty("" + mFile.length());
}
}
public String getDirName() {
return mDirName.get();
}
public void setDirName(String dirName) {
this.mDirName.set(dirName);
}
public String getDirSize() {
return mDirSize.get();
}
public void setDirSize(String dirSize) {
this.mDirSize.set(dirSize);
}
public File getFile() {
return mFile;
}
}
然后是界面的逻辑:
树状图的呈现我使用了TreeTableView,具体的view和列和bean的关联代码如下:
其中TreeTableView<PathItem> dirsTree是我在xml中已经创建好的TreeTableView对象,因此不是用new的方式获取该对象。
TreeTableView<PathItem> dirsTree = (TreeTableView) localPageLeftMenu.lookup("#ttv_dirs");
TreeTableColumn<PathItem, String> dirsColumn = new TreeTableColumn<>(
"名称");
dirsColumn
.setCellValueFactory((
TreeTableColumn.CellDataFeatures<PathItem, String> param) -> new ReadOnlyStringWrapper(
param.getValue().getValue().getDirName()));
TreeTableColumn<PathItem, String> sizeColumn = new TreeTableColumn<>(
"大小");
sizeColumn
.setCellValueFactory((
TreeTableColumn.CellDataFeatures<PathItem, String> param) -> new ReadOnlyStringWrapper(
param.getValue().getValue().getDirSize()));
dirsTree.getColumns().clear();
dirsColumn.setPrefWidth(dirsTree.getPrefWidth() / 2);
sizeColumn.setPrefWidth(dirsTree.getPrefWidth() / 2);
dirsTree.getColumns().add(dirsColumn);
dirsTree.getColumns().add(sizeColumn);
最后就是重头戏了,如何高效地呈现界面树的逻辑:
1、首先设置根结点。
2、给TreeTableView设置条目点击事件,点击的时候返回其对应的bean对象的file对象,假如其是一个文件夹,则往当前点击回调的结点中遍历生成对应的叶子结点,为了避免每次点击都追加了一次一样的叶子,因此每次需要把上次可能存在的叶子结点都清理一次(也不能说生成过一次,点击就不再生成了,万一用户新建了文件和文件夹,就会导致无法通过点击刷新的问题)。同时,如果当前添加的叶子文件是飞空文件夹,则添加一个空数据作为叶子的叶子,使得用户看到这个文件夹时知道可以继续点击:
为什么不一口气遍历整个文件数,然后一边遍历一边生成所有结点呢?首先这样性能会非常差,二来是不必要,用户只是想点击哪个就展开哪个的下一层,因此我认为,我这样的做法是最合理的。
TreeItem<PathItem> root = new TreeItem<>(new PathItem("/")); //todo 暂时设为根目录
//应用数据集:
dirsTree.setRoot(root);
//为文件夹树设置点击事件:
dirsTree.addEventFilter(MouseEvent.MOUSE_CLICKED, new EventHandler<MouseEvent>() {
public void handle(MouseEvent event) {
TreeItem node = (TreeItem) dirsTree.getSelectionModel().getSelectedItem();
PathItem item = (PathItem) node.getValue();
System.out.println("Node click: " + item.getDirName());
if (item.getFile().isDirectory()) {
node.getChildren().clear();
File filesList[] = item.getFile().listFiles();
if (filesList == null || filesList.length == 0) {
System.out.println("this is an empty directory.");
} else {
for (File s : filesList) {
PathItem nextDirUnit = new PathItem(s.getAbsolutePath());
TreeItem nextDirItem = new TreeItem<>(nextDirUnit);
if (nextDirUnit.getFile().isDirectory() && nextDirUnit.getFile().list() != null && nextDirUnit.getFile().list().length > 0) { //如果当前添加的叶子文件是飞空文件夹,则添加一个空数据作为叶子的叶子,使得用户看到这个文件夹时知道可以继续点击
nextDirItem.getChildren().add(new TreeItem<>());
}
node.getChildren().add(nextDirItem);
}
}
} else {
System.out.println("this is not a directory.");
}
}
});
最终效果: