深入剖析WTL.doc

Similar documents
概述

INTRODUCTION TO COM.DOC

BOOL EnumWindows(WNDENUMPROC lparam); lpenumfunc, LPARAM (Native Interface) PowerBuilder PowerBuilder PBNI 2

int *p int a 0x00C7 0x00C7 0x00C int I[2], *pi = &I[0]; pi++; char C[2], *pc = &C[0]; pc++; float F[2], *pf = &F[0]; pf++;

FY.DOC

untitled

CC213

untitled

Microsoft Word - 11.doc

无类继承.key

概述

新版 明解C++入門編

Guava学习之Resources

SDK 概要 使用 Maven 的用户可以从 Maven 库中搜索 "odps-sdk" 获取不同版本的 Java SDK: 包名 odps-sdk-core odps-sdk-commons odps-sdk-udf odps-sdk-mapred odps-sdk-graph 描述 ODPS 基

Microsoft Word - 01.DOC

提问袁小兵:

epub83-1

ebook

Bus Hound 5

Microsoft PowerPoint - gp2.ppt

C PICC C++ C++ C C #include<pic.h> C static volatile unsigned char 0x01; static volatile unsigned char 0x02; static volatile unsigned cha

Microsoft Word - CIN-DLL.doc

帝国CMS下在PHP文件中调用数据库类执行SQL语句实例

<4D F736F F D E4345C6BDCCA84323B1E0B3CCD2AAB5E3D6AED2BB2E646F63>

1 Project New Project 1 2 Windows 1 3 N C test Windows uv2 KEIL uvision2 1 2 New Project Ateml AT89C AT89C51 3 KEIL Demo C C File

OOP with Java 通知 Project 4: 4 月 19 日晚 9 点

基于UML建模的管理管理信息系统项目案例导航——VB篇

ebook50-11

Microsoft Word - PHP7Ch01.docx

ebook140-8

A Preliminary Implementation of Linux Kernel Virus and Process Hiding

全国计算机技术与软件专业技术资格(水平)考试

新・明解C言語入門編『索引』

res/layout 目录下的 main.xml 源码 : <?xml version="1.0" encoding="utf 8"?> <TabHost android:layout_height="fill_parent" xml

Microsoft PowerPoint - 4. 数组和字符串Arrays and Strings.ppt [兼容模式]

EJB-Programming-4-cn.doc

RunPC2_.doc

Fun Time (1) What happens in memory? 1 i n t i ; 2 s h o r t j ; 3 double k ; 4 char c = a ; 5 i = 3; j = 2; 6 k = i j ; H.-T. Lin (NTU CSIE) Referenc

C语言的应用.PDF

TwinCAT 1. TwinCAT TwinCAT PLC PLC IEC TwinCAT TwinCAT Masc

untitled

// HDevelopTemplateWPF projects located under %HALCONEXAMPLES%\c# using System; using HalconDotNet; public partial class HDevelopExport public HTuple

RUN_PC連載_12_.doc

Simulator By SunLingxi 2003

SDS 1.3

WinMDI 28

OOP with Java 通知 Project 3: 3 月 29 日晚 9 点 4 月 1 日上课

6 C51 ANSI C Turbo C C51 Turbo C C51 C51 C51 C51 C51 C51 C51 C51 C C C51 C51 ANSI C MCS-51 C51 ANSI C C C51 bit Byte bit sbit

chap07.key

MFC 2/e PDF GBK mirror - anyway solution MFC 1/e MFC 2/e

Microsoft PowerPoint - 8. 运算符重载 Operator Overloading.pptx

ebook51-14

untitled

ebook 86-15

bingdian001.com

1.ai

NOWOER.OM m/n m/=n m/n m%=n m%n m%=n m%n m/=n 4. enum string x1, x2, x3=10, x4, x5, x; 函数外部问 x 等于什么? 随机值 5. unsigned char *p1; unsigned long *p

OOP with Java 通知 Project 2 提交时间 : 3 月 14 日晚 9 点 另一名助教 : 王桢 学习使用文本编辑器 学习使用 cmd: Power shell 阅读参考资料

_汪_文前新ok[3.1].doc

mvc

软件工程文档编制

51 C 51 isp 10 C PCB C C C C KEIL

Microsoft Word - 3D手册2.doc

ebook140-9

, 7, Windows,,,, : ,,,, ;,, ( CIP) /,,. : ;, ( 21 ) ISBN : -. TP CIP ( 2005) 1

06 01 action JavaScript action jquery jquery AJAX CSS jquery CSS jquery HTML CSS jquery.css() getter setter.css('backgroundcolor') jquery CSS b

Java

epub 61-2

ebook14-4

目 錄 壹 青 輔 會 結 案 附 件 貳 活 動 計 劃 書 參 執 行 內 容 一 教 學 內 容 二 與 當 地 教 師 教 學 交 流 三 服 務 執 行 進 度 肆 執 行 成 效 一 教 學 課 程 二 與 當 地 教 師 教 學 交 流 三 服 務 滿 意 度 調 查 伍 服 務 檢

Microsoft Word - template.doc

C 1

Microsoft Word - ch04三校.doc

Strings

Important Notice SUNPLUS TECHNOLOGY CO. reserves the right to change this documentation without prior notice. Information provided by SUNPLUS TECHNOLO

四川省普通高等学校

3.1 num = 3 ch = 'C' 2

Microsoft PowerPoint - directx01.ppt

Microsoft PowerPoint - string_kruse [兼容模式]

untitled

untitled

Transcription:

深入剖析 WTL Win32 模型 WTL 是 Windows Template Library 的缩写 最初,WTL 是由微软的 ATL(Active Template Librar y) 小组成员开发的一个 SDK 例子 主要是基于 ATL 的对 Win32 API 的封装 从 2.0 后, 功能逐步完善, 成为了一个完整的支持窗口的框架 (windows framework) 与 MFC 相比较, 功能并没有 MFC 完善 比如 MFC 支持 doc/view 架构, 而 WTL 并不支持 同时,WT L 也没有 Microsoft 的官方支持 但是,WTL 是基于模版 (template) 的, 其应用程序最小只有 24KB, 同 时不象 MFC, 依赖 DLL(MFC 需要 MFC42.DLL) WTL 系列文章对 WTL 进行了深入剖析, 希望能方便您对 WTL 有一个深入的理解, 从而能得心应手的开发 出高质量的 Windows 应用程序 Win32 的线程模型 为了便于以后的探讨, 首先看一下 Win32 的线程模型 一个 Win32 应用程序 ( 或进程 ) 是由一个或多个并发的线程组成的, 其中第一个启动的线程称为主线程 Win32 定义了两种类型的线程, 界面线程和工作线程 Win32 的每个进程可以有一个或多个界面线程和 / 或多个工作线程 界面线程拥有一个或多个窗口, 拥有一个消息队列和其它属于界面线程的元素 工作线 程就是一般的线程, 它没有窗口, 没有消息队列 界面线程通常有一个或几个窗口 当某一个窗口有消息时, 界面线程会调用相应的窗口函数 (Windows Pr ocess) 来处理该事件 由于某消息循环由它界面线程处理, 同时不必在乎是哪个线程发送消息的, 因此, Windows 会保证线程间的同步问题 对于工作线程, 线程间的同步必须由程序员来实现 尽可能避免死锁和竞争出现 Win32 应用程序模型

Win32 应用程序可以分成两大类 : 控制台程序 (console application) 和窗口界面程序 (windows GUI ap plication) 控制台程序的入口函数是 main(), 窗口界面程序的入口函数是 WinMain() 入口函数就是程序的主线程的运行起点 这里讨论的开发框架 (Framework) 是针对窗口界面程序的 窗口界面程序通常分成以下几类 :SDI, MDI, multi-sdi, 和 Dialog 应用程序 SDI(Single Document Interface) 应用程序通常只有一个主窗口 ( 通常是一个框架窗口,Frame Windo w) 框架窗口包含菜单 工具栏 状态栏和称为视 (View) 的客户工作区 multi-sdi(multiple Threads SDI) 应用程序有框架个主窗口 比如 IE 浏览器, 使用 " 文件 / 新建 / 窗口 " 命 令后, 会出现另一个 IE 窗口 MDI(Multiple Document Interface) 应用程序有一个主框架窗口, 但有多个子框架窗口 每个子窗口都 有自己的视 (View) Dialog 应用程序是基于对话框的 通常一个简单的 SDI 应用程序由两个函数组成 一个是应用程序入口函数 WinMain(), 另一个是该应用程 序窗口的窗口函数 程序 ( 主线程 ) 从入口函数开始运行 在该函数中, 首先是注册并创建一个主窗口 然后, 启动消息循环 消息循环中, 根据不同的消息, 将消息发送到窗口函数中处理 当消息是退出消息时, 该入口函数会退出 消息循环, 然后结束程序 下面是一个最简单的 Windows 界面应用程序

// 应用程序入口函数 int APIENTRY WinMain(HINSTANCE hinstance, HINSTANCE hprevinstance, LPSTR lpcmdline, int ncmdshow) MSG msg; //1. 注册窗口类 WNDCLASSEX wcex; wcex.cbsize = sizeof(wndclassex); wcex.style = CS_HREDRAW CS_VREDRAW; wcex.lpfnwndproc = (WNDPROC)WndProc; // 指定窗口函数 wcex.cbclsextra = 0; wcex.cbwndextra = 0; wcex.hinstance = hinstance; wcex.hicon = LoadIcon(hInstance, (LPCTSTR)IDI_HELLOWORLD); wcex.hcursor = LoadCursor(NULL, IDC_ARROW); wcex.hbrbackground = (HBRUSH)(COLOR_WINDOW+1); wcex.lpszmenuname = (LPCSTR)IDC_HELLOWORLD; wcex.lpszclassname = szwindowclass; wcex.hiconsm = LoadIcon(wcex.hInstance,(LPCTSTR)IDI_SMALL); RegisterClassEx(&wcex); //2. 创建一个该类型的窗口 HWND hwnd; hwnd = CreateWindow(szWindowClass,szTitle,

WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, 0, CW_USEDEFAULT, 0, NULL, NULL, hinstance, NULL); if (!hwnd) return FALSE; //3. 按 ncmdshow 所指定的方式显示窗口 ShowWindow(hWnd, ncmdshow); UpdateWindow(hWnd); //4. 启动消息循环, 将消息发送给相应的窗口函数 while (GetMessage(&msg, NULL, 0, 0)) TranslateMessage(&msg); DispatchMessage(&msg); return msg.wparam; // 窗口函数 LRESULT CALLBACK WndProc(HWND hwnd, UINT message, WPARAM wparam, LPARAM lparam) PAINTSTRUCT ps; HDC hdc; char* szhello = "Hello, world!"; switch (message) case WM_PAINT: hdc = BeginPaint(hWnd, &ps);

RECT rt; GetClientRect(hWnd, &rt); DrawText(hdc, szhello, strlen(szhello),&rt,dt_center); EndPaint(hWnd, &ps); break; case WM_DESTROY: PostQuitMessage(0); // 退出应用程序 break; default: return DefWindowProc(hWnd, message, wparam, lparam); return 0; 上面程序的执行过程如下 : 1 注册窗口类 在使用 CreateWindwo() 或 CreateWindowEx() 创建窗口时, 必须提供一个标识窗口类的字符串 该窗口 类定义了一些窗口的基本属性 其中一个重要的工作是向操作系统提供窗口函数 该函数是回调函数, 用 于处理发送给该窗口的消息 在上面程序中, 仅仅简单的处理了两个消息 一个是向窗口区域画出 "Hello world." 字符串 另一个是当 窗口撤消时, 向应用程序发送 " 退出应用程序 " 消息 2 创建窗口

3 显示窗口 4 启动消息循环, 分发并处理消息 while (GetMessage(&msg, NULL, 0, 0)) TranslateMessage(&msg); DispatchMessage(&msg); 在上述消息循环代码中, 调用 GetMessage() 从线程的消息队列中取出一条消息 如果消息为 0( 由窗口 函数处理 "WM_DESTROY" 消息时发送的 "PostQuitMessage(0)"), 会退出消息循环 然后调用 TranslateMessage() 翻译消息 翻译后, 再调用 DispatchMessage() 将该消息分发至相应的窗口函数进行处理 ( 实际上 DispatchMes sage() 将该消息作为参数调用对应的窗口的窗口函数, 这就是分发的实质 ) 深入剖析 WTL 如何封装 Windows 界面程序 首先还是让我们来看看 WTL 是怎样封装应用程序线程的 和 ATL 类似,WTL 使用一个 _Module 全局变量来保存全局数据, 并通过它来引用应用程序级的代码 在 WTL 中, 该变量是 CAppModule 或 CServerAppModule 的实例 后者通常作为 COM 服务器的应用程 序 每个应用程序都有一个或多个界面线程组成 首先剖析一下 WTL 是怎样管理只有一个界面线程的 ( 除了 Mutli-SDI 应用程序 )

单个界面线程的封装 先看应用程序的入口函数 CAppModule _Module; int WINAPI _twinmain(hinstance hinstance, HINSTANCE /*hprevinstance*/, LPTSTR lpstrcmdline, int ncmdshow) HRESULT hres = ::CoInitialize(NULL); // If you are running on NT 4.0 or higher you can use // the following call instead to // make the EXE free threaded. This means that calls // come in on a random RPC thread. // HRESULT hres = ::CoInitializeEx(NULL, COINIT_MULTITHREADED); ATLASSERT(SUCCEEDED(hRes)); // this resolves ATL window thunking problem when Microsoft // Layer for Unicode (MSLU) is used ::DefWindowProc(NULL, 0, 0, 0L); AtlInitCommonControls(ICC_COOL_CLASSES ICC_BAR_CLASSES); // add flags to support other controls // 初始化模块 hres = _Module.Init(NULL, hinstance); ATLASSERT(SUCCEEDED(hRes)); // 程序逻辑, 调用全局函数 Run() int nret = Run(lpstrCmdLine, ncmdshow); // 终止模块

_Module.Term(); ::CoUninitialize(); return nret; 在上面的代码中,_Module 是一个全局变量, 这里是 CAppModule 的一个实例 而 CAppModule 类是 对应用程序的封装 它封装了诸如初始化模块等功能 一个 _Module 还维持一个消息循环 Map 入口函数名为 _twinmain() 当使用 UNICODE 时, 编译器会将它替换为 wwinmain(), 否则, 为 WinM ain() 入口函数其实就是主线程 (_Module) 的起始点 在该函数中, 最关键的逻辑是调用了全局函数 Run(), 它是核心程序逻辑所在 下面来看一下这个函数 int Run(LPTSTR /*lpstrcmdline*/ = NULL, int ncmdshow = SW_SHOWDEFAULT) CMessageLoop theloop; _Module.AddMessageLoop(&theLoop); CMainFrame wndmain; if(wndmain.createex() == NULL) ATLTRACE(_T("Main window creation failed!\n")); return 0; wndmain.showwindow(ncmdshow);

int nret = theloop.run(); _Module.RemoveMessageLoop(); return nret; 该函数创建了一个 CMessageLoop 实例, 该实例包含了这个线程的消息循环 这些消息循环都放在模块 的全局消息循环中, 通过线程的 ID 来索引 这样, 该线程的其它代码就能访问得到 每一个应用程序维护一个消息循环队列 Map, 应用程序中的每个线程都通过 "_Module.AddMessageLoo p(&theloop);", 把该线程的消息循环加入到 _Module 的消息循环 Map 中 消息循环对象包含了消息过滤和空闲处理 每个线程都可以加入空闲处理代码和消息过滤 我们将在后面更加详细地探讨消息循环 这里仅需要知道, 线程是通过创建一个消息循环对象 CMessage Loop 来包装消息循环的 多个界面线程的封装 那么再看看, 当一个应用程序有多个界面线程时,WTL 是怎样对这些线程进行管理的 通常一个 Mutli-SDI 应用程序包含多个界面线程 象 IE 浏览器就是这样的应用程序 每个主窗口都在一个 单独的线程中运行, 每个这样的线程都有一个消息处理 WTL App Wizard 通过为程序的主线程创建一个 Thread Manager 类的实例来实现的 class CThreadManager

public: // thread init param struct _RunData LPTSTR lpstrcmdline; int ncmdshow; ; static DWORD WINAPI RunThread(LPVOID lpdata); 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); ; 该类中,m_arrThreadHandles[MAXIMUM_WAIT_OBJECTS-1] 是用于存放运行的线程的句柄的数组 而 m_dwcount 是当前有多少线程的计数值 AddThread() 和 RemoveThread() 两个函数用于将线程从存放线程句柄的数组中增加或删除线程句柄 RunThread() 是线程的逻辑所在 它的主要工作是创建一个消息队列, 并把它加入主线程 (_Module) 的 消息队列 Map 中 它还创建该线程的主窗口 该函数的逻辑与前面描述的全局函数 Run() 相似 其实, 应该可以想到的 因为前面的 _Module 是应用程 序的主线程

