How to Create a TreeView File Browser Component in VB.NET – Part 2

This tutorial is part of a series: Part 1 | Part 2TreeView Part 2

In Part 1 of this tutorial series, I presented a way to use the .NET TreeView control as a file browser similar to Windows Explorer in functionality. As we navigate through this TreeView, the BeforeExpand event is handled so that each node is dynamically populated as soon as it gets expanded.

The second part of this tutorial focuses on displaying the native system icons for each file and folder in the TreeView.

Getting Started

We will continue building upon the TreeView File Browser Component from Part 1. Open the project and let’s get started.

Implement SHGetFileInfo()

Add a new Module to your project. It should have the default name of Module1.vb.

Copy/paste the following code into Module1.vb:

Public Declare Auto Function SHGetFileInfo Lib "shell32.dll" (ByVal pszPath As String, ByVal dwFileAttributes As Integer, ByRef psfi As SHFILEINFO, ByVal cbFileInfo As Integer, ByVal uFlags As Integer) As IntPtr

Public Const SHGFI_ICON As Integer = &H100
Public Const SHGFI_SMALLICON As Integer = &H1
Public Const SHGFI_LARGEICON As Integer = &H0
Public Const SHGFI_OPENICON As Integer = &H2

Structure SHFILEINFO
    Public hIcon As IntPtr
    Public iIcon As Integer
    Public dwAttributes As Integer
    <Runtime.InteropServices.MarshalAs(Runtime.InteropServices.UnmanagedType.ByValTStr, SizeConst:=260)> _
    Public szDisplayName As String
    <Runtime.InteropServices.MarshalAs(Runtime.InteropServices.UnmanagedType.ByValTStr, SizeConst:=80)> _
    Public szTypeName As String
End Structure

This is a simple implementation of the Win32 API call SHGetFileInfo, which can do more than just help us retrieve icons from the Windows shell. You can read more about it on the web.

In the code above, we have declared SHGetFileInfo (line 1), a function native to Windows which is stored in it’s own shell32.dll file. By implementing this API call in our code, we can tap into the same function that Windows uses to retrieve the actual system icons for files and folders.

Windows Explorer Icons

A single folder icon consists of several images

SHGetFileInfo needs to know whether we are requesting an icon at all (because we can actually request things other than an icon). It also needs to know whether it is a small icon (16×16), a large icon (full size), as well as whether the icon is in the open state (such as when a folder appears open in Windows Explorer), so we have defined the constants for these (line 3-6).

Furthermore, the SHGetFileInfo needs something to store the result of our request (in this case, the icon), so we must supply it with an SHFILEINFO structure (line 8-16).

Create a wrapper function to make using SHGetFileInfo easier

The important thing to understand about Win32 API calls is that they sometimes do not return values in the exact format we need them in. For example, SHGetFileInfo does not return a simple System.Drawing.Image or System.Drawing.Icon that we can use immediately in .NET code. Therefore, we will have to extract the returned values from the SHFILEINFO structure and convert them into the desired .NET objects. Basically, we need a wrapper function.

The wrapper function should call SHGetFileInfo, extract the icon from the SHFILEINFO structure and convert it to a System.Drawing.Image, then return the System.Drawing.Image. GetShellIconAsImage will be our wrapper function that wraps up the logic which will make this all happen.

Copy/paste the following code into Module1.vb:

' GetShellIconAsImage
Function GetShellIconAsImage(ByVal argPath As String) As Image
    Dim mShellFileInfo As New SHFILEINFO
    mShellFileInfo.szDisplayName = New String(Chr(0), 260)
    mShellFileInfo.szTypeName = New String(Chr(0), 80)
    SHGetFileInfo(argPath, 0, mShellFileInfo, System.Runtime.InteropServices.Marshal.SizeOf(mShellFileInfo), SHGFI_ICON Or SHGFI_SMALLICON)
    ' attempt to create a System.Drawing.Icon from the icon handle that was returned in the SHFILEINFO structure
    Dim mIcon as System.Drawing.Icon
    Dim mImage as System.Drawing.Image
    Try
        mIcon = System.Drawing.Icon.FromHandle(mShellFileInfo.hIcon)
        mImage = mIcon.ToBitmap
    Catch ex As Exception
        ' for some reason the icon could not be converted so create a blank System.Drawing.Image to return instead
        mImage = New System.Drawing.Bitmap(16, 16)
    End Try
    ' return the final System.Drawing.Image
    Return mImage
