Click here to Skip to main content
15,896,278 members
Articles / Desktop Programming / Windows Forms

An All VB.NET Explorer Tree Control with ImageList Management

Rate me:
Please Sign up or sign in to vote.
4.87/5 (138 votes)
17 May 2012CPOL30 min read 2.2M   29.6K   269  
Explorer TreeView control with Shell Folder access class and Icon management.
Imports System.Runtime.InteropServices
Imports System.IO
Imports System.IO.FileSystemInfo
Imports System.Text
Imports ExpTreeLib.ShellDll

Imports System.Windows.Forms        'only needed for some testing


Public Class CShItem
    Implements IDisposable, IComparable

#Region "   Private Fields"
    'm_Folder and m_Pidl must be released/freed at Dispose time
    Private m_Folder As IShellFolder    'if item is a folder, contains the Folder interface for this instance
    Private m_Pidl As IntPtr            'The Absolute PIDL for this item (not retained for files)
    Private m_DisplayName As String = ""
    Private m_Path As String
    Private m_TypeName As String
    Private m_Parent As CShItem = Nothing
    Private m_IconIndexNormal As Integer   'index into the System Image list for Normal icon
    Private m_IconIndexOpen As Integer 'index into the SystemImage list for Open icon
    Private m_IsBrowsable As Boolean
    Private m_IsFileSystem As Boolean
    Private m_IsFolder As Boolean
    Private m_HasSubFolders As Boolean
    Private m_IsLink As Boolean
    Private m_IsDisk As Boolean
    Private m_IsShared As Boolean

    Private m_SortFlag As Integer = 0 'Used in comparisons

    Private m_Directories As ArrayList

    'The following elements are only filled in on demand
    Private m_XtrInfo As Boolean = False
    Private m_LastWriteTime As DateTime
    Private m_CreationTime As DateTime
    Private m_LastAccessTime As DateTime
    Private m_Length As Long

    Private Shared CreCount As Integer
    Private Shared DesCount As Integer

    'Flags for Dispose state
    Private m_IsDisposing As Boolean
    Private m_Disposed As Boolean

    ' The DesktopBase is set up via Sub New() (one time only) and
    '  disposed of only when DesktopBase is finally disposed of
    Private Shared DesktopBase As CShItem
    'On some systems,files with no extension are assigned the same
    ' icon as a Folder.  The following is used instead
    'Private Shared NoExtIconIndex As Integer
    'We can avoid an extra SHGetFileInfo call once this is set up
    Private Shared OpenFolderIconIndex As Integer = -1

    'Private Shared SW As New StreamWriter("TmpLog.log")
#End Region

#Region "   Destructor"
    Public Overloads Sub Dispose() Implements IDisposable.Dispose
        Dispose(True)
        ' Take yourself off of the finalization queue
        ' to prevent finalization code for this object
        ' from executing a second time.
        GC.SuppressFinalize(Me)
    End Sub
    Protected Overridable Overloads Sub Dispose(ByVal disposing As Boolean)
        ' Allow your Dispose method to be called multiple times,
        ' but throw an exception if the object has been disposed.
        ' Whenever you do something with this class, 
        ' check to see if it has been disposed.
        If Not (m_Disposed) Then
            ' If disposing equals true, dispose all managed 
            ' and unmanaged resources.
            m_Disposed = True
            If (disposing) Then
            End If
            ' Release unmanaged resources. If disposing is false,
            ' only the following code is executed. 
            If Not IsNothing(m_Folder) Then
                Marshal.ReleaseComObject(m_Folder)
            End If
            If Not m_Pidl.Equals(IntPtr.Zero) Then
                Marshal.FreeCoTaskMem(m_Pidl)
            End If
            DesCount += 1
            'Debug.WriteLine("Disposing of: " & m_DisplayName)
            'Debug.WriteLine("CreCount = " & CreCount)
            'Debug.WriteLine("DesCount = " & DesCount)
            'If m_Path.Equals("Desktop") Then
            '    SW.Close()
            'End If
        Else
            Throw New Exception("CShItem Disposed more than once")
        End If
    End Sub

    ' This Finalize method will run only if the 
    ' Dispose method does not get called.
    ' By default, methods are NotOverridable. 
    ' This prevents a derived class from overriding this method.
    Protected Overrides Sub Finalize()
        ' Do not re-create Dispose clean-up code here.
        ' Calling Dispose(false) is optimal in terms of
        ' readability and maintainability.
        Dispose(False)
    End Sub

#End Region

