Sunday, March 27, 2011

P/Invoke SHSetKnownFolderPath

EDIT: Scotty2012 and David Morton's answers don't work for me so I have put a bounty on this question. I think I need to change the type of the string to something else before passing it in.

I'm not much cop at P/Invoke and I'm struggling with declaring and calling SHSetKnownFolderPath. I'm using VB9 but if anyone puts answers in C# I should be able to translate.

I have got SHGetKnowFolderPath working. Here is my code.

In VB

Imports System.Runtime.InteropServices

Public Class Form1
    <DllImport("shell32.dll")> _
    Private Shared Function SHGetKnownFolderPath(<MarshalAs(UnmanagedType.LPStruct)> ByVal rfid As Guid, ByVal dwFlags As UInteger, ByVal hToken As IntPtr, ByRef pszPath As IntPtr) As Integer
    End Function

    <DllImport("shell32.dll")> _
    Private Shared Function SHSetKnownFolderPath(<MarshalAs(UnmanagedType.LPStruct)> ByVal rfid As Guid, ByVal dwFlags As UInteger, ByVal hToken As IntPtr, ByRef pszPath As IntPtr) As Integer
    End Function

    Public Shared ReadOnly Documents As New Guid("FDD39AD0-238F-46AF-ADB4-6C85480369C7")


    Private Sub ButtonSetDocumentsPath_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles ButtonSetDocumentsPath.Click
        Dim pPath As IntPtr = Marshal.StringToCoTaskMemUni(TextBoxPath.Text)
        If SHSetKnownFolderPath(Documents, 0, IntPtr.Zero, pPath) = 0 Then
            MsgBox("Set Sucessfully")
        End If

    End Sub

    Private Sub ButtonGetDocumentsPath_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles ButtonGetDocumentsPath.Click
        Dim pPath As IntPtr
        If SHGetKnownFolderPath(Documents, 0, IntPtr.Zero, pPath) = 0 Then
            Dim s As String = Marshal.PtrToStringUni(pPath)
            Marshal.FreeCoTaskMem(pPath)
            TextBoxPath.Text = s
        End If

    End Sub
End Class

Thanks!

