C#中使用SHBrowseForFolder导出中文文件夹

Unity项目开发笔记(十七)

从业以来,数次踩中编码的坑, 这次又马失前蹄 , 真是事不过三此非彼白.

本来这个小问题不打算拿出来说 , 但是翻看谷歌发现若干年前也有寥寥数人遇到碰到这个问题 ,而且都并没有给出一个可行的解决方案 ,现在问题依然挂在CSDN等地方 , 似乎不会再有人去回答了, 或者其实题主们后面解决了但并没有回头来提供解决方案. 现在由我来”终结此贴”

0x00.使用SHBrowseForFolder选择文件夹

(大段代码来袭 , 不想看可直接拉到底看关键的几行)

底层接口 – 选择文件夹相关

//-------------------------------------------------------------------------
class Win32API
{
    // C# representation of the IMalloc interface.
    [InterfaceType(ComInterfaceType.InterfaceIsIUnknown),
       Guid("00000002-0000-0000-C000-000000000046")]
    public interface IMalloc
    {
        [PreserveSig]
        IntPtr Alloc([In] int cb);
        [PreserveSig]
        IntPtr Realloc([In] IntPtr pv, [In] int cb);
        [PreserveSig]
        void Free([In] IntPtr pv);
        [PreserveSig]
        int GetSize([In] IntPtr pv);
        [PreserveSig]
        int DidAlloc(IntPtr pv);
        [PreserveSig]
        void HeapMinimize();
    }

    [StructLayout(LayoutKind.Sequential, Pack = 8)]
    public struct BROWSEINFO
    {
        public IntPtr hwndOwner;
        public IntPtr pidlRoot;
        public IntPtr pszDisplayName;
        [MarshalAs(UnmanagedType.LPTStr)]
        public string lpszTitle;
        public int ulFlags;
        [MarshalAs(UnmanagedType.FunctionPtr)]
        public Shell32.BFFCALLBACK lpfn;
        public IntPtr lParam;
        public int iImage;
    }

    [Flags]
    public enum BffStyles
    {
        RestrictToFilesystem = 0x0001, // BIF_RETURNONLYFSDIRS
        RestrictToDomain = 0x0002, // BIF_DONTGOBELOWDOMAIN
        RestrictToSubfolders = 0x0008, // BIF_RETURNFSANCESTORS
        ShowTextBox = 0x0010, // BIF_EDITBOX
        ValidateSelection = 0x0020, // BIF_VALIDATE
        NewDialogStyle = 0x0040, // BIF_NEWDIALOGSTYLE
        BrowseForComputer = 0x1000, // BIF_BROWSEFORCOMPUTER
        BrowseForPrinter = 0x2000, // BIF_BROWSEFORPRINTER
        BrowseForEverything = 0x4000, // BIF_BROWSEINCLUDEFILES
    }

