65.9K
CodeProject 正在变化。 阅读更多。
Home

C# 实现 Shell 编程,第一部分

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.91/5 (91投票s)

2003年1月25日

Ms-PL

20分钟阅读

viewsIcon

542213

downloadIcon

12281

本文介绍了使用 C# 进行 Shell 编程。内容包括开发用于处理 Shell 的多个实用类,以及一个封装了可扩展的“浏览文件夹”对话框的类。

Sample Image

引言

本文介绍了使用 C# 进行 Shell 编程。说实话,第一部分没有什么趣味性,但为了在学习真正的 Shell 编程之前,先理清一些基本概念,这部分内容是必不可少的。

如果您想了解更多关于 Shell 编程的细节,我建议您在 msdn.microsoft.com 上搜索“Shell Programming”这个词。

在撰写本文时,我意识到先解释 Shell 的概念,然后再解释如何在 C# 中实现它会过于复杂。您必须阅读以下 MSDN 文章,它们解释了 Shell 的基本知识,我保证这会非常有价值:

本文假设您已经阅读了这些文章。

那么,什么是 Shell 编程?简单来说,Shell 编程就是使用 Windows 平台并扩展 Windows Shell。我将举例说明如何使用和扩展 Shell。您是否用过“打开文件”对话框?那么,为什么这个对话框在大多数应用程序中看起来都一样呢?这个对话框以及更多(打开、保存、字体、颜色、打印机)都是 Windows 自带的对话框,可以通过一组称为 Shell API 的 API 函数来使用。我将再举一个使用 Shell API 的例子。假设您想找到 Windows 目录、我的文档目录,甚至是存放待刻录到 CD 的文件的文件夹(仅存在于 XP 上)。过去,您可以使用环境变量,但并非所有信息都在那里,而且这也不是 Microsoft 鼓励的方式。正确的方法是使用 Shell API 获取这些信息。

到目前为止,使用 Shell API 很好,但扩展 Shell 是完全不同的故事。扩展 Shell 意味着您可以在资源管理器中右键单击文件时,将自己的菜单命令添加到上下文菜单中(WinZip 就是这样做的,它允许您从资源管理器中选择文件并进行压缩)。开发 Shell 扩展是将您的应用程序集成到 Windows 平台中的方法。Shell 扩展的其他示例包括:自定义文件类的图标和快捷菜单;自定义文件夹的外观;将应用程序的清理过程与 Shell 的磁盘清理管理器集成;自定义 Webview 显示文件夹内容的方式,创建自定义 Explorer 工具栏和 DeskBand;使用 Active Desktop 对象;创建控制面板应用程序;等等……

好了,让我们开始工作吧。

主要目标

在本文中,我们将介绍一些涉及 Shell 的基本函数和接口,并展示如何将它们与 C# 一起使用。然后,我将介绍我编写的一个名为 ShellLib 的库,该库以一种优雅的方式封装了几乎所有的内容,可以直接使用。本文不打算解释所有的 Shell 规范和功能。我不会重写 MSDN。

第一部分将回顾的接口:IMallocIShellFolder

第一部分将回顾的函数:SHGetMallocSHGetFolderLocationSHGetPathFromIDListSHGetFolderPathSHParseDisplayNameSHGetDesktopFolderSHBindToParentStrRetToBSTRStrRetToBuf

第二部分将回顾的接口:IFolderFilterSiteIFolderFilter

第二部分将回顾的函数:SHBrowseForFolder

本文的第二部分将介绍我编写的封装“浏览文件夹”对话框的类,它支持:

  1. 浏览文件夹、文件、计算机或打印机。
  2. 设置对话框中的根文件夹。
  3. 使用新的对话框样式,允许在对话框内进行拖放、重新排序、快捷菜单、新建文件夹、删除以及其他快捷菜单命令。
  4. 设置您自己的说明文本。
  5. 更改“确定”按钮的文本。
  6. 通知选定更改和验证的事件。
  7. 自定义筛选,包括用于特定文件类型的自定义筛选示例。

注意:上图显示了该窗口的一个示例,其中自定义筛选仅显示 txt 和 bmp 文件。

注意 2:本文的主要部分使用的是仅在 win2k 和 XP 上存在的函数,自定义筛选仅在 XP 上存在。

第一部分:了解 Shell 基础知识

接口:IMalloc

MSDN 描述:IMalloc 接口负责内存的分配、释放和管理。
相关 API:SHGetMalloc - 获取指向 Shell IMalloc 接口的指针。

C# 定义

[ComImport]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
[Guid("00000002-0000-0000-C000-000000000046")]
public interface IMalloc 
{
    // Allocates a block of memory.
    // Return value: a pointer to the allocated memory block.
    [PreserveSig]
    IntPtr Alloc(
        UInt32 cb);        // Size, in bytes, of the memory block to be allocated. 
        
    // Changes the size of a previously allocated memory block.
    // Return value:  Reallocated memory block 
    [PreserveSig]
    IntPtr Realloc(
        IntPtr pv,         // Pointer to the memory block to be reallocated.
        UInt32 cb);        // Size of the memory block (in bytes) to be reallocated.

    // Frees a previously allocated block of memory.
    [PreserveSig]
    void Free(
        IntPtr pv);        // Pointer to the memory block to be freed.

    // This method returns the size (in bytes) of a memory block previously
    // allocated with IMalloc::Alloc or IMalloc::Realloc.
    // Return value: The size of the allocated memory block in bytes 
    [PreserveSig]
    UInt32 GetSize(
        IntPtr pv);        // Pointer to the memory block for which the size
                        // is requested.

    // This method determines whether this allocator was used to allocate
    // the specified block of memory.
    // Return value: 1 - allocated 0 - not allocated by this IMalloc instance. 
    [PreserveSig]
    Int16 DidAlloc(
        IntPtr pv);        // Pointer to the memory block

    // This method minimizes the heap as much as possible by releasing unused
    // memory to the operating system, 
    // coalescing adjacent free blocks and committing free pages.
    [PreserveSig]
        void HeapMinimize();
}

代码解释:ComImport 属性表示该接口已在 COM 中预先定义。InterfaceType 属性表示该接口继承了众所周知的 IUnknown 接口。然后是 Guid 属性,当已知 GUID 时,此属性提供显式 GUID。在这种情况下,我从头文件 objidl.h 中获取了 GUID。

简而言之:此接口的常见用法是在您从函数获取 PIDL 时,需要释放其内存。在这种情况下,您需要使用 SHGetMalloc 获取接口,并使用 free 函数释放 PIDL 的内存。下一节以及库 ShellLib 中的 ShellFunctions 类中的 GetMalloc 方法中都有使用此接口的示例。

函数:SHGetMalloc

MSDN 描述:获取指向 Shell IMalloc 接口的指针。

C# 定义

// Retrieves a pointer to the Shell's IMalloc interface.
[DllImport("shell32.dll")]
public static extern Int32 SHGetMalloc(
    out IntPtr hObject);    // Address of a pointer that receives the Shell's
                            // IMalloc interface pointer. 

用法示例

// Get IMalloc interface
IntPtr ptrRet;
SHGetMalloc(out ptrRet);

System.Type mallocType = System.Type.GetType("IMalloc");
Object obj = Marshal.GetTypedObjectForIUnknown(ptrRet,mallocType);
IMalloc pMalloc = (IMalloc)obj;
 
// Get a PIDL
IntPtr pidlRoot;
SHGetFolderLocation(IntPtr.zero,CSIDL_WINDOWS,IntPtr.Zero,0,out pidlRoot);
 
// Use the IMalloc object to free PIDL
if (pidlRoot != IntPtr.Zero)
pMalloc.Free(pidlRoot);

