第十章 Winows辅助库
关于Windows98,最令人欣赏的是它确定不再使用comctl32.lib和shell32.lib的版本延续,因此IE4和活动桌面也就不再跟随发布了——在Windows98中,机器中的所有库都是一致的。
然而,我们担心这种平静的状态仅仅是暂时的,很快我们将还要建立增强控件来修补用户界面,这或许是风暴到来之前的平静。
为了使我们增强对Windows98的认识,也是为了记住程序员不堪回首的过去,在这一章中,我们将简要回顾从传统的Windows95到Windows98期间那些一直困扰我们的问题。我们也揭示后来的库中什么是新东西。而后讨论三组新的辅助函数,它们是关于‘回收站’,注册表和字符串处理方面的。
最后,我们探讨一个公开的秘密:一个非官方资料说明的,不被官方承认的驱动器格式化函数SHFormatDrive()。概括地讲,我们打算讨论:
微软回答的关于Shell版本的问题
‘回收站’API
操作字符串和注册表的辅助库的概览
Windows98中还有那些是没有说明资料的
特别,我们的目标是给出一种通用的技术来客户化和改进系统对话框。然后使用这种技术解释没有官方说明资料的SHFormatDrive()函数,这是探索用编程方式格式化驱动器的一个辅助例程。
版本特色
这得从Web IE3.0的第一个贝塔版说起,你会立刻注意到这个扁平的带有纹理的工具条,而后是更复杂的(可调尺寸的)和触手可及对象。虽然并不明显地知道它们怎样工作,但它们一定是酷的,所以它们被称之为酷条(coolbar)
在安装新的系统DLL副本期间,IE3.0抛出覆盖comctl32.dll的警告信息。从此,粗心的程序员就开始使用烙有IE3.0标记的控件了(主要是coolbar),在某些场合下,建立的应用要求安装IE3.0才能正常工作。更糟糕的是微软很长时期以来都拒绝授权发布新版的comctl32.dll,直到最近才给出了一个自抽取模块来安装最近的Windows95和WindowsNT4.0机器上的控件库。
到目前为止我们仅限制讨论到IE3.0,但是这个问题在后来的版本中一直继续。事实上,因为活动桌面的出现使事情变得更复杂了,所以,在IE4.0发布以后,这个问题得到了彻底的解决。这已经不再是一个简单的在用户界面中使用或不使用酷控件的问题了。有许多新函数和说明文档作为Windows的一部分加了进来。
因为这些问题,我们有理由在这本书中非常清晰地解释你的应用运行的环境是Windows98还是Windows95/WindowsNT并安装了IE4.0和活动桌面。如果你在具有不同特征的机器上运行了所提供的例程,你就有机会得到系统提示的错误消息,通知你在Shell库中不能找到特定的函数。
DLL版本信息
很多程序员都用程序来判断机器上指定模块的哪个版本正在运行。下面的代码段说明了怎样从存储在模块资源中的VS_VERSIONINFO块上获得这个信息:
DWORD dwLen = GetFileVersionInfoSize(szFile, &dwUseless);
LPVOID lpVI = malloc(dwLen);
GetFileVersionInfo(szFile, NULL, dwLen, lpVI);
VerQueryValue(lpVI, __TEXT("//"), reinterpret_cast<LPVOID*>(&lpFFI), &iBufSize);
DWORD dwVer1 = lpFFI->dwFileVersionMS;
DWORD dwVer2 = lpFFI->dwFileVersionLS;
这里lpFFI是一个指向以前初始化的VS_FIXEDFILEINFO结构的指针,当然,没有输出VS_FIXEDFILEINFO资源的模块不能输出任何可由外部程序读取的和检查的信息。
事实上微软现在通过新的DLL策略提供了这个工具。每一个系统DLL都输出一个DllGetVersion()函数,它返回内部版本号,这个函数执行的代码类似于上面的代码段。第三方库开发商也正在他们的产品中作相同的努力。
系统DLL的版本号
这个策略的想法是提供一个普通的和容易的方法使应用能够知道它们正在使用的或它们期望找到的DLL的版本信息。当然,想要执行这个检查的程序不应该尝试静态引入这个函数,因为老的DLL并不支持它。相反必须依赖动态加载。
下面是检查活动桌面更新的例程:
BOOL IsActiveDesktopInstalled()
{
HINSTANCE hShell32 = LoadLibrary(__TEXT("shell32.dll"));
if(!hShell32)
return FALSE;
else
{
DLLGETVERSIONPROC pFunc = reinterpret_cast<DLLGETVERSIONPROC>(
GetProcAddress(hShell32, __TEXT("DllGetVersion")));
if(!pFunc)
{
FreeLibrary(hShell32);
return FALSE;
}else{
DLLVERSIONINFO dvi;
ZeroMemory(&dvi, sizeof(dvi));
dvi.cbSize = sizeof(dvi);
(*pFunc)(&dvi);//导出调用
// Shell版本 < 4 说明是 NT 3.51
if(dvi.dwMajorVersion < 4)
{
FreeLibrary(hShell32);
return FALSE;
}
if(dvi.dwMajorVersion == 4)
{
// 安装了活动桌面
if(dvi.dwMinorVersion >= 71)
{
FreeLibrary(hShell32);
return TRUE;
}
}else{
// 高于Windows 9x 和 NT 4.0
FreeLibrary(hShell32);
return TRUE;
}
}
}
FreeLibrary(hShell32);
return FALSE;
}
这个函数加载shell32.dll,并试图查询DllGetVersion()函数是否存在,如果失败,说明这个DLL是比安装的活动桌面老的DLL,否则,导出调用,函数原型为:
HRESULT DllGetVersion(DLLVERSIONINFO* pdvi);
DLLVERSIONINFO定义在shlwapi.h中,形式如下:
typedef struct _DllVersionInfo
{
DWORD cbSize;
DWORD dwMajorVersion;
DWORD dwMinorVersion;
DWORD dwBuildNumber;
DWORD dwPlatformID;
} DLLVERSIONINFO;
使用这个结构,程序甚至可以恢复DLL的构建号和目标平台。dwMajorVersion和dwMinorVersion成员是形成版本号的头两个项;一般构建号是第三项。检查dwPlatformID的对应常量可以区分目标平台:
DLLVER_PLATFORM_WINDOWS (0x01) — DLL构建于所有Windows平台
DLLVER_PLATFORM_NT (0x02) — DLL构建于特定的Windows NT
整个结构和常量都在shlwapi.h中声明,这个头文件和其中的Shell轻量级API函数我们将在后面加以讨论。再次重申,每一个由活动桌面安装的Shell DLL都输出一个DllGetVersion()函数。
在自己的函数中输出版本号
每当你开发可执行模块时—EXE,DLL,或其它—我们都强烈推荐带有某些版本信息。这可以通过在模块的资源中定义上面提到的VS_VERSIONINFO结构,或者定义DllGetVersion()函数来完成。我们下面就检测这两种情况。
使用VS_VERSION_INFO
提供应用和模块版本信息最容易的方法是在工程(project)的.rc文件中定义VS_VERSION_INFO资源类型。有一个资源编辑器用于此目的,下面的屏幕截图显示了这一点:
上面的信息最终存储成一个工程(project) RC 文件形式的脚本,如下:
VS_VERSION_INFO VERSIONINFO
FILEVERSION 1,0,0,1
PRODUCTVERSION 1,0,0,1
FILEFLAGSMASK 0x3fL
#ifdef _DEBUG
FILEFLAGS 0x1L
#else
FILEFLAGS 0x0L
#endif
FILEOS 0x4L
FILETYPE 0x1L
FILESUBTYPE 0x0L
BEGIN
BLOCK "StringFileInfo"
BEGIN
BLOCK "040904b0"
BEGIN
VALUE "CompanyName", "/0"
VALUE "FileDescription", "/0"
VALUE "FileVersion", "1.00.001/0"
VALUE "InternalName", "VERSION/0"
VALUE "LegalCopyright", "/0"
VALUE "LegalTrademarks", "/0"
VALUE "OriginalFilename", "VERSION.exe/0"
VALUE "ProductName", "Version Checker/0"
VALUE "ProductVersion", "1, 0, 0, 1/0"
END
END
BLOCK "VarFileInfo"
BEGIN
VALUE "Translation", 0x409, 1200
END
END
使用DllGetVersion()
除此之外,我们还可以从自己的库中输出DllGetVersion()函数。如果每一个人都这样做,将导出识别模块本号的标准方法。这里是DllGetVersion()函数一个典型的实现,它返回版本号1.0。
#include <shlwapi.h>
HRESULT DllGetVersion(DLLVERSIONINFO* pdvi)
{
if(pdvi == NULL)
return E_FAIL;
ZeroMemory(pdvi, pdvi.cbSize);
pdvi->dwMajorVersion = 1;
pdvi->dwMinorVersion = 0;
pdvi->dwPlatformID = DLLVER_PLATFORM_WINDOWS;
return NOERROR;
}
更通用的函数
如上所示,对每一个Windows平台,我们不能期望在每一个DLL中都找得到DllGetVersion()的实现,在微软采取鼓励措施这样做的时候,我们需要的更多。请求DLL自身输出它的版本号听起来有点陌生,但是,我们认为这更容易编码和测试。
理想情况下,应该有一个系统API函数,它能够用于读出任何有效文件的版本信息。退一步说,这个低级想法从Windows3.1开始就已经有了,但是致力于应付版本功能的函数可能会引起麻烦。我写了一个通用函数,它能返回任何可执行文件的版本信息,不仅返回字符串而且返回数字数组。你甚至可以使用这个函数读出16-位程序和DLL的版本号,而不用顾及开发商,这个函数就是SHGetVersionOfFile():
DWORD SHGetVersionOfFile(LPTSTR szFile,LPTSTR szBuf, LPINT lpiBuf, int iNumOfFields)
{
DWORD dwUseless = 0;
UINT iBufSize = 0;
VS_FIXEDFILEINFO* lpFFI = NULL;
TCHAR s[MAX_PATH] = {0};
DWORD dwLen = GetFileVersionInfoSize(szFile, &dwUseless);
if(dwLen == 0)
{
if(szBuf)
lstrcpy(szBuf, __TEXT("<unknown>"));
return 0;
}
LPVOID lpVI = GlobalAllocPtr(GHND, dwLen);
GetFileVersionInfo(szFile, NULL, dwLen, lpVI);
VerQueryValue(lpVI, __TEXT("//"),
reinterpret_cast<LPVOID*>(&lpFFI), &iBufSize);
DWORD dwVer1 = lpFFI->dwFileVersionMS;
DWORD dwVer2 = lpFFI->dwFileVersionLS;
GlobalFreePtr(lpVI);
// 充填返回缓冲
if(szBuf != NULL)
{
wsprintf(s, __TEXT("%d.%d.%d.%d"),
HIWORD(dwVer1), LOWORD(dwVer1), HIWORD(dwVer2), LOWORD(dwVer2));
lstrcpy(szBuf, s);
}
if(lpiBuf != NULL)
{
for(int i = 0 ; i < iNumOfFields ; i++)
{
if(i == 0)
lpiBuf[i] = HIWORD(dwVer1);
if(i == 1)
lpiBuf[i] = LOWORD(dwVer1);
if(i == 2)
lpiBuf[i] = HIWORD(dwVer2);
if(i == 3)
lpiBuf[i] = LOWORD(dwVer2);
}
}
return dwVer1;
}
版本号由4个数字组成,通常由点“.”分隔。一个典型的完全版本号的例子是4.71.2106.1。头两个数(此时是4 和 71)称作为主和付版本号。
如果你需要,没有人阻止你仅使用版本号的一部分。我们发现,一般只需要版本号的两个格式中的一种—或者是显示在对话框中的串或者是易于执行检查的分开的数字。
SHGetVersionOfFile()函数的编程界面是非常灵活的,且它也试图同时满足这两种要求。函数的返回值是主版本号,而参数列表包含了指向串的指针,这个缓冲将返回格式为%d.%d.%d.%d 的信息。如果不需要串返回,可以传递一个NULL。
SHGetVersionOfFile()函数接收两个附加的变量。头一个为指向整数组的指针,而第二个则是它的尺寸—它最多可以包含四个元素。通过这个缓冲调用者可以接收组成版本号的各个元素。毋庸置疑这使任何对版本号所作的进一步处理更加容易。
上面的屏幕截图显示一个应用程序的界面,它使用SHGetVersionOfFile()函数。它的实现主要是两个按钮的处理器,下面是OnBrowse()处理器:
void OnBrowse(HWND hDlg, WPARAM wID)
{
TCHAR szFile[MAX_PATH] = {0};
TCHAR szWinDir[MAX_PATH] = {0};
GetWindowsDirectory(szWinDir, MAX_PATH);
OPENFILENAME ofn;
ZeroMemory(&ofn, sizeof(OPENFILENAME));
ofn.lStructSize = sizeof(OPENFILENAME);
ofn.lpstrFilter = __TEXT("Executable/0*.exe;*.dll;*.drv;*.vxd/0");
ofn.nMaxFile = MAX_PATH;
ofn.lpstrInitialDir = szWinDir;
ofn.lpstrFile = szFile;
if(!GetOpenFileName(&ofn))
return;
else
SetDlgItemText(hDlg, wID, ofn.lpstrFile);
}
一旦选择了要询问的文件,点击OK按钮将唤醒对DoGetVersionInfo()函数的调用,代码如下:
const int BUFSIZE = 1024;
const int MSGSIZE = 40;
void DoGetVersionInfo(HWND hDlg)
{
TCHAR szTemp[MAX_PATH] = {0};
HWND hwndList = GetDlgItem(hDlg, IDC_VIEW);
GetDlgItemText(hDlg, IDC_FILENAME, szTemp, MAX_PATH);
// 建立列表观察串
TCHAR pszBuf[BUFSIZE] = {0};
LPTSTR psz = pszBuf;
lstrcpy(psz, szTemp);
lstrcat(psz, __TEXT("/0"));
psz += lstrlen(psz) + 1;
// 取得版本信息
TCHAR szInfo[MSGSIZE] = {0};
SHGetVersionOfFile(szTemp, szInfo, NULL, 0);
lstrcpy(psz, szInfo);
lstrcat(psz, __TEXT("/0"));
psz += lstrlen(psz) + 1;
// 添加两个列文字
AddStringToReportView(hwndList, pszBuf, 2);
}
为了做这个工作,DoGetVersionInfo()函数使用了SHGetVersionOfFile()函数,这是我们前面定义的函数,和AddStringToReportView()函数,这是在第6章中定义的函数,它的姊妹函数MakeReportView()在OnInitDialog()中用来设置列表观察:
void OnInitDialog(HWND hDlg)
{
// 初始化报告观察为两个列,文件和版本号
HWND hwndList = GetDlgItem(hDlg, IDC_VIEW);
LPTSTR psz[] = { __TEXT("File"), reinterpret_cast<TCHAR*>(350),
__TEXT("Version"), reinterpret_cast<TCHAR*>(95) };
MakeReportView(hwndList, psz, 2);
// 设置图标(T/F 大/小图标)
SendMessage(hDlg, WM_SETICON, FALSE, reinterpret_cast<LPARAM>(g_hIconSmall));
SendMessage(hDlg, WM_SETICON, TRUE, reinterpret_cast<LPARAM>(g_hIconLarge));
}
最后你需要添加#includes resource.h 和comdlg.h,连接version.lib和comdlg32.lib。并添加两个新的case到APP_DlgProc()中:
case WM_COMMAND:
switch(wParam)
{
case IDC_BROWSE:
OnBrowse(hDlg, IDC_FILENAME);
return FALSE;
case IDOK:
DoGetVersionInfo(hDlg);
return FALSE;
case IDCANCEL:
EndDialog(hDlg, FALSE);
return FALSE;
}
break;
应用运行后,你可以在编辑框中输入要检查的文件名或用‘打开’对话框选择文件。‘OK’按钮的代码努力读出指定文件的版本号信息,文件名和版本号串被显示在报告观察中,除非文件中不包含版本信息,此时‘Unnown’串被返回。
回收站API
回收站是一个Shell命名空间中的客户对象,正好在桌面上。它可以被看作为用Shell函数删除的所有文件对象的临时容器,其中的文件可以被恢复或最后销毁。仅仅是那些通过Shell手动删除的文件对象或通过Shell函数编程删除的文件对象才能进入回收站,记住这一点是重要的。如果使用DeleteFile(),或从MS-DOS提示符下删除文件,则文件将直接从文件系统中删除。
由于删除请求必需被分别处理,所以回收站有一个特殊的编程接口。在第3章,我们讨论的SHFileOperation()函数。它能够发送被删除文件到回收站。在下一节我们检测另外两个允许我们清空回收站和发送查询操作的函数。这些函数都是4.71以上版的Shell所有的。
回收站的结构
回收站是通过命名空间实现在桌面上的,我们在最后两章再详细说明命名空间的定义。这个装载删除文件的容器坐落于每一个局部驱动器上的‘Recycled’文件夹中。
每一个‘Recycled’文件夹只是包含这个驱动器的被删除文件,尽管通过Shell界面你不能看到它。事实上,如果打开系统上的任何‘Recycled’文件夹,你都会看到同样的内容,它们是所在驱动器上被删除文件的列表。如果使用DOS工具环顾这些文件夹,将能看到回收站采用的逻辑。
上图说明C和E驱动器上两个‘Recycled’目录下的实际内容。事实上,任何发生在Shell内的删除操作都分成不相干的两步,开始Shell把文件从初始位置移动到当前驱动器的‘Recycled’文件夹,而后根据命名习惯重命名这个文件。当请求它的列表时,你在桌面上看到的‘回收站’只是从局部驱动器的‘Recycled’文件夹中收集文件。
命名习惯
就象上图暗示的,所有回收站中的文件都有一个D字符开始的名字,第二个字符是初始驱动器字符,而后是在文件删除时分配的增量号。文件保留它初始的扩展名。
对于本身是隐藏的‘Recycled’文件夹,这些被标注为删除的文件只有‘存档’文件的属性集。在删除名和原始名之间的连接存储在基于驱动器的一个隐藏文件info2中(打开DOS提示符,返回到一个‘Recycled’文件夹下,输入dir /AH)。
如果文件info2丢失或损坏会怎样,没有问题,因为删除这个文件不影响它所涉及的文件。而且,每次启动,如果有删除文件,Windows都保证有一个info2文件存在,如果info2文件不存在,Windows将飞快地建立它。因而,恢复文件就是把它移回原来的位置,并回复它的初始名字。如果存在另一个同名文件,弹出一个确认对话框。删除回收站的文件简单地就是将它们实际删除。
回收站观察
下图说明一个Shell回收站的外观:
你可以看到,Shell观察显示的是实际删除文件的名字—Shell基本上是站在用户的角度上解释这个名字。实际工作的文件名并不同于Shell显示的名字。
这个行为是命名空间扩展的一个例子,在物理目录的内容上建立抽象层。
回收站交互函数
除了SHFileOperation()函数之外,还有两个操作回收站的Shell函数,它们是:
SHEmptyRecycleBin()
SHQueryRecycleBin()
按照名字提示,第一个是销毁包含在机器的各个‘Recycled’文件夹中的所有文件。相反,后一个则恢复指定驱动器的‘Recycled’目录中一定数目的项,以及它们占有的存储量。让我们更详细的看一下它们的语法,从SHEmptyRecycleBin()开始。
HRESULT SHEmptyRecycleBin(HWND hwnd, LPCTSTR pszRootPath, DWORD dwFlags);
Hwnd变量表示这个函数建立的窗口或对话框的父窗口。使用pszRootPath值,函数可以清空单个磁盘的‘Recycled’文件夹,或所有磁盘的‘Recycled’—这个变量是准备清空文件夹的驱动器上的根目录路径。如果传递了一个全路径名,则仅仅这个驱动器部分被考虑。如果是NULL。则整个回收站都被清空,操作是逐个驱动器完成的。
最后的变量可用于指定某些标志,它们的解释如下表:
标志 |
描述 |
SHERB_NOCONFIRMATION |
通常,系统在进一步操作之前显示确认对话框。如果这个标志设置,对话框就被抑制。 |
SHERB_NOPROGRESSUI |
Shell将显示带有进度条的对话框(如下图所示)。如果这个位设置,将没有对话框显示。 |
SHERB_NOSOUND |
如果这个位设置,完成时不会有声音出现。 |
QueryRecycleBin()函数有下面的原型:
HRESULT SHQueryRecycleBin(LPCTSTR pszRootPath, LPSHQUERYRBINFO pSHQueryRBInfo);
pszRootPath变量与SHEmptyRecycleBin()中的意义相同:表示要恢复的驱动器的根目录,它也可以是NULL,或全路径名。前一种情形,系统恢复所有可用驱动器的信息,后一种则是指定驱动器部分信息。
被恢复的信息存储在SHQUERYRBINFO结构中,其定义如下:
typedef struct _SHQUERYRBINFO
{
DWORD cbSize;
__int64 i64Size;
__int64 i64NumItems;
} SHQUERYRBINFO, FAR* LPSHQUERYRBINFO;
通常,在产生调用之前,cbSize成员必须充填结构的尺寸值。在调用之后i64Size中包含了由pszRootPath指定的那部分回收站占有的总字节数,i64NumItems将包含标志为删除的项数。后两个成员都是64位整数。
辅助库
在半秘密的情况下,安装的活动桌面把一个新的,相对小的库存到磁盘上,它就是shlwapi.dll(Shell轻量级的API库)。这个DLL包含了许多使编程更轻松的函数。使用这个库是很容易的,只要#include 这个shlwapi.h头文件,和连接shlwapi.lib库即可。现在就让我们了解一下这个库可为我们做些什么。在Internet客户端SDK中,你可以找到这些函数基本描述的短例子。因为其中的很多函数都是自说明函数,所以这种举例说明的方法是有效的。这些函数可以划分成三个组,它们分别作用于下面的领域:
注册表
字符串
处理路径串
在下面的例子中我们将使用这些函数,但是并没有提供所有函数的详尽说明。因为它们数量太大并且又十分简单,所以我们只给出shlwapi.dll库中新的和最感兴趣的功能,而不是列出它们的名字和变量表。从Shell 的 4.71版开始,这些例程就被当作WindowsSDK的一部分了。
注册表的Shell API
Win32注册表API的一个巨大的众所周知的缺点是你必须调用三个函数才能获得即使是最无用和最没有意义的值。你必须打开/建立这个键,做读或写操作,然后关闭这个Handle。更进一步讲,它们模仿文件操作。在注册表ShellAPI中新函数前进了一步,每当你想要取值时,新函数都节省打开/关闭注册表键的操作。
注册表Shell API函数表
下表概括了最重要的新函数,它们简化了基于注册表代码的开发,增加了生产力。
函数 |
描述 |
SHDeleteEmptyKey() |
删除一个空键的整个子树,与WindowsNT的RegDeleteKey()相似。 |
SHDeleteKey() |
删除一个键和它的子树,与Windows95的RegDeleteKey()相似。 |
SHDeleteValue() |
删除一个值 |
SHEnumKeyEx() |
枚举给定键的子键 |
SHEnumValue() |
枚举给定键的值 |
SHGetValue() |
恢复值 |
SHOpenRegStream() |
返回注册值的IStream接口。 |
SHQueryInfoKey() |
恢复给定键的信息 |
SHQueryValueEx() |
查询一个特定值的注册表键 |
SHSetValue() |
设置值 |
处理字符串
高级开发工具已经提供了处理字符串的方法。如果选择的话,Windows程序员可以依赖C运行时库提供的工具,如strstr()和strchr()等,但是,如果摆脱运行时库,我们就能获得较小的内存开销。这就是为什么增加一定数量的C运行时函数到Windows库中的原因。其中包含了lstrcpy(), lstrcat(),
wsprintf()和lstrcmp()。
这种趋势随shlwapi.dll库而增强,并且名字也有了新的变化,如,StrDup(), StrChr(), StrRChr()和StrStr()。此外,这个库还提供了一些省时和必需的函数,如两个转换数值到千字节,或到时间段的函数。StrFormatByteSize()能够转换24102到“23.5KB”—它甚至可以智能地转换到MB或GB,这依赖于你所传递的值。StrTimeFromInterval()函数可以把毫秒表示的长时间转换为分钟或小时串。
主要的是,我们仍然在等待象VB的Right$()和 Mid$()一样功能的函数,尽管在MFC的CString类和STL的串类中有类似的功能。
处理字符串的函数表
下表中概括了绝大多数处理字符串的新函数。注意,某些函数有两个版本,其中一个是大小写敏感的。名字中以 I 结尾的是不敏感的。
函数 |
描述 |
ChrCmpI() |
比较两个字符串(不是敏感的) |
StrChr(), StrChrI() |
字符在串中的第一次出现 |
StrCmpN(), StrCmpNI() |
比较两个串的头 n 个字节 |
StrDup() |
复制串 |
StrFormatByteSize() |
转换数字字节值到KB,MB,GB |
StrFromTimeInterval() |
转换ms数字值到时间段 |
StrNCat() |
追加特殊字符数字 |
StrPBrk() |
在给定的缓冲中任何字符的第一次出现 |
StrRChr(), StrRStrI() |
字符在串中最后一次出现。 |
StrSpn() |
查找由给定字符集形成的整个子串 |
StrStr(), StrStrI() |
搜索子串 |
StrToInt(), StrToLong() |
串到数字转换 |
StrToIntEx() |
十进制/十六进制串到数值转换 |
StrTrim() |
删除前导和后尾空格 |
处理路径串
虽然路径串也可以说成是字符串,但是路径确实不是字符串。只有很少的程序不必写自己的函数来处理路径。我们认为在现代计算史中,增加反斜杠(/)条件的函数是前十个写的最多的函数之一。我们所用于处理这个操作的宏仅包含了一行代码:
#define ADDBACKSLASH(p) lstrcat(p, (p[lstrlen(p) - 1] == 92 ? "//" : ""))
shlwapi.dll中的函数听起来有实在的意义,可悲的是它们并不能帮助你解决现实困难的问题。它们主要的好处是处理重复的任务。例如,第8章中我们讨论了引起FindExecutable()函数失败的长文件名问题。下一节的路径函数列表中包含了一个抽取变量的函数,这个函数提供了一种解决方案,不幸的是这个函数假设第一个空格是文件名的末尾,这(正像我们看到的)并不总是正确的。
处理路径串的函数表
下面的表中囊括了许多处理路径串的新函数。我们喜欢PathCompactPathEx(),它提供一个截断的路径来适应给定的像素数,以及(甚至更好)PathSetDlgItemPath()函数,它使用PathCompactPathEx()并且自动把路径名绘制到由ID表示的子窗口中。如果路径名包含空格,它添加和删除定界引号。这正是一种能够防止FindExecutable()函数出现Bug的功能。(参见第8章)
函数 |
描述 |
PathAddBackslash() |
保证路径结尾有一个‘/’ |
PathAddExtension() |
确保路径有扩展名 |
PathBuildRoot() |
从驱动器号(0=A,等)构造一个驱动器路径 |
PathCanonicalize() |
扩展和适当置换路径中包含的所有..和. |
PathCombine() |
组合驱动器与目录路径 |
PathCompactPath() |
截断路径来适合一定数目的像素 |
PathCompactPathEx() |
插入圆括号使路径符合指定数量字符的要求 |
PathCommonPrefix() |
用公共前缀比较两个路径 |
PathFileExists() |
验证文件是否存在 |
PathFindExtension() |
取得扩展名 |
PathFindFileName() |
取得文件名 |
PathFindNextComponent() |
取得两个反斜杠之间的下一项 |
PathGetArgs() |
返回路径的命令行 |
PathGetCharType() |
检查相关于路径的给定字符。是否为可用的长文件名字符,是否为通配符或分隔符 |
PathGetDriveNumber() |
取得驱动器号(0=A,等) |
PathIsDirectory() |
检查给定的路径是否为目录 |
PathIsFileSpec() |
检查给定的路径是否包含分隔符(/,:) |
PathIsRoot() |
检查给定的路径是否包含根 |
PathIsSameRoot() |
检查两个路径是否共享相同的根 |
PathIsSystemFolder() |
检查给定的路径是否有‘System’特征 |
PathIsUNC() |
检查给定的路径是否遵循UNC习惯 |
PathIsURL() |
检查给定的路径是否为URL |
PathQuoteSpaces() |
如果路径包含空格,用引号将路径括起 |
PathRemoveArgs() |
删除变量 |
PathRemoveBackslash() |
确保最后没有反斜杠(/) |
PathRemoveExtension() |
确保没有扩展名 |
PathRemoveFileSpec() |
确保没有文件或扩展名 |
PathRenameExtension() |
置换扩展名 |
PathSearchAndQualify() |
确定路径是否是正确的和全路径。 |
PathSetDlgItemPath() |
确保包含路径的控件文字正确地显示在子窗口中 |
PathSkipRoot() |
从一个目录开始解析路径 |
PathStripPath() |
删除驱动器和目录 |
PathStripToRoot() |
仅留下驱动器 |
PathUnquoteSpaces() |
确保路径没有定界的引号 |
SHFormatDrive()函数
无论Windows98包含了什么,也不管Shell的大部分受到最近这些变化的影响有多大,SHFormatDrive()函数的说明资料仍然是贫乏的。但是,结合这里的信息和MSDN库的笔记,以及某些发表的文章,我们现在还是能够用SHFormatDrive()函数编程实现对磁盘进行格式化操作。
SHFormatDrive()函数做了什么
就像它的名字那样,SHFormatDrive()函数允许你格式化驱动器,原理上你可以试着格式化C驱动器,这个函数(或说,系统)事实上并不能防止你这样做。正常情况下Windows驱动器或相关文件正在使用的驱动器是不能格式化的,但是无论它多有价值,我们都不讨论这个问题。这个函数的原型(来自MSDN)如下:
DWORD WINAPI SHFormatDrive(HWND hwnd, UINT drive, UINT fmtID, UINT options);
变量 |
描述 |
hwnd |
被显示对话框的父窗口 |
drive |
被格式化的驱动器ID(0=A,1=B,2=C,等) |
fmtID |
应该总是设置为-1 |
Options |
格式化的类型 |
格式化类型可以有下面选择:
值 |
描述 |
0 |
快速格式化 |
1 |
完全格式化 |
2 |
制作系统盘 |
函数的返回码如下:
返回码 |
描述 |
>0 |
成功 |
0 |
传递的参数错 |
-1 |
格式化期间出错 |
-2 |
操作被取消 |
-3 |
驱动器不能被格式化 |
现在我们有了调用SHFormatDrive()函数的所有知识,格式化磁盘可以这样做:
irc = SHFormatDrive(hWnd, 0, -1, 0);
由shell32.dll输出SHFormatDrive()函数是相当正常的,因为shell32.lib中包含了它的定义。在所述的几行资料中所缺少的是在shellapi.h的声明。因为函数定义在shell32.lib库中,因此你不必动态地通过LoadLibrary()和GetProcAddress()加载它—只需使用它的规则名字就可以了。记住,你必须在某个地方添加这个函数的声明。我们上面给出的就是一个很好的声明,在C++ 中可能需要一个‘extern’关键字。在这一节的后边有代码说明这一点。
SHFormatDrive()函数和Windows NT
虽然MSDN的文章中没有提到这一点,SHFormatDrive()的行为在Windows NT下还是稍微有点不同。特别是成功的返回码是0,而在Windows9x中这是大于0。此外,用户界面是不同的,而且在按OK开始格式化时有进一步确认的消息框显示。
这个函数仅在NT4.0以上版本被支持。在shell.dll中仍然存在一个16位版本的函数。
改进系统对话框的一般方法
Windows仍然在等待格式化驱动器函数的良好说明,一旦我们解决了怎样使用SHFormatDrive()函数的问题,我们也就满意了,但是,并不那么简单。例如,我们仍然感到奇怪的是为什么这个函数不允许编程设置磁盘标号—通过SetVolumeLabel()函数干这件事似乎不需要付出额外的努力,怎样做才使函数没有确认提示地静默运行,并且仅在需要的时候停止。为此,我们必须一直向前探索。由于SHFormatDrive()是一个基于对话框的函数,所以它提供了一个放置钩子的地方,而且我们可以改变其中任何东西。事实上,有这样操作的通用技术,它是:
在调用基于对话框的函数之前,如果你想要钩住它,安装WH_CBT钩子,因为建立在线程中的WC_DIALOG类型的第一个窗口是我们感兴趣的对话框。
在截取各种钩子事件时做你想要做的操作。
卸载钩子
钩子接收来自系统的指向CREATESTRUCT结构的指针,其中包含要建立窗口的信息。我们可以修改它的模板,子类化它或修改它的位置。WH_CBT钩子提供了众多的可能操作。
这一章的后面部分,我们将开发一个例程,它的对话框在下图中显示,以此来探索设置标号和静默运行SHFormatDrive()函数的技术。在这个对话框中,你所选择的选项将通过钩子的方法直接与SHFormatDrive()对话框通讯。
驱动器下拉框使你能选择要格式化的驱动器(仅仅可以是软盘),而OK按钮则开始这个操作。各种复选框允许用户指定期望函数提供的操作行为。在OK按钮下的标签显示函数返回值 ,而No Label,Show Summary,和Copy system files 复选框对应Windows9x下SHFormatDrive()对话框中的某些控件。正如你所看到的,在NT下情况稍有不同,我们将制造几个运行时情况显示到用户界面上。
扩展SHFormatDrive()函数的语法
作为我们示例应用的第一步,我们定义一个使用SHFormatDrive()作为主要内部操作的新函数,想象上应为FormatDrive()。这个函数有如下的原型:
int FormatDrive(HWND hWnd, int iDrive, int iCapacity,
int iType, LPFORMATDRIVESTRUCT lpfd);
附加的参数集中于FORMATDRIVESTRUCT结构之中:
struct FORMATDRIVESTRUCT
{
BOOL bShowSummary; // 在NT下无用
BOOL bNoLabel; // 在NT下无用
BOOL bCopySystemFiles; // 在NT下无用
BOOL bAutomatic; // 在NT下无用
TCHAR szLabel[11];
};
typedef FORMATDRIVESTRUCT* LPFORMATDRIVESTRUCT;
这些参数许多都模拟了SHFormatDrive()在Windows9x下所产生的对话框的设计,看上去象是:
bShowSummary, bNoLabel 和 bCopySystemFiles 成员匹配系统对话框中相似的复选框szLabel指向一个要求的新标号,而bAutomatic则表示静默和要求稍微有点自动的过程。最后一个属性在对话框中没有对应的控件,所以它完全是我们自己实现的。
Windows NT的对话框
不幸,由SHFormatDrive()函数在WindowsNT下产生的对话框是相当的不同,如下图所示:
不仅仅是少了几个控件,表示bShowSummary, bNoLabel 和 bCopySystemFiles的结构成员也没用了,而且‘标号’文字框的ID也不同了。Spy++ 展示它的ID是ID_NT_DLG_TEXTLABEL,而在Windows9x下是ID_DLG_TEXTLABEL。这种不一致性强迫我们来感觉应用正在运行的平台,并适当地执行条件代码。
格式化驱动器的自动函数
我们现在已经了解了我们的FormatDrive()函数必须做的事情。需要确定运行的操作系统和验证它所要格式化的驱动器是可删除的。然后在全程存储中保存附加的参数,以便钩子读出。此后,安装钩子,调用SHFormatDrive(),然后再卸载钩子:
HHOOK g_hHook = NULL; // CBT hook
BOOL g_bIsNT; // Are we on NT?
FORMATDRIVESTRUCT g_fd; // Other options
LRESULT CALLBACK CBTProc(int, WPARAM, LPARAM);
extern "C" DWORD WINAPI SHFormatDrive(HWND hwnd, UINT drive, UINT fmtID, UINT options);
// 调用标准函数SHFormatDrive()格式化驱动器
int FormatDrive(HWND hWnd, int iDrive,int iCapacity,
int iType, LPFORMATDRIVESTRUCT lpfd)
{
// 为后面的使用读平台参数
OSVERSIONINFO os;
os.dwOSVersionInfoSize = sizeof(OSVERSIONINFO);
GetVersionEx(&os);
g_bIsNT = (os.dwPlatformId == VER_PLATFORM_WIN32_NT);
// 检查驱动器类型
TCHAR sz[5] = {0};
wsprintf(sz, __TEXT("%c://"), 'A' + iDrive);
BOOL bIsFloppy = (GetDriveType(sz) == DRIVE_REMOVABLE);
if(!bIsFloppy)
return -3;
// 拷贝附加的参数到全程存储器
CopyMemory(&g_fd, lpfd, sizeof(FORMATDRIVESTRUCT));
// 安装钩子和调用函数
g_hHook = SetWindowsHookEx(WH_CBT, CBTProc, NULL, GetCurrentThreadId());
int irc = SHFormatDrive(hWnd, iDrive, iCapacity, iType);
UnhookWindowsHookEx(g_hHook);
return irc;
}
这显然是不完整的,因为除了一点基本的设置和检查以外,绝大多数实际的工作都推给了CBTProc()回调函数,这是由SetWindowsHookEx()和 UnhookWindowsHookEx()安装和卸载的函数。可以想象,在SHFormatDrive()函数的对话框要被显示的时候,调用CBTProc()函数,也正是在这一点上我们获得控制和制造变化。
设置卷标
你将看到我们使代码产生SHFormatDrive()对话框上的设置与我们在应用中指定的相匹配是一件相对容易的任务,但是还有某些其它的事情是CBTProc()需要处理的。
例如我们可以考虑强迫对话框设置一个新的磁盘标号,把期望的文字放入适当的编辑框是绝对简单的,不幸地是 这并不够,因为这个框的内容仅在它被编辑过后才被读出和采用。除非对话框的过程感觉到EN_CHANGE通知消息,否则它完全不理睬这个框。在我们手动改变这个框的文字时,自动发送这个消息,但是用软件做效果就不同了。所以SetDlgItemText()独自是不够的。为了使对话框相信文字已经改变,我们需要调用SendDlgItemMessage()函数,如下:
SetDlgItemText(hDlg, iLabelID, szLabel);
SendDlgItemMessage(hDlg, iLabelID, EM_SETMODIFY, TRUE, 0);
这里EM_SETMODIFY消息触发EN_CHANGE通知。
静默格式化
从编程的观点看SHFormatDrive()对话框的另一个问题是它总要求用户点击确认操作,理想的是我们能够跳过这个确认,因此我们能够通过简单地点击我们的应用按钮开始格式化。
要实现这个想法,软件就必须仿真点击SHFormatDrive()对话框的OK 按钮,这可以通过发布一个WM_COMMAND消息到对话框来实现:
PostMessage(hDlg, WM_COMMAND, IDOK, 0);
这里的复杂性在于:我们必须确保发布的消息是在窗口激活的第一时机。不幸的是当进度窗口关闭时HCBT_ACTIVATE事件也出现,此时格式化对话框返回到屏幕前。
为了编程关闭进度窗口(通过另一个消息),我们也需要一个方法来感觉格式化完成的时刻。这里的方法是用定时器频繁检查OK按钮的状态,看它是否被允许—在整个格式化过程中它都是被禁止的。为了知道什么时候销毁定时器,我们需要一个HCBT_DESTROYWND通知的钩子。
进一步的NT问题
很不幸,这个窍门在WindowsNT下不能工作,因为在格式化发生之前有进一步的确认。为了绕开这个确认,我们可以考虑寻找这个附加的窗口,但是现在,我们打算把这个在NT下不能做的操作增加到我们的应用操作列表中作为‘自动’操作。下面是CBTProc()的代码:
HWND g_hwndDlg; // 对话框HWND
UINT g_idTimer; // 定时器ID
void CALLBACK TimerProc(HWND, UINT, UINT, DWORD);
// CBT 钩子回调函数
LRESULT CALLBACK CBTProc(int iCode, WPARAM wParam, LPARAM lParam)
{
static BOOL bFirstTime = TRUE;
if(iCode < 0)
return CallNextHookEx(g_hHook, iCode, wParam, lParam);
// 关于激活对话框
if(iCode == HCBT_ACTIVATE)
{
// 取得对话框的 handle
g_hwndDlg = reinterpret_cast<HWND>(wParam);
// 设置标号编辑框
int iLabelID = (g_bIsNT ? ID_NT_DLG_TEXTLABEL : ID_DLG_TEXTLABEL);
SetDlgItemText(g_hwndDlg, iLabelID, g_fd.szLabel);
SendDlgItemMessage(g_hwndDlg, iLabelID, EM_SETMODIFY, TRUE, 0);
// 检查选项按钮
CHECK(GetDlgItem(g_hwndDlg, ID_DLG_SHOWSUMMARY), g_fd.bShowSummary);
CHECK(GetDlgItem(g_hwndDlg, ID_DLG_NOLABEL), g_fd.bNoLabel);
CHECK(GetDlgItem(g_hwndDlg, ID_DLG_BOOTABLE), g_fd.bCopySystemFiles);
// 如果不是第一次,必须跳过
if(g_fd.bAutomatic && bFirstTime)
{
// 仿真点击开始按钮
bFirstTime = FALSE;
PostMessage(g_hwndDlg, WM_COMMAND, IDOK, 0);
// 设置定时器来感觉格式化终止
g_idTimer = SetTimer(NULL, 1, 1000, TimerProc);
}
}
// 关于销毁对话框
if(iCode == HCBT_DESTROYWND)
{
// 重置第一次标志和停止定时器
bFirstTime = TRUE;
if(g_fd.bAutomatic)
KillTimer(NULL, g_idTimer);
}
return CallNextHookEx(g_hHook, iCode, wParam, lParam);
}
传递到回调函数的wParam参数是一个被激活窗口的Handle,即对话框。一旦获得了这个Handle,修改对话框空间的内容就相当直接了。你仅仅需要知道这个标准对话框控件的ID(这是Spy++的工作):
// Windows 9x 标准对话框控件的 IDs
const int ID_DLG_TEXTLABEL = 0x26; // 标号编辑框
const int ID_DLG_NOLABEL = 0x27; // "无标号" 复选框
const int ID_DLG_BOOTABLE = 0x28; // "系统文件"复选框
const int ID_DLG_SHOWSUMMARY = 0x29; // "显示摘要"复选框
// NT4 标准对话框控件 IDs
const int ID_NT_DLG_TEXTLABEL = 0x7007; // 标号编辑框
然后编写一个宏CHECK(),减轻某些重复调用PostMessage()的操作:
// 快速发布消息的宏
#define CHECK(h,b) PostMessage(h, BM_SETCHECK, (b ? BST_CHECKED : BST_UNCHECKED), 0)
最后是TimerProc()回调函数:
// 定时器回调函数
void CALLBACK TimerProc(HWND hwnd, UINT uMsg, UINT idEvent, DWORD dwTime)
{
HWND hwndOK = GetDlgItem(g_hwndDlg, IDOK);
// 仿真按下关闭按钮
if(IsWindowEnabled(hwndOK))
PostMessage(g_hwndDlg, WM_COMMAND, IDCANCEL, 0);
}
正如你将要看到的,它监视OK按钮是否变成允许,然后解除这个对话框。
示例程序
现在剩下的就是生成对话框应用。象我们前面给出的,使用FormatDrive()完成格式化操作。只要添加了我们开发的所有函数,你只需要完善OnInitDialog()和 OnOK()函数的代码就可以了:
#include "resource.h"
void OnInitDialog(HWND hDlg)
{
// 读平台数据...
OSVERSIONINFO os;
os.dwOSVersionInfoSize = sizeof(OSVERSIONINFO);
GetVersionEx(&os);
BOOL bIsNT = (os.dwPlatformId == VER_PLATFORM_WIN32_NT);
// 如果NT 放弃某些选项
if(bIsNT)
{
EnableWindow(GetDlgItem(hDlg, IDC_SUMMARY), FALSE);
EnableWindow(GetDlgItem(hDlg, IDC_NOLABEL), FALSE);
EnableWindow(GetDlgItem(hDlg, IDC_COPYSYSTEMFILES), FALSE);
EnableWindow(GetDlgItem(hDlg, IDC_AUTOMATIC), FALSE);
}
// 填充驱动器列表
HWND hwndCbo = GetDlgItem(hDlg, IDC_DRIVE);
ComboBox_AddString(hwndCbo, __TEXT(" A:"));
ComboBox_AddString(hwndCbo, __TEXT(" B:"));
ComboBox_AddString(hwndCbo, __TEXT(" C:"));
ComboBox_AddString(hwndCbo, __TEXT(" D:"));
ComboBox_AddString(hwndCbo, __TEXT(" E:"));
ComboBox_SetCurSel(hwndCbo, 0);
// 设置图标(T/F 大/小图标)
SendMessage(hDlg, WM_SETICON, FALSE, reinterpret_cast<LPARAM>(g_hIconSmall));
SendMessage(hDlg, WM_SETICON, TRUE, reinterpret_cast<LPARAM>(g_hIconLarge));
}
void OnOK(HWND hDlg)
{
HWND hwndCbo = GetDlgItem(hDlg, IDC_DRIVE);
int iDrive = ComboBox_GetCurSel(hwndCbo);
FORMATDRIVESTRUCT fd;
ZeroMemory(&fd, sizeof(FORMATDRIVESTRUCT));
fd.bNoLabel = (IsDlgButtonChecked(hDlg, IDC_NOLABEL) == BST_CHECKED);
fd.bShowSummary = (IsDlgButtonChecked(hDlg, IDC_SUMMARY) == BST_CHECKED);
fd.bCopySystemFiles = (IsDlgButtonChecked(hDlg, IDC_ COPYSYSTEMFILES) ==
BST_CHECKED);
fd.bAutomatic = (IsDlgButtonChecked(hDlg, IDC_AUTOMATIC) == BST_CHECKED);
GetDlgItemText(hDlg, IDC_EDIT, fd.szLabel, 11);
int irc = FormatDrive(hDlg, iDrive, -1, 0, &fd);
TCHAR szBuf[MAX_PATH] = {0};
wsprintf(szBuf, __TEXT("%d"), irc);
SetDlgItemText(hDlg, IDC_ERRCODE, szBuf);
}
最后要说的是:OnOK()从对话框取值,然后绑定到FORMATDRIVESTRUCT结构,调用FormatDrive()函数和输出返回值。还有比这更容易的吗。
小结
这一章主要介绍了Windows9x Shell的一些新函数。事实上它们的新不是因为它们最近才被增加,而是因为它们不被广泛地了解。在这一章中我们测试了:
怎样取得一般执行文件的版本号
回收站相关API函数
Shell轻量级实用API函数
没有资料说明的驱动器格式化函数
钩子和客户化系统对话框的一般技术
到目前为止我们已经测试了Shell API的特殊部分,这一章中我们概览了非主流方面的函数。从下一章开始,我们将深入探测器内部,挖掘它的对象,注册表设置和客户化层次。我们将检测传统的部件如‘控制面板’,‘我的公文包’和‘打印机’,以及某些新的东西。其中最值得注意的就是脚本化Shell对象和Windows脚本环境。
上文来自:http://blog.csdn.net/chchzh/article/details/2346572