#Region "   Constructors"
    Private Shared Sub Logit(ByVal who As CShItem)
        CreCount += 1
        'who.DebugDump()
    End Sub
    '<Summary>New(ByVal folder As IShellFolder, ByVal pidl As IntPtr, ByVal parent As CShitem)
    '<param>folder -- the folder interface of the parent</param>
    '<param>pidl   -- the Relative PIDL of this item</param>
    '<param>parent -- the CShitem of the parent</param>
    '</Summary>
    Private Sub New(ByVal folder As IShellFolder, ByVal pidl As IntPtr, ByVal parent As CShItem)
        If IsNothing(DesktopBase) Then
            DesktopBase = New CShItem() 'This initializes the Desktop folder
        End If

        m_Parent = parent
        m_Pidl = concatPidls(parent.PIDL, pidl)

        'Get some attributes
        SetUpAttributes(folder, pidl)

        'Fix TypeName
        If m_IsFolder And m_TypeName.Equals("File") Then
            m_TypeName = "File Folder"
        End If
        'Set unfetched value for IconIndex....
        m_IconIndexNormal = -1
        m_IconIndexOpen = -1
        'finally, set up my Folder
        If m_IsFolder Then
            Dim HR As Integer
            HR = folder.BindToObject(pidl, IntPtr.Zero, IID_IShellFolder, m_Folder)
            If HR <> NOERROR Then
                Marshal.ThrowExceptionForHR(HR)
            End If
        End If
        m_SortFlag = ComputeSortFlag()
        Logit(Me)
    End Sub

    Private Sub New()           'only used when desktopfolder has not been intialized
        If Not IsNothing(DesktopBase) Then
            Throw New Exception("Attempt to initialize CShItem for second time")
        End If
        Dim HR As Integer
        m_DisplayName = "Desktop"
        m_Path = "Desktop"
        m_IsFolder = True
        m_HasSubFolders = True
        m_IsBrowsable = False
        HR = SHGetDesktopFolder(m_Folder)
        m_Pidl = GetSpecialFolderLocation(IntPtr.Zero, CSIDL.DESKTOP)
        'Get Displayname, TypeName, and primary IconIndex
        Dim shfi As New SHFILEINFO()
        Dim dwflag As Integer = SHGFI.DISPLAYNAME Or _
                                SHGFI.TYPENAME Or _
                                SHGFI.SYSICONINDEX Or _
                                SHGFI.PIDL
        Dim dwAttr As Integer = 0
        Dim H As IntPtr = SHGetFileInfo(m_Pidl, dwAttr, shfi, cbFileInfo, dwflag)
        m_DisplayName = shfi.szDisplayName
        m_TypeName = "System Folder"   'not returned correctly by SHGetFileInfo
        m_IconIndexNormal = shfi.iIcon
        m_IconIndexOpen = shfi.iIcon
        m_SortFlag = ComputeSortFlag()
        Logit(Me)
    End Sub

    '<Summary> Create instance based on a non-desktop CSIDL
    '</Summary>
    Sub New(ByVal ID As CSIDL)
        If IsNothing(DesktopBase) Then
            DesktopBase = New CShItem() 'This initializes the Desktop folder
        End If
        Dim HR As Integer
        HR = SHGetSpecialFolderLocation(0, ID, m_Pidl)
        If HR = NOERROR Then
            Dim pParent As IShellFolder
            Dim relPidl As IntPtr = IntPtr.Zero
            Dim itemCnt As Integer = PidlCount(m_Pidl)
            If itemCnt = 1 Then         'parent is desktop
                HR = SHGetDesktopFolder(pParent)
                relPidl = m_Pidl
            Else
                Dim tmpPidl As IntPtr
                tmpPidl = TrimPidl(m_Pidl, relPidl)
                HR = DesktopBase.m_Folder.BindToObject(tmpPidl, IntPtr.Zero, IID_IShellFolder, pParent)
                If Not HR = NOERROR Then Marshal.ThrowExceptionForHR(HR)
                Marshal.FreeCoTaskMem(tmpPidl)
            End If
            'Get the Attributes
            SetUpAttributes(pParent, relPidl)
            'Set unfetched value for IconIndex....
            m_IconIndexNormal = -1
            m_IconIndexOpen = -1
            'finally, set up my Folder
            If m_IsFolder Then
                HR = pParent.BindToObject(relPidl, IntPtr.Zero, IID_IShellFolder, m_Folder)
                If HR <> NOERROR Then
                    Marshal.ThrowExceptionForHR(HR)
                End If
            End If
            Marshal.ReleaseComObject(pParent)
            'if itemCnt=1 then relPidl is same as m_Pidl, don't release
            If itemCnt > 1 Then Marshal.FreeCoTaskMem(relPidl)
        Else
            Marshal.ThrowExceptionForHR(HR)
        End If
        m_SortFlag = ComputeSortFlag()
        Logit(Me)
    End Sub

    Private Sub SetUpAttributes(ByVal folder As IShellFolder, ByVal pidl As IntPtr)
        Dim attrFlag As SFGAO
        attrFlag = SFGAO.BROWSABLE
        attrFlag = attrFlag Or SFGAO.FILESYSTEM
        attrFlag = attrFlag Or SFGAO.HASSUBFOLDER
        attrFlag = attrFlag Or SFGAO.FOLDER
        attrFlag = attrFlag Or SFGAO.LINK
        attrFlag = attrFlag Or SFGAO.SHARE
        'Note: for GetAttributesOf, we must provide an array, in almost all cases with 1 element
        Dim aPidl(0) As IntPtr
        aPidl(0) = pidl
        folder.GetAttributesOf(1, aPidl, attrFlag)
        m_IsBrowsable = CBool(attrFlag And SFGAO.BROWSABLE)
        m_IsFileSystem = CBool(attrFlag And SFGAO.FILESYSTEM)
        m_HasSubFolders = CBool(attrFlag And SFGAO.HASSUBFOLDER)
        m_IsFolder = CBool(attrFlag And SFGAO.FOLDER)
        m_IsLink = CBool(attrFlag And SFGAO.LINK)
        m_IsShared = CBool(attrFlag And SFGAO.SHARE)
        'Get the Path
        Dim strr As New STRRET()
        Dim buf As New StringBuilder(260)
        Dim itemflags As SHGDN = SHGDN.FORPARSING
        folder.GetDisplayNameOf(pidl, itemflags, strr)
        Dim HR As Integer = StrRetToBuf(strr, pidl, buf, MAX_PATH)
        If HR = NOERROR Then
            m_Path = buf.ToString
            'Get Displayname, TypeName, and primary IconIndex
            Dim shfi As New SHFILEINFO()
            Dim dwflag As Integer = SHGFI.DISPLAYNAME Or _
                                    SHGFI.TYPENAME Or _
                                    SHGFI.PIDL 'Or _
            'SHGFI.SYSICONINDEX
            Dim dwAttr As Integer = 0
            If m_IsFileSystem And Not m_IsFolder Then
                dwflag = dwflag Or SHGFI.USEFILEATTRIBUTES
                dwAttr = FILE_ATTRIBUTE_NORMAL
            End If
            Dim H As IntPtr = SHGetFileInfo(m_Pidl, dwAttr, shfi, cbFileInfo, dwflag)
            m_DisplayName = shfi.szDisplayName
            m_TypeName = shfi.szTypeName
            If m_TypeName.IndexOf("Disk") > -1 OrElse _
               m_TypeName.IndexOf("Disc") > -1 Then
                m_IsDisk = True
                'Debug.WriteLine(m_Path & " is a Disk")
            End If
        Else
            Marshal.ThrowExceptionForHR(HR)
        End If
        'fix DisplayName
        If m_DisplayName.Equals("") Then
            m_DisplayName = m_Path
        End If
    End Sub
