Jerry Lum
Jerry Lum

Reputation: 33

SHOpenFolderAndSelectItems Win32 API doesn't work on Go, but works on C#

I am trying to open a Windows Explorer window with specified items in a particular folder selected.

I tried to implement the program using SHOpenFolderAndSelectItems Win32 API with C#. Following the answer here: https://stackoverflow.com/a/12262552

using System.Runtime.InteropServices;

SelectInFileExplorer("C:\\Users");

void SelectInFileExplorer(string fullPath)
{
    if (string.IsNullOrEmpty(fullPath))
        throw new ArgumentNullException("fullPath");

    fullPath = Path.GetFullPath(fullPath);

    IntPtr pidlList = NativeMethods.ILCreateFromPathW(fullPath);
    if (pidlList != IntPtr.Zero)
        try
        {
            // Open parent folder and select item
            Marshal.ThrowExceptionForHR(NativeMethods.SHOpenFolderAndSelectItems(pidlList, 0, IntPtr.Zero, 0));
        }
        finally
        {
            NativeMethods.ILFree(pidlList);
        }
}

static class NativeMethods
{

    [DllImport("shell32.dll", ExactSpelling = true)]
    public static extern void ILFree(IntPtr pidlList);

    [DllImport("shell32.dll", CharSet = CharSet.Unicode, ExactSpelling = true)]
    public static extern IntPtr ILCreateFromPathW(string pszPath);

    [DllImport("shell32.dll", ExactSpelling = true)]
    public static extern int SHOpenFolderAndSelectItems(IntPtr pidlList, uint cild, IntPtr children, uint dwFlags);
}

It worked, but now I would like to implement the same thing using Go.

Here is my code:

package main

import (
    "fmt"
    "path/filepath"
    "syscall"
    "unsafe"
)

func main() {
    SelectInFileExplorer("C:\\Users")
    fmt.Println("Hello, World!")
}

func SelectInFileExplorer(fullPath string) {
    if fullPath == "" {
        panic("fullPath cannot be empty")
    }

    fullPath, _ = filepath.Abs(fullPath)

    pidlList, err := ILCreateFromPathW(fullPath)
    if err != nil {
        panic(err)
    }
    defer ILFree(pidlList)

    // Open parent folder and select item
    err = SHOpenFolderAndSelectItems(pidlList, 0, 0)
    if err != nil {
        panic(err)
    }
}

func ILFree(pidlList uintptr) {
    shell32 := syscall.NewLazyDLL("shell32.dll")
    proc := shell32.NewProc("ILFree")
    proc.Call(pidlList)
}

func ILCreateFromPathW(pszPath string) (uintptr, error) {
    shell32 := syscall.NewLazyDLL("shell32.dll")
    proc := shell32.NewProc("ILCreateFromPathW")

    pszPathPtr, err := syscall.UTF16PtrFromString(pszPath)
    if err != nil {
        return 0, err
    }

    ret, _, err := proc.Call(uintptr(unsafe.Pointer(pszPathPtr)))
    if ret == 0 {
        return 0, err
    }
    return ret, nil
}

func SHOpenFolderAndSelectItems(pidlList uintptr, cild uint32, children uintptr) error {
    shell32 := syscall.NewLazyDLL("shell32.dll")
    proc := shell32.NewProc("SHOpenFolderAndSelectItems")

    ret, _, err := proc.Call(pidlList, uintptr(cild), children, 0)
    if ret != 0 && err.Error() != "The operation completed successfully." {
        return err
    }
    return nil
}

It doesn't work as nothing happened after I executed the code. The Explorer didn't appear.

I read some documents regarding the usage of the API: https://learn.microsoft.com/en-us/windows/win32/api/shlobj_core/nf-shlobj_core-shopenfolderandselectitems

Still not sure what's going on. Maybe it is not because of the programming language itself. I think calling DLL functions is a little bit complex in GO, and I probably did something wrong with the function calls.

Upvotes: 2

Views: 77

Answers (2)

Jerry Lum
Jerry Lum

Reputation: 33

Here is the complete solution:

package main

import (
    "fmt"
    "os"
    "path/filepath"
    "syscall"
    "unsafe"

    "golang.org/x/sys/windows/registry"
)

var (
    shell32 = syscall.NewLazyDLL("shell32.dll")
    user32  = syscall.NewLazyDLL("user32.dll")
    ole32   = syscall.NewLazyDLL("ole32.dll")

    procCoInitializeEx             = ole32.NewProc("CoInitializeEx")
    procCoUninitialize             = ole32.NewProc("CoUninitialize")
    procILCreateFromPathW          = shell32.NewProc("ILCreateFromPathW")
    procSHOpenFolderAndSelectItems = shell32.NewProc("SHOpenFolderAndSelectItems")
    procILFree                     = shell32.NewProc("ILFree")
)

func UTF16String(s string) uintptr {
    ptr, err := syscall.UTF16PtrFromString(s)
    if err != nil {
        return 0
    } else {
        return uintptr(unsafe.Pointer(ptr))
    }
}

func main() {
    procCoInitializeEx.Call(0, 0) // nullptr, COINIT_MULTITHREADED
    defer procCoUninitialize.Call()

    err := SelectInFileExplorer("C:\\Users")
    if err != nil {
        fmt.Println("Error:", err)
    }
}

func SelectInFileExplorer(fullPath string) error {
    // Alternative way to open explorer, but the explorer window will not be the front window
    // cmd := exec.Command("explorer", "/select,", path)
    // cmd.Run()

    if fullPath == "" {
        return fmt.Errorf("fullPath cannot be null or empty")
    }

    absPath, err := filepath.Abs(fullPath)
    if err != nil {
        return err
    }

    pidlList, err := ILCreateFromPathW(absPath)
    if err != nil {
        return err
    }
    defer ILFree(pidlList)

    if pidlList != 0 {
        err = SHOpenFolderAndSelectItems(pidlList, 0, 0, 0)
        if err != nil {
            return err
        }
    }

    return nil
}

func ILCreateFromPathW(pszPath string) (uintptr, error) {
    ret, _, err := procILCreateFromPathW.Call(UTF16String(pszPath))
    if ret == 0 {
        return 0, err
    }
    return ret, nil
}

func ILFree(pidlList uintptr) {
    procILFree.Call(pidlList)
}

func SHOpenFolderAndSelectItems(pidlList uintptr, cild uint, children uintptr, dwFlags uint) error {
    ret, _, err := procSHOpenFolderAndSelectItems.Call(pidlList, uintptr(cild), children, uintptr(dwFlags))
    if ret != 0 {
        return err
    }
    return nil
}

Upvotes: 0

Simon Mourier
Simon Mourier

Reputation: 139187

As explained in official SHOpenFolderAndSelectItems documentation remarks:

CoInitialize or CoInitializeEx must be called before using SHOpenFolderAndSelectItems. Not doing so causes SHOpenFolderAndSelectItems to fail.

So you must add something like this in your main for example:

func main() {
    ole32 := syscall.NewLazyDLL("ole32.dll")
    proc := ole32.NewProc("CoInitialize")
    proc.Call(0)

    SelectInFileExplorer("C:\\Users")
}

It works in .NET because .NET does this automatically

Upvotes: 1

Related Questions