定制 Windows 通用文件打开对话框






4.44/5 (14投票s)
1999年11月20日
9分钟阅读

389220

3353
摘要
本文描述了如何超越 Windows 通用文件打开对话框的常规自定义。通用文件打开对话框可以说是 Windows 世界中最常被自定义的实体之一,但考虑到它包含的自定义选项有限,要按照自己想要的方式进行自定义通常并不直观。本文将告诉您如何使其外观和行为符合您的要求。读者请注意,尽管以下想法和概念涉及通用文件打开对话框,但此处介绍的技术也可以轻松应用于任何其他通用对话框。
目标
自定义通用文件打开对话框的目标如下:- 启动到由客户端指定的特定所需目录。
- 仅在此目录中显示文件夹,不显示文件。
- 隐藏工具栏的两个按钮,以便用户无法向上导航一个级别或创建新文件夹。
- 将对话框顶部的组合框(显示当前文件夹)设为只读,以便用户无法从此处选择其他目录。
- 阻止用户在对话框底部的编辑框中键入不同路径时更改目录。包括通配符。
基本架构
基本上,需要采取以下步骤来实现上述目标:- 创建一个通用文件打开对话框。
- 设置一个钩子,拦截流向通用文件打开对话框的所有消息。
- 捕获所需的消息,针对所需的子窗口,例如工具栏或组合框。
- 通过编写自定义代码来更改行为。
详细说明
创建通用文件打开对话框
创建通用文件打开对话框是一个简单的过程。但是,它必须首先被子类化。子类化是必要的,因为我们需要访问对话框的 OnInitDialog() 处理程序,而这是我们设置钩子的主要地方。为此,我们有一个 CCustomFileDlg 类,它公开继承自 CFileDialog。OnInitDialog() 函数被重写。此外,派生类的析构函数会移除在 OnInitDialog() 函数中设置的钩子。m_ofn 结构用相关详细信息填充,并调用 DoModal() 来创建和显示对话框。
使控件只读
在对话框的 OnInitDialog() 处理程序中,首先将对话框顶部的组合框设为只读。这是通过枚举子窗口来实现的,直到我们找到组合框及其句柄。EnumChildProc() 函数执行此操作。
BOOL CALLBACK EnumChildProc(HWND hWnd, LPARAM lParam) { int id = ::GetDlgCtrlID(hWnd); switch(id) { case LOOK_IN_COMBO : // Combo box on top of the dialog ::EnableWindow(hWnd, FALSE); break; } return TRUE; }
组合框的 ID 由 LOOK_IN_COMBO 标识,值为 1137,并在头文件中定义。
设置钩子
钩子在 OnInitDialog() 处理程序中按如下方式设置:
HookHandle = SetWindowsHookEx( WH_CALLWNDPROC, (HOOKPROC) Hooker, (HINSTANCE)NULL, (DWORD)GetCurrentThreadId() );
钩子类型为 WH_CALLWNDPROC。这意味着钩子会先接收所有消息,然后目标窗口过程才会收到。因此,可以在此时进行自定义处理,因为我们能够率先处理消息。
在钩子过程中
钩子过程在 LPARAM 中接收指向 CWPSTRUCT 的指针。此结构包含有关将发送到特定窗口的消息的以下信息:- LPARAM
- WPARAM
- Message
- 窗口句柄
对我们而言,窗口句柄和消息是最重要的。很多处理将取决于这两个参数。
我们设置此钩子的主要原因是,我们无法立即对对话框中的控件进行子类化,因为某些控件,例如列表视图控件和工具栏,根本不存在于 VC++ 包含目录中的通用对话框模板中。此外,它们的 ID 未知,在模板中不可用,这使得处理起来更加困难。更奇怪的是,有一个 ID 为 lst1 的列表框控件,它似乎在模板中没有任何原因。这个控件是隐藏的,当对话框显示时,列表视图控件会覆盖在它上面。
处理窗口时最重要的两件事是其句柄和 ID。子类化方法的一个主要问题如下:为了对控件进行子类化,控件必须首先存在!启动时,列表视图控件会显示文件夹和文件。我们现在可以对其进行子类化,但此时已经太晚了 - 它显示了文件夹和文件 - 而我们只需要文件!理想情况下,我们需要一种东西,在列表视图控件初始化之前拦截它,以便我们可以移除文件夹 - 然后,我们可以让列表视图控件继续只显示文件。使用钩子对我们来说最方便,因为它绕过了传统子类化的一些问题。使用 WH_CALLBACK 类型钩子的一大优势是,在控件接收消息之前,我们就能接收到它。因此,我们可以捕获发送到列表视图控件的预期消息,并通过更改处理方式来修改它。此外,通过 LPARAM 传递给钩子的 CWPSTRUCT 指针包含重要信息,我们可以加以利用 - 窗口句柄和消息。
钩子是一个万能的捕获器。换句话说,所有生成的消息首先会发送到钩子。由于我们想选择要修改的窗口,我们必须首先识别目标窗口句柄。这是通过在通过 CWPSTRUCT 获取的窗口句柄上使用 GetClassName() API 来完成的。GetClassName() 返回我们感兴趣的窗口的类名作为字符串。对于列表视图控件的句柄,GetClassName() 返回“syslistview32”;对于工具栏,它返回“toolbarwindow32”。对于我们要修改的最后一个控件,编辑控件,它返回“edit”。
以下代码显示了如何获取句柄已在 CWPSTRUCT 指针中识别的控件的类名:
CWPSTRUCT *x = (CWPSTRUCT*)lParam; GetClassName(x -> hwnd, szClassName, MAX_CHAR);
使用相关的类名,我们进行相应的处理。例如,如果它是基于“syslistview32”的控件,则执行某项操作;如果是基于“toolbarwindow32”的控件,则执行另一项操作,依此类推。这是通过简单调用 strcmp() 来实现的。
自定义列表视图控件
这是代码
if (strcmp(_strlwr(szClassName), "syslistview32") == 0) { switch(x->message) { case WM_NCPAINT : case LAST_LISTVIEW_MSG : // Magic message sent after all items are inserted { int count = ListView_GetItemCount(x-> hwnd); for(int i= 0; i < count; i++) { item.mask = LVIF_TEXT | LVIF_PARAM; item.iItem = i; item.iSubItem = 0; item.pszText = szItemName; item.cchTextMax = MAX_CHAR; ListView_GetItem(x -> hwnd, &item); if (GetFileAttributes(szItemName) & FILE_ATTRIBUTE_DIRECTORY) ListView_DeleteItem(x -> hwnd, i); break; } } } // end switch HideToolbarBtns(hWndToolbar); } // end if
首先使用 strcmp() 进行检查,以确保它是列表视图控件。如果是,我们切换到 CWPSTRUCT 指针的消息部分(在本例中为 x)。捕获两个消息:WM_NCPAINT,用于在列表视图控件实际显示之前移除文件夹项,以及 LAST_LISTVIEW_MSG(在我的头文件中定义),这是列表视图控件在显示所有项后接收的最后一个消息。该消息的值为 4146,这是我通过研究发送到列表视图控件的消息而确定的。
由于我们现在有了列表视图控件的句柄,我们可以执行常规的列表视图控件操作 - 在这种情况下,我们只需遍历所有项,检查是否有任何项具有 FILE_ATTRIBUTE_DIRECTORY 属性 - 这意味着它是目录。如果是,我们将其删除。最后,通过调用辅助函数 HideToolbarBtns() 来隐藏工具栏的按钮,该函数接收工具栏的句柄。我们是如何获得工具栏句柄的?以下代码执行此操作 - 它只是保存工具栏句柄供以后使用。
if (strcmp(_strlwr(szClassName), "toolbarwindow32") == 0) { if (!CCustomFileDlg::OnceOnly) // Save toolbar's handle only once { hWndToolbar = x -> hwnd; ++CCustomFileDlg::OnceOnly; } }
隐藏工具栏
这是 HideToolbarBtns() 的代码:
void HideToolbarBtns ( HWND hWndToolbar ) { TBBUTTONINFO tbinfo; tbinfo.cbSize = sizeof(TBBUTTONINFO); tbinfo.dwMask = TBIF_STATE; tbinfo.fsState = TBSTATE_HIDDEN | TBSTATE_INDETERMINATE; ::SendMessage(hWndToolbar,TB_SETBUTTONINFO, (WPARAM)TB_BTN_UPONELEVEL,(LPARAM)&tbinfo); ::SendMessage(hWndToolbar,TB_SETBUTTONINFO, (WPARAM)TB_BTN_NEWFOLDER,(LPARAM)&tbinfo); }
代码只是为工具栏按钮设置新的按钮状态。困难的部分是确定工具栏按钮的 ID。在这种情况下,我们使用 TB_BTN_UPONELEVEL 和 TB_BTN_NEWFOLDER,它们是用户可能点击以向上导航一个级别和创建新文件夹的按钮。这两个都在头文件中定义如下:
const int TB_BTN_UPONELEVEL = 40961; const int TB_BTN_NEWFOLDER = 40962;
同样,这些数字来自于花费大量时间使用调试器来弄清楚发送到窗口句柄的消息,并尝试不同的工具栏按钮 ID。
最后,我们需要确保用户无法通过在编辑框中输入不同路径来更改目录。有两种方法可以做到这一点:我们可以捕获 Return 键事件,当用户在编辑框中输入路径后按下 Return 键时会发生此事件;或者对对话框中的编辑控件进行子类化,这样就无法输入表示更改目录的字符。由于本文是关于子类化的,所以我们选择后一种方法也就不足为奇了!首先要做的是创建自己的派生编辑控件类,如 myedit.h 中定义的。
class CMyEdit : public CEdit { protected: afx_msg void OnChar(UINT nChar, UINT nRepCnt, UINT nFlags); DECLARE_MESSAGE_MAP() };
我们在类中有一个 OnChar 处理程序,因为我们对捕获感兴趣。现在,在 customfiledlg.cpp 文件中创建一个 CMyEdit 类型的对象,最后,这是 myedit.cpp 中的实现:
void CMyEdit::OnChar ( UINT nChar, UINT nRepCnt, UINT nFlags ) { // Clear selection, if any DWORD dwSel = GetSel(); LOWORD(dwSel) == HIWORD(dwSel) ? NULL : SetWindowText(""); CString strWindowText; GetWindowText(strWindowText); if (nChar == '\\' || nChar == ':' || (nChar == '.' && strWindowText.GetLength() < 1)) return; // Don't pass on to base for processing CEdit::OnChar(nChar, nRepCnt, nFlags); // Pass on to base for processing }
上述逻辑解决了以下问题:
- 用户无法输入“:”
- 用户无法输入“\”
- 输入的第一个字符不能是“.”,从而不允许用户键入“..”,这会向上导航一个级别。作为第一个字符以外的字符可以是“.”。
- 我发现,当您选择文本,然后键入“.”作为第一个字符时,它会被输入。为了避免出现选择存在的情况,我首先将控件中的文本设置为 NULL(空)。GetSel() API 函数用于此目的,如上所示。
- 通配符是可能的,因为您可以使用“*”。
此时,您可能想知道为什么我从不在我的自定义对话框类中将编辑控件设为成员,为什么我没有在对话框的 OnInitDialog() 中对其进行子类化,而却选择在 .cpp 文件中创建一个编辑控件的成员并在我的钩子代码中处理其子类化。原因:它不起作用。我非常乐意听取热心读者的解释。
现在,我们有了一个文件打开对话框上的编辑控件,它可以有效地防止用户通过在编辑控件中输入目录路径来导航到不同目录。
杂项
最后一点,自定义的文件打开通用对话框支持多选。所有选定的项都会被存储并解析到一个 CStringList 对象中,该对象包含每个用户选择的项,并且是完全限定的,即包含完整路径。指向该列表的指针将通过导出的函数 getFileNames() 返回给客户端。客户端有责任释放与列表相关的内存。另请注意,当将路径作为参数传递给 getFileNames() 时,客户端应确保表示目录的路径始终以尾部“\”终止,而不是留空。换句话说,“C:\\TEMP”是不可接受的,应更改为“C:\\TEMP\\”。在我看来,前者代表文件,后者代表目录。这种用“\\”结尾目录的方法消除了歧义。