#End Region

#Region "   Properties"

#Region "       Normal Properties"
    Public ReadOnly Property PIDL() As IntPtr
        Get
            Return m_Pidl
        End Get
    End Property

    Public ReadOnly Property DisplayName() As String
        Get
            Return m_DisplayName
        End Get
    End Property
    Public ReadOnly Property Path() As String
        Get
            Return m_Path
        End Get
    End Property
    Public ReadOnly Property TypeName() As String
        Get
            Return m_TypeName
        End Get
    End Property
    Private ReadOnly Property Parent() As CShItem
        Get
            Return m_Parent
        End Get
    End Property
    ' IconIndexNormal is Filled on demand
    Public ReadOnly Property IconIndexNormal() As Integer
        Get
            If m_IconIndexNormal < 0 Then
                Dim shfi As New SHFILEINFO()
                Dim dwflag As Integer = SHGFI.PIDL Or _
                                        SHGFI.SYSICONINDEX
                    Dim dwAttr As Integer = 0
                    If m_IsFileSystem And Not m_IsFolder Then
                        dwflag = dwflag Or SHGFI.USEFILEATTRIBUTES
                        dwAttr = FILE_ATTRIBUTE_NORMAL
                    End If
                    Dim H As IntPtr = SHGetFileInfo(m_Pidl, dwAttr, shfi, cbFileInfo, dwflag)
                    m_IconIndexNormal = shfi.iIcon
                End If
            Return m_IconIndexNormal
        End Get
    End Property
    ' IconIndexOpen is Filled on demand
    Public ReadOnly Property IconIndexOpen() As Integer
        Get
            If m_IconIndexOpen < 0 Then
                If Not m_IsDisk And m_IsFileSystem And m_IsFolder Then
                    If OpenFolderIconIndex < 0 Then
                        Dim dwflag As Integer = SHGFI.SYSICONINDEX Or SHGFI.PIDL
                        Dim shfi As New SHFILEINFO()
                        Dim H As IntPtr = SHGetFileInfo(m_Pidl, 0, _
                                          shfi, cbFileInfo, _
                                          dwflag Or SHGFI.OPENICON)
                        m_IconIndexOpen = shfi.iIcon
                        If m_TypeName.Equals("File Folder") Then
                            OpenFolderIconIndex = shfi.iIcon
                        End If
                    Else
                        m_IconIndexOpen = OpenFolderIconIndex
                    End If
                Else
                    m_IconIndexOpen = m_IconIndexNormal
                End If
            End If
            Return m_IconIndexOpen
        End Get
        'Set(ByVal Value As Integer)
        '    m_IconIndexOpen = Value
        'End Set
    End Property

    Public ReadOnly Property IsBrowsable() As Boolean
        Get
            Return m_IsBrowsable
        End Get
    End Property
    Public ReadOnly Property IsFileSystem() As Boolean
        Get
            Return m_IsFileSystem
        End Get
    End Property
    Public ReadOnly Property IsFolder() As Boolean
        Get
            Return m_IsFolder
        End Get
    End Property
    Public ReadOnly Property HasSubFolders() As Boolean
        Get
            Return m_HasSubFolders
        End Get
    End Property
    Public ReadOnly Property IsDisk() As Boolean
        Get
            Return m_IsDisk
        End Get
    End Property
    Public ReadOnly Property IsLink() As Boolean
        Get
            Return m_IsLink
        End Get
    End Property
    Public ReadOnly Property IsShared() As Boolean
        Get
            Return m_IsShared
        End Get
    End Property

    Private ReadOnly Property SortFlag() As Integer
        Get
            Return m_SortFlag
        End Get
    End Property