下面是该函数的代码 : 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; 当使用 AddThread() 函数创建一个线程时, 就是创建一个 RunThread() 的线程 下面是 AddThread() 的代码

DWORD AddThread(LPTSTR lpstrcmdline, int ncmdshow) if(m_dwcount == (MAXIMUM_WAIT_OBJECTS - 1)) ::MessageBox(NULL, _T("ERROR: Cannot create ANY MORE threads!!!"), _T("test2"), MB_OK); return 0; _RunData* pdata = new _RunData; pdata->lpstrcmdline = lpstrcmdline; pdata->ncmdshow = ncmdshow; DWORD dwthreadid; HANDLE hthread = ::CreateThread(NULL, 0, RunThread, pdata, 0, &dwthreadid); if(hthread == NULL) ::MessageBox(NULL, _T("ERROR: Cannot create thread!!!"), _T("Application"), MB_OK); return 0; m_arrthreadhandles[m_dwcount] = hthread; m_dwcount++; return dwthreadid;

在上述代码的语句 : HANDLE hthread = ::CreateThread(NULL, 0, RunThread, pdata, 0, &dwthreadid); 中,RunThread 作为参数传递给 CreateThread() 那么应用程序是怎样通过 CThreadManager 类来管理多个线程的呢? 先看一下现在应用程序的入口函数 ( 主线程 ) 的逻辑 : int WINAPI _twinmain(hinstance hinstance, HINSTANCE /*hprevinstance*/, LPTSTR lpstrcmdline, int ncmdshow) HRESULT hres = ::CoInitialize(NULL); // If you are running on NT 4.0 or higher you can use the // following call instead to // make the EXE free threaded. This means that calls come in on // a random RPC thread. // HRESULT hres = ::CoInitializeEx(NULL, COINIT_MULTITHREADED); ATLASSERT(SUCCEEDED(hRes)); // this resolves ATL window thunking problem when

Microsoft // Layer for Unicode (MSLU) is used ::DefWindowProc(NULL, 0, 0, 0L); AtlInitCommonControls(ICC_COOL_CLASSES ICC_BAR_CLASSES); // add flags to support other controls hres = _Module.Init(NULL, hinstance); ATLASSERT(SUCCEEDED(hRes)); int nret = 0; // BLOCK: Run application // 注意该处和前面应用程序的差别 // 这里创建一个 CThreadManager 类的实例, 然后调用该类的 Run() 函数 CThreadManager mgr; nret = mgr.run(lpstrcmdline, ncmdshow); _Module.Term(); ::CoUninitialize(); return nret; 在这个入口函数 ( 主线程 ), 创建了一个 CThreadManager 类的实例 然后调用该类的 Run() 函数 看一下 Run() 做了什么事情 int Run(LPTSTR lpstrcmdline, int ncmdshow)

MSG msg; // force message queue to be created ::PeekMessage(&msg, NULL, WM_USER, WM_USER, PM_NOREMOVE); // 创建一个界面线程 为该应用程序的第一个界面线程 该界面线程的主体 // 逻辑 RunThread() 是创建主窗口, 并创建消息循环对象 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("test2"), 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; 该函数首先创建一个界面线程, 这是这个应用程序的第一个界面线程 通过调用 AddThread() 创建界面线 程, 指定 RunThread() 为线程的主体逻辑 它的主要任务是创建一个框架窗口, 然后创建一个消息循环对 象, 并把该对象加入到主线程 _Module 的消息循环 Map 中 随后, 该函数调用 MsgWaitForMultipleObjects(), 进入等待状态 ( 以 INFINITE 为时间参数 ) 有三种 情况可以使该函数返回 一种是 Wait for multiple objects failed, 是出错情况 一种是某一个查询的线程结束 此时调用 RemoveThread() 将该线程句柄删除 最后一种是某一线程接收到 WM_USER 消息 此时, 调用 AddThread() 创建另一个界面线程 上面的逻辑一直运行, 直到所有的界面线程都结束为止

