在应用程序中集成自动完成功能
本文的很多内容翻译自使用自动完成这篇文章。
介绍
自动完成功能,就是用户在输入时,程序自动提示匹配用户输入的条目,并且/或者自动输入剩余部分,自从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:复制记录集或者记录集参数和/或复位当前记录位置。
- 字符串数组(以CStringArray为例)
- 使用预定义的自动完成资源
- 设置自动完成资源
如果需要,你可能会通过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();
}
如果你编写了自定义的自动完成资源,那么你可以自行编写修改设置的方法,例如限制最大枚举条目数目,或者根据用户的输入和上下文过滤枚举条目(这在枚举数据库记录时特别有用)。
- 使用IPersistFolder
- 绑定到编辑框
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”或者“搜索”这样的前缀。
参考文档
IAutoComplete and custom IEnumString implementation for WTL dialogs
http://www.codeproject.com/wtl/customautocomplete_wtl.asp
下面是我的基于数据库的IEnumString实现
if !defined(AFX_ENUMSTRING_H__4D5D61AD_CD0D_
477C
_
880F
_8E5EEB5B1E
8F
__INCLUDED_)
#define AFX_ENUMSTRING_H__4D5D61AD_CD0D_
477C
_
880F
_8E5EEB5B1E
8F
__INCLUDED_
#if _MSC_VER > 1000
#pragma once
#endif // _MSC_VER > 1000
// EnumString.h : header file
//
/
// CEnumString command target
#include <shldisp.h>
#include "esuihelper.h"
class _ES_UI_EXPORT CEnumString : public IEnumString
{
public:
CEnumString(); // protected constructor used by dynamic creation
// Attributes
public:
ULONG m_nRefCount;
// Operations
public:
STDMETHODIMP_(ULONG) AddRef();
STDMETHODIMP_(ULONG) Release();
STDMETHODIMP QueryInterface(REFIID riid, void** ppvObject);
STDMETHODIMP Next(ULONG celt, LPOLESTR* rgelt, ULONG* pceltFetched);
STDMETHODIMP Skip(ULONG celt);
STDMETHODIMP Reset(void);
STDMETHODIMP Clone(IEnumString** ppenum);
BOOL Bind(HWND p_hWndEdit, DWORD p_dwOptions = 0, LPCTSTR p_lpszFormatString = NULL);
VOID Unbind();
// Overrides
// ClassWizard generated virtual function overrides
//{{AFX_VIRTUAL(CEnumString)
//}}AFX_VIRTUAL
// Implementation
protected:
virtual ~CEnumString();
CComPtr<IAutoComplete> m_pac;
BOOL m_fBound;
// Generated message map functions
//{{AFX_MSG(CEnumString)
// NOTE - the ClassWizard will add and remove member functions here.
//}}AFX_MSG
};
/
//{{AFX_INSERT_LOCATION}}
// Microsoft Visual C++ will insert additional declarations immediately before the previous line.
#endif // !defined(AFX_ENUMSTRING_H__4D5D61AD_CD0D_
477C
_
880F
_8E5EEB5B1E
8F
__INCLUDED_)
// EnumString.cpp : implementation file
//
#include "stdafx.h"
#include "EnumString.h"
#ifdef _DEBUG
#define new DEBUG_NEW
#undef THIS_FILE
static char THIS_FILE[] = __FILE__;
#endif
/
// CEnumString
CEnumString::CEnumString()
{
m_fBound = FALSE;
m_nRefCount = 0;
}
CEnumString::~CEnumString()
{
}
/
// CEnumString message handlers
ULONG FAR EXPORT CEnumString::AddRef()
{
TRACE_LINE("CEnumString::AddRef/n");
return ::InterlockedIncrement(reinterpret_cast<LONG*>(&m_nRefCount));
}
ULONG FAR EXPORT CEnumString::Release()
{
TRACE_LINE("CEnumString::Release/n");
ULONG nCount = 0;
nCount = (ULONG) ::InterlockedDecrement(reinterpret_cast<LONG*>(&m_nRefCount));
if (nCount == 0)
delete this;
return nCount;
}
HRESULT FAR EXPORT CEnumString::QueryInterface(
REFIID riid, void FAR* FAR* ppvObject )
{
HRESULT hr = E_NOINTERFACE;
if (ppvObject != NULL)
{
*ppvObject = NULL;
if (IID_IUnknown == riid)
*ppvObject = static_cast<IUnknown*>(this);
if (IID_IEnumString == riid)
*ppvObject = static_cast<IEnumString*>(this);
if (*ppvObject != NULL)
{
hr = S_OK;
((LPUNKNOWN)*ppvObject)->AddRef();
}
}
else
{
hr = E_POINTER;
}
return hr;
}
STDMETHODIMP CEnumString::Next(ULONG celt, LPOLESTR* rgelt, ULONG* pceltFetched)
{
return E_NOTIMPL;
}
STDMETHODIMP CEnumString::Skip(ULONG celt)
{
return E_NOTIMPL;
}
STDMETHODIMP CEnumString::Reset(void)
{
return E_NOTIMPL;
}
STDMETHODIMP CEnumString::Clone(IEnumString** ppenum)
{
if (!ppenum)
return E_POINTER;
CEnumString* pnew = new CEnumString;
pnew->AddRef();
*ppenum = pnew;
return S_OK;
}
BOOL CEnumString::Bind(HWND p_hWndEdit, DWORD p_dwOptions /*= 0-*/, LPCTSTR p_lpszFormatString /*= NULL*/)
{
if ((m_fBound) || (m_pac))
return FALSE;
HRESULT hr = S_OK;
hr = m_pac.CoCreateInstance(CLSID_AutoComplete);
if (SUCCEEDED(hr))
{
if (p_dwOptions)
{
CComQIPtr<IAutoComplete2> pAC2(m_pac);
ATLASSERT(pAC2);
hr = pAC2->SetOptions(p_dwOptions); // This never fails?
pAC2.Release();
}
hr = m_pac->Init(p_hWndEdit, this, NULL, (LPOLESTR)p_lpszFormatString);
if (SUCCEEDED(hr))
{
m_fBound = TRUE;
return TRUE;
}
}
return FALSE;
}
VOID CEnumString::Unbind()
{
if (!m_fBound)
return;
ATLASSERT(m_pac);
if (m_pac)
{
m_pac.Release();
m_fBound = FALSE;
}
}
#include "../esuihelper/EnumString.h"
#include "DataType.h"
class CDataType;
class _ES_DATATYPE_EXPORT CEnumDataType : public CEnumString
{
public:
CEnumDataType(LPCTSTR lpszDataType);
virtual ~CEnumDataType();
CDataType* m_pDataType;
protected:
CString m_strDataType;
STDMETHODIMP Next(ULONG celt, LPOLESTR* rgelt, ULONG* pceltFetched);
STDMETHODIMP Skip(ULONG celt);
STDMETHODIMP Reset(void);
STDMETHODIMP Clone(IEnumString** ppenum);
ado20::_RecordsetPtr m_pRecordset;
};
CEnumDataType::CEnumDataType(LPCTSTR lpszDataType)
:m_strDataType(lpszDataType)
{
m_pDataType=g_pDataTypeManager->GetDataType(m_strDataType);
ASSERT(m_pDataType);
m_pRecordset.CreateInstance("ADODB.Recordset");
try{
if(m_pRecordset!=NULL){
if( m_pRecordset->State&adStateOpen){
return;
}
}
ESRecordsetOpen((LPCTSTR)m_pDataType->m_strSQLAutoComplete, _variant_t((IDispatch *)g_connection,true),
m_pRecordset,adOpenDynamic,adLockOptimistic, adCmdUnspecified);
m_pRecordset->Requery(adCmdUnknown);
if(m_pRecordset->BOF==VARIANT_FALSE)
m_pRecordset->MoveFirst();
}
catch(_com_error &e)
{
ESErrPrintProviderError(g_connection);
ESErrPrintComError(e);
}
}
CEnumDataType::~CEnumDataType()
{
try{
if(m_pRecordset!=NULL){
if( m_pRecordset->State&adStateOpen){
m_pRecordset->Close();
}
}
}
catch(_com_error &e)
{
ESErrPrintProviderError(g_connection);
ESErrPrintComError(e);
}
}
STDMETHODIMP CEnumDataType::Next(ULONG celt, LPOLESTR* rgelt, ULONG* pceltFetched)
{
if(m_pRecordset==NULL) return OLE_E_BLANK;
HRESULT hr = S_FALSE;
ZeroMemory(rgelt, sizeof(OLECHAR*) * celt);
try{
if (!celt) celt = 1;
for (ULONG i = 0; i < celt; i++){
if (m_pRecordset->EndOfFile== VARIANT_TRUE)
break;
_bstr_t bstrText=
(LPCTSTR)g_GetValueString(
m_pRecordset->Fields->Item[(LPCTSTR)m_pDataType->m_strAutoCompleteField]->Value);
if(bstrText.length()>0){
rgelt[i] = OLESTRDUP(bstrText);
if (pceltFetched)
*pceltFetched++;
}
m_pRecordset->MoveNext();
}
if (i == celt)
hr = S_OK;
}
catch(_com_error &e)
{
ESErrPrintProviderError(g_connection);
ESErrPrintComError(e);
return e.Error();
}
return hr;
}
STDMETHODIMP CEnumDataType::Skip(ULONG celt)
{
if(m_pRecordset==NULL) return OLE_E_BLANK;
try{
m_pRecordset->Move(celt,(long)adBookmarkCurrent);
}
catch(_com_error &e)
{
ESErrPrintProviderError(g_connection);
ESErrPrintComError(e);
return e.Error();
}
return S_OK;
}
STDMETHODIMP CEnumDataType::Reset(void)
{
if(m_pRecordset==NULL) return OLE_E_BLANK;
try{
m_pRecordset->Requery(adCmdUnknown);
if(m_pRecordset->BOF==VARIANT_FALSE)
m_pRecordset->MoveFirst();
}
catch(_com_error &e)
{
ESErrPrintProviderError(g_connection);
ESErrPrintComError(e);
return e.Error();
}
return S_OK;
}
STDMETHODIMP CEnumDataType::Clone(IEnumString** ppenum)
{
if (!ppenum)
return E_POINTER;
CEnumDataType* pnew = new CEnumDataType(m_strDataType);
pnew->AddRef();
*ppenum = pnew;
return S_OK;
}