Dhan Dhan - 25 days ago 18
Vb.net Question

Show folder icon in a listview

I've managed to display icons for files in a listview using a shell32 extraction, but when do it with folders, the icon doesn't seem to show. how could It be?

This is my Shell Extraction code:



' declare the Win32 API function SHGetFileInfo'
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
' declare some constants that SHGetFileInfo requires'
Public Const SHGFI_ICON As Integer = &H100
Public Const SHGFI_SMALLICON As Integer = &H1
' define the SHFILEINFO structure'
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

Function RetrieveShellIcon(ByVal argPath As String) As Image
Dim mShellFileInfo As SHFILEINFO
Dim mSmallImage As IntPtr
Dim mIcon As System.Drawing.Icon
Dim mCompositeImage As Image
mShellFileInfo = New SHFILEINFO
mShellFileInfo.szDisplayName = New String(Chr(0), 260)
mShellFileInfo.szTypeName = New String(Chr(0), 80)
mSmallImage = SHGetFileInfo(argPath, 0, mShellFileInfo, System.Runtime.InteropServices.Marshal.SizeOf(mShellFileInfo), SHGFI_ICON Or SHGFI_SMALLICON)
' create the icon from the icon handle'
Try
mIcon = System.Drawing.Icon.FromHandle(mShellFileInfo.hIcon)
mCompositeImage = mIcon.ToBitmap
Catch ex As Exception
' create a blank black bitmap to return'
mCompositeImage = New Bitmap(16, 16)
End Try
' return the composited image'
Return mCompositeImage
End Function

Function GetIcon(ByVal argFilePath As String) As Image
Dim mFileExtension As String = System.IO.Path.GetExtension(argFilePath)
' add the image if it doesn't exist''
If cIcons.ContainsKey(mFileExtension) = False Then
cIcons.Add(mFileExtension, RetrieveShellIcon(argFilePath))
End If
' return the image'
Return cIcons(mFileExtension)
End Function


and this is how I show the icon for the files.

Dim lvi As ListViewItem
Dim di As New DirectoryInfo(Form2.TextBox1.Text)
Dim exts As New List(Of String)
ImageList1.Images.Clear()
If di.Exists = False Then
MessageBox.Show("Source path is not found", "Directory Not Found", MessageBoxButtons.OK, MessageBoxIcon.Error)
Else
For Each fi As FileInfo In di.EnumerateFiles("*.*")

lvi = New ListViewItem
lvi.Text = fi.Name

lvi.SubItems.Add(((fi.Length / 1024)).ToString("0.00"))
lvi.SubItems.Add(fi.CreationTime)

If exts.Contains(fi.Extension) = False Then
Dim mShellIconManager As New Form1
For Each mFilePath As String In My.Computer.FileSystem.GetFiles(Form2.TextBox1.Text)
ImageList1.Images.Add(fi.Extension, GetIcon(mFilePath))
exts.Add(fi.Extension)
Next

End If
lvi.ImageKey = fi.Extension
ListView1.Items.Add(lvi)
Next


this is how I show folder icons but doesn't seem to work

For Each fldr As String In Directory.GetDirectories(Form2.TextBox1.Text)
Dim mShellIconManager As New Form1

lvi = New ListViewItem
lvi.Text = Path.GetFileName(fldr)

lvi.SubItems.Add(((fldr.Length / 1024)).ToString("0.00"))
lvi.SubItems.Add(Directory.GetCreationTime(fldr))


ImageList1.Images.Add(GetIcon(fldr))
ListView1.Items.Add(lvi)
Next

Answer

There are a couple of things in your code. Some of it looks like remnants from previous attempts. At any rate, this works:

