Monday, March 14, 2005

 

Some notes on Auto-Complete

1.
翻译:jiangsheng
原文:http://msdn.microsoft.com/library/en-us/shellcc/platform/shell/programmersguide/shell_int/shell_int_programming/ac_ovw.asp

自动完成功能,就是用户在输入时,程序自动提示匹配用户输入的条目,并且/或者自动输入剩余部分,自从IE提供了表单和地址栏的自动完成功能之后,很多程序在用户界面中集成了这个功能。在适当使用时,它可以大大减少用户输入的时间。尽管这个功能如此有用,但是在平台开发工具包中甚至连一个示例都找不到。Paul DiLascia在2000年8月的C++ Q&A文章中使用子类化窗口类完成了这个功能,但是他的方法太依赖于MFC了,以至于不能很好的移植到非MFC的程序。

他的文章中最后比较了使用COM接口和他的方法之间的异同,有兴趣可以去看看。

Windows外壳中的自动完成

Windows的文件打开和保存对话框,以及运行对话框,IE的地址栏都具有自动完成功能。

这个功能也在Windows API中提供。Windows Shell Light API提供SHAutoComplete函数,可以把系统内建的自动完成资源CLSID_ACLHistory 、CLSID_ACLMRU和CLSID_ACListISF(说明见后)绑定到编辑框。这个函数使得你不必处理COM接口就可以提供自动完成功能,但是如果你想要更好地控制自动完成的行为或者列表条目,那么你需要使用一些COM接口,例如IAutoComplete和IACList。

创建一个简单的自动完成对象
你可以采用下列步骤创建一个简单自动完成对象。简单自动完成对象从一个资源来提取自动完成所需的字符串。为使代码明确起见,错误检查的代码被省略,下同。

创建自动完成对象
IAutoComplete *pac;
CoCreateInstance(CLSID_AutoComplete,
NULL,
CLSCTX_INPROC_SERVER,
IID_IAutoComplete,
(LPVOID*)&pac);
创建自动完成资源
你可以使用预定义的自动完成资源,或者编写你自己的自动完成资源。
使用预定义的自动完成资源
IUnknown *punkSource;
CoCreateInstance(clsidSource,
NULL,
CLSCTX_INPROC_SERVER,
IID_IACList,
(LPVOID*)&punkSource);
下列外壳对象具有IAutoComplete接口,可以用CoCreateInstance创建之后给你的应用程序提供自动完成功能。
CLSID_ACLHistory - IE维护,匹配当前用户的历史记录。
CLSID_ACLMRU - 也是IE维护,匹配当前用户的最近文档记录。
CLSID_ACListISF -匹配外壳命名空间(包括文件系统和虚拟目录,例如我的电脑和控制面板)
编写你自己的自动完成资源
IUnknown *punkSource;
CCustomAutoCompleteSource *pcacs = new CCustomAutoCompleteSource();
hr = pcacs->QueryInterface(IID_IUnknown,
(void **) &punkSource);
pcacs->Release();
你可以编写实现IEnumString接口的自动完成资源对象。IACList和IACList2的实现是可选的。
实现IEnumString接口
和其他IEnumXXXX接口一样,IEnumString接口有如下四个方法
HRESULT Next(ULONG celt, LPOLESTR * rgelt, ULONG * pceltFetched);
HRESULT Skip(ULONG celt);
HRESULT Reset(void);
HRESULT Clone(IEnumString ** ppenum);
你可以用MFC或者ATL的 方法快速构造实现IEnumString接口的对象,也可以自己手动实现IEnumString接口。通常,列表项来自字符串数组或者数据库。下面是这两种情况下IEnumString的实现方法的建议
字符串数组(以CStringArray为例)
Next:提取指定数量字符串,并且增加当前字符串索引。
Skip:增加当前字符串索引
Reset:当前字符串索引复位为0
Clone:复制字符串数组和当前字符串索引
数据库