// Free the IMalloc object
System.Runtime.InteropServices.Marshal.ReleaseComObject(pMalloc);

代码解释:代码首先使用 SHGetMalloc 获取接口指针,然后获取一个普通的 .NET 对象来使用该接口。之后,它使用 SHGetFolderLocation(稍后讨论)接收一个 PIDL,最后使用接口的 free 方法释放 PIDL 内存。不要忘记在使用完接口后释放它。

函数:SHGetFolderLocation

MSDN 描述:以 ITEMIDLIST 结构(PIDL)的形式获取文件夹的路径。

C# 定义

// Retrieves the path of a folder as an PIDL.
[DllImport("shell32.dll")]
public static extern Int32 SHGetFolderLocation(
    IntPtr hwndOwner,         // Handle to the owner window.
    Int32 nFolder,            // A CSIDL value that identifies the folder to be
                            // located.
    IntPtr hToken,            // Token that can be used to represent a particular
                            // user.
    UInt32 dwReserved,        // Reserved.
    out IntPtr ppidl);        // Address of a pointer to an item identifier list
                            // structure 
                              // specifying the folder's location relative to the
                            // root of the namespace 
                              // (the desktop). 

用法示例

// Get a PIDL
IntPtr pidlRoot;
SHGetFolderLocation(IntPtr.zero,CSIDL_WINDOWS,IntPtr.Zero,0,out pidlRoot);

简而言之:此函数返回请求的特殊文件夹的 PIDL。请求的文件夹由 nFolder 参数指定。在库 ShellLib 中,有一个名为 CSIDL 的枚举,其中包含此参数的所有可能值。在介绍库时,我们将回顾该枚举。

函数:SHGetPathFromIDList

MSDN 描述:将项标识符列表转换为文件系统路径。

C# 定义

// Converts an item identifier list to a file system path. 
[DllImport("shell32.dll")]
public static extern Int32 SHGetPathFromIDList(
    IntPtr pidl,                // Address of an item identifier list that
                                // specifies a file or directory location
                                // relative to the root of the namespace (the
                                // desktop). 
    StringBuilder pszPath);        // Address of a buffer to receive the file system
                                // path.

用法示例

IntPtr pidlRoot;
SHGetFolderLocation(IntPtr.zero,CSIDL_WINDOWS,IntPtr.Zero,0,out pidlRoot);

System.Text.StringBuilder path = new System.Text.StringBuilder(256);
SHGetPathFromIDList(pidlRoot,path); 

代码解释:首先,我们获取 Windows 文件夹的 PIDL,然后创建一个结果缓冲区,并获取 PIDL 的路径。注意,此函数仅适用于代表文件系统中的文件或文件夹的 PIDL。

函数:SHGetFolderPath

MSDN 描述:获取文件夹的 CSIDL 并返回其路径名。

C# 定义

// Takes the CSIDL of a folder and returns the pathname.
[DllImport("shell32.dll")]
public static extern Int32 SHGetFolderPath(
    IntPtr hwndOwner,            // Handle to an owner window.
    Int32 nFolder,                // A CSIDL value that identifies the folder whose
                                // path is to be retrieved.
    IntPtr hToken,                // An access token that can be used to represent
                                // a particular user.
    UInt32 dwFlags,                // Flags to specify which path is to be returned.
                                // It is used for cases where 
                                 // the folder associated with a CSIDL may be moved
                                // or renamed by the user. 
    StringBuilder pszPath);        // Pointer to a null-terminated string which will
                                // receive the path.

用法示例

System.Text.StringBuilder path = new System.Text.StringBuilder(256);
SHGetFolderPath(IntPtr.Zero,CSIDL_WINDOWS,IntPtr.Zero,SHGFP_TYPE_CURRENT,path); 
代码解释:这很简单,这个函数可以更快地完成工作。但这两个示例是相同的。

函数:SHParseDisplayName

MSDN 描述:将 Shell 命名空间对象的显示名称转换为项标识符列表,并返回对象的属性。此函数是将字符串转换为项标识符列表(PIDL)指针的首选方法。

C# 定义

// Translates a Shell namespace object's display name into an item 
// identifier list and returns the attributes of the object. This function is 
// the preferred method to convert a string to a pointer to an item identifier 
// list (PIDL). 
[DllImport("shell32.dll")]
public static extern Int32 SHParseDisplayName(
    [MarshalAs(UnmanagedType.LPWStr)]
    String pszName,                // Pointer to a zero-terminated wide string that
                                // contains the display name 
                                   // to parse. 
    IntPtr pbc,                    // Optional bind context that controls the parsing
                                // operation. This parameter 
                                   // is normally set to NULL.
    out IntPtr ppidl,            // Address of a pointer to a variable of type
                                // ITEMIDLIST that receives the item
                                   // identifier list for the object.
    UInt32 sfgaoIn,                // ULONG value that specifies the attributes to
                                // query.
    out UInt32 psfgaoOut);        // Pointer to a ULONG. On return, those attributes
                                // that are true for the 
                                   // object and were requested in sfgaoIn will be set. 

用法示例

ShellLib.IMalloc pMalloc;
pMalloc = ShellLib.ShellFunctions.GetMalloc();

IntPtr pidlRoot;
String sPath = @"c:\temp\divx";
uint iAttribute;

ShellLib.ShellApi.SHParseDisplayName(sPath,IntPtr.Zero,out pidlRoot,0,
    out iAttribute);

if (pidlRoot != IntPtr.Zero)
    pMalloc.Free(pidlRoot);

System.Runtime.InteropServices.Marshal.ReleaseComObject(pMalloc);

代码解释:假设您想要“我的文档”文件夹的 PIDL。我们已经看到如何做到这一点,我们有一个名为 SHGetFolderLocation 的函数,它可以返回所有特殊文件夹的 PIDL。如果我想要一个代表 C:\temp\Divx 的 PIDL 怎么办?在这种情况下,我们将使用 SHParseDisplayName 函数。该示例非常简单,我设置了一个包含所需文件夹的字符串并调用 SHParseDisplayName,结果存储在 pidlRoot 变量中。最后,我不会忘记在使用完 PIDL 后释放其内存。

接口:IShellFolder

MSDN 描述:IShellFolder 接口用于管理文件夹。它由所有 Shell 命名空间文件夹对象公开。
相关 API:SHGetDesktopFolder - 获取桌面文件夹的 IShellFolder 接口,它是 Shell 命名空间的根。SHBindToParent - 此函数接收命名空间对象的项标识符列表(PIDL)的完全限定指针,并返回父对象上的指定接口指针。

C# 定义

[ComImport]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
[Guid("000214E6-0000-0000-C000-000000000046")]
public interface IShellFolder 
{
    // Translates a file object's or folder's display name into an item identifier list.
    // Return value: error code, if any
    [PreserveSig]
    Int32 ParseDisplayName( 
        IntPtr hwnd,            // Optional window handle
        IntPtr pbc,                 // Optional bind context that controls the
                                // parsing operation. This parameter is 
                                // normally set to NULL. 
        [MarshalAs(UnmanagedType.LPWStr)] 
        String pszDisplayName,    // Null-terminated UNICODE string with the
                                // display name.
        ref UInt32 pchEaten,    // Pointer to a ULONG value that receives the
                                // number of characters of the 
                                // display name that was parsed.
        out IntPtr ppidl,           // Pointer to an ITEMIDLIST pointer that receives
                                // the item identifier list for 
                                // the object.
        ref UInt32 pdwAttributes); // Optional parameter that can be used to
                                // query for file attributes.
                                // this can be values from the SFGAO enum
        