现在, 您是否对如何封装 Windows 界面程序有一定的了解了呢? 如果是, 我们接下来就将讨论 WTL 的消 息循环 深入剖析 WTL WTL 消息循环机制详解收藏 WTL 消息循环机制实现了消息过滤和空闲处理机制 消息过滤 首先看一下 CMessageLoop 的核心逻辑 CMessageLoop.Run() 的代码 : int CMessageLoop.Run() BOOL bdoidle = TRUE; int nidlecount = 0; BOOL bret; for(;;) while(!::peekmessage(&#38;m_msg, NULL, 0, 0, PM_NOREMOVE) &#38;&#38; bdoidle) if(!onidle(nidlecount++)) bdoidle = FALSE; bret = ::GetMessage(&#38;m_msg, NULL, 0, 0); if(bret == -1)

ATLTRACE2(atlTraceUI, 0, _T("::GetMessage returned -1 (error)\n")); continue; // error, don't process else if(!bret) ATLTRACE2(atlTraceUI, 0, _T("CMessageLoop::Run - exiting\n")); break; // WM_QUIT, exit message loop if(!pretranslatemessage(&#38;m_msg)) ::TranslateMessage(&#38;m_msg); ::DispatchMessage(&#38;m_msg); if(isidlemessage(&#38;m_msg)) bdoidle = TRUE; nidlecount = 0; return (int)m_msg.wparam; 在上面的代码中, 有三个需要注意的地方

消息循环中, 首先调用 PeekMessage() 判断消息队列中是否有消息 如果没有, 则调用 OnIdle() 函数 这 就是调用空闲处理 第二个注意点是, 如果有消息, 则调用 GetMessage() 得到消息 然后做判断, 如果是错误返回, 则对消 息并不进行处理 然后再判断是否是 WM_QUIT 消息, 如果是, 则退出消息循环, 从而结束该界面线程 接下来是第三个注意点 在 TranslateMessage() 消息之前, 调用了成员函数 PreTranslateMessage() 这 为在 TranslateMessage() 之前对消息进行处理提供了机会 PreTranslateMessage() 会遍历 CMessageLoop 中所有 CMessageFilterd 对象的 PreTranslateMessage () 函数, 直到其中一个返回为 TRUE 或它们都返回为 FALSE 当有一个返回为 TRUE 时, 即对消息处理了, 那么, 就不再调用 TranslateMessage(), 而是进入下一个循环 这种消息过滤机制提供了一种在不同窗口之间传递消息的机制 CMessageFilter 是一个 C++ 的接口, 即只定义了抽象虚拟函数 class CMessageFilter public: virtual BOOL PreTranslateMessage(MSG* pmsg) = 0; ; 这样, 任何类想要实现消息过滤, 只需实现这个接口 在 C++ 中就采用继承 然后再实现 PreTranslate Message() 函数即可 ATL/WTL App Wizard 生成的框架窗口中实现 PreTranslateMessage() 的代码如下 :

BOOL CMainFrame::PreTranslateMessage(MSG* pmsg) if(cframewindowimpl<cmainframe>::pretranslatemessage(pmsg)) return TRUE; return m_view.pretranslatemessage(pmsg); 这种消息过滤机制的好处是任何实现了 CMessageFilter 接口的对象, 都可以接受消息过滤 程序通过 AddMessageFilter() 和 RemoveMessageFilter() 把这些对象加入到 CMessageLoop 中 空闲处理 空闲处理的机制和消息过滤类似 这里不再介绍 我们要把主要经历放在 WTL 的框架窗口分析上 稍后, 我们将进入这部分内容 深入剖析 WTL WTL 框架窗口分析 (1) 收藏 WTL 的基础是 ATL WTL 的框架窗口是 ATL 窗口类的继承 因此, 先介绍一下 ATL 对 Windows 窗口的 封装 由第一部分介绍的 Windows 应用程序可以知道创建窗口和窗口工作的逻辑是 : 1 注册一个窗口类 2 创建该类窗口

3 显示和激活该窗口 4 窗口的消息处理逻辑在窗口函数中 该函数在注册窗口类时指定 从上面的逻辑可以看出, 要封装窗口主要需解决怎样封装窗口消息处理机制 对于窗口消息处理机制的封装存在两个问题 一是, 为了使封装好的类的窗口函数对外是透明的, 我们就会想到, 要将窗口函数的消息转发到不同的类的实例 那么怎样将窗口函数中的消息转发给封装好的类的实例? 因为所有封装好的类窗口的窗口函数只有一个, 即一类窗口只有一个窗口函数 而我们希望的是将消息发送给某个类的实例 问题是窗口函数并不知道是哪个实例 它仅仅知道的是 HWND, 而不是类的实例的句柄 因此, 必须有一种办法, 能通过该 HWND, 找到与之相对应的类的实例 二是, 假设已经解决了上面的问题 那么怎样将消息传递给相应的类的实例 通常的办法是采用虚函数 将每个消息对应生成一个虚函数 这样, 在窗口处理函数中, 对于每个消息, 都调用其对应的虚函数即可 但这样, 会有很多虚函数, 使得类的虚函数表十分巨大 为此, 封装窗口就是要解决上面两个基本问题 对于第二个问题,ATL 是通过只定义一个虚函数 然后, 通过使用宏, 来生成消息处理函数 对于第一个问题,ATL 通过使用动态改变 HWND 参数方法来实现的 ATL 对窗口的封装 图示是 ATL 封装的类的继承关系图 从图中可以看到有两个最基本的类 一个是 CWindow, 另一个是 C MessageMap

CWindows 是对 Windows 的窗口 API 的一个封装 它把一个 Windows 句柄封装了起来, 并提供了对该 句柄所代表的窗口的操作的 API 的封装 CWindow 的实例是 C++ 语言中的一个对象 它与实际的 Windows 的窗口通过窗口句柄联系 创建一个 CWindow 的实例时并没有创建相应的 Windows 的窗口, 必须调用 CWindow.Create() 来创建 Windows 窗口 也可以创建一个 CWindow 的实例, 然后将它与已经存在的 Windows 窗口挂接起来 CMessageMap 仅仅定义了一个抽象虚函数 ProcessWindowMessage() 所有的包含消息处理机制 的窗口都必须实现该函数 通常使用 ATL 开发程序, 都是从 CWindowImplT 类派生出来的 从类的继承图可以看出, 该类具有一般 窗口的操作功能和消息处理机制 在开发应用程序的时候, 你必须在你的类中定义 消息映射 BEGIN_MSG_MAP(CMainFrame) MESSAGE_HANDLER(WM_CREATE, OnCreate) COMMAND_ID_HANDLER(ID_APP_EXIT, OnFileExit) COMMAND_ID_HANDLER(ID_FILE_NEW, OnFileNew) COMMAND_ID_HANDLER(ID_VIEW_TOOLBAR, OnViewToolBar) COMMAND_ID_HANDLER(ID_VIEW_STATUS_BAR, OnViewStatusBar) COMMAND_ID_HANDLER(ID_APP_ABOUT, OnAppAbout) CHAIN_MSG_MAP(CUpdateUI<CMainFrame>)

CHAIN_MSG_MAP(CFrameWindowImpl<CMainFrame>) END_MSG_MAP() 我们知道一个窗口函数的通常结构就是许多的 case 语句 ATL 通过使用宏, 直接形成窗口函数的代码 消息映射是用宏来实现的 通过定义消息映射和实现消息映射中的消息处理句柄, 编译器在编译时, 会为 你生成你的窗口类的 ProcessWindowMessage() 消息映射宏包含消息处理宏和消息映射控制宏 BEGIN_MSG_MAP() 和 END_MSG_MAP() 每个消息映射都由 BEGIN_MSG_MAP() 宏开始 我们看一下这个宏的定义 : #define BEGIN_MSG_MAP(theClass) \ public: \ BOOL ProcessWindowMessage(HWND hwnd, UINT umsg, WPARAM wparam, LPARAM lparam, LRESULT& lresult, DWORD dwmsgmapid = 0) \ \ BOOL bhandled = TRUE; \ hwnd; \ umsg; \ wparam; \ lparam; \

lresult; \ bhandled; \ switch(dwmsgmapid) \ \ case 0: 一目了然, 这是函数 ProcessWindowMessage() 的实现 与 MFC 的消息映射相比,ATL 的是多么的简单 记住越是简单越吸引人 需要注意的是 dwmsgmapid 每个消息映射都有一个 ID 后面会介绍为什么要用这个 于是可以推断, 消息处理宏应该是该函数的函数体中的某一部分 也可以断定 END_MSG_MAP() 应该定义 该函数的函数结尾 我们来验证一下 : #define END_MSG_MAP() \ break; \ default: \ ATLTRACE2(atlTraceWindowing, 0, _T("Invalid message map ID (%i)\n"), dwmsgmapid); \ ATLASSERT(FALSE); \ break; \ \ return FALSE; \

深入剖析 WTL WTL 框架窗口分析 (2) ATL 的消息处理宏 消息映射的目的是实现 ProcessWindowMessage() ProcessWindowMessage() 函数是窗口函数的关键 逻辑 一共有三种消息处理宏, 分别对应三类窗口消息 普通窗口消息 ( 如 WM_CREATE), 命令消息 (WM _COMMANS) 和通知消息 (WM_NOTIFY) 消息处理宏的目的是将消息和相应的处理函数 ( 该窗口的成员函数 ) 联系起来 普通消息处理宏 有两个这样的宏 :MESSAGE_HANDLER 和 MESSAGE_RANGE_HANDLER 第一个宏将一个消息和一个消息处理函数连在一起 第二个宏将一定范围内的消息和一个消息处理函数连 在一起 消息处理函数通常是下面这样的 : LRESULT MessageHandler(UINT umsg, WPARAM wparam, LPARAM lparam, BOOL& bhandled); 注意最后一个参数 bhandled 它的作用是该处理函数是否处理该消息 如果它为 FALSE, 消息 MAP 的 其它处理函数会来处理这个消息

我们看一下 MESSAGE_HANDLE 的定义 : #define MESSAGE_HANDLER(msg, func) \ if(umsg == msg) \ \ bhandled = TRUE; \ lresult = func(umsg, wparam, lparam, bhandled); \ if(bhandled) \ return TRUE; \ 在上面的代码中, 首先判断是否是想要处理的消息 如果是的, 那么调用第二个参数表示的消息处理函数 来处理该消息 注意 bhandled, 如果在消息处理函数中设置为 TRUE, 那么, 在完成该消息处理后, 会进入 return TRU E 语句, 从 ProcessWindowMessage() 函数中返回 如果 bhandled 在调用消息处理函数时, 设置为 FALSE, 则不会从 ProcessWindowMessage 中返回, 而 是继续执行下去 命令消息处理宏和通知消息处理宏 命令消息处理宏有五个 COMMAND_HANDLER,COMMAND_ID_HANDLER,COMMAND_CODE _HANDLER,COMMAND_RANGE_HANDLER 和 COMMAND_RANGE_CODE_HANDLER 通知消息处理宏有五个 --NOTIFY_HANDLER,NOTIFY_ID_HANDLER,NOTIFY_CODE_HANDLER,N

OTIFY_RANGE_HANDLER 和 NOTIFY_RANGE_CODE_HANDLER 我们不再详细分析 通过上面的分析, 我们知道了 ATL 是怎样实现窗口函数逻辑的 那么 ATL 是怎样封装窗口函数的呢? 为了 能理解 ATL 的封装方法, 还必须了解 ATL 中的窗口 subclass 等技术 我们将在分析完这些技术之后, 再 分析 ATL 对窗口消息处理函数的封装 扩展窗口类的功能 我们知道 Windows 窗口的功能由它的窗口函数指定 通常在创建 Windows 应用程序时, 我们要开发一 个窗口函数 通过定义对某些消息的相应来实现窗口的功能 在每个窗口处理函数的最后, 我们一般用下面的语句 : default: return DefWindowProc(hWnd, message, wparam, lparam); 它的意思是, 对于没有处理的消息, 我们将它传递给 Windows 的确省窗口函数 Windows 除了提供这个缺省的窗口函数, 还为某些标准的控制提供了一些预定义的窗口函数 我们在注册窗口类的时候, 指定了该窗口类的窗口处理函数 扩展窗口类的功能, 就是要改变窗口函数中对某些消息的处理逻辑

下面我们来看几种扩展窗口功能的技术, 以及看看 ATL 是怎样实现的 派生和 CHAIN_MSG_MAP() 很自然, 我们会在一个窗口类的基础上派生另一个 然后通过定义不同的消息处理函数 下面是一个简单的实例 ( 该例子摘自 MSDN) class CBase: public CWindowImpl< CBase > // simple base window class: shuts down app when closed BEGIN_MSG_MAP( CBase ) MESSAGE_HANDLER( WM_DESTROY, OnDestroy ) END_MSG_MAP() LRESULT OnDestroy( UINT, WPARAM, LPARAM, BOOL& ) PostQuitMessage( 0 ); return 0; ; class CDerived: public CBase // derived from CBase; handles mouse button events BEGIN_MSG_MAP( CDerived ) MESSAGE_HANDLER( WM_LBUTTONDOWN, OnButtonDown ) CHAIN_MSG_MAP( CBase ) // chain to base class END_MSG_MAP()

LRESULT OnButtonDown( UINT, WPARAM, LPARAM, BOOL& ) ATLTRACE( "button down\n" ); return 0; ; 深入剖析 WTL WTL 框架窗口分析 (3) 在上面的例子中,CDerived 从 CBase 中派生出来 CDerived 类通过定义一个 WM_LBUTTONDOWN 消 息处理函数来改变 CBase 类代表的窗口的功能 这样,CBase 类的消息映射定义了一个 ProcessWindowMessage() 函数, 而 CDerived 类的消息映射也 定义了一个 ProcessWindowMessage() 函数 那么, 我们在窗口处理函数逻辑中怎样把这两个类的 ProcessWindowMessage() 连起来呢?( 想想为什 么要连起来?) 在 CDerived 的消息映射中, 有一个宏 CHAIN_MSG_MAP() 它的作用就是把两个类对消息的处理连起来 看一下这个宏的定义 : #define CHAIN_MSG_MAP(theChainClass) \ \ if(thechainclass::processwindowmessage(hwnd, umsg, wparam, lparam, lresult)) \

return TRUE; \ 很简单, 它仅仅调用了基类的 ProcessWindowMessage() 函数 也就是说,CDerived 类的 ProcessWindowMessage() 包含两部分, 一部分是调用处理 WM_LBUTTOND OWN 的消息处理函数, 该函数是该类的成员函数 第二部分是调用 CBase 类的 ProcessWindowMessa ge() 函数, 该函数用于处理 WM_DESTROY 消息 在后面对窗口函数的封装中, 我们会知道, 对于其他消息处理,CDerived 会传递给缺省窗口函数 派生和 ALT_MSG_MAP() 如果我们希望在 CBase 类上再派生一个新的窗口类 该类除了要对 WM_RBUTTONDOWN 做不同的处理 外, 还希望 CBase 对 WM_DESTROY 消息的响应与前一个例子不同 比如希望能提示关闭窗口信息 那怎么处理呢?ATL 提供了一种机制, 它由 ALT_MSG_MAP() 实现 它使得一个类的消息映射能处理多个 Windows 窗口类 下面是具体的示例 : // in class CBase: BEGIN_MSG_MAP( CBase ) MESSAGE_HANDLER( WM_DESTROY, OnDestroy1 ) ALT_MSG_MAP( 100 ) MESSAGE_HANDLER( WM_DESTROY, OnDestroy2 ) END_MSG_MAP()

ALT_MSG_MAP() 将消息映射分成两个部分 每个部分的消息映射都有一个 ID 上面的消息映射的 ID 分 别为 0 和 100 分析一下 ALT_MSG_MAP(): #define ALT_MSG_MAP(msgMapID) \ break; \ case msgmapid: 很简单, 它结束了前面的 msgmapid 的处理, 开始进入另一个 msgmapid 的处理 那么, 在 CDerived 类的消息映射中, 是怎样将两个类的 ProcessWindowMessage() 函数的逻辑连在一起 的呢? // in class CDerived: BEGIN_MSG_MAP( CDerived ) CHAIN_MSG_MAP_ALT( CBase, 100 ) END_MSG_MAP() 这里使用 CHAIN_MSG_MAP_ALT() 宏 它的具体定义如下 : #define CHAIN_MSG_MAP_ALT(theChainClass, msgmapid) \

\ if(thechainclass::processwindowmessage(hwnd, umsg, wparam, lparam, lresult, msgmapid)) \ return TRUE; \ 不再分析其原理 请参考前面对 CHAIN_MSG_MAP() 宏的分析 深入剖析 WTL WTL 框架窗口分析 (4) superclass 是一种生成新的窗口类的方法 它的中心思想是依靠现有的窗口类, 克隆出另一个窗口类 被 克隆的类可以是 Windows 预定义的窗口类, 这些预定义的窗口类有按钮或下拉框控制等等 也可以是一 般的类 克隆的窗口类使用被克隆的类 ( 基类 ) 的窗口消息处理函数 克隆类可以有自己的窗口消息处理函数, 也可以使用基类的窗口处理函数 需要注意的是,superclass 是在注册窗口类时就改变了窗口的行为 即通过指定基类的窗口函数或是自己 定义的窗口函数 这与后面讲到的 subclass 是不同的 后者是在窗口创建完毕后, 通过修改窗口函数的地 址等改变一个窗口的行为的 请看示例 ( 摘自 MSDN): class CBeepButton: public CWindowImpl< CBeepButton > public: DECLARE_WND_SUPERCLASS( _T("BeepButton"), _T("Button") ) BEGIN_MSG_MAP( CBeepButton ) MESSAGE_HANDLER( WM_LBUTTONDOWN, OnLButtonDown )

END_MSG_MAP() LRESULT OnLButtonDown( UINT, WPARAM, LPARAM, BOOL& bhandled ) MessageBeep( MB_ICONASTERISK ); bhandled = FALSE; // alternatively: DefWindowProc() return 0; ; // CBeepButton 该类实现一个按钮, 在点击它时, 会有响声 该类的消息映射处理 WM_LBUTTONDOWN 消息 其它的消息由 Windows 缺省窗口函数处理 在消息映射前面, 有一个宏 --DECLARE_WND_SUPERCLASS() 它的作用就是申明 BeepButton 是 Butt on 的一个 superclass 分析一下这个宏 : #define DECLARE_WND_SUPERCLASS(WndClassName, OrigWndClassName) \ static CWndClassInfo& GetWndClassInfo() \ \ static CWndClassInfo wc = \ \ sizeof(wndclassex), 0, StartWindowProc, \ 0, 0, NULL, NULL, NULL, NULL, NULL,

WndClassName, NULL, \ OrigWndClassName, NULL, NULL, TRUE, 0, _T("") \ ; \ return wc; \ 这个宏定义了一个静态函数 GetWndClassInfo() 这个函数返回了一个窗口类注册时用到的数据结构 CW ndclassinfo 该结构的详细定义如下 : struct _ATL_WNDCLASSINFOA WNDCLASSEXA m_wc; LPCSTR m_lpszorigname; WNDPROC pwndproc; LPCSTR m_lpszcursorid; BOOL m_bsystemcursor; ATOM m_atom; CHAR m_szautoname[13]; ATOM Register(WNDPROC* p) return AtlModuleRegisterWndClassInfoA(&_Module, this, p); ; struct _ATL_WNDCLASSINFOW

return AtlModuleRegisterWndClassInfoW(&_Module, this, p); ; typedef _ATL_WNDCLASSINFOA CWndClassInfoA; typedef _ATL_WNDCLASSINFOW CWndClassInfoW; #ifdef UNICODE #define CWndClassInfo CWndClassInfoW #else #define CWndClassInfo CWndClassInfoA #endif 这个结构调用了一个静态函数 AtlModuleRegisterWndClassInfoA(&_Module, this, p); 这个函数的用 处就是注册窗口类 它指定了 WndClassName 是 OrigWdClassName 的 superclass subclass subclass 是普遍采用的一种扩展窗口功能的方法 它的大致原理如下 在一个窗口创建完了之后, 将该窗口的窗口函数替换成新的窗口消息处理函数 这个新的窗口函数可以对 某些需要处理的特定的消息进行处理, 然后再将处理传给原来的窗口函数 注意它与 superclass 的区别

Superclass 是以一个类为原版, 进行克隆 既在注册新的窗口类时, 使用的是基类窗口的窗口函数 而 subclass 是在某一个窗口注册并创建后, 通过修改该窗口的窗口消息函数的地址而实现的 它是针对窗 口实例 看一个从 MSDN 来的例子 : class CNoNumEdit: public CWindowImpl< CNoNumEdit > BEGIN_MSG_MAP( CNoNumEdit ) MESSAGE_HANDLER( WM_CHAR, OnChar ) END_MSG_MAP() LRESULT OnChar( UINT, WPARAM wparam, LPARAM, BOOL& bhandled ) TCHAR ch = wparam; if( _T('0') <= ch && ch <= _T('9') ) MessageBeep( 0 ); else bhandled = FALSE; return 0; ; 这里定义了一个只接收数字的编辑控件 即通过消息映射, 定义了一个特殊的消息处理逻辑 然后, 我们使用 CWindowImplT. SubclassWindow() 来 subclass 一个编辑控件

class CMyDialog: public CDialogImpl<CMyDialog> public: enum IDD = IDD_DIALOG1 ; BEGIN_MSG_MAP( CMyDialog ) MESSAGE_HANDLER( WM_INITDIALOG, OnInitDialog ) END_MSG_MAP() LRESULT OnInitDialog( UINT, WPARAM, LPARAM, BOOL& ) ed.subclasswindow( GetDlgItem( IDC_EDIT1 ) ); return 0; CNoNumEdit ed; ; 上述代码中,ed.SubclassWindow( GetDlgItem( IDC_EDIT1 ) ) 语句是对 IDC_EDIT1 这个编辑控件进 行 subclass 该语句实际上是替换了编辑控件的窗口函数 由于 SubClassWindows() 实现的机制和 ATL 封装窗口函数的机制一样, 我们会在后面介绍 ATL 是怎么实 现它的 深入剖析 WTL WTL 框架窗口分析 (5) ATL 对窗口消息处理函数的封装 在本节开始部分谈到的封装窗口的两个难题, 其中第一个问题是怎样解决将窗口函数的消息转发到 HWN D 相对应的类的实例中的相应函数

下面我们来看一下,ATL 采用的是什么办法来实现的 我们知道每个 Windows 的窗口类都有一个窗口函数 LRESULT WndProc(HWND hwnd, UINT umsg, WPARAM wparam, LPARAM lparam); 在类 CWindowImplBaseT 中, 定义了两个类的静态成员函数 template <class TBase = CWindow, class TWinTraits = CControlWinTraits> class ATL_NO_VTABLE CWindowImplBaseT : public CWindowImplRoot< TBase > public: static LRESULT CALLBACK StartWindowProc(HWND hwnd, UINT umsg, WPARAM wparam, LPARAM lparam); static LRESULT CALLBACK WindowProc(HWND hwnd, UINT umsg, WPARAM wparam, LPARAM lparam); 它们都是窗口函数 之所以定义为静态成员函数, 是因为每个类必须只有一个窗口函数, 而且, 窗口函数 的申明必须是这样的

在前面介绍的消息处理逻辑过程中, 我们知道怎样通过宏生成虚函数 ProcessWindowsMessage(HWND hwnd, UINT umsg, WPARAM wparam, LPARAM lparam, LRESULT& lresult, DWORD dwms gmapid) 现在的任务是怎样在窗口函数中把消息传递给某个实例 ( 窗口 ) 的 ProcessWindowsMessage() 这是一 个难题 窗口函数是类的静态成员函数, 因此, 它不象类的其它成员函数, 参数中没有隐含 this 指针 注意, 之所以存在这个问题是因为 ProcessWindowsMessage() 是一个虚函数 而之所以用虚函数是考虑 到类的派生及多态性 如果不需要实现窗口类的派生及多态性, 是不存在这个问题的 通常想到的解决办法是根据窗口函数的 HWND 参数, 寻找与其对应的类的实例的指针 然后, 通过该指 针, 调用该实例的消息逻辑处理函数 ProcessWindowsMessage() 这样就要求存储一个全局数组, 将 HWND 和该类的实例的指针一一对应地存放在该数组中 ATL 解决这个问题的方法很巧妙 该方法并不存储这些对应关系, 而是使窗口函数接收 C++ 类指针作为参 数来替代 HWND 作为参数 具体步骤如下 : 在注册窗口类时, 指定一个起始窗口函数 创建窗口类时, 将 this 指针暂时保存在某处 Windows 在创建该类的窗口时会调用起始窗口函数 它的作用是创建一系列二进制代码 (thunk) 这 些代码用 this 指针的物理地址来取代窗口函数的 HWND 参数, 然后跳转到实际的窗口函数中 这是通过 改变栈来实现的

然后, 用这些代码作为该窗口的窗口函数 这样, 每次调用窗口函数时都对参数进行转换 在实际的窗口函数中, 只需要将该参数 cast 为窗口类指针类型 详细看看 ATL 的封装代码 1. 注册窗口类时, 指定一个起始窗口函数 在 superclass 中, 我们分析到窗口注册时, 指定的窗口函数是 StartWindowProc() 2. 创建窗口类时, 将 this 指针暂时保存在某处 template <class TBase, class TWinTraits> HWND CWindowImplBaseT< TBase, TWinTraits >::Create(HWND hwndparent, RECT& rcpos, LPCTSTR szwindowname, DWORD dwstyle, DWORD dwexstyle, UINT nid, ATOM atom, LPVOID lpcreateparam) ATLASSERT(m_hWnd == NULL); if(atom == 0) return NULL; _Module.AddCreateWndData(&m_thunk.cd, this); if(nid == 0 && (dwstyle & WS_CHILD)) nid = (UINT)this; HWND hwnd = ::CreateWindowEx(dwExStyle, (LPCTSTR)MAKELONG(atom, 0), szwindowname, dwstyle, rcpos.left, rcpos.top, rcpos.right - rcpos.left,

rcpos.bottom - rcpos.top, hwndparent, (HMENU)nID, _Module.GetModuleInstance(), lpcreateparam); ATLASSERT(m_hWnd == hwnd); return hwnd; 该函数用于创建一个窗口 它做了两件事 第一件就是通过 _Module.AddCreateWndData(&m_thunk.c d, this); 语句把 this 指针保存在 _Module 的某个地方 第二件事就是创建一个 Windows 窗口 3. 一段奇妙的二进制代码 下面我们来看一下一段关键的二进制代码 它的作用是将传递给实际窗口函数的 HWND 参数用类的实例 指针来代替 ATL 定义了一个结构来代表这段代码 : #pragma pack(push,1) struct _WndProcThunk DWORD m_mov; // mov dword ptr [esp+0x4], pthis (esp+0x4 is hwnd) DWORD m_this; // BYTE m_jmp; // jmp WndProc DWORD m_relproc; // relative jmp ;

#pragma pack(pop) #pragma pack(push,1) 的意思是告诉编译器, 该结构在内存中每个字段都紧紧挨着 因为它存放的是机 器指令 这段代码包含两条机器指令 : mov dword ptr [esp+4], pthis jmp WndProc MOV 指令将堆栈中的 HWND 参数 (esp+0x4) 变成类的实例指针 pthis JMP 指令完成一个相对跳转 到实际的窗口函数 WndProc 的任务 注意, 此时堆栈中的 HWND 参数已经变成了 pthis, 也就是说,W inproc 得到的 HWND 参数实际上是 pthis 上面最关键的问题是计算出 jmp WndProc 的相对偏移量 我们看一下 ATL 是怎样初始化这个结构的 class CWndProcThunk public: union _AtlCreateWndData cd;

_WndProcThunk thunk; ; void Init(WNDPROC proc, void* pthis) thunk.m_mov = 0x042444C7; //C7 44 24 0C thunk.m_this = (DWORD)pThis; thunk.m_jmp = 0xe9; thunk.m_relproc = (int)proc - ((int)this+sizeof(_wndprocthunk)); // write block from data cache and // flush from instruction cache FlushInstructionCache(GetCurrentProcess(), &thunk, sizeof(thunk)); ; ATL 包装了一个类并定义了一个 Init() 成员函数来设置初始值的 在语句 thunk.m_relproc = (int)proc - ((int)this+sizeof(_wndprocthunk)); 用于把跳转指令的相对地址设置为 (int)proc - ((int)this+sizeof (_WndProcThunk)) 上图是该窗口类的实例 ( 对象 ) 内存映象图, 图中描述了各个指针及它们的关系 很容易计算出相对地址

是 (int)proc - ((int)this+sizeof(_wndprocthunk)) 4. StartWindowProc() 的作用 template <class TBase, class TWinTraits> LRESULT CALLBACK CWindowImplBaseT< TBase, TWinTraits >::StartWindowProc(HWND hwnd, UINT umsg, WPARAM wparam, LPARAM lparam) CWindowImplBaseT< TBase, TWinTraits >* pthis = (CWindowImplBaseT< TBase, TWinTraits >*)_Module.ExtractCreateWndData(); ATLASSERT(pThis!= NULL); pthis->m_hwnd = hwnd; pthis->m_thunk.init(pthis->getwindowproc(), pthis); WNDPROC pproc = (WNDPROC)&(pThis->m_thunk.thunk); WNDPROC poldproc = (WNDPROC)::SetWindowLong(hWnd, GWL_WNDPROC, (LONG)pProc); #ifdef _DEBUG // check if somebody has subclassed us already since we discard it if(poldproc!= StartWindowProc) ATLTRACE2(atlTraceWindowing, 0, _T("Subclassing through a hook discarded.\n")); #else poldproc; // avoid unused warning

#endif return pproc(hwnd, umsg, wparam, lparam); 该函数做了四件事 : 一是调用 _Module.ExtractCreateWndData() 语句, 从保存 this 指针的地方得到该 this 指针 二是调用 m_thunk.init(pthis->getwindowproc(), pthis) 语句初始化 thunk 代码 三是将 thunk 代码设置为该窗口类的窗口函数 WNDPROC pproc = (WNDPROC)&(pThis->m_thunk.thunk); WNDPROC poldproc = (WNDPROC)::SetWindowLong(hWnd, GWL_WNDPROC, (LONG)pProc); 这样, 以后的消息处理首先调用的是 thunk 代码 它将 HWND 参数改为 pthis 指针, 然后跳转到实际的 窗口函数 WindowProc() 四是在完成上述工作后, 调用上面的窗口函数 由于 StartWindowProc() 在创建窗口时被 Windows 调用 在完成上述任务后它应该继续完成 Windows 要求完成的任务 因此在这里, 就简单地调用实际的窗口函数来处理 5. WindowProc() 窗口函数

下面是该函数的定义 : template <class TBase, class TWinTraits> LRESULT CALLBACK CWindowImplBaseT< TBase, TWinTraits >::WindowProc(HWND hwnd, UINT umsg, WPARAM wparam, LPARAM lparam) CWindowImplBaseT< TBase, TWinTraits >* pthis = (CWindowImplBaseT< TBase, TWinTraits >*)hwnd; // set a ptr to this message and save the old value MSG msg = pthis->m_hwnd, umsg, wparam, lparam, 0, 0, 0 ; const MSG* poldmsg = pthis->m_pcurrentmsg; pthis->m_pcurrentmsg = &msg; // pass to the message map to process LRESULT lres; BOOL bret = pthis->processwindowmessage(pthis->m_hwnd, umsg, wparam, lparam, lres, 0); // restore saved value for the current message ATLASSERT(pThis->m_pCurrentMsg == &msg); pthis->m_pcurrentmsg = poldmsg; // do the default processing if message was not handled if(!bret) if(umsg!= WM_NCDESTROY) lres = pthis->defwindowproc(umsg, wparam,

lparam); else // unsubclass, if needed LONG pfnwndproc = ::GetWindowLong(pThis->m_hWnd, GWL_WNDPROC); lres = pthis->defwindowproc(umsg, wparam, lparam); if(pthis->m_pfnsuperwindowproc!= ::DefWindowProc && ::GetWindowLong(pThis->m_hWnd, GWL_WNDPROC) == pfnwndproc) ::SetWindowLong(pThis->m_hWnd, GWL_WNDPROC, (LONG)pThis->m_pfnSuperWindowProc); // clear out window handle HWND hwnd = pthis->m_hwnd; pthis->m_hwnd = NULL; // clean up after window is destroyed pthis->onfinalmessage(hwnd); return lres; 首先, 该函数把 hwnd 参数 cast 到一个类的实例指针 pthis

然后调用 pthis->processwindowmessage(pthis->m_hwnd, umsg, wparam, lparam, lres, 0); 语 句, 也就是调用消息逻辑处理, 将具体的消息处理事务交给 ProcessWindowMessage() 接下来, 如果 ProcessWindowMessage() 没有对任何消息进行处理, 就调用缺省的消息处理 注意这里处理 WM_NCDESTROY 的方法 这和 subclass 有关, 最后恢复没有 subclass 以前的窗口函数 WTL 对 subclass 的封装 前面讲到过 subclass 的原理, 这里看一下是怎么封装的 template <class TBase, class TWinTraits> BOOL CWindowImplBaseT< TBase, TWinTraits >::SubclassWindow(HWND hwnd) ATLASSERT(m_hWnd == NULL); ATLASSERT(::IsWindow(hWnd)); m_thunk.init(getwindowproc(), this); WNDPROC pproc = (WNDPROC)&(m_thunk.thunk); WNDPROC pfnwndproc = (WNDPROC)::SetWindowLong(hWnd, GWL_WNDPROC, (LONG)pProc); if(pfnwndproc == NULL) return FALSE; m_pfnsuperwindowproc = pfnwndproc; m_hwnd = hwnd; return TRUE;

没什么好说的, 它的工作就是初始化一段 thunk 代码, 然后替换原先的窗口函数 深入剖析 WTL WTL 框架窗口分析 (6) WTL 对框架窗口的封装 ATL 仅仅是封装了窗口函数和提供了消息映射 实际应用中, 需要各种种类的窗口, 比如, 每个界面线程 所对应的框架窗口 WTL 正是在 ATL 基础上, 为我们提供了框架窗口和其他各种窗口 所有的应用程序类型中, 每个界面线程都有一个框架窗口 (Frame) 和一个视 (View) 它们的概念和 MFC 中 的一样 图示是 WTL 的窗口类的继承图 WTL 框架窗口为我们提供了 :

一个应用程序的标题, 窗口框架, 菜单, 工具栏 视的管理, 包括视的大小的改变, 以便与框架窗口同步 提供对菜单, 工具栏等的处理代码 在状态栏显示帮助信息等等 WTL 视通常就是应用程序的客户区 它通常用于呈现内容给客户 WTL 提供的方法是在界面线程的逻辑中创建框架窗口, 而视的创建由框架窗口负责 后面会介绍, 框架窗 口在处理 WM_CREATE 消息时创建视 如果要创建一个框架窗口, 需要 : 从 CFrameWindowImpl 类派生你的框架窗口 加入 DECLARE_FRAME_WND_CLASS, 指定菜单和工具栏的资源 ID 加入消息映射, 同时把它与基类的消息映射联系起来 同时, 加入消息处理函数 下面是使用 ATL/WTL App Wizard 创建一个 SDI 应用程序的主框架窗口的申明 class CMainFrame : public CFrameWindowImpl<CMainFrame>, public CUpdateUI<CMainFrame>, public CMessageFilter, public CIdleHandler

public: DECLARE_FRAME_WND_CLASS(NULL, IDR_MAINFRAME) // 该框架窗口的视的实例 CView m_view; // 该框架窗口的命令工具行 CCommandBarCtrl m_cmdbar; virtual BOOL PreTranslateMessage(MSG* pmsg); virtual BOOL OnIdle(); BEGIN_UPDATE_UI_MAP(CMainFrame) UPDATE_ELEMENT(ID_VIEW_TOOLBAR, UPDUI_MENUPOPUP) UPDATE_ELEMENT(ID_VIEW_STATUS_BAR, UPDUI_MENUPOPUP) END_UPDATE_UI_MAP() BEGIN_MSG_MAP(CMainFrame) MESSAGE_HANDLER(WM_CREATE, OnCreate) COMMAND_ID_HANDLER(ID_APP_EXIT, OnFileExit) COMMAND_ID_HANDLER(ID_FILE_NEW, OnFileNew) COMMAND_ID_HANDLER(ID_VIEW_TOOLBAR, OnViewToolBar) COMMAND_ID_HANDLER(ID_VIEW_STATUS_BAR, OnViewStatusBar) COMMAND_ID_HANDLER(ID_APP_ABOUT, OnAppAbout) CHAIN_MSG_MAP(CUpdateUI<CMainFrame>)

CHAIN_MSG_MAP(CFrameWindowImpl<CMainFrame>) END_MSG_MAP() // Handler prototypes (uncomment arguments if needed): // LRESULT MessageHandler(UINT /*umsg*/, WPARAM /*wparam*/, LPARAM /*lparam*/, BOOL& /*bhandled*/) // LRESULT CommandHandler(WORD /*wnotifycode*/, WORD /*wid*/, HWND /*hwndctl*/, BOOL& /*bhandled*/) // LRESULT NotifyHandler(int /*idctrl*/, LPNMHDR /*pnmh*/, BOOL& /*bhandled*/) LRESULT OnCreate(UINT /*umsg*/, WPARAM /*wparam*/, LPARAM /*lparam*/, BOOL& /*bhandled*/); LRESULT OnFileExit(WORD /*wnotifycode*/, WORD /*wid*/, HWND /*hwndctl*/, BOOL& /*bhandled*/); LRESULT OnFileNew(WORD /*wnotifycode*/, WORD /*wid*/, HWND /*hwndctl*/, BOOL& /*bhandled*/); LRESULT OnViewToolBar(WORD /*wnotifycode*/, WORD /*wid*/, HWND /*hwndctl*/, BOOL& /*bhandled*/); LRESULT OnViewStatusBar(WORD /*wnotifycode*/, WORD /*wid*/, HWND /*hwndctl*/, BOOL& /*bhandled*/); LRESULT OnAppAbout(WORD /*wnotifycode*/, WORD /*wid*/, HWND /*hwndctl*/,

BOOL& /*bhandled*/); ; DECLARE_FRAME_WND_CLASS() 宏是为框架窗口指定一个资源 ID, 可以通过这个 ID 和应用程序的资源 联系起来, 比如框架的图标, 字符串表, 菜单和工具栏等等 WTL 视 通常应用程序的显示区域分成两个部分 一是包含窗口标题, 菜单, 工具栏和状态栏的主框架窗口 另一 部分就是被称为视的部分 这部分是客户区, 用于呈现内容给客户 视可以是包含 HWND 的任何东西 通过在框架窗口处理 WM_CREATE 时, 将该 HWND 句柄赋植给主窗 口的 m_hwndclien 成员来设置主窗口的视 比如, 在用 ATL/WTL App Wizard 创建了一个应用程序, 会创建一个视, 代码如下 : class CTestView : public CWindowImpl<CTestView> public: DECLARE_WND_CLASS(NULL) BOOL PreTranslateMessage(MSG* pmsg); BEGIN_MSG_MAP(CTestView) MESSAGE_HANDLER(WM_PAINT, OnPaint) END_MSG_MAP()