Next:提取指定数量字符串,并且向后移动当前记录位置。
Skip:向后移动当前记录位置
Reset:复位当前记录位置到记录集开头
Clone:复制记录集或者记录集参数和/或复位当前记录位置。
设置自动完成资源
如果需要,你可能通过IACList或者IACList2接口设置一些选项来自定义自动完成资源,例如过滤某些列表条目。这对CLSID_ACListISF对象特别有用。可以查看IACList2::SetOptions的文档来查看有效的选项。
IACList2 *pal2;
if (SUCCEEDED(punkSource->QueryInterface(IID_IACList2, (LPVOID*)&pal2))){
pal2->SetOptions(ACLO_FILESYSONLY);
pal2->Release();
}
对于某些特定资源,你可能需要查询其他接口来进行更加复杂的设置,例如在资源管理器或者IE浏览到一个新的目录时,自动完成的列表内容也随之变化。对于CLSID_ACListISF对象,你可以设置ACLO_CURRENTDIR让它来枚举当前目录的内容,这样你需要查询它的IACList接口,并且设置它的当前 位置。
有两种方法可以设置当前目录。如果传递的只是目录的话,它们是等价的。
使用IPersistFolder
你可以用这个接口来告诉自动完成对象当前的位置。因为当前位置可以不是真实的目录,例如是桌面或者是我的电脑所以这个方法比下一个更加灵活。
IPersistFolder *ppf;
extern LPITEMIDLIST pidlCurrentDirectory;//这个是有效的命名空间位置
if (SUCCEEDED(pal->QueryInterface(IID_IPersistFolder, (LPVOID*)&ppf))){
ppf->Initialize(pidlCurrentDirectory);
ppf->Release();
}
使用ICurrentWorkingDirectory
你可以通过对象的这个接口把当前目录传递给对象。
ICurrentWorkingDirectory *pcwd;
if (SUCCEEDED(pal->QueryInterface(IID_ICurrentWorkingDirectory, (LPVOID*)&pcwd))){
pcwd->SetDirectory(pwszDirectory);
pcwd->Release();
}
如果你编写了自定义的自动完成资源,那么你可以自行编写修改设置的方法,例如限制最大枚举条目数目,或者根据用户的输入和上下文过滤枚举条目(这在枚举数据库记录时特别有用)。

绑定到编辑框
IAutoComplete::Init将自动完成资源绑定到一个编辑框。有一些未公开的方法可以获得COMBOBOX控件中的编辑框的句柄,但是COMBOBOXEX控件提供了直接访问其中的编辑框的方法 (CBEM_GETEDITCONTROL),所以推荐使用COMBOBOXEX控件。
extern HWND hwndEdit;//要添加自动完成的目标窗口
extern LPUNKNOWN punkSource;//第二步创建的自动完成资源的IUnknown接口指针。
pac->Init(hwndEdit, punkSource, NULL, NULL);
可以查看IAutoComplete::Init来的文档了解其他参数的含义。
IAutoComplete::Init将会增加对象的引用计数,并且在编辑框销毁时减少对象的引用计数,见步骤6。
一个自动完成对象一次只能绑定到一个窗口。
设置自动完成对象的行为
如果需要的话,可以通过IAutoComplete2接口设置选项来更改自动完成对象的行为。可以查看IAutoComplete2::SetOptions的文档来了解其他参数。
IAutoComplete2 *pac2;
if (SUCCEEDED(pac->QueryInterface(IID_IAutoComplete2, (LPVOID*)&pac2))){
pac2->SetOptions(ACO_AUTOSUGGEST);
pac2->Release();
}
释放对象
pac->Release();
punkSource->Release();
自动完成对象在释放后仍绑定到编辑框(因为IAutoComplete::Init增加了对象的引用计数),并且在编辑框销毁时自动释放。在还需要访问对象的时候不应该提前销毁对象。例如,你可能在自动完成对象绑定到对话框之后,仍需要通过步骤3的方法设置自动完成资源。
创建复合自动完成对象
复合自动完成对象从多个自动完成资源中查找匹配的项目。例如,IE的地址栏查找访问过的URL和当前目录中的内容。可以采用下列步骤创建一个复合自动完成对象。