#End Region

#Region "       Filled on Demand Properties"
    'Private m_XtrInfo As Boolean = False
    'Private m_LastWriteTime As DateTime
    'Private m_CreationTime As DateTime
    'Private m_LastAccessTime As DateTime
    'Private m_Length As Long

    Private Sub FillDemandInfo()
        If Not m_IsDisk And m_IsFileSystem And Not m_IsFolder Then
            'in this case, it's a file
            If File.Exists(m_Path) Then
                Dim fi As New FileInfo(m_Path)
                m_LastWriteTime = fi.LastWriteTime
                m_LastAccessTime = fi.LastAccessTime
                m_CreationTime = fi.CreationTime
                m_Length = fi.Length
                m_XtrInfo = True
            End If
        Else
            If m_IsFileSystem And m_IsFolder Then
                If Directory.Exists(m_Path) Then
                    Dim di As New DirectoryInfo(m_Path)
                    m_LastWriteTime = di.LastWriteTime
                    m_LastAccessTime = di.LastAccessTime
                    m_CreationTime = di.CreationTime
                    m_XtrInfo = True
                End If
            End If
        End If
    End Sub
    Public ReadOnly Property LastWriteTime() As DateTime
        Get
            If Not m_XtrInfo Then
                FillDemandInfo()
            End If
            Return m_LastWriteTime
        End Get
    End Property
    Public ReadOnly Property LastAccessTime() As DateTime
        Get
            If Not m_XtrInfo Then
                FillDemandInfo()
            End If
            Return m_LastAccessTime
        End Get
    End Property
    Public ReadOnly Property CreationTime() As DateTime
        Get
            If Not m_XtrInfo Then
                FillDemandInfo()
            End If
            Return m_CreationTime
        End Get
    End Property
    Public ReadOnly Property Length() As Long
        Get
            If Not m_XtrInfo Then
                FillDemandInfo()
            End If
            Return m_Length
        End Get
    End Property

#End Region
#End Region

#Region "   Public Methods"
#Region "       Shared Public Methods"
    '<Summary> Public Shared Function GetDeskTop() as CShItem
    '          If not initialized, then build DesktoBase
    '          once done, or if initialized already,
    '<Return>  the DesktopBase CShItem representing the desktop</Return>
    '</Summary>
    Public Shared Function GetDeskTop() As CShItem
        If IsNothing(DesktopBase) Then
            DesktopBase = New CShItem()
        End If
        Return DesktopBase
    End Function

#Region "      AllFolderWalk"
    '<Summary> 
    'The WalkAllCallBack delegate defines the signature of 
    'the routine to be passed to DirWalker
    ' Usage:  dim d1 as new CshItem.WalkAllCallBack(addressof yourroutine)
    '   Callback function receives a CShItem for each file & Directory in
    '   Starting Directory and each sub-directory of this directory and
    '   each sub-dir of each sub-dir ....
    '   
    '</Summary>
    Public Delegate Function WalkAllCallBack(ByVal info As CShItem, _
                                             ByVal UserLevel As Integer, _
                                             ByVal Tag As Integer) _
                                             As Boolean
    '<Summary> 
    ' AllFolderWalk recursively walks down directories from cStart, calling its
    '   callback routine, WalkAllCallBack, for each Directory and File encountered, including those in
    '   cStart.  UserLevel is incremented by 1 for each level of dirs that DirWalker
    '  recurses thru.  Tag in an Integer that is simply passed, unmodified to the 
    '  callback, with each CShItem encountered, both File & Directory CShItems.
    '</Summary>
    Public Shared Function AllFolderWalk(ByVal cStart As CShItem, _
                                          ByVal cback As WalkAllCallBack, _
                                          ByVal UserLevel As Integer, _
                                          ByVal Tag As Integer) _
                                          As Boolean
        If Not IsNothing(cStart) AndAlso cStart.IsFolder Then
            Dim cItem As CShItem
            'first processes all files in this directory
            For Each cItem In cStart.GetFiles
                If Not cback(cItem, UserLevel, Tag) Then
                    Return False        'user said stop
                End If
            Next
            'then process all dirs in this directory, recursively
            For Each cItem In cStart.GetDirectories
                If Not cback(cItem, UserLevel + 1, Tag) Then
                    Return False        'user said stop
                Else
                    If Not AllFolderWalk(cItem, cback, UserLevel + 1, Tag) Then
                        Return False
                    End If
                End If
            Next
            Return True
        Else        'Invalid call
            Throw New ApplicationException("AllFolderWalk -- Invalid Start Directory")
        End If
    End Function