End Function

Let us understand what is going on in this function.

First, we define the GetShellIconAsImage function (line 2), which accepts a single argument for the path of the file whose icon we wish to retrieve.

We then instantiate a new SHFILEINFO structure (line 3) and fill it with some required data (line 4-5). Basically, we must stuff the szDisplayName and szTypeName properties of the SHFILEINFO structure with blank strings before passing it to SHGetFileInfo, otherwise the structure will be of the wrong size and appear as garbage to SHGetFileInfo. A tad bit strange, but this is common with the Win32 API…

Next we call SHGetFileInfo with several parameters (line 6). You can see that we pass in our SHFILEINFO structure, which will get populated with file information after SHGetFileInfo executes.

Finally, we attempt to convert the icon handle in the SHFILEINFO structure to a System.Drawing.Icon, using the System.Drawing.Icon.FromHandle function (line 11). We then convert the System.Drawing.Icon to a System.Drawing.Image (line 12) because this is what .NET likes most. If this procedure fails, we just return a blank System.Drawing.Image (line 15).

We now have a method of retrieving an icon from the Windows shell as a System.Drawing.Image, but where do we store all of these icons, how do we know which icons to retrieve, and when?

Caching the icons for repeated use and quicker access

If you recall back to part one, nodes are populated on demand, meaning every time a user expands a node we request a new listing from the underlying filesystem. This is great, because as we are adding each child node, we can also retrieve the icon from the Windows shell using our wrapper function GetShellIconAsImage, then apply this icon to the node’s ImageKey property before adding it to the TreeView.

Since the TreeView control supports displaying icons from an ImageList, we are going to cache the icons from the Windows shell and store them in an ImageList that is bound to the TreeView control. This caching process should appear straightforward in just a moment.

Open UserControl1 in Design Mode and add an ImageList control from the ToolBox. It should have the default name of ImageList1.

We now want to bind this ImageList control to our TreeView control. Select the TreeView1 control and change it’s ImageList property to ImageList1. The TreeView is now able to display images from ImageList1.

Add the following code to UserControl1.vb:

Function CacheShellIcon(ByVal argPath As String) As String
    Dim mKey As String = Nothing
    ' determine the icon key for the file/folder specified in argPath
    If IO.Directory.Exists(argPath) = True Then
        mKey = "folder"
    ElseIf IO.File.Exists(argPath) = True Then
        mKey = IO.Path.GetExtension(argPath)
    End If
    ' check if an icon for this key has already been added to the collection
    If ImageList1.Images.ContainsKey(mKey) = False Then
        ImageList1.Images.Add(mKey, GetShellIconAsImage(argPath))
    End If
    Return mKey
End Function

When adding an Image to an ImageList, we can supply a unique key (line 4-8) that can be used to retrieve the Image later. Using this logic, we can store each icon that we cache from the Windows shell in the ImageList using a key equal to the file’s extension (line 11). For example, when the icon gets cached for a Text File, we add it to the ImageList using .txt as the key. The next time we encounter a Text File, we check if the ImageList already contains a .txt key (line 10), if so we skip the file since the icon has already been cached.

At this point, we haven’t actually associated any cached icons with a node in the TreeView. Let’s do that now.

Modify the UserControl1_Load event and include these lines of code:

mRootNode.ImageKey = CacheShellIcon(RootPath)
mRootNode.SelectedImageKey = mRootNode.ImageKey

Modify the TreeView1_BeforeExpand event and include these lines of code:

mDirectoryNode.ImageKey = CacheShellIcon(mDirectory.FullName)
mDirectoryNode.SelectedImageKey = mDirectoryNode.ImageKey

What about the “open folder” icon?

You may have noticed that when you select a folder in Windows Explorer and it becomes highlighted, the icon also changes to one where the folder appears to be “open” rather than closed. Remember earlier when we defined SHGFI_OPENICON? We simply supply this constant in the uFlags parameter of the SHGetFileInfo call in our GetShellIconAsImage method.

