none
Hew to Declare/Wrap/Call SHGetNameFromIDList from PowerShell RRS feed

  • General discussion

  • Been learning PowerShell for the past few months, having as much fun as I can on a standalone computer. My exploration has led to want to use the SHGetNameFromIDList function. While I have done some programming and scripting, I haven't used C# before and the intricacies of the Win32 API are a bit overwhelming. The MSDN documentation is thus:

    HRESULT SHGetNameFromIDList( _In_  PCIDLIST_ABSOLUTE pidl, _In_  SIGDN             sigdnName, _Out_ PWSTR             *ppszName );

    I tried to use Example 5 from the Add-Type help as a template, and came up with this:

    $Signature = @"
    [DllImport("shell32.dll")]public static extern long SHGetNameFromIDList(Byte[] pidl, int nCmdShow, String outString);
    "@
    $SHGetNameFromIDList = Add-Type -MemberDefinition $Signature -Name "WIN32SHGetNameFromIDList" -Namespace Win32Functions -PassThru
    
    $FolderName = ''
    $ItemIDLIst = [Byte[]](20, 0, 31, 80, 224, 79, 208, 32, 234, 58, 105, 16, 162, 216, 8, 0, 43, 48, 48, 1, 57, 0, 0)
    
    $SHGetNameFromIDList::SHGetNameFromIDList($ItemIDLIst, 0, $FolderName)
    
    $FolderName #Should contain returned string, currently empty

    I freely admit my ignorance in this area but am looking to learn. I've gotten to the point where the above produces no PS errors, but the string is empty.

    Is my Add-type correct? I'm sure my parameters aren't. Looking for guidance on mapping the types and how pointers are handled.

    Keith


    Keith

    Tuesday, May 1, 2018 3:35 AM