// Handler prototypes (uncomment arguments if needed): // LRESULT MessageHandler(UINT /*umsg*/, WPARAM /*wparam*/, LPARAM /*lparam*/, BOOL& /*bhandled*/) // LRESULT CommandHandler(WORD /*wnotifycode*/, WORD /*wid*/, HWND /*hwndctl*/, BOOL& /*bhandled*/) // LRESULT NotifyHandler(int /*idctrl*/, LPNMHDR /*pnmh*/, BOOL& /*bhandled*/) LRESULT OnPaint(UINT /*umsg*/, WPARAM /*wparam*/, LPARAM /*lparam*/, BOOL& /*bhandled*/); ; 这个视是一个从 CWindowImpl 派生的窗口 在主窗口的创建函数中, 将该视的 HWND 设置给主窗口的 m_hwndclient 成员 LRESULT CMainFrame::OnCreate(UINT /*umsg*/, WPARAM /*wparam*/, LPARAM /*lparam*/, BOOL& /*bhandled*/) m_hwndclient = m_view.create(m_hwnd, rcdefault, NULL, WS_CHILD WS_VISIBLE WS_CLIPSIBLINGS WS_CLIPCHILDREN, WS_EX_CLIENTEDGE);

return 0; 上述代码为主窗口创建了视 到此为止, 我们已经从 Win32 模型开始, 到了解 windows 界面程序封装以及 WTL 消息循环机制, 详细 分析了 WTL 通过我们的分析, 您是否对 WTL 有一个深入的理解, 并能得心应手的开发出高质量的 Win dows 应用程序? 别急, 随后, 我们还将一起探讨开发 WTL 应用程序的技巧 WTL 编程的十个技巧 介绍 这篇文章将介绍十个开发 WTL 应用程序的小技巧 这些技巧内容涉及从怎么控制和放置应用程序主窗口到怎么在控件中显示字符串和整数等系列问题 你可以到 http://www.codeproject.com/wtl/tips/tipspro ject.zip 下载使用这十个技巧的示例程序 十个技巧包括 : 设置主窗口的大小 启动时在屏幕中央显示主窗口 设置主窗口的最小 / 最大尺寸 动态加载主窗口标题 将工具栏设置成平面风格 设置对话框文字和背景的颜色 交换对话框按钮位置 设置平面风格的 ListView 头 在控件中显示整数 在控件中显示资源字符串 主窗口技巧

下面的技巧能够同时使用在 SDI 和 MDI 应用程序中 : 1. 设置窗口生成时的大小在程序.CPP 文件的 Run() 函数中使用下面的技术可以控制窗口生成时的大小尺寸 用你想要的窗口大小设置 rect 的值, 然后将这个值作为第二个函数传递给 CreateEx() 函数, 如下所示 : RECT rc = 0, 0, 380, 265; if(wndmain.createex(null, rc) == NULL) 2. 将主窗口在桌面中央显示要让主窗口在桌面中央显示, 只要在应用程序的.CPP 文件的 Run() 的函数的 ShowWindow() 命令前增加下面的一行代码 : wndmain.centerwindow(); 3. 设置最小 / 最大尺寸如果你想要控制你的主窗口的最大最小尺寸, 你要在在头文件 mainframe.h 的 CMainFrame 消息映射表中增加下面的消息处理过程 MESSAGE_HANDLER(WM_GETMINMAXINFO, OnGetMinMaxInfo) 完全实现功能, 还需要在文件中增加处理函数 : LRESULT OnGetMinMaxInfo(UINT, WPARAM, LPARAM lparam, BOOL&) // lparam 传递 MINMAXINFO 结构的指针 LPMINMAXINFO lpmmi = (LPMINMAXINFO)lParam; // 改变 size 结构中的相应的值为我们想要的窗口大小值 lpmmi->ptmintracksize.x = 200; // 最小宽度 lpmmi->ptmintracksize.y = 150; // 最小高度 lpmmi->ptmaxtracksize.x = 600; // 最大宽度 lpmmi->ptmaxtracksize.y = 450; // 最大高度 return 0; 4. 动态设置标题 是可以通过加载资源中的字符串生成 Cstring 对象, 然后通过这个对象实现动态设置窗口标题 把下面的

代码加入到 OnCreate() 函数中就可以完成实现这个功能 另外需要在项目中 #include atlmisc.h, 这个文件定义了 Cstring 类 你能够用 LoadString() 加载最长 255 个字符的字符串 CString str; str.loadstring(ids_editstring); SetWindowText(str); 5. Flat-style Toolbar 平面风格工具栏使用 WTL AppWizard 生成程序的时候, 如果没有选择 rebar 的话, 生成的工具栏是标准立体按钮 如果你想在没有 rebar 时工具栏有平面风格, 只要在主框架 OnCreate 函数的创建工具栏的代码后边增加以下代码就可以了 : CToolBarCtrl tool = m_hwndtoolbar; tool.modifystyle(0, TBSTYLE_FLAT); Dialog Tips 对话框技巧以下的技巧可以使用在对话框或者基于对话框的应用程序中 下边的图显示了我们的示例函数的 About 对话框, 其中使用了两个技巧 6. Dialog Text and Background Color 对话框文字和背景的颜色这个技巧提供了简单快速改变对话框的文字或背景颜色的方法 这篇文章示例程序的 About 对话框里, 我们使用 SetTextColor 设置文字颜色为白色 北京颜色使用 Stock brush 设置成黑色 第一步是在对话框消息映射表中增加以下的两行代码 : MESSAGE_HANDLER(WM_CTLCOLORDLG, OnCtrlColor) MESSAGE_HANDLER(WM_CTLCOLORSTATIC, OnCtrlColor) 第二步是在 OnCtrlColor 函数中改变文字和背景的颜色 将背景模式设置为透明, 这样可以正确显示静态控件和 group box 控件文字 接着, 将文字设置成我们想要的颜色, 最后设置背景 brush 在项目中增加 atlmisc.h 头文件, 因为 AtlGetStockBrush() 函数在这个头文件中定义 有几种 Stock br usk 可以选择 WHITE_BRUSH, LTGRAY_BRUSH, GRAY_BRUSH, DKGRAY_BRUSH, 和 BLACK_BRUS H, 如果使用其它颜色, 你需要生成新的 brush, LRESULT OnCtrlColor(UINT, WPARAM, LPARAM, BOOL&) // 设置背景模式和文字颜色