创建自动完成对象
IAutoComplete *pac;
CoCreateInstance(CLSID_AutoComplete,
NULL,
CLSCTX_INPROC_SERVER,
IID_IAutoComplete,
(LPVOID*)&pac);
创建复合自动完成资源对象管理器。它将多个自动完成资源组合成一个自动完成资源。
IObjMgr *pom;
CoCreateInstance(CLSID_ACLMulti,
NULL,
CLSCTX_INPROC_SERVER,
IID_IObjMgr,
(LPVOID*)&pom);
对每一个简单自动完成对象进行前面一个例子的步骤2和3,完成创建和初始化工作。
连接简单自动完成对象到复合自动完成资源对象管理器。
extern LPUNKNOWN punkSource;//第3步创建的自动完成资源的IUnknown接口指针。
pom->Append(punkSource);
绑定到编辑框
这个步骤和前面一个例子的步骤4一样,除了把自动完成资源的IUnknown接口指针换成了复合自动完成资源对象管理器的IUnknown接口指针之外。
pac->Init(hwndEdit, pom, NULL, NULL);
设置自动完成的选项
同前面一个例子。
释放对象
pac->Release();
pom->Release();
punkSource->Release(); /* 每一个步骤3的创建对象都要释放 */
你可以在使用对象之后立刻释放对象,但是你可能需要保留对象接口指针以便以后修改选项。

自动完成的模式
自动完成具有两种互相独立的模式:自动扩展和自动建议。你可以调用IAutoComplete2::SetOptions来分别启用/禁用这两种模式。

自动扩展:在此模式下,自动完成功能自动将最可能的候选字符串添加到当前子复制或并且高亮显示。编辑框表现得和扩展的字符串已经输入并高亮显示一样。如果用户继续输入字符,那么它们被添加到已经输入的部分字符串。如果用户输入的字符和扩展的部分的首字符一致,那么这个字符的高亮显示被关闭,剩余的扩展字符串继续保持高亮显示状态;否则自动完成试图用用户输入的新字符串产生一个新的候选扩展字符串,并和前面一样添加到当前字符串末尾并高亮显示。如果没找到候选扩展字符串,那么编辑框的行为和没有自动完成功能时一样。这个过程持续到用户完成输入。
自动提示:在此模式下,自动提示显示一个下拉列表,显示一个或多个建议的完成字符串。用户可以选择其中一个,通常是用鼠标,或者继续输入以减少匹配的列表项数目。在输入过程中,列表内容可能根据输入的字符串更改。如果你在IAutoComplete2::SetOptions的dwFlag参数中设置了ACO_SEARCH标志,那么自动完成在列表末尾提供根据当前的部分字符串搜索的选项。这个选项甚至在没有提示的候选字符串时也显示。如果用户选择了这个选项,应用程序应该启动一个搜索引擎来帮助用户。
尽管可以通过字符串匹配来判断用户是否选择了搜索选项,但是由于语言的不同,搜索选项的前缀也不一样。你应该考虑如何去掉“Search for”或者“搜索”这样的前缀。

2.
补充一个C#的实现
from http://vbaccelerator.com/home/NET/Code/Libraries/Shell_Projects/Auto-File_Completion_for_Text_Boxes_and_Combo_Boxes/article.asp

Version 5 of the Shell, shipped with Windows 2000 and ME or above, provides the ability to add Auto-Complete to any Text Box. This sample provides two simple extensions to the TextBox and ComboBox classes to add AutoComplete to your application.

Shell AutoCompletion
There are two variants to the Auto-Completion facilities provided with version 5 of the Windows Shell. The simple method, presented here, provides a hardcoded auto-completion list based on either files, directories or URLs on the system. A more sophisticated technique which will be the basis for a future article is to implement the COM IEnumString, IAutoComplete and IAutoComplete2 interfaces, which allows you to specify arbitrary auto-complete strings.

Note that if you plan to target older systems, such as NT4 or Win 98/SE, you should enclose any attempt to initialise Auto-Complete in a Try - Catch block since the call is not supported.

Using the simple version of the Auto-Complete method is very straightforward. The shell exposes an API call, SHAutoComplete, which takes a handle to a the TextBox to start auto-completing on, and a flag parameter which specifies which type of auto-completion should be applied.

