同自动化浏览器(http://blog.joycode.com/jiangsheng/archive/2005/10/20/65489.aspx)相比,自动化浏览器控件(WebBrowser Control) 在应用程序中更加常用。从Outlook的预览窗格到Maxthon这样的基于IE引擎的浏览器,从无界面的HTML分析器到Norton Antivirusd的主界面,浏览器控件在众多领域被用作各种各样的用途。这也使得有必要根据具体的用户需求自定义浏览器控件的行为。
在应用程序中加入浏览器控件
集成浏览器控件的最简单的方法是找一个支持ActiveX的集成开发环境,在工具箱中加入Microsoft Web Browser这个控件,往表单上拖一个这个控件就可以完成工作。你甚至可以用集成开发环境添加ActiveX的事件处理函数。如果要直接导入ActiveX的话,建议使用mehrcpp的vbMHWB控件(http://www.codeproject.com/atl/vbmhwb.asp)。这个控件在浏览器控件的基础上进行了扩展,暴露了很多底层接口。
通常导入ActiveX就可以满足大部分需求 ,但是有些类库中也集成了浏览器控件,并且提供了更多的功能,例如MFC的CHTMLView和CDHtmlDialog,ATL的HTML Control,以及.Net 2.0中的Windows.Forms.WebBrowser。如果使用Visual C++来进行非托管编程,那么建议使用MFC或者ATL的封装类,或者使用vbMHWB控件。托管编程中当然首选Windows.Forms.WebBrowser。除非这些类的BUG影响到了应用程序的开发,否则建议使用这些功能更加强大的封装类。
在使用浏览器控件及其封装类的时候要注意一些已知问题
常见任务
在集成浏览器控件之后,可以完成基本的网页浏览,但是对于不同的任务,也需要进一步的处理,例如设置控件的属性、为控件添加事件处理、操作HTML文档等等。
修改浏览器控件的属性
这在集成开发环境中可以很容易地设置,也可以自己实现容器来设置,但是CHTMLView这样的封装类没有这个选项(http://support.microsoft.com/kb/197921)。
- 链接目标解析。对于用浏览器控件来做浏览器的场合来说,需要将浏览器的RegisterAsBrowser属性设置为true。这使得Internet Explorer在解析HTML链接的target属性指定的目标窗口时可以找到这个窗口。
- 禁用拖放。对于使用浏览器控件来做预览窗格的场合来说,需要将浏览器的RegisterAsDropTarget属性设置为false。这使得窗口不接受拖进来的文件和链接。
- 禁用消息框。对于用浏览器控件来做HTML分析器的场合来说,有时需要屏蔽脚本产生的消息框以避免阻塞程序运行。这可以通过设置浏览器的Silent属性来实现,或者实现IDocHostShowUI::ShowMessage。
捕获浏览器控件的事件
集成开发环境中可以也很容易地添加浏览器的事件处理函数。比较常用的事件包括
操作MSHTML文档
通常HTML分析和浏览器自动化程序都需要分析网页的结构,找到需要操作的元素。这需要对网页的结构进行分析,找到目标元素的标识方法。 一些常用的操作包括:
在页面包含框架的时候,可能需要跨框架访问HTML文档。可以通过查询框架元素所支持的IWebBrowser2接口或者IHTMLWindow2接口来访问框架中的文档(http://support.microsoft.com/kb/196340),但是也有可能因为安全设置而无法访问(http://support.microsoft.com/kb/167796)。
在浏览器控件中显示其它类型的文档时,可以用IWebBrowser2的document属性来访问ActiveX文档,例如在显示Microsoft Word时,IWebBrowser2的document属性就是Word的文档对象,在显示文件夹的时候,IWebBrowser2的document属性就是文件夹对象等等。
扩展浏览器的宿主
浏览器控件在创建时会查询ActiveX容器的IOleClientSite的实现的如下接口:IDocHostUIHandler, IDocHostUIHandler2 and IDocHostShowUI。
虽然在无法自定义ActiveX容器的情况下可以用ICustomDoc::SetUIHandler来挂接IDocHostUIHandler到浏览器控件,但是这样也会造成内存泄漏(http://support.microsoft.com/kb/893629)。一些类库,例如MFC、ATL和.Net类库都实现了IDocHostUIHandler接口。
除了专门用于浏览器用途的程序之外,通常都需要自定义浏览器控件的上下文菜单。这需要实现IDocHostUIHandler::ShowContextMenu。通常的实现包括完全禁用上下文菜单、完全替换上下文菜单、以及修改部分上下文菜单。经常被从上下文菜单中移除的菜单项包含查看源代码、刷新和属性。一种替代的方案是在容器中过滤右键消息(http://support.microsoft.com/kb/231578)。
与浏览器相比,一些Internet Explorer的宿主功能在浏览器控件中并不是默认启用。在某些场合,默认启用的宿主功能可能并非预期。这时需要实现IDocHostUIHandler::GetHostInfo。可以通过实现IDocHostUIHandler::GetHostInfo来自定义的功能包括:
- 自动完成功能。对于用浏览器控件来做浏览器的场合来说,这个功能是有必要启用的。启用的方法是设置DOCHOSTUIFLAG_ENABLE_FORMS_AUTOCOMPLETE位
- 如果浏览器中的链接网址包含非ASCII的字符,那么需要实现IDocHostUIHandler::GetHostInfo,并且在返回的DOCHOSTUIINFO结构中设置dwFlags成员的DOCHOSTUIFLAG_URL_ENCODING_ENABLE_UTF8位。这使得网址会在发送之前用UTF-8编码。
- 3D边框、滚动条,禁用文字选择功能和禁用页面上的脚本。
- 对于使用浏览器控件来做HTML编辑器的场合来说,有时需要修改默认的页面样式。这都需要实现IDocHostUIHandler::GetHostInfo(http://support.microsoft.com/kb/328803)。注意在有些版本的IE中IDocHostUIHandler::GetHostInfo只在MSHTML被初始化的时候被调用,所以如果你需要在MSHTML被初始化之后使你的修改生效,你需要浏览到一个Word之类的非HTML Active document文档,之后再浏览回来。
在使用浏览器控件来做数据录入界面的场合,需要更改浏览器控件默认的Tab键处理使得用户可以使用Tab键切换到容器中的其他控件。这需要实现IDocHostUIHandler::TranslateAccelerator来自定义浏览器控件的快捷键处理。对于MFC这样用消息钩子来做消息预处理的可自定义容器来说,也可以用PreTranslateMessage来过滤F5键盘消息,而不是实现IDocHostUIHandler::TranslateAccelerator。
在脚本中调用应用程序对浏览器控件的扩展,这需要实现IDocHostUIHandler::GetExternal。使用.Net的WebBrowser控件的话设置ObjectForScripting属性就可以了。
对于用浏览器控件来做HTML分析器的场合来说,有时需要屏蔽脚本产生的消息框。这需要实现IDocHostShowUI::ShowMessage,或者设置浏览器的Silent属性。
另外,浏览器也会查询IOleClientSite来获得其它的服务信息,例如
其他控制
对于用浏览器控件来做HTML分析器的场合来说,有时需要禁用浏览器的脚本、ActiveX或者图片下载。这可以通过在容器中实现IDispatch,处理DISPID_AMBIENT_DLCONTROL来做到(http://msdn.microsoft.com/library/default.asp?url=/workshop/browser/overview/Overview.asp)。
看来离线浏览的控制并不能用这种方法来控制(http://support.microsoft.com/kb/247336)。不过你可以自己编写一个HTTP层传递 BINDF_OFFLINEOPERATION标志 (http://groups-beta.google.com/group/microsoft.public.inetsdk.programming.mshtml_hosting/msg/76bf4910a289d4b3)
在浏览器控件中java小程序可能不能正常运行,如果使用Sun JVM1.4之后的版本,可以用SetEnvironmentVariable 来设置JAVA_PLUGIN_WEBCONTROL_ENABLE为1来启用Sun JVM。
默认情况下在页面载入时会有点击声。屏蔽点击声的一个方法是在程序运行时修改注册表键(http://support.microsoft.com/kb/201901),另一个方法是将浏览器控件隐藏,在调用Navigate2之后再显示,但是这也需要锁定控件的更新区域(LockWindowUpdate)以避免闪烁。在IE7中,也可以调用 CoInternetSetFeatureEnabled函数,传递FEATURE_DISABLE_NAVIGATION_SOUNDS来禁用浏览时的声音。
在需要使用代理服务器时,有可能需要在应用程序中使用非默认的代理服务器设置。这可以通过调用UrlMkSetSessionOption来实现。
Joseph M. Newcomer [MVP]最近在其个人网站上发表了一篇文章(http://www.flounder.com/vs2325.htm),描述了如何创建与Visual C++ 2003和Visual C++ 2005都兼容的项目。这对一些想逐步把项目升级到VS2005的人很有用。不过是逐步升级还是一次到位则取决于项目经理对人力、费用和功能的取舍。
除了工程文件之外,逐步升级的时候项目中的代码可能也有必要兼容多个Visual C++版本。比如从2003升级到2005的时候需要看看Breaking Changes in the Visual C++ 2005 Compiler (http://msdn2.microsoft.com/en-us/library/ms177253.aspx)和What's New in the Visual C++ Libraries(http://msdn2.microsoft.com/en-us/library/y8bt6w34.aspx )。一个很好地兼容了各个Visual C++的版本的示例是BCG ControlBar,可以去其下载页面( http://www.bcgsoft.com/download.htm )下载一个试用版。兼容不同版本的.Net的代码应该判断System.Environment.Version,或者像Aaron Stebner这样读注册表(http://blogs.msdn.com/astebner/archive/2004/09/18/231253.aspx)。
PS: Joseph M. Newcomer的另外一篇文章(http://www.flounder.com/getminmaxinfo.htm)描述了如何在更改对话框大小的时候相应移动控件,不过我个人则偏向于集成一个Windows Forms之后设置控件的Dock属性。
在计算亲和数的时候,由于涉及到密集运算,有必要把计算工作转移到背景线程,以避免界面会失去响应。在.Net 1.0中,可以用
ManualResetEvent、
线程和
Delegate的异步调用来实现,但是在.Net 2.0中,可以使用
BackgroundWorker对象
来简化这个工作。这个对象自动化了进度报告和终止线程的功能。
要使用这个对象来创建工作线程,首先需要加入一个BackgroundWorker对象到表单(Form)或者用户控件(UserControl),然后调用其RunWorkerAsync方法:
private: System::Void AmicableNumberView_Load(System::Object^ sender, System::EventArgs^ e)

...{
propertyGrid->SelectedObject = Range;
listViewPairs->VirtualListSize=0;
timer->Start();
m_rAmicableNumberPairs->Clear();
backgroundWorker->RunWorkerAsync (Range);
}
在线程创建之后会自动触发DoWork事件。这个事件中的处理类似于1.1中的线程函数体,可以通过访问DoWorkEventArgs参数的argument属性来访问在用RunWorkerAsync启动线程时传递的参数,以及调用ReportProgress定时报告进度。
private: System::Void backgroundWorker_DoWork(System::Object^ sender, System::ComponentModel::DoWorkEventArgs^ e)

...{
CAmicableNumberRange range=(CAmicableNumberRange)e->Argument;
int nNumbers=range.Max-range.Min;
int nPercentComplete=0;
m_rGenerator->Range=range;
m_rGenerator->StartWork();

while(!backgroundWorker->CancellationPending && m_rGenerator->DoWork())...{
nPercentComplete = (int)(
(float)(m_rGenerator->GetNumberInQuestion()-range.Min)
/(float)nNumbers
* 100);
backgroundWorker->ReportProgress(nPercentComplete,m_rGenerator->GetProgress());
}
m_rGenerator->EndWork();
if(backgroundWorker->CancellationPending)
e->Cancel=true;
}

在主线程中可以处理ProgressChanged事件来更新界面。
private: System::Void backgroundWorker_ProgressChanged(System::Object^ sender, System::ComponentModel::ProgressChangedEventArgs^ e)

...{
progressBar->Value=e->ProgressPercentage;
if(e->UserState!=nullptr)//a new pair is found

...{
CAmicableNumberPair^ anp=(CAmicableNumberPair^)e->UserState;
m_rAmicableNumberPairs->Add(anp);
}
}
在计算完成之后,可以处理RunWorkCompleted事件来更新界面
private: System::Void backgroundWorker_RunWorkerCompleted(System::Object^ sender, System::ComponentModel::RunWorkerCompletedEventArgs^ e)

...{
timer->Stop();
if(e->Error!=nullptr)
MessageBox::Show(e->Error->Message);
progressBar->Value=100;
}
当然,为了避免过于频繁地更新界面,仍然可以使用传统的定时更新方法
private: System::Void timer_Tick(System::Object^ sender, System::EventArgs^ e)

...{
if(IsHandleCreated)

...{
int nOldListSize=listViewPairs->VirtualListSize;
int nNewListSize=m_rAmicableNumberPairs->Count;
if(nNewListSize>nOldListSize)

...{
listViewPairs->VirtualListSize=nNewListSize;
listViewPairs->RedrawItems(nOldListSize, nNewListSize - 1, true);
}
}
}
用户有时会在计算中途取消计算,这时候可以调用CancelAsync方法,然后等待至IsBusy属性成为false为止。在DoWork中检查到CancellationPending属性为true时,应该终止并从DoWork中返回。
void Stop()

...{
backgroundWorker->CancelAsync();
while(backgroundWorker->IsBusy)

...{
Application::DoEvents();
}
}
对于Windows Forms,应该在表单的FormClosing事件处理中自动停止计算,但是对于UserControl,由于没有办法判断其容器的类型,所以只能在容器中手动调用一个方法来终止计算。下面是在MFC8.0的容器类CWinFormsView中调用的方法:
void CAmicableNumbersView::OnDestroy()

...{
m_ViewControl->Stop();
CWinFormsView::OnDestroy();
}
在使用MFC的.Net容器类和用户控件来计算亲和数时,需要在.Net控件的Load事件之前给控件传递一个计算范围参数。在MFC中,.Net控件是以ActiveX控件的形式被创建的,但是在传统的窗口初始化函数CWinFormsView::OnInitialUpdate被执行之前,控件的Load事件已经被调用了,这使得参数的传递更加困难。.Net控件是在窗口的Create方法中被创建的:
BOOL CWinFormsView::Create(LPCTSTR lpszClassName,
LPCTSTR lpszWindowName, DWORD dwStyle,
const RECT& rect,
CWnd* pParentWnd, UINT nID,
CCreateContext* pContext)

...{
m_nFlags |= WF_ISWINFORMSVIEWWND;
BOOL ok=__super::Create(lpszClassName, lpszWindowName, dwStyle, rect,pParentWnd, nID, pContext);
ASSERT(ok);
ok=ok && m_control.CreateManagedControl(m_pManagedViewType,WS_VISIBLE, rect, this,nID);
ASSERT(ok);
.....
return ok;
}

但是在CWinFormsControl::CreateManagedControl返回之前,Load事件就已经被触发了。在Load事件中设置一个断点,在执行时查看调用堆栈发现,这个事件是COleControlSite::CreateControlCommon中的下面语句调用的
// control is visible: just activate it
hr = DoVerb(OLEIVERB_INPLACEACTIVATE);
也就是说,为了在这之前调用自定义代码,需要自定义一个COleControlSite类。在MFC6.0中,COleControlSite是一个未文档化的类,但是在微软知识库文章Q236312和Q329802中描述了自定义全局ActiveX控件站点的方法。从MFC7.0开始,COleControlSite被文档化了,而且可以通过重载CWnd::CreateControlSite来使用自定义控件站点类。这个方法在MFC7.0中被用来扩展CHTMLView和CDHTMLDialog,而且在MFC8.0中被用来将默认的COleControlSite替换成CWinFormsControlSite以提供托管控件支持。同样的方法可以用来将CWinFormsControlSite替换成其派生类来自定义控件创建过程。
virtual BOOL CreateControlSite(COleControlContainer* pContainer, COleControlSite** ppSite, UINT nID, REFCLSID clsid)

...{
COleControlSite* pSite=NULL;
if (InlineIsEqualGUID(clsid , CLSID_WinFormsControl))

...{
pSite=new CCAmicablesControlSite(this, pContainer);
if(pSite)

...{
*ppSite=pSite;return TRUE;
}
}
return FALSE;
}
自定义控件创建过程中需要重载的函数是CWinFormsControlSite::CreateOrLoad。CWinFormsControlSite的成员函数并不多,只有这个函数是在控件的创建之前被调用,并且在控件的Load事件被触发之前结束执行。重载的函数只是简单的调用视图的一个成员函数,而由视图来负责进行具体的处理
class CCAmicablesControlSite:public CWinFormsControlSite

...{
public:
CCAmicablesControlSite(CAmicableNumbersView* pView, COleControlContainer* pCtrlCont)

: CWinFormsControlSite(pCtrlCont) ,m_pView(pView) ...{}

virtual HRESULT CreateOrLoad(const CControlCreationInfo& creationInfo)...{
HRESULT hr =CWinFormsControlSite::CreateOrLoad(creationInfo);

if (SUCCEEDED(hr)) ...{
m_pView->OnControlCreated();
}
return hr;
}
protected:
CAmicableNumbersView* m_pView;
};
void CAmicableNumbersView::OnControlCreated()

...{
m_ViewControl = safe_cast<Amicable::AmicableNumberView ^>(this->GetControl());
m_ViewControl->Range = m_range;
}
和传统的视图不同,使用CWinFormsView时,不能在重载的PreCreateWindow函数中返回FALSE来终止视图的创建。这是因为CWinFormsView::Create没有对视图创建失败的情况进行正确处理,而是在创建.Net控件失败之后仍试图访问这个对象而造成的。如果确实要终止视图的创建,那么可以重载CWinFormsView::Create,在这之前终止视图的创建。
BOOL CAmicableNumbersView::Create(LPCTSTR lpszClassName, LPCTSTR lpszWindowName, DWORD dwStyle, const RECT& rect, CWnd* pParentWnd, UINT nID, CCreateContext* pContext)

...{
// TODO: Add your specialized code here and/or call the base class
//prompt for search range
FormRange^ fr= gcnew FormRange();
fr->Range=m_range;
if (fr->ShowDialog() == DialogResult::OK)

...{
Amicable::CAmicableNumberRange range=fr->Range;
m_range=range;
}
else
return FALSE;
return CWinFormsView::Create(lpszClassName, lpszWindowName, dwStyle, rect, pParentWnd, nID, pContext);
}

尽管可以使用MFC来给应用程序加上托管扩展,但是托管代码和非托管代码互操作造成的性能影响还是很明显。没有绝对必要的话,还是建议像上面调用FormRange表单那样直接调用托管的表单而不是使用MFC的托管支持。
尽管4月份还没到,一篇MSDN杂志4月号的文章已经出来了(http://msdn.microsoft.com/msdnmag/issues/06/04/ManagedSpy)。这篇文章描述了如何使用ManagedSpyLib库来访问其他进程内的托管控件及其属性和事件。尽管这个程序的主要目的是监视,但是也可以用ManagedSpyLib库来控制其他进程内的托管控件。
好长时间没更新BLOG了,向大家拜个晚年先。最近没怎么写代码,转几篇在网易虚拟社区发的文章过来充数。
对于BUG的自信
Donald E. Knuth(高德纳)在TeX: The Program的前言中说:
"我相信,在1985年11月27日,TeX代码里面的最后一个BUG已经被发现和解决了。但是,如果代码中仍旧有BUG,我很高兴付给任何第一个发现BUG的人20.48美元(这是前一个金额的两倍,而且我计划在一年内把它翻倍。你看,我很自信!)"
想知道后来发生了什么吗?
在http://truetex.com/knuthchk.htm可以看到他写出去的支票的金额是从2.56美元开始翻倍的。微基百科中关于这种支票的文章(http://en.wikipedia.org/wiki/Knuth_reward_check)说,截至2001年10月为止,他写出去了超过两千张这样的支票,但是他的BUG支票是如此有名,以至于很多人把他的支票收藏起来而不是拿出去兑现(http://www.tug.org/whatis.html)。
有多少程序员在发布产品的时候可以这样自信地声明产品没有问题?
遗憾的是,现在的程序员经常把发现BUG的责任推给测试人员——“不用担心,测试人员会发现所有BUG的,这是他们的工作”。实际上,测试人员并没有开发人员的条件,他们不可能进行源代码级别的调试,很大程度上只能靠运气——没错,是靠运气,如果一个BUG很容易被发现,程序员不太可能自己没有发现它——来发现BUG。
还有一些人干脆就认为BUG是不可避免的,或者认为不值得这么精益求精(参见网易虚拟社区http://p5.club.163.com/viewArticleByWWW.m?boardId=clanguage&articleId=clanguage_108eacc622169e7&boardOffset=0的讨论),但是实际上防止BUG出现的最好的时机,就是在编写代码的时候。在编写代码一段时间之后,即使是编写者本人也可能需要一段时间来理解代码(如果不习惯写注释的话,这段时间会更长),更别说定位问题所在了。在编写代码时,如果具有良好的习惯,可以免去很多在之后消灭BUG的困难。
规范不是语法
太多人把不要使用goto奉为圣旨,从来不想去打破。他们会争论,goto会造成难以维护的难读的代码,以及使编译器无法进行优化。这两点在很大程度上是真的,但是也有使用goto可以增加程序可读性和效率时候。在这种情况下,遵循“不使用goto语句”规范会产生更糟糕的代码。
一些人喜欢在成员函数后面加const,但是另外一些人没有养成这个习惯。一个直接的结果就是,一些看起来对对象完全没有影响的函数不能在const函数里面使用。这时候应该怎么办?看看Paul DiLascia建议的,把this指针强行转化为一个非const指针(http://www.microsoft.com/msj/archive/S126E.aspx)。如果函数实际上会对对象成员造成影响(例如CToolBar::GetItemRect),这也会带来潜在危险。
为了和ANSI标准之前编写的代码兼容,ANSI C中的memchr函数的声明为
void *memchr(
const void *buf,
int c,
size_t count
);

这里c是一个字符。很明显,标准为了兼容性放弃了明确性和更强的类型检查。如果放弃兼容性,这个函数应该声明为如下形式
void *memchr(
const void *buf,
unsigned char c,
size_t count
);
微软的很多代码使用一种叫做匈牙利表示法的命名规范。这使得标识符的含义和类型更加明确——但是这是从广义的角度来说的。考虑如下函数声明
char *strcpy(
char *strDestination,
const char *strSource
);
如果严格遵循原始的匈牙利表示法,那么两个参数的声明应该是pch开头。但是以str开头给这两个参数更多含义:它们指向以\0为结束符的字符串。
规范是用来在大部分时间里遵循,以及在可以得到更好的结果时打破的。
编译警告的意义
智能化的编译器开始将语法正确的语句列为警告:
while(size-->0);//注意这里有个分号
*pTo++=*pFrom++;
编译器会报告空循环问题。
但是对于以0结尾的字符串复制
while(*pTo++=*pFrom++);
,这样的警告是多余的。
更加常见的警告是在条件判断语句中
if(ch='\0')
EndOfString();
为了绕过这个警告,需要添加额外的运算或者语句,或者更正错误的赋值。

while((*pTo++=*pFrom++)!='\0')...{}
if(ch=='\0')
一些程序员甚至将比较语句修改成
if('\0'==ch)
这样作的原因显而易见:为了减少潜在的BUG。如果你的编译器没有这样的警告,那么你可以使用一些工具来检查那些语法正确但是有潜在BUG的代码。LintProject (http://www.codeproject.com/tools/lintproject.asp)就是其中一个。但是,良好的编程习惯还是减少BUG出现的最好的方法。
在觉得警告消息太烦人的时候,不妨想想编译器的开发人员为什么要编写这么多警告消息,而不是仅仅寻求关闭警告的方法。
P.S. Visual C++的默认警告等级是3级。发布软件之前应该改成4级,之后检查所有的编译警告。
无处不在的断言
使用编译器来捕获BUG的主意很好,Visual Studio 2005甚至会报告定义的变量不符合命名规范(Warning 1 CA1709 : Microsoft.Naming : Correct the casing of type name 'welcome'.);但是我敢打赌你检查BUG列表的时候,你会发现只有一小部分BUG会被编译器抓到。很多BUG在程序运行过程中很少会出现,例如内存分配失败的问题
char* strBuffer=new char [length];
MyZeroMemory(strBuffer,length);
这段代码在绝大多数情况下会成功,但是在虚拟内存不足的时候,Windows会报告“您的系统虚拟内存太低,WINDOWS会增加虚拟内存页面文件的大小。在这个过程中,一些应用程序的内存请求会被拒绝”然后开始增加虚拟内存,在这个过程中,new这样的内存分配可能会因为内存不足而失败,而MyZeroMemory则可能会造成访问越界。如果你足够幸运,你会在产品发布之前发现这个BUG,否则,你的用户会代替你发现这个BUG。要是用户刚好没有备份的习惯,丢失了几十分钟甚至是几小时的工作进度,用户会很生气,后果很严重。
编译器不能捕获这种运行时才会出现的错误(顺便说一下,我在CSDN上居然还看到有人抱怨编译器不会报告除0错误);也不能捕获算法中的BUG和检验参数中的数据。但要是你知道怎么做的话,这类问题很容易被发现。你可以用SetProcessWorkingSetSize函数或者msconfig工具减少虚拟内存大小,或者使用Virtual PC之类的虚拟机或者磁盘配额策略来模拟内存和磁盘空间不足的情况。
你有可能想在这种极限情况下调试你的代码,但是大多数时间内,内存分配不会失败,而设置条件断点又太麻烦了。这时候可以在代码里面加上一段用来在内存分配失败时触发调试器的断言代码
void MyZeroMemory(char* strBuffer, int length)

...{
assert(strBuffer!=NULL);
}
如果使用的是MFC或者ATL,建议使用对应的宏ASSERT和ATLASSERT。现在你可以编写健壮的代码使得程序在strBuffer这块内存分配失败时也能够正常运行。
现在的问题是,加入的这些代码增加了应用程序的大小,减慢了运行速度。在解决了内存分配失败造成的程序崩溃的问题之后,有必要在发布的版本中去掉这些断言代码。一个简单的办法是使用预处理标识符:
void MyZeroMemory(char* strBuffer, int length)

...{
#ifdef DEBUG
assert(strBuffer!=NULL);
#endif
}
这样你可以只维护同一份代码。当然,这也意味着调试的代码在发行版中会被去除,所以为了避免不可预料的行为,为了调试而加入的代码应该尽可能少地影响应用程序的行为。
你有可能需要重新定义assert来实现扩展的行为——例如在assert断言失败中断程序时打开源文件并且跳到assert那一行——这时候你可以编写自己的断言函数,然后重定义assert为这个断言函数。
#ifdef DEBUG

/**//*display a dialog and if the user selected break
, jump to the assert line*/
void _assert(char*,unsigned int);
#define assert(f)\
if(f)\

...{}\
else
_assert(__FILE__,__LINE__)
#else
#define assert(f)
#endif

空的if语句块可能看起来有点奇怪,但是这可以避免和宏外的if-else产生冲突。同样,最后一行语句没有结束的分号,因为在使用的时候再加上会更加自然。
assert最有用的地方就是用来检验函数的参数——但是也可以在其他地方起作用。在程序中的断言语句越多,异常的情况就越容易被侦测到。
既然assert是代码,它不可避免的需要注释。即使是自己写的代码,过了六个月之后再来审视也可能需要一点时间来重新理解这部分代码。一个简单的注释可以把这部分时间减少:
void MyZeroMemory(char* strBuffer, int length)

...{

/**//*should not be called when buffer allocation failed*/
#ifdef DEBUG
assert(strBuffer!=NULL);
#endif
}

在编写完函数之后,应该审视函数中的代码,之后在函数的开头验证函数正常运行所需的条件。如果你在写一个库函数,那么应该在函数的文档中加入函数正常运行所需的条件——否则就会增加使用者发现BUG的难度。举例来说,Windows API的文档不可谓不详尽,但是我在用汇编调用Windows API的时候,也花了很长时间才发现调用Windows API之前栈顶要设置成4的倍数。
注意不要把一些条件当成默认成立的了。assert(sizeof(int)==4);这样的语句在一些人看来很荒谬,但是在Windows开发中通常是32位的long在一些64位平台上已经是64位的了,而在目前还不知道sizeof(int)在什么时候会升位。如果你的代码依赖于int的大小,那么写上这行可以在未来升位之后更快发现问题。
一些保守的程序员在参数错误时会让函数继续运行——返回一个错误码——但是不报告错误。在编写核心模块时这可能很有必要,但是这也经常会把BUG藏起来——在多层函数返回之后时候,错误码经常会丢失或者被替代。尽量不要使用保守编程来替代断言,如果你认为保守编程会造成定位问题的困难,那么就加上断言代码。
在一些时候,校验参数数据似乎是不可能的事情——想象一下那些被设计来搞糊涂解密者的加密算法的中间数据——但是校验这种复杂算法的方法也不是没有。为了确认手算和心算的正确性,我们会使用电子计算器的结果来进行比较,反过来,我们也可以编写另外一个的算法来断言计算结果的正确性。这种方法也可以被用来断言一个函数的汇编版本和C版本的一致性——为了获取最大性能,函数的汇编版本的算法可能和C版本的有很大差异。当然,不是每个函数都有必要用这种方式来验证,实际上,只有极其重要的算法和对性能极其敏感的代码才会需要这种双保险来验证。同样,为了调试而加入的算法也应该尽可能少地影响应用程序的行为。
最后,你不应该在发布程序时从代码中去掉断言语句,而是把它们留在那里以供你升级或者查找BUG时使用。帖来自于网易社区:http://p5.club.163.com/viewArticleByWWW.m?boardId=clanguage&articleId=clanguage_10938d0575c4afe
消灭不可预料的行为
断言并不能抓住所有BUG——它们都是人写出来的,而是人就会犯错误。一些常见的错误包括:
- 使用状态不确定的资源
- 在释放资源之后继续访问资源
- 在资源重新定位之后继续引用旧的资源
- 申请资源之后丢失对资源的引用
- 访问时未注意是否越界
- 忽略错误信息
这些并不是杞人忧天的问题——实际上,这些问题属于日常开发中最常见的问题。这些问题的特点是,它们并不是时常造成程序行为的异常,并且症状不可重复。以内存为例,释放内存之后编译器和操作系统通常不会自动去擦除内存中的内容,所以继续访问内存不太可能造成程序行为的异常——直到内存被重新分配出去,而另一块代码开始重写这块数据。申请资源之后丢失对资源的引用可能只是造成长时间运行之后系统资源不足而已。另外,这些问题都是算法的问题,而编译器并不会替你校验你的算法,你自己也不太可能会怀疑你自己的算法。
不要认为职业的程序员就不会犯这类错误。举几个例子来说明这些问题。
- 在IE4.0中,MSHTML的HTMLDocument对象的IPersistStreamInit::Load假定传入的IStream流的访问指针已经定位到开头——而在IE5.0中,IPersistStreamInit::Load会自行调用IStream::Seek
- 在Visual C++ 6.0中,CHTMLView类有字符串资源未释放问题(http://support.microsoft.com/kb/241750)
- 在Visual C++.Net中,CHTMLView类有两个BUG:
- 参数传递错误:
HRESULT CHtmlView::ExecFormsCommand(DWORD dwCommandID, VARIANT* pVarIn,
VARIANT* pVarOut)

...{
HRESULT hr = E_FAIL;
CComPtr spDoc = (IHTMLDocument2*) GetHtmlDocument();
if (spDoc != NULL)

...{
CComQIPtr spCmdTarget = spDoc;
if (spCmdTarget != NULL)
hr = spCmdTarget->Exec(&CMDSETID_Forms3, dwCommandID,
OLECMDEXECOPT_DONTPROMPTUSER, pVarOut, pVarIn);
}
return hr;
}

COM指针未释放错误:
void CHtmlView::OnFilePrint()

...{
// get the HTMLDocument
if (m_pBrowserApp != NULL)

...{
CComPtr spDisp = GetHtmlDocument();
if (spDisp != NULL)

...{
// the control will handle all printing UI
CComQIPtr spTarget = spDisp;
if (spTarget != NULL)
spTarget->Exec(NULL, OLECMDID_PRINT, 0, NULL, NULL);
}
}
}
这些不确定的行为是很大一部分不可重复(因此也很难跟踪)的BUG的根源。举例来说,释放一块内存之后,在操作系统切换到另外一个线程之前,重新分配同一块内存,并且写入数据这个事件发生的可能性十分之小。为了解决这些问题,Visual C++编译器采取了一种保护性的措施,在调试模式下再分配和释放内存时将内存用一般不会用到的值填充,例如0xccccccc,0xdddddddd和0xfefefefe(参见编译器文档中的/RTC参数的说明)。这样你可以减少不可预料的程序行为,强迫BUG重现。如果你的编译器没有这么做,你可以自己编写一个调试模式下专用的内存管理程序进行这样的工作。为什么选择这样的值?在Intel系统中,0xcc的含义是int 3中断(参见http://blogs.msdn.com/oldnewthing/archive/2004/11/11/255800.aspx)——如果不小心执行了这块数据,那么程序会马上中断并且提示用户,其他的值则是典型的非法数据。如果你在为其他环境编写程序。你可能需要查阅一些资料来决定使用什么值来在调试模式下填充内存。
MFC的另一个保护措施是内存泄漏监测器——这也是在每个文件开头要加上#define new DEBUG_NEW的原因——但是这也变更了应用程序的行为。举例来说,为了检查内存泄漏,MFC总是分配比所需要多的内存,然后加入调试信息。如果你的程序有访问越界的代码,那么有可能擦除一部分额外分配的内存,可能的结果就是在调试模式下运行正常,而在发布模式下程序崩溃。当然,这是必须的。如果你的发布版本的程序依赖于这些额外字节,那么你就有麻烦了。
在发布模式下程序的崩溃有助于你发现问题,但是也造成定位问题的困难。你可以在发布模式下加入调试信息(没错,在工程的C++和连接选项中选中Program Database和Generate Debug Info)来生成一个中间版本;MSDN文章Generating and Deploying Debug Symbols with Microsoft Visual C++ 6.0(http://msdn.microsoft.com/library/en-us/dnvc60/html/gendepdebug.asp)甚至教你怎么怎么发布这样的版本,但是也要注意这样的版本和最终发布版本还是可能有区别的——特别是在程序中有BUG的情况下。另一个办法是在调试时将EIP寄存器修改成崩溃信息中的值,这样可以很容易在源代码中定位造成崩溃的代码的位置(参见http://www.codeproject.com/debug/XCrashReportPt1.asp)。
MFC开发中另一个比较有用的定位内存访问越界方法是,将数据封装成对象成员变量,尽量可能让所有类都从CObject派生,并且在代码中大量加上ASSERT_VALID。如果成员变量被越界的访问重写了,那么CObject指向AssertValid的虚函数表会被改写,而ASSERT_VALID会报告这个错误。
不要发布调试版本,这对用户来说并无意义。虽然这么说可能是多此一举,但是我在玩游戏的时候还真看见过断言对话框。调试信息是被设计用来发现问题的,不是用来隐藏问题的。如果你确实需要这么做(微软就定期发布核心模块的调试信息以供软件开发人员定位问题),那么你需要让用户认识到调试版本和最终发布版本的性能差异,例如在程序开始时显示一个消息。
广告时间:
最新Windows SDK不支持Visual C++ 6.0
可能大部分人已经知道了,但是CSDN论坛上仍旧不断出现关于这个兼容性的提问。最新的支持Visual C++ 6.0的版本是2003年2月版,下载地址是http://www.microsoft.com/msdownload/platformsdk/sdkupdate/psdk-full.htm。
取消对Visual C++ 6.0的支持的原因是为了支持新的/GS参数。XP SP2和Windows Server 2003 SP 1都增加了很多安全特性,以致于新的Windows SDK中包含的编译器和库文件不再和Visual C++ 6.0兼容。
参见
我在很久之前就开始用程序自动化Shell窗口——主要对象是IE窗口。有时浏览器控件或者MFC类CHTMLView可以满足我的需要,但是很多时候我需要从头嵌入浏览器控件并且尽可能模拟IE的行为,例如实现IDocHostUIHandler来启用自动完成功能。一个很自然的替代方案是直接操作IE窗口。
创建新的Internet Explorer窗口
最简单的方法是调用Windows API ShellExecute (Ex),Paul DiLascia在他的C++ Q&A专栏文章"Browser Detection Revisited, Toolbar Info, IUnknown with COM and MFC"里面有一段示例代码:
但是,这样没法控制新的窗口,而且在用户关闭程序之后会留下一个IE窗口。为了扫我自己的门前雪,我需要找到我创建的窗口,并且控制它。
我的下一个尝试是创建和控制一个InternetExplorer对象,并且在必要时关掉它。微软知识库中有这么一篇文章"How To Automate Internet Explorer to POST Form Data" 基本上描述的就是我想要的,除了最后的关闭窗口。嗯,简单的调用IWebBrowser2::Quit就可以做到这一点
还有一个问题。要是用户在我的WM_TIMER处理函数中操作窗口之前关掉了新的IE窗口怎么办?可以IWebBrowser2接口控制的IE对象现在不再存在了。幸亏微软考虑到了这一点,程序不会崩溃,但是最好还是能够知道什么时候它会关闭,这样我可以避免意外发生。
处理Internet Explorer的事件
Internet Explorer对象在退出时会触发DWebBrowserEvents2::OnQuit事件。这是一个理想的释放控制的时机。因为对象要被销毁,所以我同时也停止监视对象的事件
if(m_pWebBrowser2)

...{
UnadvisesinkIE();
m_pWebBrowser2=(LPUNKNOWN)NULL;
}

连接到当前的Internet Explorer窗口
虽然我不在乎我会控制到哪个IE窗口,但是既然微软知识库里面有"如何连接到一个Internet Explorer的实例"这样一篇文章,我假定一些人会觉得"如何连接到当前的Internet Explorer实例"这样一篇文章比较有用.
这样的话,什么是“当前的Internet Explorer实例”?实际上,它就是最后一个活动的IE窗口。因为Windows会把活动的窗口移动到z-order的顶部,所以它会保留在所有IE窗口的z-order的最高处。因此我需要做的就是找到哪个IE窗口具有最高的z-order值。这样我需要先判断哪个窗口是IE窗口。在一些和Spy++有关的调查之后,我假定IE窗口具有一个共同的窗口类"IEFrame",然后编写了一个函数来获得Shell窗口的窗口类:
剩下的问题就很简单了:沿Z轴枚举顶层窗口,找到第一个Shell窗口列表中的具有窗口类"IEFrame"的第一个实例。之后我操作了一下IE的DHTML文档对象模型(也称为DOM,它只在IE窗口触发最后一个DocumentComplete事件只后有效)来确认成功连接到窗口。
void CAutomationDlg::DocumentComplete(IDispatch *pDisp, VARIANT *URL)

...{
//HTML DOM is available AFTER the DocumentComplete event is fired.
//For more information, please visit KB article
//"How To Determine When a Page Is Done Loading in WebBrowser Control"
//http://support.microsoft.com/kb/q180366/
CComQIPtr pWBUK(m_pWebBrowser2);
CComQIPtr pSenderUK( pDisp);
USES_CONVERSION;
TRACE( _T( "Page downloading complete:\r\n"));
CComBSTR bstrName;
m_pWebBrowser2->get_LocationName(&bstrName);
CComBSTR bstrURL;
m_pWebBrowser2->get_LocationURL(&bstrURL);
TRACE( _T( "Name:[ %s ]\r\nURL: [ %s ]\r\n"),
OLE2T(bstrName),
OLE2T(bstrURL));
if (pWBUK== pSenderUK)

...{
CComQIPtr pHTMLDocDisp;
m_pWebBrowser2->get_Document(&pHTMLDocDisp);
CComQIPtr pHTMLDoc(pHTMLDocDisp);
CComQIPtr ecAll;
CComPtr pTagLineDisp;
if(pHTMLDoc)

...{
CComBSTR bstrNewTitle(_T("Sheng Jiang's Automation Test"));
pHTMLDoc->put_title(bstrNewTitle);
pHTMLDoc->get_all(&ecAll);
}
if(ecAll)

...{
ecAll->item(COleVariant(_T("tagline")),COleVariant((long)0),&pTagLineDisp);
}
CComQIPtr eTagLine(pTagLineDisp);
if(eTagLine)

...{
eTagLine->put_innerText(
CComBSTR(_T("Command what is yours, conquer what is not. --Kane")));
}
}
}

现在控制的窗口和IE打开文件时选择的一样了。
副产品: 连接到当前的Windows Explorer窗口
在研究ShellWindows对象的shell窗口列表时,我获得一个副产品:看起来Windows Explorer窗口也有共同的窗口类名。这样同样的机制在把窗口类从"IEFrame"改成"ExploreWClass"之后对Windows Explorer窗口也适用。因为没有DHTML DOM可供操作,我通知Windows Explorer 窗口打开一个现存路径,来标志我接管了这个窗口。
这代码有点长,因为我想区别对待文件和文件夹。如果你调用IShellBrowser::BrowseObject并且给这个方法传递一个文件pidl,那么Windows Explorer会提示你是否打开这个文件,就像在资源管理器的地址栏中输入路径之后按回车一样。我想模拟"Explorer.exe /select"的行为,在文件夹视图中选择指定的文件,所以我在DocumentComplete事件处理函数中加入了一些代码:
if(m_pidlToNavigate)

...{
//If the start address is a file, browse to the parent folder
//and then select it
CComQIPtr psp(m_pWebBrowser2);
CComPtr psb;
CComPtr psv;
if(psp)
psp->QueryService(SID_STopLevelBrowser,IID_IShellBrowser,(LPVOID*)&psb);
if(psb)
psb->QueryActiveShellView(&psv);
if(psv)

...{
LPCITEMIDLIST pidlChild=NULL;
CComPtr psf;
SFGAOF rgfInOut=SHCIDS_ALLFIELDS;
HRESULT hr = SHBindToParent(m_pidlToNavigate, IID_IShellFolder, (LPVOID*)&psf, &pidlChild);

if (SUCCEEDED(hr))...{
hr=psf->GetAttributesOf(1,&pidlChild,&rgfInOut);

if (SUCCEEDED(hr))...{

if((rgfInOut&SFGAO_FOLDER)==0)...{
//a file, select it
hr=psv->SelectItem(ILFindLastID(m_pidlToNavigate)
,SVSI_SELECT|SVSI_ENSUREVISIBLE|SVSI_FOCUSED|
SVSI_POSITIONITEM);
}
}
}
}
//clean up
ILFree(m_pidlToNavigate);
m_pidlToNavigate=NULL;
}

创建Explorer窗口
解决了这么多问题,可以衣锦还乡了。既然我可以以和当前的Internet Explorer窗口基本相同的方式连接到当前的Windows Explorer窗口,那么我是否可以以和创建Internet Explorer窗口基本相同的方式创建Windows Explorer窗口?遗憾的是,这不可行。不存在Windows Explorer对应的类ID来创建一个COM对象。虽然我仍旧可以创建IE窗口,浏览到文件夹,显示文件夹侧边栏,使得它看起来就像一个Windows Explorer窗口,但是我不能改变窗口类"IEFrame",因此较难把它和其他的显示HTML网页和活动文档的IE窗口区分开来。
好吧,既然我不能以COM的方式来创建它,我还可以尝试用传统的方式。我可以创建一个explorer.exe进程之后查找其主窗口,就像Paul DiLascia 在他的文章"Get the Main Window, Get EXE Name"中演示的那样,并且发送未文档化的消息WM_GETISHELLBROWSER来获得窗口的IShellBrowser接口:
啊喔,这在我的计算机上也没有效果。怎么回事?在我的资源管理器的文件夹选项中,“在同一窗口中打开每一个文件夹”被选中,所以新的Windows Explorer窗口被创建在现有的Windows Explorer进程中。看起来这是条死胡同。
等一下,我手头还有另一个ShellWindows对象,它可以给我一个Shell窗口的列表,包含每一个Windows Explorer窗口和每个窗口对应的IWebBrowser2接口,这是到IShellBrowser接口的入口。.现在我需要获得两份shell窗口列表,创建explorer.exe进程之前和之后各一份,之后要比较它们来找到新的shell窗口:
m_pShellWindows.CoCreateInstance(CLSID_ShellWindows);
if(m_pShellWindows)

...{
//get the list of running IE windows
//using the ShellWindows collection
//For more information, please visit
//http://support.microsoft.com/kb/176792
long lCount=0;
m_pShellWindows->get_Count(&lCount);
for(long i=0;i pdispShellWindow;
m_pShellWindows->Item(COleVariant(i),&pdispShellWindow);
if(pdispShellWindow)

...{
m_listShellWindows.AddTail(new CComQIPtrIDispatch(pdispShellWindow));
}
}
}
//enumerate through the new shell window list
long lCount=0;
m_pShellWindows->get_Count(&lCount);
for(long i=0;i//search the new window
//using the ShellWindows collection
//For more information, please visit
//http://support.microsoft.com/kb/176792
BOOL bFound=FALSE;
CComPtr pdispShellWindow;
m_pShellWindows->Item(COleVariant(i),&pdispShellWindow);
//search it in the old shell window list
POSITION pos=m_listShellWindows.GetHeadPosition();
while(pos)

...{
CComQIPtrIDispatch* pDispatch=m_listShellWindows.GetNext(pos);
if(pDispatch&&pdispShellWindow.p==pDispatch->p)

...{
bFound=TRUE;break;
}
}
if(!bFound)//new window found

...{
//attach to it
m_pWebBrowser2=pdispShellWindow;
m_bOwnIE=TRUE;
//sink for the Quit and DocumentComplete events
AdviseSinkIE();
NavigateToSamplePage(FALSE);
}
}

等一下,你的"创建explorer.exe进程之后"是什么意思?一秒钟之后?还是两秒钟?实际上,一个WindowRegistered事件会被ShellWindows 对象触发,所以我在事件处理中加入一些代码:.
为什么不用Browser Helper Objects?
因为新的窗口在进程外,所以跨进程列集COM调用很慢。如果你的自动化操作包含很多的COM调用,那么你可能要把代码本地化,例如编写一个浏览器辅助对象(BHO)。但是,BHO会被每一个Windows Explorer和Internet Explorer的实例加载,而且我不想拖慢整个系统来让它们扫瓦上霜。一些人倒是使用了这个技术连接到当前的Internet Explorer窗口.
已知问题
ShellWindows对象在explorer.exe process被终止或者尚未启动时不可访问。BHO在这种情况下可以作为替代方案。
结论
这里有一大堆让人迷糊的代码,而且可能还有你不熟悉的COM和Windows API函数混合调用。希望你会觉得本文有用,并且不会被我的代码搞得头昏脑胀。自动化Internet Explorer和Windows Explorers窗口可以节省你模拟系统默认行为的时间,并且给用户提供一个熟悉的界面。
参考
历史
个人觉得这次MVP峰会最大的进步就是技术相关的Session数量大大增加,按照MVP专长来分类;而不像上次那样按主题分类。我只需要在VC产品组的日程里面选择就可以了,而不是像上回那样不得不去听移动开发。当然这回也有MVP不去参加VC的Session,跑去听IE和移动开发。内容方面也比上次活泼很多,Don Box还是那么幽默,比尔·盖茨也有搞笑的演出,不过他看起来比去年七月份在北京的时候老多了。
一些可能有人会感兴趣的技术信息
一些建议
- 停止开发新的面向Win9x的程序和静态链接MFC的程序。使用新的MFC版本编译旧的程序来增加应用程序的安全性。
- 在新的程序中使用Unicode编码,同时尽可能将现有程序移植到Unicode。
- 移植到Visual C++ 2005来使用强大的编译器和调试器。
尽管限于保密协议我不能说得更多,但是微软在11月7号就会正式发布Visual Studio 2005、SQL Server 2005和BizTalk Server 2006了。新的Visual Studio版本(代号Orca和Hawaii)也正在规划中。
?
MFC提供了许多十分有用的类和对象,在很多时候在Office插件、BHO、常规DLL这样的工程中加入MFC支持是一个不错的选择。但是,MFC中的很多功能,例如资源查找,消息预处理等等都依赖于在进程或者线程创建时被初始化的MFC内部数据;而对于需要添加MFC支持的工程,这些数据并不会被自动地初始化。这时候使用一些MFC的功能,例如使用CString从字符串表加载一个字符串,或者使用CDialog::DoModal()创建一个模态对话框,都会有断言错误,用ATL向导创建的支持MFC的程序也没有多少改善,在CWinApp的DLL版本中没有初始化线程数据,所以调用AfxGetThread会返回空指针。解决这个问题的一个办法是使用AfxBeginThread来启动一个MFC线程,这样MFC会初始化线程相关的数据。在下面的示例中,我在线程初始化时建立了一个模态对话框,以避免直接创建模态对话框会触发的断言失败信息。为了模拟模态对话框的效果,在CDialogThread::WaitForDoModal()这个函数中创建了一个消息循环来等待线程结束,同时用MsgWaitForMultipleObjects来避免死锁。因为MFC中和进程相关的数据并不总是被正确初始化,在调用模态对话框之前也需要手动设置一下。
微软的桌面搜索API推出也有一段时间了,但是网上可以找到的相关技术资料还不多。官方的资料在http://addins.msn.com/devguide.aspx可以看到,而且网页上有SDK和一个C#的示例供下载。
使用搜索API非常的简单,首先创建一个桌面搜索对象
BOOL CWDSSampleView::PreCreateWindow(CREATESTRUCT& cs)
{
// TODO: Modify the Window class or styles here by modifying
// the CREATESTRUCT cs
if(m_pSearchDesktop.CreateInstance(CLSID_SearchDesktop))
{
AfxMessageBox(IDS_FAILED_TO_CREATE_SEARCH_ENGINE);
return FALSE;
}
……
之后就可以开始执行搜索了。桌面搜索对象有两个方法,ExecuteSQLQuery和ExecuteQuery,都返回一个ADO记录集对象。ExecuteQuery是对用户比较友好的版本,参数虽然比较多,但是不需要自己构建SQL;而ExecuteSQLQuery是底层版本,只有一个参数——需要自己构造的SQL。相信我,你不会渴望自己来创建SQL的。传递给ExecuteQuery的参数就已经够长的了。字符串表中IDS_COLUMNS_GENERAL的内容是:
DocTitle,DocFormat,Url,DocAuthor,PrimaryDate,FileName, FileExt,IsAttachment,Characterization,Rank,PerceivedType, HasAttach,DocTitlePrefix,FileExtDesc,DisplayFolder, DocKeywords,DocComments,ConversationID,Size, Create,Write。
void CWDSSampleView::Search(LPCTSTR lpszQuery,LPCTSTR lpszSort)
{
CString strQuery(lpszQuery);if(strQuery.IsEmpty())return;
CString strSort(lpszSort);
USES_CONVERSION;
HRESULT hr=S_OK;
GetListCtrl().SetItemCount(0);
ClearCache();
try{
CString strColumns;
VERIFY(strColumns.LoadString(IDS_COLUMNS_GENERAL));
if(strSort.IsEmpty())
m_pRecordset=m_pSearchDesktop->ExecuteQuery(T2OLE(strQuery),
T2OLE(strColumns),NULL,NULL);
else
m_pRecordset=m_pSearchDesktop->ExecuteQuery(T2OLE(strQuery),
T2OLE(strColumns),T2OLE(strSort),NULL);
int nItemCount=m_pRecordset->GetRecordCount();
GetListCtrl().SetItemCount(nItemCount);
}
catch(_com_error&e)
{
……
}
}

但是,访问返回的记录集的速度比访问数据库要慢。我不得不用虚列表和缓存来提高性能。在搜索结果很多(例如关键字选择"Microsoft")时程序有假死现象——当然也不排除我选择的字段过多的原因。
最近在写一个14位CPU的模拟器,CPU指令长度是固定的——13字节,十分的不吉利^_^b,而且CPU指令集中一些特定指令会根据上下文判断是否跳过下一个指令。但是在Intel系统上没有这样的指令,而且指令长度是可变的,所以无法知道下一个指令的长度来跳过它。我现在是在内存中设置一个标志,在执行每个指令之前检查这个标志来判断前一个指令是否指明跳过当前指令——低效,但是可以正常工作。
现在我知道一些模拟器为什么慢得像乌龟爬了……