SetBkMode((HDC)wParam, TRANSPARENT); // 透明背景 SetTextColor((HDC)wParam, RGB(255, 255, 255)); // 白色文字 return (LRESULT)AtlGetStockBrush(BLACK_BRUSH); 7. 动态交换按钮位置下面的代码来自 About 对话框的 OnInitDialog() 函数中, 用来交换 OK 和 Cancel 按钮的位置 其中关键点是将屏幕位置如何转换成相对于 Client 的位置 CButton bok = GetDlgItem(IDOK)); CButton bcancel = GetDlgItem(IDCANCEL)); // 取得按钮位置 RECT rcok, rccancel; bok.getwindowrect(&rcok); ScreenToClient(&rcOk); bcancel.getwindowrect(&rccancel); ScreenToClient(&rcCancel); // 交换按钮位置 bok.setwindowpos(null, &rccancel, SWP_NOZORDER SWP_NOSIZE); bcancel.setwindowpos(null, &rcok, SWP_NOZORDER SWP_NOSIZE); 控件技巧技巧 8 适用于报表类型的 listview 控件, 技巧 9 和 10 适用于接受 text 的任何控件, 如 edit 控件和 rich edit 控件等 8. 平面风格的 ListView 头将报表型的 listview 的头控件改成平面外观, 只要如下取得头控件对象, 并且修改它的类型 CHeaderCtrl hdr = MyListView.GetHeader(); hdr.modifystyle(hds_buttons, 0); 9. 显示整数在项目中增加 atlmisc.h 文件, 这个文件定义了 Cstring 类 然后使用下面的代码在控件中显示整数值

int nvalue = 9999; CString sinteger; sinteger.format("%i", nvalue); MyControl.SetWindowText(sInteger); 10. 显示资源字符串使用 atlmisc.h 头文件中的辅助函数 AtlLoadString 加载长度可以超过 255 个字符的字符串, 然后将这个字符串显示到控件中 示例程序中使用下面的代码实现在 edit 控将中显示字符串 当在资源字符串表中输入字符串时, 要使用 \r\n 来分行, 仅仅 \n 不能正确分行 TCHAR carray[1000]; AtlLoadString(IDS_EDITSTRING, carray, 1000 + 1); MyControl.SetWindowText(cArray); 附加技巧下面的技巧可以使用在所有的控件中 11. 缺省字体当一个控件被放置在对话框上时, 控件就采用对话框的缺省字体 然而, 当一个控件如视图或者分割面板被使用在窗口中时, 将使用 SYSTEM_FONT 字体, 这种字体不是很漂亮 要改变字体的话, 只要在工程中增加 atlmisc.h 文件, 然后调用 AtlGetStockFont 取得 truetype 字体 DEFAULT_GUI_FONT, 将控件设置成这种字体 : MyControl.SetFont(AtlGetStockFont(DEFAULT_GUI_FONT), TRUE); 使用条款本文章的示例程序是免费的, 你可以在任何地方使用 THIS SOFTWARE IS DISTRIBUTED AS-IS, WITHOUT WARRANTIES OF ANY KIND. WTL 流程分析 一个窗口从创建到销毁, 有这么几个主要过程 在 winmain 中 注册窗口类 创建窗口 进入消息循环

在 wndproc 中 处理消息 现在我们就是要挖掘出 wtl 中在何处处理这些东西, 怎么处理的 首先 : winmain 在哪里? winmain 在和工程名相同的 cpp 文件中 名字叫做 _twinmain int WINAPI _twinmain(hinstance hinstance, HINSTANCE /*hprevinstance*/, LPTSTR l pstrcmdline, int ncmdshow) HRESULT hres = ::CoInitialize(NULL); // If you are running on NT 4.0 or higher you can use the following call instead to ad. // make the EXE free threaded. This means that calls come in on a random RPC thre // HRESULT hres = ::CoInitializeEx(NULL, COINIT_MULTITHREADED); ATLASSERT(SUCCEEDED(hRes)); (MSLU) is used // this resolves ATL window thunking problem when Microsoft Layer for Unicode ::DefWindowProc(NULL, 0, 0, 0L); AtlInitCommonControls(ICC_COOL_CLASSES ICC_BAR_CLASSES); // add flags to s upport other controls hres = _Module.Init(NULL, hinstance); ATLASSERT(SUCCEEDED(hRes)); int nret = Run(lpstrCmdLine, ncmdshow); _Module.Term(); ::CoUninitialize(); return nret;

从这个函数中, 看不出什么, 基本上实质上的内容都被分配在别的函数中处理了 这里所说的别的 函数就是 Run(lpstrCmdLine, ncmdshow); 这个函数是我们自己写的, 就在这个 _twinmain 的上面 Run 的作用 int Run(LPTSTR /*lpstrcmdline*/ = NULL, int ncmdshow = SW_SHOWDEFAULT) CMessageLoop theloop; _Module.AddMessageLoop(&theLoop); CMainFrame wndmain; if(wndmain.createex() == NULL) ATLTRACE(_T("Main window creation failed!\n")); return 0; wndmain.showwindow(ncmdshow); int nret = theloop.run(); _Module.RemoveMessageLoop(); return nret; 所以 从名字 MessageLoop 和 CreateEx 就可以猜测到这个 Run 就是创建窗口并进入消息循环的地方 winmain 进行必要的初始化, 主要的工作在 Run 中进行 Run 创建窗口并进入消息循环 窗口的创建 很容易就可以知道这么一段完成了窗口的创建 CMainFrame wndmain; if(wndmain.createex() == NULL)

ATLTRACE(_T("Main window creation failed!\n")); return 0; CMainFrame 定义在 MainFrm.h 中 class CMainFrame : public CFrameWindowImpl<CMainFrame>, public CUpdateUI< CMainFrame>, public CMessageFilter, public CIdleHandler 可见这里使用了多继承, 这是一个普遍行为 主要继承于 CFrameWindowImpl, 而且这个是模板, 提供的参数就是 CMainFrame 后面可以发现, 这个参数在基类中用于强制类型转换, 算是向下转换 创建调用的是 wndmain.createex(), 这个函数在 CMainFrame 中找不到, 自然在其基类中有 这 个是 CFrameWindowImpl 中的 CreateEx(): HWND CreateEx(HWND hwndparent = NULL, _U_RECT rect = NULL, DWORD dw Style = 0, DWORD dwexstyle = 0, LPVOID lpcreateparam = NULL) TCHAR szwindowname[256]; szwindowname[0] = 0; ::LoadString(_Module.GetResourceInstance(), T::GetWndClassInfo().m_uCommon ResourceID, szwindowname, 256); HMENU hmenu=::loadmenu(_module.getresourceinstance(), MAKEINTRESOUR CE(T::GetWndClassInfo().m_uCommonResourceID)); T* pt = static_cast<t*>(this); HWND hwnd = pt->create(hwndparent, rect, szwindowname, dwstyle, dwe xstyle, hmenu, lpcreateparam); if(hwnd!=null) m_haccel=::loadaccelerators(_module.getresourceinstance(),makeintreso URCE(T::GetWndClassInfo().m_uCommonResourceID)); return hwnd; 等等, 我们在这里发现了一个奇异的行为