#End Region

    ''<Summary>
    ''  Returns the Directories of the Desktop as an
    ''   ArrayList of CShitems
    ''</Summary>
    'Public Shared Function GetDeskTopDirectories() As ArrayList
    '    If IsNothing(DesktopBase) Then
    '        DesktopBase = New CShItem()
    '    End If
    '    Return DesktopBase.GetDirectories
    'End Function

    ''<Summary>
    ''GetDesktopFiles() As ArrayList
    ''  Returns the Files of Desktopfolder as an
    ''   ArrayList of CShitems
    '' Note: we do not keep the arraylist of files, Generate it each time
    ''</Summary>
    'Public Shared Function GetDesktopFiles() As ArrayList
    '    If IsNothing(DesktopBase) Then
    '        DesktopBase = New CShItem()
    '    End If
    '    Return DesktopBase.GetFiles
    'End Function

    ''<Summary>
    ''GetDesktopItems() As ArrayList
    ''  Returns the Files of Desktopfolder as an
    ''   ArrayList of CShitems
    '' Note: we do not keep the arraylist of files, Generate it each time
    ''</Summary>
    'Public Shared Function GetDesktopItems() As ArrayList
    '    If IsNothing(DesktopBase) Then
    '        DesktopBase = New CShItem()
    '    End If
    '    Return DesktopBase.GetItems
    'End Function

    'Public Shared Function GetDesktopIcon_Index(ByVal size As ShellIconStateConstants)
    '    If IsNothing(DesktopBase) Then
    '        DesktopBase = New CShItem()
    '    End If
    '    If size = ShellIconStateConstants.ShellIconStateNormal Then
    '        Return DesktopBase.IconIndexNormal
    '    Else
    '        Return DesktopBase.IconIndexOpen
    '    End If
    'End Function
#End Region

#Region "       Instance Methods"
    '<Summary>
    'GetDirectories() As ArrayList
    '  Returns the Directories of this sub-folder as an
    '   ArrayList of CShitems
    '</Summary>
    Public Function GetDirectories() As ArrayList
        If m_IsFolder Then
            If IsNothing(m_Directories) Then    'Build the list
                m_Directories = GetContents(SHCONTF.FOLDERS Or SHCONTF.INCLUDEHIDDEN)
                Return m_Directories
            Else    'Built this once, just returned saved list
                Return m_Directories
            End If
        Else    'if it is not a Folder, then return empty arraylist
            Return New ArrayList()
        End If
    End Function

    '<Summary>
    'GetFiles() As ArrayList
    '  Returns the Files of this sub-folder as an
    '   ArrayList of CShitems
    ' Note: we do not keep the arraylist of files, Generate it each time
    '</Summary>
    Public Function GetFiles() As ArrayList
        If m_IsFolder Then
            Return GetContents(SHCONTF.NONFOLDERS Or SHCONTF.INCLUDEHIDDEN)
        Else
            Return New ArrayList()
        End If
    End Function

    '<Summary>
    'GetItems() As ArrayList
    '  Returns the Directories and Files of this sub-folder as an
    '   ArrayList of CShitems
    ' Note: we do not keep the arraylist of files, Generate it each time
    '</Summary>
    Public Function GetItems() As ArrayList
        Dim rVal As New ArrayList()
        If m_IsFolder Then
            rVal.AddRange(Me.GetDirectories)
            rVal.AddRange(Me.GetFiles)
            Return rVal
        Else
            Return rVal
        End If
    End Function

    Public Overrides Function ToString() As String
        Return m_DisplayName
    End Function

    Public Sub DebugDump()
        Debug.WriteLine("DisplayName = " & m_DisplayName)
        Debug.WriteLine("PIDL        = " & m_Pidl.ToString)
        Debug.WriteLine(vbTab & "Path        = " & m_Path)
        Debug.WriteLine(vbTab & "TypeName    = " & m_TypeName)
        Debug.WriteLine(vbTab & "iIconNormal = " & m_IconIndexNormal)
        Debug.WriteLine(vbTab & "iIconSelect = " & m_IconIndexOpen)
        Debug.WriteLine(vbTab & "IsBrowsable = " & m_IsBrowsable)
        Debug.WriteLine(vbTab & "IsFileSystem= " & m_IsFileSystem)
        Debug.WriteLine(vbTab & "IsFolder    = " & m_IsFolder)
        Debug.WriteLine(vbTab & "IsLink    = " & m_IsLink)
        Debug.WriteLine(vbTab & "CreationCnt = " & CreCount)
        Debug.WriteLine(vbTab & "DestroyCnt  = " & DesCount)
        If m_IsFolder Then
            If Not IsNothing(m_Directories) Then
                Debug.WriteLine(vbTab & "Directory Count = " & m_Directories.Count)
            Else
                Debug.WriteLine(vbTab & "Directory Count Not yet set")
            End If
        End If
    End Sub
#End Region

#End Region

