| 如何在编辑框中使用IAutoComplete接口 如果可能我想用打包类来实现 |
|
| 赵湘宁 | |
| 本文例子代码 | |
| 唉!,就叫我封装先生吧。 你算是找对地方了。但是我要先声明我的解决办法不是你所希望的-甚至也不是我自己所希望的! |
|
| 什么是autocomplete呢?你也许已经注意到当你在IE的地址编辑框中敲入什么东西的时候,就会出现一个下拉组合框显示所有匹配敲入字符的URLs,亮条落在第一个匹配项上,你只要按下回车键就可以了(图一)。在“文件/打开”对话框及Windows其它地方也有相同的效果。 Autocompletion真是一个伟大的发明。 |
|
|
|
|
|
(图一) |
|
| 我第一次看到你的问题时,说句实话,我还从来没有听说过IAutoComplete-你是不是觉得我应该熟悉微软发布的每一个新的COM接口?-对我来说这似乎是个好主意。代码一中列出了IAutoComplete的一些可选项。IAutoComplete与IEnumString一起工作,IEnumString是一个通用的枚举串列表。你只要将一个串枚举器指针和一个 Windows 编辑框或组合框句柄赋给IAutoComplete对象,其它的事情你就不用管了。如果你想设置发烧选项,就使用IAutoComplete2接口。每一个COM接口都是使用二号版本加以完善的,即便它只有两个方法。 | |
| IAutoComplete有一个缺陷,它只存在于Windows 2000,具体地说,实现IAutoComplete(CLSID_IAutoComplete)的COM对象位于shell32.dll的5.0版本中,它只随Windows 2000一起发布,Windows 95,Windows 98和Windows NT 4.0中则没有。如果你要使用它,要做的第一件事情是实现IEnumString接口。 | |
| 当我劳神费力处理完QueryInterface,AddRef,Release以及CLSIDs,CoInitialize,并在构造器中决定了m_dwRef是取0还是1后,然后我使用自己认为还不错的方法,并打算经历所有痛苦和磨难来封装IAutoComplete,如果最终这个类将只能在Windows 2000中运行,那对我所做的努力打击实在是太大了。 | |
| 这真是个难题,我该怎么办呢?我们的目的是在一个列表串中搜索与用户输入匹配的串。自己来写这种代码有多难啊!现代编程的问题之一是没有人愿意多写代码。不要让我犯错误-COM很棒。但是除非你已经有一个现成的IEnumString,否则对于autocompletion来说似乎是太繁琐了。 | |
| CAutoComplete是我写的一个类,这个类大体上实现了autocompletion,不用COM,也不用shell32.dll,它只是一个简单的类而已,你可以将它的cpp文件添加到你的应用,DLL或者扩展库中。它可以工作于任何的Windows版本,甚至是Windows 3.1。 | |
| CAutoComplete没有实现IAutoComplete中的所有的特性。例如,IAutoComplete有一个特性是当用户按下 Ctrl+Enter 时的快速完成格式串。这个格式串是一个Windows用来转换用户输入的 sprintf 串。如果这个格式串是“http://www.%s.com” 并且用户敲入“woowoo”,IAutoComplete 将完成整个内容http://www.woowoo.com。另外一个IAutoComplete特性是让你指定一个串作为注册键来存储格式串。这些特性都很好,但他们太IE化,似乎不属于通用的 autocompletion接口,所以我没有将它们放进CAutoComplete,而是让CAutoComplete提供更通用的方式来改变它实现autocompletion的方法。 |
|
| 本文的例子程序ACTest使用了CAutoComplete,如图二所示,ACTest是一个基于 对话框的迷你程序,对话框中包含有一个编辑框和一个组合框,既两个CAutoComplete实例。 |
|
|
|
|
|
(图二) |
|
class CMyDialog : public CDialog {protected: CAutoComplete m_acEdit; // 编辑框实例 CAutoComplete m_acCombo; // 组合框实例......};
|
|
| 为了使用CAutoComplete,你必须用窗口(编辑框和组合框)指针初始化每一个实例,然后再添加串。例子中这些都是在 CMyDialog 的OnInitDialog中完成。 | |
// in CMyDialog::OnInitDialogm_acCombo.Init(GetDlgItem(IDC_COMBO1));m_acEdit.Init(GetDlgItem(IDC_EDIT1));static LPCTSTR STRINGS[] = { "alpha", "alphabet", ...... NULL};for (int i=0; STRINGS[i]; i++) { m_acCombo.GetStringList().Add(STRINGS[i]); m_acEdit.GetStringList().Add(STRINGS[i]);}
|
|
| 因为ACTest只是个简单的例子,编辑框和组合框在其中不做任何事情,调用CWnd::GetDlgItem 获得对话框控制。在实际应用中,你很可能有对话框类的成员如:m_wndEdit,m_wndCombo,在用SubclassDlgItem子类化(subclassing )这些成员后传递它们的地址。 | |
| 要做的就这些,不用IEnumString,也不用COM。仅仅做一下初始化和添加一些串。当用户敲入一个字符如“b”,如图二所示,CAutoComplete 会显示与“b”匹配的选择并显示完整的文本串“bata”。 | |
| 实现 CAutoComplete 的基本思路是从CSubclassWnd类派生,CSubclassWnd是一个通用的子类窗口类,这个类在VC知识库中出现的频率很高。CSubclassWnd让任何对象截获消息发送到窗口。它通过加载一个窗口过程使用普通的窗口子集,这些好东西在MFC的编程中是没有的,因为MFC的设计不是用来实现这种功能的。 当应用调用CAutoComplete::Init时,CAutoComplete会调用CSubclassWnd::HookWindow函数,它子类化窗口。CSubclassWnd::HookWindow将CSubclassWnd对象作为附件交给窗口(使用类似MFC的机制)以便任何发送到窗口的消息首先被虚函数CSubclassWnd::WindowProc指定路由。CAutoComplete重载这个函数来处理感兴趣的消息。 |
|
// 重载 CSubclassWnd::WindowProcLRESULT CAutoComplete::WindowProc(...){ if (/* EN_CHANGE or CBN_EDITCHANGE */))) { // try to complete } return CSubclassWnd::WindowProc(...);}
|
|
| 请注意CAutoComplete不是一个CWnd派生的对象。它是从CSubclassWnd派生的,而CSubclassWnd又派生于CObject。在处理完消息之后,CAutoComplete调用 CSubclassWnd::WindowProc,它以自己的方式经过原来的窗口过程传递消息到任何消息映射中的消息处理器。 一旦你明白了CSubclassWnd截获消息的方式,剩下的事情就是要完成实际的autocompletion功能了-也就是比较串列表和用户输入,CAutoComplete在虚函数OnComplete中完成这个工作。缺省实现(见AutoCompl.cpp代码)比较用户输入与内部串表,如果匹配的话便把匹配项显示在控制(编辑框)中,如果是组合框,CAutoComplete将所有匹配选择项显示在下拉框中。 不管怎么说,CAutoComplete的实现还是需要一些技巧的,当CAutoComplete截获消息EN_CHANGE (改变编辑框输入)或CBN_ EDITCHANGE时,它必须在调用SetWindowText设置新的 内容之前将自己关闭起来。否则,SetWindowText 将触发另一个CHANGE通知并且控制将离开你而去......嘿、嘿,试一试就知道了。 第二个诀窍是个小聪明。假设用户敲入“al”,CAutoComplete通过高亮“pha”三个字符来完成整个串“alpha”。用户现在想按Backspace删除掉“pha”,可怜的用户会发现Backspace不起作用。解决办法是当用户缩短(删除)输入的字符时就忽略掉文本完成动作。也就是说如果输入的内容匹配以前的输入,就忽略完成动作。如果我没有解释清楚,实在是对不起,你到程序代码中去找感觉吧。我添加了一个虚函数IgnoreCompletion来测试这种情况;如果这个函数返回FALSE,CAutoComplete只实现了文本完成,那明摆着算法不正确,我把它做成虚函数以便让你能重载。 |
|
| 最后一个诀窍是其它的虚函数,它们将OnComplete分割成更小的操作,使你比较容易改变基本的行为。例如,OnComplete 做的第一件事情是调用一个虚函数 GetMatches 来获取与用户输入匹配的列表(CStringArray)。 | |
void CAutoComplete::OnComplete(CWnd* pWnd, CString s){ CStringArray arMatches; // 匹配串 if (s.GetLength()>0 && GetMatches(s, arMatches, m_iType==Edit)>0) { DoCompletion(...); } m_sPrevious=s; // 记住当前串}
|
|
| GetMatches 调用多个虚函数轮流操作串表:OnFirstString,OnNextString和OnMatchString。缺省实现操作一个内部的CStringArray-与调用CAutoComplete::Add获得的填充数组相同。(记住CMyDialog::OnInitDialog 调用Add来提供串表),最后,你既可以调用Add添加串,也可以派生一个新类,重载OnFirstString和其它的复杂行为。例如,你不想在CAutoComplete的CStringArray中存储串,或者你可能想重载DoCompletion实现实际的匹配完成(设置窗口文本和下拉组合框)动作。你可以重载DoCompletion来支持一些其它的非编辑、非组合框控制。 罗罗嗦嗦了这么多,其实关键的地方无非就是设计一个API,这个API使你能重载函数来改变特定的行为,这是所有好程序的奥妙所在。你是否在C,C++或COM中写过类(在C中当然不叫类)?你仍然要设计API,那时最精彩的部分。别看Windows中有那么多的接口,但正真编程起来好像总觉得API不够用,一个原因就是这些API被设计用来只满足Windows或IE本身的需要,或微软产品偶尔使用到它们,除此之外没有别的。不管怎么说,自己写代码常常较容易获得你实际想要的结果。 本文最后对CAutoComplete和微软在shell32.dll中实现的IAutoComplete做了一个比较,并不是说谁好谁坏,而是哪一个更适合你的需要。 |