Public Partial Class NativeMethods
    Private Const MAX_PATH As Integer = 256
    Private Const NAMESIZE As Integer = 80
    Private Const SHGFI_ICON As Int32 = &H100

    <StructLayout(LayoutKind.Sequential)> 
    Private Structure SHFILEINFO
        Public hIcon As IntPtr
        Public iIcon As Integer
        Public dwAttributes As Integer
        <MarshalAs(UnmanagedType.ByValTStr, SizeConst:=MAX_PATH)>
        Public szDisplayName As String
        <MarshalAs(UnmanagedType.ByValTStr, SizeConst:=NAMESIZE)>
        Public szTypeName As String
    End Structure

    <DllImport("Shell32.dll")> 
    Private Shared Function SHGetFileInfo(pszPath As String,
                                          dwFileAttributes As Integer,
                                          ByRef psfi As SHFILEINFO,
                                          cbFileInfo As Integer,
                                          uFlags As Integer) As IntPtr
    End Function

    <DllImport("user32.dll", SetLastError:=True)>
    Private Shared Function DestroyIcon(hIcon As IntPtr) As Boolean
    End Function

    Public Shared Function GetShellIcon(path As String) As Bitmap
        Dim shfi As SHFILEINFO = New SHFILEINFO()

        Dim ret As IntPtr = SHGetFileInfo(path, 0, shfi, Marshal.SizeOf(shfi), SHGFI_ICON)
        If ret <> IntPtr.Zero Then
            Dim bmp As Bitmap = System.Drawing.Icon.FromHandle(shfi.hIcon).ToBitmap
            DestroyIcon(shfi.hIcon)
            Return bmp
        Else
            Return Nothing
        End If
    End Function
End Class

Putting the PInvoke code in its own class has several benefits. First, it helps isolate your code form all those magic numbers, structures and constants. The PInvoke(s) can be private and exposed thru a method (GetShellIcon) which does all the scut work and invokes the API method. Also, the VS CodeAnalysis tool wont complain about it when it is used from a NativeMethods class.

One of the things your code was not doing was destroying the icon retrieved and releasing that resource; also your SHGetFileInfo doesn't look right which can lead to bad things. When it can't get the icon, I would not create a blank/empty bitmap in the PInvoke code, instead this returns Nothing for the code to handle.

In the end, it is easier to use and shorter with the PInvoke code encapsulated:

Dim fPath As String = "C:\Temp"
Dim di = New DirectoryInfo(fPath)
' store imagelist index for known/found file types
Dim exts As New Dictionary(Of String, Int32)

Dim img As Image
Dim lvi As ListViewItem
For Each d In di.EnumerateDirectories("*.*", SearchOption.TopDirectoryOnly)
    lvi = New ListViewItem(d.Name)
    lvi.SubItems.Add("")        ' no file name
    lvi.SubItems.Add(Directory.GetFiles(d.FullName).Count().ToString)

    myLV.Items.Add(lvi)

    img = NativeMethods.GetShellIcon(d.FullName)
    imgLst.Images.Add(img)
    lvi.ImageIndex = imgLst.Images.Count - 1
Next

For Each f In di.EnumerateFiles("*.*")
    lvi = New ListViewItem(f.DirectoryName)
    lvi.SubItems.Add(f.Name)        ' no file name
    lvi.SubItems.Add("n/a")

    myLV.Items.Add(lvi)
    If exts.ContainsKey(f.Extension) = False Then
        ' try simplest method
        img = Drawing.Icon.ExtractAssociatedIcon(f.FullName).ToBitmap
        If img Is Nothing Then
            img = NativeMethods.GetShellIcon(f.FullName)
        End If
        If img IsNot Nothing Then
            imgLst.Images.Add(img)
            exts.Add(f.Extension, imgLst.Images.Count - 1)
        Else
            ' ?? use some default or custom '?' one?
        End If

    End If

    lvi.ImageIndex = exts(f.Extension)
Next

For the file, it first tries to get the icon using the NET Icon.ExtractAssociatedIcon method and resorts to the PInvoke of that failed for some reason.

I changed the exts List(Of String to a Dictionary(Of String, Int32). Once the code gets the icon for an extension, it saves the index of that image in the ImageList so that extension/icon doesn't need to be looked up again. This speeds it up quite a bit on large folders.

If you declare the Dictionary outside the method and then don't clear the ImageList each time, you could let them both accumulate images as it runs. The text file icon in folder Foo wont be different than the image for text files else where.

Result:
enter image description hereenter image description here

Comments