This tutorial is part of a series: Part 1 | 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.
We will continue building upon the TreeView File Browser Component from Part 1. Open the project and let’s get started.
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.
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
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: