65.9K
CodeProject 正在变化。 阅读更多。
Home

WTL 对话框的 IAutoComplete 和自定义 IEnumString 实现

starIconstarIconstarIcon
emptyStarIcon
starIcon
emptyStarIcon

3.47/5 (12投票s)

2002年6月1日

MIT

13分钟阅读

viewsIcon

212119

downloadIcon

3629

一个自定义的IEnumString实现,它与IAutoComplete一起工作,为WTL应用程序中的编辑框和组合框控件提供自动完成功能。

Sample Image - CustomAutocomplete_wtl.jpg

引言

自从IE在其地址栏和表单字段中提供了那个精巧的“前瞻”功能以来,许多应用程序都将该功能融入了它们的GUI设计中。自动完成编辑控件和组合列表是用户非常有价值的工具(如果使用得当),因为它们可以减少向应用程序提供反馈所需的时间。我在网上找到的关于实现此类功能的代码,要么过于定制化(作为Shell提供的功能的替代方案),要么特定于MFC。Paul DiLascia 在MSJ(我记得是1999年6月刊)中探讨了第一种情况,而 Cassio Goldschmidt 在2000年11月版的VCDJ上的一篇文章则讨论了第二种情况(文章末尾有两者的链接)。而且,在Google上只能找到这些——即使是PSDK也不包含这方面的示例。然而,我需要一些不那么“强大”(在我看来)并且可以在不接触MFC的情况下与WTL/ATL一起工作的解决方案。所以,尽管我并不情愿,我还是决定写一个,现在我们就开始。

Windows Shell 中的自动完成功能

在深入研究代码之前,让我们先回顾一下Win32 Shell为应用程序提供自动完成功能的一些选项。Shell(实际上是从BROWSEUI.DLL导出的)提供的主要COM接口称为IAutoComplete。还有一个名为IAutoComplete2的接口,除非你需要自定义自动完成的行为,否则它并不重要,但我们稍后会谈到它。

IAutoComplete有三种实现,由三个匹配的CLSID标识,它们提供不同类型的自动完成列表:

  • CLSID_ACLHistory - 提供与当前用户的历史列表匹配的项,由IE Shell管理。
  • CLSID_ACLMRU - 提供与当前用户的最近文档列表匹配的项,同样由IE Shell管理。
  • CLSID_ACListISF - 提供与Shell命名空间(包括文件系统)匹配的项。

如果这些实现能满足你的特定需求,它们各自都有用,并且在许多情况下就足够了。例如,Windows 2000和XP在“打开”和“保存”通用对话框的文件名编辑框中使用了CLSID_ACListISF,这样你就可以看到任何给定文件夹中所有文件和子文件夹的前瞻列表。运行对话框和Shell任务栏中的地址栏(如果激活)也是如此。

顺便说一句,其中大部分信息都包含在MSDN中,其中有一个“IAutoComplete教程”。问题在于,关于创建和使用自定义字符串枚举器以及与IAutoComplete结合使用的议题在那里并没有真正被覆盖,除了基本步骤。在某些情况下,你不想枚举收藏夹、文件或最近使用的文档,而是邮政编码、鸟类、电话号码或奶酪的种类。或者任何其他东西。如果你正在寻找这个,并且你正在使用WTL,那么这个示例将会有所帮助。

在继续之前,最后一点说明:上面提到的三个IAutoComplete实现可以通过使用从SHLWAPI.DLL导出的SHAutoComplete函数来启用,用于任何给定的编辑控件。只需将编辑控件的HWND和几个标志传递进去,就可以了。无需处理COM,即可实现自动完成。可能让你远离这个函数的唯一情况是你需要复合自动完成——通过IObjMgr对象绑定的同一个编辑控件上的多个列表——或者对CLSID_ACListISF进行更精细控制,这可以通过IPersistFolder完成。MSDN教程也简要地涵盖了这一点(链接在文章末尾)。

还在跟着吗?这可能意味着你对为你的WTL对话框或文档/视图应用程序创建自定义列表枚举器感兴趣。是的,我们已经准备好了。