#Region "   Private Methods"
    '<Summary>
    'GetContents(ByVal flags As SHCONTF)
    '  Returns the requested Items of this sub-folder as an
    '   ArrayList of CShitems
    '<param>
    '   A set of one or more SHCONTF flags indicating which
    '   items to return
    '</param>
    '</Summary>
    Private Function GetContents(ByVal flags As SHCONTF) As ArrayList
        Dim rVal As New ArrayList()
        Dim HR As Integer
        Dim IEnum As IEnumIDList
        HR = m_Folder.EnumObjects(0, flags, IEnum)
        If HR = NOERROR Then
            Dim item As IntPtr = IntPtr.Zero
            Dim itemCnt As Integer
            HR = IEnum.GetNext(1, item, itemCnt)
            If HR = NOERROR Then
                Do While itemCnt > 0 AndAlso Not item.Equals(IntPtr.Zero)
                    'there is no setting to exclude folders from enumeration,
                    ' just one to include non-folders
                    ' so we have to screen the results to return only
                    '  non-folders if folders are not wanted
                    If Not CBool(flags And SHCONTF.FOLDERS) Then 'don't want folders. see if this is one
                        Dim attrFlag As SFGAO
                        attrFlag = attrFlag Or SFGAO.FOLDER
                        'Note: for GetAttributesOf, we must provide an array, in almost all cases with 1 element
                        Dim aPidl(0) As IntPtr
                        aPidl(0) = item
                        m_Folder.GetAttributesOf(1, aPidl, attrFlag)
                        If CBool(attrFlag And SFGAO.FOLDER) Then 'Don't need it
                            Marshal.FreeCoTaskMem(item)
                            GoTo SKIPONE
                        End If
                    End If
                    rVal.Add(New CShItem(m_Folder, item, Me))
                    Marshal.FreeCoTaskMem(item) 'if New kept it, it kept a copy
SKIPONE:            item = IntPtr.Zero
                    itemCnt = 0
                    HR = IEnum.GetNext(1, item, itemCnt)
                Loop
            Else
                If HR <> 1 Then GoTo HRError '1 means no more
            End If
        Else : GoTo HRError
        End If
        'Normal Exit
NORMAL: If Not IsNothing(IEnum) Then
            Marshal.ReleaseComObject(IEnum)
        End If
        rVal.TrimToSize()
        Return rVal

        ' Error Exit for all Com errors
HRError:  'not ready disks will return the following error
        If HR = &HFFFFFFFF800704C7 Then
            GoTo NORMAL
            'unavailable net resources will return these
        ElseIf HR = &HFFFFFFFF80040E96 Or HR = &HFFFFFFFF80040E19 Then
            GoTo NORMAL
        ElseIf HR = &HFFFFFFFF80004001 Then 'Certain "Not Implemented" features will return this
            GoTo NORMAL
        End If
        If Not IsNothing(IEnum) Then Marshal.ReleaseComObject(IEnum)
        Marshal.ThrowExceptionForHR(HR)
        Return New ArrayList()  'sometimes it is a non-fatal error,ignored
    End Function

