• 标 题:windows下32位汇编语言学习笔记 第四章 第一个窗口程序 1 (消息的使用和入口代码)
  • 作 者:jasonnbfan
  • 时 间:2009-05-08 22:05
  • 链 接:http://bbs.pediy.com/showthread.php?t=88340

键盘消息的使用

可以使用PostMessage给目标窗口或者控件发送键盘消息,按键消息和字符消息,但是使用SendMessage只能发送字符消息,而不能发送按键消息,想想为什么?
开始练习按键消息前,必须要先了解2个函数:
HWND FindWindow(LPCTSTR lpClassName,LPCTSTR lpWindowName);通过lpClassName窗口注册类名(就是WNDCLASS里的lpszClassName名称)或者lpWindowName窗口标题名获得窗口句柄。
2个参数随便用一个就可以,不使用的给NULL。

HWND FindWindowEx(HWND,hwndParent,HWND hwndChildAfter,LPCTSTR lpszClass,LPSTSTR lpszWindow);这个函数可以通过窗口句柄和控件类名或者控件标题名获得这个控件的句柄。
先通过FindWindow得到主窗口句柄,然后通过FindWindowEx得到主窗口内某个控件的句柄。

下面看看如何通过PostMessage给windows记事本发送按键消息
首先找到记事本
szClac  db  'Notepad',0  记事本主窗体的类名,可以通过Spy++获取
szEdit  db  'Edit',0   内容用于写内容的Edit控件

hwndnote  db    ?  用于保存句柄

invoke FindWindow,offser szCalc,NULL    ;找到记事本句柄
invoke FindWindowEx,eax,NULL,offset szEdit,0  ;找到edit控件的句柄
mov hwndnote,eax

下面就可以给记事本发送各种键盘消息,比如

invoke SendMessage,hwndnote,WM_KEYDOWN,VK_1,0    ;发送一个按键消息1
                    
invoke PostMessage,hwndnote,WM_KEYDOWN,VK_2,0  ;发送一个按键消息2

invoke SendMessage,hwndnote,WM_CHAR,VK_3,0  ;用SendMessage发送一个字符消息3

想象发送后记事本上的的字符顺序是1,2,3么?

发送一个组合键Alt+E,就是打开记事本的编辑菜单
            
invoke PostMessage,hWndnd,WM_SYSKEYDOWN,VK_MENU,020000001h  ALT键按下
invoke PostMessage,hWndnd,WM_SYSKEYDOWN,VK_E,020000001h  E键按下必须要把第29位设置成1,代表alt键已经下
invoke PostMessage,hWndnd,WM_SYSCHAR,VK_E,020000001h    发送一个系统字符E
invoke PostMessage,hWndnd,WM_SYSKEYUP,VK_E,080000001h    E键放开,必须把31位设置成1,表示这个是系统键
invoke PostMessage,hWndnd,WM_KEYUP,VK_MENU,080000001h    ALT键放开,31位系统键设置成1

这组消息可以通过SPY++监视记事本的键盘输入状态得到,其实可以精简,只用下面2条就可以。
invoke PostMessage,hWndnd,WM_SYSKEYDOWN,VK_E,020000001h  E键按下必须要把第29位设置成1,代表alt键已经下
invoke PostMessage,hWndnd,WM_SYSKEYUP,VK_E,080000001h    E键放开,必须把31位设置成1,表示这个是系统键

因为E键的lParam参数的29位置1,已经说明这个E在这里表示系统按键,29位置1表示ALT键已经按下。
按键弹起的时候,必须把31位置1,表示这是个系统键弹起。否则会当做普通键,并且在记事本里打印出字母e。

现在想出来这组消息后,记事本上会是什么字符么?答案是:321,前面说过SendMessage的优先级高于PostMessage,所以是先打出3,然后是1,最后是2。

关于windows消息的操作还有很多,这里只举出了最基本的发送键盘消息的方法。理解这些基本的操作是位日后学习使用其他消息操作打下一个好的基础。


鼠标消息的使用
键盘消息只发送给当前拥有输入焦点的窗口,鼠标消息不同,只要鼠标达到,窗口就会收到鼠标消息。当鼠标在窗口显示区域内,鼠标消息的lParam参数是鼠标所在窗口的X,Y坐标值,当鼠标不在窗口显示区域内,参数lParam是桌面的X,Y坐标值。
显示区域:是指用户能够输出显示信息结果的区域。非显示区域是指:菜单,标题栏,滚动条

对于显示区内发送鼠按键消息,wParam参数指定鼠标按键以及Shift和Ctrl按键的状态,键值如下:
MK_CONTROL 表示ctrl按下 MK_?BUTTON 表示鼠标3个键按下 MK_SHIFT 表示shift按下 
lParam参数指定鼠标的坐标值,高位Y坐标,低位X坐标

下面的例子代码是使用键盘的上下左右方向键移动鼠标光标,空格键发送鼠标单击消息。可以把SendMessage句柄改成“画图”程序句柄,这样在当前窗口按空格键,将会在画图程序的同样位置画出一个点。

