第10章 通用对话框
通用对话框库包含一组用于执行常见应用程序任务的对话框,例如打开文件、选择颜色和打印文档等,通用对话框实现了应用程序用户界面的一致性,例如单击不同程序的文件菜单下的打开文件子菜单项,通常都会弹出相同的打开文件对话框。要弹出打开文件、选择颜色和打印文档等通用对话框用的是不同的函数,在使用这些函数时,基本上都要初始化一个结构的一些字段并将该结构的指针传递给这些函数。函数会创建并显示相应的对话框,当用户关闭对话框时,函数将控制权返还给程序,然后程序可以从先前传递给函数的结构中获取需要的信息。
常见的通用对话框以及创建通用对话框所需的函数和所用结构如表10.1所示。
表10.1
| 打开文件 | 用户可以在对话框中输入或选择要打开的文件名,还包括一个文件扩展名列表以过滤显示的文件名 | GetOpenFileName | OPENFILENAME |
| 保存文件 | 用户可以在对话框中输入或选择用于保存的文件名,还包括一个文件扩展名列表以过滤显示的文件名 | GetSaveFileName | OPENFILENAME |
| 查找 | 用户可以在对话框中输入要查找的字符串,还可以指定搜索选项,例如搜索方向以及是否区分大小写 | FindText | FINDREPLACE |
| 替换 | 用户可以在对话框中输入要查找的字符串和替换字符串,还可以指定搜索选项,例如是否区分大小写以及替换选项 | ReplaceText | FINDREPLACE |
| 选择字体 | 用户可以在对话框的字体列表中选择一个字体及其样式、磅值和其他字体属性,例如字体颜色、下划线、删除线 | ChooseFont | CHOOSEFONT |
| 选择颜色 | 用户可以选择基本颜色或自定义颜色 | ChooseColor | CHOOSECOLOR |
| 页面设置 | 用户可以选择页面配置选项,例如纸张方向、大小、来源和边距 | PageSetupDlg | PAGESETUPDLG |
| 打印 | 显示已安装的打印机及其配置信息,用户可以选择打印作业选项,例如要打印的页面范围和份数,然后开始打印过程 | PrintDlg | PRINTDLG |
除“查找”和“替换”对话框以外,其他通用对话框都是模态的。“查找”和“替换”对话框则是非模态对话框,如果使用“查找”和“替换”对话框,还应该在程序的主消息循环中使用IsDialogMessage函数,以确保“查找”和“替换”对话框正确处理键盘输入(例如Tab和Esc键)。
10.1 打开和保存文件
用于创建打开文件对话框的函数是GetOpenFileName:
BOOL WINAPI GetOpenFileName(_Inout_ LPOPENFILENAME lpofn);
参数lpofn是一个指向OPENFILENAME结构的指针,调用GetOpenFileName函数前需要初始化该结构的一些字段,当函数返回时会填充该结构的相关字段,结构中包含用户所选择文件的信息。如果用户输入或选择了一个文件名并单击“打开”按钮,则函数返回值为TRUE,OPENFILENAME结构的lpstrFile字段指向的缓冲区包含用户选择的文件名(完整路径);如果用户单击了“取消”按钮或关闭了对话框或发生错误,则返回值为FALSE。
GetOpenFileName函数只是返回用户所选择文件名的信息,至于如何打开这个文件并读写文件内容,还需要用户自己写代码去操作,例如记事本程序在用户单击打开菜单以后会调用GetOpenFileName函数获取到文件名,然后额外写代码打开这个文本文件,读取文本内容并显示到多行编辑控件中。
OPENFILENAME结构在commdlg.h头文件中定义如下:
typedef struct tagOFN {
DWORD lStructSize; // 该结构的大小
HWND hwndOwner; // 对话框的拥有者窗口句柄
HINSTANCE hInstance; // 用于自定义对话框,指定包含对话框模板的模块句柄
LPCTSTR lpstrFilter; // 文件扩展名过滤字符串
LPTSTR lpstrCustomFilter;
DWORD nMaxCustFilter;
DWORD nFilterIndex; // 过滤器索引(从1开始),设置为1表示默认显示第1个过滤器
LPTSTR lpstrFile; // 返回用户所选择的文件名(完整路径)的缓冲区
DWORD nMaxFile; // lpstrFile缓冲区的大小,以字符为单位
LPTSTR lpstrFileTitle; // 返回用户所选择文件的文件名和扩展名(不包括路径)的缓冲区
DWORD nMaxFileTitle; // lpstrFileTitle缓冲区的大小,以字符为单位
LPCTSTR lpstrInitialDir; // 初始目录字符串指针,对话框显示以后默认显示的目录
LPCTSTR lpstrTitle; // 在对话框标题栏中显示的字符串
DWORD Flags; // 标志位
WORD nFileOffset; // lpstrFile指向的字符串中的文件名部分从0开始的字符偏移量
WORD nFileExtension; // lpstrFile指向的字符串中的扩展名部分从0开始的字符偏移量
LPCTSTR lpstrDefExt; // 默认扩展名字符串
LPARAM lCustData;
LPOFNHOOKPROC lpfnHook;
LPCTSTR lpTemplateName; // 自定义对话框模板名称
void *pvReserved;
DWORD dwReserved;
DWORD FlagsEx;
} OPENFILENAME, *LPOPENFILENAME;
常用的字段解释如下。
-
lStructSize字段指定该结构的大小。
-
hwndOwner字段指定对话框的拥有者窗口句柄。
-
lpstrFilter字段指定文件扩展名过滤字符串列表,显示在对话框右下角的文件类型组合框中。每一项由两个字符串组成。第1个字符串是过滤器描述字符串,例如“文本文件(*.txt)\\0”;第2个字符串指定过滤器模式,例如“*.txt\\0”。要为一项指定多个过滤器模式,需要使用分号分隔过滤器模式,例如“*.txt;*.doc;*.docx\\0”,不要在过滤器模式字符串中包含空格。文件扩展名过滤字符串列表的最后一个字符串必须以两个空字符结束。如果lpstrFilter字段为NULL,则对话框不显示任何过滤器,此时可以选择任何扩展名的文件。下面的文件扩展名过滤字符串列表定义了两项:
ofn.lpstrFilter = TEXT("文本文件(*.txt, *.doc, *.docx)\\0*.txt;*.doc;*.docx\\0All(*.*)
\\0*. *\\0");
用" "定义的字符串结尾会自动添加一个“\\0”,因此双引号内部最后一个字符串后面只需要一个“\\0”。
-
文件扩展名过滤字符串列表的每一项都有一个索引,从1开始,nFilterIndex字段指定默认显示哪一项,例如设置为1表示默认显示第1个过滤器。GetOpenFileName函数返回后,nFilterIndex字段被设置为用户选择的过滤器的索引。
-
lpstrFile字段是返回用户所选择文件名(完整路径)的缓冲区,该缓冲区也用于初始化对话框中编辑控件所显示的文件名。如果不需要初始化,则缓冲区的第1个字符必须设置为NULL。
如果Flags字段指定了OFN_ALLOWMULTISELECT标志并且用户选择了多个文件,则缓冲区包含当前目录,后跟所有所选文件的文件名。对于资源管理器样式的对话框,目录和每个文件名字符串以NULL分隔,在最后一个文件名后面有一个额外的NULL字符。如果缓冲区太小,则函数返回FALSE,调用CommDlgExtendedError函数获取错误码会返回FNERR_BUFFERTOOSMALL。在这种情况下,lpstrFile缓冲区的前2字节包含所需的大小,以字符为单位。例如原书的图10.1中打开了6个文件。
lpstrFile字段返回的内容如原书的图10.2所示。
目录名末尾通常没有“\\”字符(在根目录下选择多个文件时会包括“\\”字符)。如果需要把目录名和每一个文件名拼接成一个带完整路径的文件名,需要做一些处理,具体请看后面的示例程序。
-
nMaxFile字段指定lpstrFile缓冲区的大小,以字符为单位。
-
lpstrFileTitle字段是返回用户所选文件的文件名和扩展名的缓冲区。
-
nMaxFileTitle字段指定lpstrFileTitle缓冲区的大小,以字符为单位。
-
lpstrInitialDir字段指定初始目录,也就是对话框显示以后默认显示的目录。
-
lpstrTitle字段指定在对话框标题栏中显示的字符串。如果该字段为NULL,则系统使用默认标题(“打开”或“另存为(GetSaveFileName)”)。
-
Flags字段是标志位,常用的标志如表10.2所示。
表10.2
| OFN_EXPLORER | 默认值,对话框使用资源管理器样式的用户界面。如果指定了OFN_ALLOWMULTISELECT多选标志,则必须指定该标志才可以使用资源管理器样式的用户界面 |
| OFN_FILEMUSTEXIST | 用户只能在文件名编辑控件中输入一个已经存在的文件名,如果用户输入了一个当前所选目录中不存在的文件名,则系统会提示文件不存在,使用这个标志的时候必须同时指定 OFN_PATHMUSTEXIST标志 |
| OFN_PATHMUSTEXIST | 用户只能在文件名编辑控件中输入一个已经存在的路径 |
| OFN_ALLOWMULTISELECT | 用户可以选择多个文件 |
| OFN_CREATEPROMPT | 如果用户在文件名编辑控件中输入了一个不存在的文件,系统会弹出对话框提示用户是否新建这个文件,如果用户选择了“是”按钮,对话框关闭并且函数返回所指定的名字 |
| OFN_OVERWRITEPROMPT | 保存文件时,如果用户在文件名编辑控件中输入了一个已经存在的文件名,系统会提示用户是否覆盖已经存在的文件 |
-
nFileOffset字段返回lpstrFile指向的字符串中的文件名部分从0开始的字符偏移量。
-
nFileExtension字段返回lpstrFile指向的字符串中的扩展名部分从0开始的字符偏移量,扩展名是文件名字符串中最后一个点(.)之后的子字符串。例如对于文件名“C:\\dir1\\dir2\\file.ext”,nFileOffset字段返回13(file.ext),nFileExtension字段返回18(ext)。
-
lpstrDefExt字段是默认扩展名字符串。如果用户没有在编辑控件中输入扩展名,则会将默认扩展名附加到文件名字符串中,扩展名字符串不需要包含点(.)。
用于创建保存文件对话框的函数是GetSaveFileName:
BOOL WINAPI GetSaveFileName(_Inout_ LPOPENFILENAME lpofn);
参数lpofn同样是一个指向OPENFILENAME结构的指针。调用GetSaveFileName函数前需要初始化该结构的一些字段,当函数返回时会填充该结构的相关字段,结构中包含用户选择的文件的信息。如果用户输入或选择了一个文件名并单击“保存”按钮,则返回值为TRUE,OPENFILENAME结构的lpstrFile字段指向的缓冲区包含用户选择的文件名(完整路径);如果用户单击了“取消”按钮或关闭了对话框或发生错误,则返回值为FALSE。
保存文件对话框让用户选择一个要保存的文件名,函数返回所选择的文件名的信息。保存文件需要程序额外书写代码。
下面实现一个打开文件(支持多选)、保存文件对话框的程序OpenSaveFile:
#include <windows.h>
#include <tchar.h>
#include <strsafe.h>
#include "resource.h"
#pragma comment(linker,"\\"/manifestdependency:type='win32' \\
name='Microsoft.Windows.Common-Controls' version='6.0.0.0' \\
processorArchitecture='*' publicKeyToken='6595b64144ccf1df' language='*'\\"")
// 全局变量
HINSTANCE g_hInstance;
// 函数声明
INT_PTR CALLBACK DialogProc(HWND hwndDlg, UINT uMsg, WPARAM wParam, LPARAM lParam);
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int
nCmdShow)
{
g_hInstance = hInstance;
// 创建模态对话框
DialogBoxParam(hInstance, MAKEINTRESOURCE(IDD_MAIN), NULL, DialogProc, NULL);
return 0;
}
INT_PTR CALLBACK DialogProc(HWND hwndDlg, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
static HWND hwndList;
TCHAR szFile[MAX_PATH * 512] = { 0 }; // 返回用户选择的文件名的缓冲区大一点,本程序允许多选
TCHAR szFileTitle[MAX_PATH] = { 0 }; // 返回用户所选文件的文件名和扩展名的缓冲区
OPENFILENAME ofn = { 0 };
ofn.lStructSize = sizeof(ofn);
ofn.hwndOwner = hwndDlg;
ofn.lpstrFilter =
TEXT("文本文件(*.txt, *.doc, *.docx)\\0*.txt;*.doc;*.docx\\0All(*.*)\\ 0*.*\\0");
ofn.nFilterIndex = 1; // 默认选择第1个过滤器
ofn.lpstrFile = szFile; // 返回用户选择的文件名的缓冲区
ofn.lpstrFile[0] = NULL; // 不需要初始化文件名编辑控件
ofn.nMaxFile = _countof(szFile);
ofn.lpstrFileTitle = szFileTitle; // 返回用户选择的文件的文件名和扩展名的缓冲区
ofn.nMaxFileTitle = _countof(szFileTitle);
ofn.lpstrInitialDir = TEXT("C:\\\\"); // 初始目录
LPTSTR lpStr;
TCHAR szDir[MAX_PATH] = { 0 };
TCHAR szBuf[MAX_PATH] = { 0 };
switch (uMsg)
{
case WM_INITDIALOG:
hwndList = GetDlgItem(hwndDlg, IDC_LIST_FILENAME);
return TRUE;
case WM_COMMAND:
switch (LOWORD(wParam))
{
case IDC_BTN_OPEN:
ofn.lpstrTitle = TEXT("请选择要打开的文件");// 对话框标题栏中显示的字符串
ofn.Flags = OFN_EXPLORER | OFN_PATHMUSTEXIST | OFN_FILEMUSTEXIST | OFN_ CREATEPROMPT | OFN_ALLOWMULTISELECT;
if (GetOpenFileName(&ofn))
{
// 先清空列表框
SendMessage(hwndList, LB_RESETCONTENT, 0, 0);
lpStr = ofn.lpstrFile + _tcslen(ofn.lpstrFile) + 1;
if (lpStr[0] == NULL)
{
// 用户只选择了一个文件
SendMessage(hwndList, LB_ADDSTRING, 0, (LPARAM)ofn.lpstrFile);
}
else
{
// 用户选择了多个文件
StringCchCopy(szDir, _countof(szDir), ofn.lpstrFile);
if (szDir[_tcslen(szDir) – 1] != TEXT('\\\\'))
StringCchCat(szDir, _countof(szDir), TEXT("\\\\"));
while (lpStr[0] != NULL)
{
StringCchCopy(szBuf, _countof(szBuf), szDir);
StringCchCat(szBuf, _countof(szBuf), lpStr);
SendMessage(hwndList, LB_ADDSTRING, 0, (LPARAM)szBuf);
lpStr += _tcslen(lpStr) + 1;
}
}
}
break;
case IDC_BTN_SAVE:
ofn.lpstrTitle = TEXT("请选择要保存的文件名"); // 对话框标题栏中显示的字符串
ofn.lpstrDefExt = TEXT("txt"); // 默认扩展名
ofn.Flags = OFN_EXPLORER | OFN_OVERWRITEPROMPT;
if (GetSaveFileName(&ofn))
{
SendMessage(hwndList, LB_RESETCONTENT, 0, 0);
SendMessage(hwndList, LB_ADDSTRING, 0, (LPARAM)ofn.lpstrFile);
}
break;
case IDCANCEL:
EndDialog(hwndDlg, 0);
break;
}
return TRUE;
}
return FALSE;
}
具体代码参见Chapter10\\OpenSaveFile项目。
10.2 浏览文件夹与遍历目录
要想让用户选择一个文件夹,可以调用SHBrowseForFolder函数:
PIDLIST_ABSOLUTE SHBrowseForFolder(_In_ LPBROWSEINFO lpbi);
函数返回值为PIDLIST_ABSOLUTE数据类型,在shtypes.h头文件中定义如下:
#define PIDLIST_ABSOLUTE LPITEMIDLIST
typedef ITEMIDLIST *LPITEMIDLIST;
typedef struct _ITEMIDLIST
{
SHITEMID mkid;
} ITEMIDLIST;
typedef struct _SHITEMID
{
USHORT cb;
BYTE abID[ 1 ];
} SHITEMID;
返回值类型PIDLIST_ABSOLUTE指定所选文件夹相对于命名空间根目录的位置。如果用户在对话框中单击了“取消”按钮、关闭了对话框或发生了错误,则返回值为NULL。
lpbi参数是一个指向BROWSEINFO结构的指针,BROWSEINFO结构在ShlObj.h头文件中定义如下:
typedef struct _browseinfo {
HWND hwndOwner; // 对话框的拥有者窗口句柄
PCIDLIST_ABSOLUTE pidlRoot; // 开始浏览的根文件夹的位置,不需要可以设置为NULL
LPTSTR pszDisplayName;// 返回用户选择的文件夹名称的缓冲区
LPCTSTR lpszTitle; // 显示在对话框上部静态控件中的文字
UINT ulFlags; // 标志位
BFFCALLBACK lpfn;
LPARAM lParam;
int iImage;
} BROWSEINFO, *PBROWSEINFO, *LPBROWSEINFO;
ulFlags字段指定标志位,常用的标志如表10.3所示。
表10.3
| BIF_RETURNONLYFSDIRS | 如果用户选择不属于文件系统的文件夹(例如网络、家庭组),则对话框的“确定”按钮将显示为灰色 |
| BIF_NEWDIALOGSTYLE | 使用新的用户界面,新的用户界面具有多项新功能 |
| BIF_EDITBOX | 在对话框中包含一个编辑控件,允许用户输入文件夹名称 |
| BIF_USENEWUI | 使用新用户界面,对话框中包含一个编辑控件,相当于`BIF_NEWDIALOGSTYLE |
| BIF_BROWSEINCLUDEFILES | 对话框中显示文件和文件夹,通常不设置该标志 |
| BIF_BROWSEINCLUDEURLS | 对话框可以显示URL快捷方式,还必须设置BIF_USENEWUI和BIF_BROWSEINCLUDEFILES标志,通常不设置该标志 |
SHBrowseForFolder函数返回一个PIDLIST_ABSOLUTE类型值,即指向ITEMIDLIST结构的指针。不必深究这个结构,因为使用SHGetPathFromIDList函数可以很方便地将它转换成目录名称字符串(完整路径):
BOOL SHGetPathFromIDList(
_In_ PCIDLIST_ABSOLUTE pidl, // 指向ITEMIDLIST结构的指针
_Out_ LPTSTR pszPath);// 返回目录名称的缓冲区
要遍历一个目录中的子目录或文件,首先调用FindFirstFile函数。该函数返回一个HANDLE类型的查找句柄hFindFile,并返回找到的第一个子目录或文件的信息;如果FindFirstFile函数执行成功,则接下来可以利用hFindFile句柄循环调用FindNextFile函数继续查找其他目录或文件,直到FindNextFile函数返回FALSE为止;最后调用FindClose函数关闭hFindFile查找句柄。使用这几个函数查找文件的代码通常如下所示:
WIN32_FIND_DATA fd = { 0 };
// 遍历目录
hFindFile = FindFirstFile(szDir, &fd);
if (hFindFile != INVALID_HANDLE_VALUE)
{
do
{
// 处理本次找到的文件
} while (FindNextFile(hFindFile, &fd));
FindClose(hFindFile);
}
FindFirstFile函数用于在目录中搜索指定名称的子目录或文件:
HANDLE WINAPI FindFirstFile(
_In_ LPCTSTR lpFileName, // 要查找的文件名,文件名可以包含通配符,例如*或?
_Out_ LPWIN32_FIND_DATA lpFindFileData);// 指向查找结构WIN32_FIND_DATA的指针
- lpFileName参数指定要查找的文件名。如果文件名中不包含路径,那么将在当前目录中查找,包含路径的话将在指定路径中查找。在文件名中可以使用通配符“*”或“?”指定查找符合指定特征的文件。下面是文件名格式的几个示例:
C:\\Windows\\*.* // 在C:\\Windows目录中查找所有类型的文件
C:\\Windows\\System32\\*.dll // 在C:\\Windows\\System32目录中查找所有扩展名为.dll的文件
C:\\Windows\\System.ini // 在C:\\Windows目录中查找System.ini文件
C:\\Windows\\a???.* // 在C:\\Windows目录中查找所有以a开头的长度为4个字符的任何文件
Test.dat // 在当前目录中查找Test.dat文件
*.* // 在当前目录中查找所有文件
- lpFindFileData参数是一个指向查找结构WIN32_FIND_DATA的指针。该结构返回找到的目录或文件的信息,在minwinbase.h头文件中定义如下:
typedef struct _WIN32_FIND_DATA {
DWORD dwFileAttributes; // 文件系统属性
FILETIME ftCreationTime; // FILETIME格式的文件创建时间
FILETIME ftLastAccessTime; // FILETIME格式的最后访问时间
FILETIME ftLastWriteTime; // FILETIME格式的最后修改时间
DWORD nFileSizeHigh; // 文件大小的高32位DWORD值,以字节为单位
DWORD nFileSizeLow; // 文件大小的低32位DWORD值,以字节为单位
DWORD dwReserved0; // 保留字段
DWORD dwReserved1; // 保留字段
TCHAR cFileName[MAX_PATH]; // 文件名称(不包括路径)
TCHAR cAlternateFileName[14];// 该文件的替代名称,8.3文件名格式,通常用不到
} WIN32_FIND_DATA, *PWIN32_FIND_DATA, *LPWIN32_FIND_DATA;
dwFileAttributes字段包含文件系统属性,通过这个字段可以检查找到的究竟是一个子目录还是一个文件,以及其他文件系统属性,常见的属性如表10.4所示。
表10.4
| FILE_ATTRIBUTE_NORMAL | 普通文件 |
| FILE_ATTRIBUTE_DIRECTORY | 找到的是一个目录 |
| FILE_ATTRIBUTE_READONLY | 只读文件 |
| FILE_ATTRIBUTE_TEMPORARY | 用于临时存储的文件 |
| FILE_ATTRIBUTE_HIDDEN | 隐藏文件或目录 |
| FILE_ATTRIBUTE_SYSTEM | 操作系统使用的文件或目录 |
| FILE_ATTRIBUTE_ARCHIVE | 存档文件或目录 |
| FILE_ATTRIBUTE_COMPRESSED | 压缩的文件或目录 |
| FILE_ATTRIBUTE_ENCRYPTED | 已加密的文件或目录 |
如果FindFirstFile函数执行成功,则返回值是在后续调用FindNextFile或FindClose时使用的查找句柄,而lpFindFileData参数指向的WIN32_FIND_DATA结构包含找到的第一个目录或文件的信息;如果函数执行失败或无法从lpFileName参数指定的搜索字符串中找到目录或文件,则返回值为INVALID_HANDLE_VALUE(−1),这种情况下的lpFindFileData参数指向的结构的内容不确定。
FindNextFile函数以查找句柄和查找结构为参数继续查找目录或文件:
BOOL WINAPI FindNextFile(
_In_ HANDLE hFindFile, // 查找句柄
_Out_ LPWIN32_FIND_DATA lpFindFileData);// 指向查找结构WIN32_FIND_DATA的指针
如果函数执行成功,则返回值为TRUE,lpFindFileData参数指向的结构包含找到的下一个目录或文件的信息;如果函数执行失败,则返回值为FALSE,这种情况下的lpFindFileData参数指向的结构的内容不确定。
例如下面的代码,调用SHBrowseForFolder函数创建一个浏览文件夹对话框,然后调用SHGetPathFromIDList函数把上一个函数的返回值转换为完整目录名称字符串,拼接一个查找字符串szSearch用于FindFirstFile函数,接着就是FindNextFile(hFindFile, &fd)的循环调用。如果找到的是一个目录,那么应该递归往下一层找。这有点麻烦,本例中仅处理了找到的各种文件,例如普通文件、临时文件、隐藏文件等,后面会有递归查找的示例。代码如下:
#include <windows.h>
#include <Shlobj.h>
#include <tchar.h>
#include <strsafe.h>
#include "resource.h"
#pragma comment(linker,"\\"/manifestdependency:type='win32' \\
name='Microsoft.Windows.Common-Controls' version='6.0.0.0' \\
processorArchitecture='*' publicKeyToken='6595b64144ccf1df' language='*'\\"")
// 全局变量
HINSTANCE g_hInstance;
// 函数声明
INT_PTR CALLBACK DialogProc(HWND hwndDlg, UINT uMsg, WPARAM wParam, LPARAM lParam);
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int
nCmdShow)
{
g_hInstance = hInstance;
// 创建模态对话框
DialogBoxParam(hInstance, MAKEINTRESOURCE(IDD_MAIN), NULL, DialogProc, NULL);
return 0;
}
INT_PTR CALLBACK DialogProc(HWND hwndDlg, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
static HWND hwndList;
PIDLIST_ABSOLUTE pItemIdList; // SHBrowseForFolder函数返回值
BROWSEINFO bi = { 0 };
bi.hwndOwner = hwndDlg;
bi.lpszTitle = TEXT("请选择一个文件夹");
bi.ulFlags = BIF_USENEWUI | BIF_RETURNONLYFSDIRS;
TCHAR szDir[MAX_PATH] = { 0 }; // SHGetPathFromIDList函数返回的目录名称的缓冲区
HANDLE hFindFile;
WIN32_FIND_DATA fd = { 0 };
TCHAR szSearch[MAX_PATH] = { 0 };
TCHAR szDirFile[MAX_PATH] = { 0 };
switch (uMsg)
{
case WM_INITDIALOG:
hwndList = GetDlgItem(hwndDlg, IDC_LIST_FILENAME);
return TRUE;
case WM_COMMAND:
switch (LOWORD(wParam))
{
case IDC_BTN_BROWSE:
pItemIdList = SHBrowseForFolder(&bi);
if (pItemIdList)
{
// 用户选择的文件夹名称显示到静态控件中
SHGetPathFromIDList(pItemIdList, szDir);
SetDlgItemText(hwndDlg, IDC_STATIC_DIR, szDir);
if (szDir[_tcslen(szDir) – 1] != TEXT('\\\\'))
StringCchCat(szDir, _countof(szDir), TEXT("\\\\"));
// 拼接搜索字符串
StringCchCopy(szSearch, _countof(szSearch), szDir);
StringCchCat(szSearch, _countof(szSearch), TEXT("*.*"));
// 遍历目录
hFindFile = FindFirstFile(szSearch, &fd);
if (hFindFile != INVALID_HANDLE_VALUE)
{
// 先清空列表框
SendMessage(hwndList, LB_RESETCONTENT, 0, 0);
do
{
// 处理本次找到的各种文件,没有处理找到的目录
if (!(fd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY))
{
StringCchCopy(szDirFile, _countof(szDirFile), szDir);
StringCchCat(szDirFile, _countof(szDirFile), fd.cFileName);
SendMessage(hwndList, LB_ADDSTRING, 0, (LPARAM)szDirFile);
}
} while (FindNextFile(hFindFile, &fd));
// 关闭查找句柄
FindClose(hFindFile);
}
}
break;
case IDCANCEL:
EndDialog(hwndDlg, 0);
break;
}
return TRUE;
}
return FALSE;
}
具体代码参见Chapter10\\SHBrowseForFolder项目。程序运行效果如图10.3所示。
10.3 查找和替换
“查找”和“替换”对话框的样式通常如原书的图10.4所示。
创建查找对话框的函数是FindText,用户可以在对话框中输入要搜索的字符串和相关搜索选项;创建替换对话框的函数是ReplaceText,用户可以在对话框中输入要搜索的字符串和替换的字符串,以及控制查找和替换操作的选项。
HWND WINAPI FindText(_In_ LPFINDREPLACE lpfr);
HWND WINAPI ReplaceText(_Inout_ LPFINDREPLACE lpfr);
“查找”和“替换”对话框都是非模态对话框。如果函数执行成功,则返回值是对话框的窗口句柄,可以通过该句柄与父窗口进行通信;如果函数执行失败,则返回值为NULL。
参数lpfr是一个指向FINDREPLACE结构的指针,先看一下该结构的定义,再介绍“查找”和“替换”对话框的工作原理。该结构在commdlg.h头文件中定义如下:
typedef struct {
DWORD lStructSize; // 该结构的大小
HWND hwndOwner; // 对话框的拥有者窗口句柄,不能为NULL
HINSTANCE hInstance;
DWORD Flags; // 标志位
LPTSTR lpstrFindWhat; //“查找内容”编辑控件中的字符串缓冲区,缓冲区大小至少为80个字符
LPTSTR lpstrReplaceWith;//“替换为”编辑控件中的字符串缓冲区,缓冲区大小至少为80个字符
WORD wFindWhatLen; // lpstrFindWhat字段指向的缓冲区的长度,以字节为单位
WORD wReplaceWithLen; // lpstrReplaceWith字段指向的缓冲区的长度,以字节为单位
LPARAM lCustData;
LPFRHOOKPROC lpfnHook;
LPCTSTR lpTemplateName;
} FINDREPLACE, *LPFINDREPLACE;
Flags字段是标志位,可以是标志的组合(常用的),如表10.5所示。
表10.5
| FR_FINDNEXT | 单击了“查找下一个”按钮 |
| FR_REPLACE | 单击了“替换”按钮 |
| FR_REPLACEALL | 单击了“全部替换”按钮 |
| FR_DOWN | 已选中搜索方向的“向下”单选按钮,指示应从当前位置搜索到文档末尾;否则就是已选中“向上”单选按钮,指示应从当前位置搜索到文档开头 |
| FR_MATCHCASE | 已选中“匹配大小写”复选框 |
| FR_WHOLEWORD | 已选中“全字匹配”复选框 |
| FR_HIDEUPDOWN | 隐藏搜索方向的“向上”和“向下”单选按钮 |
| FR_HIDEMATCHCASE | 隐藏“匹配大小写”复选框 |
| FR_HIDEWHOLEWORD | 隐藏“全字匹配”复选框 |
| FR_NOUPDOWN | 禁用搜索方向的“向上”和“向下”单选按钮 |
| FR_NOMATCHCASE | 禁用“匹配大小写”复选框 |
| FR_NOWHOLEWORD | 禁用“全字匹配”复选框 |
| FR_DIALOGTERM | 对话框正在关闭 |
在调用FindText和ReplaceText函数后,系统根据FINDREPLACE结构中字段的设置初始化对话框,例如根据Flags字段的值设置对话框中相关控件的选中、禁用或隐藏状态,lpstrFindWhat字段指向的缓冲区的内容会显示到“查找内容”编辑控件中,lpstrReplaceWith字段指向的缓冲区的内容会显示到“替换为”编辑控件中(查找对话框不需要该字段)。
“查找”和“替换”对话框都是非模态对话框。在调用FindText和ReplaceText函数后,系统显示对话框并马上返回,对话框保持显示状态,直到用户关闭对话框。如果用户按下了对话框中的某个按钮,则对话框设置FINDREPLACE结构的相关字段并通过“查找替换消息”通知父窗口的窗口过程,程序中处理查找和替换的功能集中在这个“查找替换消息”中完成。在该消息中可以根据Flags字段的值确定用户按下了哪个按钮,根据lpstrFindWhat字段指向的缓冲区确定“查找内容”编辑控件的内容,根据lpstrReplaceWith字段指向的缓冲区确定“替换为”编辑控件的内容。由于对话框必须向父窗口发送消息,因此hwndOwner字段必须指定为父窗口的句柄。
“查找替换消息”的ID值是多少呢?和拖动列表消息类似,在调用FindText和ReplaceText前,需要调用RegisterWindowMessage(FINDMSGSTRING)函数以获取“查找替换消息”的ID,“查找替换消息”的lParam参数是指向创建对话框时指定的FINDREPLACE结构的指针。FINDMSGSTRING常量在commdlg.h头文件中定义如下:
#define FINDMSGSTRING TEXT("commdlg_FindReplace")
在程序的消息循环中使用IsDialogMessage函数以确保对话框正确处理键盘输入(例如Tab键和Esc键)。因此,如果需要使用“查找”和“替换”对话框,不适合使用对话框程序。另外,FINDREPLACE结构和搜索与替换字符串的缓冲区变量应该是全局或静态局部变量,这样在下一次消息处理的时候还可以继续使用上次保存的值。
“查找”和“替换”对话框的应用场合不多,主要用于多行编辑控件和富文本控件,这里仅实现一个简单的示例程序FindReplaceText,FindReplaceText.cpp,源文件的内容如下所示:
#include <Windows.h>
#include <tchar.h>
#include <strsafe.h>
#include "resource.h"
#pragma comment(linker,"\\"/manifestdependency:type='win32' \\
name='Microsoft.Windows.Common-Controls' version='6.0.0.0' \\
processorArchitecture='*' publicKeyToken='6595b64144ccf1df' language='*'\\"")
// 函数声明
LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam);
// 全局变量
HWND g_hwndFind; // 消息循环中会用到这两个全局变量
HWND g_hwndReplace;
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int
nCmdShow)
{
WNDCLASSEX wndclass;
TCHAR szAppName[] = TEXT("FindReplaceText");
HWND hwnd;
MSG msg;
wndclass.cbSize = sizeof(WNDCLASSEX);
wndclass.style = CS_HREDRAW | CS_VREDRAW;
wndclass.lpfnWndProc = WindowProc;
wndclass.cbClsExtra = 0;
wndclass.cbWndExtra = 0;
wndclass.hInstance = hInstance;
wndclass.hIcon = LoadIcon(NULL, IDI_APPLICATION);
wndclass.hCursor = LoadCursor(NULL, IDC_ARROW);
wndclass.hbrBackground = (HBRUSH)GetStockObject(WHITE_BRUSH);
wndclass.lpszMenuName = MAKEINTRESOURCE(IDR_MENU);
wndclass.lpszClassName = szAppName;
wndclass.hIconSm = NULL;
RegisterClassEx(&wndclass);
hwnd = CreateWindowEx(0, szAppName, szAppName, WS_OVERLAPPEDWINDOW,
CW_USEDEFAULT, CW_USEDEFAULT, 400, 300, NULL, NULL, hInstance, NULL);
ShowWindow(hwnd, nCmdShow);
UpdateWindow(hwnd);
while (GetMessage(&msg, NULL, 0, 0) != 0)
{
if ((g_hwndFind == NULL || !IsDialogMessage(g_hwndFind, &msg)) &&
(g_hwndReplace == NULL || !IsDialogMessage(g_hwndReplace, &msg)))
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
}
return msg.wParam;
}
LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
static UINT WM_FINDREPLACE;
static TCHAR szFindWhat[80] = { 0 };
static TCHAR szReplaceWith[80] = { 0 };
static FINDREPLACE fr = { 0 };
LPFINDREPLACE lpfr;
TCHAR szBuf[256] = { 0 };
switch (uMsg)
{
case WM_CREATE:
// 获取查找替换消息的ID
WM_FINDREPLACE = RegisterWindowMessage(FINDMSGSTRING);
// 初始化FINDREPLACE结构,该结构只应该初始化一次
fr.lStructSize = sizeof(FINDREPLACE);
fr.hwndOwner = hwnd;
fr.lpstrFindWhat = szFindWhat;
fr.lpstrReplaceWith = szReplaceWith;
fr.wFindWhatLen = sizeof(szFindWhat);
fr.wReplaceWithLen = sizeof(szReplaceWith);
//fr.Flags = FR_DOWN | FR_MATCHCASE;
return 0;
case WM_COMMAND:
switch (LOWORD(wParam))
{
case ID_EDIT_FIND:
if (g_hwndFind == NULL && g_hwndReplace == NULL)
{
// &掉上次关闭对话框时设置的FR_DIALOGTERM标志值
fr.Flags &= ~FR_DIALOGTERM;
g_hwndFind = FindText(&fr);
}
break;
case ID_EDIT_REPLACE:
if (g_hwndReplace == NULL && g_hwndFind == NULL)
{
// &掉上次关闭对话框时设置的FR_DIALOGTERM标志值
fr.Flags &= ~FR_DIALOGTERM;
g_hwndReplace = ReplaceText(&fr);
}
break;
}
return 0;
case WM_DESTROY:
PostQuitMessage(0);
return 0;
}
//////////////////////////////////////////////////////////////////////////
if (uMsg == WM_FINDREPLACE)
{
lpfr = (LPFINDREPLACE)lParam;
// 关闭对话框
if (lpfr->Flags & FR_DIALOGTERM)
{
g_hwndFind = NULL;
g_hwndReplace = NULL;
}
// 查找下一个
else if (lpfr->Flags & FR_FINDNEXT)
{
StringCchCopy(szBuf, _countof(szBuf), TEXT("单击了"查找下一个"按钮"));
if (lpfr->Flags & FR_DOWN)
StringCchCat(szBuf, _countof(szBuf), TEXT("\\n选中了"向下"单选按钮"));
else
StringCchCat(szBuf, _countof(szBuf), TEXT("\\n选中了"向上"单选按钮"));
if (lpfr->Flags & FR_MATCHCASE)
StringCchCat(szBuf, _countof(szBuf), TEXT("\\n选中了"匹配大小写"复选框"));
if (lpfr->Flags & FR_WHOLEWORD)
StringCchCat(szBuf, _countof(szBuf), TEXT("\\n选中了"全字匹配"复选框"));
StringCchCat(szBuf, _countof(szBuf), TEXT("\\n\\n查找内容:"));
StringCchCat(szBuf, _countof(szBuf), fr.lpstrFindWhat);
MessageBox(hwnd, szBuf, TEXT("提示"), MB_OK);
}
// 替换
else if (lpfr->Flags & FR_REPLACE)
{
StringCchCopy(szBuf, _countof(szBuf), TEXT("单击了"替换"按钮"));
if (lpfr->Flags & FR_DOWN)
StringCchCat(szBuf, _countof(szBuf), TEXT("\\n选中了"向下"单选按钮"));
else
StringCchCat(szBuf, _countof(szBuf), TEXT("\\n选中了"向上"单选按钮"));
if (lpfr->Flags & FR_MATCHCASE)
StringCchCat(szBuf, _countof(szBuf), TEXT("\\n选中了"匹配大小写"复选框"));
if (lpfr->Flags & FR_WHOLEWORD)
StringCchCat(szBuf, _countof(szBuf), TEXT("\\n选中了"全字匹配"复选框"));
StringCchCat(szBuf, _countof(szBuf), TEXT("\\n\\n查找内容:"));
StringCchCat(szBuf, _countof(szBuf), fr.lpstrFindWhat);
StringCchCat(szBuf, _countof(szBuf), TEXT("\\n替换内容:"));
StringCchCat(szBuf, _countof(szBuf), fr.lpstrReplaceWith);
MessageBox(hwnd, szBuf, TEXT("提示"), MB_OK);
}
// 全部替换
else if (lpfr->Flags & FR_REPLACEALL)
{
StringCchCopy(szBuf, _countof(szBuf), TEXT("单击了"全部替换"按钮"));
if (lpfr->Flags & FR_DOWN)
StringCchCat(szBuf, _countof(szBuf), TEXT("\\n选中了"向下"单选按钮"));
else
StringCchCat(szBuf, _countof(szBuf), TEXT("\\n选中了"向上"单选按钮"));
if (lpfr->Flags & FR_MATCHCASE)
StringCchCat(szBuf, _countof(szBuf), TEXT("\\n选中了"匹配大小写”复选框"));
if (lpfr->Flags & FR_WHOLEWORD)
StringCchCat(szBuf, _countof(szBuf), TEXT("\\n选中了"全字匹配"复选框"));
StringCchCat(szBuf, _countof(szBuf), TEXT("\\n\\n查找内容:"));
StringCchCat(szBuf, _countof(szBuf), fr.lpstrFindWhat);
StringCchCat(szBuf, _countof(szBuf), TEXT("\\n替换内容:"));
StringCchCat(szBuf, _countof(szBuf), fr.lpstrReplaceWith);
MessageBox(hwnd, szBuf, TEXT("提示"), MB_OK);
}
return 0;
}
//////////////////////////////////////////////////////////////////////////
return DefWindowProc(hwnd, uMsg, wParam, lParam);
}
10.4 选择字体
字体对话框允许用户选择逻辑字体的属性,例如字体名称、大小、字形、效果(下划线、删除线、字体颜色)以及字符集等。创建字体对话框用的函数是ChooseFont:
BOOL WINAPI ChooseFont(_Inout_ LPCHOOSEFONT lpcf);
如果用户单击了“确定”按钮,则函数返回TRUE,并在CHOOSEFONT结构中返回有关用户选择的字体的信息;如果用户单击了“取消”按钮、关闭了对话框或发生错误,则返回FALSE。
lpcf参数是一个指向CHOOSEFONT结构的指针,该结构包含函数用于初始化字体对话框的信息,函数在该结构中返回有关用户选择的字体的信息。该结构在commdlg.h头文件中定义如下:
typedef struct {
DWORD lStructSize; // 该结构的大小
HWND hwndOwner; // 对话框的拥有者窗口句柄
HDC hDC; // 忽略该字段
LPLOGFONT lpLogFont; // 函数使用该LOGFONT结构初始化对话框的字体,并返回字体信息
INT iPointSize; // 返回所选字体的大小,单位是1/10磅
DWORD Flags; // 标志位
COLORREF rgbColors; // 字体颜色
LPARAM lCustData;
LPCFHOOKPROC lpfnHook;
LPCTSTR lpTemplateName;
HINSTANCE hInstance;
LPTSTR lpszStyle;
WORD nFontType; // 返回所选字体的类型
INT nSizeMin;
INT nSizeMax;
} CHOOSEFONT, *LPCHOOSEFONT;
-
lStructSize字段指定该结构的大小。
-
hwndOwner字段指定对话框的拥有者窗口句柄。
-
lpLogFont字段指定为一个指向LOGFONT结构的指针,函数使用该结构初始化对话框的字体,并通过该结构返回用户所选择的字体信息,返回的LOGFONT结构可以用于创建逻辑字体。
-
iPointSize字段返回所选字体的大小,单位是1/10磅。
-
Flags字段是标志位,用于初始化字体对话框,函数返回时也会设置这些标志以指示用户的选择。常用的标志如表10.6所示。
表10.6
| CF_EFFECTS | 显示“效果”分组框,包括删除线、下划线复选框和颜色组合框。指定该标志以后,可以通过CHOOSEFONT结构的lpLogFont->lfStrikeOut、lpLogFont-> lfUnderline、rgbColors字段初始化字体对话框的效果分组框中的删除线、下划线、颜色控件,函数在这些字段中返回用户的相关选择 |
| CF_INITTOLOGFONTSTRUCT | 使用lpLogFont字段指向的结构来初始化对话框相关控件 |
| CF_NOVERTFONTS | 仅列出水平方向字体 |
| CF_TTONLY | 仅列出TrueType字体 |
| CF_FIXEDPITCHONLY | 仅列出等宽字体 |
- nFontType字段返回用户所选字体的类型,例如BOLD_FONTTYPE粗体、ITALIC_FONTTYPE斜体、REGULAR_FONTTYPE常规等。实际上这些字体类型信息以及字体大小、效果信息等都已经包含在LOGFONT结构中。
关于字体对话框的简单示例程序参见Chapter10\\ChooseFont项目。
10.5 选择颜色
选择颜色对话框的效果如原书的图10.5所示。
用户可以从对话框左侧的“基本颜色”或右侧的“自定义颜色”中选择一种颜色。“基本颜色”是系统预定义好的颜色,也可以在“自定义颜色”中单击选择一种颜色,或者输入红、绿、蓝颜色值,也可以输入色调、饱和度、亮度来生成一个颜色值。选择一种颜色后,颜色效果会实时显示在“颜色|纯色(O)”静态控件中。实际上对话框中除几个编辑控件和按钮以外,其他的控件基本上都是静态控件。
创建选择颜色对话框的函数是ChooseColor:
BOOL WINAPI ChooseColor(_Inout_ LPCHOOSECOLOR lpcc);
lpcc参数是一个指向CHOOSECOLOR结构的指针,该结构包含用于初始化对话框的信息。当ChooseColor函数返回时,该结构包含用户选择的颜色的信息。
如果用户单击对话框的“确定”按钮,则返回值为TRUE;如果用户单击了“取消”按钮、关闭了对话框或发生错误,则返回值为FALSE。
CHOOSECOLOR结构在commdlg.h头文件中定义如下:
typedef struct {
DWORD lStructSize; // 该结构的大小
HWND hwndOwner; // 对话框的拥有者窗口句柄
HWND hInstance;
COLORREF rgbResult; // 创建对话框时初始选择的颜色,函数返回用户所选择的颜色
COLORREF *lpCustColors; // 自定义颜色数组,用于存放对话框左下角的16个自定义颜色
DWORD Flags; // 标志位
LPARAM lCustData;
LPCCHOOKPROC lpfnHook;
LPCTSTR lpTemplateName;
} CHOOSECOLOR, *LPCHOOSECOLOR;
Flags字段是标志位,用于初始化颜色对话框。当对话框返回时,系统会设置这些标志以指示用户的输入。该字段可以是表10.7所示的标志的组合。
表10.7
| CC_FULLOPEN | 对话框显示右侧的自定义颜色相关控件。如果未设置该标志,则用户必须单击对话框左侧的“规定自定义颜色(D)>>”按钮才可以显示自定义颜色相关控件 |
| CC_PREVENTFULLOPEN | 禁用对话框左侧的“规定自定义颜色(D)>>”按钮 |
| CC_RGBINIT | 使用CHOOSECOLOR结构中rgbResult字段指定的颜色作为对话框的初始颜色选择 |
关于选择颜色对话框的简单示例程序参见Chapter10\\ChooseColor项目。
还有一个用于创建页面设置对话框的PageSetupDlg函数,用于打印的PrintDlg函数,因为应用不多,有需要的读者请自行参考MSDN。