#Region "   Really nasty Pidl manipulation"
    ' Get Size in bytes of the first (possibly only)
    '  SHItem in an ID list.  Note: the full size of
    '   the item is the sum of the sizes of all SHItems
    '   in the list!!
    Private Shared Function ItemIDSize(ByVal pidl As IntPtr) As Integer
        If Not pidl.Equals(IntPtr.Zero) Then
            Dim b(1) As Byte
            Marshal.Copy(pidl, b, 0, 2)
            Return b(1) * 256 + b(0)
        Else
            Return 0
        End If
    End Function

    '<Summary>Private Shared Function ItemIDListSize(ByVal pidl As IntPtr) As Integer
    ' computes the actual size of the ItemIDList pointed to by pidl
    '<param>pidl -- the pidl pointing to an ItemIDList</param>
    '<return> Returns actual size of the ItemIDList, less the terminating nulnul
    '</return>
    Private Shared Function ItemIDListSize(ByVal pidl As IntPtr) As Integer
        If Not pidl.Equals(IntPtr.Zero) Then
            Dim cnt As Integer = 1
            Dim i As Integer = ItemIDSize(pidl)
            Dim b As Byte
            b = Marshal.ReadByte(pidl, i)
            Do While b <> 0
                i += b + (Marshal.ReadByte(pidl, i + 1) * 256)
                b = Marshal.ReadByte(pidl, i)
                If b <> 0 Then cnt += 1
            Loop
            Return i
        Else : Return 0
        End If
    End Function
    '<Summary>Private Shared Function PidlCount(ByVal pidl As IntPtr) As Integer
    ' counts the total number of SHItems in input pidl
    '<param>pidl -- the pidl to obtain the count for</param>
    '<return> Returns the count of SHItems pointed to by pidl</return>
    Private Shared Function PidlCount(ByVal pidl As IntPtr) As Integer
        If Not pidl.Equals(IntPtr.Zero) Then
            Dim cnt As Integer = 1
            Dim i As Integer = ItemIDSize(pidl)
            Dim b As Byte
            b = Marshal.ReadByte(pidl, i)
            Do While b <> 0
                i += b + (Marshal.ReadByte(pidl, i + 1) * 256)
                b = Marshal.ReadByte(pidl, i)
                If b <> 0 Then cnt += 1
            Loop
            Return cnt
        Else : Return 0
        End If

    End Function
    '<Summary> concatPidls(pidl1 as IntPtr, pidl2 as IntPtr) as IntPtr
    ' Concatenates the contents of two pidls into a new Pidl (ended by 00)
    '      allocating CoTaskMem to hold the result,
    '      placing the concatenation (followed by 00) into the
    '      allocated Memory,
    '      and returning an IntPtr pointing to the allocated mem
    '<param> pidl1 -- ptr to a well formed SHItemIDList or IntPtr.Zero</param>
    '<param> pidl2 -- ptr to a well formed SHItemIDList or IntPtr.Zero</param>
    '<Return> Returns a ptr to an ItemIDList containing the 
    '         concatenation of the two (followed by the req 2 zeros
    '     Caller must Free this pidl when done with it
    '</Return>
    Private Shared Function concatPidls(ByVal pidl1 As IntPtr, ByVal pidl2 As IntPtr) As IntPtr
        Dim cb1 As Integer, cb2 As Integer
        cb1 = ItemIDListSize(pidl1)
        cb2 = ItemIDListSize(pidl2)
        Dim rawCnt As Integer = cb1 + cb2
        If (rawCnt) > 0 Then
            Dim b(rawCnt + 1) As Byte
            If cb1 > 0 Then
                Marshal.Copy(pidl1, b, 0, cb1)
            End If
            If cb2 > 0 Then
                Marshal.Copy(pidl2, b, cb1, cb2)
            End If
            Dim rVal As IntPtr = Marshal.AllocCoTaskMem(cb1 + cb2 + 2)
            b(rawCnt) = 0 : b(rawCnt + 1) = 0
            Marshal.Copy(b, 0, rVal, rawCnt + 2)
            Return rVal
        Else
            Return IntPtr.Zero
        End If
    End Function

    '<Summary>Private Shared Function TrimPidl(ByVal pidl As IntPtr) As IntPtr
    '  Returns an ItemIDList with the last ItemID trimed off
    '  This is necessary since I cannot get SHBindToParent to work 
    '  It's purpose is to generate an ItemIDList for the Parent of a
    '  Special Folder which can then be processed with DesktopBase.BindToObject,
    '  yeilding a Folder for the parent of the Special Folder
    '  It also creates and passes back a RELATIVE pidl for this item
    '<param>pidl -- A pointer to a well formed ItemIDList</param>
    '<param>relPidl -- BYREF IntPtr which will point to a new relative pidl
    '          containing the contents of the last ItemID in the ItemIDList
    '          terminated by the required 2 nulls.
    '<Returns> an ItemIDList with the last element removed.
    '  Caller must Free this item when through with it
    '  Also returns the new relative pidl in the 2nd parameter
    '   Caller must Free this pidl as well, when through with it
    '</Returns>
    '</Summary>
    Private Shared Function TrimPidl(ByVal pidl As IntPtr, ByRef relPidl As IntPtr) As IntPtr
        Dim cb As Integer = ItemIDListSize(pidl)
        Dim b(cb + 1) As Byte
        Marshal.Copy(pidl, b, 0, cb)
        Dim prev As Integer = 0
        Dim i As Integer = b(0) + (b(1) * 256)
        Do While i < cb AndAlso b(i) <> 0
            prev = i
            i += b(i) + (b(i + 1) * 256)
        Loop
        If (prev + 1) < cb Then
            'first set up the relative pidl
            b(cb) = 0
            b(cb + 1) = 0
            Dim cb1 As Integer = b(prev) + (b(prev + 1) * 256)
            relPidl = Marshal.AllocCoTaskMem(cb1 + 2)
            Marshal.Copy(b, prev, relPidl, cb1 + 2)
            b(prev) = 0 : b(prev + 1) = 0
            Dim rVal As IntPtr = Marshal.AllocCoTaskMem(prev + 2)
            Marshal.Copy(b, 0, rVal, prev + 2)
            Return rVal
        Else
            Return IntPtr.Zero
        End If
    End Function
#End Region

