一直想写些关于子类化的东西,因为对于界面编程来说,子类化是一个很基础而且实用的技术。不过一直没有找到一个很好的应用实例,因为这个例子不能太复杂,否则不容易让人理解,而太简单又没什么写头。最近突然想到其实自己一直使用的操作路径对话框的方式就是一个很好的例子,于是就做了一个小例子,写了这篇文章。
对于熟悉界面编程的朋友,子类化这个概念一定不陌生,很多复合控件本身就是有父子关系的。比如listctrl的表头实际上就是一个headctrl,而ComboBox中的edit和list也是这个类型。曾经也写过一篇《ComboBox的高级处理》,文章里介绍的处理ComboBox的方法就是通过子类化的技术实现的,应该说在对这类控件进行重载的时候子类化的应用是比较典型的。不过还有一种情况,也就是本文要介绍的类型,下面让我们来具体看一看。
图1 图2
看一下上面两张图,大家都应该不陌生,这是windows的路径选择对话框。如果我们在调用时设置了默认路径的话(比如D:\NTDDK\bin)对话框弹出时如果指定的位置有下级目录那么就会将这级目录展开,如图1所示。我个人认为这个处理方式不太合理,我觉得既然是选择路径对话框,路径或文件夹就应该是目标对象,所以没有必要对默认位置进行展开,也就是应该像图2那个样子。好了,有了想法就要去实现,那应该怎么办?
方法其实有很多,不过本文介绍的方法是通过子类化的方式进行定制。我们先来看一下调用选择路径对话框的相关代码:
void CPathSelDlg::OnClickButtonBrowse()
{
BROWSEINFO browseInfo;
LPITEMIDLIST pItemID;
TCHAR szPath[MAX_PATH];
// 获取当前路径
memset(szPath, 0, sizeof(szPath));
GetDlgItemText(IDC_EDIT_PATH, szPath, MAX_PATH);
SHILCreateFromPath(_T("D:\\"), &pItemID, NULL);
// 配置路径对话框
memset(&browseInfo, 0, sizeof(BROWSEINFO));
browseInfo.hwndOwner = m_hWnd;
browseInfo.pidlRoot = pItemID;
browseInfo.lpszTitle = _T("选择路径");
browseInfo.lpfn = BrowseForFolderProc;
browseInfo.lParam = (LPARAM)szPath;
pItemID = SHBrowseForFolder(&browseInfo);
if(pItemID)
{
if(SHGetPathFromIDList(pItemID, szPath))
{
int nPahtLen = _tcslen(szPath);
szPath[nPahtLen] = _T('\\');
SetDlgItemText(IDC_EDIT_PATH, szPath);
}
}
}
这段代码大家应该都不陌生,使用过这个对话框的朋友都应该对这些代码很熟悉,这里我特意加了一个设置根目录的功能,希望能对需要用到类似功能的朋友有所帮助。需要说明的是,设置默认路径的功能是通过对browseInfo.lParam参数进行设置来实现的,而这个参数是在browseInfo.lpfn参数中设定的函数中调用的,也就是说如果这两个参数不同时设置是实现不了设置默认参数的功能的。同时我们也会想到一个问题,在browseInfo.lpfn所指定的函数中我们是不是也可以做些什么?看一下这个函数是做什么用的:应用程序定义的浏览对话框回调函数的地址,当对话框中的事件发生时,该对话框将调用回调函数。果不其然这个函数可以响应路径选择对话框的消息,只要我们找准时机对对话框中的TreeCtrl进行子类化就可以实现对它的控制。下面我们看一下实现代码:
int CALLBACK CPathSelDlg::BrowseForFolderProc(HWND hWnd, UINT uMsg, LPARAM lParam, LPARAM lpData)
{
if(uMsg == BFFM_INITIALIZED)
{
CTreeCtrl treePath;
HTREEITEM hItemSel;
::SendMessage(hWnd, BFFM_SETSELECTION, TRUE, lpData);
treePath.SubclassWindow(::GetDlgItem(hWnd, 0x3741));
hItemSel = treePath.GetSelectedItem();
treePath.Expand(hItemSel, TVE_COLLAPSE);
treePath.UnsubclassWindow();
}
return 1;
}
通过uMsg来判断消息,一目了然BFFM_INITIALIZED相当于对话框的OnInitDialog,在这个时候进行子类化可以说是最佳时机。0x3741是通过spy++观察到的TreeCtrl的ID,于是通过GetDlgItem获取到控件的句柄然后通过SubclassWindow实现子类化,一旦这个操作成功,我们就可以为所欲为了。这里我只是想折叠其当前选中节点的下层节点,不过如果要还想做些什么的话当然也是可以。操作完毕后通过UnsubclassWindow将控件释放掉,如果你想一直对这个控件有控制权可以选择在窗口销毁的时候在释放,不过这样的话treePath应该是一个静态变量,否则会出现问题。
好了,到这里我要介绍的东西就都介绍完了。简单总结一下,如果你想对一个第三方窗口的某个控件实现控制,子类化是一个很好的方式,你需要做的就是获取那个窗口的句柄和目标控件的ID,在一个合适的时机调用SubclassWindow,这样你就可以像一般控件那要自由的操作了,操作结束之后再找一个合适的时机释放掉这种控制即可。有兴趣的朋友可以下载我这个例子看看,希望大家都可以做出个性化的应用。