_MoveMouse proc hwnd,wParam,lParam
  local @szPos [128]:byte
  local @stPoint:POINT
  local @stRect:RECT
  
  invoke GetCursorPos,addr @stPoint            ;获得当前鼠标屏幕坐标位置
  invoke ScreenToClient,hwnd,addr @stPoint          ;将鼠标的屏幕坐标位位置转换成当前窗口内的坐标位置
  invoke wsprintf,addr @szPos,offset szMsg,@stPoint.x,@stPoint.y        
  invoke SetWindowText,hwnd,addr @szPos
  mov eax,wParam
  .if eax == VK_LEFT
    sub @stPoint.x,1
  .elseif eax == VK_RIGHT
    add @stPoint.x,1
  .elseif eax == VK_UP
    sub @stPoint.y,1
  .elseif eax == VK_DOWN
    add @stPoint.y,1
  .elseif eax == VK_SPACE
    mov eax,@stPoint.y
    shl eax,16
    add eax,@stPoint.x
    invoke PostMessage,hwnd,WM_LBUTTONDOWN,MK_LBUTTON,eax
    invoke PostMessage,hwnd,WM_LBUTTONUP,0,eax
  .endif
  invoke ClientToScreen,hwnd,addr @stPoint          ;将当前窗口坐标位置转换成屏幕位置
  invoke SetCursorPos,@stPoint.x,@stPoint.y          ;设置光标位置
  ret  
_MoveMouse endp

在窗口的回调函数中加入以下代码:

.elseif eax == WM_KEYDOWN
  mov eax,wParam
  .if wParam == VK_LEFT || wParam || VK_RIGHT || wParam == VK_UP || wParam == VK_DOWN || wParam == VK_SPACE
    invoke _MoveMouse,hWnd,wParam,lParam
  .endif

对于非显示区鼠标消息和显示区鼠标消息类似,消息后加"NC"代码表示非显示区消息,例如WM_NCLBUTTONCLICK 
参数wParam是一些定义在winuser.h里以HT开头的的非显示区域代码,比如HTCAPTION 代表标题栏,HTCLOSE,代表窗口右上角的关闭按钮等等。
参数lParam表示屏幕坐标,不是显示区坐标,同样低位是X坐标,高位是Y坐标。



纯C写的FirstWindow和汇编FirstWindow的区别

同样的FirstWindow程序,我用C写了一个,反汇编后比较,发现反汇编结果里多了很多编译器添加的代码。尺寸也大了不少,查了一些资料,发现原来这些编译器添加的代码就是传说中的CRT,C语言运行时环境。

用C写windows程序,都知道程序从winMain开始执行,实际上在这之前,是有其他的函数来调用WinMain的。这个函数就叫做入口函数。
入口函数对运行时库和程序运行环境进行初始化,包括堆,I/O,线程等等。入口函数执行完后才回去调用main函数正式开始执行程序,WinMain执行完后,返回到入口函数,由入口函数进行清理工作。

这倒也好理解,winMain之前肯定有些东西执行了什么,比如winMain的4个参数,hInstance,szCmdLine,iCmdShow 都是从启动函数传给winMain的。
对于我现在使用的vs2008的编译器来说,入口函数的代码位于srt\src\crt0.c文件里。函数的名称是__tmainCRTStartup。现在看看里面都干了些什么关键:

首先定义了个STARTUPINFO StartupInfo结构,使用GetStartupInfo(&StartupInfo)初始化。STARTUPINFO结构包含一些进程的信息。具体细节可以查看msdn.
紧接着初始化堆    _heap_init(1)
初始化堆是很重要的,否则不能使用C++的new 或c的malloc来分配内存。
然后初始化多线程  _mtinit() 
然后初始化I/O,_ioinit(),得到命令行参数GetCommandLineT();得到当前进程进程版本信息

最后调用启动函数
WinMain((HINSTANCE) & __ImageBase,NULL,lpszCommandLine,StartupInfo.dwFlags & STARTF_USESHOWWINDOW? StartupInfo.wShowWindow: SW_SHOWDEFAULT);
到这里就可以看见,winMain的参数是怎么来的了,hInsteance 就是__ImageBase(载入基址),命令行参数也是传进来的,最后的iCmdShow,参数就是STRTUPINFO里的显示方式。

就是因为编译时加入了启动函数所以使C程序编译出来的可执行文件比汇编程的大了30多K。
其实启动函数不是必须的,可以自定义一个自己的启动函数代替默认的启动函数。
比如定义一个
int WINAPI main()
{
    HINSTANCE hInstance = GetModuleHandle(NULL);      //得到当前进程的句柄,和汇编一样
    LPSTR lpszCmdLine = GetCommandLine();          //获得命令行参数
    int r = WinMain(hInstance, NULL, lpszCmdLine, SW_SHOWDEFAULT);  //调用WinMain函数,就开始执行
    ExitProcess(r);                //最后结束进程
    return r; // this will never be reached.
}
需要在link.exe 后加/entry:main /nodefaultlib:msvcrt90.lib参数,/entry指定入口点函数, /nodefaultlib指定不连接运行时库。

这样编译连接后,可执行文件尺寸和汇编后的大小一样。反汇编后比较内容也基本差不多。要不说C语言执行速度快,编译后的内容和直接用汇编写的程序基本上一样。