    // Allows a client to determine the contents of a folder by creating an item
    // identifier enumeration object and returning its IEnumIDList interface.
    // Return value: error code, if any
    [PreserveSig]
    Int32 EnumObjects( 
        IntPtr hwnd,            // If user input is required to perform the
                                // enumeration, this window handle 
                                // should be used by the enumeration object as
                                // the parent window to take 
                                // user input.
        Int32 grfFlags,             // Flags indicating which items to include in the
                                // enumeration. For a list 
                                   // of possible values, see the SHCONTF enum. 
        out IntPtr ppenumIDList); // Address that receives a pointer to the
                                // IEnumIDList interface of the 
                                // enumeration object created by this method. 

    // Retrieves an IShellFolder object for a subfolder.
    // Return value: error code, if any
    [PreserveSig]
    Int32 BindToObject( 
        IntPtr pidl,            // Address of an ITEMIDLIST structure (PIDL)
                                // that identifies the subfolder.
        IntPtr pbc,                // Optional address of an IBindCtx interface on
                                // a bind context object to be 
                                // used during this operation.
        Guid riid,                  // Identifier of the interface to return. 
        out IntPtr ppv);        // Address that receives the interface pointer.
        
    // Requests a pointer to an object's storage interface. 
    // Return value: error code, if any
    [PreserveSig]
    Int32 BindToStorage( 
        IntPtr pidl,            // Address of an ITEMIDLIST structure that
                                // identifies the subfolder relative 
                                // to its parent folder. 
        IntPtr pbc,                // Optional address of an IBindCtx interface on a
                                // bind context object to be 
                                // used during this operation.
        Guid riid,                  // Interface identifier (IID) of the requested
                                // storage interface.
        out IntPtr ppv);        // Address that receives the interface pointer specified by riid.
        
    // Determines the relative order of two file objects or folders, given their
    // item identifier lists. Return value: If this method is successful, the
    // CODE field of the HRESULT contains one of the following values (the code
    // can be retrived using the helper function GetHResultCode): Negative A
    // negative return value indicates that the first item should precede
    // the second (pidl1 < pidl2). 

    // Positive A positive return value indicates that the first item should
    // follow the second (pidl1 > pidl2).  Zero A return value of zero
    // indicates that the two items are the same (pidl1 = pidl2). 
    [PreserveSig]
    Int32 CompareIDs( 
        Int32 lParam,               // Value that specifies how the comparison
                                // should be performed. The lower 
                                   // Sixteen bits of lParam define the sorting rule.
                                // The upper sixteen bits of 
                                // lParam are used for flags that modify the
                                // sorting rule. values can be from 
                                // the SHCIDS enum
        IntPtr pidl1,               // Pointer to the first item's ITEMIDLIST structure.
        IntPtr pidl2);              // Pointer to the second item's ITEMIDLIST structure.

    // Requests an object that can be used to obtain information from or interact
    // with a folder object.
    // Return value: error code, if any
    [PreserveSig]
    Int32 CreateViewObject( 
        IntPtr hwndOwner,           // Handle to the owner window.
        Guid riid,                  // Identifier of the requested interface. 
        out IntPtr ppv);        // Address of a pointer to the requested interface. 

    // Retrieves the attributes of one or more file objects or subfolders. 
    // Return value: error code, if any
    [PreserveSig]
    Int32 GetAttributesOf( 
        UInt32 cidl,            // Number of file objects from which to retrieve
                                // attributes. 
        [MarshalAs(UnmanagedType.LPArray, SizeParamIndex=0)]
        IntPtr[] apidl,            // Address of an array of pointers to ITEMIDLIST
                                // structures, each of which 
                                // uniquely identifies a file object relative to
                                // the parent folder.
        ref UInt32 rgfInOut);    // Address of a single ULONG value that, on entry,
                                // contains the attributes that 
                                // the caller is requesting. On exit, this value
                                // contains the requested 
                                // attributes that are common to all of the
                                // specified objects. this value can
                                // be from the SFGAO enum
       
    // Retrieves an OLE interface that can be used to carry out actions on the
    // specified file objects or folders.
    // Return value: error code, if any
    [PreserveSig]
    Int32 GetUIObjectOf( 
        IntPtr hwndOwner,        // Handle to the owner window that the client
                                // should specify if it displays 
                                // a dialog box or message box.
        UInt32 cidl,            // Number of file objects or subfolders specified
                                // in the apidl parameter. 
        IntPtr[] apidl,            // Address of an array of pointers to ITEMIDLIST
                                // structures, each of which 
                                // uniquely identifies a file object or subfolder
                                // relative to the parent folder.
        Guid riid,                // Identifier of the COM interface object to return.
        ref UInt32 rgfReserved,    // Reserved. 
        out IntPtr ppv);        // Pointer to the requested interface.

    // Retrieves the display name for the specified file object or subfolder. 
    // Return value: error code, if any
    [PreserveSig]
    Int32 GetDisplayNameOf(
        IntPtr pidl,            // Address of an ITEMIDLIST structure (PIDL)
                                // that uniquely identifies the file 
                                // object or subfolder relative to the parent folder. 
        UInt32 uFlags,              // Flags used to request the type of display name
                                // to return. For a list of 
                                // possible values, see the SHGNO enum. 
        out ShellApi.STRRET pName); // Address of a STRRET structure in which to
                                // return the display name.
        
    // Sets the display name of a file object or subfolder, changing the item
    // identifier in the process.
    // Return value: error code, if any
    [PreserveSig]
    Int32 SetNameOf( 
        IntPtr hwnd,            // Handle to the owner window of any dialog or
                                // message boxes that the client 
                                // displays.
        IntPtr pidl,            // Pointer to an ITEMIDLIST structure that uniquely
                                // identifies the file object
                                // or subfolder relative to the parent folder. 
        [MarshalAs(UnmanagedType.LPWStr)] 
        String pszName,            // Pointer to a null-terminated string that
                                // specifies the new display name. 
        UInt32 uFlags,            // Flags indicating the type of name specified by
                                // the lpszName parameter. For a list of possible
                                // values, see the description of the SHGNO enum. 
        out IntPtr ppidlOut);   // Address of a pointer to an ITEMIDLIST structure
                                // which receives the new ITEMIDLIST. 
}

用法示例

int retVal;

ShellLib.IMalloc pMalloc;
pMalloc = ShellLib.ShellFunctions.GetMalloc();

IntPtr pidlSystem;
retVal = ShellLib.ShellApi.SHGetFolderLocation(
                IntPtr.Zero,
                (int)ShellLib.ShellApi.CSIDL.CSIDL_SYSTEM,
                IntPtr.Zero,
                0,
                out pidlSystem);

IntPtr ptrParent;
IntPtr pidlRelative = IntPtr.Zero;
retVal = ShellLib.ShellApi.SHBindToParent(
                pidlSystem,
                ShellLib.ShellGUIDs.IID_IShellFolder,
                out ptrParent,
                ref pidlRelative);

System.Type shellFolderType = ShellLib.ShellFunctions.GetShellFolderType();
Object obj = System.Runtime.InteropServices.Marshal.GetTypedObjectForIUnknown(
    ptrParent,shellFolderType);
ShellLib.IShellFolder ishellParent = (ShellLib.IShellFolder)obj;

ShellLib.ShellApi.STRRET ptrString;
retVal = ishellParent.GetDisplayNameOf(pidlRelative,
    (uint)ShellLib.ShellApi.SHGNO.SHGDN_NORMAL, out ptrString);

System.Text.StringBuilder strDisplay = new System.Text.StringBuilder(256);
retVal = ShellLib.ShellApi.StrRetToBuf(ref ptrString ,pidlSystem,strDisplay,
    (uint)strDisplay.Capacity);