#Region "   Routines to test PIDL manipulation"
    'Public Shared Function doPIDLTest() As ArrayList
    '    Dim lvi As New ListViewItem("Not Set")
    '    Dim aRet As New ArrayList()

    '    Dim xx As IntPtr
    '    xx = ShellDll.GetSpecialFolderLocation(IntPtr.Zero, CSIDL.HISTORY)
    '    Dim bb(299) As Byte
    '    Marshal.Copy(xx, bb, 0, 300)
    '    Dim sz As Integer = ItemIDSize(xx)
    '    sz = PidlCount(xx)
    '    'Dim shfi1 As New SHFILEINFO()
    '    'Dim uFlags1 As Integer = SHGFI.PIDL Or SHGFI.DISPLAYNAME Or SHGFI.TYPENAME
    '    'uFlags1 = uFlags1 Or SHGFI.SYSICONINDEX
    '    'Dim dwAttr1 As Integer = 0
    '    'Dim resIP1 As IntPtr = SHGetFileInfo(xx, dwAttr1, shfi1, cbFileInfo, uFlags1)

    '    Dim ppshf As IShellFolder     'Desktop Folder
    '    Dim HR As Integer
    '    HR = ShellDll.SHGetDesktopFolder(ppshf)

    '    If HR = NOERROR Then
    '        Dim psf As IShellFolder     'History Folder
    '        HR = ppshf.BindToObject(xx, IntPtr.Zero, IID_IShellFolder, psf)
    '        Dim psfEnum As IEnumIDList
    '        HR = psf.EnumObjects(0, SHCONTF.INCLUDEHIDDEN Or SHCONTF.FOLDERS, psfEnum)
    '        Marshal.ReleaseComObject(ppshf)
    '        Dim item As IntPtr
    '        HR = psfEnum.GetNext(1, item, sz)
    '        'ok, we now have a pidl relative to xx, see if we can concatenate with xx
    '        Dim itemAbsID As IntPtr
    '        itemAbsID = concatPidls(xx, item)
    '        'with that, we should be able to  get a SHFILEINFO
    '        Dim shfi As New SHFILEINFO()
    '        Dim uFlags As Integer = SHGFI.PIDL Or SHGFI.DISPLAYNAME Or SHGFI.TYPENAME
    '        uFlags = uFlags Or SHGFI.SYSICONINDEX
    '        Dim dwAttr As Integer = 0
    '        Dim resIP As IntPtr = SHGetFileInfo(itemAbsID, dwAttr, shfi, cbFileInfo, uFlags)
    '        lvi.Text = shfi.szDisplayName
    '        lvi.ImageIndex = shfi.iIcon
    '        aRet.Add(lvi)
    '        Dim lv2 As New ListViewItem("Not Set")

    '        Dim psfLv2 As IShellFolder
    '        HR = psf.BindToObject(item, IntPtr.Zero, IID_IShellFolder, psfLv2)
    '        Dim iEnum2 As IEnumIDList
    '        HR = psfLv2.EnumObjects(0, SHCONTF.INCLUDEHIDDEN Or SHCONTF.NONFOLDERS Or SHCONTF.FOLDERS, iEnum2)
    '        Dim item2 As IntPtr
    '        HR = iEnum2.GetNext(1, item2, sz)
    '        Dim item2AbsID As IntPtr
    '        item2AbsID = concatPidls(itemAbsID, item2)
    '        Dim shfi2 As New SHFILEINFO()
    '        resIP = SHGetFileInfo(item2AbsID, dwAttr, shfi2, cbFileInfo, uFlags)
    '        lv2.Text = shfi2.szDisplayName
    '        lv2.ImageIndex = shfi2.iIcon
    '        aRet.Add(lv2)
    '        Marshal.FreeCoTaskMem(item2)
    '        Marshal.FreeCoTaskMem(item2AbsID)
    '        Marshal.ReleaseComObject(iEnum2)
    '        Marshal.ReleaseComObject(psfLv2)
    '        Marshal.FreeCoTaskMem(item)
    '        Marshal.FreeCoTaskMem(itemAbsID)
    '        Marshal.ReleaseComObject(psfEnum)
    '        Marshal.ReleaseComObject(psf)
    '    End If
    '    Marshal.FreeCoTaskMem(xx)
    '    Return aRet
    'End Function
#End Region
#End Region

#Region "   Icomparable -- for default Sorting"

    Private Function ComputeSortFlag() As Integer
        Dim rVal As Integer = 0
        If m_IsDisk Then rVal = &H100000
        If m_TypeName.Equals("System Folder") Then
            If Not m_IsBrowsable Then
                rVal = rVal Or &H10000
            Else
                rVal = rVal Or &H1000
            End If
        End If
        If m_IsFolder Then rVal = rVal Or &H100
        Return rVal
    End Function

    '<Summary> CompareTo(obj as object)
    '  Compares obj to this instance -- obj must be as CShItem
    '<SortOrder>  (low)Disks,non-browsable System Folders,
    '              browsable System Folders, 
    '               Directories, Files, Nothing (high)</SortOrder>
    '</Summary>
    Public Overridable Overloads Function CompareTo(ByVal obj As Object) As Integer _
            Implements IComparable.CompareTo
        If IsNothing(obj) Then Return 1 'non-existant is always low
        Dim Other As CShItem = obj
        'If m_IsDisk Then
        '    If Other.IsDisk Then
        '        Return String.Compare(m_Path, Other.Path)
        '    Else
        '        Return -1
        '    End If
        'Else
        '    If Other.IsDisk Then Return 1
        'End If
        ''Neither is a Disk
        'If m_TypeName.Equals("System Folder") Then
        '    If Other.TypeName.Equals("System Folder") Then
        '        Return String.Compare(m_DisplayName, Other.DisplayName)
        '    Else
        '        Return -1
        '    End If
        'Else
        '    If Other.TypeName.Equals("System Folder") Then Return 1
        'End If
        'If m_IsFolder Then
        '    If Other.IsFolder Then
        '        Return String.Compare(m_Path, Other.Path)
        '    Else
        '        Return -1
        '    End If
        'Else
        '    If Other.IsFolder Then Return 1
        'End If
        Dim cmp As Integer = Other.SortFlag - m_SortFlag 'Note the reversal
        If cmp <> 0 Then
            Return cmp
        Else
            If m_IsDisk Then 'implies that both are
                Return String.Compare(m_Path, Other.Path)
            Else
                Return String.Compare(m_DisplayName, Other.DisplayName)
            End If
        End If
    End Function
#End Region
End Class

By viewing downloads associated with this article you agree to the Terms of Service and the article's licence.

If a file you wish to view isn't highlighted, and is a text file (not binary), please let us know and we'll add colourisation support for it.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
United States United States
After 30+ years working in the IT field, mostly managing SysAdmins, I have retired. One of my hobbies returns me to programming, basically just to keep my hand in.

Comments and Discussions