Take a look at the original call to SHGetFileInfo:

SHGetFileInfo(IO.Path.GetTempPath, 0, mShellFileInfo, System.Runtime.InteropServices.Marshal.SizeOf(mShellFileInfo), SHGFI_ICON Or SHGFI_SMALLICON)

If you change the uFlags parameter from this:

SHGFI_ICON Or SHGFI_SMALLICON

To this:

SHGFI_ICON Or SHGFI_SMALLICON Or SHGFI_OPENICON

You can retrieve the small open icon. Here’s a simple way to implement this in your current component.

Make a copy of your original GetShellIconAsImage method and name it GetShellOpenIconAsImage. The only thing different about this method is that you want to supply SHGFI_OPENICON in the uFlags parameter as I have described above.

The next thing to do is modify the CacheShellIcon method to include this line after the one that is similar to it:

ImageList1.Images.Add(mKey & "-open", GetShellOpenIconAsImage(argPath))

Displaying Files in the TreeView

Up to this point, we’ve only been displaying folders in the TreeView because this is how Windows Explorer does it. Windows Explorer has a separate panel for displaying the contents of the selected folder. However, let’s not reinvent the wheel. My goal in these tutorials is to show you the code and the logic that makes these ideas work, but it’s your job to apply them to your own projects in your own way.

What we are going to do instead is display the files directly in the TreeView. All we need to do is include a second loop in the TreeView1_BeforeExpand event to populate the files. Just copy/paste the loop block that we used for populating folders, and modify it for files like so:


' add each file from the file system that is a child of the argNode that was passed in
For Each mFile As IO.FileInfo In mNodeDirectory.GetFiles
' declare a TreeNode for this file
Dim mFileNode As New TreeNode
' store the full path to this file in the file TreeNode's Tag property
mFileNode.Tag = mFile.FullName
' set the file TreeNodes's display text
mFileNode.Text = mFile.Name
mFileNode.ImageKey = CacheShellIcon(mFile.FullName)
mFileNode.SelectedImageKey = mFileNode.ImageKey & "-open"
' add this file TreeNode to the TreeNode that is being populated
e.Node.Nodes.Add(mFileNode)
Next

What fun would this tutorial be without being able to double click the files to open them? Handle the TreeView1_NodeMouseDoubleClick event in UserControl1 like so:

Private Sub TreeView1_NodeMouseDoubleClick(ByVal sender As Object, ByVal e As System.Windows.Forms.TreeNodeMouseClickEventArgs) Handles TreeView1.NodeMouseDoubleClick
  ' only proceed if the node represents a file
  If e.Node.ImageKey = "folder" Then Exit Sub
  If e.Node.Tag = "" Then Exit Sub
  ' try to open the file
  Try
    Process.Start(e.Node.Tag)
  Catch ex As Exception
    MessageBox.Show("Error opening file: " & ex.Message)
  End Try
End Sub

Here it is in action:

TreeView with Icons

Resources

Related

Tags: , ,

72 Comments

  1. I added the following code to InitializeRoot() to add the MyDocuments folder to the root list:

    mRootNode = New TreeNode
    mRootNode.Tag = My.Computer.FileSystem.SpecialDirectories.MyDocuments
    mRootNode.Text = Path.GetFileName(mRootNode.Tag)
    mRootNode.ImageKey = CacheShellIcon(mRootNode.Tag)
    mRootNode.SelectedImageKey = mRootNode.ImageKey & “-open”
    mRootNode.Nodes.Add(“DUMMY”)
    TreeView1.Nodes.Add(mRootNode)

  2. Marcus Taylor says:

    There’s a error in your code. the code for putting in files to the treelist threw an error in my face:

    ‘mNodeDirectory.GetFiles’

    Visual basic says this doesn’t exist.
    other than that this is a wonderful example!

  3. Marcus Taylor says:

    Found out what was wrong.
    You forgot this line, I put it in and the error vanished:
    ‘Dim mNodeDirectory As IO.DirectoryInfo’

Leave a Reply

Your email address will not be published. Required fields are marked *