Win32汇编:创建简单的窗口

Windows 程序中,在写图形用户界面时需要调用大量的标准 Windows Gui 函数。其实这对用户和程序员来说都有好处,对于用户,面对的是同一套标准的窗口,对这些窗口的操作都是一样的,所以使用不同的应用程序时无须重新学习操作。对程序员来说,这些 Gui 源代码都是经过了微软的严格测试,随时拿来就可以用的。当然至于具体地写程序对于程序员来说还是有难度的。为了创建基于窗口的应用程序,必须严格遵守规范。作到这一点并不难,只要用模块化或面向对象的编程方法即可。

下面我就列出在桌面显示一个窗口的几个步骤:

  1. 得到您应用程序的句柄(必需);
  2. 得到命令行参数(如果您想从命令行得到参数,可选);
  3. 注册窗口类(必需,除非您使用 Windows 预定义的窗口类,如 MessageBox 或 dialog box;
  4. 产生窗口(必需);
  5. 在桌面显示窗口(必需,除非您不想立即显示它);
  6. 刷新窗口客户区;
  7. 进入无限的获取窗口消息的循环;
  8. 如果有消息到达,由负责该窗口的窗口回调函数处理;
  9. 如果用户关闭窗口,进行退出处理。

下面列出简单的窗口程序的源代码:

 1:  .386 
 2:  .model flat,stdcall 
 3:  option casemap:none 
 4:   
 5:  WinMain proto :DWORD,:DWORD,:DWORD,:DWORD 
 6:   
 7:  include /masm32/include/windows.inc 
 8:  include /masm32/include/user32.inc 
 9:  includelib /masm32/lib/user32.lib 
10:  include /masm32/include/kernel32.inc 
11:  includelib /masm32/lib/kernel32.lib 
12:   
13:  .DATA 
14:  ClassName db "SimpleWinClass",0 
15:  AppName  db "Our First Window",0 
16:  OurText  db "Win32 assembly is great and easy!",0 
17:   
18:  .DATA? 
19:  hInstance HINSTANCE ? 
20:  CommandLine LPSTR ? 
21:   
22:  .CODE 
23:  start: 
24:      invoke GetModuleHandle, NULL 
25:      mov    hInstance,eax 
26:      invoke GetCommandLine
27:      mov CommandLine,eax
28:      invoke WinMain, hInstance,NULL,CommandLine, SW_SHOWDEFAULT 
29:      invoke ExitProcess,eax 
30:   
31:  WinMain proc hInst:HINSTANCE, hPrevInst:HINSTANCE, CmdLine:LPSTR, CmdShow:DWORD 
32:      LOCAL wc:WNDCLASSEX 
33:      LOCAL msg:MSG 
34:      LOCAL hwnd:HWND 
35:      mov   wc.cbSize,SIZEOF WNDCLASSEX 
36:      mov   wc.style, CS_HREDRAW or CS_VREDRAW 
37:      mov   wc.lpfnWndProc, OFFSET WndProc 
38:      mov   wc.cbClsExtra,NULL 
39:      mov   wc.cbWndExtra,NULL 
40:      push  hInst 
41:      pop   wc.hInstance 
42:      mov   wc.hbrBackground,COLOR_WINDOW+1 
43:      mov   wc.lpszMenuName,NULL 
44:      mov   wc.lpszClassName,OFFSET ClassName 
45:      invoke LoadIcon,NULL,IDI_APPLICATION 
46:      mov   wc.hIcon,eax 
47:      mov   wc.hIconSm,eax 
48:      invoke LoadCursor,NULL,IDC_ARROW 
49:      mov   wc.hCursor,eax 
50:      invoke RegisterClassEx, addr wc 
51:      invoke CreateWindowEx,NULL,ADDR ClassName,ADDR AppName,/ 
52:             WS_OVERLAPPEDWINDOW,CW_USEDEFAULT,/ 
53:             CW_USEDEFAULT,CW_USEDEFAULT,CW_USEDEFAULT,NULL,NULL,/ 
54:             hInst,NULL 
55:      mov   hwnd,eax 
56:      invoke ShowWindow, hwnd,SW_SHOWNORMAL 
57:      invoke UpdateWindow, hwnd 
58:          .WHILE TRUE 
59:                  invoke GetMessage, ADDR msg,NULL,0,0 
60:                  .BREAK .IF (!eax) 
61:                  invoke TranslateMessage, ADDR msg 
62:                  invoke DispatchMessage, ADDR msg 
63:          .ENDW 
64:          mov     eax,msg.wParam 
65:          ret 
66:  WinMain endp 
67:   
68:  WndProc proc hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM 
69:      LOCAL hdc:HDC 
70:      LOCAL ps:PAINTSTRUCT 
71:      LOCAL rect:RECT 
72:      .IF uMsg==WM_DESTROY 
73:          invoke PostQuitMessage,NULL 
74:      .ELSEIF uMsg==WM_PAINT 
75:          invoke BeginPaint,hWnd, ADDR ps 
76:          mov    hdc,eax 
77:          invoke GetClientRect,hWnd, ADDR rect 
78:          invoke DrawText, hdc,ADDR OurText,-1, ADDR rect, / 
79:                  DT_SINGLELINE or DT_CENTER or DT_VCENTER 
80:          invoke EndPaint,hWnd, ADDR ps 
81:      .ELSE 
82:          invoke DefWindowProc,hWnd,uMsg,wParam,lParam 
83:          ret 
84:      .ENDIF 
85:      xor   eax, eax 
86:      ret 
87:  WndProc endp 
88:  end start 

分析:

看到一个简单的 Windows 程序有这么多行,您是不是有点想撤? 但是您必须要知道的是上面的大多数代码都是模板而已,模板的意思即是指这些代码对差不多所有标准 Windows 程序来说都是相同的。在写 Windows 程序时您可以把这些代码拷来拷去,当然把这些重复的代码写到一个库中也挺好。其实真正要写的代码集中在 WinMain 中。这和一些 C 编译器一样,无须要关心其它杂务,集中精力于 WinMain 函数。唯一不同的是 C 编译器要求您的源代码有必须有一个函数叫 WinMain。否则 C 无法知道将哪个函数和有关的前后代码链接。相对C,汇编语言提供了较大的灵活性,它不强行要求一个叫 WinMain 的函数。

.386
.model flat,stdcall
option casemap:none

WinMain proto :DWORD,:DWORD,:DWORD,:DWORD

include /masm32/include/windows.inc
include /masm32/include/user32.inc
includelib /masm32/lib/user32.lib
include /masm32/include/kernel32.inc
includelib /masm32/lib/kernel32.lib

前三行是必须的,接下来的一行是函数WinMain的原型声明,因为我们稍后要用到该函数,故必须先声明。window.inc 文件包含大量要用到的常量和结构的定义。

用 includelib 指令,包含您的程序要引用的库文件,譬如:若您的程序要调用 "MessageBox", 您就应当在源文件中加入如下一行: includelib user32.lib 这条语句告诉 MASM 您的程序将要用到一些引入库。如果您不止引用一个库,只要简单地加入 includelib 语句,不要担心链接器如何处理这么多的库,只要在链接时用链接开关 /LIBPATH 指明库所在的路径即可。我们的程序调用驻扎在 user32.dll (譬如:CreateWindowEx, RegisterWindowClassEx) 和 kernel32.dll (ExitProcess)中的函数,所以必须链接这两个库。

.DATA
ClassName db "SimpleWinClass",0
AppName db "Our First Window",0
.DATA?
hInstance HINSTANCE ?
CommandLine LPSTR ?

接下来是DATA"分段"。 在 .DATA 中我们定义了两个以 NULL 结尾的字符串 (ASCIIZ):其中 ClassName 是 Windows 类名,AppName 是我们窗口的名字。这两个变量都是初始化了的。未进行初始化的两个边量放在 .DATA? "分段"中,其中 hInstance 代表应用程序的句柄,CommandLine 保存从命令行传入的参数。HINSTACE 和 LPSTR 是两个数据类型名,它们在头文件中定义,可以看做是 DWORD 的别名,之所以要这么重新定仅是为了易记。在.DATA?中的的变量都是未初始化的。

.CODE
start:
invoke GetModuleHandle, NULL
mov hInstance,eax
invoke GetCommandLine
mov CommandLine,eax
invoke WinMain, hInstance,NULL,CommandLine, SW_SHOWDEFAULT
invoke ExitProcess,eax
.....
end start

.CODE"分段"包含您应用程序的所有代码。程序的第一条语句是调用GetModuleHandle去查找我们应用程序的句柄。在Win32下应用程序的句柄和模块的句柄是一样的。您可以把实例句柄看成是你的应用程序的ID号。我们在调用几个函数时都把它作为参数来传递。

特别注意:WIN32下的实例句柄实际上是您应用程序在内存中的线性地址。

WIN32 中函数的函数如果有返回值,那它是通过 eax 寄存器来传递的。其他的值可以通过传递进来的参数地址进行返回。

如果您的应用程序不处理命令行那么就无须调用 GetCommandLine,这里只是告诉您如果要调用应该怎么做。

下面则是调用WinMain了。该函数共有4个参数:应用程序的实例句柄,该应用程序的前一实例句柄,命令行参数串指针和窗口如何显示。Win32 没有前一实例句柄的概念,所以第二个参数总为0。之所以保留它是为了和 Win16 兼容的考虑,在 Win16下,如果 hPrevInst 是 NULL,则该函数是第一次运行。特别注意:您不用必须申明一个名为 WinMain 函数,事实上在这方面您可以完全作主,您甚至无须有一个和 WinMain 等同的函数。您只要把 WinMain 中的代码拷到GetCommandLine 之后,其所实现的功能完全相同。在 WinMain 返回时,把返回码放到 eax 中。然后在应用程序结束时通过 ExitProcess 函数把该返回码传递给 Windows 。

WinMain proc Inst:HINSTANCE,hPrevInst:HINSTANCE,CmdLine:LPSTR,CmdShow:DWORD

上面是WinMain的定义。注意跟在 proc 指令后的parameter:type形式的参数,它们是由调用者传给 WinMain 的,我们引用是直接用参数名即可。至于压栈和退栈时的平衡堆栈工作由 MASM 在编译时加入相关的前序和后序汇编指令来进行 LOCAL wc:W

LOCAL wc:WNDCLASSEX      
LOCAL msg:MSG
LOCAL hwnd:HWND

LOCAL 伪指令为局部变量在栈中分配内存空间,所有的 LOCAL 指令必须紧跟在 PROC 之后。LOCAL 后跟声明的变量,其形式是 变量名:变量类型。譬如 LOCAL wc:WNDCLASSEX 即是告诉 MASM 为名字叫 wc 的局部边量在栈中分配长度为 WNDCLASSEX 结构体长度的内存空间,然后我们在用该局部变量是无须考虑堆栈的问题,这样申明的局部变量在函数结束时释放栈空间。

mov wc.cbSize,SIZEOF WNDCLASSEX
mov wc.style, CS_HREDRAW or CS_VREDRAW
mov wc.lpfnWndProc, OFFSET WndProc
mov wc.cbClsExtra,NULL
mov wc.cbWndExtra,NULL
push hInstance
pop wc.hInstance
mov wc.hbrBackground,COLOR_WINDOW+1
mov wc.lpszMenuName,NULL
mov wc.lpszClassName,OFFSET ClassName
invoke LoadIcon,NULL,IDI_APPLICATION
mov wc.hIcon,eax
mov wc.hIconSm,eax
invoke LoadCursor,NULL,IDC_ARROW
mov wc.hCursor,eax invoke
RegisterClassEx, addr w

上面几行从概念上说确实是非常地简单。只要几行指令就可以实现。其中的主要概念就是窗口类(window class),一个窗口类就是一个有关窗口的规范,这个规范定义了几个主要的窗口的元素,如:图标、光标、背景色、和负责处理该窗口的函数。您产生一个窗口时就必须要有这样的一个窗口类。如果您要产生不止一个同种类型的窗口时,最好的方法就是把这个窗口类存储起来,这种方法可以节约许多的内存空间。如果您要定义自己的创建窗口类就必须:在一个 WINDCLASS 或 WINDOWCLASSEXE 结构体中指明您窗口的组成元素,然后调用 RegisterClass 或 RegisterClassEx ,再根据该窗口类产生窗口。对不同特色的窗口必须定义不同的窗口类。 WINDOWS有几个预定义的窗口类,譬如:按钮、编辑框等。要产生该种风格的窗口无须预先再定义窗口类了,只要包预定义类的类名作为参数调用 CreateWindowEx 即可。

WNDCLASSEX 中最重要的成员莫过于lpfnWndProc了。前缀 lpfn 表示该成员是一个指向函数的长指针。在 Win32中由于内存模式是 FLAT 型,所以没有 near 或 far 的区别。每一个窗口类必须有一个窗口过程,当 Windows 把属于特定窗口的消息发送给该窗口时,该窗口的窗口类负责处理所有的消息,如键盘消息或鼠标消息。由于窗口过程差不多智能地处理了所有的窗口消息循环,所以您只要在其中加入消息处理过程即可。下面讲解 WNDCLASSEX 的每一个成员:

WNDCLASSEX STRUCT DWORD
  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

  • cbSize:WNDCLASSEX 的大小。我们可以用sizeof(WNDCLASSEX)来获得准确的值。
  • style:从这个窗口类派生的窗口具有的风格。您可以用“or”操作符来把几个风格或到一起。
  • lpfnWndProc:窗口处理函数的指针。
  • cbClsExtra:指定紧跟在窗口类结构后的附加字节数。
  • cbWndExtra:指定紧跟在窗口事例后的附加字节数。如果一个应用程序在资源中用CLASS伪指令注册一个对话框类时,则必须把这个成员设成DLGWINDOWEXTRA。
  • hInstance:本模块的事例句柄。
  • hIcon:图标的句柄。
  • hCursor:光标的句柄。
  • hbrBackground:背景画刷的句柄。
  • lpszMenuName:指向菜单的指针。
  • lpszClassName:指向类名称的指针。
  • hIconSm:和窗口类关联的小图标。如果该值为NULL。则把hCursor中的图标转换成大小合适的小图标。

    invoke CreateWindowEx, NULL,/
    ADDR ClassName,/
    ADDR AppName,/
    WS_OVERLAPPEDWINDOW,/
    CW_USEDEFAULT,/
    CW_USEDEFAULT,/
    CW_USEDEFAULT,/
    CW_USEDEFAULT,/
    NULL,/
    NULL,/
    hInst,/
    NULL

    注册窗口类后,我们将调用CreateWindowEx来产生实际的窗口。请注意该函数有12个参数。

    CreateWindowExA proto dwExStyle:DWORD,/
    lpClassName:DWORD,/
    lpWindowName:DWORD,/
    dwStyle:DWORD,/
    X:DWORD,/
    Y:DWORD,/
    nWidth:DWORD,/
    nHeight:DWORD,/
    hWndParent:DWORD ,/
    hMenu:DWORD,/
    hInstance:DWORD,/
    lpParam:DWORD

    下面是参数的解释:

  • dwExStyle:附加的窗口风格。相对于旧的CreateWindow这是一个新的参数。在9X/NT中您可以使用新的窗口风格。您可以在Style中指定一般的窗口风格,但是一些特殊的窗口风格,如顶层窗口则必须在此参数中指定。如果您不想指定任何特别的风格,则把此参数设为NULL。
  • lpClassName:(必须)。ASCIIZ形式的窗口类名称的地址。可以是您自定义的类,也可以是预定义的类名。像上面所说,每一个应用程序必须有一个窗口类。
  • lpWindowName:ASCIIZ形式的窗口名称的地址。该名称会显示在标题条上。如果该参数空白,则标题条上什么都没有。
  • dwStyle:窗口的风格。在此您可以指定窗口的外观。可以指定该参数为零,但那样该窗口就没有系统菜单,也没有最大化和最小化按钮,也没有关闭按钮,那样您不得不按Alt+F4 来关闭它。最为普遍的窗口类风格是 WS_OVERLAPPEDWINDOW。 一种窗口风格是一种按位的掩码,这样您可以用“or”把您希望的窗口风格或起来。像 WS_OVERLAPPEDWINDOW 就是由几种最为不便普遍的风格或起来的。
  • X,Y: 指定窗口左上角的以像素为单位的屏幕坐标位置。缺省地可指定为 CW_USEDEFAULT,这样 Windows 会自动为窗口指定最合适的位置。
  • nWidth, nHeight: 以像素为单位的窗口大小。缺省地可指定为 CW_USEDEFAULT,这样 Windows 会自动为窗口指定最合适的大小。
  • hWndParent: 父窗口的句柄(如果有的话)。这个参数告诉 Windows 这是一个子窗口和他的父窗口是谁。这和 MDI(多文档结构)不同,此处的子窗口并不会局限在父窗口的客户区内。他只是用来告诉 Windows 各个窗口之间的父子关系,以便在父窗口销毁是一同把其子窗口销毁。在我们的例子程序中因为只有一个窗口,故把该参数设为 NULL。
  • hMenu: WINDOWS菜单的句柄。如果只用系统菜单则指定该参数为NULL。回头看一看WNDCLASSEX 结构中的 lpszMenuName 参数,它也指定一个菜单,这是一个缺省菜单,任何从该窗口类派生的窗口若想用其他的菜单需在该参数中重新指定。其实该参数有双重意义:一方面若这是一个自定义窗口时该参数代表菜单句柄,另一方面,若这是一个预定义窗口时,该参数代表是该窗口的 ID 号。Windows 是根据lpClassName 参数来区分是自定义窗口还是预定义窗口的。
  • hInstance: 产生该窗口的应用程序的实例句柄。
  • lpParam: (可选)指向欲传给窗口的结构体数据类型参数的指针。如在MDI中在产生窗口时传递 CLIENTCREATESTRUCT 结构的参数。一般情况下,该值总为零,这表示没有参数传递给窗口。可以通过GetWindowLong 函数检索该值。

    mov hwnd,eax
    invoke ShowWindow, hwnd,CmdShow
    invoke UpdateWindow, hwnd

    调用CreateWindowEx成功后,窗口句柄在eax中。我们必须保存该值以备后用。我们刚刚产生的窗口不会自动显示,所以必须调用 ShowWindow 来按照我们希望的方式来显示该窗口。接下来调用 UpdateWindow 来更新客户区。

    .WHILE TRUE
    invoke GetMessage, ADDR msg,NULL,0,0
    .BREAK .IF (!eax)
    invoke TranslateMessage, ADDR msg
    invoke DispatchMessage, ADDR msg
    .ENDW

    这时候我们的窗口已显示在屏幕上了。但是它还不能从外界接收消息。所以我们必须给它提供相关的消息。我们是通过一个消息循环来完成该项工作的。每一个模块仅有一个消息循环,我们不断地调用 GetMessage 从 Windows 中获得消息。GetMessage 传递一个 MSG 结构体给 Windows ,然后 Windows 在该函数中填充有关的消息,一直到 Windows 找到并填充好消息后 GetMessage 才会返回。在这段时间内系统控制权可能会转移给其他的应用程序。这样就构成了Win16 下的多任务结构。如果 GetMessage 接收到 WM_QUIT 消息后就会返回 FALSE,使循环结束并退出应用程序。TranslateMessage 函数是一个是实用函数,它从键盘接受原始按键消息,然后解释成 WM_CHAR,在把 WM_CHAR 放入消息队列,由于经过解释后的消息中含有按键的 ASCII 码,这比原始的扫描码好理解得多。如果您的应用程序不处理按键消息的话,可以不调用该函数。DispatchMessage 会把消息发送给负责该窗口过程的函数。

    mov eax,msg.wParam
    ret
    WinMain endp

    如果消息循环结束了,退出码存放在 MSG 中的 wParam中,您可以通过把它放到 eax 寄存器中传给 Windows目前 Windows 没有利用到这个结束码,但我们最好还是遵从 Windows 规范已防意外。

    WndProc proc hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM

    WndProc是我们的窗口处理函数。您可以随便给该函数命名。其中第一个参数 hWnd 是接收消息的窗口的句柄。uMsg 是接收的消息。注意 uMsg 不是一个 MSG 结构,其实上只是一个 DWORD 类型数。Windows 定义了成百上千个消息,大多数您的应用程序不会处理到。当有该窗口的消息发生时,Windows 会发送一个相关消息给该窗口。其窗口过程处理函数会智能的处理这些消息。wParam 和 lParam 只是附加参数,以方便传递更多的和该消息有关的数据。

    .IF uMsg==WM_DESTROY
    invoke PostQuitMessage,NULL
    .ELSE
    invoke DefWindowProc,hWnd,uMsg,wParam,lParam
    ret
    .ENDIF
    xor eax,eax
    ret
    WndProc endp

    上面可以说是关键部分。这也是我们写 Windows 程序时需要改写的主要部分。此处您的程序检查 Windows 传递过来的消息,如果是我们感兴趣的消息则加以处理,处理完后,在 eax 寄存器中传递 0,否则必须调用 DefWindowProc,把该窗口过程接收到的参数传递给缺省的窗口处理函数。所有消息中您必须处理的是 WM_DESTROY,当您的应用程序结束时 Windows 把这个消息传递进来,当您的应用程序解说到该消息时它已经在屏幕上消失了,这仅是通知您的应用程序窗口已销毁,您必须自己准备返回 Windows 。在此消息中您可以做一些清理工作,但无法阻止退出应用程序。如果您要那样做的话,可以处理 WM_CLOSE 消息。在处理完清理工作后,您必须调用 PostQuitMessage,该函数会把 WM_QUIT 消息传回您的应用程序,而该消息会使得 GetMessage 返回,并在 eax 寄存器中放入 0,然后会结束消息循环并退回 WINDOWS。您可以在您的程序中调用 DestroyWindow 函数,它会发送一个 WM_DESTROY 消息给您自己的应用程序,从而迫使它退出。

     

     

  • 2
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值