Windows的对话框是获取信息输入的主要手段,增加线程则是获得更好UI响应的重要方式。由于Windows在处理对话框时进行线程调度的特殊性,如果不对此加以特别注意,增加线程可能不能带来UI响应的改善。
有这样的应用场景:创建非模态对话框后需要马上做些耗时的工作,而同时又希望能够立刻在对话框上操作,所以,希望让非模态对话框工作在单独的线程上。
主线程启动一个UI线程,并且,让这个线程创建非模态对话框。
想法是:既然用单独的线程创建了对话框,所以,主线程在创建UI线程后,就可以继续自己其他耗时的工作了。
用一个简单的例子程序来试验这个方案:在VC++中创建一个基于SDI的MFC应用程序,在其CView派生类中处理“Call”菜单命令,创建派生自CWinThread的UI线程,在这个UI线程初始化过程中创建一个对话框。
实现一个派生自CWinThread的UI线程类,其中最关键的是CreateMyDlg和DestroyMyDlg函数。
类如下
class UIWorker : public CWinThread
{
DECLARE_DYNCREATE(UIWorker)
protected:
UIWorker(); // protected constructor used by dynamic creation
// Attributes
public:
// Operations
public:
bool CreateMyDlg( void );
void SetOwnerWnd( HWND hWnd );
void DestroyMyDlg( void );
……
private:
CDlgUserTest* m_pDlgTest;
HWND m_hOwnerWnd;
};
CreateMyDlg和DestroyMyDlg的实现都非常简单
bool UIWorker::CreateMyDlg( void )
{
m_pDlgTest = new CDlgUserTest;
if ( NULL == m_pDlgTest )
{
return false;
}
CWnd* pWnd = NULL;
if ( NULL != m_hOwnerWnd )
{
pWnd = reinterpret_cast<CWnd*>( CWnd::FromHandle( m_hOwnerWnd ));
if ( NULL == pWnd )
{
return false;
}
}
BOOL bSuccess = m_pDlgTest->Create( IDD_DIALOG_TEST, pWnd );
if ( bSuccess )
{
bSuccess = m_pDlgTest->ShowWindow( SW_SHOW );
}
return bSuccess?true:false;
}
void UIWorker::DestroyMyDlg( void )
{
if ( NULL != m_pDlgTest )
{
delete m_pDlgTest;
m_pDlgTest = NULL;
}
}
主线程就更加简单了。在菜单的对应操作中,创建线程,为了表示主线程继续工作,提供一个循环。
bool CUserView::Call(void)
{
m_pUIWorker = static_cast<UIWorker*>( AfxBeginThread(
RUNTIME_CLASS( UIWorker )));
for ( int i = 0; i < 100000; ++i )
for ( int j = 0; j < 10000; ++j )
;
return true;
}
希望达到的效果是:
主程序启动后,显示一个单文档视界面,有一个Work菜单
<shapetype id="_x0000_t75" stroked="f" filled="f" path="m@4@5l@4@11@9@11@9@5xe" o:preferrelative="t" o:spt="75" coordsize="21600,21600"><stroke joinstyle="miter"></stroke><formulas><f eqn="if lineDrawn pixelLineWidth 0"></f><f eqn="sum @0 1 0"></f><f eqn="sum 0 0 @1"></f><f eqn="prod @2 1 2"></f><f eqn="prod @3 21600 pixelWidth"></f><f eqn="prod @3 21600 pixelHeight"></f><f eqn="sum @0 0 1"></f><f eqn="prod @6 1 2"></f><f eqn="prod @7 21600 pixelWidth"></f><f eqn="sum @8 21600 0"></f><f eqn="prod @7 21600 pixelHeight"></f><f eqn="sum @10 21600 0"></f></formulas><path o:connecttype="rect" gradientshapeok="t" o:extrusionok="f"></path><lock aspectratio="t" v:ext="edit"></lock></shapetype><shape id="_x0000_i1025" style="WIDTH: 6in; HEIGHT: 298.5pt" type="#_x0000_t75"><imagedata o:title="MainUI" src="file:///C:%5CDOCUME~1%5Choward%5CLOCALS~1%5CTemp%5Cmsohtml1%5C03%5Cclip_image001.jpg"></imagedata></shape>
点击Call菜单后,对话框应该马上弹出,显示为:
<shape id="_x0000_i1026" style="WIDTH: 6in; HEIGHT: 298.5pt" type="#_x0000_t75"><imagedata o:title="ShowDialog" src="file:///C:%5CDOCUME~1%5Choward%5CLOCALS~1%5CTemp%5Cmsohtml1%5C03%5Cclip_image003.jpg"></imagedata></shape>
执行中,对话框不会马上弹出,而会等待一定的时间,直到循环结束,CUserView的Call函数返回,对话框才会弹出。等待的时间和循环的长短成正比。
对话框是一种很不单纯的窗口。无论是创建、消息分发还是销毁,Windows都会对对话框做一些特殊的处理。如果用SDK进行对话框编程,就会发现创建对话框需要专门的Win32 API。而且,我们查阅平台SDK的讲述时,也会发现对话框需要Windows进行若干额外的“照顾”。事实上,之所以会出现前述的“延迟”情况,就是Windows进行额外协调的结果。
使用SPY++研究窗口消息,会发现非模态对话框创建时,原来拥有焦点的窗口会收到WM_KILLFOCUS消息,而且获得焦点的窗口是创建的对话框。
根据例子来看,这个窗口焦点的协调过程被上升到了线程协调的层次。现象就是:如果被去激活的窗口的线程被阻塞,不能立刻处理WM_KILLFOCUS消息的话,创建对话框的线程也会被阻塞,对话框一直不能被显示出来,直到线程不再阻塞,WM_KILLFOCUS被分发和处理。
这个问题产生的原因是:在主线程繁忙的时候有主线程必须要处理的消息,也就是主线程消息循环因为窗口处理函数占用时间过长而被阻塞。因此,这个问题更多是一个设计问题而非技术难点。也许,我们该问的是:
n 我真的要用一个冗长的工作来阻塞主线程这样长的时间吗?
n 我是否可以在单独的一个工作者线程中来处理这个长的工作?
考虑了其他的可能性后,如果对上述问题的答案仍然为“是”的话,可以采取以下解决方案:因为我们缺少的是消息循环,所以,加上消息循环,让对话框能够显示出来之后,再去进行其他工作的处理。
bool CUserView::Call(void)
{
m_pUIWorker = static_cast<UIWorker*>( AfxBeginThread(
RUNTIME_CLASS( UIWorker )));
while ( !m_pUIWorker->GetDoneFlag())
{
MSG msg;
::GetMessage( &msg, this->m_hWnd, NULL, NULL );
::TranslateMessage( &msg );
::DispatchMessage( &msg );
}
for ( int i = 0; i < 100000; ++i )
for ( int j = 0; j < 10000; ++j )
;
return true;
}
红色的代码就是加上消息循环。要注意的是,相应的线程类里面应该在显示出对话框后设置一个标志,并且让主线程可以查询到这个标志,从而终止这个临时的消息循环。
Windows的线程调度原则对于程序员来说非常简单。这条原则是:程序员无法决定线程调度过程。
只是因为“程序员无法决定线程调度过程”,并不意味着程序员不应该去了解一些特别的线程调度过程。在某些场合下,正如上面在和对话框相关的某些时机,也许,Windows的线程调度是有明确规则的。所以,大多数情况下,程序员可以认为Windows的线程调度对于自己来说是一个黑盒。但是,某些时候,这个盒子中间发生的事情也需要了解和掌握。
对话框是一个古老的话题,很多的人仔细讨论了对话框的方方面面。对话框一直是Windows里面非常特殊的一种窗口。它的消息循环,与其他窗口的协调要求,都和普通的窗口有不同之处。因此,为了配合这些不同之处,Windows在线程协调上也做了一些手脚。
更重要的启示是:多线程需要更多的考虑设计。
毫无疑问,多线程可以使多种工作并行进行,提高工作效率,改善界面响应。然而,多线程应用中一个麻烦的问题是:决定对哪些工作使用单独的线程。这个决定过程其实就是设计过程。如果设计方案不合理,比如,如本例子中反映出来的问题——在主线程被长时间的工作阻塞的情况下,增加的线程并不会给我们带来明显的响应改善。而且,如果设计方案不合理,会带来更多的“临时机制”的采用,如本例中必须增加一个单独的消息循环,并且需要在两个线程中就对话框是否创建出来进行通讯。这样的实现在很大程度上削减了希望用多线程带来的好处。