DotNet剖析系列(一)
——窗口从生到死
如果你的第一个Windows程序是从C#开始的,并且你是使用VS.net的模板来创建程序,可能你会习惯于看到以下的代码,也许并不会想到它背后有些什么运作。
[STAThread]
static void Main()
{
Application.Run(new Form1());
}
但是我感到很疑惑,没有了消息循环,不需写WndProc,难道Dotnet下不再需要这些,还是MS把它们包装起来了。《Windows程序设计》开篇就是告诉你写一个Hello,World,我想下面这段代码大家都不会陌生,列在这儿只是为了让大家回顾一下。
HELLOWIN.C
#include <windows.h>
LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM) ;
int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance,
PSTR szCmdLine, int iCmdShow)
{
static TCHAR szAppName[] = TEXT ("HelloWin") ;
HWND hwnd ;
MSG msg ;
WNDCLASwndclass ;
wndclass.style = CS_HREDRAW | CS_VREDRAW ;
wndclass.lpfnWndProc = WndProc ;
wndclass.cbClsExtra = 0 ;
wndclass.cbWndExtra = 0 ;
wndclass.hInstance = hInstance ;
wndclass.hIcon = LoadIcon (NULL, IDI_APPLICATION) ;
wndclass.hCursor = LoadCursor (NULL, IDC_ARROW) ;
wndclass.hbrBackground= (HBRUSH) GetStockObject (WHITE_BRUSH) ;
wndclass.lpszMenuNam = NULL ;
wndclass.lpszClassName= szAppName ;
if (!RegisterClass (&wndclass))
{
MessageBox ( NULL, TEXT ("This program requires Windows NT!"),
szAppName, MB_ICONERROR) ;
return 0 ;
}
hwnd = CreateWindow( szAppName, // window class name
TEXT ("The Hello Program"), // window caption
WS_OVERLAPPEDWINDOW, // window style
CW_USEDEFAULT,// initial x position
CW_USEDEFAULT,// initial y position
CW_USEDEFAULT,// initial x size
CW_USEDEFAULT,// initial y size
NULL, // parent window handle
NULL, // window menu handle
hInstance, // program instance handle
NULL) ; // creation parameters
ShowWindow (hwnd, iCmdShow) ;
UpdateWindow (hwnd) ;
while (GetMessage (&msg, NULL, 0, 0))
{
TranslateMessage (&msg) ;
DispatchMessage (&msg) ;
}
return msg.wParam ;
}
LRESULT CALLBACK WndProc (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
{
HDC hdc ;
PAINTSTRUCT ps ;
RECT rect ;
switch (message)
{
case WM_CREATE:
PlaySound (TEXT ("hellowin.wav"), NULL, SND_FILENAME | SND_ASYNC) ;
return 0 ;
case WM_PAINT:
hdc = BeginPaint (hwnd, &ps) ;
GetClientRect (hwnd, &rect) ;
DrawText (hdc, TEXT ("Hello, Windows 98!"), -1, &rect,
DT_SINGLELINE | DT_CENTER | DT_VCENTER) ;
EndPaint (hwnd, &ps) ;
return 0 ;
case WM_DESTROY:
PostQuitMessage (0) ;
return 0 ;
}
return DefWindowProc (hwnd, message, wParam, lParam) ;
}
上面的代码很清楚地显示传统Windows程序用消息泵来传递消息的机制。
我们还是拿出工具Reflector,仔细来看看到底在Application.Run中发生了什么?
public static void Run(Form mainForm){ Application.ThreadContext.FromCurrent().RunMessageLoop(-1, new ApplicationContext(mainForm));} |
可以看到实际上调用了Application内部定义的类ThreadContext的RunMessageLoop方法,而ApplicationContext是有关应用程序线程的上下文信息,而RunMessageLoop调用了下面的内部方法:
private void RunMessageLoopInner(int reason, ApplicationContext context)
这个函数很长,我们只列出其中有关的部分(代码有删节)
if (reason == -1)
{
this.applicationContext = context;
this.applicationContext.ThreadExit += new EventHandler(this.OnAppThreadExit);
if (this.applicationContext.MainForm != null)
{
this.applicationContext.MainForm.Visible = true;
}
}
if (context != null)
{
this.currentForm = context.MainForm;
}
try
{
flag2 = this.LocalModalMessageLoop(this.currentForm); }
}
finally
{
this.Dispose();
}
可以看到它完成了两个部分,一是将窗体设为可见,另一个是呼叫LocalModalMessageLoop,嗯,好象有那么点意思了。是不是对应了经典程序中的ShowWindow和消息泵呀,那我们再看看LocalModalMessageLoop是不是起到了消息泵的作用呢,还是来看源码。
bool flag2 = true;
while (flag2)
{
UnsafeNativeMethods.GetMessageW(out msg1, NativeMethods.NullHandleRef, 0, 0);//还有一些判断代码
{
UnsafeNativeMethods.TranslateMessage(out msg1);
UnsafeNativeMethods.DispatchMessageW(ref msg1);
}
}
同样上面的代码是一个小部分,但是关键部分,清楚地看到跟原来的处理方式是一样的。需要说明的是实际上CLR考虑了在非Unicode的Windows系统上调用不同的函数如DispatchMessageA之类的。另外UnsafeNativeMethods这个类中的方法的模块实际上就是对WinAPI的简单引用,不再详细说明。
现在消息泵有了,那么WndProc在哪里呢,消息是处理成事件的呢,还是有这个东东,在Form类里不过它是一个保护方法
protected override void WndProc(ref Message m)
代码也很清楚,跟老版的差不多,来看一小段
protected override void WndProc(ref Message m)
{
switch (m.Msg)
{
case 5:
this.WmSize(ref m);
return;
case 6:
this.WmActivate(ref m);
return;
case 1:
this.WmCreate(ref m);
return;
case 0x10:
case 0x11:
case 0x16:
this.WmClose(ref m);
return;
case 0x18:
this.WmShowWindow(ref m);
return;
}
base.WndProc(ref m);
}
是不是还是老样子呀。呀,有一点不一样,最后是调用Control类的WndProc,实际上Control的WndProc在最后调用了Control的DefWndProc,它还恰恰是个虚函数。可以看到每个消息最后转成了WmXXXX之类的函数调用,我们来看WmClose有些什么,下面代码仍是选择了部分关键代码。
this.OnClosing(args1);
if (m.Msg == 0x11)
{
m.Result = args1.Cancel ? IntPtr.Zero : ((IntPtr) 1);
}
if ( !args1.Cancel)
{
this.OnClosed(EventArgs.Empty);
base.Dispose();
}
嗯,现在是不是知道DotNet中的事件是如何产生的了吧。最后看到base.Dispose,知道我们的窗口要结束了,然后VS模板中生成的Dispose就会调用。
CLR通过一系列的包装将消息泵和消息进行包装,形成了我们现在熟悉的事件,并且对多种平台进行了综合考虑,不需要程序员再去分别考虑Unicode的问题,真是很方便了,不过它的运行机理没有改变。