System.Runtime.InteropServices.Marshal.ReleaseComObject(ishellParent);
pMalloc.Free(pidlSystem);
System.Runtime.InteropServices.Marshal.ReleaseComObject(pMalloc); 

代码解释:在此示例中,我调用了 SHGetFolderLocation 来获取系统的 PIDL。然后我调用 SHBindToObject,它给了我父对象的 IShellFolder。然后我使用接口方法 GetDisplayNameOf 来获取系统 PIDL 的显示名称。结果是一个名为 STRRET 的结构。为了将此结构转换为普通字符串,我需要调用 StrRetToBufStrRetToBstr。我选择了前者,仅仅因为这个示例是 MSDN 文章中 C++ 示例的重写。然后我们需要释放我们使用的所有 COM 对象。就这样。花了我 6 个小时才让这段代码工作起来。

函数:SHGetDesktopFolder

MSDN 描述:获取桌面文件夹的 IShellFolder 接口,它是 Shell 命名空间的根。

C# 定义

// Retrieves the IShellFolder interface for the desktop folder,
//which is the root of the Shell's namespace. 
[DllImport("shell32.dll")]
public static extern Int32 SHGetDesktopFolder(
    out IntPtr ppshf);        // Address that receives an IShellFolder interface
                            // pointer for the 
                            // desktop folder.

用法示例

public static IShellFolder GetDesktopFolder()
{    
    IntPtr ptrRet;
    ShellApi.SHGetDesktopFolder(out ptrRet);

    System.Type shellFolderType = System.Type.GetType("ShellLib.IShellFolder");
    Object obj = Marshal.GetTypedObjectForIUnknown(ptrRet,shellFolderType);
    IShellFolder ishellFolder = (IShellFolder)obj;

    return ishellFolder;
}

{ 
    ... 

    ShellLib.IShellFolder pShellFolder;
    pShellFolder = ShellLib.ShellFunctions.GetDesktopFolder();3

    IntPtr pidlRoot;
    ShellLib.ShellApi.SHGetFolderLocation(
                                IntPtr.Zero,
                                (short)ShellLib.ShellApi.CSIDL.CSIDL_SYSTEM,
                                IntPtr.Zero,
                                0,
                                out pidlRoot);

    ShellLib.ShellApi.STRRET ptrDisplayName;
    pShellFolder.GetDisplayNameOf(
                        pidlRoot,
                        (uint)ShellLib.ShellApi.SHGNO.SHGDN_NORMAL 
                        | (uint)ShellLib.ShellApi.SHGNO.SHGDN_FORPARSING,
                        out ptrDisplayName);

    String sDisplay;
    ShellLib.ShellApi.StrRetToBSTR(ref ptrDisplayName,pidlRoot,out sDisplay);

    System.Runtime.InteropServices.Marshal.ReleaseComObject(pShellFolder);
}

代码解释:在这里,我创建了一个名为 GetDesktopFolder 的函数,它返回桌面文件夹的 IShellFolder。然后我做了一些与上一个示例类似的事情。我获取了显示名称,这次是不同的格式,并使用不同的 API 将返回的 STRRET 结构转换为字符串。

函数:SHBindToParent

MSDN 描述:此函数接收命名空间对象的项标识符列表(PIDL)的完全限定指针,并返回父对象上的指定接口指针。

C# 定义

// This function takes the fully-qualified pointer to an item
// identifier list (PIDL) of a namespace object, and returns a specified
// interface pointer on the parent object.
[DllImport("shell32.dll")]
public static extern Int32 SHBindToParent(
    IntPtr pidl,            // The item's PIDL. 
    [MarshalAs(UnmanagedType.LPStruct)]
    Guid riid,                  // The REFIID of one of the interfaces exposed by
                            // the item's parent object. 
    out IntPtr ppv,            // A pointer to the interface specified by riid. You
                            // must release the object when 
                            // you are finished. 
    ref IntPtr ppidlLast);    // The item's PIDL relative to the parent folder. This
                            // PIDL can be used with many
                            // of the methods supported by the parent folder's
                            // interfaces. If you set ppidlLast 
                            // to NULL, the PIDL will not be returned. 

简而言之:我不会给出使用此函数的示例,因为我已经在 IShellFolder 部分给出过了。

函数:StrRetToBSTR

MSDN 描述:接受 IShellFolder::GetDisplayNameOf 返回的 STRRET 结构,该结构包含或指向一个字符串,然后将该字符串作为 BSTR 返回。

C# 定义

// Accepts a STRRET structure returned by
// ShellFolder::GetDisplayNameOf that contains or points to a string, and then
// returns that string as a BSTR.
[DllImport("shlwapi.dll")]
public static extern Int32 StrRetToBSTR(
    ref STRRET pstr,    // Pointer to a STRRET structure.
    IntPtr pidl,        // Pointer to an ITEMIDLIST uniquely identifying a file
                        // object or subfolder relative
                        // to the parent folder.
    [MarshalAs(UnmanagedType.BStr)]
    out String pbstr);    // Pointer to a variable of type BSTR that contains the
                        // converted string.

函数:StrRetToBuf

MSDN 描述:接收 IShellFolder::GetDisplayNameOf 返回的 STRRET 结构,将其转换为字符串,并将结果放入缓冲区。

C# 定义

// Takes a STRRET structure returned by IShellFolder::GetDisplayNameOf,
// converts it to a string, and places the result in a buffer. 
[DllImport("shlwapi.dll")]
public static extern Int32 StrRetToBuf(
    ref STRRET pstr,    // Pointer to the STRRET structure. When the function
                        // returns, this pointer will no
                        // longer be valid.
    IntPtr pidl,        // Pointer to the item's ITEMIDLIST structure.
    StringBuilder pszBuf, // Buffer to hold the display name. It will be returned
                        // as a null-terminated
                        // string. If cchBuf is too small, the name will be
                        // truncated to fit. 
    UInt32 cchBuf);        // Size of pszBuf, in characters. If cchBuf is too small,
                        // the string will be 
                        // truncated to fit. 

第二部分:ShellBrowseForFolderDialog - 封装对话框的类

我们如何获取“浏览文件夹”对话框?答案在于一个名为 SHBrowseForFolder 的 Shell API 函数。根据 MSDN 的说法,此函数:显示一个对话框,允许用户选择一个 Shell 文件夹。那么,我们如何使用它呢?首先,我们需要它的 C# 声明。

// Displays a dialog box that enables the user to select a Shell folder. 
[DllImport("shell32.dll")]
public static extern IntPtr SHBrowseForFolder(
    ref BROWSEINFO lbpi);    // Pointer to a BROWSEINFO structure that contains
                            // information used to display 
                            // the dialog box. 

这很好,返回值是所选 Shell 项的 PIDL(记住,可以是文件、文件夹或虚拟文件夹)。稍后在类中,我们将看到如何返回所选 PIDL 的显示名称。BROWSEINFO 结构包含有关对话框外观的特定详细信息。BROWSEINFO 也需要一个声明。

