第三章 窗口和消息
/*------------------------------------------------------------ HELLOWINDOW.CPP -- Displays "Hello,Windows!" in client area (c) Seamanj.2013/6/27 ------------------------------------------------------------*/ #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("HelloWindow"); HWND hwnd; MSG msg; WNDCLASS wndclass; 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.lpszMenuName = NULL;//知识点1 wndclass.lpszClassName = szAppName; /*知识点1:窗口类中的窗口菜单 --------------------------------------------- 来自<<Windows环境下32位汇编语言程序设计>> P101 --------------------------------------------- wndclass.lpszMenuName指定了窗口上显示的默认菜单,它指向一个字符串,描述资源文件中菜单的名称,如果资源文件中菜单是用数值定义的, 那么这里使用菜单资源的数值(注意不是句柄).窗口中的菜单也可以在建立窗口函数CreateWindow的参数中指定.如果在两个地方都没有指定,那么建立的窗口上就 没有菜单. */ 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 //知识点2 NULL, //window menu handle //知识点3 hInstance, //program instance handle NULL); //creation parameters /*知识点2:创建窗口时的父窗口句柄参数 --------------------------------------------- 来自<<Windows环境下32位汇编语言程序设计>> P103 --------------------------------------------- 这个参数是窗口所属父窗口的句柄,对于普通窗口,这里的"父子"关系只是从属关系,主要用来在父窗口销毁时一同将其"子"窗口销毁,并不 把窗口位置限制在父窗口的客户区范围内,但如果要建立的是真正的子窗口(dwstyle中指定了WS_CHILD的时候),这时窗口位置会被限制在父窗口 的客户区范围内,同时窗口的坐标(x,y)也是以父窗口的左上角为基准的. */ /*知识点3:创建窗口时的菜单句柄参数 --------------------------------------------- 来自<<Windows环境下32位汇编语言程序设计>> P103 --------------------------------------------- 这个参数是窗口上要出现的菜单的句柄.在注册窗口类的时候也定义了一个菜单,那是窗口的默认菜单,意思是如果这里没有定义菜单而注册 窗口类时定义了菜单,则使用窗口类中定义的菜单;如果这里指定了菜单句柄,则不管窗口类中有没有定义都将使用这里定义的菜单;两个地方都 没有定义菜单句柄,则窗口上没有菜单.另外,当建立的窗口是子窗口时(dwStyle中指定了WS_CHILD),这个参数是另一个含义,这时hMenu参数指定 的是子窗口的ID号(这样可以节省一个参数的位置,因为子窗口不会有菜单). */ 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://知识点4 hdc = BeginPaint(hwnd, &ps);//ps.hdc与返回的hdc是一样的,知识点6 GetClientRect(hwnd, &rect); DrawText(hdc, TEXT("Hello,Windows!"), -1, &rect, DT_SINGLELINE | DT_CENTER | DT_VCENTER);//知识点8 EndPaint(hwnd, &ps); return 0; case WM_DESTROY: PostQuitMessage(0); return 0; } return DefWindowProc(hwnd, message, wParam, lParam); } /*知识点4:WM_PAINT消息 --------------------------------------------- 来自<<Windows环境下32位汇编语言程序设计>> P194 --------------------------------------------- 当客户区被覆盖并重新显示的时候,Windows并不是在所有的情况下都发送WM_PAINT消息,下面是几种不同的情况: 当鼠标光标移过窗口客户区,以及图标拖过客户区这两种情况,Windows总是自己保存被覆盖的的区域并恢复它,并不 需要发送WM_PAINT消息通过用户程序. 当窗口窗户区被自己的下拉式菜单覆盖,或者被自己弹出的对话框覆盖后,Windows会尝试保存被覆盖的区域并在以后 恢复它,如果因为某种原因无法保存并恢复的话,Windows会发送一个WM_PAINT消息通知程序. 别的情况造成窗口的一部分从不可见变到可见,如程序从最小化的状态恢复,其他的窗口覆盖客户区后移开,用户改变 了窗口的大小和用户按动滚动条等,在这些情况下,Windows会向窗口发送WM_PAINT消息. 一些函数会引发WM_PAINT消息,如UpdateWindow, InvalidateRect, 以及InvalidateRgn等. 窗口过程收到WM_PAINT消息后,并不代表整个客户区都需要被刷新,有可能客户区被覆盖的区域只有一小块,这个区域就 叫做"无效区域",程序只需要更新这个区域. 与WM_TIMER消息类似,WM_PAINT消息也是一个低级别的消息(知识点5),虽然它不会像WM_TIMER消息一样被丢弃,但Windows总是在消息 循环空的时候才把WM_PAINT放入其中,实际上,Windows为每个窗口维护一个"绘图信息结构",无效区域的坐标就在其中,每当消息 循环空的时候,如果Windows发现存在一个无效区域,就会放入一个WM_PAINT消息. 无效区域的坐标并不附带在WM_PAINT消息的参数中,在程序中有其他方法可以获取,WM_PAINT消息只是通知程序有个区域需要更新而已, 所以Windows也不会同时将两条WM_PAINT消息放入消息循环,当Windows要放入一条WM_PAINT消息的时候,如果发现已经存在一个无效区域了, 那么它只需要把新旧两个无效区域合并计算出一个新的无效区域就可以了,消息循环中还是只需要一条WM_PAINT消息. 由于存在"无效区域"这样一个机制,所以程序在WM_PAINT消息中对客户区刷新完毕后工作并汉有结束,如果不使无效区域变得有效,Windows 会在下一轮消息循环中继续放入一个WM_PAINT消息.也正是因为Windows仅仅根据是否存在"无效区域"来决定是否发送WM_PAINT消息,而不是根据 程序是否执行了刷新过程,所以程序也可以不去刷新客户区,而是简单用一个ValidateRect函数直接让客户区变得有效,以此来"欺骗"Windows已经 无效区域了,当Windows检查"绘图信息结构"的时候发现没有了无效区域,也就不会继续发送WM_PAINT消息了. WM_PAINT消息的处理流程一般是: .if eax == WM_PAINT ;eax为uMsg invoke BeginPaint,hWnd,addr stPS ;这里BeginPaint填充stPS结构 ;刷新客户区的代码 invoke EndPaint,hwnd,addr stPS ;这里EndPaint使用stPS结构来刷新 xor eax,eax ret 读者可以发现中间并没有调用ValidateRect来使无效区域变得有效,这是因为BeginPaint函数和EndPaint函数隐含这个功能,如果不是以 BeginPaint/EndPaint当做消息处理代码的头尾的话,那么在WM_PAINT消息返回的时候就必须调用ValidateRect函数. BeginPaint函数的第二个参数是一个绘图信息结构的缓冲区地址,Windows会在这时返回绘图信息结构,结构中包含了无效区域的位置和 大小,绘图信息结构的定义如下: PAINTSTRUCT STRUCT hdc DWORD ? fErase DWROD ? rcPaint RECT <> fRestore DWORD ? fIncUpdate DWORD ? rgbReserved BYTE 32 dup(?) PAINTSTRUCT ENDS 其中hdc字段是窗口的设备环境句柄,rcPaint字段是一个RECT结构,它指定了无效区域矩形的对角顶点,fErase字段如果为非零值,表示Windows 在发送WM_PAINT消息前已经用背景色擦除了无效区域,后面3个字段是Windows内部使用的,应用程序不必去理会它们. */ /*知识点5:WM_TIMER消息 --------------------------------------------- 来自<<Windows环境下32位汇编语言程序设计>> P183 --------------------------------------------- 定时器消息是一个低级别的消息,这表现在两个方面: 首先,Windows只有在消息队列中没有其他消息的情况下才会发送WM_TIMER消息,如果窗口过程忙于处理某个消息没有返回,使消息 队列中有消息积累起来,那么WM_TIMER消息就会被丢弃,在消息队列再度空闲的时候,被丢弃的WM_TIMER消息不会被补发(用一句经典的 话来描述就是:"过去的就让它过去吧") 其次,消息队列中不会有多条WM_TIMER消息,如果消息队列中已经有一条WM_TIMER消息,还没有来得及处理,又到了定时的时刻,那么 两条WM_TIMER消息会被合并成一条 */ /*知识点6:设备环境 --------------------------------------------- 来自<<Windows环境下32位汇编语言程序设计>> P195 --------------------------------------------- 在DOS操作系统中,向屏幕输出数据实际上是把输出内容拷贝到视频缓冲区中,如果在文本模式下显示信息,只需要把内容到B8000h处的内存中; 显示图形信息,可以把图形数据拷贝到A0000h处的内存中. 在Windows中,GDI接口把程序和硬件分隔开来,在Win32编程中,再也不能通过直接向视频缓冲区拷贝数据的办法来显示信息了,我们需要通过 "设备环境"来输出图形. 设备环境实际上是一个数据结构,结构中保存的就是设备的属性,当对设备环境进行图形操作的时候,Windows可以根据这些属性找到对应的设 备进行相关的操作. 在实际使用中,通过"设备环境"可以操作的对象很广泛,除了可以是打印机或绘图仪等硬件设备外,也可以是窗口的客户区,包括大大小小的所 有可以被称为窗口的按钮与控件等的客户区,也可以是一个位图.总之,任何需要用到图形操作的对象都可以通过"设备环境"进行绘图. --------------------------------------------- 来自<<Windows环境下32位汇编语言程序设计>> P200 --------------------------------------------- 每个窗口客户区的"设备环境"对应的设备都是屏幕,但由于它们在位置上可能重叠,所以向一个窗口的客户区写数据相当于同时写了下层窗口 的客户区.为了让当前激活窗口在视觉上保持在最上面,下层窗口向自己客户区写的内容首选要经过Windows的"过滤",只有没有被其他窗口覆盖掉 的部分才真正被写到了屏幕上. "设备环境"只是一个环境,是设备属性的一组定义,程序输出的图形数据透过"设备环境"被定向到了具体的设备上,"设备环境"本身并不存储 这些数据.设备环境的上面是应用程序,下面是具体设备. 要想对任何设备绘图,首选必须获取设备的"设备环境句柄"(hDC),几乎所有的GDI函数的操作目标都是hDC,在程序中得到一个hDC有几种方法: 最常用的方法是在WM_PAINT消息中用BeginPaint函数得到hDC,WM_PAINT消息的代码结构一般是: .if eax == WM_PAINT ;eax为uMsg invoke BeginPaint,hWnd,addr stPS ;刷新客户区的代码 invoke EndPaint,hWnd,addr stPS xor eax,eax ret BeginPaint函数的返回值就是需要刷新区域的hDC.BeginPaint返回的hDC对应的尺寸仅是无效区域,无法用它绘画到这个区域以外的地方去. 由于窗口过程每次接收WM_PAINT消息时的无效区域可能都是不同的,所以这个hDC的值仅在WM_PAINT消息中有效,程序不应该保存它并把它用在 WM_PAINT消息以外的代码中.BeginPaint和EndPaint函数只能用在WM_PAINT消息中,因为只有这时候才存在无效区域. 程序中常常有这种需求,就是在非WM_PAINT消息中主动绘画客户区,由于BeginPaint和EndPaint函数必须在WM_PAINT消息中使用,所以这时 必须用另外的方法获取hDC,可以使用以下的方法: invoke GetDC,hWnd ;获取hDC ;返回值是hDC ;绘图代码 invoke ReleaseDC,hWnd,hDC ;释放hDC GetDC函数返回的hDC对应窗口的整个客户区,当使用完毕的时候,hDC必须用ReleaseDC函数释放.对于用GetDC获取的hDC,Windows建议使用的 范围限于单条消息内,当程序在处理某条消息的时候需要绘画客户区时,可以用GetDC获取hDC,但在消息返回前,必须用ReleaseDC将它释放掉,如果 在下一条消息中需要继续用到hDC,那么必须重新用GetDC函数获取. 上面两种方法获取的hDC都是窗口的hDC,如果要操作的是其他的对象,如打印机,位图等,就不能使用BeginPaint或GetDC函数了.当绘图的对象 是一个设备的时候,可以用Create DC函数来建立一个DC: invoke CreateDC,lpszDeive,lpszOutput,lpInitData lpszDriver指向设备名称,如显示设备的设备名是DISPLAY,打印机的设备名一般为WINSPOOL,下面这几句代码建立的DC对应整个屏幕 szDriver db "DISPLAY",0 ... invoke CreateDC,addr szDriver,NULL,NULL,NULL mov hDC,eax 当绘图对象是位图的时候,同样需要一个与位图句柄相联系的DC,这时可以用函数CreateCompatibleDC来创建一个显示表面仅存在于内存中 的DC: invoke CreateCompatibleDC,hDC ;这里的返回值只创建了一个与设备相兼容的句柄,这时并没有与位图关联 参数中的hDC是用来参考的DC句柄,如果指定的参数是NULL,那么建立的DC将和当前屏幕的设置兼容,为了用CreateCompatibleDC建立的DC绘制 一个位图,还需要用SelectObject函数(知识点7)将hDC和位图句柄联系起来.在这之后,通过hDC进行的绘图操作会将像素数据更新到位图中. 用CreateDC和CreateCompatibleDC函数建立的hDC在使用结束以后,必须用DeleteDC函数删除,注意这里不能用ReleaseDC,这个函数是和GetDC 配合使用的. 用BeginPaint/EndPaint,以及GetDC获取的hDC的使用时间不能超出本条消息,与此相比,用CreateDC,以及CreateCompatibleDC建立的hDC就没 有这个限制,可以在任何时候建立它并且一直使用到不再需要为此. */ /*知识点7:SelectObject函数 --------------------------------------------- 来自<<Windows环境下32位汇编语言程序设计>> P211 --------------------------------------------- GDI中的绘画函数有3大类:画点,画线和画填充区域.画笔,画刷以及其他一些GDI要使用的东西,包括字体,区域,路径,图案和位图统称GDI中的 "对象",通过SelectObject函数可以指定一个DC当前使用的对象对应哪个对象句柄,称为"当前对象",当设置了一个当前对象的时候,以后和这种对 象相关的函数都将使用当前对象,直到再次用SelectObject选择新的对象为止. SelectObject函数的用法是: invoke SelectObject,hDC,hGDIObject mov hOldObject,eax 其中参数hGDIObject就是对象的句柄,它可以是位图句柄,画笔句柄,画刷句柄,字体句柄或区域句柄,函数会根据句柄的种类自动替换原有的对象, 并将原来使用的对象句柄返回(当对象类型是区域的时候除外),如果DC中原来没有设置当前对象,那么函数的返回值是GDI_ERROR或NULL. Windows预定义了一些常用的画笔和画刷在程序中可以用GetStockObject来获取它们的句柄.Stock的中文含义是"常备的,库存的",所以这个 函数字面上的意思就是"获取常用的对象",所有获取常用对象的操作统一使用GetStockObject函数. GetStockObject函数的用法是: invoke GetStockObject,fnObject mov hObject,eax 用GetStockObject函数得到对象句柄以后,就可以用SelectObject函数将对象句柄设置到DC中了. */ /*知识点8:DrawText函数 --------------------------------------------- 来自<<WINDOWS程序设计>> P59 --------------------------------------------- DrawText(hdc, TEXT("Hello,Windows!"), -1, &rect, DT_SINGLELINE | DT_CENTER | DT_VCENTER); DrawText可以输出文本.由于该函数要输出文本,第一个参数是从BeginPaint返回的设备描述表句柄,第二个参数是输出的文本,第三个 参数是-1,指示文本串是以字节0终结的.DrawText最后一个参数是一系列位标志,它们均在WINUSER.H中定义. */