All replies

  • You will have to define all of the structures that support the API.  You will also have to use memory functions to allocate and return the buffer.

    With  et access to Shell32 API it is necessary to P/Invoke all of the required items.

    See: https://www.pinvoke.net/default.aspx/shell32.SHGetNameFromIDList

    I would write this as a DLL class in Visual Studio and just use Add-Type to load the DLL.

    Without good experience with Windows programming and the Win32 API this will be a pretty big challenge for you.

    Here aer some methods and shortcuts to the API wrappers in PowerShell: http://www.leeholmes.com/blog/2009/01/19/powershell-pinvoke-walkthrough/


    \_(ツ)_/

    Tuesday, May 1, 2018 4:40 AM
  • Note that "PCIDLIST_ABSOLUTE " is a list of "PCIDLIST" which is a structure of lengths and USHORT values (unsigned byte).

    This can all be built is C++ or C# relatively easily but can be a challenge in PowerShell.

    The returned string can be declared as a StringBuilder object.  To use that you will have to declare Interop support and all "System" namespaces.

    I don't have a simple example that comes close to the required tech iques but this old code shows much of what you need:

    https://gallery.technet.microsoft.com/Demo-of-calling-C-and-6ef0cd2b?redir=0

    And this one which shows some ways to return strings.

    https://gallery.technet.microsoft.com/Edit-old-fashioned-INI-f8fbc067?redir=0

    I can't find my demo of using the string builder right now but will look.


    \_(ツ)_/


    • Edited by jrv Tuesday, May 1, 2018 4:52 AM
    Tuesday, May 1, 2018 4:46 AM
  • Here is one from Lee Holmes using the StringBuilder:

    http://www.leeholmes.com/blog/2009/01/19/powershell-pinvoke-walkthrough/


    \_(ツ)_/

    Tuesday, May 1, 2018 4:54 AM
  • What problem are you solving?

    -- Bill Stewart [Bill_Stewart]

    Tuesday, May 1, 2018 2:01 PM
    Moderator
  • Thanks so much for the info & pointers, jrv. 

    The one nice thing is that I'm 99% sure that Windows is supplying me the PCIDLIST_ABSOLUTE already in the form a byte array.

    Back in the XP days, I dove into how Windows saved views for folders and wrote a popular vbs script that allowed a greater deal of control over inheritance. In exploring the BagMRU key, the index of saved views, it became clear that the numerically-named values corresponded to folders in the Shell Namespace. I was always frustrated by not being able to translate them into human-readable values. So it's been a back-burner curiosity for a long time.

    So fast-forward to the present. In familiarizing myself with PowerShell, I stared looking at this key again. On a hunch, I found some undocumented functionality of the Namespace method of the Shell object -- it can take those byte arrays and return the name of a fair number (those that translate to a CLSID or a file system folder)!:

    $MRU = "HKCU:\Software\Classes\Local Settings\Software\Microsoft\Windows\Shell\BagMRU"
    $BagIndex = gci -Path $MRU -Recurse -Depth 3
    $Shell = New-Object -ComObject Shell.Application
    
     $i = 0
     $MyTable = foreach ($Node in $BagIndex) {
        $ShItemID = [byte[]](Get-Item ($Node.PSParentPath)).GetValue($Node.PSChildName)
        $oProperties = @{
            'Index'    = $i
            'Bag #'    = $Node.GetValue("NodeSlot")
            'MRUPath'  = $Node.PSPath.Remove(-0,119)
            'Name'     = $Shell.NameSpace($ShItemID).title
            'ShItemID' = $ShItemID
        }
        New-Object psobject -Property $oProperties
        $i++
      }
    $MyTable


    Kinda neat, huh? So this really made me want to dig deeper.

    So again, thanks for the info. I'll be posting back as I go deeper...

    Keith


    Keith



    Wednesday, May 2, 2018 3:29 AM
  • Been meaning to get back here & share waht I learned. Pinvokke.net did not have a working signature for SHGetNameFromIDList. But I was able to find other signatures that used the same types as the parameters of SHGetNameFromIDList. Piecing those together along with some trial & error led me to this working signature:

    [DllImport("shell32.dll", SetLastError=true, CharSet=CharSet.Unicode)]
    [return: MarshalAs(UnmanagedType.Error)]
    private static extern int SHGetNameFromIDList(IntPtr pidl, uint sigdnName, out StringBuilder ppszName) 
    

    PowerShell Add-Type with wrapper and demo:

    $Apisource = @"
    using System;
    using System.Runtime.InteropServices;
    using System.Text;
    
    public class API
    {
        [DllImport("shell32.dll", SetLastError=true, CharSet=CharSet.Unicode)]
        [return: MarshalAs(UnmanagedType.Error)]
        private static extern int SHGetNameFromIDList(IntPtr pidl, uint sigdnName, out StringBuilder ppszName);
        
        public static string GetFolderName(Byte[] IDL) {
            GCHandle pinnedArray = GCHandle.Alloc(IDL, GCHandleType.Pinned);
            IntPtr PIDL          = pinnedArray.AddrOfPinnedObject();
            StringBuilder name   = new StringBuilder(2048);
            int result           = SHGetNameFromIDList(PIDL, 0x0, out name);
            pinnedArray.Free();
            return name.ToString();
        }
    }
    "@ # End $Apisource
    
    Add-Type -TypeDefinition $APIsource
    
    #********************* Get-IDL( $PSPath ) ********************
    #               Recursively construct absolute IDL             *
    #***************************************************************
    Function Get-IDL {
       param(
         [Parameter(Mandatory)]
         [String]
         $PSPath
       )
       $ParentPath = Split-Path $PSPath
       $Child      = Split-Path $PSPath -leaf
       $IDL        = Get-ItemPropertyValue $ParentPath $Child  
    
       If ( $ParentPath -notMatch 'BagMRU$') { # Recurse
          $ParentIDL = Get-IDL( $ParentPath )
          $IDL       = $ParentIDL[0..($ParentIDL.length-3)] + $IDL
       }
       $IDL  # <----------------------------------------- Return ***
    } # End Function Get-IDL()
    #---------------------------------------------------------------
    
    #******************** Get-NSPath( $PSPath ) ********************
    #             Recursively construct Namespace path             *
    #            uinsg Get-IDL and [API]:GetFolderName()           *
    #***************************************************************
    Function Get-NSPath { 
      Param(
         [Parameter(Mandatory,
            ValueFromPipeline,
            ValueFromPipelineByPropertyName)]
         [String[]]
         $PSPath
      )
      Process{
        ForEach ( $Key in $PSPath ) {
            $ParentPath = Split-Path $Key
            $IDL        = [Byte[]]( Get-IDL $Key )
            Try {
               $NSPath = [API]::GetFolderName($IDL)
            } Catch {
               $NSPath = "{Disconnected}"
            }
    
            If ( $ParentPath -notMatch 'BagMRU$') { # Recurse
                $NSPath = ( Get-NSPath $ParentPath ), $NSPath -join '\'
            }
            $NSPath # <--------------------------------------- Return ***
        }
      }
    } # End Function Get-NSPath()
    #---------------------------------------------------------------
    
    
    #******************************DEMO*************************
    
    gci $rp.bagmru -Recurse | ForEach{
        $HasBag = $_.Property -contains 'NodeSlot'
        If ( $_.Property -contains 'NodeSlot' ) {
            $BagNum     = $_.GetValue('NodeSlot')
        } Else {
            $BagNum = 'n/a'
        }
        [PSCustomObject]@{
            'Bag #'      = $BagNum
            'MRUPath'    = ($_.Name -split 'Shell\\')[-1]
            'NSPath'     = Get-NSPath $_.PSPath
        }
    } | Out-GridView
    


    Keith

    Sunday, September 20, 2020 6:32 AM