// Contains parameters for the SHBrowseForFolder function and
// receives information about the folder selected 
// by the user.
[StructLayout(LayoutKind.Sequential)]
public struct BROWSEINFO
{
    public IntPtr hwndOwner;                // Handle to the owner window for the
                                            // dialog box.
    public IntPtr pidlRoot;                    // Pointer to an item identifier list
                                            // (PIDL) specifying the location of
                                            // the root folder from which to start
                                            // browsing.
    [MarshalAs(UnmanagedType.LPStr)]        // Address of a buffer to receive the
                                            // display name of the 
    public String pszDisplayName;            // folder selected by the user.
    [MarshalAs(UnmanagedType.LPStr)]        // Address of a null-terminated string
                                            // that is displayed 
    public String lpszTitle;                // above the tree view control in the
                                            // dialog box.
    public UInt32 ulFlags;                    // Flags specifying the options for the
                                            // dialog box.
    [MarshalAs(UnmanagedType.FunctionPtr)]    // Address of an application-defined
                                            // function that the 
    public BrowseCallbackProc lpfn;               // dialog box calls when an event occurs.
    public Int32 lParam;                    // Application-defined value that the
                                            // dialog box passes to 
                                              // the callback function
    public Int32 iImage;                    // Variable to receive the image
                                            // associated with the selected folder.
}

hwndOwner 是您放置所有者窗口句柄的地方,该对话框相对于此所有者窗口是模态的。pidlRoot 是一个 PIDL,指定对话框中的根文件夹。pszDisplayName 是所选项目的短显示名称返回的地方。lpszTitle 是您可以在对话框中定义的说明文本。ulFlags 是您设置对话框所有小细节的地方,例如您是否希望对话框包含文件,或者对话框是否只返回打印机或计算机,您是否希望对话框具有新的对话框样式,是否希望对话框中存在“新建文件夹”按钮,所有这些选项以及更多选项都在此标志成员中定义。lpfn 是一个委托函数,对话框会使用某些事件(如选定更改或对话框初始化)调用此函数,因此您可以定义一个函数并响应这些事件。这些是重要的成员。

所以,您需要做的就是创建一个 BROWSEINFO 结构,用您的数据填充它,然后调用该函数。当然,您需要处理 PIDL、数字标志和 CALLBACK 事件。幸好我已经为您做好了,让我们看看我做了什么,以及它是如何工作的。

设置主要成员

ShellBrowseForFolderDialog 类有几个主要属性需要设置,它们是:

/// <summary> Handle to the owner window for the dialog box. </summary>
public IntPtr hwndOwner;

/// <summary> Select the root type </summary>
public RootTypeOptions RootType;

/// <summary> valid only if RootType is RootTypeOptions.ByPath </summary>
public string RootPath;

/// <summary> valid only if RootType is RootTypeOptions.BySpecialFolder </summary>
public ShellApi.CSIDL RootSpecialFolder;

/// <summary> 
/// Address of a null-terminated string that is displayed above the tree view 
/// control in the dialog box. 
/// </summary>
public string Title;

/// <summary> Token that can be used to represent a particular user. </summary>
public IntPtr UserToken; 

hwndOwner 是所有者窗口的句柄,通常会设置为窗体的 this.Handle 属性。它也可以是 IntPtr.Zero,在这种情况下,对话框父窗口将是桌面。

RootType 可以是以下 RootTypeOptions 枚举值之一:BySpecialFolderByPath。如果将其设置为 BySpecialFolder,那么您还应该设置 RootSpecialFolder 属性,该属性是所有特殊文件夹的枚举(有关特殊文件夹的讨论,请参阅第一部分 SHGetFolderLocation 下面的内容),这样做会将对话框的根文件夹设置为选定的特殊文件夹。但是,如果您想将根文件夹设置为特定路径怎么办?在这种情况下,您应该将 RootType 属性设置为 ByPath,然后将 RootPath 属性设置为您想要的路径。请注意,对话框将根据 RootType 的值进行操作,将 RootType 设置为 BySpecialFolder 将忽略 RootPath 的值。

Title 是您在对话框中设置说明文本的地方,最多可以输入 3 行文本,用 '\n' 分隔。

最后是 UserToken。大多数情况下,它将是 IntPtr.Zero,这是默认值,但假设您希望对话框的根文件夹是特定用户的“我的文档”?在这种情况下,您需要获取用户令牌并设置此属性,不要忘记将 RootType 设置为 BySpecialFolder,并将 RootSpecialFolder 设置为“我的文档”值。

设置标志成员

还记得 BROWSEINFO 结构中的 flags 成员吗?好吧,我决定最好将其分成许多布尔标志,并具有方便的默认值。当我需要设置 flags 成员时,我将使用一个函数来根据所有布尔属性创建正确的 flags 值。所以,以下是布尔标志:

/// <summary> 
Only return computers. If the user selects anything other than a computer,
/// the OK button is grayed. </summary>
public bool BrowseForComputer;

/// <summary> 
Only return printers. If the user selects anything other than a printer, the
/// OK button is grayed.</summary>
public bool BrowseForPrinter;

/// <summary> The browse dialog box will display files as well as folders.
/// </summary>
public bool IncludeFiles;

/// <summary> 
/// The browse dialog box can display URLs. The BIF_USENEWUI and
/// BIF_BROWSEINCLUDEFILES flags must also be set. If these three flags are not
/// set, the browser dialog box will reject URLs. Even when these flags are set,
/// the browse dialog box will only display URLs if the folder that contains the 
/// selected item supports them. When the folder's IShellFolder::GetAttributesOf
/// method is called to request the selected item's attributes, the folder must
/// set the SFGAO_FOLDER attribute flag. Otherwise, the browse dialog box will
/// not display the URL. </summary>
public bool IncludeUrls;

/// <summary> Do not include network folders below the domain level in the 
/// dialog box's tree view control. </summary>
public bool DontGoBelowDomain;

/// <summary> Include an edit control in the browse dialog box that allows the 
/// user to type the name of an item. </summary>
public bool EditBox;

/// <summary> Use the new user interface. Setting this flag provides the user 
/// with a larger dialog box that can be resized. The dialog box has several new 
/// capabilities including: drag and drop capability within the dialog box, 
/// reordering, shortcut menus, new folders, delete, and other shortcut menu 
/// commands. </summary>
public bool NewDialogStyle;

/// <summary> Do not include the New Folder button in the browse dialog box.
/// </summary>
public bool NoNewFolderButton;

/// <summary> When the selected item is a shortcut, return the PIDL of the 
/// shortcut itself rather than its target. </summary>
public bool NoTranslateTargets;

/// <summary> Only return file system ancestors. An ancestor is a subfolder that 
/// is beneath the root folder in the namespace hierarchy. If the user selects an 
/// ancestor of the root folder that is not part of the file system, the OK button 
/// is grayed. </summary>
public bool ReturnOnlyFileSystemAncestors;

/// <summary> Only return file system directories. If the user selects folders 
/// that are not part of the file system, the OK button is grayed. </summary>
public bool ReturnOnlyFileSystemDirs;

/// <summary> The browse dialog box can display shareable resources on remote 
/// systems. It is intended for applications that want to expose remote shares on a 
/// local system. The BIF_USENEWUI flag must also be set. </summary>
public bool Shareable;

/// <summary> Include a status area in the dialog box. The callback function can 
/// set the status text by sending messages to the dialog box. </summary>
public bool StatusText;

/// <summary> When combined with BIF_NEWDIALOGSTYLE, adds a usage hint to the 
/// dialog box in place of the edit box. BIF_EDITBOX overrides this flag. </summary>
public bool UsageHint;

/// <summary> Use the new user interface, including an edit box. This flag is 
/// equivalent to BIF_EDITBOX | BIF_NEWDIALOGSTYLE. </summary>
public bool UseNewUI;

/// <summary> If the user types an invalid name into the edit box, the browse 
/// dialog box will call the application's BrowseCallbackProc with the 
/// BFFM_VALIDATEFAILED message. This flag is ignored if BIF_EDITBOX is not 
/// specified. </summary>
public bool Validate;