    [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)]
    public class OpenFileName
    {
        public int structSize = 0;
        public IntPtr dlgOwner = IntPtr.Zero;
        public IntPtr instance = IntPtr.Zero;
        public String filter = null;
        public String customFilter = null;
        public int maxCustFilter = 0;
        public int filterIndex = 0;
        public String file = null;
        public int maxFile = 0;
        public String fileTitle = null;
        public int maxFileTitle = 0;
        public String initialDir = null;
        public String title = null;
        public int flags = 0;
        public short fileOffset = 0;
        public short fileExtension = 0;
        public String defExt = null;
        public IntPtr custData = IntPtr.Zero;
        public IntPtr hook = IntPtr.Zero;
        public String templateName = null;
        public IntPtr reservedPtr = IntPtr.Zero;
        public int reservedInt = 0;
        public int flagsEx = 0;
    }

    public class Shell32
    {
        public delegate int BFFCALLBACK(IntPtr hwnd, uint uMsg, IntPtr lParam, IntPtr lpData);

        [DllImport("Shell32.DLL")]
        public static extern int SHGetMalloc(out IMalloc ppMalloc);

        [DllImport("Shell32.DLL")]
        public static extern int SHGetSpecialFolderLocation(
                    IntPtr hwndOwner, int nFolder, out IntPtr ppidl);

        [DllImport("Shell32.DLL")]
        public static extern int SHGetPathFromIDList(
                    IntPtr pidl, byte[] pszPath);

        [DllImport("Shell32.DLL", CharSet = CharSet.Auto)]
        public static extern IntPtr SHBrowseForFolder(ref BROWSEINFO bi);
    }

    public class User32
    {
        public delegate bool delNativeEnumWindowsProc(IntPtr hWnd, IntPtr lParam);

        [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
        public static extern bool EnumWindows(delNativeEnumWindowsProc callback, IntPtr extraData);

        [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
        public static extern int GetWindowThreadProcessId(HandleRef handle, out int processId);
    }
}
//-------------------------------------------------------------------------
class Win32Instance
{
    //-------------------------------------------------------------------------
    private HandleRef unityWindowHandle;
    private bool bUnityHandleSet;
    //-------------------------------------------------------------------------
    public IntPtr GetHandle(ref bool bSuccess)
    {
        bUnityHandleSet = false;
        Win32API.User32.EnumWindows(__EnumWindowsCallBack, IntPtr.Zero);
        bSuccess = bUnityHandleSet;
        return unityWindowHandle.Handle;
    }
    //-------------------------------------------------------------------------
    private bool __EnumWindowsCallBack(IntPtr hWnd, IntPtr lParam)
    {
        int procid;

        int returnVal =
            Win32API.User32.GetWindowThreadProcessId(new HandleRef(this, hWnd), out procid);

        int currentPID = System.Diagnostics.Process.GetCurrentProcess().Id;

        HandleRef handle =
            new HandleRef(this, 
            System.Diagnostics.Process.GetCurrentProcess().MainWindowHandle);

        if (procid == currentPID)
        {
            unityWindowHandle = new HandleRef(this, hWnd);
            bUnityHandleSet = true;
            return false;
        }

        return true;
    }
}
//-------------------------------------------------------------------------

简单介绍一下 Win32API 所有接口的结构体 都是参照SHBrowseForFolder函数而写 , Win32Instance 主要是精确的获取当前进程的ID

接下来是 获取文件夹路径的简单例子

//-------------------------------------------------------------------------
private  void __SelectFolder(out string directoryPath)
{
    directoryPath = "null";
    try
    {
        IntPtr pidlRet = IntPtr.Zero;
        int publicOptions = (int)Win32API.BffStyles.RestrictToFilesystem |
        (int)Win32API.BffStyles.RestrictToDomain;
        int privateOptions = (int)Win32API.BffStyles.NewDialogStyle;

        // Construct a BROWSEINFO.
        Win32API.BROWSEINFO bi = new Win32API.BROWSEINFO();
        IntPtr buffer = Marshal.AllocHGlobal(1024);
        int mergedOptions = (int)publicOptions | (int)privateOptions;
        bi.pidlRoot = IntPtr.Zero;
        bi.pszDisplayName = buffer;
        bi.lpszTitle = "文件夹";
        bi.ulFlags = mergedOptions;

        Win32Instance w = new Win32Instance();
        bool bSuccess = false;
        IntPtr P = w.GetHandle(ref bSuccess);
        if (true == bSuccess)
        {
            bi.hwndOwner = P;
        }

        pidlRet = Win32API.Shell32.SHBrowseForFolder(ref bi);
        Marshal.FreeHGlobal(buffer);

        if (pidlRet == IntPtr.Zero)
        {
            // User clicked Cancel.
            return;
        }
        
        byte[] pp = new byte[2048];
        if (0 == Win32API.Shell32.SHGetPathFromIDList(pidlRet, pp))
        {
            return;
        }

        int nSize = 0;
        for (int i = 0; i < 2048; i++)
        {
            if (0 != pp[i])
            {
                nSize++;
            }
            else
            {
                break;
            }

        }

        if (0 == nSize)
        {
            return;
        }

        byte[] pReal = new byte[nSize];
        Array.Copy(pp, pReal, nSize);
        // 关键转码部分
        Gb2312Encoding gbk = new Gb2312Encoding();
        Encoding utf8 = Encoding.UTF8;
        byte[] utf8Bytes = Encoding.Convert(gbk, utf8, pReal);
        string utf8String = utf8.GetString(utf8Bytes);
        utf8String = utf8String.Replace("\0", "");
        directoryPath = utf8String.Replace("\\", "/") + "/";

    }
    catch (Exception e)
    {
        Console.WriteLine("获取文件夹目录出错:" + e.Message);
    }
}

以上用到的一个GBK转码库 位置查看 - github传送门

0x01.GBK转码

以下是关键的一段代码:

Gb2312Encoding gbk = new Gb2312Encoding();
Encoding utf8 = Encoding.UTF8;
byte[] utf8Bytes = Encoding.Convert(gbk, utf8, pReal);
string utf8String = utf8.GetString(utf8Bytes);
utf8String = utf8String.Replace("\0", "");

谷歌上找到的一个方案是把项目编码全部改为unicode , 但是C#项目里貌似没这个设定 , 所以使用SHGetPathFromIDList拿出的数据直接转码即可支持中文.(全部为英文的路径也不会有影响)

-EOF-

发表评论

电子邮件地址不会被公开。 必填项已用*标注