From stackoverflow
  • I think this should work in C# (I'm not running vista here so I can't check):

    [DllImport("shell32.dll")]
    private static int SHSetKnownFolderPath(ref Guid guid, int flags, IntPtr hToken, string newPath);
    

    you can call it this way

    SHSetKnownFolderPath(ref Documents, 0, IntPtr.Zero, "c:\\my new path\\");
    
    Christopher Edwards : Thanks. However this doesn't work for me. I'm not sure why. I get a return value of -2147024735...
    Stu Mackellar : -2147024735 == 0x800700A1 == "The specified path is invalid". The prototype is correct. Try declaring the string outside of the function call and make sure you're setting a valid path.
  • This would be the declaration:

    [DllImport("shell32.dll")]
    static extern int SHSetFolderPath(int csidl, IntPtr hToken, uint dwFlags, StringBuilder path)
    

    You would need to create a StringBuilder, passing in the maximum path of 260 into the constructor (This would be true for Vista/XP.) This is the stringbuilder that would have the new directory for the folder you're trying to set, so append your text into the StringBuilder for your new location. The biggest problem with your implementation, though is that the csidl parameter isn't the same as the Guid specified in Windows. These values are actually the values that are declared in shlobj.h. Follow the link to see the values that are top be passed in. The hToken should always be IntPtr.Zero, unless you have a pointer to a specific user you're trying to change this for. IntPtr.Zero will use the current user. dwFlags should always be set to 0.

    Christopher Edwards : Sorry it was SHSetKnownFolderPath that I was interested in, not SHSetFolderPath which is deprecated. I linked to the old version by accident (I will now fix this). That explains the GUIDs. I still can't get it work!
    scottm : I don't think you need to use a StringBuilder because the path is not an [out] variable.
    JaredPar : Also the default marshaling for a string in PInvoke is ANSI. This API requires Unicode. That was my mistake (fixed)
  • Try this code out. Sorry for the length, but it's all needed to properly PInvoke this particular function. It's a simple console application that includes a definition for both functions and an example usage of SHGetKnownFolderPath.

    I went ahead and included the definitions for KNOWN_FOLDER_FLAG as well as a few definitions for the folder ID's. All of the folder Id's are actually just GUIDs. All of the possible ID's can be found at %ProgramFiles%\Windows SDK\v6.0A\Include\KnownFolders.h and added in the same manner that I added in the sample.

    I included several wrapper functions that hide all of the evil marashal'ing details for calling the particular functions.

    If there is any particular folder id you'd like or explanation please add a comment and I'll update the sample.

    EDIT Corrected a mistake in the Marshalling of SHSetKnownFolderPath. I did not add a MarshalAs tag to the String value and it was defaulting to an ANSI string. The API required unicode. The SHSetFolderFunction now works (confirmed with RecentFolder)

    Imports System.Runtime.InteropServices
    
    
    
    Module NativeMethods
    
        Public Enum KNOWN_FOLDER_FLAG
    
            '''KF_FLAG_CREATE -> 0x00008000
            KF_FLAG_CREATE = 32768
    
            '''KF_FLAG_DONT_VERIFY -> 0x00004000
            KF_FLAG_DONT_VERIFY = 16384
    
            '''KF_FLAG_DONT_UNEXPAND -> 0x00002000
            KF_FLAG_DONT_UNEXPAND = 8192
    
            '''KF_FLAG_NO_ALIAS -> 0x00001000
            KF_FLAG_NO_ALIAS = 4096
    
            '''KF_FLAG_INIT -> 0x00000800
            KF_FLAG_INIT = 2048
    
            '''KF_FLAG_DEFAULT_PATH -> 0x00000400
            KF_FLAG_DEFAULT_PATH = 1024
    
            '''KF_FLAG_NOT_PARENT_RELATIVE -> 0x00000200
            KF_FLAG_NOT_PARENT_RELATIVE = 512
    
            '''KF_FLAG_SIMPLE_IDLIST -> 0x00000100
            KF_FLAG_SIMPLE_IDLIST = 256
    
            '''KF_FLAG_ALIAS_ONLY -> 0x80000000
            KF_FLAG_ALIAS_ONLY = &H80000000
        End Enum
    
    
        Public ComputerFolder As Guid = New Guid("0AC0837C-BBF8-452A-850D-79D08E667CA7")
        Public DesktopFolder As Guid = New Guid("B4BFCC3A-DB2C-424C-B029-7FE99A87C641")
        Public DocumentsFolder As Guid = New Guid("FDD39AD0-238F-46AF-ADB4-6C85480369C7")
    
    
        <DllImport("shell32.dll")> _
        Public Function SHGetKnownFolderPath( _
            ByRef folderId As Guid, _
            ByVal flags As UInteger, _
            ByVal token As IntPtr, _
            <Out()> ByRef pathPtr As IntPtr) As Integer
    
        End Function
    
        <DllImport("shell32.dll")> _
        Public Function SHSetKnownFolderPath( _
            ByRef folderId As Guid, _
            ByVal flags As UInteger, _
            ByVal token As IntPtr, _
            <[In](), MarshalAs(UnmanagedType.LPWStr)> ByVal path As String) As Integer
    
        End Function
    
        Public Function SHGetKnownFolderPathWrapper(ByVal folderId As Guid) As String
            Return SHGetKnownFolderPathWrapper(folderId, 0)
        End Function
    
        Public Function SHGetKnownFolderPathWrapper( _
            ByVal folderId As Guid, _
            ByVal flags As KNOWN_FOLDER_FLAG) As String
    
            Dim ptr = IntPtr.Zero
            Dim path = String.Empty
            Try
                Dim ret = SHGetKnownFolderPath(folderId, CUInt(flags), IntPtr.Zero, ptr)
                If ret <> 0 Then
                    Throw Marshal.GetExceptionForHR(ret)
                End If
                path = Marshal.PtrToStringUni(ptr)
            Finally
                Marshal.FreeCoTaskMem(ptr)
            End Try
            Return path
        End Function
    
        Public Sub SHSetKnownFolderPathWrapper( _
            ByVal folderId As Guid, _
            ByVal path As String)
    
            SHSetKnownFolderPathWrapper(folderId, 0, path)
        End Sub
    
        Public Sub SHSetKnownFolderPathWrapper( _
            ByVal folderId As Guid, _
            ByVal flags As KNOWN_FOLDER_FLAG, _
            ByVal path As String)
    
            Dim ret = SHSetKnownFolderPath(folderId, CUInt(flags), IntPtr.Zero, path)
            If ret <> 0 Then
                Throw Marshal.GetExceptionForHR(ret)
            End If
        End Sub
    
    End Module
    
    Module Module1
    
        Sub Main()
            Dim path = SHGetKnownFolderPathWrapper(NativeMethods.DesktopFolder)
            Console.WriteLine(path)
        End Sub
    
    End Module
    
    Christopher Edwards : Thanks for that. Unfortunately I am getting "The specified path is invalid. (Exception from HRESULT: 0x800700A1)" thrown by the line Throw Marshal.GetExceptionForHR(ret) in SHSetKnownFolderPathWrapper. This happens even when I set the Documents folder path to that it's existing value. Any ideas?
    JaredPar : What OS are you using?
    JaredPar : Also, what GUID are you using for documents?
    Christopher Edwards : I'm on Vista, but I'll need to test it on XP too. The GUID I'm using is "FDD39AD0-238F-46AF-ADB4-6C85480369C7".
    JaredPar : Weird, I just verified that GUID works fine on my machine (updated sample to include the Documents folder definition). Do any of the other folders work on your machine? Are you on a 64bit or 32bit machine? Shouldn't matter but I'm struggling to figure out why this isn't working on your box.
    Christopher Edwards : Well thanks for all your help. If it's working on your box I'll try it on another box in the office. My machine is 32bit Vista Business, I have local admin rights.
    JaredPar : I poked around in the documentation a bit and it says that not all folder Id's are present in all systems. It's possible your configuration does not have the Documents folder set. Do any of the other folders work? I've unfortunately been unable to repro your issue on any of my machines
    Christopher Edwards : Hi, I tried this on another PC with the same issue. I also tried it with the desktop folder instead of MyDocs. I must be doing something differently from you. I am calling SHSetKnownFolderPathWrapper(DesktopFolder, "C:\Users\c.edwards\Desktop"), do I need any other arguments?
    JaredPar : Oh. I thought you were testing ShGet* version. I haven't played much with the SHSet. I'll check that out tonight and get back to you.
    JaredPar : I'm a moron, I completely read SHGet in your first comment
    JaredPar : Figured outh te issue. It was a Marshalling problem. I updated the solution nad confirmed it works with RecentFolder: AE50C081-EBD2-438A-8655-8A092E34987A
    Christopher Edwards : It works! Brilliant, that has really helped me. Many many thanks.
  • Hi guys, great stuff. Unfortunatelly this is not working with Visual Basic 2010, as declaration and variable types have changed (again)

    Doe anyone have an idea how this declaration and function calling of SHSetKnownFolderpath works in VB 2010???

0 comments:

Post a Comment