Multi-SDI
IE 浏览器可以同时打开多个窗口 , 这些打开的窗口是同一个应用程序管理的多个顶层 SDI 窗口 , 每一窗口对应一个线程 , 而不是每个窗口对应一个单独的应用程序 . 利用 WTL 对 Multi-SDI 的支持你也可以使用这个特性 . Multi-SDI 应用程序的设计原理非常简单 :
· 每个顶层窗口 ( 及其子窗口 ) 运行一个单线程 .
· 每个单线程都是一个 UI 线程 , 有自己独立的消息循环 , 负责处理其所属窗口的消息分发和消息映射 . 这意味着 , 所有线程在进程级别共享资源 , 并且当某个线程过于繁忙时 , 其它线程不受任何影响 , 仍然能快速响应用户请求 .
· 因此 , 每个单线程的顶层窗口对于用户来说 , 就像是一个独立的应用程序 .
创建 Multi-SDI 应用程序的关键是设计一个线程管理类,负责 :
1) 为每个请求的线程创建一个 SDI 框架窗口 .
2) 管理所有线程句柄及活动线程数 .
3) 随时掌握命令行参数以及窗口初始尺寸
WTL 并不提供任何基本的辅助类来支持 Multi-SDI 应用程序 . 然而 , WTL 应用程序向导 WTLAppWizard( 文章末尾详细讨论 ) 可以为 Multi-SDI 应用程序自动产生代码 . WTL 应用程序向导产生的线程管理类如下 :
class CThreadManager {
public:
// thread init param
struct _RunData
{
LPTSTR lpstrCmdLine;
int nCmdShow;
};
DWORD m_dwCount; //count of threads
HANDLE m_arrThreadHandles[MAXIMUM_WAIT_OBJECTS - 1];
CThreadManager() : m_dwCount(0) {}
DWORD AddThread(LPTSTR lpstrCmdLine, int nCmdShow);
void RemoveThread(DWORD dwIndex);
int Run(LPTSTR lpstrCmdLine, int nCmdShow);
};
int WINAPI WinMain(…) {
hRes = _Module.Init(NULL, hInstance);
CThreadManager mgr;
int nRet = mgr.Run(lpstrCmdLine, nCmdShow);
_Module.Term();
return nRet;
}
CThreadManager::Run 成员函数创建一个新的 UI 线程 , 然后调用 MsgWaitForMultipleObjects 无限等待 , MsgWaitForMultipleObjects 函数 的特点是它不但可以等待内核对象 , 还可以等消息 . 也就是当有消息到来时 , 该函数也一样可以返回 , 并处理消息 . 如果有线程终结了 , 它就递减线程数 , 然后返回继续等待 . 当所有线程完成后 , Run() 函数返回 , 应用程序结束 . 另一方面 , 如果 MsgWaitForMultipleObjects 确定收到了 WM_USER 消息 (WTL 的线程管理类用此消息来请求新的 UI 线程 ), 则线程管理类创建一个新 UI 线程 .
int CThreadManager::Run(LPTSTR lpstrCmdLine, int nCmdShow) {
MSG msg;
// force message queue to be created
::PeekMessage(&msg, NULL, WM_USER, WM_USER, PM_NOREMOVE);
AddThread(lpstrCmdLine, nCmdShow);
int nRet = m_dwCount;
DWORD dwRet;
while(m_dwCount > 0)
{
dwRet = ::MsgWaitForMultipleObjects(m_dwCount,
m_arrThreadHandles, FALSE, INFINITE, QS_ALLINPUT);
if(dwRet == 0xFFFFFFFF)
{
::MessageBox(NULL, _T("ERROR: Wait for multiple
objects failed!!!"), _T("MultiSDI_HTMLView"), MB_OK);
}
else if(dwRet >= WAIT_OBJECT_0 && dwRet <= (WAIT_OBJECT_0 +
m_dwCount - 1))
{
RemoveThread(dwRet - WAIT_OBJECT_0);
}
else if(dwRet == (WAIT_OBJECT_0 + m_dwCount))
{
::GetMessage(&msg, NULL, 0, 0);
if(msg.message == WM_USER)
AddThread("", SW_SHOWNORMAL);
else
::MessageBeep((UINT)-1);
}
else
::MessageBeep((UINT)-1);
}
return nRet;
}
注意 , WTL 应用程序向导生成的代码中使用的是 WM_USER 消息而不必是特殊的用户自定义消息 . 这是因为代码中仅仅是主线程用此消息来创建新 UI 线程 , 并且新建的 UI 线程自己并不创建任何窗口 , 所以通常情况下不会引起自定义消息的冲突 .
AddThread() 例程中把线程过程函数 (thread procedure) 地址传递给 Win32 API CreateThread() 来创建线程 . 线程管理类的静态方法 RunThread() 中创建了 SDI 框架窗口 , 并调用 Win32 API ShowWindow() 显示 , 然后启动消息循环 .
// CThreadManager::RunThread
static DWORD WINAPI RunThread(LPVOID lpData)
{
CMessageLoop theLoop;
_Module.AddMessageLoop(&theLoop);
_RunData* pData = (_RunData*)lpData;
CMainFrame wndFrame;
if(wndFrame.CreateEx() == NULL)
{
ATLTRACE(_T("Frame window creation failed!/n"));
return 0;
}
wndFrame.ShowWindow(pData->nCmdShow);
::SetForegroundWindow(wndFrame); // Win95 needs this
delete pData;
int nRet = theLoop.Run();
_Module.RemoveMessageLoop();
return nRet;
}
在 Multi-SDI 应用程序框架窗口菜单项 File/New 的消息映射函数中 , 需要投递一个 WM_USER 消息到主线程 , 以便通知线程管理类实例创建一个新的拥有框架窗口的 UI 线程 .
LRESULT CMainFrame::OnFileNewWindow(WORD, WORD, HWND, BOOL&) {
::PostThreadMessage(_Module.m_dwMainThreadID, WM_USER, 0, 0L);
return 0;
}
Figure 8a: Multi-SDI Application
Figure 8b: Multi-SDI Application
Multi-SDI 应用程序最适合于总是以单应用程序实例运行 . 也就是当用户双击 YourMultiSdi.exe 时 , 如果已经有此程序的实例在运行 , 那么启动的应该是一个新线程 , 而不是又启动一个新的应用程序实例 . WTL 的 Multi-SDI 并不提供此功能 . 不过使用 DDE 很容易实现 , 可以参考本文章附带的例子SingleInstance.
To be continued···