T* pt = static_cast<t*>(this); 这是什么, 强制类型转换, 而且是基于模板参数的类型转换 嗯, 这个就是 ATL 开发组偶然发明的仿真动态绑定 利用给基类提供派生类作为模板参数, 在函数调用的时候强制类型转换以在编译期间决定调用是哪个函数 这样作使得我们可以在派生类中改写基类中的函数, 并且免去了虚函数带来的代价 所以说 ram); pt->create(hwndparent, rect, szwindowname, dwstyle, dwexstyle, hmenu, lpcreatepa 调用的是派生类的 Create 函数, 虽然派生类并没有改写这个函数, 但是你可以这么作并获得灵活性 下面继续跟踪这个 Create 的行为, 不用寻找了, 这个函数就在 CreateEx 的上面一点 派生类没有 改写, 调用的就是基类中的版本 HWND Create(HWND hwndparent = NULL, _U_RECT rect = NULL, LPCTSTR szwi ndowname = NULL, DWORD dwstyle = 0, DWORD dwexstyle = 0, HMENU hm enu = NULL, LPVOID lpcreateparam = NULL) ATOM atom = T::GetWndClassInfo().Register(&m_pfnSuperWindowProc); dwstyle = T::GetWndStyle(dwStyle); dwexstyle = T::GetWndExStyle(dwExStyle); if(rect.m_lprect == NULL) rect.m_lprect = &TBase::rcDefault; return CFrameWindowImplBase< TBase, TWinTraits >::Create(hWndParent, rect.m_lprect, szwindowname, dwstyle, dwexstyle, hmenu, atom, lpcreateparam); 红色标记了两个重要的过程, 一个注册窗口类, 一个创建了窗口 先关注窗口类的注册 窗口类与注册 T::GetWndClassInfo().Register(&m_pfnSuperWindowProc); 这条代码完成了窗口类的注册 T 是传递给基类的参数, 也就是派生类 所以 T 就是 CMainFrame T::GetWndClassInfo() 表示, 这里调用的是类的静态函数 那么, 这个函数在哪里定义的呢? 我们要注意到 CMainFrame 定义中的这么 一行 : DECLARE_FRAME_WND_CLASS(NULL, IDR_MAINFRAME)

显然, 这是一个宏 ( 你看看后面没有分号就知道了 ) 所以继续搜索这个宏的定义 #define DECLARE_FRAME_WND_CLASS(WndClassName, ucommonresourceid) \ static CFrameWndClassInfo& GetWndClassInfo() \ \ static CFrameWndClassInfo wc = \ \ sizeof(wndclassex), 0, StartWindowProc, \ Name, NULL, \ 0, 0, NULL, NULL, NULL, (HBRUSH)(COLOR_WINDOW + 1), NULL, WndClass NULL, NULL, IDC_ARROW, TRUE, 0, _T(""), ucommonresourceid \ ; \ return wc; \ ^_^, 我逮着你了 就是这个宏把一个静态函数搞进来了 这个静态函数就是根据参数产生一个类型 CFrameWndClassInfo 的静态变量, 并返回它 这个静态变量包含了 WNDCLASS 信息, 以及和创建窗口 时需要提供的一些信息 是一个所以 Register(&m_pfnSuperWindowProc); 调用的就是 CFrameWndClassInfo 中的 member function 所以, 我们要看看 CFrameWndClas sinfo 的定义了 : class CFrameWndClassInfo public: WNDCLASSEX m_wc; LPCTSTR m_lpszorigname; WNDPROC pwndproc; LPCTSTR m_lpszcursorid; BOOL m_bsystemcursor;

ATOM m_atom; TCHAR m_szautoname[5 + sizeof(void*) * 2]; // sizeof(void*) * 2 is the number of digits %p outputs UINT m_ucommonresourceid; ATOM Register(WNDPROC* pproc) if (m_atom == 0) ::EnterCriticalSection(&_Module.m_csWindowCreate); if(m_atom == 0) HINSTANCE hinst = _Module.GetModuleInstance(); if (m_lpszorigname!= NULL) ATLASSERT(pProc!= NULL); LPCTSTR lpsz = m_wc.lpszclassname; WNDPROC proc = m_wc.lpfnwndproc; WNDCLASSEX wc; wc.cbsize = sizeof(wndclassex); // try process local class first &wc)) if(!::getclassinfoex(_module.getmoduleinstance(), m_lpszorigname, // try global class if(!::getclassinfoex(null, m_lpszorigname, &wc))

::LeaveCriticalSection(&_Module.m_csWindowCreate); return 0; memcpy(&m_wc, &wc, sizeof(wndclassex)); pwndproc = m_wc.lpfnwndproc; m_wc.lpszclassname = lpsz; m_wc.lpfnwndproc = proc; else _lpszcursorid); m_wc.hcursor = ::LoadCursor(m_bSystemCursor? NULL : hinst, m m_wc.hinstance = hinst; sses m_wc.style &= ~CS_GLOBALCLASS; // we don't register global cla if (m_wc.lpszclassname == NULL) wsprintf(m_szautoname, _T("ATL:%p"), &m_wc); m_wc.lpszclassname = m_szautoname; WNDCLASSEX wctemp; memcpy(&wctemp, &m_wc, sizeof(wndclassex)); e, &wctemp); m_atom = (ATOM)::GetClassInfoEx(m_wc.hInstance, m_wc.lpszclassnam if (m_atom == 0)

if(m_ucommonresourceid!= 0) // use it if not zero m_wc.hicon = (HICON)::LoadImage(_Module.GetResourceInsta nce(), MAKEINTRESOURCE(m_uCommonResourceID), IMAGE_ICON, 32, 32, LR_DEFAULTCOLO R); m_wc.hiconsm = (HICON)::LoadImage(_Module.GetResourceIn stance(), MAKEINTRESOURCE(m_uCommonResourceID), IMAGE_ICON, 16, 16, LR_DEFAULTCO LOR); m_atom = ::RegisterClassEx(&m_wc); ::LeaveCriticalSection(&_Module.m_csWindowCreate); if (m_lpszorigname!= NULL) ATLASSERT(pProc!= NULL); ATLASSERT(pWndProc!= NULL); *pproc = pwndproc; return m_atom; ; 不要管乱七八糟的一大堆, 关键部分就是 m_atom = ::RegisterClassEx(&m_wc); 显而易见, 这一句完成了真正的窗口类的注册 并用 m_atom 标记是否应注册过了 关于窗口类的注册, 我们还要留意很关键的一点, 那就是 wndproc 的地址 一路过来, 明显的就是 StartWindowProc 好的, 到此, 窗口类的注册已经完成了 下面 :

窗口的创建 CFrameWindowImplBase< TBase, TWinTraits >::Create(hWndParent, rect.m_lprect, szwindowname, dwstyle, dwexstyle, hmenu, atom, lpcreateparam); 这是这个函数的定义 : HWND Create(HWND hwndparent, _U_RECT rect, LPCTSTR szwindowname, DWO RD dwstyle, DWORD dwexstyle, _U_MENUorID MenuOrID, ATOM atom, LPVOID lpcreatepar am) ATLASSERT(m_hWnd == NULL); if(atom == 0) return NULL; _Module.AddCreateWndData(&m_thunk.cd, this); if(menuorid.m_hmenu == NULL && (dwstyle & WS_CHILD)) MenuOrID.m_hMenu = (HMENU)(UINT_PTR)this; if(rect.m_lprect == NULL) rect.m_lprect = &TBase::rcDefault; HWND hwnd=::createwindowex(dwexstyle, (LPCTSTR)(LONG_PTR)MAKEL ONG(atom, 0), szwindowname, dwstyle, rect.m_lprect->left, rect.m_lprect->top, rect.m_lprect->right - rect.m_lprect->left, rect.m_lprect->bottom-rect.m_lprect->top, hwndparent, MenuOrID.m_hMenu, _Module.GetModuleInstance(), lpcreateparam); ATLASSERT(m_hWnd == hwnd); return hwnd; if(atom == 0) return NULL; 检查窗口类是否已经正确注册了 然后是 CreateWindowEx 实质的创建工作 里面的参数窗口类 是 (LPCTSTR)(LONG_PTR)MAKELONG(atom, 0) 所以这里, 窗口类名没有被使用 注册窗口类时返 回的 atom 被用作相应的功能了 这个和 mfc 的做法很不一样 到现在为止, 窗口类已经注册并创建了一个窗口 我们回到 Run 中 :

int Run(LPTSTR /*lpstrcmdline*/ = NULL, int ncmdshow = SW_SHOWDEFAULT) CMessageLoop theloop; _Module.AddMessageLoop(&theLoop); CMainFrame wndmain; if(wndmain.createex() == NULL) ATLTRACE(_T("Main window creation failed!\n")); return 0; wndmain.showwindow(ncmdshow); int nret = theloop.run(); _Module.RemoveMessageLoop(); return nret; 消息循环 AddMessageLoop 和 RemoveMessageLoop 把 theloop 挂到模块 ( 程序 ) 对象上或者取下 现在问题的核心是消息循环的处理 theloop.run(); 我们来看 CMessageLoop 的 Run 的定义 : int Run() BOOL bdoidle = TRUE; int nidlecount = 0; BOOL bret; for(;;)

le) while(!::peekmessage(&m_msg, NULL, 0, 0, PM_NOREMOVE) && bdoid if(!onidle(nidlecount++)) bdoidle = FALSE; bret = ::GetMessage(&m_msg, NULL, 0, 0); if(bret == -1) ATLTRACE2(atlTraceUI, 0, _T("::GetMessage returned -1 (error)\n")); continue; // error, don't process else if(!bret) ATLTRACE2(atlTraceUI, 0, _T("CMessageLoop::Run - exiting\n")); break; // WM_QUIT, exit message loop if(!pretranslatemessage(&m_msg)) ::TranslateMessage(&m_msg); ::DispatchMessage(&m_msg); if(isidlemessage(&m_msg)) bdoidle = TRUE; nidlecount = 0;

return (int)m_msg.wparam; 很简单, 就是用 PeekMessage 决定当前是否有消息需要处理, 然后在把需要处理的消息进行常规 的翻译和分发 其中有进行空闲时间处理的机会 然后消息循环已经开始了, 现在要关注是哪里处理消息? 消息的处理 前面都是小菜, 很清晰 到这里才遇到了大问题 我们回忆到 WNDCLASS 中的 wndproc 记录的是 StartWndProc 不论如何, 消息一开始进入的就是这个函数 抓住它, 就有希望 : template <class TBase, class TWinTraits> LRESULT CALLBACK CWindowImplBaseT< TBase, TWinTraits >:: StartWindowProc(HWND hwnd, UINT umsg, WPARAM wparam, LPARAM lparam) CWindowImplBaseT< TBase, TWinTraits >* pthis = (CWindowImplBaseT< TBase, T WinTraits >*)_Module.ExtractCreateWndData(); ATLASSERT(pThis!= NULL); pthis->m_hwnd = hwnd; pthis->m_thunk.init(pthis->getwindowproc(), pthis); WNDPROC pproc = (WNDPROC)&(pThis->m_thunk.thunk); (LONG)pProc); WNDPROC poldproc = (WNDPROC)::SetWindowLong(hWnd, GWL_WNDPROC, #ifdef _DEBUG // check if somebody has subclassed us already since we discard it if(poldproc!= StartWindowProc) n")); ATLTRACE2(atlTraceWindowing, 0, _T("Subclassing through a hook discarded.\ #else

poldproc; // avoid unused warning #endif return pproc(hwnd, umsg, wparam, lparam); 首先我们来看 SetWindowLong, 知道这个是干什么的吗?SetWindowLong 改变窗口的一些基本 属性 GWL_WNDPROC 表示要改变的是 wndproc 的地址 ^_^, 知道了为什么要先看这个了吧 这一 步就是要把 wndproc 改为 正确 的地方 也就是 pproc WNDPROC pproc = (WNDPROC)&(pThis->m_thunk.thunk); 这个就是执行了一次 StartWindowProc 之后,wndproc 将改为的函数 pthis 是从 _Module 中取 出来的 取出来的信息是窗口创建的时候记录的, 为了清晰, 先不管它, 反正它是一个 CWindowImplBa set 类型的指针 现在要明白 m_thunk 是啥子东西 m_thunk 定义在 CWindowImplBaseT 的基类中是类型为 CWn dprocthunk 的变量 我们来看 CWndProcThunk: class CWndProcThunk public: union _AtlCreateWndData cd; _WndProcThunk thunk; ; void Init(WNDPROC proc, void* pthis) #if defined (_M_IX86) thunk.m_mov = 0x042444C7; file://c7 44 24 0C thunk.m_this = (DWORD)pThis; thunk.m_jmp = 0xe9; thunk.m_relproc = (int)proc - ((int)this+sizeof(_wndprocthunk));

#elif defined (_M_ALPHA) thunk.ldah_at = (0x279f0000 HIWORD(proc)) + (LOWORD(proc)>>15); thunk.ldah_a0 = (0x261f0000 HIWORD(pThis)) + (LOWORD(pThis)>>15); thunk.lda_at = 0x239c0000 LOWORD(proc); thunk.lda_a0 = 0x22100000 LOWORD(pThis); thunk.jmp = 0x6bfc0000; #endif // write block from data cache and file:// flush from instruction cache FlushInstructionCache(GetCurrentProcess(), &thunk, sizeof(thunk)); ; 恐怖吧, 居然出现了机器码 基本的思想是通过 Init 准备好一段机器码, 然后把机器码的地址作为 函数地址 这段机器码就干两件事情, 一个是把 wndproc 的 hwnd 参数替换为 pthis, 另外一个是跳转 到相应窗口的真实的 wndproc 中 pthis->getwindowproc() 这条代码返回的就是实际处理消息的地方 现在来看这个函数 : virtual WNDPROC GetWindowProc() return WindowProc; template <class TBase, class TWinTraits> LRESULT CALLBACK CWindowImplBaseT< TBase, TWinTraits >:: WindowProc(HWND hwnd, UINT umsg, WPARAM wparam, LPARAM lparam) WinTraits >*)hwnd; CWindowImplBaseT< TBase, TWinTraits >* pthis = (CWindowImplBaseT< TBase, T

// set a ptr to this message and save the old value MSG msg = pthis->m_hwnd, umsg, wparam, lparam, 0, 0, 0 ; const MSG* poldmsg = pthis->m_pcurrentmsg; pthis->m_pcurrentmsg = &msg; // pass to the message map to process LRESULT lres; Param, lres, 0); BOOL bret = pthis->processwindowmessage(pthis->m_hwnd, umsg, wparam, l // restore saved value for the current message ATLASSERT(pThis->m_pCurrentMsg == &msg); pthis->m_pcurrentmsg = poldmsg; // do the default processing if message was not handled if(!bret) if(umsg!= WM_NCDESTROY) lres = pthis->defwindowproc(umsg, wparam, lparam); else // unsubclass, if needed C); LONG pfnwndproc = ::GetWindowLong(pThis->m_hWnd, GWL_WNDPRO lres = pthis->defwindowproc(umsg, wparam, lparam); if(pthis->m_pfnsuperwindowproc!= ::DefWindowProc && ::GetWindowLo ng(pthis->m_hwnd, GWL_WNDPROC) == pfnwndproc) pfnsuperwindowproc); ::SetWindowLong(pThis->m_hWnd, GWL_WNDPROC, (LONG)pThis->m_ // clear out window handle

HWND hwnd = pthis->m_hwnd; pthis->m_hwnd = NULL; // clean up after window is destroyed pthis->onfinalmessage(hwnd); return lres; 可见几经周折, 最终还是落到了派生类的 ProcessWindowMessage 中 这里同样使用了模拟虚函 数 还有一个问题是我在 CMainFrame 中并没有写 ProcessWindowMessage 啊? 但是你写了 BEGIN_MSG_MAP(CMainFrame) MESSAGE_HANDLER(WM_CREATE, OnCreate) COMMAND_ID_HANDLER(ID_APP_EXIT, OnFileExit) COMMAND_ID_HANDLER(ID_FILE_NEW, OnFileNew) COMMAND_ID_HANDLER(ID_FILE_OPEN, OnFileOpen) COMMAND_ID_HANDLER(ID_VIEW_TOOLBAR, OnViewToolBar) COMMAND_ID_HANDLER(ID_VIEW_STATUS_BAR, OnViewStatusBar) COMMAND_ID_HANDLER(ID_APP_ABOUT, OnAppAbout) CHAIN_MSG_MAP(CUpdateUI<CMainFrame>) CHAIN_MSG_MAP(CFrameWindowImpl<CMainFrame>) END_MSG_MAP() 这些东西其实就是 ProcessWindowMessage, 他们是宏 #define BEGIN_MSG_MAP(theClass) \ public: \ BOOL ProcessWindowMessage(HWND hwnd, UINT umsg, WPARAM wparam, LPA RAM lparam, LRESULT& lresult, DWORD dwmsgmapid = 0) \

\ BOOL bhandled = TRUE; \ hwnd; \ umsg; \ wparam; \ lparam; \ lresult; \ bhandled; \ switch(dwmsgmapid) \ \ case 0: #define MESSAGE_RANGE_HANDLER(msgFirst, msglast, func) \ if(umsg >= msgfirst && umsg <= msglast) \ \ bhandled = TRUE; \ lresult = func(umsg, wparam, lparam, bhandled); \ if(bhandled) \ return TRUE; \ #define COMMAND_ID_HANDLER(id, func) \ if(umsg == WM_COMMAND && id == LOWORD(wParam)) \ \ bhandled = TRUE; \ d); \ lresult = func(hiword(wparam), LOWORD(wParam), (HWND)lParam, bhandle if(bhandled) \

return TRUE; \ #define CHAIN_MSG_MAP(theChainClass) \ \ t)) \ if(thechainclass::processwindowmessage(hwnd, umsg, wparam, lparam, lresul return TRUE; \ #define END_MSG_MAP() \ break; \ default: \ wmsgmapid); \ ATLTRACE2(atlTraceWindowing, 0, _T("Invalid message map ID (%i)\n"), d ATLASSERT(FALSE); \ break; \ \ return FALSE; \ 至此, 一切都明白了 其实 WTL 只是 ATL 的窗口部分的扩展, 这里所分析的东西绝大部分是 ATL 中的 而且这部分内容在 ATL INTERNAL 中也有比较详细描述了 WTL 体系结构 绪论 WTL 最终来了, 而且提供了我所希望的功能. 我在 WTL Bytesize( 译文 ) 的文章列出 WTL 主要特征. 在本文中, 我将描述一下 WTL 的体系结构, 同时我会给出一些简单的例子来演示如何使用它的那些特征. 希望 能够对您有所帮助. WTL 应用程序的类型 WTL 有好几种应用程序类型, 供您在 AppWizard 选取.

下表对这些应用程序进行了描述. 这种弹性构成了 WTL 体系结构的一部分. 应用程序类型 描述 SDI Application 单文本界面 只有一个窗口 Multiple Threads SDI 单个进程拥有一个或多个窗口 MDI Application 多文本界面 在框架内, 您可以有零个或多个子窗口 Dialog Based 基于对话框模版 你可能还是首次听说多线程 SDI 应用程序, 但是不用担心, 它的概念很容易理解. 一个多线程 SDI 程序启动后它会有一个窗口, 窗口显示了一个文档. 当你想要程序要再创建一个文档时, 问题就出现了 --SDI 程序只能显示一个文档. 为了解决这个问题, 多线程 SDI 创建了另一个 SDI 窗口. 看起来是一个新的实例在运行, 实际上它不过是原来的进程创建了一个新的窗口, 并把它依附到进程的一个新线程. IE 的新建窗口就是这样做的. 除了多线程 SDI, 所有这些应用程序都可以作为 COM 服务器, 并且应用程序向导 (AppWizard) 为此提供了一个选项. 另外应用程序向导还可以让你指定该程序是否主持 ActiveX 控件. 令人费解的是, 不同的程序类型, 选取 "Host ActiveX Controls" 的地方不同. 除对话框应用程序外的其他类型在第一页上选取, 而对话框类型却放到第二页. 第二页的其他选项, 对对话框程序以外的类型都是可用的. 它们让你指定程序是否需要工具条 (tool bar), 状态条 (status bar) 和视窗口 (View Window). 如果选取了 "Toolbar" 选项, 你可以通过 "Rebar" 选择是否将工具条放入 IE Rebar 控件中. 如果你选取了 Rebar, 你就可以通过框架窗口 (frame window) 的成员 m_hwndtoolbar( 后边会有详细的描述 ) 来访问它. 你可以按照你的意愿, 在里边加入其他的工具条. 选取了 "Rebar" 后, 你可以决定是否选取 "Com mand Bar". Command bar 很像 CE 的 command bar 控件. 只是 WTL 是用一个类来实现, 而在 CE, co mmand bar 是一个系统窗口类 (system window class). Command bar 非常有用, 它能够把窗口也加入到工具条中去. 如果你选取了这个选项, 工具条和菜单都将被当做 toolbar 来实现. 这使菜单项也可以有关联的图标, 并且当你移动鼠标到一个菜单项上时, 该菜单项会被置成高亮. 从 Office 97 以来, Office 软件的菜单都具有上述特征. 第二页还有指定程序是否使用视的选项 ( 多半你想要使用 ), 同时你可以决定这些视如何实现. 下 表列出了所有可选的视.

视 描述 Generic Window 一个简单的窗口. 此类窗口允许程序员编写 WM_PAINT 消息的处理函数. 适用于需要 直接进行 paint 的文档. Form 这类视具有一个对话框模版. 适用于带 ActiveX 控件的窗口. 应用程序来操作这些控件. List Box 这个视是个 list box. 它最简单的形式意味着可以通过调用 AddString() 方法来添加字 符串. Edit 这个视是个 edit control. 本质上, 它提供了一个像 Notepad 一样的程序. List View 这个视是个 list view 通用控件. 用这个控件来显示相关的项 ( 比如, 控制面板是一个 Explorer 主持的 List View, 所有的项都是控制面板 applet). Tree View 这个视是个 tree view 通用控件. 这个适用于具有层次关系的数据, 比如, 可以用它来显 示数据库的 schema. 顶层分支为表和存储过程, 次级的分支为表中的字段. Rich Edit 这个视是个 rich edit 控件, 像 WordPad. HTML Page 这个视主持了一个 IE Web Browser 控件. 它把主持的一个 web page 当成一个视. 本文的例子需要一个对话框模版, 同时还需要菜单, 因此 Form view 是个理想的选择. 程序线程 跟 ATL 一样,WTL 程序也需要一个 _Module 全局变量来保存全局数据, 方便应用级代码访问. 在 W TL 中, 这个变量是 CAppModule 或 CServerAppModule 的实例, 后者在程序同时作为一个 COM 服务器 时用到. 每个应用程序具有一个或者多个 UI 线程.WTL 使用两种方式来管理这些线程. 如果应用程序只有一个 UI 线程 ( 除了多线程 SDI 以外, 其他程序类型默认只有一个 UI 线程 ), 线程调用全 局函数 run(): int Run(LPTSTR /*lpstrcmdline*/ = NULL, int ncmdshow = SW_SHOWDEFAULT) CMessageLoop theloop; _Module.AddMessageLoop(&theLoop); CMainFrame wndmain; if (wndmain.createex() == NULL)

ATLTRACE(_T("Main window creation failed!\n")); return 0; wndmain.showwindow(ncmdshow); int nret = theloop.run(); _Module.RemoveMessageLoop(); return nret; 线程的消息循环包含在 CMessageLoop 内部. 函数创建了一个 CMessageLoop 实例, 把它放入全局的消息循环映射 (message loop map) 数组. 以线程 ID 为索引, 线程中运行的其他的代码可以访问到这个实例. 消息循环对象包含了 message filter 和 idle handler. 运行在这个 UI 线程的 UI 元件 (UI elem ent) 可以有它自己的 idle handler, 在线程的消息队列为空时运行 译注 : 通过 CMessageLoop::AddIdle Handler() 把这个 UI 元件加入到 CMessageLoop 的 idle handler 数组中. CMessageLoop::Run() 包含了 UI 线程的主消息映射 (main message map). 下边是它的伪代码 : MSG m_msg; int CMessageLoop::Run() for (;;) while (!::PeekMessage(&m_msg, NULL, 0, 0, PM_NOREMOVE)) DoIdleHandlers(); bret = ::GetMessage(&m_msg, NULL, 0, 0); if(bret == -1) continue; else if(!bret) break; if (!DoMessageFilters(&m_msg)) ::TranslateMessage(&m_msg); ::DispatchMessage(&m_msg); return (int)m_msg.wparam; 可以看到, 这个函数推动着消息队列. 没有消息时, 运行注册到线程的 idle hander. 如果在队列中检测 到消息, 把它取出来, 传给每个 message filter. 如果消息没有被这些函数处理, 它将按照通常的方式, 发送到 目标窗口. 如果程序有超过一个的 UI 线程, 可以用 WTL 的线程管理器, 多线程 SDI 就是这样做的. 主线程作为一个 管理者线程, 它会为每个新窗口创建一个新的新线程. 主要流程如下 :

int nret = m_dwcount; DWORD dwret; while(m_dwcount > 0) dwret = ::MsgWaitForMultipleObjects(m_dwCount, m_arrthreadhandles, FALSE, INFINITE, QS_ALLINPUT); 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(_T(""), SW_SHOWNORMAL); 那些线程句柄放在一个数组中. 线程通过 AddThread() 加入到数组 ( 同时启动线程 ), RemoveThread() 从数组移走. wait 语句在两种情况下会被打断 : 线程死亡 ( 将线程从数组中移出 ) 或线程收到了 WM_USER 消息 ( 一个线程在一个新线程里新建了一个窗口 ). 线程管理者为程序中的一个类, 因此可以在循环中加入自己的 message handler, 比如, 当程序有不止一种窗口类型时. 创建一个新的窗口非常简单, 只需在任意一个窗口中调用 : ::PostThreadMessage(_Module.m_dwMainThreadID, WM_USER, 0, 0L); 这个循环会一直运行下去, 直到所有的 UI 线程都关闭了. UI 线程具有一个 thread procedure, 它跟单 UI 线 程的 Run() 方法一样. 不过, 由于线程管理者使用了 MsgWaitForMultipleObjects(), 这意味者最多只能有 MAXIMUM_WAIT_OBJECTS-1 个 UI 线程, 这也意味着最多只能创建 63 个窗口. 框架 WTL 实际上是两类窗口 : 框架窗口和视图窗口. 正如名字所暗示的那样, 框架窗口为窗口提供标题栏 (c aption bar) 和边框, 你的代码用它来处理工具条 (tool bar) 和菜单项命令. 你看到的程序窗口实际上是视图窗口, 视图覆盖了框架窗口的客户区. 客户区是指框架窗口没有被诸如状态条, 工具条之类的修饰部件所遮挡的部分. 线程会创建主框架窗口的一个实例, 创建视图的工作由主框架窗口的 WM_CREATE 消息处理函数完成. 对于 SDI 程序来说, 这个过程很简单. 把视图类的一个实例作为主框架类的一个成员, 调用视图类的 Create() 方法即可.MDI 程序稍微有些不同, MDI 主框架窗口通过 CMDIFrameWindowImpl<>::CreateMDICli ent() 建立一个名为 MDICLIENT 的窗口. 这个客户窗口将 CMDIChildWindowImpl<> 窗口当做它的子窗口, 子窗口有一个视图. 这也反映了这么一个事实,MDI 程序可以具有零个或者多个子窗口, 每个都有边框和标题栏. 框架窗口的 OnCreate() 很有意思, 让我看看 :

LRESULT OnCreate(UINT, WPARAM, LPARAM, BOOL&) // create command bar window HWND hwndcmdbar = m_cmdbar.create(m_hwnd, rcdefault, NULL, ATL_SIMPLE_CMDBAR_PANE_STYLE); // attach menu m_cmdbar.attachmenu(getmenu()); // load command bar images m_cmdbar.loadimages(idr_mainframe); // remove old menu SetMenu(NULL); HWND hwndtoolbar = CreateSimpleToolBarCtrl(m_hWnd, IDR_MAINFRAME, FALSE, ATL_SIMPLE_TOOLBAR_PANE_STYLE); CreateSimpleReBar(ATL_SIMPLE_REBAR_NOBORDER_STYLE); AddSimpleReBarBand(hWndCmdBar); AddSimpleReBarBand(hWndToolBar, NULL, TRUE); CreateSimpleStatusBar(); m_hwndclient = m_view.create(m_hwnd, rcdefault, NULL, WS_CHILD WS_VISIBLE WS_CLIPSIBLINGS WS_CLIPCHILDREN, WS_EX_CLIENTEDGE); UIAddToolBar(hWndToolBar); UISetCheck(ID_VIEW_TOOLBAR, 1); UISetCheck(ID_VIEW_STATUS_BAR, 1); CMessageLoop* ploop = _Module.GetMessageLoop(); ploop->addmessagefilter(this); ploop->addidlehandler(this); return 0; 这是从一个 SDI 程序拿来的一段代码, 该程序有一个基于 command bar 的工具条和一个状态条. 函数的第一行创建了一个 command bar 实例, 然后对它进行初始化, 在其中加入框架窗口的菜单和工具条位图. 这段代码先将菜单取出, 把所有的下拉菜单转换为工具条按钮, 并将菜单保存在一个变量中, 以备后用. 给人的感觉是菜单是由工具条实现的 - 那我们就把它叫做工具条菜单 (menu toolbar) 吧. 然后 Command Bar 将程序工具条的图标装入 image list 并将它们的 ID 保存在数组中. 当点击工具条菜单的按钮时,comman dbar 会找到对应的子菜单, 创建一个弹出菜单. Command bar 将子菜单项的 ID 和它保存的 ID 进行比较, 这些 ID 跟 image list 中的工具条按钮图标是相关联的. 如果比较成功, 则将关联的图标加到菜单项上去. 这意味着相同 ID 的菜单项和工具条按钮具有相同的图标. 接下来, 创建工具条并把它关联到 commandbar, 然后创建状态条和视图. 可以看到视图的 HWND 存放在 框架窗口的 m_hwndclient 变量中. 这个窗口句柄在框架窗口的 WM_SIZE handler 中会用到. 当框架窗 口改变大小时, 它告知视图改变自身, 于此同时也要考虑状态条和 command bar. 在下来的三行 ( 从调用 UIAddToolBar() 开始 ) 用来显示在运行时会改变状态的 UI 项 (UI item). 文章后面还 会重提这个话题. 最后, 访问消息循环 (message loop), 你应该还记得该消息循环存放在一全局数组中. G

etmessageloop() 取得当前线程的消息循环, 加入框架窗口的 message filter 和 idle handler, 分别默 认是 PreTranslateMessage() 和 OnIdle(). 框架窗口继承于以下类 : class CMainFrame : public CFrameWindowImpl<CMainFrame>, public CUpdateUI<CMainFrame>, public CMessageFilter, public CIdleHandler 后两个抽象类宣称了框架窗口类实现了 PreTranslateMessage() 和 OnIdle(). 从 CUpdateUI<> 继承表 示框架类支持 UI update map. 视图 视图窗口看起来显得很简单 : class CMyView : public CWindowImpl<CMyView> public: DECLARE_WND_CLASS(NULL) BOOL PreTranslateMessage(MSG* pmsg) pmsg; return FALSE; BEGIN_MSG_MAP(CMyView) MESSAGE_HANDLER(WM_PAINT, OnPaint) END_MSG_MAP() LRESULT OnPaint(UINT, WPARAM, LPARAM, BOOL&) CPaintDC dc(m_hwnd); //TODO: Add your drawing code here return 0; ; 上面是一个 SDI 程序的视图类. 多线程 SDI 和 MDI 的视图类在本质上也跟这个一样, 但他们没有 PreTrans latemessage() 方法. SDI 程序就是使用这个函数, 赶在框架类处理消息之前把消息抓住. PreTranslateM essage() 在 SDI 的框架类中的实现是, 直接将消息转发给视图类. 这里显示的视图实际上没有做什么工作. 你应该自己在 OnPaint() 函数中加入画出文档内容的代码. 如果需 要支持输入, 如鼠标的点击和键盘的按键, 你应该加入相应消息处理函数到类和映射中. 可以看到这个窗口

是从 CWindowImpl<> 继承下来的, 如果你想让它基于一个 Win32 控件的话, 就应该从定义在 AtlCtrls.h 文件中某个 WTL 类继承. 如果想在基于 CWindowImpl<> 的类里加上滚动条, 那么你应该把基类换成 CScrollWindowImpl<>, 同 时把消息链给它 : class CMyView : public CScrollWindowImpl<CMyView> public: typedef CScrollWindowImpl<CMyView> parent; BEGIN_MSG_MAP(CMyView) CHAIN_MSG_MAP(parent) END_MSG_MAP() void DoPaint(CDCHandle dc) ; 基类保证窗口具备滚动条, 并提供滚动条消息的默认处理. 视图类不再有 WM_PAINT 的处理函数, 因为它已被 CScrollWindowImpl<> 处理. 根据滚动条的位置,CScrollWindowImpl<> 画出视图相对应的部分. 取而代之的是, 在你的类里实现 DoPaint(), 在这里你需要画出整个视图. 如果你想指定滚动的范围, 大小或起点, 你需要加上处理 WM_CREATE 消息的函数, 把这些初始化代码放到里边. 正如我先前所提到的, 框架窗口会改变视图窗口的大小, 以使它客户区未被状态条和工具条覆盖的部分为视图所填充. 在大多数情况下, 这样就够了. 但是当你想要一个具有 Windows Explorer 样子的程序时, 该怎么办呢? Windows Explorer 的窗口包含了一个 tree view 和一个 list view, 还有两者之间的分割条. WTL 的解决方案很简单 : 使用 splitter 窗口! 为此你需要改变一下框架窗口, 让它创建 splitter 窗口的一个实例作为它的视图. 例如, 在你的框架类里有 如下的数据成员 : CSplitterWindow m_view; CTreeViewCtrl m_tree; CListViewCtrl m_list; 你可以在 OnCreate() 创建一个 splitter 窗口 : // get the frame client rect, so that we set the splitter initial size // and we can get the splitter bar in the centre RECT rect; GetClientRect(&rect); m_hwndclient = m_view.create(m_hwnd, rect, NULL, WS_CHILD WS_VISIBLE); m_tree.create(m_view, rcdefault, NULL,

WS_CHILD WS_VISIBLE TVS_HASBUTTONS TVS_HASLINES TVS_LINESATROOT, WS_EX_CLIENTEDGE); m_list.create(m_view, rcdefault, NULL, WS_CHILD WS_VISIBLE LVS_REPORT, WS_EX_CLIENTEDGE); m_view.setsplitterpanes(m_tree, m_list); m_view.setsplitterpos(); Splitter 窗口如同一个视图, 将框架窗口作为它的父窗口. 在这段代码里, 我将框架窗口客户区的实际大小传给了 splitter 窗口. 我也可以在这里使用 rcdefault, 因为一旦框架窗口创建完成, 框架窗口就会转发 WM_ SIZE 消息给 splitter. 这样 splitter 可以马上改变自身的大小来填充框架. 然而, 当我准备使用不带参数的 S etsplitterpos(), 把分割条设置于窗口中线时, 出现了问题.Splitter 窗口使用它的大小来决定中线的位置, 由于 rcdefault 告诉窗口它的大小是 0( 因此中线的位置也是 0), 从而意味着分割条将出现在 z 最左边, 将左窗口隐藏了起来. 创建了 splitter 窗口后, 你需要创建那些你想要分割的窗口了. 它们将作为 splitter 窗口的子窗口被创建. 最后 你将这些子窗口通过 SetSplitterPanes() 加到 splitter 窗口中去, 并确定分割条的位置所在. UI Update 菜单项可以被设置为有效或无效, 可以带 check 记号或着像 radio 按钮一样, 在一组菜单项中同时有且只有一个能被 check. 此外, 菜单项还可以带图标和文字. 所有的这些状态都可以在运行时根据程序中的某个值进行改变. 工具条在某种程度上可以看做是菜单的易见形态, 因为它们的按钮可以个别地, 或者作为一组的一部分被置成有效或无效, 推入推出. UI update 机制允许你指定哪些 UI 元件 (UI element) 的状态可以在运行时改变. WTL 使用如下的 UI update 映射来实现这一功能 : BEGIN_UPDATE_UI_MAP(CMainFrame) UPDATE_ELEMENT(ID_FILE_SAVERESULTS, UPDUI_MENUPOPUP UPDUI_TOOLBAR) UPDATE_ELEMENT(ID_VIEW_TOOLBAR, UPDUI_MENUPOPUP) UPDATE_ELEMENT(ID_VIEW_STATUS_BAR, UPDUI_MENUPOPUP) END_UPDATE_UI_MAP() 这个例子指出三个菜单项在运行时有一个状态需要显示, 其中的一个, ID_FILE_SAVERESULTS, 还有一个工 具条按钮跟它相关联. WTL 通过建立一个数组来保存这些信息. 为此你需要完成两方面的工作 : 首先是 UI 元件的状态. 如果是菜单项, 你可以使用 UIEnable() 使能该菜单项, UISetCheck() 设置 c heck 记号, UISetText() 改变菜单的文字. 如果是工具条按钮, 那么你使用 UIEnable() 使能该按钮, UISetC heck() 或者 UISetRadio() 决定按钮是推入还是推出. 下边的代码根据是否有文本被选中, 来使能 Cut 菜单项和工具条按钮 : BOOL bselected = GetSelected(); UIEnable(ID_EDIT_CUT, bselected); 你可以把这样的代码放入相应处理函数中 ( 如一个菜单项的状态依赖于另一个菜单项的动作, 将它放入后者 的处理函数中 ), 或者放入 OnIdle() 方法, 通过检查某个类变量来决定元件的状态.

