在计算亲和数的时候,由于涉及到密集运算,有必要把计算工作转移到背景线程,以避免界面会失去响应。在.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的托管支持。