用WIN32来写窗口程序需要编写两个文件,一个是资源脚本文件:*.rc,一个是汇编源文件:*.asm。将.asm文件编译后与.rc编译后的文件链接在一起就可以得到我们的窗口程序。而资源脚本文件中包含了对菜单,加速键,图标,光标,位图等资源的定义,源文件中包含如何使用这些资源的代码。
首先我们来看资源脚本文件中菜单和加速键以及图标光标的定义。
#include
#define ICO_MAIN 0x1000 //图标
#define IDM_MAIN 0x2000 //菜单
#define IDA_MAIN 0x2000 //加速键
#define IDM_OPEN 0x4101
#define IDM_OPTION 0x4102
#define IDM_EXIT 0x4103
#define IDM_SETFONT 0x4201
#define IDM_SETCOLOR 0x4202
#define IDM_INACT 0x4203
#define IDM_GRAY 0x4204
#define IDM_LIST 0x4207
#define IDM_DETAIL 0x4208
#define IDM_TOOLBAR 0x4209
#define IDM_TOOLBARTEXT 0x4201
#define IDM_INPUTBAR 0x4211
#define IDM_HELP 0x4301
#define IDM_STATUSBAR 0x4212
#define IDM_ABOUT 0x4302
#define IDM_CLG 0x4303
//图标和光标
#define ICO_BIG 0x4304
#define ICO_SMALL 0x4305
#define CUR_2 0x4306
//图标和光标的菜单
#define IDM_BIG 0x4307
#define IDM_SMALL 0x4308
#define IDM_CUR_1 0x4309
#define IDM_CUR_2 0x4310
//>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
ICO_BIG ICON "Big.ico"
ICO_SMALL ICON "Small.ico"
CUR_2 CURSOR "2.cur"
//>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
IDM_MAIN menu discardable
BEGIN
popup "文件(&F)"
BEGIN
menuitem "打开文件(&O)...",IDM_OPEN
menuitem "关闭文件(&C)...",IDM_OPTION
menuitem separator
menuitem "退出(&X)",IDM_EXIT
END
popup "查看(&V)"
BEGIN
menuitem "字体(&F)...\tAlt+F",IDM_SETFONT
menuitem "背景色(&B)...\tCtrl+Alt+B",IDM_SETCOLOR
menuitem separator
menuitem "被禁用的菜单项",IDM_INACT,INACTIVE //表示菜单是禁用的
menuitem "被灰化的菜单项",IDM_GRAY,GRAYED //表示菜单项是灰化的
menuitem separator
menuitem "列表(&L)",IDM_LIST
menuitem "详细资料(&D)",IDM_DETAIL,MENUBREAK //表示从这一行起后面的菜单项都另起一行
menuitem separator
popup "工具栏(&T)"
BEGIN
menuitem "标准按钮(&S)",IDM_TOOLBAR
menuitem "文字标签(&C)",IDM_TOOLBARTEXT
menuitem "命令栏(&I)",IDM_INPUTBAR
END
menuitem "状态栏(&U)",IDM_STATUSBAR
END
popup "图标和光标(&I)"
BEGIN
menuitem "大图标(&G)",IDM_BIG
menuitem "小图标(&M)",IDM_SMALL
menuitem separator
menuitem "光标A(&A)",IDM_CUR_1
menuitem "光标B(&B)",IDM_CUR_2
END
popup "帮助(&H)",HELP
BEGIN
menuitem "帮助主题(&H)\tF1",IDM_HELP
menuitem separator
menuitem "关于本程序(&A)...",IDM_ABOUT
END
END
//>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
IDA_MAIN ACCELERATORS
BEGIN
VK_F1, IDM_HELP,VIRTKEY
"B", IDM_SETCOLOR,VIRTKEY,CONTROL,ALT
"F", IDM_SETFONT,VIRTKEY,ALT
END
先给出一个完整的文件。我们可以看到这是C语言的格式,因为我们的资源编译器 RC.EXE是从VS里面提取出来的,所以这个就是用C语言的格式。首先我们定义了一些菜单,图标以及加速键的命令ID,当然菜单的ID也可以使用字符串,但是这样的话在使用的时候会比较麻烦。这里有一个要注意的就是图标和光标的定义格式,图标: ID ICON “文件名.ico” 光标: ID CURSOR "文件名.cur“。这些是图标和光标的定义,上面的IDM_BIG那些是他们的菜单的命令的定义,不要搞混了。
接着往下就是菜单的定义了,定义格式是:
**菜单ID MENU [DISCARDABLE]
BEGIN
菜单项定义
END**
菜单ID就是文件中用define定义的那些东西,每一个ID都对应一个不同的菜单选项(就是那些文件啊,查看啊之类的),当然如果你想两个菜单的功能一样,可以用一样的ID。中括号中括起来的威可选部分,如果选了它就表示菜单在不再使用的时候就可以从内存中释放以节省内存。下面就是菜单项的定义,当我们点击窗口上的文件或者查看的菜单时往往会弹出许多选项,比如打开文件,关闭文件之类的,菜单项就是这些东西。菜单项的定义方法有三种。
MENUITEM 菜单文字 ,命令ID [,选项列表]
MENUITEM SEPARATOR
POPUP 菜单文字 [,选项列表]
BEGIN
菜单项目
END
第一种方法,就是定义普通的菜单项目,菜单文字就是选项上面显示的文字, ID就是上面定义的ID,这个主要是用来告诉我们的窗口过程我们选择了哪个菜单项目。选项列表就是菜单的各种属性,比如前面打个勾啊打个点什么的。选项列表有下面几个选项:
CHECKED——表示打上对勾
GRAYED——表示菜单是灰化的
INACTIVE——表示菜单项是禁用的
MENUBREAK或MENUBARBREAK——表示这个菜单项和以后的菜单项列到新的列中
第二种方法,就是定义一个分隔线
第三种方法,就是定义一个弹出式菜单,就是当我们把鼠标放在那个地方,它会显示出一个子菜单。
定义完菜单项目后就是开始定义加速键,加速键就是快捷键,定义加速键的方法和定义菜单项目的方法类似,比菜单更简单一点,格式如下
加速键ID ACCELERATORS
BEGIN
键名,命令ID [,类型] [,选项]
…
END
键名有三种方式可以定义:
**1. “^字母”:表示Ctrl加上字母键
然后是命令ID,要把加速键和对应的菜单项联系起来就要用相同的命令ID
类型:就是用来表示键名字段是用虚拟键来定义还是用ASCII码来定义,比如类型是VIRTKEY,那么键名就是用虚拟键来定义
选项:Alt,Control或Shift中的单个或多个的组合,如果是多个中间用逗号隔开,这样就表示菜单加速键是这些的组合键。
完成了资源文件的定义后就开始写汇编源文件了,首先我们要了解建立一个窗口的基本步骤,首先,我们要注册一个窗口类,这里我们可以自己定义一个类名,然后我们用这个类名再去创建我们的窗口,这个操作有点像C++中的类和对象,注册窗口类的时候可以定义一些窗口风格,重要的是窗口过程的地址就是在注册窗口类的时候获得的(窗口过程后面会讲到)。建立完窗口后刷新窗口,然后就进入我们的消息循环。
注册窗口类:RegisterClassEx,addr @stWndClass。这个函数就一个参数,但是这个参数指向一个结构,结构里却有12个参数要填,当然我们不会去记这些参数,应该没人会去记吧,一般都是要用的时候再去查。这个参数指向一个名为WNDCLASSEX的结构。
WNDCLASSEX STRUCT
cbsize DWORD ? ;结构体的字节数
style DWORD ? ;类风格
lpfnwndproc DWORD ? ;窗口过程地址
cbclsextra DWORD ? ;
cbwndextra DWORD ? ;
hinstance DWORD ? ;所属实例句柄’
hicon DWORD ? ;窗口图标
hcursor DWORD ? ;窗口光标
hbrbackground DWORD ? ;背景色
lpszmenuname DWORD ? ;窗口菜单
lpszclassname DWORD ? ;类名字符串的地址
hiconsm DWORD ? ;小图标
WNDCLASSEX ENDS
上面就是这个结构的定义,其实有将近一半是我们用不着定义的,所以这里只介绍一部分。
结构体的字节数,我们直接用 sizeof获取。
style 风格,比如CS_HREDRAW和CS_VREDEAW表示窗口的宽度或高度改变的时候是否重画窗口,CS_DBLCLKS表示把两次快速单击的行为翻译成双击,其他还有很多参数可以在官网中查到.
lpfnwndproc 窗口过程地址,这个是我们写的窗口过程函数的地址
hinstance窗口所属实例句柄
lpszclassname 类名字符串的地址,就是我们自己定义的类名的字符串的地址
当然在填结构体之前我们需要先把结构体中的字段都填0,这样以防有漏填的字段调用函数的时候出现错误。我们可以直接用一个函数来完成这个填0的操作:
invoke RtlZeroMemory,addr @stWndClass,sizeof @stWndClass
第一个参数是我们要填0的变量或结构体,第二个就是大小。
完成注册窗口后就要开始建立窗口,建立窗口函数:CreateWindowEx的参数也有12个。
invoke CreateWindowEx,dwExStyle,lpClassName,lpWindowName,dwStyle,x,y,nWidth,nHeight,hWndParent,hMenu,hInstance,lpParam
然后就是消息循环。
消息循环一般由三个函数组成,GetMessage,TranslateMessage,DispatchMessage。
在介绍这三个函数之前,先看一下Windows中窗口过程的运行过程。
windows为每一个程序的每个线程维护一个消息队列,消息队列里面存放产生的消息,比如我们鼠标的点击,或者击键,或者移动鼠标,都会产生消息。这些消息放在对应的消息队列里面。如果某个消息产生于某个应用程序的窗口内,那么这个消息也会被放到这个应用程序的消息队列里。
应用程序没有来取这个消息,这个消息就一直躺这个队列里面。当程序执行到消息循环中的GetMessage函数的时候,CPU控制权就转接到该函数所在的USER32.DLL所在的地方,其实就是转到这个函数里面取去运行,只是这个函数是在系统的DLL中。
然后该函数就从消息队列里面取出一条消息,然后把这个消息返回到应用程序里面。就是说现在指令执行又回到了应用程序中。
该消息通过TranslateMessage函数转换成ASCII码,这一步可以不要,这个不是必须的。
最后消息在应用程序中来到了DIspatchMessage*函数手里。该函数看字面意思就知道是分派,派遣消息的。该函数得到消息后,要把消息发给窗口过程,之前有说到窗口过程,窗口过程是一个函数,但是这是一个回调函数,回调函数的意思就是该函数由系统来调用,而不是我们。我们在注册窗口的时候指定了回调函数的地址,所以系统知道到哪里调用它。
DispatchMessage并不会在程序里面直接把消息传递给回调函数,因为这个函数也是处于系统DLL内的,所以该消息严格来讲,现在是处于USER32.DLL中,而因为一个程序可能有多个窗口,该函数判断消息是哪个窗口的之后。从系统中将该消息发给窗口过程,也就是窗口回调函数。
最后窗口过程处理完消息之后,就返回到USER32.DLL的DIspatchMessge中,然后再返回到消息循环开始位置。这样,一个循环就结束了。
注册窗口和消息循环一般会写再一个函数里面,定义一般如下:
_WinMain proc
local @stWndClass:WNDCLASSEX
local @stMsg:MSG
local @stWndClassChild:WNDCLASSEX
local @stMsgChild:MSG
invoke GetModuleHandle,NULL ;获取模块句柄
mov hInstance,eax
invoke RtlZeroMemory,addr @stWndClass,sizeof @stWndClass;数据结构所有字段初始化为0
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
;注册窗口类
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
invoke LoadCursor,0,IDC_HAND ;定义光标的样子
mov @stWndClass.hCursor,eax
push hInstance
pop @stWndClass.hInstance ;获得模块句柄
mov @stWndClass.cbSize,sizeof WNDCLASSEX ;结构的长度
mov @stWndClass.style,CS_HREDRAW or CS_VREDRAW or CS_DBLCLKS ;定义窗口风格
mov @stWndClass.lpfnWndProc,offset _ProcWinMain ;获取窗口过程的地址
mov @stWndClass.hbrBackground,COLOR_WINDOW + 1 ;刷子
mov @stWndClass.lpszClassName,offset szClassName;类名
invoke RegisterClassEx,addr @stWndClass ;注册窗口
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
;注册子窗口类
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
invoke LoadCursor,0,IDC_UPARROW ;定义光标的样子
mov @stWndClassChild.hCursor,eax
push hInstance
pop @stWndClassChild.hInstance ;获得模块句柄
mov @stWndClassChild.cbSize,sizeof WNDCLASSEX ;结构的长度
mov @stWndClassChild.style,CS_HREDRAW or CS_VREDRAW or CS_DBLCLKS ;定义窗口风格
mov @stWndClassChild.lpfnWndProc,offset _ProcWinMain;获取窗口过程的地址
mov @stWndClassChild.hbrBackground,COLOR_WINDOW + 3 ;刷子
mov @stWndClassChild.lpszClassName,offset szClassNameChild;类名
invoke RegisterClassEx,addr @stWndClassChild ;注册窗口
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
;建立并显示窗口
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
invoke CreateWindowEx,NULL,offset szClassName,\
offset szCaptionMain,WS_MAXIMIZEBOX or WS_MINIMIZEBOX or WS_SYSMENU or WS_THICKFRAME or WS_VSCROLL or WS_HSCROLL,\
100,100,600,400,\
NULL,NULL,hInstance,NULL
;窗口风格,窗口的类的名称,窗口的标题,窗口风格,窗口坐标,窗口大小,窗口所属父窗口,菜单,模块句柄,指针
mov hWinMain,eax ;获取窗口句柄
invoke ShowWindow,hWinMain,SW_SHOWNORMAL ;显示窗口
invoke UpdateWindow,hWinMain ;刷新窗口
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
;建立子窗口
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
invoke CreateWindowEx,WS_EX_WINDOWEDGE,offset szClassNameChild,\
offset szText,WS_CHILD,\
50,10,100,100,\
hWinMain,NULL,hInstance,NULL
;窗口风格,窗口的类的名称,窗口的标题,窗口风格,窗口坐标,窗口大小,窗口所属父窗口,菜单,模块句柄,指针
mov hWinMainChild,eax ;获取窗口句柄
invoke ShowWindow,hWinMainChild,SW_SHOWNORMAL ;显示窗口
invoke UpdateWindow,hWinMainChild ;刷新窗口
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
;按钮设置
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
invoke CreateWindowEx,NULL,\
offset szButton,offset szButtonText,\
WS_CHILD or WS_VISIBLE,\
10,300 ,65,22,\
hWinMain,NULL,hInstance,NULL
mov hWinMainButton,eax
;invoke ShowWindow,hWinMainButton,SW_SHOWNORMAL
;invoke UpdateWindow,hWinMainButton
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
;子窗口的按钮
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
invoke CreateWindowEx,NULL,\
offset szButton,offset szButtonChileText,\
WS_CHILD or WS_VISIBLE,\
22,70,60,18,\
hWinMainChild,NULL,hInstance,NULL
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
;文本框窗口设置
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
invoke CreateWindowEx,NULL,\
offset szEdit,offset szEditText,\
WS_CHILD or WS_VISIBLE,\
100,300,65,22,\
hWinMain,NULL,hInstance,NULL
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
;消息循环
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
.while TRUE
invoke GetMessage,addr @stMsg,NULL,0,0 ;获取本窗口的消息,存储到消息结构中
.break .if eax == 0 ;如果消息为退出则退出循环
invoke TranslateMessage,addr @stMsg
invoke DispatchMessage,addr @stMsg ;这个函数将回调我们的窗口过程
.endw
ret
_WinMain endp
这里增加了一个子窗口的定义,子窗口的建立方法和窗口的一样,只是创建窗口的时候参数里要标明父窗口句柄。
然后窗口过程,也就是回调函数的定义一般如下:
_ProcWinMain proc uses ebx edi esi,hWnd,uMsg,wParam,lParam ;服务窗口的句柄,消息标识,参数,参数
local @stPs:PAINTSTRUCT
local @stRect:RECT
local @hDc
mov eax,uMsg
.if eax == WM_CLOSE
invoke DestroyWindow,hWinMain ;销毁窗口
invoke PostQuitMessage,NULL ;退出程序
.elseif eax == WM_PAINT
invoke BeginPaint,hWnd,addr @stPs
mov @hDc,eax
invoke GetClientRect,hWnd,addr @stRect
invoke DrawText,@hDc,addr szText,-1,addr @stRect,\
DT_SINGLELINE or DT_CENTER or DT_VCENTER
invoke EndPaint,hWnd,addr @stPs
.else
invoke DefWindowProc,hWnd,uMsg,wParam,lParam
ret
.endif
xor ax,ax
ret
_ProcWinMain endp
回调函数有着固定的格式,规定为四个参数。
第一个参数hWnd是产生该消息的窗口的句柄。第二个参数uMsg是消息标识,表示该消息的类型。后面两个参数会因为消息的类型不同而有点不同。
要注意窗口过程中有一个函数:DefWindowProc。该函数就是帮我们处理我们不想处理的消息,因为在窗口中,就算是鼠标移动一下或者随便点击一下都会产生消息。如果所有的消息都我们自己来处理的话,那就不是上面这些代码能解决的了。该函数会以默认的方式处理我们不处理的消息。
如果我们要关闭窗口,可以使用DestroyWindow函数,该函数只有一个参数,就是要销毁的窗口的句柄。
DestroyWindow函数只是销毁窗口,如果我们没有调用PostQuitMessage* 函数来退出消息循环的话,就算窗口销毁了,消息循环还在继续,所以一定要记得使用这个函数。
最后,只要在程序开始的地方调用消息循环所在的函数就可以了。
将上面的代码编译链接后运行一下:
大功告成!