Here's the code for the extended Text Box which provides auto-completion:

using System;
using System.Windows.Forms;
using System.Runtime.InteropServices;

namespace vbAccelerator.Controls.TextBox
{
///
/// Adds Shell File System and URL AutoCompletion facilities to
/// a text box
///
public class AutoCompleteTextBox : System.Windows.Forms.TextBox
{
#region Unmanaged Code
[Flags]
public enum SHAutoCompleteFlags : uint
{
SHACF_DEFAULT = 0x0,
SHACF_FILESYSTEM = 0x1,
SHACF_URLHISTORY = 0x2,
SHACF_URLMRU = 0x4,
SHACF_USETAB = 0x8,
SHACF_URLALL = (SHACF_URLHISTORY | SHACF_URLMRU),
SHACF_FILESYS_ONLY = 0x10,
SHACF_FILESYS_DIRS = 0x20,
SHACF_AUTOSUGGEST_FORCE_ON = 0x10000000,
SHACF_AUTOSUGGEST_FORCE_OFF = 0x20000000,
SHACF_AUTOAPPEND_FORCE_ON = 0x40000000,
SHACF_AUTOAPPEND_FORCE_OFF = 0x80000000
}

[DllImport("shlwapi.dll")]
private static extern int SHAutoComplete (
IntPtr hwndEdit,
AutoCompleteTextBox.SHAutoCompleteFlags dwFlags );
#endregion

#region Member Variables
private AutoCompleteTextBox.SHAutoCompleteFlags autoCompleteFlags =
SHAutoCompleteFlags.SHACF_FILESYS_ONLY;
private bool flagsSet = false;
private bool handleCreated = false;
#endregion

#region Implementation

///
/// Gets/sets the flags controlling automcompletion for the
/// text box
///
public AutoCompleteTextBox.SHAutoCompleteFlags AutoCompleteFlags
{
get
{
return this.autoCompleteFlags;
}
set
{
this.autoCompleteFlags = value;
this.flagsSet = true;
if (handleCreated)
{
SetAutoComplete();
}
}
}

protected override void OnHandleCreated ( System.EventArgs e )
{
// call this first as SHAutoComplete may not be supported
// on the OS
base.OnHandleCreated(e);
// don't do anything if we're in design mode:
if (!this.DesignMode)
{
// remember we've created the handle for any future
// get/ set
handleCreated = true;

// if we've provided some flags then start autocompletion:
if (flagsSet)
{
SetAutoComplete();
}
}
}

private void SetAutoComplete()
{
SHAutoComplete(this.Handle, this.autoCompleteFlags);
}

///
/// Constructs an auto-complete capable text box but
/// does not automatically start auto-completion.
///
public AutoCompleteTextBox() : base()
{
}

///
/// Constructs an auto-complete capable text box and
/// starts auto-completion with the specified flags.
///
/// Flags controlling
/// auto-completion
public AutoCompleteTextBox(
AutoCompleteTextBox.SHAutoCompleteFlags autoCompleteFlags
) : this()
{
// Handle will not be available at this point; we need
// to wait for HandleCreated:
this.autoCompleteFlags = autoCompleteFlags;
this.flagsSet = true;
}

#endregion
}
}

AutoCompletion and ComboBoxes
Using the function with a ComboBox isn't much more difficult; all you need to do is to find the handle to the TextBox within the ComboBox itself and call the SHAutoComplete API on that. Some versions of Windows (XP) provide a message you can use to get the handle to the TextBox, however, it is just as simple to use the Windows FindWindowEx API call to get the handle, and this works regardless of the type of ComboBox or OS version. The code to do this is as follows:

using System.Runtime.InteropServices;

[DllImport("user32.dll", CharSet = CharSet.Auto)]
private static extern IntPtr FindWindowEx (
IntPtr hWnd1,
IntPtr hWnd2,
[MarshalAs(UnmanagedType.LPTStr)] string lpsz1,
IntPtr lpsz2);

...

IntPtr textHandle = FindWindowEx(
comboHandle,
IntPtr.Zero,
"EDIT",
IntPtr.Zero);



<< Home

This page is powered by Blogger. Isn't yours?