IAutoComplete 和 IEnumString

为了创建一个对象来管理任意列表的任意项,并将其提供给IAutoComplete,你需要实现IEnumString接口。如果你在想,IEnumString与COM定义的其他IEnumXXXX接口完全相同。

    HRESULT Next(ULONG celt, LPOLESTR * rgelt, ULONG * pceltFetched);
    HRESULT Skip(ULONG celt);
    HRESULT Reset(void);
    HRESULT Clone(IEnumString ** ppenum);

正如你所见,它与IEnumVARIANTIEnumMoniker等完全相同。没有什么特别之处,除了显而易见的是,被枚举的对象是字符串(准确地说是OLESTR),而不是变体或标识符。

如果你曾经实现过枚举器,你就会知道实现它相对比较简单,所以我不会在这里列出代码。但是,我想对它做一个小小的澄清。当我开始编写代码时,IEnumString实现的原型看起来是这样的:

   class CCustomAutoComplete :
        public CComObject<CComEnum<IEnumString,
             &IID_IEnumString, LPOLESTR,
                         _Copy<LPOLESTR>>>

后来我在VCDJ上找到了上述文章,其中定义了完全相同的原型(作为typedef,但仍然如此),所以我决定完全不使用ATL模板来控制IUnknownIEnumString的实现,而是直接编码。我已经很久没有编写过哪怕是最精简的原始COM包装器了,所以这有点有趣。但无论如何,就调用代码而言,这应该没有区别。

持久化支持

代码围绕一个名为CCustomAutoComplete的单个类展开,该类旨在不仅提供字符串给IAutoComplete,还可以作为IAutoComplete对象本身的“管理器”,以及提供对单个字符串元素和持久化的控制(如果需要)。在内部,CCustomAutoComplete使用一个CString对象的CSimpleArray(这是WTL的CString,在atlmisc.h中)来维护其元素列表。

除了提供字符串给IAutoComplete(这很简单)之外,实现自定义枚举器的第二个考虑是存储。在大多数情况下,你希望有一种方法来存储这些字符串,并将它们从应用程序的一次会话持久化到下一次。我的解决方案是使用注册表。使用标准的注册表函数(特别是枚举函数),加载和保存列表相对简单。尽管类本身能够管理持久化,但使用注册表不是必需的。你仍然可以向它传递一个字符串数组,或者简单地逐个添加项,它们就会正常显示在列表中。当然,你必须从某个地方获取字符串。

如果你确实决定使用注册表,你必须使用应用程序主注册表键的一个子键,该子键不能用于其他任何地方。如果你使用多种类型的列表,请将每种列表存储在自己的子键中。例如,如果你的应用程序的主注册表键是HKEY_CURRENT_USER\Software\Widgets,并且你有一个名为Blue Widgets的列表,你想让它出现在Select Blue Widget对话框的下拉列表中,可以像这样调用SetStorageSubkey()

    // Where m_pac is an instance of CCustomAutoComplete

    m_pac->SetStorageSubkey(HKEY_CURRENT_USER,
        _T("Software\\Widgets\\Blue Widgets"));

这样,如果你有一个名为Red Widgets的列表,你可以使用一个单独的子键,以此类推。请记住,你不能将同一个IAutoComplete实例绑定到两个不同的编辑控件上,所以尽管你可以为两个不同的控件使用同一个子键(通常不推荐!),但你仍然需要使用两个不同的CCustomAutoComplete实例,每个编辑控件一个。

实际的存储格式非常简单。该类将在指定的子键中为自动完成列表中的每个唯一条目存储一个相同的键值对。它看起来并不怎么高级,但它完成了工作,因为它使得使用简单的、直接的API调用能够快速找到所需项,从而更加容易。所以,假设你在给定子键中存储水果用于枚举。注册表编辑器会显示类似这样的内容:

Red AppleRed Apple
KiwiKiwi
WatermelonWatermelon