其次是确定各个 UI 元件是否都被更新了, 为此你需要调用 CUpdateUI<> 的某个方法将 UI 元件加入到列表 中. 主菜单已被自动加入, 但是其他的任何菜单和所有的工具条必须分别通过调用 UIAddMenuBar() 和 UI AddToolBar() 手动加入. 其他还有一堆事情要注意. 首先, 设置了工具条的状态后, 使用 UIUpdateToolBar() 以使工具条状态更新. 对于菜单, 你不需如此, 因为子菜单是动态生成的.UIUpdateMenuBar() 这个方法也存在, 但是它的作用是把菜单恢复到初始状态, 如果你改变过某些项的文字, 调用 UIUpdateMenuBar() 的结果可能不是你所期望的 ( 因为菜单项的文字会变成老的 ). 尽管还有一个方法 UISetRadio(), 但是还没有一个把几个菜单项或者工具条按钮当做 radio 按钮组 ( 也就是 说, 有一个而且只有一个被选中 ) 的机制. 如果你希望得到这样效果, 你必须自己编码, 不过它并不难. 对话框 ATL 的对话框支持一向很好, 对此 WTL 新增了通用对话框的封装. 本质上是为对话框加入了输入验证和回 调函数. 比如, 你想在用户改变年 Open 对话框中的文件夹时有所动作, 那么你应该从 CFileDialogImpl< > 继承一个类, 实现 OnFolderChange(): class CMyFileDialog : public CFileDialogImpl<CMyFileDialog> public: CMyFileDialog(BOOL b) : CFileDialogImpl<CMyFileDialog>(b) void OnFolderChange(LPOFNOTIFY lpon) char strfolder[max_path]; if (GetFolderPath(strFolder, sizeof(strfolder)) > 0) MessageBox(strFolder); ; 当文件夹的路径改变时,CFileDialogImpl<> 调用 OnFolderChange(). 该函数使用基类的 GetFol derpath(), 来取得新路径. 控件 WTL 为所有的 Win32 和通用控件提供了封装类, 包括 Windows 2000 新加入的. 虽然只是简单的包装, 但是它们使这些控件更加容易访问. 譬如, 你能记清楚从 List View 读出当前选定项的文字的消息和需要传的参数吗?( 实际上, 你需要发送两个消息, 一个是得到选定项的索引, 另一个是读出它的文字.) WTL 的作者为你完成了这些烦人的工作, 提供了一个简单的封装函数供你使用. 使用这些控件类有两种方法. 如果你的对话框里有一个控件, 你可以将控件的 HWND 依附到一个封装对

象, 使用封装类的方法来访问控件. 这种方法简化了你读写控件数据和处理 notification 消息的代码. 另外的用法是把这些类加到你的视图类的继承层次中去 : class CMyView : public CWindowImpl<CMyView, CListBox> 这表示 CWindowImpl<> 是从 CListBox 继承而来, 因此创建的窗口将是一个 list box ( 因为窗口类的名字是通过调用 CListBox::GetWndClassName() 得到的 ). 另外, ATL 的窗口机制会子类化这个窗口, 将发给它的消息路由到你的消息映射中去. 它保留了老的窗口函数, 这样, 你没有处理的消息将由老的窗口函数来处理. 当你的视图类从控件类继承时,WTL 就会使用这一技术. 在 notification 消息和子类化这个主题上, 有一点很值得指出, 那就是当某事件发生时, 绝大多数窗口控件都会发送 notification 消息给它们的父窗口. 让你窗口来处理这些 notification 消息要比子类化一个已存在控件窗口 ( 或子类化一个已存在的类, 然后建立一个实例 ), 从而在控件之前取得消息好得多. 譬如, 你想处理按钮的 click 事件, 你所需要做的只是处理 BN_CLICKED notification. 它将由按钮发送给你的窗口类. 另外的一种方法是从 CContainedWindow<> 子类化 BUTTON 窗口来处理 click 消息. 我之所以说这个是因为一个知名的 ATL 鼓吹者给我一份代码里就是这么做的. 他的代码取得一个简单的按 钮 click 事件所花的时间是别人的 3 到 4 倍, 因为他子类化了按钮控件, 而不是简单的处理 BN_CLICKED n otification. WTL 还提供了一些新的控件, 在 win32 中没有对等者. 你已经看到过一个 -- command bar, 实际上还有 其他一些非常有用类 : 类 描述 CBitmapButton 这是一个用位图替代标题的按钮. 你可以提供一个 image list, 里边包含按钮在正常状态, 失效, 推入和鼠标落在按钮 上的图表. CHyperLink 让你建立一个 static 控件, 它代表一个 hyperlink, 这样当 用户点击它时, 默认的 web 浏览器打开该链接. CWaitCursor 这不过是在它的构造函数中把鼠标图标改成等待状态, 而 在析构函数中还原. CCheckListViewCtrl 在每一项边上都有一个 check box 的 list box. CMultiPaneStatusBarCtrl 具有多个 pane 的状态条

