Writing a simple file browser in JavaFX


Note: This article was written for an early version of JavaFX and I have good reason to believe it does not work in later versions. I wrote this shortly after the first release of JavaFX. The most recent version of the code is here: https://github.com/huguesjohnson/debigulator/tree/master/fx/src/com/huguesjohnson/debigulatorfx. Several years passed and I became an Oracle employee via acquisition. So I need to say in no uncertain terms that this article represents my own opinion and does not in any way represent an official position from Oracle. 100% personal opinion, 0% Oracle opinion. I hope that's clear.

Why am I writing this?

I want to like JavaFX, really I do. The return of the applet reminds me of the 90s which is nice. I also like the idea of being able to drag an applet into Windows, Ubuntu, and Mac to run it as a desktop application. It's a whole new take on their "write once, run anywhere" promise and breathing some life into a platform that needs it.

Java used to be so trendy and cool, it was the "Ruby on Rails of the 90s" now it's seemingly destined to be the "COBOL of the 20s". If JavaFX lives up to its promise it could turn things around.

So far though I've been a little disappointed with JavaFX. It's like an El Camino, a strange combination of AWT and Swing that doesn't quite feel natural. I'm going to keep trying it anyway and hope that one day it catches up to C# 1.0.

Look, I know this sounds terribly cynical so far but you have to believe me when I say I'm trying to like it.

The reality is, even if JavaFX is a little clunky now it's still a considerable improvement over Swing. Over the next few months I'm going to upgrade all my ugly Swing applications to JavaFX. The first one is something called Debigulator. It's a batch archive program that I wrote for myself but has been downloaded more than I expected.

It's also one of the ugliest programs ever created. Just look at this monstrosity:

Debigulator

Besides being unattractive it also doesn't resize well. JavaFX addresses both of those so I'm porting it. The first thing to go is that awful file browser in the top left region, I'm embarrassed to look at it. I think I'll replace it with a simple TreeView.


The code

