第五章 浏览文件夹
我在第二章中给出了文件夹的概览和它在Windows Shell中的地位,在这一章中我们打算更详细地讨论它们。我们主要集中精力阐述涉及文件夹所有层面的Shell函数,以及保证所有操作顺利进行的潜在机理。因此,我们需要深入研究两个起着非常重要作用的概念:快捷方式和PIDLs。前者是下一章的题目,在这一章中我们将研究PIDLs,其中包括:
SHBrowseForFolder()函数的用途
关于PIDLs进一步的讨论,以及怎样使用PIDLs
虚拟文件夹和位置
怎样获得文件夹的设置
我们将要论述的例子包含了一个增强版本的API函数SHBrowseForFolder(),一些使它更容易同PIDLs一道工作的辅助函数,以及一些怎样枚举某些指定位置(如,‘发送到’,‘Favorites’,以及‘我的资料’)内容的样例程序。
选择文件夹
让我们先从各种文件夹选择方法开始我们的讨论。对于让用户能够从特定驱动器上选择特殊目录的应用程序,这是一个普通的需求。Windows3.x API没有为这提供任何内建的工具,所以必须建立自己的辅助函数,然而,有一项通用技术可以使用,它由修改通用对话框模版组成,例如删除象列表框那样的包含文件名的控件。
然而,推出这个方案到Win32有一个障碍:你必须抛弃新探测器风格的用户界面,而仍然忠实地使用老界面:
在Win32平台上,探测器风格的‘打开’对话框是一个单一实体,其中的任何控件都是不能摆脱掉的(比如:文件列表框)。
要选择采用老的Windows3.x界面,另一个选择是安排一个VC++显示方式的对话框到新项目中来请求一个特殊的文件夹。
跳出Win32关于GetOpenFileName()函数的资料,能够发现更多的东西。
更现代的方法
从Wondwos95 开始,Win32 SDK 就包含了浏览文件夹的系统解决方案:这个函数称为SHBrowseForFolder()。其主要的特征是使用类似于我们已知的和探测器钟情的树观察:
与前两章中我们测试过的函数一样,SHBrowseForFolder()函数有一个简单的原型,但是,它实际上包含了一个带有大量设置和标志的结构,可以把这个函数看作是文件夹的中心函数,其目的之一就是使我们能够在桌面命名空间中选择可用的文件夹。
SHBrowseForFolder()函数的原型
现在看一下SHBrowseForFolder()的原型,它声明在shlobj.h中:
LPITEMIDLIST WINAPI SHBrowseForFolder(LPBROWSEINFO lpbi);
参数只有一个BROWSEINFO结构的指针,它的声明也在同一个文件中:
typedef struct _browseinfo
{
HWND hwndOwner;
LPCITEMIDLIST pidlRoot;
LPSTR pszDisplayName;
LPCSTR lpszTitle;
UINT ulFlags;
BFFCALLBACK lpfn;
LPARAM lParam;
int iImage;
} BROWSEINFO, *PBROWSEINFO, *LPBROWSEINFO;
成员的说明如下表:
名称 | 描述 |
hwndOwner | 拥有这个对话框的窗口Handle |
pidlRoot | 被表述层次对象的根节点标识。是一个PIDL。 |
pszDisplayName | 必须是一个已分配缓冲区的指针,它将包含选择对象的显示名。 |
lpszTitle | 必须是一个缓冲区指针,包含一个作为树观察标题的串。 |
ulFlags | 指定外观和窗口行为(后面将介绍有效的值)。 |
Lpfn | 用于钩住对话框的回调函数。 |
lParam | 32位传递给回调函数的客户数据。通常是一个指针或Handle。 |
Iimage | 包含选中文件夹或文件的图标索引。是相对系统图像列表的索引。 |
调用SHBrowseForFolder()最简单的方法是:
BROWSEINFO bi;
ZeroMemory(&bi, sizeof(BROWSEINFO));
bi.hwndOwner = hDlg;
LPITEMIDLIST pidl = SHBrowseForFolder(&bi);
这段代码显示一个前面看到过的对话框,并且恢复选中文件夹的PIDL。如果文件夹有一个对应的路径,你可以通过下述代码获得:
TCHAR szPath[MAX_PATH] = {0};
SHGetPathFromIDList(pidl, szPath);
Msg(szPath);
有几个有趣的结果与使用SHBrowseForFolder()函数有关。下面给出概述,在整个下一节我们都将详细地讨论这些问题:
这个函数透明地处理PIDLs和路径名
这个函数允许浏览特殊的系统文件夹
这个函数返回大量的信息,是SHGetFileInfo()所不能的。
对话框稍微可以客户化,这总是一个好消息。
SHBrowseForFolder()函数的用法
SHBrowseForFolder()函数所能做的事情由BROWSEINFO结构的ulFlags成员所限制,其合法的值由下述标志的组合构成:
标志 | 描述 |
BIF_RETURNONLYFSDIRS | 如果设置,仅在用户选择了文件系统的目录后,OK按钮才被允许。例如,你选择‘网上邻居’节点,如果这个标志设置,OK按钮是灰的。 |
BIF_DONTGOBELOWDOMAIN | 不显示网络文件夹,仅有域名节点。 |
BIF_STATUSTEXT | 对话框模版含有可以显示任何文字的标签,特别是在子类化这个对话框窗口后(后面将详细讲解)。 |
BIF_EDITBOX | 这是Shell 4.71版以后的新特征,它允许一个编辑框,在这里可以手动输入文件夹。 |
BIF_VALIDATE | 这是Shell 4.71版的另一个新特征,它是对BIF_EDITBOX标志功能的补充。如果你设置了这个标志,并且子类化了这个对话框,则用户每次在编辑框键入和确认一个不正确的文件或文件夹名时,你都能收到通知(后面将详细讲解)。 |
BIF_BROWSEFORCOMPUTER | 允许用户仅选择计算机名。浏览正常发生,但是OK按钮总是灰的,除非选择了计算机名。 |
BIF_BROWSEFORPRINTER | 与上相同,但是是打印机。 |
BIF_BROWSEINCLUDEFILES | 如果这个标志设置,不管其它标志如何,在树观察中都显示文件名,而不仅仅是文件夹名。这就提供了设置对话框显示系统中所有打印机或可用字体的机会。 |
在调用SHBrowseForFolder()函数时,有两种方法来客户化最终对话框的外观,经由回调函数子类化这个窗口更有力一些,我们将在这一章的以后部分讨论这个内容。获得有限程度客户化的较简单方法是修改树观察上面的文字。BROWSEINFO结构的lpszTitle成员负责这一点。它声明为一个指针,所以,你必须传递一个有效的内存缓冲区:
TCHAR szBuf[MAX_PATH] = {0};
lstrcpy(szBuf, __TEXT("Choose a folder:"));
bi.lpszTitle = static_cast<LPCSTR>(szBuf);
对pszDisplayName成员,也一样,它是一个返回缓冲区。如果你对选中文件夹的显示名感兴趣,就需要传递一个有效的缓冲区,首先声明或分配它,然后 把指针赋值到pszDisplayName。
TCHAR szDisp[MAX_PATH] = {0};
bi.pszDisplayName = static_cast<LPSTR>(szDisp);
函数认为pszDisplayName至少有MAX_PATH字节尺寸。
正如前几章说明的,文件夹的显示名是探测器用来显示文件夹的名字。例如,(C:)的显示名是C:/。
函数返回了什么
技术上讲,函数返回的是PIDL,它标识一个选中的文件或文件夹。如果‘取消’了对话框,函数返回NULL,非常简单。然而,这个函数还能够通过传递的BROWSEINFO结构返回其它有用的信息。这一点的特殊例子是包含选中对象的显示名(上面已经提到了),和代表它的图标。
获取文件夹的图标
即使SHBrowseForFolder()看起来似乎正在重复我们已经从SHGetFileInfo()函数获得的功能。然而,就获得和显示图标仍然有相当的工作需要做。
在函数返回时,BROWSEINFO结构的iImage成员含有一个数字,它是图标在系统图像列表中的位置索引。因而,如果想要绘制图标—或更简单,想要它的HICON Handle—你就必须首先获得这个图像列表的Handle。
在前一章中已经讲到了怎样取得图标,但是,采用这里的方法要容易一些。如果使用SHGFI_ICON和标志调用SHGetFileInfo(),并且设置了SHGFI_SYSICONINDEX,函数则返回系统图像列表的Handle。
HICON SHGetSystemIcon(int iIconIndex)
{
SHFILEINFO sfi;
ZeroMemory(&sfi, sizeof(SHFILEINFO));
// 不指定文件名,因为我们只想要一个Handle...
HIMAGELIST himl = reinterpret_cast<HIMAGELIST>(SHGetFileInfo(
"*.*", 0, &sfi, sizeof(SHFILEINFO), SHGFI_ICON | SHGFI_SYSICONINDEX));
HICON hIcon = ImageList_ExtractIcon(0, himl, iIconIndex);
return hIcon;
}
上面的代码是一个辅助例程,给定一个索引,返回系统图像列表中对应的图标。要运行这段代码需要包含shellapi.h,和通过调用InitCommonControls()或InitCommonControlsEx()初始化公共控件库。就象附录A中讨论的那样,第一个方法适用于老版本的Shell,第二才被推荐到Shell 4.71及其以后的版本。
使用回调函数
有趣的是SHBrowseForFolder()函数要求一个回调函数。要子类化由这个函数建立的对话框,你需要指派一个有效的函数指针到BROWSEINFO的lpfn字段。这个指针必须指向有如下原型的函数:
int CALLBACK BrowseCallbackProc(HWND hwnd,
UINT uMsg,
LPARAM lParam,
LPARAM dwData);
其中hwnd是被钩住窗口的Handle,uMsg接收到的消息。lParam是一个值,根据uMsg它有不同的意义,而最后这个dwData是用户定义的数据—与你通过BROWSEINFO的lParam成员指定的数据相同。如果你需要回调函数在调用程序建立的数据上工作,而不是使用全程变量,应该使用一个32位值来填写BROWSEINFO结构的lParam成员,并且保证它自动地经由dwData变量传递给回调函数。为了适合多个数据的传递,可以使用指针,更好地,分配一个Handle内存块,锁定它,封包所有东西,解锁,然后把它存入lParam字段。下图显示了回调函数设置的情形:SHBrowseForFolder()调用了你所定义的函数,传送一些数据和通知某些事件。
可以感知的事件
由SHBrowseForFolder()建立的对话框可以通知回调函数下列事件:
对话框初始化完成
选择已经改变
用户在编辑框中键入了无效文件或文件夹。
通过发送下面消息完成这些功能:
BFFM_INITIALIZED
BFFM_SELCHANGED
BFFM_VALIDATEFAILED
这些消息由回调函数通过uMsg参数接收。每一个消息都在lParam变量带有一个LPARAM类型的值。现在就让我们逐个消息地观看一下lParam是怎样配置的:
消息 | lParam 意义 |
BFFM_INITIALIZED | 无用消息—它总是NULL,这个消息在对话框窗口过程完成WM_INITDIALOG后发送。 |
BFFM_SELCHANGED | 指向新选中文件夹的标识符列表,注意,就象其他Windows控件一样,在选择已经改变时通知改变事件。 |
BFFM_VALIDATEFAILED | 指向编辑框的当前内容。就象它标志的那样,这个消息仅仅在Shell 4.71中支持。由返回0,回调函数可以强迫浏览对话框关闭,返回1,对话框保持活动。 |
可以发送的消息
有几个回调函数可以发送给对话框窗口的消息,使它能执行一定的活动。它们是:
消息 | 描述 |
BFFM_ENABLEOK | 根据lParam值,允许或禁止OK按钮。如果非零,按钮允许,此时可以确认当前选中的文件夹。wParam 没有用。 |
BFFM_SETSELECTION | 选择特殊文件或文件夹。lParam中存储一个指向PIDL或路径名的指针,wParam表示怎样解释这个指针。FALSE说明是一个PIDL,TRUE说明路径名。 |
BFFM_SETSTATUSTEXT | 设置你提供的文字到对话框的状态区域。实际文字由lParam指向,WParam无用。 |
这些消息正常地使用SendMessage()函数发送,通过组合它们,你可以实际地增强SHBrowseForFolder()的行为。
客户化用户界面
通过使用回调函数,你可以介入和改变对话框的用户界面,因而,使它可以更好地适应你的需求。例如,你不希望?(帮助)按钮在标题条上出现,或使某些控件具有更显著的3D外观等。下面内容将说明这些功能是怎样实现的
删除关联帮助按钮
从标题条中删除关联帮助按钮是一个关于窗口显示风格的简单问题:你只需要关闭处使Windows绘制和处理它的指示位即可。在扩展风格的任何窗口设置了WS_EX_CONTEXTHELP位时,这个按钮就显示。
扩展风格首先在Windows3.x中提出,然后在Windows95的SDK版本中得到加强。在正常风格和扩展风格之间的唯一不同在于它们占用的存储区域,而不是概念上的差别。
你需要使用不同于访问窗口风格的代码来访问‘扩展’风格,为了关闭使帮助按钮显示的位,在回调函数响应BFFM_INITIALIZED消息时中所必须这样做:
DWORD dwStyle = GetWindowLong(hwnd, GWL_EXSTYLE);
SetWindowLong(hwnd, GWL_EXSTYLE, dwStyle & ~WS_EX_CONTEXTHELP);
首先,取得当前扩展风格(用GWL_EXSTYLE 代替 GWL_STYLE),然后关闭指定位,最后回存这个风格值。
给状态文字加3D边框
做这个比前一个稍微复杂一点,并且要求多行代码。然而你应该清楚,现在所做的并不能保证你的代码在所有存在的和未来的Windows版本中都能正常工作。它仅仅能工作在你已经成功地测试了这个功能的地方。
现在,我们想要绘制一个带有3D边框的状态标签,就象状态条那样。BIF_STATUSTEXT标志可能有点误导—它不是象所期望的那样在窗口的底部加一个状态条,而是在树观察上方和标题之间加一个静态标签。这个标签窗口有一个控件ID,我们可以通过Spy++得知:
当你知道了这个控件的ID之后,一旦回调函数进入到对话框代码之中,获得任何子窗口的Handle就象调用下述代码那样容易:
HWND hwndChild = GetDlgItem(hDlg, controlID);
在上图中你可以看到,我们感兴趣的标签有ID号0x3743,所以:
HWND hwndLabel = GetDlgItem(hwnd, 0x3743);
dwStyle = GetWindowLong(hwndLabel, GWL_EXSTYLE);
SetWindowLong(hwndLabel, GWL_EXSTYLE, dwStyle | WS_EX_STATICEDGE);
SetWindowPos(hwndLabel, NULL, 0, 0, 0, 0,
SWP_NOSIZE | SWP_NOMOVE | SWP_DRAWFRAME);
上述代码加了一个细边框到窗口中,外观如下图所示:
注意,为了看到这个变化,你需要强迫窗口重画它的非客户区域,这就是SetWindowPos()函数出现的原因。再有就是,所有这些操作均在回调函数响应BFFM_INITIALIZED消息时完成。
前面我已经警告过,这段代码潜在的风险,现在,在所有Win32 平台上都能很好地工作,但是有一天微软决定改变这个ID时,将会怎样?一个好的方法可能是:
HWND hwndLabel = GetDlgItem(hwnd, 0x3743);
// 检查是否为一可用的窗口
if(IsWindow(hwndLabel))
{
// 现在检查体是否为一个'static'窗口类
TCHAR szClass[MAX_PATH] = {0];
GetClassName(hwndLabel, szClass, MAX_PATH);
if(lstrcmpi(szClass, __TEXT("static")))
return;
}
else
return;
我们对GetDlgItem()函数返回的Handle执行了两个检查,头一个是使用IsWindow()来检查是否为有效的窗口,第二个是验证这个窗口是不是一个标签—一个‘static’类窗口。如果其中有一个检查失败,则退出,以避免非法操作。
改变对话框标题
比加3D边框更有用的是改变对话框窗口的标题—使用新字符串调用SetWindowText(),即,在响应BFFM_INITIALIZED消息时,执行下面代码:
SetWindowText(hwnd, szNewCaption);
移动对话框窗口
另一个在响应BFFM_INITIALIZED消息的初始化中可以做的是定位窗口到指定位置。典型地,可以移动对话框到屏幕中心:
RECT rc;
GetClientRect(hwnd, &rc);
SetWindowPos(hwnd, NULL,
(GetSystemMetrics(SM_CXSCREEN) - (rc.right - rc.left)) / 2,
(GetSystemMetrics(SM_CYSCREEN) - (rc.bottom - rc.top)) / 2,
0, 0, SWP_NOZORDER | SWP_NOSIZE);
变动状态标签
状态标签的典型用途是显示当前选中的文件或文件夹名,就象在前面图中显示的那样。其实现方法就是响应BFFM_SELCHANGED消息:
TCHAR szText[MAX_PATH] = {0};
SHGetPathFromIDList(reinterpret_cast<LPITEMIDLIST>(lParam), szText);
SendMessage(hwnd, BFFM_SETSTATUSTEXT, 0, reinterpret_cast<LPARAM>(szText));
在接收到这个消息的时候,lParam变量指向一个新选中文件夹或文件的PIDL,说明文件或文件夹是存在的,因此可以调用SHGetPathFromIDList()函数获得可显示的路径名,注意,不是所有文件夹映射到物理目录—例如‘我的计算机’,是一个没有实际目录的文件夹。如果使用‘我的计算机’的PIDL调用SHGetPathFromIDList()函数,可能会获得一个NULL字符串。
从SHGetPathFromIDList()中恢复的字符串可以使用BFFM_SETSTATUSTEXT消息发送到状态窗口。
允许手动编辑
自绑定到IE4.0的Shell 4.71版开始,这个对话框的用户界面就加入了一个编辑框,而且不需要借助于回调方式就可以操作。你只要在调用SHBrowseForFolder()函数时简单地设置BIF_EDITBOX标志即可。结果显示如下:
这个编辑框使你能够用输入文件夹的名字来选择它们。当你点击‘OK’时,函数将确认你的输入有效。如果编辑框中包含了一个文件夹的全路径名,或当前选择的文件夹名,则它的内容是正确的,如上图所示。如果BIF_VALIDATE标志设置,并且SHBrowseForFolder()函数发现编辑框的内容是不正确的,则它用BFFM_VALIDATEFAILED消息唤醒回调函数,在编辑框中的字符串经由lParam变量传递给回调函数。经由BROWSEINFO结构的lParam成员传递的任何用户数据,作为回调函时的第四个参数传递。因此,如果需要通过输入选择文件夹,绝对必须输入全路径名。
下面列出的代码例子说明我们目前所看到的全部情况,后面章节中将给出整个应用的例程。
int CALLBACK BrowseCallbackProc(
HWND hwnd, UINT uMsg, LPARAM lParam, LPARAM dwData)
{
switch(uMsg)
{
case BFFM_INITIALIZED:
{
// 删除标题中的?
DWORD dwStyle = GetWindowLong(hwnd, GWL_EXSTYLE);
SetWindowLong(hwnd, GWL_EXSTYLE, dwStyle & ~WS_EX_CONTEXTHELP);
// 给状态文字加3D边框
HWND hwndLabel = GetDlgItem(hwnd, 0x3743);
// 检查是否为有效的窗口
if(IsWindow(hwndLabel))
{
// 检查是否为'static'类窗口
TCHAR szClass[MAX_PATH] = {0};
GetClassName(hwndLabel, szClass, MAX_PATH);
if(lstrcmpi(szClass, __TEXT("static")))
break;
}
else
break;
dwStyle = GetWindowLong(hwndLabel, GWL_EXSTYLE);
SetWindowLong(hwndLabel, GWL_EXSTYLE, dwStyle | WS_EX_STATICEDGE);
SetWindowPos(hwndLabel, NULL, 0, 0, 0, 0,
SWP_NOSIZE | SWP_NOMOVE | SWP_DRAWFRAME);
}
break;
case BFFM_SELCHANGED:
{
TCHAR szText[MAX_PATH] = {0};
SHGetPathFromIDList(reinterpret_cast<LPITEMIDLIST>(lParam),
szText);
SendMessage(hwnd, BFFM_SETSTATUSTEXT, 0,
reinterpret_cast<LPARAM>(szText));
}
break;
case BFFM_VALIDATEFAILED:
Msg("/"%s/" is a wrong path name.",
reinterpret_cast<LPTSTR>(lParam));
return 1;
}
return 0;
}
指定初始文件夹
SHBrowseForFolder()函数设计时的一个缺陷是没有一个方便的方法来指定开始浏览的初始目录。你可以指定显示层的根,就是这样,如果想要使用目录而不是文件夹,也不简单。为了在代码中设置初始选中的文件夹,我们必须借助于回调函数,特别是,必须探索BFFM_SETSELECTION消息,和请求函数移动焦点到一个特殊的文件夹。实现的最好方式是在响应BFFM_INITIALIZED通知消息的地方,下面就是实现的代码:
int CALLBACK BrowseCallbackProc(
HWND hwnd, UINT uMsg, LPARAM lParam, LPARAM dwData)
{
switch(uMsg)
{
case BFFM_INITIALIZED:
{
...
SendMessage(hwnd, BFFM_SETSELECTION, TRUE, dwData);
}
break;
...
}
return 0;
}
BFFM_SETSELECTION消息需要知道lParam变量是一个PIDL还是一个路径名,在上面代码中因为设置(第三个参数)为TRUE,所以dwData指向一个路径名,若dwData指向PIDL,则第三个参数应该设置为FALSE。
指定根节点
前面已经暗示,SHBrowseForFolder()函数允许指定桌面层上的哪一个节点作为根节点。换句话说,你可以选择想要浏览的探测器观察子树。能这样做的参数就是BROWSEINFO结构的pidlRoot成员。如果这个成员设置为NULL,树观察以桌面作为根。下面图中显示浏览对话框以‘打印机’作为根节点,并且设置了BIF_BROWSEINCLUDEFILES标志:
附带地,这个例子还说明,BIF_BROWSEINCLUDEFILES的作用。代码如下:
LPITEMIDLIST pidl = NULL;
BROWSEINFO bi;
ZeroMemory(&bi, sizeof(BROWSEINFO));
bi.lpszTitle = __TEXT("Choose a printer:");
SHGetSpecialFolderLocation(NULL, CSIDL_PRINTERS, &pidl);
bi.pidlRoot = pidl;
bi.ulFlags = BIF_BROWSEINCLUDEFILES;
SHBrowseForFolder(&bi);
如果核对一下BROWSEINFO结构的声明,你将看到,pidlRoot成员应该有LPCITEMIDLIST类型—即,PIDL。在上面代码段中,通过在函数SHGetSpecialFolderLocation()第二个参数中传递CSIDL_PRINTERS,我们已经取得了这个值,这一点将在后面继续讨论。现在,概括地讲,你可以指定显示树的根节点,但是需要提供一个PIDL。
使用目录作为根
如果我们的目标正好就是浏览某个系统文件夹,如‘打印机’或‘字体’或‘Favorites’,没有问题—使用上面的代码段,只需把CSIDL_PRINTERS换成想要的文件夹ID即可。如果想要把普通的目录作为树观察的根,事情就有点严重了。
对于特殊文件夹ID,翻看SHGetSpecialFolderLocation()函数的资料或研究shlobj.h源码就可以找到某些资料没有说明的IDs。
转换路径名到PIDL
没有别的办法,你只有转换路径名到PIDL。现在,你可能希望有一个Shell的API函数做这个转换,不幸的是没有这样的函数。然而,有一种方法能够转换路径名到PIDL,这需要两个步骤:
获得指向IShellFolder接口的指针
调用它的ParseDisplayName()的方法
ParseDisplayName()方法确实能实现你所要求的转换:它接受路径名,然后把它转换成PIDL,问题是我们怎样才能获得指向IShellFolder接口的指针。在你写一个命名空间扩展的时候,IShellFolder接口是一个需要实现的接口,并且,探测器也使用这个接口一起工作,请求绘制和枚举其内容。一个指向IShellFolder接口的指针由SHGetDesktopFolder()函数返回—精确地讲,它返回一个桌面文件夹的IShellFolder接口。就我们考虑的情况而言,我们只是需要一个提供实际实现ParseDisplayName()功能的对象指针,因而SHGetDesktopFolder()函数返回的是可用的。下面是一个新Shell函数的代码,它接受路径名和返回对应的PIDL,以微软命名习惯,我们称之为SHPathToPidl():
HRESULT SHPathToPidl(LPCTSTR szPath, LPITEMIDLIST* ppidl)
{
LPSHELLFOLDER pShellFolder = NULL;
OLECHAR wszPath[MAX_PATH] = {0};
ULONG nCharsParsed = 0;
// 获取IShellFolder接口指针
HRESULT hr = SHGetDesktopFolder(&pShellFolder);
if(FAILED(hr))
return hr;
// 转换路径名到Unicode
MultiByteToWideChar(CP_ACP, MB_PRECOMPOSED, szPath, -1, wszPath, MAX_PATH);
// 调用ParseDisplayName()函数
hr = pShellFolder->ParseDisplayName(
NULL, NULL, wszPath, &nCharsParsed, ppidl, NULL);
// 清理
pShellFolder->Release();
return hr;
}
ParseDisplayName()函数的原形如下:
HRESULT ParseDisplayName(HWND hwndOwner,
LPBC pbcReserved,
LPOLESTR lpszDisplayName,
ULONG* pchEaten,
LPITEMIDLIST* ppidl,
ULONG* pdwAttributes);
第一个变量是用作函数可能需要显示消息框的父窗口的Handle。第二个,pbcReserved没有使用,必须设置为NULL。头一个有意义的变量是lpszDisplayName,表示一个要求转换的Unicode格式的名字串,pchEaten是一个包含实际传递字符数的缓冲,而pdwAttributes(如果不空NULL),则包含由lpszDisplayName所指定文件夹的属性,这些属性都是SHGAO_前缀常量属性。如果不注意,可能会传递NULL属性。最后是ppidl变量,新生成的PIDL的返回缓冲。一旦对指定的路径成功地生成了PIDL,你就能够限制用户的浏览到指定的子树,而不能继续向上,例如:
这个图显示C:/Program Files作为根文件夹的情形。
总结例程
到目前为止,我们孤立地讨论了SHBrowseForFolder()函数,并且总是给出解决特殊问题的代码段,现在,我们构造一个完整的应用来总结上面所有我们看到的特性。
图中的对话框是测试SHBrowseForFolder()函数特征所使用过的,称之为SHBrowse。通过选择路径名(文件夹编辑框)或PIDL(PIDL列表框)指定根文件夹—使用PIDL复选框确定是哪一种路径。可以在标题编辑框中设置对话框标题,以及几个与SHBrowseForFolder()函数标志大致匹配的复选框,结果显示在下面的区域,显示名,路径名和文件夹图标。增加的第一段代码在OnInitDialog()函数中,以便使用特殊文件夹的名字设置PIDL下拉列表框。
void OnInitDialog(HWND hDlg)
{
// 设置图标(T/F 大/小图标)
SendMessage(hDlg, WM_SETICON, FALSE, reinterpret_cast<LPARAM>(g_hIconSmall));
SendMessage(hDlg, WM_SETICON, TRUE, reinterpret_cast<LPARAM>(g_hIconLarge));
// 填写下拉列表框
HWND hwndCbo = GetDlgItem(hDlg, IDC_SPECIAL);
int i = ComboBox_AddString(hwndCbo, "Control Panel");
ComboBox_SetItemData(hwndCbo, i, CSIDL_CONTROLS);
i = ComboBox_AddString(hwndCbo, "Favorites");
ComboBox_SetItemData(hwndCbo, i, CSIDL_FAVORITES);
i = ComboBox_AddString(hwndCbo, "Printers");
ComboBox_SetItemData(hwndCbo, i, CSIDL_PRINTERS);
i = ComboBox_AddString(hwndCbo, "Fonts");
ComboBox_SetItemData(hwndCbo, i, CSIDL_FONTS);
i = ComboBox_AddString(hwndCbo, "SendTo");
ComboBox_SetItemData(hwndCbo, i, CSIDL_SENDTO);
ComboBox_SetCurSel(hwndCbo, 0);
}
此时你可以从这个列表中选择‘发送到’(SendTo)文件夹,这依赖于你的其它选择设置。如图:
当然,这个对话框在你自己的计算机上可能稍有不同,这是因为‘SendTo’目录可能有不同的快捷方式。在此选择‘OutLook Express’,结果如下:
整个项目是可用的,代码包含了BrowseCallbackProc(),SHGetSystemIcon()和SHPathToPidl()函数。编译之前一定要记住#include shlobj.h 和resource.h。这些函数在点击OK按钮时被执行。
void OnOK(HWND hDlg)
{
BROWSEINFO bi;
TCHAR szTitle[MAX_PATH] = {0};
TCHAR szPath[MAX_PATH] = {0};
TCHAR szDisplay[MAX_PATH] = {0};
LPITEMIDLIST pidl = NULL;
LPMALLOC pMalloc = NULL;
// 准备
ZeroMemory(&bi, sizeof(BROWSEINFO));
bi.hwndOwner = hDlg;
// 标题和显示名
GetDlgItemText(hDlg, IDC_TITLE, szTitle, MAX_PATH);
bi.lpszTitle = szTitle;
bi.pszDisplayName = szDisplay;
//初始目录
if(IsDlgButtonChecked(hDlg, IDC_USEPIDL))
{
HWND hwndCbo = GetDlgItem(hDlg, IDC_SPECIAL);
int i = ComboBox_GetCurSel(hwndCbo);
int nFolder = ComboBox_GetItemData(hwndCbo, i);
SHGetSpecialFolderLocation(NULL, nFolder, &pidl);
bi.pidlRoot = pidl;
}else{
// 转换路径名到 PIDL
GetDlgItemText(hDlg, IDC_FOLDER, szPath, MAX_PATH);
if(lstrlen(szPath) == 0)
GetCurrentDirectory(MAX_PATH, szPath);
SHPathToPidl(szPath, &pidl);
bi.pidlRoot = pidl;
}
// 采集标志
UINT uiFlags = 0;
if(IsDlgButtonChecked(hDlg, IDC_NOBELOW))
uiFlags |= BIF_DONTGOBELOWDOMAIN;
if(IsDlgButtonChecked(hDlg, IDC_ONLYDIRS))
uiFlags |= BIF_RETURNONLYFSDIRS;
if(IsDlgButtonChecked(hDlg, IDC_INCLUDEFILES))
uiFlags |= BIF_BROWSEINCLUDEFILES;
if(IsDlgButtonChecked(hDlg, IDC_EDITBOX))
uiFlags |= BIF_EDITBOX | BIF_VALIDATE;
if(IsDlgButtonChecked(hDlg, IDC_STATUS))
uiFlags |= BIF_STATUSTEXT;
if(IsDlgButtonChecked(hDlg, IDC_COMPUTER))
uiFlags |= BIF_BROWSEFORCOMPUTER;
bi.ulFlags = uiFlags;
// 设置回调
bi.lpfn = BrowseCallbackProc;
bi.lParam = 0;
// 调用函数
LPITEMIDLIST pidlFolder = SHBrowseForFolder(&bi);
if(pidlFolder == NULL)
return;
// 显示结果...
// 显示显示名
SetDlgItemText(hDlg, IDC_DISPLAYNAME, bi.pszDisplayName);
// 显示路径名
SHGetPathFromIDList(pidlFolder, szPath);
SetDlgItemText(hDlg, IDC_PATHNAME, szPath);
// 显示文件夹图标
HICON hIcon = SHGetSystemIcon(bi.iImage);
SendDlgItemMessage(
hDlg, IDI_ICON, STM_SETICON, reinterpret_cast<WPARAM>(hIcon), 0);
// 释放
SHGetMalloc(&pMalloc);
pMalloc->Free(pidl);
pMalloc->Free(pidlFolder);
pMalloc->Release();
}
上面函数的工作方式对你来讲是很显然的—除了,最后面的一段。为了进一步解释它,我们需要更深地了解关于PIDL的知识。
有点变态的PIDL
在第二章中我们检测了PIDLs,在这里,我们得到了它的特殊应用:使用PIDL浏览文件夹的内容,无论这个内容是什么。每一个Windows Shell 的元素都有它自己的PIDL并且包含在某种文件夹中,因而,对于每一个元素,都有一段代码来处理文件夹和依据文件夹自有的规则和需求提供PIDL。也就是说,我们从不能设定PIDL结构或它所组合成的数据,而是必须使用通用接口来处理它。例如,如果希望探索SHITEMID结构链,你就应该在每一步检查下一个块的长度。就像你已经看到过的那样,一个ITEMIDLIST—或PIDL—是由一个或多个连续分配的SHITEMID组成的,这个链在cb字段为0的元素上终止。下面是从MSDN上摘录的函数,它可以说明怎样遍历这个列表的项。这与普通的列表操作没有太大的差别。
LPITEMIDLIST GetNextItemID(LPITEMIDLIST pidl)
{
// 取得制定项的尺寸
int cb = pidl->mkid.cb;
// 尺寸为零则列表终止
if(cb == 0)
return NULL;
// 加cb到pidl (生成字节增量)
pidl = (LPITEMIDLIST)(((LPBYTE)pidl) + cb);
// 如果NULL终止则返回NULL,否则返回一个pidl
return (pidl->mkid.cb == 0) ? NULL : pidl;
}
你不可以设定PIDL的格式,对于一个文件夹,一种方法可能工作的很好,对于另一个可能会失败。例如,为了保证两个项是相同的,你必须通过IShellFolder::CompareIDs()方法请求文件夹自己来比较它们。
释放PIDL
在进一步讨论之前,有必要解释一下上面例子中的最后一段代码。在文件夹建立PIDLs时,通常必须由其它模块销毁,这就是我们在OnOK()函数最后所做的。标识符列表的内存从Shell应用的分配器上取得,向第二章中所见,我们可以调用SHGetMalloc()函数获得指向分配器的指针。一般,调用顺序如下:
LPMALLOC pMalloc;
SHGetMalloc(&pMalloc); // 取得IMalloc 接口
pMalloc->Free(pidl); // 释放标识符列表
pMalloc->Release(); // 释放IMalloc 接口
怎样使用PIDL
回到我们的题目当中,使PIDLs有某些实际的用途,这里有两个主要的目标,头一个,我们想要能够枚举任何文件夹的内容,第二个是希望重复探测器在Shell在4.71和更高版本中所支持的特征。为了展示这个想法,下面是探测器在地址栏输入‘打印机’后,键入‘回车’截图:
探测器允许使用‘打印机’作为通常意义的文件夹名。换言之,它模糊了物理文件夹和虚拟文件夹的差异,精确地讲,‘打印机’是一个内容为有效打印机设备的虚拟文件夹的显示名。在这个例子中我们建立一个示范程序称之为Pidl,其用户界面如下:
‘搜索路径’按钮接受编辑框的内容并努力查找具有这个名字的文件夹。编辑框中的串就是一个文件夹的显示名(记住,路径名也是显示名)。如果成功,应用将在列表观察中显示所有在这个文件夹中找到的文件对象。另一方面,‘显示PIDL内容’按钮将在列表观察中枚举特殊文件夹中找到的所有文件对象,特殊文件夹在下拉列表中选择。
通过显示名搜索
让我们从点击‘搜索路径’按钮所执行的代码开始,当然这两个新按钮都需要APP_DlgProc()过程来处理,所以在这里添加代码如下:
case WM_COMMAND:
switch(wParam)
{
case IDC_SEARCHPATH:
DoSearchPath(hDlg);
return FALSE;
case IDC_PIDLCONTENT:
DoEnumeratePidl(hDlg);
return FALSE;
case IDCANCEL:
EndDialog(hDlg, FALSE);
return FALSE;
}
break;
这里所涉及到的第一个函数是DoSearchPath(),它从‘文件夹名’编辑框中读出你键入的名字,把它作为搜索路径名,如果它确实是一个路径名,所有操作都正常进行,但是如果它是一个文件夹的显示名,将会有什么事情发生呢?我们希望函数能够处理如C:/和(C:)等的串,这个实现将能够正确处理所有路径名和与桌面关联的子文件夹的显示名,或‘我的计算机’等虚拟文件夹。
注意,正常地,驱动器的显示名由括弧中驱动器字符加标号给出,例如:Ms-Dos_6(C:)。如果没有标号,则认为有一个前导空格在(C:)中。
DoSearchPath()一开始就枚举桌面文件夹的内容:
void DoSearchPath(HWND hDlg)
{
LPITEMIDLIST pidl = NULL;
LPSHELLFOLDER pFolder = NULL;
LPSHELLFOLDER pSubFolder = NULL;
// 取得内存分配器
LPMALLOC pMalloc = NULL;
SHGetMalloc(&pMalloc);
// 取得搜索名
TCHAR szName[MAX_PATH] = {0};
GetDlgItemText(hDlg, IDC_FOLDER, szName, MAX_PATH);
// 取得卓面的IShellFolder接口
SHGetDesktopFolder(&pFolder);
// 试图在桌面上找到一个匹配
int iNumOfItems = SHEnumFolderContent(pFolder, NULL, 0, NULL);
int rc = SHEnumFolderContent(
pFolder, SearchText, reinterpret_cast<DWORD>(szName), &pidl);
SHEnumFolderContent()是用户定义的函数,他接受一个文件夹的PIDL和一个回调函数作为输入,然后枚举这个文件夹的所有项,并传递这些项到这个函数作进一步的处理。后面我们将进一步讨论它。为了理解它在这里的用途,你只需要知道,如果没有指定回调函数,它返回所找到的项目数:
int iNumOfItems = SHEnumFolderContent(pFolder, NULL, 0, NULL);
否则,它返回实际处理的项目数。这两个值必然是不同的,因为回调函数可以在本身选中的点上停止枚举。例如SearchText()函数,当它找到了它正在寻找的名字时,SHEnumFolderContent()停止了。
SHEnumFolderContent()函数开始搜索,检查我们在编辑框中键入的名字是否对应桌面下一个文件夹的显示名。这就是上面代码终止后,rc 和 iNumOfItems不等的情况。如果它们相等,我们就在‘我的计算机’节点上开始一个新的搜索:
// 如果没找到,在‘我的计算机’上再试
if(rc == iNumOfItems)
{
// 绑定到‘我的计算机’
LPITEMIDLIST pidlMyComp;
SHGetSpecialFolderLocation(NULL, CSIDL_DRIVES, &pidlMyComp);
pFolder->BindToObject(pidlMyComp, NULL, IID_IShellFolder,
reinterpret_cast<LPVOID*>(&pSubFolder));
//释放桌面文件夹指针
pFolder->Release();
pMalloc->Free(pidlMyComp);
pFolder = pSubFolder;
//扫描‘我的计算机’
iNumOfItems = SHEnumFolderContent(pFolder, NULL, 0, NULL);
rc = SHEnumFolderContent(
pFolder, SearchText, reinterpret_cast<DWORD>(szName), &pidl);
在重复调用SHEnumFolderContent()函数工作于‘我的计算机’文件夹上之前,我们需要为之获得IShellFolder接口指针,在这一点上我们有的只是桌面的IShellFolder接口,然而,我们可以通过接口的BindToObject()方法获得想要的接口。这个操作使你能绑定到子文件夹的IShellFolder接口,因此,你可以同样使用这个方法作用于PIDL。
HRESULT IShellFolder::BindToObject(
LPCITEMIDLIST pidl, // 我们想要的文件夹的PIDL
LPBC pbcReserved, // 保留,必须为空NULL
REFIID riid, // 必须是IID_IShellFolder
LPVOID* ppvOut // 接收IShellFolder 的指针
);
如果既没在桌面上也没在‘我的计算机’上找到指定的显示名,则我们所取得的是一个快捷方式,而且我们确实不能确定它的位置。尽管如此,也不要认为这是一个系统限制—在文件夹上使用递归搜索来确定名字的位置是很有可能的。这个方法略述如下:
枚举桌面文件夹的内容,就象上面所做的。
对每一个找到的文件夹(不仅是‘我的计算机’)重复这个搜索过程
然而,完全递归的搜索可能导致试图通过名字查找一个不唯一的文件夹—这是很有可能的。当然可能有两个文件夹具有相同的显示名MyDir,一个在c:/,另一个在d:/,上面的算法将总是停止在头一个文件夹出现的地方。
一个较好的方法是接受和分析全质量文件夹名,比如:
My Computer/ (c:)/Windows
Control Panel/Add New Hardware
这样做仅仅需要很少的额外代码来分析文件夹名,并且从搜索桌面上的第一个项开始,前一步完成到达文件夹的下一个项,等等。上面所看到的代码可以稍微加推广,和封装在一个循环中。
回想一下,这确实与在文件系统中搜索没有什么不同,正好就是使用FindFirstFile()和 FindNextFile()来枚举目录的内容,只是使用了由文件夹对象的COM接口暴露的方法而已。
在完成代码之前,注意,键入的显示名是一个完整的路径名的情况,如c:/。在输出一个消息框之前处理这种情况是有价值的—正象我们需要转换这个名字为PIDL格式一样,看一下什么情况发生了,如果没有错误,则一个路径名被接受了。
if(rc == iNumOfItems)
{
// 做最后的努力,它是一个路径名
HRESULT hr = SHPathToPidlEx(szName, &pidl, pFolder);
if(FAILED(hr))
{
Msg("/"%s/" not found under Desktop or My Computer.", szName);
pMalloc->Free(pidl);
pFolder->Release();
// 调用辅助函数刷新UI
ClearUI(hDlg);
return;
}
}
}
最后,如果函数在这一点之前没有返回,我们知道,已经有了一个可用于输出文件夹图标的PIDL,也就是说,在编辑框中有一个输入串作为上面源码中的szName被引用。我们就用那个名字标识文件夹对象并获得它的PIDL。现在,要枚举这个文件夹的内容,我们需要获得它的IShellFolder接口和把它传递给SHEnumFolderContent()函数。
因而,‘搜索路径’按钮处理程序的结尾代码有如下形式:
// 如果到达这里,则:
// pidl 指向我们需要绑定的文件夹以枚举它的内容
// pFolder 指向pidl父文件夹的IShellFolder
// 绑定到我们正在搜索的子文件夹
// pFolder 可以指向桌面的或‘我的计算机’的IShellFolder
pFolder->BindToObject(pidl, NULL, IID_IShellFolder,
reinterpret_cast<LPVOID*>(&pSubFolder));
// 刷新UI (清空列表观察和图像列表等)
ClearUI(hDlg);
// 枚举文件夹内容到列表观察
HWND hwndListView = GetDlgItem(hDlg, IDC_LISTVIEW);
SHEnumFolderContent(pSubFolder, ShowFolderContent,
reinterpret_cast<DWORD>(hwndListView), NULL);
// 清理
pFolder->Release();
pSubFolder->Release();
pMalloc->Free(pidl);
pMalloc->Release();
return;
}
转换路径名到PIDLs(续)
查看上面的代码,你可能已经注意到,我使用了SHPathToPidlEx()函数来转换路径名到PIDL。在这一章的开始,我们开发了有同样目的的SHPathToPidl()辅助函数—它使用IShellFolder接口的ParseDisplayName()方法。SHPathToPidl()函数的代码浓缩到这一点上,它取得相对于桌面的PIDL—即,层次的根是
SHGetDesktopFolder(&pFolder);
pFolder->ParseDisplayName(NULL, NULL, wszPath, &n, ppidl, NULL);
不幸的是这个PIDL是相对于提供IShellFolder接口文件夹的,是桌面。新情况是我们需要相对于操作文件夹之父文件夹的PIDL。理由是,当我们使用BindToObject()方法来获得一个子文件夹的IShellFolder时,要求传递一个PIDL,它是与我们调用BindToObject()位于相同位置文件夹的PIDL。
此后,我们还需要在获得IShellFolder接口指针和ParseDisplayName()函数之间添加几步,这几步是要保证用于ParseDisplayName()调用的IShellFolder接口确实是我们想要的文件夹的接口。
代码如下:
HRESULT SHPathToPidlEx(
LPCTSTR szPath, LPITEMIDLIST* ppidl, LPSHELLFOLDER pFolder)
{
OLECHAR wszPath[MAX_PATH] = {0};
ULONG nCharsParsed = 0;
LPSHELLFOLDER pShellFolder = NULL;
BOOL bFreeOnExit = FALSE;
MultiByteToWideChar(CP_ACP, MB_PRECOMPOSED, szPath, -1, wszPath, MAX_PATH);
// 默认使用桌面的IShellFolder
if(pFolder == NULL)
{
SHGetDesktopFolder(&pShellFolder);
bFreeOnExit = TRUE;
}
else
pShellFolder = pFolder;
HRESULT hr = pShellFolder->ParseDisplayName(
NULL, NULL, wszPath, &nCharsParsed, ppidl, NULL);
if(bFreeOnExit)
pShellFolder->Release();
return hr;
}
这个函数比SHPathToPidl()更一般,而且它也要求传递PIDL相对的文件夹。如果传递一个NULL,而不是一个IShellFolder指针,则桌面的IShellFolder接口被使用,然后被释放。在这个例子中调用转换函数的代码是:
HRESULT hr = SHPathToPidlEx(szName, &pidl, pFolder);
试着传递一个NULL而不是确定的路径名给pFolder,进行搜索,将会看到:无论如何,你将总是枚举‘桌面’文件夹的内容。
清除用户界面
作为SHEnumFolderContent()细节的一部分,在这个代码中我们所看到的最简单的辅助函数是ClearUI():
void ClearUI(HWND hDlg)
{
HWND hwndListView = GetDlgItem(hDlg, IDC_LISTVIEW);
ListView_DeleteAllItems(hwndListView);
ImageList_RemoveAll(g_himl);
SetDlgItemText(hDlg, IDC_FOUND, __TEXT("0 item(s) found."));
}
这正是重置应用的对话框,从观察列表中删除所有项,以及清空由SHEnumFolderContent()函数建立的图像列表,最后一项任务由HIMAGELIST类型的全称变量g_himl完成,它在WinMain()中被初始化为0。
构建枚举器函数
关于枚举给定文件夹内容这方面仍然有大量的问题需要考虑,比如,它是一个物理目录还是一个如‘打印机’的虚拟文件夹等。我们所看到的源码给出一个重要的函数SHEnumFolderContent(),这是一个负责查询文件夹和逐个枚举其内容的函数。
有些文件夹其内容是文件的集合,还有些文件夹其可见内容可以是单个文件的记录或某种硬件设备。一般而言,仅仅是文件夹才确切地知道它的内容是什么。对探测器或程序而言,没有任何保险的方法不需要查询文件夹就能枚举它所包含的项。这并不奇怪,因为这种通讯是基于COM接口的。
在程序中,SHEnumFolderContent()询问一个文件夹的内容,并传输它找到的每一个项的名字到另外的函数做进一步的处理。你已经看到了这两个函数:SearchText()和ShowFolderContent()。然而要理解它们的适当作用,首先要研究项目枚举是怎样发生的。
读文件夹的内容
关联于‘搜索路径’(和显示PIDLs内容)按钮的代码,其目的在于读出文件夹的内容。为了允许枚举其中的项,文件夹实现了IEnumIDList接口,这个接口暴露了四个函数:Next(), Skip(), Reset()和 Clone(),使之可以在给定的集合中前后移动。它们的原型为:
HRESULT IEnumIDList::Next(ULONG celt,
LPITEMIDLIST* rgelt,
ULONG* pceltFetched);
头一个变量是请求的项目数,第二个是一个PIDLs数组指针,第三个则是返回的实际拷贝的项目数。IEnumIDList接口本身负责分配保存PIDL数据的内存。
要想了解指定文件夹的内容,需要设计一段代码,要记住是通过获取指向IEnumIDList接口的指针和IShellFolder接口所暴露的方法EnumObjects()实际完成这个操作的。这个方法的原型如下:
HRESULT IShellFolder::EnumObjects(HWND hwndOwner, //一个窗口Handle
DWORD grfFlags, //一个标志集
LPENUMIDLIST* ppenumIDList //接收IEnumIDList的指针
);
这个方法的第二个参数允许你规定枚举项目的类型。它取如下定义的枚举类型组合值:
typedef enum tagSHCONTF
{
SHCONTF_FOLDERS = 32,
SHCONTF_NONFOLDERS = 64,
SHCONTF_INCLUDEHIDDEN = 128,
} SHCONTF;
就如助记名所述的一样:你可以决定枚举文件夹、非文件夹对象,甚至是隐藏对象。
以后在写一个命名空间扩展得时候,详细讨论这些接口是非常必要的。现在,我们建议你花点时间看一下VC++的帮助文件,以澄清这些方法名和原型。
LPENUMIDLIST pEnumIDList = NULL;
LPITEMIDLIST pItem = NULL;
ULONG ulFetched = 0;
pFolder->EnumObjects(
NULL, SHCONTF_FOLDERS | SHCONTF_NONFOLDERS, &pEnumIDList);
while(pEnumIDList->Next(1, &pItem, &ulFetched) == NOERROR)
{
...
}
上面这段代码描述了触发枚举文件夹项函数的过程。每次循环条件成立,pItem指向一个PIDL的单项。获得这个数据后有两件事情需要处理:它的显示名和可能的图标。
取得项的显示名
即使有了PIDL,取得项目的显示名也不是一件容易的事。尽管有IShellFolder::GetDisplayNameOf()函数,仍有一些工作要做,问题在于这个方法不提供正常的ANSI或Unicode格式。相反,它返回的是一个STRRET结构的指针。STRRET结构定义如下:
typedef struct _STRRET
{
UINT uType;
union{
LPWSTR pOleStr;
LPSTR pStr; // Unused
UINT uOffset;
char cStr[MAX_PATH];
} DUMMYUNIONNAME;
} STRRET, *LPSTRRET;
正像你所看到的,这个结构用一个标志表示格式,这个标志说明了其后字符串的类型。串可以是Unicode串(pOleStr),ANSI串(cStr),或甚至是一个串的地址偏移(uOffset)。也就是说不管初始串是什么格式的,你都需要自己写展开例程处理返回串。这里给出一个样例:
void StrretToString(LPITEMIDLIST pidl, LPSTRRET pStr, LPSTR pszBuf)
{
lstrcpy(pszBuf, "");
switch(pStr->uType)
{
case STRRET_WSTR: // Unicode 串
WideCharToMultiByte(
CP_ACP, 0, pStr->pOleStr, -1, pszBuf, MAX_PATH, NULL, NULL);
break;
case STRRET_OFFSET: // 地址偏移
lstrcpy(pszBuf, reinterpret_cast<LPSTR>(pidl) + pStr->uOffset);
break;
case STRRET_CSTR: // ANSI 串
lstrcpy(pszBuf, pStr->cStr);
break;
}
}
StrretToString()函数接收一个PIDL和一个STRRET结构,经由第三个变量返回一个LPSTR。附带地上面代码还显示了uType的合法值。回到我们主要讨论的话题,GetDisplayNameOf()函数的原型是:
HRESULT IShellFolder::GetDisplayNameOf(LPCITEMIDLIST pidl,
DWORD uFlags,
LPSTRRET lpName);
这里uFlags标志来自SHGNO枚举类型:
typedef enum tagSHGDN
{
SHGDN_NORMAL = 0,
SHGDN_INFOLDER = 1,
SHGDN_INCLUDE_NONFILESYS = 0x2000,
SHGDN_FORADDRESSBAR = 0x4000,
SHGDN_FORPARSING = 0x8000,
} SHGNO;
资料中对这些标志的描述是足够清晰的。因此,你可以构建你所期望的具有最终行为的函数。然而,无论我怎样设置标志,所有样例程序都是以同样的方式工作。坦白地说,我也不知道哪儿除了问题。所以我建议总是使用0作为这个参数的值。
STRRET sName;
CHAR szBuf[MAX_PATH] = {0};
pFolder->GetDisplayNameOf(pItem, 0, &sName);
StrretToString(pItem, &sName, szBuf);
这个代码段以可阅读格式显示项目名。再次提醒注意,对于文件型文件夹以及Fonts, Favorites, Printers, Control Panel等特殊文件夹这是正确的。也就是说,我们能够列出所有‘控制面板’中的小程序。
本身,我们没有用到STRRET结构,这要感谢StrretToString()函数提供的帮助,它当然也包含在Shell库函数中。
读取项目的图标
已开始接触Shell编程,以为非常艰巨,然而,当你从初始的头三四个月中挺过来之后,你就开始有机会接触高级项目问题的答案了。要说明这一点,你认为怎样才能取得一个项的图标?回答是,你必须询问提供它的文件夹。IShellFolder::GetUIObjectOf()方法返回所有你可能需要处理用户界面和文件对象的接口。
HRESULT IShellFolder::GetUIObjectOf(
HWND hwndOwner, // 窗口Handle
UINT cidl, // 下一个参数中的元素数
LPCITEMIDLIST* apidl, // 指向一个PIDLs数组的指针
REFIID riid, // 要求的接口ID
UINT* prgfInOut, // 保留(必须为NULL)
LPVOID* ppvOut // 接收接口的指针
);
对这个声明我们感兴趣的是可以请求一定数量的不同接口指针,它们都能影响UI显示。例如,请求IContextMenu,以获得元素的关联菜单HMENU的Handle。在我们的任务中,请求的是IExtractIcon接口,以便查找图标(在第十六章中将看到更多关于GetUIObjectOf()的解释)。
pFolder->GetUIObjectOf(NULL, 1, const_cast<LPCITEMIDLIST*>(&pItem),
IID_IExtractIcon, NULL, reinterpret_cast<LPVOID*>(&pExtractIcon));
IExtractIcon接口有两个新方法:GetIconLocation()和Extract()。头一个使你能知道图标的索引和位置,第二个则返回一个HICON 型的Handle。在客户端调用GetIconLocation()函数时,它返回包含图标的文件名,以及在这个文件的资源中的一个从零开始的图标索引。
HRESULT IExtractIcon::GetIconLocation(UINT uFlags,
LPSTR szIconFile,
INT cchMax,
LPINT piIndex,
UINT* pwFlags);
Extract()函数依次从指定文件中抽取给定的图标,和返回它的HICON。这个方法几乎与API函数ExtractIconEx()是一样的。
HRESULT IExtractIcon::Extract(LPCSTR pszFile,
UINT nIconIndex,
HICON* phiconLarge,
HICON* phiconSmall,
UINT nIconSize);
对这些函数而言资料显得有点冗长,例如,即使你不关心pwFlags的内容,你也需要知道它不能为NULL。类似地,即使你仅仅需要大图标,你仍然必须为小图标传递一个有效的非零HICON。下面测试一个例子,说明怎样调用它:
pExtractIcon->GetIconLocation(0, szIconFile, MAX_PATH, &iIconIndex, &u);
pExtractIcon->Extract(szIconFile, iIconIndex, &hIcon, &hIconSm, MAKELONG(32, 16));
pExtractIcon->Release();
在开发这个实例代码时,我获得了另一个有趣的结果,坦白地讲,这超出了我的想象。在有些情况下Extract()函数返回的Handle是NULL,即使在图标的位置和索引都是正确的情况下,也是如此。奇怪,使用相同的参数调用ExtractIconEx()函数工作完好。当然,工作环境是直接的:
if(hIcon == NULL)
ExtractIconEx(szIconFile, iIconIndex, &hIcon, NULL, 1);
在这一点上,最后要做的工作就是需要建立一个新的Shell函数,它接收IShellFolder指针,在它的项上循环,为每个项唤醒回调函数。就像很多其他称为‘枚举’的函数一样,我们的SHEnumFolderContent()函数将提供一个用户定义的缓冲(dwData)发送程序级的变量到回调函数。进一步,如果回调函数返回FALSE,则函数将停止工作。这里是它的原型:
int SHEnumFolderContent(LPSHELLFOLDER pFolder,
FOLDERCONTENTPROC pfn, DWORD dwData, LPITEMIDLIST* ppidl);
其中FOLDERCONTENTPROC是一个用户定义的函数指针,其声明是:
typedef BOOL (CALLBACK *FOLDERCONTENTPROC)(LPCSTR, HICON, DWORD);
第一个变量是元素显示名,而后是一个图标Handle。然后是用户定义的缓冲。像已经提到的那样,函数返回FALSE来中断枚举,TRUE则继续。
SHEnumFolderContent()的最后一个参数是PIDL。这并不严格地必要,只是有时需要(如在我们的例子中),理解这个最后传递的参数PIDL是有帮助的。如果这个变量是NULL,则它被忽略。下面是SHEnumFolderContent()函数的源代码:
int SHEnumFolderContent(LPSHELLFOLDER pFolder,
FOLDERCONTENTPROC pfn, DWORD dwData, LPITEMIDLIST* ppidl)
{
int iNumOfItems = 0;
// 枚举内容
LPENUMIDLIST pEnumIDList = NULL;
pFolder->EnumObjects(
NULL, SHCONTF_FOLDERS | SHCONTF_NONFOLDERS, &pEnumIDList);
ULONG ulFetched = 0;
LPITEMIDLIST pItem = NULL;
while(NOERROR == pEnumIDList->Next(1, &pItem, &ulFetched))
{
STRRET sName;
TCHAR szBuf[MAX_PATH] = {0};
pFolder->GetDisplayNameOf(pItem, 0, &sName);
StrretToString(pItem, &sName, szBuf);
// 唤醒回调函数
if(pfn)
{
// 获取图标
UINT u = 0;
int iIconIndex = 0;
HICON hIcon = NULL;
HICON hIconSm = NULL;
TCHAR szIconFile[MAX_PATH] = {0};
LPEXTRACTICON pExtractIcon = NULL;
pFolder->GetUIObjectOf(NULL, 1, const_cast<LPCITEMIDLIST*>(&pItem),
IID_IExtractIcon, NULL,
reinterpret_cast<LPVOID*>(&pExtractIcon));
pExtractIcon->GetIconLocation(0, szIconFile, MAX_PATH,
&iIconIndex, &u);
pExtractIcon->Extract(szIconFile, iIconIndex, &hIcon,
&hIconSm, MAKELONG(32, 16));
pExtractIcon->Release();
if(hIcon == NULL)
ExtractIconEx(szIconFile, iIconIndex, &hIcon, NULL, 1);
if(!pfn(szBuf, hIcon, dwData))
{
// 返回当前的PIDL
if(ppidl != NULL)
*ppidl = pItem;
break;
}
}
++iNumOfItems;
}
return iNumOfItems;
}
回调函数
典型地,回调函数用于实现某些项的采集任务。如此,SHEnumFolderContent()调用这样的函数采集各个文件夹项。SearchText()函数仅仅报告是否传递过来的两个串是相等的。
BOOL CALLBACK SearchText(LPCSTR pszItem, HICON hIcon, DWORD dwData)
{
return static_cast<BOOL>(lstrcmpi(pszItem, reinterpret_cast<LPCSTR>(dwData)));
}
ShowFolderContent()函数用于构建一个传递过来图标的图像列表,插入图标到提供的列表观察中:
BOOL CALLBACK ShowFolderContent(LPCSTR pszItem, HICON hIcon, DWORD dwData)
{
//建立图像列表
int iIconWidth = GetSystemMetrics(SM_CXICON);
int iIconHeight = GetSystemMetrics(SM_CYICON);
if(g_himl == NULL)
g_himl = ImageList_Create(iIconWidth, iIconHeight, ILC_MASK, 1, 0);
int iIconPos = ImageList_AddIcon(g_himl, hIcon);
HWND hwndListView = reinterpret_cast<HWND>(dwData);
ListView_SetImageList(hwndListView, g_himl, LVSIL_NORMAL);
LV_ITEM lvi;
ZeroMemory(&lvi, sizeof(LV_ITEM));
lvi.mask = LVIF_TEXT | LVIF_IMAGE;
lvi.pszText = const_cast<LPSTR>(pszItem);
lvi.cchTextMax = lstrlen(pszItem);
lvi.iImage = iIconPos;
ListView_InsertItem(hwndListView, &lvi);
//更新计数
TCHAR s[MAX_PATH] = {0};
wsprintf(s, "%d item(s) found.", ListView_GetItemCount(hwndListView));
SetDlgItemText(GetParent(hwndListView), IDC_FOUND, s);
return TRUE;
}
示例程序
一定要保证,主程序代码中包含#includes shlobj.h 和resource.h,下面图中显示使用这一阶段开发的实例程序可以做的操作。键入‘打印机’,你能充填这个列表观察,就像标准的文件夹窗口一样:
再有,指定一个路径名,可以看到文件和文件夹,正象在探测器中所看到的那样:
回想一下,如果想要得到任何驱动器根目录的内容时,你必须包括一个最后的反斜杠‘/’,例如C:/工作正常,而C: 则产生这个结果:
通过PIDL搜索
适当地使用我们所有的辅助函数,写一个‘显示PIDLs内容’按钮的处理器不是太困难的事。与这个按钮相关的combo框由名字和一些特殊文件夹的ID初始化,这个过程执行与SHBrowse例子中相同的代码。此处由函数在点击‘显示PIDLs内容’按钮时执行,并充填列表观察:
void DoEnumeratePidl(HWND hDlg)
{
LPITEMIDLIST pidl = NULL;
// 取得特殊文件夹和它的PIDL
HWND hwndCbo = GetDlgItem(hDlg, IDC_SPECIAL);
int i = ComboBox_GetCurSel(hwndCbo);
int nFolder = ComboBox_GetItemData(hwndCbo, i);
SHGetSpecialFolderLocation(NULL, nFolder, &pidl);
//取得IShellFolder接口
LPSHELLFOLDER pFolder = NULL;
SHGetDesktopFolder(&pFolder);
// 绑定到子文件夹
LPSHELLFOLDER pSubFolder = NULL;
pFolder->BindToObject(pidl, NULL, IID_IShellFolder,
reinterpret_cast<LPVOID*>(&pSubFolder));
pFolder->Release();
pFolder = pSubFolder;
//清除程序的UI
ClearUI(hDlg);
//枚举内容
HWND hwndListView = GetDlgItem(hDlg, IDC_LISTVIEW);
SHEnumFolderContent(pFolder, ShowFolderContent,
reinterpret_cast<DWORD>(hwndListView), NULL);
// 清理
LPMALLOC pMalloc = NULL;
SHGetMalloc(&pMalloc);
pMalloc->Free(pidl);
pMalloc->Release();
pFolder->Release();
}
这个函数从获得特殊文件夹的ID开始,这个ID来自combo框的选择,而后调用SHGetSpecialFolderLocation()函数获得文件夹的PIDL,再从这个PIDL获得IShellFolder接口,然后传递给SHEnumFolderContent()函数。图中显示了这个应用怎样枚举‘控制面板’的小程序:
特殊文件夹
我们首先在第二章中看到了特殊文件夹和它们的基本概念,其中有三种基本类型。几乎所有的特殊文件夹都有对应的目录,但是这些与普通的文件型文件夹和客户文件夹是完全不同的。第三种类型是由没有目录的文件夹(虚拟文件夹)组成。
虚拟文件夹感觉上象一个文件夹,但是它们的位置和内容没有文件和目录概念的映射。‘控制面板’,‘打印机’,‘网上邻居’,‘我的计算机’都是虚拟文件夹的例子。例如‘控制面板’可以包含所有安装了的小程序。
除了外观,没有称为‘控制面板’的物理目录可以包含与之相关的任何东西,比如‘添加新硬件’或‘Modems’。这个文件夹所列出的所有图标都来自系统目录下的.cpl文件。他们由命名空间扩展聚集和表示为虚拟文件夹。
系统对特殊文件夹的支持
Windows API定义了一定数量的特殊文件夹,并且有一堆函数作用其上。这些例程通过如ID一样的数字标识每一个特殊文件夹,但是对PIDLs或CLSIDs则没有什么可做的。
ID定义在shlobj.h中,并且都有相当奇怪的符号名:都以CSIDL_开始。下表列出了一些可用的特殊文件夹:
文件夹ID | 虚拟的 | 描述 |
CSIDL_DESKTOP | Yes | 桌面 |
CSIDL_DRIVES | Yes | 我的计算机 |
CSIDL_BITBUCKET | Yes | 回收站 |
CSIDL_CONTROLS | Yes | 控制面板 |
CSIDL_NETWORK | Yes | 网上邻居 |
CSIDL_INTERNET | Yes | Shell上的IE探测器节点(版本4.71以上) |
CSIDL_PRINTERS | Yes | 打印机 |
CSIDL_DESKTOPDIRECTORY |
| 含有全部桌面快捷方式的目录 |
CSIDL_FAVORITES |
| Favorite文件夹的快捷方式 |
CSIDL_FONTS |
| 安装的字体 |
CSIDL_NETHOOD |
| 网络域的引用 |
CSIDL_PRINTHOOD |
| 打印机的引用 |
CSIDL_PERSONAL |
| 私有文件的快捷方式 |
CSIDL_PROGRAMS |
| ‘程序’菜单的快捷方式 |
CSIDL_RECENT |
| 最近使用文档的快捷方式 |
CSIDL_SENDTO |
| ‘发送到’菜单项的快捷方式 |
CSIDL_STARTMENU |
| ‘开始’菜单中用户定义的项 |
CSIDL_STARTUP |
| 启动时运行的程序的快捷方式 |
CSIDL_COOKIES |
| Cookies |
CSIDL_TEMPLATES |
| 文档模版的快捷方式 |
CSIDL_HISTORY |
| 访问过的Web页面的快捷方式 |
CSIDL_INTERNET_CACHE |
| IE的临时Internet文件 |
CSIDL_APPDATA |
| 一个应用专有数据的文件夹 |
CSIDL_ALTSTARTUP |
| 非本地‘启动’组 |
资料中还提到了另外一些标号为CSIDL_COMMON_XXX的文件夹,它们是:
CSIDL_COMMON_STARTUP CSIDL_COMMON_STARTMENU
CSIDL_COMMON_PROGRAMS CSIDL_COMMON_FAVORITES
CSIDL_COMMON_DESKTOPDIRECTORY CSIDL_COMMON_ALTSTARTUP
除了它们指向一个任何用户都可看到的物理文件夹以外,这些文件夹与不包含COMMON的文件夹相同。虽然这一点在资料中没有显式提到,这些文件夹似乎感觉仅在WindowsNT下出现。
获取文件夹路径
非虚拟文件夹在机器的某个地方有一个路径,你可以通过调用SHGetSpecialFolderPath() API函数获得一个特殊文件夹的路径。特殊文件夹与它的路径之间的连接存储在注册表中,注册键入下:
HKEY_CURRENT_USER
/Software
/Microsoft
/Windows
/CurrentVersion
/Explorer
/Shell Folders
在HKEY_LOCAL_MACHINE下相同的键存储了所有可用的COMMON文件夹,但是,在Windows95和Windoes98下并不是所有COMMON文件夹都有路径。事实上,仅仅CSIDL_COMMON_DESKTOPDIRECTORY和CSIDL_COMMON_STARTUP有路径存储。假设C:/Windows是系统目录,所列出的路径在C:/Windows/All Users folder下。然而,在Windows95/Windows98下SHGetSpecialFolderPath()函数不能为其返回任何值。反之,在WindowsNT下使用同样的函数,则返回正确的路径。
函数
我们可以通过观察曾经使用过的函数SHGetSpecialFolderLocation()来走进函数SHGetSpecialFolderPath()函数。这个函数恢复指定的特殊文件夹的PIDL,它有下面的原型:
HRESULT SHGetSpecialFolderLocation(HWND hwndOwner,
int nFolder,
LPITEMIDLIST* ppidl);
hwndOwner是任何弹出显示窗口的父窗口,nFolder是特殊文件夹的标识符,可以是上表列出的常量之一,而ppidl则是指向包含这个文件夹的PIDL缓冲的指针。SHGetSpecialFolderPath()函数,试图恢复给定文件夹的路径,与上函数非常相似:
HRESULT SHGetSpecialFolderPath(HWND hwndOwner,
LPTSTR lpszPath,
int nFolder,
BOOL fCreate);
lpszPath将包含路径名,而fCreate是一个逻辑值,表示如果文件夹不存在,是否需要建立。当然,在这种情况下,你不能指定一个虚拟文件夹的ID。注意,与SHGetSpecialFolderLocation()函数不同,SHGetSpecialFolderPath()仅仅支持Shell 4.71以上版本。
文件夹设置
IE4.0和活动桌面极大地增加了系统文件夹的设置量。‘文件夹选项’对话框有完善的复选框来确定文件夹的外观,使其具有我们希望的形式:
这个对话框是Windows系统上人人都使用的对话框,它使你能设置查看系统文件和隐藏文件。有些设置(不是全部)可以通过编程读出,自然这些也仅能在Shell 4.71以上版本上有效。
在VC++的资料中可以找到每一个设置的详细说明,在这里所能找到的仅仅是一个例子程序。
SHGetSettings()函数
实际上,使用SHGetSettings()函数是相当简单的,它仅需要两个变量:
void SHGetSettings(LPSHELLFLAGSTATE lpsfs, DWORD dwMask);
SHELLFLAGSTATE是一个非常简洁的结构:
typedef struct
{
BOOL fShowAllObjects : 1;
BOOL fShowExtensions : 1;
BOOL fNoConfirmRecycle : 1;
BOOL fShowSysFiles : 1;
BOOL fShowCompColor : 1;
BOOL fDoubleClickInWebView : 1;
BOOL fDesktopHTML : 1;
BOOL fWin95Classic : 1;
BOOL fDontPrettyPath : 1;
BOOL fShowAttribCol : 1;
BOOL fMapNetDrvBtn : 1;
BOOL fShowInfoTip : 1;
BOOL fHideIcons : 1;
UINT fRestFlags : 3;
} SHELLFLAGSTATE, *LPSHELLFLAGSTATE;
dwMask参数是二进制屏蔽位—对于结构中每一个感兴趣的字段和需要函数恢复的字段,都必须设置适当的屏蔽位。可能的值是:
字段 | 屏蔽位 | 在文件夹选择对话框中的设置 |
fShowAllObjects | SSF_SHOWALLOBJECTS | 显示所有文件 |
fShowExtensions | SSF_SHOWEXTENSIONS | 隐藏已知文件类型的文件扩展 |
fNoConfirmRecycle | SSF_NOCONFIRMRECYCLE | None |
fShowSysFiles | SSF_SHOWSYSFILES | 不显示隐藏文件 |
fShowCompColor | SSF_SHOWCOMPCOLOR | None |
fDoubleClickInWebView | SSF_DOUBLECLICKINWEBVIEW | 在‘一般|客户设置’对话框上‘双击打开项’选择 |
fWin95Classic | SSF_WIN95CLASSIC | 在‘一般’页上‘典型风格’选择 |
fDontPrettyPath | SSF_DONTPRETTYPATH | 全部允许大写名字 |
fMapNetDrvBtn | SSF_MAPNETDRVBUTTON | 在工具条中显示‘网络驱动器映射’按钮 |
fShowAttribCol | SSF_SHOWATTRIBCOL | 在‘细节观察’中显示文件属性 |
fShowInfoTip | SSF_SHOWINFOTIP | 对文件夹和桌面项显示弹出的描述 |
fDesktopHTML | SSF_DESKTOPHTML | 在活动桌面关联菜单上设置‘Web页面观察’ |
fHideIcons | SSF_HIDEICONS | 当桌面作为Web页面时,隐藏图标 |
很多资料都说明fHideIcons是没用的,事实上它仅仅说明当桌面设置成Web模式时,是否桌面上的图标应该被显示。现在让我们查看一些应用,以便探讨可以从这些标志中获得的信息。
观察文件扩展
头一个用途是程序员是否想要在用户的应用界面中显示文件扩展。如果应用程序需要在任何情况下显示文件名,你就应该依据这个标志的状态使用户能够选择确定是否显示扩展名。
使桌面更具活力
fHideIcons标志使你在观察模式设置为‘Web页面’时知道桌面上的图标是否可以看到。而fDesktopHTML标志从另一方面告诉你是否桌面使用了一个HTML页作为它的背景。如果桌面是在Web模式中,并且图标不可见,则你就不能在桌面上建立新的快捷方式。
如果我们仅仅能设置这些位而不能获得它们的状态的话,组合使用fDesktopHTML和fHideIcons是非常有用的。考虑下面的情况:有很多方法清除桌面以禁止公共计算机用户而不是你浏览和运行应用程序。而使用fDesktopHTML和fHideIcons的组合给出了一种新方法。首先,它允许你设置标志显示HTML页面作为桌面的背景,其次隐藏所有桌面上的图标。使用这种方法,你可以把Windows桌面(和机器)变成一个专门运行单一HTML应用的服务器。诚然,任务条还在,但是,你可以通过取得其HWND,然后使用SW_HIDE标志调用ShowWindow()函数来隐藏它:
// 任务条是一个窗口类'Shell_TrayWnd'
HWND hwnd = FindWindow("Shell_TrayWnd", NULL);
if(IsWindow(hwnd))
ShowWindow(hwnd, SW_HIDE);
点击列表观察
在Shell 4.71以上版的众多文件夹的设置中,有可能设置文件夹的行为使其在被选中时变成下划线形式,和只要一次点击便可打开它们。你可以通过‘文件夹选项’对话框的‘一般’页设置这些选择。有趣的是,这些风格对于通用控件库版本4.70的列表观察也有效。所以你可以根据fDoubleClickInWebView标志来修改列表观察的活动形式和鼠标跟踪能力。如此,需要考虑下列关系:
LVS_EX_ONECLICKACTIVATE (4.70)
LVS_EX_TWOCLICKACTIVATE (4.70)
LVS_EX_UNDERLINECOLD (4.71)
LVS_EX_UNDERLINEHOT (4.71)
这个列表中的版本号指的是通用控件库的版本号,不是Shell的版本号。版本4.70的comctl32.dll与IE4.0一同发布(无论活动桌面是否安装)。4.71与IE4.01。
设置扩展风格的列表观察,需要使用ListView_SetExtendedListViewStyle()函数,它是一个围绕LVM_SETEXTENDEDLISTVIEWSTYLE消息建立的宏。上面列表中的头两种风格的意义是直接的,其它的涉及到热点项(一个术语,用于描述鼠标正在经过的项)。LVS_EX_UNDERLINECOLD引起非热点项下划线,而LVS_EX_UNDERLINEHOT则仅仅热点项下划线。
删除操作的确认
fNoConfirmRecycle标志指示在删除文件之前是否显示确认对话框。可以想象,这仅适用于删除到‘回收站’和Shell操作的场合。然而,即使你没有使用Shell函数,如SHFileOperation(),来删除文件,如果用户希望有这样的询问时,是否不能很好地给出确认提示呢?阅读一下fNoConfirmRecycle可以向这个方向前进一大步,进而使之成为可能。
示例程序
程序界面,作为这一章最后一个例子,如下图所示,可能你已经猜到了什么是我们要用于建立的程序框架。
这个例子的代码是十分容易的:只需要为‘取得设置’按钮加一个处理器即可,它将引起当前Shell的选项设置被读出。下面的源码产生了在图中所看到的结果。总之,一定要记住在源文件顶部包含#include shlobj.h 和 resource.h。
void OnSettings(HWND hDlg)
{
SetDlgItemText(hDlg, IDC_SETTINGS, "");
SHELLFLAGSTATE sfs;
SHGetSettings(&sfs, SSF_DESKTOPHTML | SSF_SHOWALLOBJECTS |
SSF_MAPNETDRVBUTTON | SSF_SHOWATTRIBCOL | SSF_SHOWEXTENSIONS);
TCHAR szBuf[MAX_PATH] = {0};
if(sfs.fDesktopHTML)
lstrcat(szBuf, __TEXT("Active Desktop - View as Web page is active/r/n"));
if(sfs.fMapNetDrvBtn)
lstrcat(szBuf, __TEXT("Network buttons on the toolbar/r/n"));
if(sfs.fShowAllObjects)
lstrcat(szBuf, __TEXT("Shows all files/r/n"));
if(sfs.fShowAttribCol)
lstrcat(szBuf, __TEXT("Shows attributes in Detail view/r/n"));
if(sfs.fShowExtensions)
lstrcat(szBuf, __TEXT("Shows extensions for known file types/r/n"));
SetDlgItemText(hDlg, IDC_SETTINGS, szBuf);
}
参数设置
阅读了这些参数的设置,对处理各种情况确实有不少帮助,但是更有兴趣的应该是编程地设置这些特征。不幸地是,到目前为止只有SHSetSettings()例程出现,现在,我们要说明的是有大量的可以达到这个目标的方法可用而不需要微软的帮助。
参数存储在哪里
正如你已经大致猜到的那样,所有可以使用SHGetSettings()读到的参数都存储在注册表的某个地方。也即,可以由一个相对安全的方法来编程地设置这些参数。
在向下进行之前,我们要强调一个重点。在官方资料缺乏的情况下,微软在未来的操作系统版本中是可以自由改变注册表键的用途的,这就间接地影响到你的代码。在编写代码时,我们使用的技术在版本4.71下是好用的。
打开注册表,查看下面的键:
HKEY_CURRENT_USER
/Software
/Microsoft
/Windows
/CurrentVersion
/Explorer
/Advanced
似乎我们已经找到了要找的东西。修改这些注册表项是足够简单的,对吗?很不幸,不是这样的—你很快就能注意到,这个值列表缺少了条目数。尤其是‘Web观察’设置。
回想一下测试反向工程注册表设置的黄金定律:总是比较HKEY_CURRENT_USER和 HKEY_LOCAL_MACHINE下相同键的内容。这里就是我们要找的:
正如所见,有一个完整的层次结构复制了与‘文件夹选项’对话框相同的树结构。主节点是一个“group”类型的节点,有自己的Bitmap,和显示名。结构的叶含有一个属性集,其中突出的有两个值:RegPath和 HKeyRoot:
这就是说,在子树中每一个条目都指向注册表中的另一个键,实际值就存储在那儿,其路径是HKeyRoot/RegPath/ValueName。叶特征确定了显示文字,选项类型(复选框或收音按钮),选择值(选中或没选中),默认值,甚至文件名和有帮助的主题ID等。
给出了这些之后,处理客户的SHSetSettings()函数就只是简单地读写注册表数据操作了。
附加客户选项到标准对话框
因为在‘文件夹选项’对话框的层次结构和注册表子树布局之间有良好的对应,我们能立即猜测到添加一个新键到注册表应该在标准对话框中产生一个新的客户选项。为了证明这一点,仅需要做一件事情:加一个新键到注册表子树。
我在Folder下定义了一个新键,称为MySetting。然后定义所有在其它叶上看到的值:
保存这个改变到注册表之后,期待地打开‘文件夹选项’对话框,没有任何新东西出现。事实上,对此有一个显而易见的原因:对话框仅在能够读出所存储的值时才添加新项。正如早先提到过的那样,这个值存储在注册表的另一个位置—是由HKeyRoot,RegPath 和ValueName指向的。剩下的就是在下面的键上建立一个称为MySetting的新值:
HKEY_CURRENT_USER
/Software
/Microsoft
/Windows
/CurrentVersion
/Explorer
/Advanced
还应该设置所期望选项的默认值。在保存改变和响应‘文件夹选项’对话框后,新的设置就出现了,如下图所示:
什么时候客户选项是有用的
在‘文件夹选项’对话框中添加新的客户选项不是为了向人炫耀—也不是一项允许用户客户化你的程序的便利方法。我们并不建议你使用这个对话框来设置一个应用的所有设置。但是,考虑使用选项来解决用户界面和文件夹的问题是有价值的。能更好地探索这些特征的模块应该在命名空间扩展应用中。
使用注册表路径的选择完全在于你自己,但是,应该认识到这一点,把你自己的设置存储到远离标准设置的地方,好的选择是使用应用特殊的注册键。
小结
文件夹是一个广泛的题目,这一章一直在努力给出详细解释。你已经看到怎样浏览特殊文件安夹和怎样与它们一道工作。枚举它们的内容和设置它们的参数。特别,在这一章中还揭示了:
怎样更好地使用SHBrowseForFolder()
怎样枚举任何文件夹的内容
处理特殊系统文件夹的函数
那些文件夹设置是可读的,怎样编程设置它们
为此,我们构建了潜在有用的函数来扩展API所提供的工具。例如SHEnumFolderContent()和 SHPathToPidlEx()辅助例程。此外,我们还揭示了Shell怎样存储文件夹的设置,和给出了添加新选项到标准的‘文件夹选项’对话框的方法。