......以此类推。非常简单的东西。如果你觉得这不够满足你的应用程序需求,你可以随时修改代码。私有的LoadFromStorage()函数及其相关函数足够独立,你可以修改它们,仍然填充类其余部分所期望的CString数组。

关于存储功能的最后一点。该类假定它拥有由你的应用程序指定的子键的完全所有权。所以不要在里面存储其他任何东西,否则下次当你绑定到同一个键时,该类将加载碰巧在里面的任何内容,用户看到的列表将会是,嗯,奇怪的。

实现

CCustomAutoComplete提供的类方法如下:

CCustomAutoComplete::CCustomAutoComplete()
默认构造函数。

CCustomAutoComplete::CCustomAutoComplete(const HKEY p_hRootKey, const CString& p_sSubKey)
用一个注册表键初始化对象作为存储。有关更多信息,请参阅SetStorageSubkey()函数。

CCustomAutoComplete::CCustomAutoComplete(const CSimpleArray<CString>& p_sItemList)
构造函数。用一个CString数组初始化对象作为存储。

BOOL CCustomAutoComplete::SetList(const CSimpleArray<CString>& p_sItemList)
设置用于枚举的CString数组。

BOOL CCustomAutoComplete::SetStorageSubkey(HKEY p_hRootKey, const CString& p_sSubKey)
BOOL CCustomAutoComplete::SetStorageSubkey(LPCTSTR p_lpszSubKey, HKEY p_hRootKey = HKEY_CURRENT_USER)
设置类将加载和保存项的注册表根目录。将p_hRootKey参数设置为其中一个HKEY_*常量。你也可以传递一个打开的HKEY句柄,但说实话我没有尝试过。该类将尝试以KEY_READ | KEY_WRITE权限打开密钥,然后返回(如果不存在则创建)。如果发生任何错误(注册表权限或其他),返回值将是FALSE。HKEY句柄将在类实例销毁之前保持打开状态。

BOOL CCustomAutoComplete::Bind(HWND p_hWndEdit, DWORD p_dwOptions = 0, LPCTSTR p_lpszFormatString = NULL)
初始化内部IAutoComplete指针,设置选项(如果需要),并将其绑定到由p_hWndEdit标识的编辑控件。可选的p_dwOptions参数是一个掩码,包含一个或多个对应于可以传递给IAutoComplete2::SetOptions()的ACO_*标志的值。我不会在这里列出它们,因为它们可以从MSDN参考中获取。p_lpszFormatString参数用于支持IAutoComplete::Init()函数的最后一个参数,该参数指出,如果你指定一个看起来像这样的掩码“http://www.%s.com/”,并且用户输入“codeproject”,然后按CTRL+ENTER,则最终应用于绑定编辑控件的字符串将是“https://codeproject.org.cn/”。但是,我在Windows 2000上无法使其工作。请注意,如果确实使其工作,ACO_FILTERPREFIXES标志将非常有用,因为你可以过滤诸如“www”和“http://”之类的常见字符串片段。

VOID CCustomAutoComplete::Unbind()
释放内部IAutoComplete指针,但不清除项列表。除了在不再需要类实例时调用此函数外,还可以通过再次调用Bind()来将同一个列表(在注册表中或其他地方)绑定到另一个编辑控件。

BOOL CCustomAutoComplete::AddItem(CString& p_sItem)
BOOL CCustomAutoComplete::AddItem(LPCTSTR p_lpszItem)
向列表中添加新项。如果项添加成功,则返回TRUE。如果项已存在于列表中,并且/或者如果正在使用注册表持久化且无法移除该项,则返回FALSE。

BOOL CCustomAutoComplete::RemoveItem(CString& p_sItem)
BOOL CCustomAutoComplete::RemoveItem(LPCTSTR p_lpszItem)
从列表中移除指定的项。如果项移除成功,则返回TRUE。如果项不在列表中,并且/或者如果正在使用注册表持久化且无法移除该项,则返回FALSE。

INT CCustomAutoComplete::GetItemCount()
简单地返回当前类实例所持有的项数。