WTL 源码剖析 --- ATLAPP.H ATLAPP.H 包含了消息循环类 接口类 和产生应用程序所必需的一些基础类定义 类定义如下 : CmessageFilter 类 --- 用于消息过滤的 CidleHandler 类 --- 用于空闲消息处理的 CmessageLoop 类 --- 用于消息循环的 CappModule 类 --- 应用程序基础类 CserverAppModule 类 --- 用于 Com 服务构架的应用程序类另外还有 3 个全局函数 : AtlGetDefaultGuiFont() 获得默认的显示字体 AtlCreateBoldFont() 产生一个粗体字体 AtlInitCommonControls() 初始化一些控件所需共同的 DLL WTL 程序的结构一个窗口程序的创建到销毁过程主要经过如下几个阶段 1. 注册窗口类 2. 创建窗口 3. 进入消息循环如果用 C 写过 Win32 窗口程序的人一定会记得如下的结构 : // 窗口过程处理函数 LRESULT CALLBACK WndProc(HWND hwnd,uint Message,WPARAM wparam,lparam l Param); int WINAPI WinMain(HINSTANCE hinstance,hinstance hprevinstance,lpstr szc mdline,int icmdshow) HWND hwnd = NULL; MSG msg; WNDCLASS wndclass; wndclass.style = CS_HREDRAW CS_VREDRAW; wndclass.lpfnwndproc = WndProc; // 注册窗口 if(!registerclass(&wndclass)) MessageBox(NULL,TEXT("Porgram requires Windows NT!"),szAppName,MB_ICO NERROR); return 0; // 创建窗口 hwnd = CreateWindow(szAppName,TEXT("My Application"), WS_OVERLAPPEDWINDOW WS_VSCROLL WS_HSCROLL, CW_USEDEFAULT,CW_USEDEFAULT,

CW_USEDEFAULT,CW_USEDEFAULT, NULL,NULL,hInstance,NULL); ShowWindow(hwnd,iCmdShow); UpdateWindow(hwnd); // 进入消息循环 while(getmessage(&msg,null,0,0)) TranslateMessage(&msg); DispatchMessage(&msg); return msg.wparam; 那么你可能会问 WTL 的 WinMain 函数再哪里? 如果你通过 WTL/ATL 导向生成一个应用程序, 那么你会在跟工程名字同名的.cpp 文件中发现如下的代码 : int WINAPI _twinmain(hinstance hinstance, HINSTANCE /*hprevinstance*/, LPTSTR lpstrc mdline, int ncmdshow) HRESULT hres = ::CoInitialize(NULL); // If you are running on NT 4.0 or higher you can use the following call instea d to // make the EXE free threaded. This means that calls come in on a random RPC thread. // HRESULT hres = ::CoInitializeEx(NULL, COINIT_MULTITHREADED); ATLASSERT(SUCCEEDED(hRes)); (MSLU) is used // this resolves ATL window thunking problem when Microsoft Layer for Unicode ::DefWindowProc(NULL, 0, 0, 0L); AtlInitCommonControls(ICC_COOL_CLASSES ICC_BAR_CLASSES); // add flags to s upport other controls D(hRes)); hres = _Module.Init(NULL, hinstance); // 等下分析它的实现 ATLASSERT(SUCCEEDE int nret = Run(lpstrCmdLine, ncmdshow);// 程序的关键分 _Module.Term(); ::CoUninitialize(); return nret;

从这个 _twinmain 函数的定义, 你可以发现程序的关键部分是我紫色标记出来的 Run() 函数 这个函数是一个自定义的函数, 不过如果通过 ATL/WTL 导向程序, 那么会自动生成这样一个 Run() 函数的, 下面我们先分析一下这个自动生成的 Run 函数 int Run(LPTSTR /*lpstrcmdline*/ = NULL, int ncmdshow = SW_SHOWDEFAULT) CMessageLoop theloop; // 定义消息循环 _Module.AddMessageLoop(&theLoop); // 将消息添加到消息循环 CMainFrame wndmain; // 应用程序框架类 // 生成框架 if(wndmain.createex() == NULL) ATLTRACE(_T("Main window creation failed!\n")); return 0; // 显示框架 wndmain.showwindow(ncmdshow); // 运行消息循环 int nret = theloop.run(); // 清除消息 _Module.RemoveMessageLoop(); return nret; 通过这个 Run 函数我们可以看到在函数中完成了如下几个过程 : 1. 生成一个消息循环对象 (theloop) 2. 在全局的 _Module 中加入这个消息循环 3. 生成一个应用程序框架对象 4. 显示应用程序框架 5. 开始消息循环 6. 结束消息循环 7. 返回 WinMain 函数, 结束程序实现分析在这篇文章我不想过多的分析应用程序框架和窗口的细节, 这些内容将放在以后的几篇文章中详细分析, 本文主要对 ATLAPP.H 头文件中实现的一些过程进行详细分析 首先从全局变量 _Module 开始 _Module 维持着生成应用程序的主线程, 控制着程序的消息循环队列, 是一个 CAppModule 的对象 该 CAppModule 从 ATL::CcomModule 继承 在 WTL::CappModule 中定义了 8 个公有成员函数, 分别为 : AddMessageLoop() 添加一个消息循环, 进入消息循环队列里

RemoveMessageLoop() 移除消息循环队列 GetMessageLoop() 获得消息循环 InitSettingChangeNotify() 初始化环境 AddSettingChangeNotify() 添加一个窗口句柄 RemoveSettingChangeNotify() 清理环境除了 8 个公有成员函数外, 该类还定义了 3 个公有成员变量 m_dwmainthreadid 负责保存该应用程序的主线程 ID m_pmsgloopmap 负责存储消息循环 m_psettingchangenotify 负责存放窗口句柄下面分别来分析几个主要成员函数的实现 : BOOL AddMessageLoop(CMessageLoop* pmsgloop) CStaticDataInitCriticalSectionLock lock; // 锁住关键片断, 由于进程同步的关系!!! if(failed(lock.lock())) ATLTRACE2(atlTraceUI, 0, _T("ERROR : Unable to lock critical section in CApp Module::AddMessageLoop.\n")); ATLASSERT(FALSE); return FALSE; n map yet ATLASSERT(pMsgLoop!= NULL); ATLASSERT(m_pMsgLoopMap->Lookup(::GetCurrentThreadId()) == NULL); // not i BOOL bret = m_pmsgloopmap->add(::getcurrentthreadid(), pmsgloop); lock.unlock(); return bret; 关键部分我用红色的字体标记出来了, 意思是什么? 通过当前线程的 Id 来标示一个消息循环, 存储在 m_pmsgloopmap 中 BOOL RemoveMessageLoop() CStaticDataInitCriticalSectionLock lock; if(failed(lock.lock())) ATLTRACE2(atlTraceUI, 0, _T("ERROR : Unable to lock critical section in C AppModule::RemoveMessageLoop.\n")); ATLASSERT(FALSE); return FALSE;

BOOL bret = m_pmsgloopmap->remove(::getcurrentthreadid()); lock.unlock(); return bret; 关键部分同样通过红色字体标记出来, 嗯, 没错正如 AddMessageLoop 函数一样, 该函数也是通过线程 Id 来寻找消息循环移除对象的 CMessageLoop* GetMessageLoop(DWORD dwthreadid = ::GetCurrentThreadId()) const CStaticDataInitCriticalSectionLock lock; if(failed(lock.lock())) ATLTRACE2(atlTraceUI, 0, _T("ERROR : Unable to lock critical section in C AppModule::GetMessageLoop.\n")); ATLASSERT(FALSE); return NULL; CMessageLoop* ploop = m_pmsgloopmap->lookup(dwthreadid); lock.unlock(); return ploop; 该函数通过线程 Id 在 m_pmsgloopmap 消息队列中寻找对应的消息循环, 找到后返回 BOOL InitSettingChangeNotify(DLGPROC pfndlgproc = _SettingChangeDlgProc) CStaticDataInitCriticalSectionLock lock; if(failed(lock.lock())) ATLTRACE2(atlTraceUI, 0, _T("ERROR : Unable to lock critical section in C AppModule::InitSettingChangeNotify.\n")); ATLASSERT(FALSE); return FALSE; if(m_psettingchangenotify == NULL) typedef ATL::CSimpleArray<HWND> _notifyclass;

ATLTRY(m_pSettingChangeNotify = new _notifyclass); ATLASSERT(m_pSettingChangeNotify!= NULL); BOOL bret = (m_psettingchangenotify!= NULL); if(bret && m_psettingchangenotify->getsize() == 0) // init everything _ATL_EMPTY_DLGTEMPLATE templ; // 增加一个无模式对话框 HWND hntfwnd = ::CreateDialogIndirect(GetModuleInstance(), &templ, N ULL, pfndlgproc); ATLASSERT(::IsWindow(hNtfWnd)); if(::iswindow(hntfwnd)) // need conditional code because types don't match in winuser.h #ifdef _WIN64 ::SetWindowLongPtr(hNtfWnd, GWLP_USERDATA, (LONG_PTR)this); #else ::SetWindowLongPtr(hNtfWnd, GWLP_USERDATA, PtrToLong(this)); #endif // 加入该窗口句柄 bret = m_psettingchangenotify->add(hntfwnd); else bret = FALSE; lock.unlock(); return bret; 该函数用来初始化一个存放窗口句柄的对象 BOOL InitSettingChangeNotify(DLGPROC pfndlgproc = _SettingChangeDlgProc) CStaticDataInitCriticalSectionLock lock; if(failed(lock.lock())) ATLTRACE2(atlTraceUI, 0, _T("ERROR : Unable to lock critical section in C AppModule::InitSettingChangeNotify.\n")); ATLASSERT(FALSE);

return FALSE; if(m_psettingchangenotify == NULL) typedef ATL::CSimpleArray<HWND> _notifyclass; ATLTRY(m_pSettingChangeNotify = new _notifyclass); ATLASSERT(m_pSettingChangeNotify!= NULL); BOOL bret = (m_psettingchangenotify!= NULL); if(bret && m_psettingchangenotify->getsize() == 0) // init everything //?? 空的 ATL Dialog Template 吗? _ATL_EMPTY_DLGTEMPLATE templ; // 增加一个无模式对话框 HWND hntfwnd = ::CreateDialogIndirect(GetModuleInstance(), &templ, N ULL, pfndlgproc); ATLASSERT(::IsWindow(hNtfWnd)); if(::iswindow(hntfwnd)) // need conditional code because types don't match in winuser.h #ifdef _WIN64 ::SetWindowLongPtr(hNtfWnd, GWLP_USERDATA, (LONG_PTR)this); #else ::SetWindowLongPtr(hNtfWnd, GWLP_USERDATA, PtrToLong(this)); #endif bret = m_psettingchangenotify->add(hntfwnd); else bret = FALSE; lock.unlock(); return bret; // 清理消息 void TermSettingChangeNotify()

CStaticDataInitCriticalSectionLock lock; if(failed(lock.lock())) ATLTRACE2(atlTraceUI, 0, _T("ERROR : Unable to lock critical section in C AppModule::TermSettingChangeNotify.\n")); ATLASSERT(FALSE); return; 0) if(m_psettingchangenotify!= NULL && m_psettingchangenotify->getsize() > // 销毁窗口 ::DestroyWindow((*m_pSettingChangeNotify)[0]); delete m_psettingchangenotify; m_psettingchangenotify = NULL; lock.unlock(); BOOL AddSettingChangeNotify(HWND hwnd) CStaticDataInitCriticalSectionLock lock; if(failed(lock.lock())) ATLTRACE2(atlTraceUI, 0, _T("ERROR : Unable to lock critical section in C AppModule::AddSettingChangeNotify.\n")); ATLASSERT(FALSE); return FALSE; ATLASSERT(::IsWindow(hWnd)); BOOL bret = FALSE; if(initsettingchangenotify()!= FALSE) bret = m_psettingchangenotify->add(hwnd); lock.unlock(); return bret; BOOL RemoveSettingChangeNotify(HWND hwnd) CStaticDataInitCriticalSectionLock lock; if(failed(lock.lock()))

ATLTRACE2(atlTraceUI, 0, _T("ERROR : Unable to lock critical section in C AppModule::RemoveSettingChangeNotify.\n")); ATLASSERT(FALSE); return FALSE; BOOL bret = FALSE; if(m_psettingchangenotify!= NULL) bret = m_psettingchangenotify->remove(hwnd); lock.unlock(); return bret; 现在回到刚才提到的 Run() 函数, 里面最开始就定义了一个 CmessageLoop 循环对象, 然后通过 _Module 对象的 AddMessageLoop 成员函数加入到循环队列里面, 直到 _Module 调用了 RemoveMe ssageloop 移除循环队列, 程序才结束循环, 返回到 WinMain 函数 在这里还有一个比较重要的类, 那就是 CMessageLoop, 是他维持了系统的消息, 维持了程序的生命周期 那么下面我们来看看这个类的定义和具体的实现方法 CmessageLoop 包含了如下一些成员函数和成员变量成员变量 // 处理消息 ATL::CSimpleArray<CMessageFilter*> m_amsgfilter; // 处理空闲句柄 ATL::CSimpleArray<CIdleHandler*> m_aidlehandler; //Win32API 消息结构 MSG m_msg; 成员函数 ( 用红色标记的函数是虚函数 ) AddMessageFilter 加入一条消息过滤 RemoveMessageFilter 移除一条消息过滤 AddIdleHandler 加入一个空闲句柄 RemoveIdleHandler 移出一个空闲句柄 AddUpdateUI 为了兼容老的 ATL 而设计的 RemoveUpdateUI 为了兼容老的 ATL 而设计的 IsIdleMessage 过滤一些比如 WM_MOUSEMOVE 之类的消息 Run 消息循环 关键部分!!! PreTranslateMessage 消息过滤 OnIdle 空闲处理 再这里我不准备对每个函数都进行详细的分析, 主要分析核心的函数 Run,CmessageLoop 由它来 维持着系统的消息循环

函数如下 : int Run() // 空闲? BOOL bdoidle = TRUE; // 空闲计数器 int nidlecount = 0; // 返回标志 BOOL bret; // 开始消息循环了哦!!! for(;;) // 当 bdoidle 为 TRUE, 并且不能从消息队列里面取出消息了, 那么开始空闲操作了! //PM_NOREMOVE: 再 PeekMessage 函数处理后不将消息从队列里移除 while(bdoidle &&!::PeekMessage(&m_msg, NULL, 0, 0, PM_NOREMOVE)) if(!onidle(nidlecount++)) bdoidle = FALSE; // 从当前线程获取一个消息 // 返回 -1 表示出现一个错误 // 返回 0 表示提交了一个 WM_QUIT, 程序将要退出 // 成功获得一个消息, 返回不等于 0 的值 bret = ::GetMessage(&m_msg, NULL, 0, 0); if(bret == -1) ATLTRACE2(atlTraceUI, 0, _T("::GetMessage returned -1 (error)\n")); continue; // error, don't process else if(!bret) ATLTRACE2(atlTraceUI, 0, _T("CMessageLoop::Run - exiting\n")); break; // WM_QUIT, exit message loop // 如果熟悉使用 c 语言来写 Win32 的程序员会发现, 原来 WinMain 中的哪个处理消息循环的语句放到这里来了!!! if(!pretranslatemessage(&m_msg)) //translates virtual-key messages into character messages.

::TranslateMessage(&m_msg); //dispatches a message to a window procedure ::DispatchMessage(&m_msg); // 判断是否为空闲消息? // 排除 WM_MOUSEMOVE WM_NCMOUSEMOVE WM_SYSTIMER 消息 if(isidlemessage(&m_msg)) bdoidle = TRUE; nidlecount = 0; return (int)m_msg.wparam; 以上就是对 ATLAPP.H 中的几个比较重要的类的分析, 还有其他几个类的分析我将放在以后的文章中 ( 待续 )