To create a TreeView we first have to create a TreeItem subclass to store in the tree. The constructor and class members look a little something like:


		public class FilePathTreeItem extends TreeItem<String>{
		  public static Image folderCollapseImage=new Image(ClassLoader.getSystemResourceAsStream("com/huguesjohnson/javafxfilebrowsedemo/folder.png"));
		  public static Image folderExpandImage=new Image(ClassLoader.getSystemResourceAsStream("com/huguesjohnson/javafxfilebrowsedemo/folder-open.png"));
		  public static Image fileImage=new Image(ClassLoader.getSystemResourceAsStream("com/huguesjohnson/javafxfilebrowsedemo/text-x-generic.png"));
		  
		  //this stores the full path to the file or directory
		  private String fullPath;
		  public String getFullPath(){return(this.fullPath);}
		  
		  private boolean isDirectory;
		  public boolean isDirectory(){return(this.isDirectory);}
		    
		  public FilePathTreeItem(Path file){
		    super(file.toString());
		    this.fullPath=file.toString();

Next we want to set the icon, full path, and isDirectory members. This would be a good time to mention that all the icons in this demo come from the Tango library.


		    //test if this is a directory and set the icon
		    if(Files.isDirectory(file)){
		      this.isDirectory=true;
		      this.setGraphic(new ImageView(folderCollapseImage));
		    }else{
		      this.isDirectory=false;
		      this.setGraphic(new ImageView(fileImage));
		      //if you want different icons for different file types this is where you'd do it
		    }
		    
		    //set the value
		    if(!fullPath.endsWith(File.separator)){
		      //set the value (which is what is displayed in the tree)
		      String value=file.toString();
		      int indexOf=value.lastIndexOf(File.separator);
		      if(indexOf>0){
		        this.setValue(value.substring(indexOf+1));
		      }else{
		        this.setValue(value);
		      }
		    }

Now let's add the event handler for the node expanded event. That check for source.isExpanded() sure seems unnecessary. Man that was a fun piece of unexpected behavior to track down.


		    this.addEventHandler(TreeItem.branchExpandedEvent(),new EventHandler(){
		      @Override
		      public void handle(Event e){
		        FilePathTreeItem source=(FilePathTreeItem)e.getSource();
		        if(source.isDirectory()&&source.isExpanded()){
		          ImageView iv=(ImageView)source.getGraphic();
		          iv.setImage(folderExpandImage);
		        }
		        try{
		          if(source.getChildren().isEmpty()){
		            Path path=Paths.get(source.getFullPath());
            BasicFileAttributes attribs=Files.readAttributes(path,BasicFileAttributes.class);
		            if(attribs.isDirectory()){
              DirectoryStream<Path> dir=Files.newDirectoryStream(path);
		              for(Path file:dir){
                FilePathTreeItem treeNode=new FilePathTreeItem(file);
                source.getChildren().add(treeNode);
		              }
		            }
		          }else{
		            //if you want to implement rescanning a directory for changes this would be the place to do it
		          }
		        }catch(IOException x){
		          x.printStackTrace();
		        }
		      }
		    });

We'll wrap up this TreeItem implementation with an handler for the node collapsed event. Again the source.isExpanded() check really shouldn't be needed but just go ahead and remove it to see the goofiness that follows.


		    this.addEventHandler(TreeItem.branchCollapsedEvent(),new EventHandler(){
		      @Override
		      public void handle(Event e){
		        FilePathTreeItem source=(FilePathTreeItem)e.getSource();
		        if(source.isDirectory()&&!source.isExpanded()){
		          ImageView iv=(ImageView)source.getGraphic();
		          iv.setImage(folderCollapseImage);
		        }
		      }
		    });

Now we can go to work on the main program. Here's all the basic stuff.


		public class JavaFXFileBrowseDemoApp extends Application{
		  private TreeView<String> treeView;
		   
		  public static void main(String[] args){
		    launch(args);
		  }
		  
		  @Override
		  public void start(Stage primaryStage){
		    //create tree pane
		    VBox treeBox=new VBox();
		    treeBox.setPadding(new Insets(10,10,10,10));
		    treeBox.setSpacing(10);

Now it's time to start populating the tree. We'll use the computer name as the root node. Although I might go back and hide the root node since it's kind of pointless for this application. It's really just showing off how to get the name from the InetAddress class which you either already knew or didn't care about.


		    //setup the file browser root
		    String hostName="computer";
		    try{hostName=InetAddress.getLocalHost().getHostName();}catch(UnknownHostException x){}
		    TreeItem<String> rootNode=new TreeItem<>(hostName,new ImageView(new Image(ClassLoader.getSystemResourceAsStream("com/huguesjohnson/javafxfilebrowsedemo/computer.png"))));

One nifty addition to JDK7 is the ability to list all the drives on the system. That comes in handy for the next step where we need to add all the drives under the root node.


		    Iterable<Path> rootDirectories=FileSystems.getDefault().getRootDirectories();
		    for(Path name:rootDirectories){
		      FilePathTreeItem treeNode=new FilePathTreeItem(name);
		      rootNode.getChildren().add(treeNode);
		    }
		    rootNode.setExpanded(true);

All that's left is to add the TreeView to the window and show it.


		    //create the tree view
		    treeView=new TreeView<>(rootNode);
		    //add everything to the tree pane
		    treeBox.getChildren().addAll(new Label("File browser"),treeView);
		    VBox.setVgrow(treeView,Priority.ALWAYS);
		     
		    //setup and show the window
		    primaryStage.setTitle("JavaFX File Browse Demo");
		    StackPane root=new StackPane();
		    root.getChildren().addAll(treeBox);
		    primaryStage.setScene(new Scene(root,400,300));
		    primaryStage.show();

Here's what the final product looks like, much cleaner than the awful Swing version and less than half the code:

JavaFX file browser


Download

Source code for this demo



Related