BOOL SetRecentItem(const CString& p_sItem)
BOOL GetRecentItem(CString& p_sItem)
设置/返回添加到或从自动完成列表中使用的“最近”项。如果您希望在对话框加载时将编辑控件设置为给定自动完成列表的最新值,此方法非常方便。您必须在每次适当的时候设置和检索字符串——该类无法自动执行此操作。

BOOL CCustomAutoComplete::Clear()
清除内部项列表,并且如果正在使用注册表持久化,则清除根键下的所有项。但是,它不会销毁内部IAutoComplete指针,也不会将其与编辑控件解绑。

BOOL CCustomAutoComplete::Disable()
调用IAutoComplete::Enable(FALSE)来关闭自动完成。

BOOL CCustomAutoComplete::Enable()
调用IAutoComplete::Enable(TRUE)来打开自动完成。如果您在某个时候调用了Disable(),您才需要调用此函数,因为一旦调用了Bind(),自动完成默认就是启用的。

如您所见,类接口非常简单。接下来,是一些关于如何使用它的技巧。

用法

使用CCustomAutoComplete非常简单。首先,决定是否需要通过注册表进行存储。使用适当的构造函数在堆上创建一个类实例(或在创建后初始化它),根据需要添加项,然后调用Bind()函数将其绑定到您选择的EDIT控件。然后根据您的需求插入或删除新项——例如,一个列出URL的自动完成文本框可能会在用户输入后添加一个新项,并且您的程序可以成功ping通该域,或者如果站点无法访问则从列表中删除它。以下是演示项目中包含的代码的一部分:

private:
    CCustomAutoComplete* m_pac;

    ...


    // When the dialog initializes...

    LRESULT OnInitDialog(UINT /*uMsg*/,
                  WPARAM /*wParam*/, LPARAM /*lParam*/,
                                    BOOL& /*bHandled*/)
    {
        ...

        m_pac = new CCustomAutoComplete(HKEY_CURRENT_USER,
                       _T("Software\\VBBox.com\\
                       StgAutoCompleteDemo\\Recent"));

        m_pac->Bind(GetDlgItem(IDC_TXTAUTOCOMPLETE),
                 ACO_UPDOWNKEYDROPSLIST | ACO_AUTOSUGGEST);

        ...

演示还包括了在运行时从列表移除/添加项所需的其他几个调用。当您不再需要该类时,只需调用Unbind()将其释放(不要对指针使用delete)。

除此之外,还有几点需要记住:

  • 该代码假定您在某个时候包含了<atlmisc.h>,因为CString在该文件中定义。我见过的大多数(也做过的)WTL代码都使用了CString,所以这可能不是什么大问题。当然,您始终可以更改内部数组以使用其他内容。
  • 您不能使用单个CCustomAutoComplete实例并将其绑定到多个编辑控件。每个控件使用一个实例。这与其说是类的限制,不如说是IAutoComplete本身的限制。
  • 请注意您使用的注册表键,否则您可能会发现自己向用户呈现了错误的列表(有关更多信息,请参阅前面的存储讨论)。
  • 您可以在绑定到编辑控件后,使用SetStorageSubkey()函数在不同的存储子键之间切换同一个CCustomAutoComplete实例,或者使用SetList()函数使用不同的项集。当单个编辑控件必须根据情况或用户输入显示不同的列表时,这非常方便。
  • 由于IAutoComplete仅在Windows 2000和XP中实现(MSDN如是说),该类在整个过程中都假定为Unicode。值得思考。
  • 该类不包含“卸载”自身子键的功能,因为它不知道父键。如果您的应用程序在用户卸载时删除了其整个注册表项,这应该不是问题。

总结

希望有人觉得这个类有用。代码在现有工具和应用程序中得到了相对充分的测试,但肯定没有进行详尽的测试。我非常乐意听到任何bug或问题!下面是一些关于IAutoComplete的更多信息链接:

修订历史

2002年6月20日 - 初始修订
2002年6月20日 - 代码部分重新格式化
2002年6月20日 - 文本重新格式化

© . All rights reserved.