我不会逐一介绍这些标志,因为它们相当简单,并且旁边有简短的说明。您也可以通过设置标志来测试会发生什么。另一个关于标志的重要代码是,当我需要调用 SHBrowseForFolder 函数时,获取标志值的函数。代码如下:

private UInt32 GetFlagsValue()
{
    UInt32 flags = 0;

    if (BrowseForComputer)            flags |= (uint)ShellApi.BIF.BIF_BROWSEFORCOMPUTER;
    if (BrowseForPrinter)             flags |= (uint)ShellApi.BIF.BIF_BROWSEFORPRINTER;
    if (IncludeFiles)                flags |= (uint)ShellApi.BIF.BIF_BROWSEINCLUDEFILES;
    if (IncludeUrls)                flags |= (uint)ShellApi.BIF.BIF_BROWSEINCLUDEURLS;
    if (DontGoBelowDomain)             flags |= (uint)ShellApi.BIF.BIF_DONTGOBELOWDOMAIN;
    if (EditBox)                    flags |= (uint)ShellApi.BIF.BIF_EDITBOX;
    if (NewDialogStyle)                flags |= (uint)ShellApi.BIF.BIF_NEWDIALOGSTYLE;
    if (NoNewFolderButton)            flags |= (uint)ShellApi.BIF.BIF_NONEWFOLDERBUTTON;
    if (NoTranslateTargets)            flags |= (uint)ShellApi.BIF.BIF_NOTRANSLATETARGETS;
    if (ReturnOnlyFileSystemAncestors) flags |= (uint)ShellApi.BIF.BIF_RETURNFSANCESTORS;
    if (ReturnOnlyFileSystemDirs)    flags |= (uint)ShellApi.BIF.BIF_RETURNONLYFSDIRS;
    if (Shareable)                    flags |= (uint)ShellApi.BIF.BIF_SHAREABLE;
    if (StatusText)                    flags |= (uint)ShellApi.BIF.BIF_STATUSTEXT;
    if (UsageHint)                    flags |= (uint)ShellApi.BIF.BIF_UAHINT;
    if (UseNewUI)                    flags |= (uint)ShellApi.BIF.BIF_USENEWUI;
    if (Validate)                    flags |= (uint)ShellApi.BIF.BIF_VALIDATE;

    return flags;
}

更新:如您所见,本节中的代码相当难看,一个小消息灵通人士告诉我,可以用一种漂亮简单的方式完成。因此,我更改了代码,感谢 leppie 的精彩建议。因此,更改包括:BIF 枚举现在具有 [Flags] 属性,并且我还将其名称更改为 BrowseInfoFlags。在对话框类中,有一个名为 DetailsFlags 的成员,定义如下:

public BrowseInfoFlag DetailsFlags;

现在,当您想设置一些标志时,您可以这样做:

DetailsFlags = BrowseInfoFlag.BIF_BROWSEINCLUDEFILES 
                | BrowseInfoFlag.BIF_EDITBOX 
                | BrowseInfoFlag.BIF_NEWDIALOGSTYLE 
                | BrowseInfoFlag.BIF_SHAREABLE
                | BrowseInfoFlag.BIF_STATUSTEXT
                | BrowseInfoFlag.BIF_USENEWUI
                | BrowseInfoFlag.BIF_VALIDATE;

显示对话框并获取结果

设置完类属性后,我们将要显示对话框,然后将选定的项目作为普通路径获取。在 ShowDialog 方法中,我们要做的第一件事是获取根文件夹的 PIDL。如果我们使用特殊文件夹,我们需要调用 SHGetFolderLocation 来接收特殊文件夹的 PIDL。如果我们要一个特定路径,我们使用 SHParseDisplayName 来获取指定路径的 PIDL。

然后我们创建一个 BROWSEINFO 结构并用属性中的信息填充它。然后我们调用 SHBrowseForFolder,它会显示对话框。当我们选择一个项目时,它会返回所选项目的 PIDL。但我们想要返回该项目的显示名称,而不是 PIDL。因此,我们要做的是请求桌面项目的 IShellFolder 接口,并使用接口方法 GetDisplayNameOf 来获取我们 PIDL 的显示名称。我们需要桌面项目的 IShellFolder 的原因是,所选 PIDL 是相对于桌面 PIDL 的。我们还没有完成,因为 GetDisplayNameOf 返回一个 STRRET 结构。为了获得普通的字符串值,我们需要使用 StrRetToBSTR 函数。

我们差不多完成了。如果我们想成为好的开发者,我们就需要释放返回给我们的 PIDL。要做到这一点,我们需要获取 Shell 的 IMalloc 接口,并调用其 Free 方法来释放 PIDL。并且不要忘记释放 IMalloc 对象本身。

下面的代码是我所做的,请仔细阅读,看看是否清晰。设置 BROWSEINFO 结构函数指针到类委托的部分将在后面解释。

public void ShowDialog()
{
    m_FullName = "";
    m_DisplayName = "";

    // Get shell's memory allocator, it is needed to free some memory later
    IMalloc pMalloc;
    pMalloc = ShellFunctions.GetMalloc();

    IntPtr pidlRoot;
    
    if (RootType == RootTypeOptions.BySpecialFolder)
    {
        ShellApi.SHGetFolderLocation(hwndOwner,(int)RootSpecialFolder,UserToken,
            0,out pidlRoot);
    }
    else // m_RootType = RootTypeOptions.ByPath
    {
        uint iAttribute;
        ShellApi.SHParseDisplayName(RootPath,IntPtr.Zero,out pidlRoot,0,
            out iAttribute);
    }
                            
    ShellApi.BROWSEINFO bi = new ShellApi.BROWSEINFO();
    
    bi.hwndOwner = hwndOwner;
    bi.pidlRoot = pidlRoot;
    bi.pszDisplayName = new String(' ',256);
    bi.lpszTitle = Title;
    bi.ulFlags = (uint)DetailsFlags; 
    bi.lParam = 0;
    bi.lpfn = new ShellApi.BrowseCallbackProc(this.myBrowseCallbackProc);
    
    // Show dialog
    IntPtr pidlSelected;
    pidlSelected = ShellLib.ShellApi.SHBrowseForFolder(ref bi);

    // Save the display name
    m_DisplayName = bi.pszDisplayName.ToString();

    IShellFolder isf = ShellFunctions.GetDesktopFolder();

    ShellApi.STRRET ptrDisplayName;
    isf.GetDisplayNameOf(
        pidlSelected,
        (uint)ShellApi.SHGNO.SHGDN_NORMAL | 
        (uint)ShellApi.SHGNO.SHGDN_FORPARSING,
        out ptrDisplayName);
    
    String sDisplay;
    ShellLib.ShellApi.StrRetToBSTR(ref ptrDisplayName,pidlRoot,out sDisplay);
    m_FullName = sDisplay;

    if (pidlRoot != IntPtr.Zero)
        pMalloc.Free(pidlRoot);
    
    if (pidlSelected != IntPtr.Zero)
        pMalloc.Free(pidlSelected);
    
    Marshal.ReleaseComObject(isf);
    Marshal.ReleaseComObject(pMalloc);
}

使用消息和事件

好吧,还记得将委托函数设置为 BROWSEINFO 结构的行吗?委托函数类型声明如下:

public delegate Int32 BrowseCallbackProc(IntPtr hwnd, UInt32 uMsg, Int32 lParam, Int32 lpData);

这就是 Shell 通知您事件的方式。您需要做的是声明一个此类型的函数,将函数指针设置为您的函数,当事件发生时,Shell 会用正确的消息通知您的函数。有 4 种消息:

BFFM_INITIALIZED:已初始化 - 当对话框完成初始化时通知您,让您有机会初始化您的内容。

