如果可能我想用打包类来实现。唉!,就叫我封装先生吧。
你算是找对地方了。但是我要先声明我的解决办法不是你所希望的-甚至也不是我自己所希望的!
什么是autocomplete呢?你也许已经注意到当你在IE的地址编辑框中敲入什么东西的时候,就会出现一个下拉组合框显示所有匹配敲入字符的URLs,亮条落在第一个匹配项上,你只要按下回车键就可以了(图一)。在“文件/打开”对话框及Windows其它地方也有相同的效果。Autocompletion真是个了不起的东西。可惜 到了Windows 2000才有,来得太迟了。
(图一)
我第一次看到你的问题时,说句实话,我还从来没有听说过 IAutoComplete:
01.IAutocomplete 和 IAutoComplete2 02. 03.IAutoComplete 04.HRESULT Init( 05. HWND hwndEdit, // 编辑控制或组合框 06. IUnknown *punkACL, // 实现 IEnumString 的对象指针 07. LPCOLESTR pwszRegKeyPath, // 存储格式串的注册库路径 08. LPCOLESTR pwszQuickComplete); // CTRL+Enter的格式化串 09. 10. // Enable 或 Disable 自动完成功能 11.HRESULT Enable(BOOL fEnable); 12. 13.enum { 14. ACO_NONE = 0, 15. ACO_AUTOSUGGEST = 0x1, 16. ACO_AUTOAPPEND = 0x2, 17. ACO_SEARCH = 0x4, 18. ACO_FILTERPREFIXES= 0x8, 19. ACO_USETAB = 0x10, 20. ACO_UPDOWNKEYDROPSLIST= 0x20, 21. ACO_RTLREADING = 0x40 22.} AUTOCOMPLETEOPTIONS; 23. 24.IAutoComplete2 25.HRESULT SetOptions(DWORD dwFlag); 26.HRESULT GetOptions(DWORD *pdwFlag);你是不是觉得我应该熟悉微软发布的每一个新的COM接口? 对我来说这似乎是个好主意。
下面是 IAutoComplete 接口的一些选项:
| 选项标志 | 描述 |
| ACO_NONE | 没有自动完成 |
| ACO_AUTOSUGGEST | 启用自动建议的下拉列表框 |
| ACO_AUTOAPPEND | 启用自动添加 |
| ACO_SEARCH | 在完成的串中添加搜索项目,选中此项目启动搜索引擎 |
| ACO_FILTERPREFIXES | 不匹配逗号前缀,如“www.”,“http://”等 |
| ACO_USETAB | 使用Tab键从下拉框清单中选择 |
| ACF_UPDOWNKEYDROPSLIST | 使用上下箭头键显示自动建议的下拉框清单 |
| ACO_RTLREADING | 常规窗口从左到右显示文本。Windows 可以被映射显示诸如 Hebrew 或 Arabic 这样从右到左阅读的语言。通常,某个控制的文本与其父窗口文本的阅读/显示方向相同。如果设置ACO_RTLREADING,那么文本阅读方向与其父窗口文本阅读方向相反。 |
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:
001.//////////////////////////////////////////////////////////////// 002.// AutoCompl.h 003.// 004.#pragma once 005. 006.#include "subclass.h" 007. 008./////////////////////////////////////////////////////////////////// 009.// 只是个通用可重用类,你可以将它用于编辑框和组合框的自动完成输入应用. 010.// 011.// 使用方法: 012.// - 实例化你想要 hook 的编辑/组合控制 013.// - 添加一些串到串数组中 014.// - 调用 Init 015.class CAutoCompleteWnd : public CSubclassWnd { 016.protected: 017. CStringArray m_arStrings; // 串列表(数组) 018. CString m_sPrevious; // 前一个内容 019. int m_bIgnoreChangeMsg; // 忽略 EN_CHANGE 消息 020. UINT m_idMyControl; // 子类化的控制 ID 021. int m_iType; // 控制类型(编辑/组合) 022. int m_iCurString; // 当前串的索引 023. enum { Edit=1,ComboBox }; 024. 025. // hook 函数 026. virtual LRESULT WindowProc(UINT msg, WPARAM wp, LPARAM lp); 027. 028. // 可以重载的辅助函数 029. virtual UINT GetMatches(LPCTSTR pszText, CStringArray& arMatches, 030. BOOL bFirstOnly=FALSE); 031. virtual void OnFirstString(); 032. virtual BOOL OnNextString(CString& sNext); 033. virtual BOOL OnMatchTest(const CString& s, const CString& sMatch); 034. virtual BOOL IgnoreCompletion(CString s); 035. virtual void OnComplete(CWnd* pWnd, CString s); 036. 037.public: 038. CAutoCompleteWnd(); 039. ~CAutoCompleteWnd(); 040. void Init(CWnd* pWnd); 041. CStringArray& GetStringList() { return m_arStrings; } 042.}; 043. 044.AutoCompl.cpp 045.//////////////////////////////////////////////////////////////// 046.// VCKBASE August 2000 047.// Visual C++ 6.0环境编译, 在 Windows 98 或 Windows NT 中运行. 048.// 049.#include "stdafx.h" 050.#include "autocompl.h" 051. 052.////////////////// 053.// 构造函数: 用于初始化 054.CAutoComplete::CAutoComplete() 055.{ 056. m_bIgnoreChangeMsg=0; 057. m_iType = 0; 058. m_idMyControl = 0; 059. m_iCurString = 0; 060.} 061. 062.CAutoComplete::~CAutoComplete() 063.{ 064.} 065. 066./////////////////////////////////////////////////////////////// 067.// 安装 hook. 初始化控制 ID 和基于类名的控制类型 068.// 069.void CAutoComplete::Init(CWnd* pWnd) 070.{ 071. CSubclassWnd::HookWindow(pWnd->GetParent()); 072. CString sClassName; 073. ::GetClassName(pWnd->GetSafeHwnd(), sClassName.GetBuffer(32), 32); 074. sClassName.ReleaseBuffer(); 075. if (sClassName=="Edit") { 076. m_iType = Edit; 077. } else if (sClassName=="ComboBox") { 078. m_iType = ComboBox; 079. } 080. m_idMyControl = pWnd->GetDlgCtrlID(); 081.} 082. 083.///////////////////////////////////////////////////////////////// 084.// 扫描串数组中匹配的文本,并添加匹配项到一个新数组,返回匹配项的数目。 085.// 对于编辑控制而言, 只需要找到第一个串,在这里使用了一个BOOL参数。 086.// 087.// 088.UINT CAutoComplete::GetMatches(LPCTSTR pszText, CStringArray& arMatches, 089. BOOL bFirstOnly) 090.{ 091. arMatches.RemoveAll(); 092. int nMatch = 0; 093. CString s=pszText; 094. if (s.GetLength()>0) { 095. OnFirstString(); 096. CString sMatch; 097. while (OnNextString(sMatch)) { 098. if (OnMatchString(s, sMatch)) { 099. TRACE("Add %s\n",(LPCTSTR)sMatch); 100. arMatches.Add(sMatch); 101. nMatch++; 102. if (bFirstOnly) 103. break; 104. } 105. } 106. } 107. return nMatch; 108.} 109. 110.////////////////////////////////////////////////////////////////// 111.// 这个虚拟函数的参数是两个字符串,如果有匹配则返回 TRUE. 缺省情况下实现普通的 112.// 前缀比较-你可以重载它,例如,在忽略掉两个串中的‘www’字符。 113.// 114.// 115.BOOL CAutoComplete::OnMatchString(const CString& s, const CString& sMatch) 116.{ 117. return s==sMatch.Left(s.GetLength()); 118.} 119. 120.void CAutoComplete::OnFirstString() 121.{ 122. m_iCurString=0; 123.} 124. 125.BOOL CAutoComplete::OnNextString(CString& sNext) 126.{ 127. if (m_iCurString < m_arStrings.GetSize()) { 128. sNext = m_arStrings[m_iCurString++]; 129. return TRUE; 130. } 131. sNext = (LPCTSTR)NULL; 132. return FALSE; 133.} 134. 135.////////////////////////////////////////////////////////////// 136.// "hook" 函数截取发送到编辑控制和组合框的消息.我只对 EN_CHANGE 或 137.// CBN_EDITCHANGE感兴趣: 编辑控制的内容已经改变. 138.// 139.LRESULT CAutoComplete::WindowProc(UINT msg, WPARAM wp, LPARAM lp) 140.{ 141. if ((msg==WM_COMMAND && LOWORD(wp)==m_idMyControl) && 142. ((m_iType==Edit && HIWORD(wp)==EN_CHANGE) || 143. (m_iType==ComboBox && HIWORD(wp)==CBN_EDITCHANGE))) { 144. 145. // 因为我要改变控制的内容,它将触发更多的EN_CHANGE消息, 146. // 当我获得控制时使用 m_bIgnoreChangeMsg 结束处理. 147. if (!m_bIgnoreChangeMsg++) { 148. CString s; 149. CWnd* pWnd = CWnd::FromHandle((HWND)lp); 150. pWnd->GetWindowText(s); 151. OnComplete(pWnd, s); 152. } 153. m_bIgnoreChangeMsg--; 154. } 155. return CSubclassWnd::WindowProc(msg, wp, lp); 156.} 157. 158.////////////////////////////////////////////////////////////// 159.// 这是实现输入完成的主函数. 160.// 161.void CAutoComplete::OnComplete(CWnd* pWnd, CString s) 162.{ 163. CStringArray arMatches; // 匹配串 164. if (s.GetLength()>0 && GetMatches(s, arMatches, m_iType==Edit)>0) { 165. DoCompletion(pWnd, s, arMatches); 166. } 167. m_sPrevious=s; // 记住当前串 168.} 169. 170.void CAutoComplete::DoCompletion(CWnd* pWnd, CString s, 171. const CStringArray& arMatches) 172.{ 173. if (m_iType==ComboBox) { 174. // 这种强制转换从技术上讲是不正确的,但它是一个标准的MFC诀窍. 175. CComboBox* pComboBox = (CComboBox*)pWnd; 176. 177. // 更新下拉框以反映可能的匹配 178. pComboBox->ResetContent(); 179. for (int i=0; iAddString(arMatches[i]); 180. } 181. // 用户箭头光标,这样用户才能选择 182. ::SetCursor(LoadCursor(NULL,MAKEINTRESOURCE(IDC_ARROW))); 183. // 显示下拉框 184. pComboBox->ShowDropDown(); 185. pComboBox->SetWindowText(IgnoreCompletion(s) ? s : arMatches[0]); 186. pComboBox->SetEditSel(s.GetLength(),-1); 187. 188. } else if (m_iType==Edit && !IgnoreCompletion(s)) { 189. // 这种强制转换从技术上讲是不正确的,但它是一个标准的MFC诀窍. 190. CEdit* pEdit = (CEdit*)pWnd; 191. pEdit->SetWindowText(arMatches[0]); 192. pEdit->SetSel(s.GetLength(),-1); 193. } 194.} 195. 196.////////////////////////////////////////////////////////////// 197.// 当用户按下Backspace键删除敲入的字符时,这个函数用于关闭输入完成特性。 198.// 这时,当前的串将匹配最后输入的(前一个)串。在这种情况下,输入完成特性 199.// 是被屏蔽掉的。例如,如果用户敲如‘foo’,程序将自动完成‘foobar’,字符 200.// ‘bar’是高亮,如果用户按下Backspace键或者Delete键删除‘bar’时,程序 201.// 不会再次完成foobar,而是保留‘foo’,即便是用户继续按Backspace键也一样。 202.// 这个函数是我写的唯一一个说明比代码本身还长函数:)! 203.// 204.// 205.BOOL CAutoComplete::IgnoreCompletion(CString s) 206.{ 207. return s==m_sPrevious.Left(s.GetLength()); 208.}这个类大体上实现了 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实例。
(图二)
1.class CMyDialog : public CDialog { 2.protected: 3. CAutoComplete m_acEdit; // 编辑框实例 4. CAutoComplete m_acCombo; // 组合框实例 5....... 6.};为了使用CAutoComplete,你必须用窗口(编辑框和组合框)指针初始化每一个实例,然后再添加串。例子中这些都是在 CMyDialog 的OnInitDialog中完成。
01.// in CMyDialog::OnInitDialog 02.m_acCombo.Init(GetDlgItem(IDC_COMBO1)); 03.m_acEdit.Init(GetDlgItem(IDC_EDIT1)); 04.static LPCTSTR STRINGS[] = { 05. "alpha", 06. "alphabet", 07. ...... 08. NULL 09.}; 10.for (int i=0; STRINGS[i]; i++) { 11. m_acCombo.GetStringList().Add(STRINGS[i]); 12. m_acEdit.GetStringList().Add(STRINGS[i]); 13.}因为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重载这个函数来处理感兴趣的消息。
1.// 重载 CSubclassWnd::WindowProc 2.LRESULT CAutoComplete::WindowProc(...) 3.{ 4. if (/* EN_CHANGE or CBN_EDITCHANGE */))) { 5. // try to complete 6. } 7. return CSubclassWnd::WindowProc(...); 8.}请注意CAutoComplete不是一个CWnd派生的对象。它是从CSubclassWnd派生的,而CSubclassWnd又派生于CObject。在处理完消息之后,CAutoComplete调用CSubclassWnd::WindowProc,它以自己的方式经过原来的窗口过程传递消息到任何消息映射中的消息处理器。
一旦你明白了CSubclassWnd截获消息的方式,剩下的事情就是要完成实际的autocompletion功能了,也就是比较串列表和用户输入,CAutoComplete在虚函数 OnComplete 中完成这个工作。缺省实现 晴参见源代码。通过比较用户输入与内部串表,如果匹配的话便把匹配项显示在控制(编辑框)中,如果是组合框,CAutoComplete将所有匹配选择项显示在下拉框中。
不管怎么说,CAutoComplete的实现还是需要一些技巧的,当CAutoComplete截获消息EN_CHANGE (改变编辑框输入)或CBN_ EDITCHANGE时,它必须在调用SetWindowText设置新的
内容之前将自己关闭起来。否则,SetWindowText 将触发另一个CHANGE通知并且控制将离开你而去......嘿、嘿,试一试就知道了。
第二个诀窍是个小聪明。假设用户敲入“al”,CAutoComplete通过高亮“pha”三个字符来完成整个串“alpha”。用户现在想按Backspace删除掉“pha”,可怜的用户会发现Backspace不起作用。解决办法是当用户缩短(删除)输入的字符时就忽略掉文本完成动作。也就是说如果输入的内容匹配以前的输入,就忽略完成动作。如果我没有解释清楚,实在是对不起,你到程序代码中去找感觉吧。我添加了一个虚函数IgnoreCompletion来测试这种情况;如果这个函数返回FALSE,CAutoComplete只实现了文本完成,那明摆着算法不正确,我把它做成虚函数以便让你能重载。
最后一个诀窍是其它的虚函数,它们将OnComplete分割成更小的操作,使你比较容易改变基本的行为。例如,OnComplete 做的第一件事情是调用一个虚函数 GetMatches 来获取与用户输入匹配的列表(CStringArray)。
01.void CAutoComplete::OnComplete(CWnd* pWnd, CString s) 02.{ 03. CStringArray arMatches; // 匹配串 04. if (s.GetLength()>0 && 05. GetMatches(s, arMatches, m_iType==Edit)>0) { 06. DoCompletion(...); 07. } 08. m_sPrevious=s; // 记住当前串 09.}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 接口做了一个比较,见下表:
| CAutoCompleteWnd | IAutoComplete |
| 自己用 C++/MFC 实现,有充分的自主性。 | shell32.dll 中现成的 COM 对象, |
| 可用于所有 Win32 平台。 | 只有 Windows 2000 才具备 |
| 容易改写完成行为 | 无法改写特性行为 |
| 不用格式串 | Ctrl + Enter 格式串,如 www.%s.com |
| 不用注册表 | 需要默认的注册表格式串 |
| 不需要串枚举(IEnumString) | 必须实现 IEnumString |
| 可以进行完全控制 | 必须将就接口提供的功能 |
做这样的比较并不是说谁好谁坏,而是哪一个更适合你的需要。