BFFM_IUNKNOWN:IUnknown - 提供一个 iunknown 接口指针,让您可以设置对话框的自定义筛选,稍后将对此进行介绍。

BFFM_SELCHANGED:选定更改 - 当用户更改其选定项时通知您。

BFFM_VALIDATEFAILED:验证失败 - 如果您设置了 EditBox 的标志,表示允许用户输入字符串,但用户输入了无效字符串(输入的文件夹不存在),Shell 会通知您。(实际上,有两个消息用于此,一个用于 ansi 版本,一个用于 Unicode 版本。在我的测试中,我只收到其中一个,我猜是平台相关的)。

所以,这些是您可以接收的通知。在我的类中,我没有让您指定该函数,因为这很难看。我所做的是定义 4 个委托属性,因此如果您想收到通知,只需创建一个事件处理程序并设置类以使用您的事件处理程序。这还允许我将指针转换为普通字符串,并将正常值传递给您的函数。委托及其参数声明如下:

public class InitializedEventArgs : EventArgs
{
    public InitializedEventArgs(IntPtr hwnd)
    {
        this.hwnd = hwnd;
    }
    public readonly IntPtr hwnd;
}

public class IUnknownEventArgs : EventArgs
{
    public IUnknownEventArgs(IntPtr hwnd, IntPtr iunknown)
    {
        this.hwnd = hwnd;
        this.iunknown = iunknown;
    }
    public readonly IntPtr hwnd;
    public readonly IntPtr iunknown;
}

public class SelChangedEventArgs : EventArgs
{
    public SelChangedEventArgs(IntPtr hwnd, IntPtr pidl)
    {
        this.hwnd = hwnd;
        this.pidl = pidl;
    }
    public readonly IntPtr hwnd;
    public readonly IntPtr pidl;
}

public class ValidateFailedEventArgs : EventArgs
{
    public ValidateFailedEventArgs(IntPtr hwnd, string invalidSel)
    {
        this.hwnd = hwnd;
        this.invalidSel = invalidSel;
    }
    public readonly IntPtr hwnd;
    public readonly string invalidSel;
}


public delegate void InitializedHandler(ShellBrowseForFolderDialog sender,
    InitializedEventArgs args);
public delegate void IUnknownHandler(ShellBrowseForFolderDialog sender,
    IUnknownEventArgs args);
public delegate void SelChangedHandler(ShellBrowseForFolderDialog sender,
    SelChangedEventArgs args);
public delegate int ValidateFailedHandler(ShellBrowseForFolderDialog sender,
    ValidateFailedEventArgs args);  

public event InitializedHandler OnInitialized;
public event IUnknownHandler OnIUnknown;
public event SelChangedHandler OnSelChanged;
public event ValidateFailedHandler OnValidateFailed;

我的 CALLBACK 函数调用您的委托函数的代码如下:

private Int32 myBrowseCallbackProc(IntPtr hwnd, UInt32 uMsg,
    Int32 lParam, Int32 lpData)
{
    switch ((BrowseForFolderMessages)uMsg)
    {
        case BrowseForFolderMessages.BFFM_INITIALIZED:
            System.Diagnostics.Debug.WriteLine("BFFM_INITIALIZED");

            if (OnInitialized != null)
            {
                InitializedEventArgs args = new InitializedEventArgs(hwnd);
                OnInitialized(this,args);
            }

            break;

        case BrowseForFolderMessages.BFFM_IUNKNOWN:
            System.Diagnostics.Debug.WriteLine("BFFM_IUNKNOWN");

            if (OnIUnknown != null)
            {
                IUnknownEventArgs args = new IUnknownEventArgs(hwnd,(IntPtr)lParam);
                OnIUnknown(this,args);
            }

            break;

        case BrowseForFolderMessages.BFFM_SELCHANGED:
            System.Diagnostics.Debug.WriteLine("BFFM_SELCHANGED");
            
            if (OnSelChanged != null)
            {
                SelChangedEventArgs args = new SelChangedEventArgs(hwnd,(IntPtr)lParam);
                OnSelChanged(this,args);
            }
        
            break;
        
        case BrowseForFolderMessages.BFFM_VALIDATEFAILEDA:
            System.Diagnostics.Debug.WriteLine("BFFM_VALIDATEFAILEDA");
        
            if (OnValidateFailed != null)
            {
                string failedSel = Marshal.PtrToStringAnsi((IntPtr)lParam);
                ValidateFailedEventArgs args = new ValidateFailedEventArgs(hwnd,failedSel);
                return OnValidateFailed(this,args);
            }
            break;

        case BrowseForFolderMessages.BFFM_VALIDATEFAILEDW:
            System.Diagnostics.Debug.WriteLine("BFFM_VALIDATEFAILEDW");
            
            if (OnValidateFailed != null)
            {
                string failedSel = Marshal.PtrToStringUni((IntPtr)lParam);
                ValidateFailedEventArgs args = new ValidateFailedEventArgs(hwnd,failedSel);
                return OnValidateFailed(this,args);
            }
                                
            break;
    }

    return 0;
}

另一种消息类型是您可以通过 API 函数 SendMessage 向 Shell 对话框窗口发送的特定消息。这些消息允许您稍微控制对话框,并且只能从您的事件处理程序发送,这就是为什么您在所有事件中都会收到 Shell 对话框窗口的句柄。消息是:

BFFM_ENABLEOK:启用 OK - 此消息告诉 Shell 对话框启用或禁用 OK 按钮。例如,您可以响应选定更改事件,如果选定项是“my_secret_file.txt”,那么您可以发送一条消息来禁用 OK 按钮。

BFFM_SETEXPANDED:设置展开 - 使用此消息,您可以告诉 Shell 对话框展开一个文件夹。例如,您可以响应初始化事件并将 Windows 文件夹设置为展开状态,即使您仍然可以从其他文件夹中选择。

BFFM_SETSELECTION:设置选定 - 与前一个消息类似,但此消息会选中一个项目,而不是展开它。

BFFM_SETSTATUSTEXT:设置状态文本 - 当您使用旧的对话框样式时,您可以使用此消息设置状态文本。

BFFM_SETOKTEXT:设置 OK 文本 - 此消息允许您设置 OK 按钮的文本。例如,您可以响应初始化事件并将按钮文本设置为“KABOOM!”如果您愿意的话。另一个想法是在响应选定更改事件时设置按钮文本。

那么,如何发送这些事件呢?您可能需要声明 SendMessage API 并开始处理原始 Win32 代码……幸运的是,我已经为您完成了,您只需要调用以下方法。不要忘记,您只能从您的事件处理程序调用这些方法。以下是方法:

public void EnableOk(IntPtr hwnd, bool Enabled)
{
    SendMessage(hwnd, (uint)BrowseForFolderMessages.BFFM_ENABLEOK, 0, Enabled ? 1 : 0); 
}

public void SetExpanded(IntPtr hwnd, string path)
{
    SendMessage(hwnd, (uint)BrowseForFolderMessages.BFFM_SETEXPANDED, 1, path);
}

public void SetOkText(IntPtr hwnd, string text)
{
    SendMessage(hwnd, (uint)BrowseForFolderMessages.BFFM_SETOKTEXT, 0, text);
}

public void SetSelection(IntPtr hwnd, string path)
{
    SendMessage(hwnd, (uint)BrowseForFolderMessages.BFFM_SETSELECTIONW, 1, path);
}

public void SetStatusText(IntPtr hwnd, string text)
{
    SendMessage(hwnd, (uint)BrowseForFolderMessages.BFFM_SETSTATUSTEXTW, 1, text);
}

请注意,有时我使用 SendMessage 函数,最后一个参数是数字,有时是字符串。这不是魔术,我不得已声明了两次 SendMessage 函数,如下所示:

// The SendMessage function sends the specified message to a 
// window or windows. It calls the window procedure for the specified 
// window and does not return until the window procedure has processed the message. 
[DllImport("User32.dll")]
public static extern Int32 SendMessage(
    IntPtr hWnd,               // handle to destination window
    UInt32 Msg,                // message
    UInt32 wParam,             // first message parameter
    Int32 lParam               // second message parameter
    );

[DllImport("User32.dll")]
public static extern Int32 SendMessage(
    IntPtr hWnd,               // handle to destination window
    UInt32 Msg,                // message
    UInt32 wParam,             // first message parameter
    [MarshalAs(UnmanagedType.LPWStr)]
    String lParam              // second message parameter
    );

自定义筛选

什么是自定义筛选?我将从一个例子开始。假设我想让对话框只显示 txt 或 bmp 文件,当然还有所有文件夹。当然,我可以处理选定更改事件,如果选定的文件不是 bmp 或 txt,则禁用 OK 按钮,但这并不是我想要的。我不想看到除 bmp 和 txt 文件之外的任何文件。这就是自定义筛选,意味着您可以为每个项目选择是否显示它们。

要实现自定义筛选,您需要执行以下步骤:

1. 创建一个继承 IFolderFilter 接口的对象。此接口有两个方法:GetEnumFlags - 当 Shell 需要知道您想显示哪种类型的项目时,会调用此函数。在这里,您可以返回您想要只显示文件夹、只显示文件还是两者都显示。这是一个最小的筛选。另一个函数:ShouldShow - 在显示每个项目之前,Shell 会调用此函数,让您决定是否要显示它。在此函数中,您可以获取特定项目的 PIDL,以便您可以检查项目的任何信息,例如,您可以检查它是一个文件还是一个文件夹,如果它是一个文件,您可以检查该文件是否具有有效的扩展名(bmp 或 txt)。甚至,您还可以检查文件大小,让对话框只显示大于 666K 的文件……

在创建了一个继承 IFolderFilter 的类并编写了自己的 GetEnumFlagsShouldShow 函数后,您可以进入下一步。

2. 响应 IUnknown 事件(我告诉过您稍后会讨论它),您将该接口查询为 IFolderFilterSite 接口。此接口只有一个函数:SetFilter。此函数接收您在第一步中创建的 FolderFilter 对象的实例。这样,Shell 在需要决定是否显示项目时就知道调用谁。

就是这样,完成这两个步骤后,每次 Shell 需要知道是否在 Shell 对话框中显示项目时,它都会调用您预定义的类中的 ShouldShow 函数。

注意,IFolderFilterIFolderFilterSite 接口已在本文随附的源代码中声明。此外,还添加了一个自定义筛选的示例。我创建的筛选器具有字符串数组(string[])类型的属性,它将保存允许显示的有效文件扩展名。这是代码:

[ComImport]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
[Guid("9CC22886-DC8E-11d2-B1D0-00C04F8EEB3E")]
public interface IFolderFilter
{
    // Allows a client to specify which individual items should be enumerated.
    // Note: The host calls this method for each item in the folder. Return S_OK,
    // to have the item enumerated. 
    // Return S_FALSE to prevent the item from being enumerated.
    [PreserveSig] 
    Int32 ShouldShow(
        [MarshalAs(UnmanagedType.Interface)]Object psf,                
                        // A pointer to the folder's IShellFolder interface.
        IntPtr pidlFolder,    // The folder's PIDL.
        IntPtr pidlItem);    // The item's PIDL.

    // Allows a client to specify which classes of objects in a Shell folder 
    // should be enumerated.
    [PreserveSig] 
    Int32 GetEnumFlags( 
        [MarshalAs(UnmanagedType.Interface)]Object psf,                
                        // A pointer to the folder's IShellFolder interface.
        IntPtr pidlFolder,    // The folder's PIDL.
        IntPtr phwnd,        // A pointer to the host's window handle.
        out UInt32 pgrfFlags); // One or more SHCONTF values that specify which
                        // classes of objects to enumerate.
    
};

最后,您需要响应 IUnknown 事件才能使用此筛选器。代码如下:

private void IUnknownEvent(
    ShellLib.ShellBrowseForFolderDialog sender,
    ShellLib.ShellBrowseForFolderDialog.IUnknownEventArgs args)
{
    IntPtr iFolderFilterSite;

    if (args.iunknown == IntPtr.Zero)
        return;

    System.Runtime.InteropServices.Marshal.QueryInterface(
        args.iunknown,
        ref ShellLib.ShellGUIDs.IID_IFolderFilterSite,
        out iFolderFilterSite);
    
    Object obj = System.Runtime.InteropServices.Marshal.GetTypedObjectForIUnknown(
        iFolderFilterSite,
        ShellLib.ShellFunctions.GetFolderFilterSiteType());
    ShellLib.IFolderFilterSite folderFilterSite = (ShellLib.IFolderFilterSite)obj;
                
    ShellLib.FilterByExtension filter = new ShellLib.FilterByExtension();
    
    string[] ext = new string[2];
    ext[0] = "bmp";
    ext[1] = "txt";
                
    filter.ValidExtension = ext;
    
    folderFilterSite.SetFilter(filter);
}

使用该类

使用该类非常简单,几乎不涉及 Shell 相关内容,这是一个示例:

private void button5_Click(object sender, System.EventArgs e)
{
    ShellLib.ShellBrowseForFolderDialog folderDialog =
        new ShellLib.ShellBrowseForFolderDialog();
    folderDialog.hwndOwner = this.Handle;
    // Scenario A - take the defaults

    // Scenario B - select from a special folder
    //folderDialog.RootType = ShellLib.ShellBrowseForFolderDialog.RootTypeOptions.
    // BySpecialFolder;
    //folderDialog.RootSpecialFolder = ShellLib.ShellApi.CSIDL.CSIDL_WINDOWS;
    
    // Scenario C - select from a specific path
    //folderDialog.RootType = ShellLib.ShellBrowseForFolderDialog.RootTypeOptions.
    //ByPath;
    //folderDialog.RootPath = @"c:\temp\divx";
    
    folderDialog.Title = "Hello CodeProject readers!";
    folderDialog.Title += "\n";
    folderDialog.Title += "This is my extensible Shell dialog.";
    folderDialog.Title += "\n";
    folderDialog.Title += "Please select a bmp or a txt file:";

    // register events
    folderDialog.OnInitialized += 
        new ShellLib.ShellBrowseForFolderDialog.InitializedHandler(
            this.InitializedEvent);
    folderDialog.OnIUnknown += 
        new ShellLib.ShellBrowseForFolderDialog.IUnknownHandler(
            this.IUnknownEvent);
    folderDialog.OnSelChanged += 
        new ShellLib.ShellBrowseForFolderDialog.SelChangedHandler(
            this.SelChangedEvent);
    folderDialog.OnValidateFailed += 
        new ShellLib.ShellBrowseForFolderDialog.ValidateFailedHandler(
            this.ValidateFailedEvent);

    folderDialog.ShowDialog();
    MessageBox.Show("Display Name: " + folderDialog.DisplayName + "\nFull Name: "
        + folderDialog.FullName );
}

好了。这是一篇很长的文章,但对于开始使用 Shell 来说是必要的。我保证未来的文章将更有趣,更少枯燥的函数和接口……

如果您有任何评论,我将非常乐意知道。希望您喜欢,请不要忘记投票。

历史

2003/01/25 - 文章首次发布。
2003/01/26 - 文章和代码已更新。

© . All rights reserved.