说明简介

Win32汇编编程系列

声明：

本帮助文件由yyc个人整理后出版，内容全摘自网络。版权归实际作者所有。

为了让网友们能更好的学习Win32汇编编程，于是我为大家将网上的资料整理成一个帮助文件。

本帮助文件包括：Iczelion的Win32汇编教程、Iczelion 的 ODBC 教程、Iczelion 的 VxD 教程共三部分

作者简介：

姓名：yyc
网名：yyc
E-mail：yycmail@263.net
主页：http://yycnet.yeah.net/

mouse.gif (9455 字节) 版权所有：Copyright [C] 2001-2002 yyc
2001-07-12


Iczelion 的 Win32asm 教程

第一课 基本概念

我们先假设您已知道了如何使用MASM。如果您还不知道的话，请下载 win32asm.exe ，并请仔细研读其中所附带的文档资料。好，如果您已准备就绪，我们这就开始吧!

理论：

WIN32 程序运行在保护模式下的，保护模式的历史可以追溯到 80286。而今 80286 已成为了历史。所以我们将只把精力集中于 80386 及后续的X86 系列 CPU。Windows 把每一个 Win32 应用程序放到分开的虚拟地址空间中去运行，也就是说每一个应用程序都拥有其相互独立的 4GB 地址空间，当然这倒不是说它们都拥有 4GB 的物理地址空间，而只是说能够在 4GB 的范围内寻址。操作系统将会在应用程序运行时完成 4GB 的虚拟地址和物理内存地址间的转换。这就要求编写应用程序时必须格守 Windows 的规范，否则极易引起内存的保护模式错误。而过去的 Win16 内存模式下，所有的应用程序都运行于同一个 4GB 地址空间，它们可以彼此"看"到别的程序的内容，这极易导致一个应用程序破坏另一个应用程序甚至是操作系统的数据或代码。
和 16 位 Windows 下的把代码分成 DATA，CODE 等段的内存模式不同，WIN32 只有一种内存模式，即 FLAT 模式，意思是"平坦"的内存模式，再没有 64K 的段大小限制，所有的 WIN32 的应用程序运行在一个连续、平坦、巨大的 4GB 的空间中。这同时也意味着您无须和段寄存器打交道，您可以用任意的段寄存器寻址任意的地址空间，这对于程序员来说是非常方便的，这也使得用32位汇编语言和用C语言一样方便。 在Win32下编程，有许多重要的规则需要遵守。有一条很重要的是：Windows 在内部频繁使用 ESI，EDI，EBP，EBX 寄存器，而且并不去检测这些寄存器的值是否被更改，这样当您要使用这些寄存器时必须先保存它们的值，待用完后再恢复它们，一个最显著的应用例子就是 Windows 的 CallBack 函数中。

内容：

下面的程序段是一个框架， 若您现在还不知道这些指令的确切意义的话，没关系， 随后我就会给大家详细解释。

.386
.MODEL Flat, STDCALL
.DATA
<Your initialized data>
......
.DATA?
<Your uninitialized data>
......
.CONST
<Your constants>
......
.CODE
<label>
<Your code>
.....
end <label>

框架就这么简单，好，我现在就给您解释：

.386
这是一个汇编语言伪指令，他告诉编译器我们的程序是使用80386指令集编写的。您还可以使用 .486、.586， 但最安全的还是使用.386。对于每一种CPU有两套几乎功能相同伪指令： .386/.386P、 486/.486P、 586/.586P。 带P的指令标明您的程序中可以用特权级指令。特权级指令是保留给操作系统的，如虚拟设备驱动程序。在大多数时间，您的程序都无须运行在RING0层，故用不带后缀P的伪指令已足够了。

.MODEL FLAT，STDCALL
.MODEL 是用来指定内存模式的伪指令，在Win32下，只有一种内存模型，那就是FLAT。 STDCALL 告诉编译器参数的传递约定。参数的传递约定是指参数传达时的顺序(从左到右或从右到左)和由谁恢复堆栈指针(调用者或被调用者)。在Win16下有两种约定：C 和 PASCAL。C 约定规定参数传递顺序是从右到左，即最右边的参数最先压栈，由调用者恢复堆栈指针。

例如：为调用函数 foo ( int first_param， int second_param， int third_param )； 按C约定的汇编代码应该是这样的：

push [third_param]
push [second_param]
push [first_param]
call foo
add esp， 3 * 4 ;调用者自己恢复堆栈指针

PASCAL约定和C约定正好相反，它规定参数是从左向右传递，由被调用者恢复堆栈。Win16采用了PASCAL约定， 因为PASCAL约定产生的代码量要小。当不知道参数的个数时，C约定特别有用。如在函数wsprintf () 中， wsprintf预先并不知道要传递几个参数，所以它不知道如何恢复堆栈。STDCALL是C约定和PASCAL约定的混合体，它规定参数的传递是从右到左，恢复堆栈的工作交由被调用者。Win32只用STDCALL约定，但除了一个特例，即：wsprintf。

.DATA .DATA? .CONST .CODE
上面的四个伪指令是"分段"(SECTION)伪指令。我们上面刚讲过Win32下没有"段"(SEGMENT)的概念，但是您可以把您的程序分成不同的"分段"， 一个"分段"的开始即是上一个"分段"的结束。WIN32中只有两种性质的"分段"：DATA和CODE。
其中DATA"分段"又分为三种：
.DATA 其中包括已初始化的数据。
.DATA? 其中包括未初始化的数据。比如有时您仅想预先分配一些内存但并不想指定初始值。使用未初始化的数据的优点是它不占据可执行文件的大小，如：若您要在 .DATA? 段中分配10，000字节的空间，您的可执行文件的大小无须增加10，000字节，而仅仅是要告诉编译器在装载可执行文件时分配所需字节。
.CONST 其中包括常量定义。这些常量在程序运行过程中是不能更改的。 应用程序并不需要以上所有的三个"分段"， 可以根据需要进行定义。
.CODE 这是代码"分段"。
<译者注：实际上，分段并不是象在 Dos 下一样，为不同的段分别指出不同的段寄存器，因为 Windows 下只有一个 4GB 的段，Windows 程序中的分段表现在当程序装载时，赋予不同的分段不同的属性，比如说当你的程序加载时，对于 Ring3 程序来说，.code 段是不可写的，而 .data 段是可写的，如果你尝试象在 Dos 下一样写自己的代码部分，你会得到一个蓝屏错误>

<label>
end <label>
是用来唯一标识您的代码范围的标签， 两个标签必须相同，应用程序的所有可执行代码必修在两个标签之间。

翻译：Lxx，校对：LuoYunBin's Win32 ASM Page，http://asm.yeah.net


Iczelion 的 Win32asm 教程

第二课 消息框

在本课中，我们将用汇编语言写一个 Windows 程序，程序运行时将弹出一个消息框并显示"Win32 assembly is great!"。

理论：

Windows 为编写应用程序提供了大量的资源。其中最重要的是Windows API (Application Programming Interface)。 Windows API是一大组功能强大的函数，它们本身驻扎在 Windows 中供人们随时调用。这些函数的大部分被包含在几个动态链接库(DLL)中，譬如：kernel32.dll、 user32.dll 和 gdi32.dll。 Kernel32.dll中的函数主要处理内存管理和进程调度；user32.dll中的函数主要控制用户界面；gdi32.dll中的函数则负责图形方面的操作。除了上面主要的三个动态链接库，您还可以调用包含在其他动态链接库中的函数，当然您必须要有关于这些函数的足够的资料。

动态链接库，顾名思义，这些 API 的代码本身并不包含在 Windows 可执行文件中，而是当要使用时才被加载。为了让应用程序在运行时能找到这些函数，就必须事先把有关的重定位信息嵌入到应用程序的可执行文件中。这些信息存在于引入库中，由链接器把相关信息从引入库中找出插入到可执行文件中。您必须指定正确的引入库，因为只有正确的引入库才会有正确的重定位信息。

当应用程序被加载时 Windows 会检查这些信息，这些信息包括动态链接库的名字和其中被调用的函数的名字。若检查到这样的信息，Windows 就会加载相应的动态链接库，并且重定位调用的函数语句的入口地址，以便在调用函数时控制权能转移到函数内部。

如果从和字符集的相关性来分，API 共有两类：一类是处理 ANSI 字符集的，另一类是处理 UNICODE 字符集的。前一类函数名字的尾部带一个"A"字符，处理UNICODE的则带一个"W"字符(我想"W"也许是代表宽字符的意思吧)。我们比较熟悉的ANSI字符串是以 NULL 结尾的一串字符数组，每一个ANSI字符是一个 BYTE 宽。对于欧洲语言体系，ANSI 字符集已足够了，但对于有成千上万个唯一字符的几种东方语言体系来说就只有用 UNICODE 字符集了。每一个 UNICODE 字符占有两个 BYTE 宽，这样一来就可以在一个字符串中使用 65336 个不同字符了。

这也是为什么引进 UNICODE 的原因。在大多数情况下我们都可以用一个包含头文件，在其中定义一个宏，然后在实际调用函数时，函数名后不需要加后缀"A"或"W"。
<译者注：如在头文件中定义函数foo()；
#ifdef UNICODE
#define foo() fooW()
#else
#define foo() fooA()
#endif
>

例子：

我先把框架程序放在下面，然后我们再向里面加东西。

.386
.model flat， stdcall
.data
.code
start：
end start

应用程序的执行是从 END

ExitProcess proto uExitCode：DWORD

上面一行是函数原型。函数原型会告诉编译器和链接器该函数的属性，这样在编译和链接时，编译器和链接器就会作相关的类型检查。 函数的原型定义如下：

FunctionName PROTO [ParameterName]：DataType，[ParameterName]：DataType，...

简言之，就是在函数名后加伪指令PROTO，再跟一串由逗号相隔的数据类型链表。在前面的 ExitProcess 定义中，该函数有一个 DWORD 类型的参数。当您使用高层调用语句 INVOKE 时，使用函数原型定义特别有用，您可以简单地认为 INVOKE 是一个有参数类型检查的调用语句。譬如，假设您这样写：

call ExitProcess

若您事先没把一个DWORD类型参数压入堆栈，编译器和链接器都不会报错，但毫无疑问，在您的程序运行时将引起崩溃。但是，当您这样写：

invoke ExitProcess

连接器将报错提醒您忘记压入一个 DWORD 类型参数。所以我建议您用 INVOKE 指令而不是CALL去调用一个函数。INVOKE 的语法如下：

INVOKE expression [，arguments]

expression 既可以是一个函数名也可以是一个函数指针。参数由逗号隔开。大多数API函数的原型放在头文件中。 如果您用的是 hutch 的 MASM32，这些头文件在文件夹MASM32/include 下， 这些头文件的扩展名为 INC，函数名和 DLL 中的函数名相同，譬如：KERNEL32.LIB 引出的函数 ExitProcess 的函数原形声明于kernel.inc中。您也可以自己声明函数原型。 在我的教学课程中都使用 hutch 的windows。inc，这些头文件您可以从http：//win32asm.cjb.net下载。

好，我们现在回到ExitProcess 函数，参数uExitCode 是您希望当您的应用程序结束时传递 Windows 的。 您可以这样写：

invoke ExitProcess，0

把这一行放到开始标识符下，这个应用程序就会立即退出 Windows，当然毫无疑问个应用程序本身是一个完整的 Windows 程序。

.386
.model flat， stdcall
option casemap：none

include \masm32\include\windows.inc
include \masm32\include\kernel32.inc
includelib \masm32\lib\kernel32.lib

.data
.code
start：
invoke ExitProcess，0
end start

option casemap：none 一句的意思是告诉 MASM 要区分标号的大小写，譬如：start 和 START 是不同的。请注意新的伪指令 include，跟在其后的文件名所指定的文件在编译时将“插”在该处。在我们上面的程序段中，当MASM处理到语句 include \masm\include\windows.inc 时，它就会打开文件夹\MASM32\include 中的文件windows.inc，这和您把整个文件都粘贴到您的源程序中的效果是一样的。 hutch 的 windows.inc 包含了 WIN32 编程所需要的常量和结构体的定义。 但是它不包含函数原型的定义。尽管 hutch 和我尽力包含所有的常量和结构体的定义，但仍会有不少遗漏，为此我们将不断加入新的内容。请随时注意我们主页，下载最新的头文件。

您的应用程序除了从 windows.inc 中得到相关变量结构体的定义外，还需要从其他的头文件中得到函数原型的声明，这些头文件都放在 \masm32\include 文件夹中。 在我们上面的例子中调用了驻扎在 kernel.dll 中的函数，所以需要包含有这个函数原型声明的头文件 kernel.inc。如果用文本编辑器打开该文件您会发现里面全是从 kernel.dll中引出的函数的声明。如果您不包含kernel.inc，您仍然可以调用（call）ExitProcess，但不能够调用（invoke）ExitProcess（这会无法通过编译器和连接器的参数合法性检查）。所以若用 invoke 去调用一个函数，您就必须事先声明，当然不一定要包含我们的头文件，您完全可以在调用该函数前在源代码的适当位置进行声名。包含头文件主要是为了节省时间（译者：当然还有正确性）

接下来我们来看看 includelib 伪指令，和 include 不同，它仅仅是告诉编译器您的程序引用了哪个库。当编译器处理到该指令时会在生成的目标文件中插入链接命令告诉链接器链入什么库。当然您还可以通过在链接器的命令行指定引入库名称的方法来达到和用includelib指令相同的目的，但考虑到命令行仅能够传递128个字符而且要不厌其烦地在命令行敲字符，所以这种方法是非常不可取的。

好了，现在保存例子，取名为msgbox.asm。把 ml.exe 的路径放到 PATH 环境变量中，键入下面一行 进行编译：

ml /c /coff /Cp msgbox。asm （译者注：命令行参数大小写是有区别的）

  • /c 是告诉MASM只编译不链接。这主要是考虑到在链接前您可能还有其他工作要做。
  • /coff 告诉MASM产生的目标文件用 coff 格式。MASM 的 coff 格式是COFF（Common Object File Format：通用目标文件格式） 格式的一种变体。在 UNIX 下的 COFF 格式又有不同。
  • /Cp 告诉 MASM 不要更改用户定义的标识符的大小写。若您用的是 hutch 的包含文件的话，在.model 指令下加入 "option casemap：none" 语句，可达到同样的效果。

当您成功的编译了 msgbox.asm 后，编译器会产生 msgbox.obj 目标文件，目标文件和可执行文件只一步之遥，目标文件中包含了以二进制形式存在的指令和数据，比可执行文件相差的只是链接器加入的重定位信息。

好，我们来链接目标文件：

link /SUBSYSTEM：WINDOWS /LIBPATH：c：\masm32\lib msgbox.obj

  • /SUBSYSTEM：WINDOWS 告诉链接器可执行文件的运行平台
  • /LIBPATH：〈path to import library〉 告诉链接器引入库的路径。

链接器做的工作就是根据引入库往目标文件中加入重定位信息，最后产生可执行文件。 既然得到了可执行文件，我们来运行一下。好，一、二、三，GO！屏幕上什么都没有。哦，对了，我们除了调用了 ExitProcess 函数外，甚麽都还没做呢！但是别一点成就感都没有哦，因为我们用汇编所写的是一个真正 Windows 程序，不信的话，查查您磁盘上的 msgbox.exe文件，在我的机器上它的大小足有1，536字节呢。

下面我们来做一点可以看的见摸的着的，我们在程序中加入一个对话框。该函数的原型如下：

MessageBox PROTO hwnd：DWORD， lpText：DWORD， lpCaption：DWORD， uType：DWORD

  • hWnd 是父窗口的句柄。句柄代表您引用的窗口的一个地址指针。它的值对您编 Windows 程序并不重要（译者注：如果您想成为高手则是必须的），您只要知道它代表一个窗口。当您要对窗口做任何操作时，必须要引用该窗口的指针。
  • lpText 是指向您要显示的文本的指针。指向文本串的指针事实上就是文本串的首地址。
  • lpCaption 是指向您要显示的对话框的标题文本串指针。
  • uType 是显示在对话框窗口上的小图标的类型。

下面是源程序

.386
.model flat,stdcall
option casemap:none
include \masm32\include\windows.inc
include \masm32\include\kernel32.inc
includelib \masm32\lib\kernel32.lib
include \masm32\include\user32.inc
includelib \masm32\lib\user32.lib

.data
MsgBoxCaption db "Iczelion Tutorial No.2",0
MsgBoxText db "Win32 Assembly is Great!",0

.code
start:
invoke MessageBox, NULL, addr MsgBoxText, addr MsgBoxCaption, MB_OK
invoke ExitProcess, NULL
end start

编译、链接上面的程序段，得到可执行文件。运行，哈哈，窗口上弹出了一个对话框，上面有一行字：“Win32 Assembly is Great！”。想一想，我们是用汇编写出来的，所以我们有理由为编写了一个最简单的 WIN32 程序感到高兴。（译者注：如果明天我们能够像在 DOS 下那样每一行都用汇编写，那我们有理由为自己感到自豪。）

好，我们回过头来看看上面的源代码。我们在.DATA“分段”定义了两个NULL结尾的字符串。我们用了两个常量：NULL 和 MB_OK。这些常量在windows.inc 文件中有定义，使用常量使得您的程序有较好的可读性。 addr　操作符用来把标号的地址传递给被调用的函数，它只能用在 invoke 语句中，譬如您不能用它来把标号的地址赋给寄存器或变量，如果想这样做则要用 offset 操作符。在 offset 和 addr 之间有如下区别：

  1. addr不可以处理向前引用，offset则能。所谓向前引用是指：标号的定义是在invoke 语句之后，譬如在如下的例子：
    invoke MessageBox，NULL， addr MsgBoxText，addr MsgBoxCaption，MB_OK

    ......

    MsgBoxCaption db "Iczelion Tutorial No.2"，0
    MsgBoxText db "Win32 Assembly is Great!"，0

    如果您是用 addr 而不是 offset 的话，那 MASM 就会报错。
  2. addr可以处理局部变量而 offset 则不能。局部变量只是在运行时在堆栈中分配内存空间。而 offset 则是在编译时由编译器解释，这显然不能用offset 在运行时来分配内存空间。编译器对 addr 的处理是先检查处理的是全局还是局部变量，若是全局变量则把其地址放到目标文件中，这一点和 offset 相同，若是局部变量，就在执行 invoke 语句前产生如下指令序列：
    lea eax， LocalVar
    push eax
    因为lea指令能够在运行时决定标号的有效地址，所以有了上述指令序列，就可以保证 invoke 的正确执行了。

翻译：Lxx，校对：LuoYunBin's Win32 ASM Page，http://asm.yeah.net


Iczelion 的 Win32asm 教程

第三课 创建简单的窗口

在本课中我们将写一个 Windows 程序，它会在桌面显示一个标准的窗口。

理论：

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

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

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

相对于单用户的 DOS 下的编程来说，Windows 下的程序框架结构是相当复杂的。但是 Windows 和 DOS 在系统架构上是截然不同的。Windows 是一个多任务的操作系统，故系统中同时有多个应用程序彼此协同运行。这就要求 Windows 程序员必须严格遵守编程规范，并养成良好的编程风格。

内容：

下面是我们简单的窗口程序的源代码。在进入复杂的细节前，我将提纲挈领地指出几点要点：

  1. 您应当把程序中要用到的所有常量和结构体的声明放到一个头文件中，并且在源程序的开始处包含这个头文件。这么做将会节省您大量的时间，也免得一次又一次的敲键盘。目前，最完善的头文件是 hutch 写的，您可以到 hutch 或我的网站下载。您也可以定义您自己的常量和结构体，但最好把它们放到独立的头文件中
  2. 用 includelib 指令，包含您的程序要引用的库文件，譬如：若您的程序要调用 "MessageBox"， 您就应当在源文件中加入如下一行： includelib user32.lib 这条语句告诉 MASM 您的程序将要用到一些引入库。如果您不止引用一个库，只要简单地加入 includelib 语句，不要担心链接器如何处理这么多的库，只要在链接时用链接开关 /LIBPATH 指明库所在的路径即可。
  3. 在其它地方运用头文件中定义函数原型，常数和结构体时，要严格保持和头文件中的定义一致，包括大小写。在查询函数定义时，这将节约您大量的时间；
  4. 在编译，链接时用makefile文件，免去重复敲键。

.386
.model flat,stdcall
option casemap:none
include \masm32\include\windows.inc
include \masm32\include\user32.inc
includelib \masm32\lib\user32.lib ; calls to functions in user32.lib and kernel32.lib
include \masm32\include\kernel32.inc
includelib \masm32\lib\kernel32.lib

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

.DATA ; initialized data
ClassName db "SimpleWinClass",0 ; the name of our window class
AppName db "Our First Window",0 ; the name of our window

.DATA? ; Uninitialized data
hInstance HINSTANCE ? ; Instance handle of our program
CommandLine LPSTR ?
.CODE ; Here begins our code
start:
invoke GetModuleHandle, NULL ; get the instance handle of our program.
; Under Win32, hmodule==hinstance mov hInstance,eax
mov hInstance,eax
invoke GetCommandLine ; get the command line. You don't have to call this function IF
; your program doesn't process the command line.
mov CommandLine,eax
invoke WinMain, hInstance,NULL,CommandLine, SW_SHOWDEFAULT ; call the main function
invoke ExitProcess, eax ; quit our program. The exit code is returned in eax from WinMain.

WinMain proc hInst:HINSTANCE,hPrevInst:HINSTANCE,CmdLine:LPSTR,CmdShow:DWORD
LOCAL wc:WNDCLASSEX ; create local variables on stack
LOCAL msg:MSG
LOCAL hwnd:HWND

mov wc.cbSize,SIZEOF WNDCLASSEX ; fill values in members of wc
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 wc ; register our window class
invoke CreateWindowEx,NULL,\
ADDR ClassName,\
ADDR AppName,\
WS_OVERLAPPEDWINDOW,\
CW_USEDEFAULT,\
CW_USEDEFAULT,\
CW_USEDEFAULT,\
CW_USEDEFAULT,\
NULL,\
NULL,\
hInst,\
NULL
mov hwnd,eax
invoke ShowWindow, hwnd,CmdShow ; display our window on desktop
invoke UpdateWindow, hwnd ; refresh the client area

.WHILE TRUE ; Enter message loop
invoke GetMessage, ADDR msg,NULL,0,0
.BREAK .IF (!eax)
invoke TranslateMessage, ADDR msg
invoke DispatchMessage, ADDR msg
.ENDW
mov eax,msg.wParam ; return exit code in eax
ret
WinMain endp

WndProc proc hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM
.IF uMsg==WM_DESTROY ; if the user closes our window
invoke PostQuitMessage,NULL ; quit our application
.ELSE
invoke DefWindowProc,hWnd,uMsg,wParam,lParam ; Default message processing
ret
.ENDIF
xor eax,eax
ret
WndProc endp

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
include \masm32\include\kernel32.inc
includelib \masm32\lib\user32.lib
includelib \masm32\lib\kernel32.lib

您可以把前三行看成是"必须"的.

.386告诉MASN我们要用80386指令集。
. model flat，stdcall告诉MASM 我们用的内存寻址模式，此处也可以加入stdcall告诉MASM我们所用的参数传递约定。

接下来是函数 WinMain 的原型申明，因为我们稍后要用到该函数，故必须先声明。我们必须包含 window.inc 文件，因为其中包含大量要用到的常量和结构的定义，该文件是一个文本文件，您可以用任何文本编辑器打开它， window.inc还没有包含所有的常量和结构定义，不过 hutch 和我一直在不断加入新的内容。如果暂时在 window.inc 找不到，您也可以自行加入。

我们的程序调用驻扎在 user32.dll (譬如：CreateWindowEx， RegisterWindowClassEx) 和 kernel32.dll (ExitProcess)中的函数，所以必须链接这两个库。接下来我如果问：您需要把什么库链入您的程序呢 ? 答案是：先查到您要调用的函数在什么库中，然后包含进来。譬如：若您要调用的函数在 gdi32.dll 中，您就要包含gdi32.inc头文件。和 MASM 相比，TASM 则要简单得多，您只要引入一个库，即：import32.lib。<译者注：但 Tasm5 麻烦的是 windows.inc 非常的不全面，而且如果在 Windows.inc 中包含全部的 API 定义会内存不够，所以每次你得把用到的 API 定义拷贝出来>

.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 的别名，之所以要这么重新定仅是为了易记。您可以查看 windows.inc 文件，在 .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

.DATA "分段"包含了您应用程序的所有代码，这些代码必须都在 .code 和 end 之间。至于 label 的命名只要遵从 Windows 规范而且保证唯一则具体叫什么倒是无所谓。我们程序的第一条语句是调用 GetModuleHandle 去查找我们应用程序的句柄。在Win32下，应用程序的句柄和模块的句柄是一样的。您可以把实例句柄看成是您的应用程序的 ID 号。我们在调用几个函数是都把它作为参数来进行传递，所以在一开始便得到并保存它就可以省许多的事。

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

WIN32 中函数的函数如果有返回值，那它是通过 eax 寄存器来传递的。其他的值可以通过传递进来的参数地址进行返回。一个 WIN32 函数被调用时总会保存好段寄存器和 ebx，edi，esi和ebp 寄存器，而 ecx和edx 中的值总是不定的，不能在返回是应用。特别注意：从 Windows API 函数中返回后，eax，ecx，edx 中的值和调用前不一定相同。当函数返回时，返回值放在eax中。如果您应用程序中的函数提供给 Windows 调用时，也必须尊守这一点，即在函数入口处保存段寄存器和 ebx，esp，esi，edi 的值并在函数返回时恢复。如果不这样一来的话，您的应用程序很快会崩溃。从您的程序中提供给 Windows 调用的函数大体上有两种：Windows 窗口过程和 Callback 函数。

如果您的应用程序不处理命令行那么就无须调用 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：WNDCLASSEX LOCAL msg：MSG LOCAL hwnd：HWND LOCAL 伪指令为局部变量在栈中分配内存空间，所有的 LOCAL 指令必须紧跟在 PROC 之后。LOCAL 后跟声明的变量，其形式是 变量名:变量类型。譬如 LOCAL wc：WNDCLASSEX 即是告诉 MASM 为名字叫 wc 的局部边量在栈中分配长度为 WNDCLASSEX 结构体长度的内存空间，然后我们在用该局部变量是无须考虑堆栈的问题，考虑到 DOS 下的汇编，这不能不说是一种恩赐。不过这就要求这样申明的局部变量在函数结束时释放栈空间，(也即不能在函数体外被引用)，另一个缺点是您因不能初始化您的局部变量，不得不在稍后另外再对其赋值。

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），一个窗口类就是一个有关窗口的规范，这个规范定义了几个主要的窗口的元素，如：图标、光标、背景色、和负责处理该窗口的函数。您产生一个窗口时就必须要有这样的一个窗口类。如果您要产生不止一个同种类型的窗口时，最好的方法就是把这个窗口类存储起来，这种方法可以节约许多的内存空间。也许今天您不会太感觉到，可是想想以前 PC 大多数只有 1M 内存时，这么做是非常有必要的。如果您要定义自己的创建窗口类就必须：在一个 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

是我们的窗口处理函数。您可以随便给该函数命名。其中第一个参数 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 消息给您自己的应用程序，从而迫使它退出。

翻译：Lxx，校对：LuoYunBin's Win32 ASM Page，http://asm.yeah.net


Iczelion 的 Win32asm 教程

第四课 绘制文本

本课中，我们将学习如何在窗口的客户区“绘制”字符串。我们还将学习关于“设备环境”的概念。

理论：

Windows 中的文本是一个GUI（图形用户界面）对象。每一个字符实际上是由许多的像素点组成，这些点在有笔画的地方显示出来，这样就会出现字符。这也是为什么我说“绘制”字符，而不是写字符。通常您都是在您应用程序的客户区“绘制”字符串（尽管您也可以在客户区外“绘制”）。Windows 下的“绘制”字符串方法和 Dos 下的截然不同，在 Dos 下，您可以把屏幕想象成 85 x 25 的一个平面，而 Windows 下由于屏幕上同时有几个应用程序的画面，所以您必须严格遵从规范。Windows 通过把每一个应用程序限制在他的客户区来做到这一点。当然客户区的大小是可变的，您随时可以调整。

在您在客户区“绘制”字符串前，您必须从 Windows 那里得到您客户区的大小，确实您无法像在 DOS 下那样随心所欲地在屏幕上任何地方“绘制”，绘制前您必须得到 Windows 的允许，然后 Windows 会告诉您客户区的大小，字体，颜色和其它 GUI 对象的属性。您可以用这些来在客户区“绘制”。

什么是“设备环境”（DC）呢？ 它其实是由 Windows 内部维护的一个数据结构。一个“设备环境”和一个特定的设备相连。像打印机和显示器。对于显示器来说，“设备环境”和一个个特定的窗口相连。

“设备环境”中的有些属性和绘图有关，像：颜色，字体等。您可以随时改动那些缺省值，之所以保存缺省值是为了方便。您可以把“设备环境”想象成是Windows 为您准备的一个绘图环境，而您可以随时根据需要改变某些缺省属性。

当应用程序需要绘制时，您必须得到一个“设备环境”的句柄。通常有几种方法。

  • 在 WM_PAINT 消息中使用 call BeginPaint
  • 在其他消息中使用 call GetDC
  • call CreateDC 建立你自己的 DC

您必须牢记的是，在处理单个消息后你必须释放“设备环境”句柄。不要在一个消息处理中获得 “设备环境”句柄，而在另一个消息处理中在释放它。

我们在Windows 发送 WM_PAINT 消息时处理绘制客户区，Windows 不会保存客户区的内容，它用的是方法是“重绘”机制（譬如当客户区刚被另一个应用程序的客户区覆盖），Windows 会把 WM_PAINT 消息放入该应用程序的消息队列。重绘窗口的客户区是各个窗口自己的责任，您要做的是在窗口过程处理 WM_PAINT 的部分知道绘制什么和何如绘制。

您必须了解的另一个概念是“无效区域”。Windows 把一个最小的需要重绘的正方形区域叫做“无效区域”。当 Windows 发现了一个”无效区域“后，它就会向该应用程序发送一个 WM_PAINT 消息，在 WM_PAINT 的处理过程中，窗口首先得到一个有关绘图的结构体，里面包括无效区的坐标位置等。您可以通过调用BeginPaint 让“无效区”有效，如果您不处理 WM_PAINT 消息，至少要调用缺省的窗口处理函数 DefWindowProc ，或者调用 ValidateRect 让“无效区”有效。否则您的应用程序将会收到无穷无尽的 WM_PAINT 消息。

下面是响应该消息的步骤：

  1. 取得“设备环境”句柄
  2. 绘制客户区
  3. 释放“设备环境”句柄

注意，您无须显式地让“无效区”有效，这个动作由 BeginPaint 自动完成。您可以在 BeginPaint 和 Endpaint 之间，调用所有的绘制函数。几乎所有的GDI 函数都需要“设备环境”的句柄作为参数。

内容：

我们将写一个应用程序，它会在客户区的中心显示一行 "Win32 assembly is great and easy!"

.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

.DATA
ClassName db "SimpleWinClass",0
AppName db "Our First Window",0
OurText db "Win32 assembly is great and easy!",0

.DATA?
hInstance HINSTANCE ?
CommandLine LPSTR ?

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

WinMain proc hInst:HINSTANCE, hPrevInst:HINSTANCE, CmdLine:LPSTR, CmdShow:DWORD
LOCAL wc:WNDCLASSEX
LOCAL msg:MSG
LOCAL hwnd:HWND
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 hInst
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 wc
invoke CreateWindowEx,NULL,ADDR ClassName,ADDR AppName,\
WS_OVERLAPPEDWINDOW,CW_USEDEFAULT,\
CW_USEDEFAULT,CW_USEDEFAULT,CW_USEDEFAULT,NULL,NULL,\
hInst,NULL
mov hwnd,eax
invoke ShowWindow, hwnd,SW_SHOWNORMAL
invoke UpdateWindow, hwnd
.WHILE TRUE
invoke GetMessage, ADDR msg,NULL,0,0
.BREAK .IF (!eax)
invoke TranslateMessage, ADDR msg
invoke DispatchMessage, ADDR msg
.ENDW
mov eax,msg.wParam
ret
WinMain endp

WndProc proc hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM
LOCAL hdc:HDC
LOCAL ps:PAINTSTRUCT
LOCAL rect:RECT
.IF uMsg==WM_DESTROY
invoke PostQuitMessage,NULL
.ELSEIF uMsg==WM_PAINT
invoke BeginPaint,hWnd, ADDR ps
mov hdc,eax
invoke GetClientRect,hWnd, ADDR rect
invoke DrawText, hdc,ADDR OurText,-1, ADDR rect, \
DT_SINGLELINE or DT_CENTER or DT_VCENTER
invoke EndPaint,hWnd, ADDR ps
.ELSE
invoke DefWindowProc,hWnd,uMsg,wParam,lParam
ret
.ENDIF
xor eax, eax
ret
WndProc endp
end start

分析：

这里的大多数代码和第三课中的一样。我只解释其中一些不相同的地方。

LOCAL hdc：HDC
LOCAL ps：PAINTSTRUCT
LOCAL rect：RECT

这些局部变量由处理 WM_PAINT 消息中的 GDI 函数调用。hdc 用来存放调用 BeginPaint 返回的“设备环境”句柄。ps 是一个 PAINTSTRUCT 数据类型的变量。通常您不会用到其中的许多值，它由 Windows 传递给 BeginPaint，在结束绘制后再原封不动的传递给 EndPaint。rect 是一个 RECT 结构体类型参数，它的定义如下：

RECT Struct left LONG ?
top LONG ?
right LONG ?
bottom LONG ?
RECT ends

left 和 top 是正方形左上角的坐标。right 和 bottom 是正方形右下角的坐标。客户区的左上角的坐标是 x=0，y=0，这样对于 x=0，y=10 的坐标点就在它的下面。

invoke BeginPaint，hWnd， ADDR ps
mov hdc，eax
invoke GetClientRect，hWnd， ADDR rect
invoke DrawText， hdc，ADDR OurText，-1， ADDR rect， \
DT_SINGLELINE or DT_CENTER or DT_VCENTER
invoke EndPaint，hWnd， ADDR ps

在处理 WM_PAINT 消息时，您调用BeginPaint函数，传给它一个窗口句柄和未初始化的 PAINTSTRUCT 型参数。调用成功后在 eax 中返回“设备环境”的句柄。下一次，调用 GetClientRect 以得到客户区的大小，大小放在 rect 中，然后把它传给 DrawText。DrawText 的语法如下：

DrawText proto hdc：HDC， lpString：DWORD， nCount：DWORD， lpRect：DWORD， uFormat：DWORD

DrawText是一个高层的调用函数。它能自动处理像换行、把文本放到客户区中间等这些杂事。所以您只管集中精力“绘制”字符串就可以了。我们会在下一课中讲解低一层的函数 TextOut，该函数在一个正方形区域中格式化一个文本串。它用当前选择的字体、颜色和背景色。它处理换行以适应正方形区域。它会返回以设备逻辑单位度量的文本的高度，我们这里的度量单位是像素点。让我们来看一看该函数的参数：

  • hdc： “设备环境”的句柄。
  • lpString：要显示的文本串，该文本串要么以NULL结尾，要么在nCount中指出它的长短。
  • nCount：要输出的文本的长度。若以NULL结尾，该参数必须是-1。
  • lpRect： 指向要输出文本串的正方形区域的指针，该方形必须是一个裁剪区，也就是说超过该区域的字符将不能显示。
  • uFormat：指定如何显示。我们可以用 or 把以下标志或到一块：
    • DT_SINGLELINE：是否单行显示。
    • DT_CENTER：是否水平居中。
    • DT_VCENTER ：是否垂直居中。

结束绘制后，必须调用 EndPaint 释放“设备环境”的句柄。 好了，现在我们把“绘制”文本串的要点总结如下：

  1. 必须在开始和结束处分别调用 BeginPaint 和 EndPaint；
  2. 在 BeginPaint 和 EndPaint 之间调用所有的绘制函数；
  3. 如果在其它的消息处理中重新绘制客户区，您可以有两种选择：
    （1）用GetDC和ReleaseDC代替BeginPaint和EndPaint；
    （2）调用InvalidateRect或UpdateWindow让客户区无效，这将迫使WINDOWS把WM_PAINT放入应用程序消息队列，从而使得客户区重绘。

翻译：Lxx，校对：LuoYunBin's Win32 ASM Page，http://asm.yeah.net


Iczelion 的 Win32asm 教程

第五课 学习更多关于“绘制”文本串的知识

我们将做更多的实践去了解有关文本的诸多属性如字体和颜色等。

理论：

Windows 的颜色系统是用RGB值来表示的，R 代表红色，G 代表绿色，B 代表兰色。如果您想指定一种颜色就必须给该颜色赋相关的 RGB 值，RGB 的取值范围都是从 0 到 255，譬如您想要得到纯红色，就必须对RGB赋值（255，0，0），纯白色是 （255，255，255）。从我们下面的例子中您可以看出来要想运用好这套基于数字的颜色系统并不容易，这要求您必须对混色和颜色匹配有良好的感觉。

您可以用函数 SetTextColor 和 SetBkColor 来“绘制”背景色和字符颜色，但是必须传递一个“设备环境”的句柄和 RGB 值作为参数。RGB 的结构体的定义如下：

RGB_value struct
unused db 0
blue db ?
green db ?
red db ?
RGB_value ends

其中第一字节为 0 而且始终为 0，其它三个字节分别表示兰色、绿色和红色，刚好和 RGB 的次序相反。这个结构体用起来挺别扭，所以我们重新定义一个宏用它来代替。该宏接收红绿蓝三个参数，并在 eax 寄存器中返回 32 位的 RGB 值，宏的定义如下：

RGB macro red，green，blue
xor eax，eax
mov ah，blue
shl eax，8
mov ah，green
mov al，red
endm

您可以把该宏放到头文件中以方便使用。

您可以调用 CreateFont 和 CreateFontIndirect 来创建自己的字体，这两个函数的差别是前者要求 您传递一系列的参数，而后着只要传递一个指向 LOGFONT 结构的指针。这样就使得后者使用起来更方便，尤其当您需要频繁创建字体时。在我们的例子中由于只要创建一种字体，故用 CreateFont 就足够了。在调用该函数后会返回所创建的字体的句柄，然后把该句柄选进“设备环境”使其成为当前字体，随后所有的“绘制”文本串的函数在被调用时都要把该句柄作为一个参数传递

例子：

.386
.model flat,stdcall
option casemap:none

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

include \masm32\include\windows.inc
include \masm32\include\user32.inc
include \masm32\include\kernel32.inc
include \masm32\include\gdi32.inc
includelib \masm32\lib\user32.lib
includelib \masm32\lib\kernel32.lib
includelib \masm32\lib\gdi32.lib

RGB macro red,green,blue
xor eax,eax
mov ah,blue
shl eax,8
mov ah,green
mov al,red
endm

.data
ClassName db "SimpleWinClass",0
AppName db "Our First Window",0
TestString db "Win32 assembly is great and easy!",0
FontName db "script",0

.data?
hInstance HINSTANCE ?
CommandLine LPSTR ?

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

WinMain proc hInst:HINSTANCE,hPrevInst:HINSTANCE,CmdLine:LPSTR,CmdShow:DWORD
LOCAL wc:WNDCLASSEX
LOCAL msg:MSG
LOCAL hwnd:HWND
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 hInst
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 wc
invoke CreateWindowEx,NULL,ADDR ClassName,ADDR AppName,\
WS_OVERLAPPEDWINDOW,CW_USEDEFAULT,\
CW_USEDEFAULT,CW_USEDEFAULT,CW_USEDEFAULT,NULL,NULL,\
hInst,NULL
mov hwnd,eax
invoke ShowWindow, hwnd,SW_SHOWNORMAL
invoke UpdateWindow, hwnd
.WHILE TRUE
invoke GetMessage, ADDR msg,NULL,0,0
.BREAK .IF (!eax)
invoke TranslateMessage, ADDR msg
invoke DispatchMessage, ADDR msg
.ENDW
mov eax,msg.wParam
ret
WinMain endp

WndProc proc hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM
LOCAL hdc:HDC
LOCAL ps:PAINTSTRUCT
LOCAL hfont:HFONT

.IF uMsg==WM_DESTROY
invoke PostQuitMessage,NULL
.ELSEIF uMsg==WM_PAINT
invoke BeginPaint,hWnd, ADDR ps
mov hdc,eax
invoke CreateFont,24,16,0,0,400,0,0,0,OEM_CHARSET,\
OUT_DEFAULT_PRECIS,CLIP_DEFAULT_PRECIS,\
DEFAULT_QUALITY,DEFAULT_PITCH or FF_SCRIPT,\
ADDR FontName
invoke SelectObject, hdc, eax
mov hfont,eax
RGB 200,200,50
invoke SetTextColor,hdc,eax
RGB 0,0,255
invoke SetBkColor,hdc,eax
invoke TextOut,hdc,0,0,ADDR TestString,SIZEOF TestString
invoke SelectObject,hdc, hfont
invoke EndPaint,hWnd, ADDR ps
.ELSE
invoke DefWindowProc,hWnd,uMsg,wParam,lParam
ret
.ENDIF
xor eax,eax
ret
WndProc endp

end start

分析：

CreateFont 函数产生一种逻辑字体，它尽可能地接近参数中指定的各相关值。这个函数大概是所有 Windows API函数中所带参数最多的一个。它返回一个指向逻辑字体的句柄供调用 SelectObject 函数使用。下面我们详细讲解该函数的参数：

CreateFont proto \
nHeight：DWORD，\
nWidth：DWORD，\
nEscapement：DWORD，\
nOrientation：DWORD，\
nWeight：DWORD，\
cItalic：DWORD，\
cUnderline：DWORD，\
cStrikeOut：DWORD，\
cCharSet：DWORD，\
cOutputPrecision：DWORD，\
cClipPrecision：DWORD，\
cQuality：DWORD，\
cPitchAndFamily：DWORD，\
lpFacename：DWORD

nHeight： 希望使用的字体的高度，0为缺省。
nWidth： 希望使用的字体的宽度，一般情况下最好用0， 这样 Windows 将会自动为您选择一个和高度匹配的值。因为在我们的例子中那样做的话会使得字符因太小而无法显示，所以 我 们设定它为16。
nEscapement： 每一个字符相对前一个字符的旋转角度，一般设成0。900代表转90度，1800转190度，2700转270度。
nOrientation： 字体的方向。
nWeight： 字体笔画的粗细。

Windows 为我们预定义了如下值：

FW_DONTCARE 等于 0
FW_THIN 等于 100
FW_EXTRALIGHT 等于 200
FW_ULTRALIGHT 等于 200
FW_LIGHT 等于 300
FW_NORMAL 等于 400
FW_REGULAR 等于 400
FW_MEDIUM 等于 500
FW_SEMIBOLD 等于 600
FW_DEMIBOLD 等于 600
FW_BOLD 等于 700
FW_EXTRABOLD 等于 800
FW_ULTRABOLD 等于 800
FW_HEAVY 等于 900
FW_BLACK 等于 900

cItalic： 0为正常，其它值为斜体。
cUnderline： 0为正常，其它值为有下划线。
cStrikeOut： 0为正常，其它值为删除线。
cCharSet： 字体的字符集。一般选择OEM_CHARSET，它使得 Windows 会选用和操作系统相关的字符集。
cOutputPrecision： 指定我们选择的字体接近真实字体的精度。 一般选用OUT_DEFAULT_PRECIS，它决定了缺省的映射方式。
cClipPrecision： 指定我们选择的字体在超出裁剪区域时的裁剪精度。 一般选用CLIP_DEFAULT_PRECIS，它决定了裁剪精度。
cQuality： 指定输出字体的质量。它指出GDI应如何尽可能的接近真实 字体，一共有三种方式：DEFAULT_QUALITY， PROOF_QUALITY 和DRAFT_QUALITY。
cPitchAndFamily：字型和字体家族。
lpFacename： 指定字体的名称。

上面的描述不一定好理解，您如果要的到更多的信息，应参考 WIN32 API 指南。

invoke SelectObject， hdc， eax
mov hfont，eax

在我们得到了指向逻辑字体的句柄后必须调用 SelectObject 函数把它选择进“设备环境”，我们还可以调用该函数把诸如此类的像颜色、笔、画刷 等GDI对象选进“设备环境”。该函数会返回一个旧的“设备环境”的句柄。您必须保存该句柄，以便在完成“绘制”工作后再把它选回。在调用 SelectObject 函数后一切的绘制函数都是针对该“设备环境”的。

RGB 200，200，50
invoke SetTextColor，hdc，eax
RGB 0，0，255
invoke SetBkColor，hdc，eax

我们用宏 RGB 产生颜色，然后分别调用 SetTextColor 和 SetBkColor。

invoke TextOut，hdc，0，0，ADDR TestString，SIZEOF TestString

我们调用 TextOut 在客户区用我们前面选定的字体和颜色“绘制”文本串。

invoke SelectObject，hdc， hfont

在我们“绘制”完成后，必须恢复“设备环境”。我们必须每一次都这么做。

翻译：Lxx，校对：LuoYunBin's Win32 ASM Page，http://asm.yeah.net


Iczelion 的 Win32asm 教程

第六课 处理键盘输入消息

在本课中，我们将要学习WINDOWS程序是如何处理键盘消息的。

理论：

因为大多数的PC只有一个键盘，所以所有运行中的WINDOWS程序必须共用它。WINDOWS 将负责把击键消息送到具有输入焦点的那个应用程序中去。尽管屏幕上可能同时有几个应用程序窗口，但一个时刻仅有一个窗口有输入焦点。有输入焦点的那个应用程序的标题条总是高亮度显示的。 实际上您可以从两个角度来看键盘消息：一是您可以把它看成是一大堆的按键消息的集合，在这种情况下，当您按下一个键时，WINDOWS就会发送一个WM_KEYDOWN给有输入焦点的那个应用程序，提醒它有一个键被按下。当您释放键时，WINDOWS又会发送一个WM_KYEUP消息，告诉有一个键被释放。您把每一个键当成是一个按钮；另一种情况是：您可以把键盘看成是字符输入设备。当您按下“a”键时，WINDOWS发送一个WM_CHAR消息给有输入焦点的应用程序，告诉它“a”键被按下。实际上WINDOWS 内部发送WM_KEYDOWN和WWM_KEYUP消息给有输入焦点的应用程序，而这些消息将通过调用TranslateMessage翻译成WM_CHAR消息。WINDOWS窗口过程函数将决定是否处理所收到的消息，一般说来您不大会去处理WM_KEYDOWN、WM_KEYUP消息，在消息循环中TranslateMessage函数会把上述消息转换成WM_CHAR消息。在我们的课程中将只处理WM_CHAR。

例子：

.386
.model flat,stdcall
option casemap:none

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

include \masm32\include\windows.inc
include \masm32\include\user32.inc
include \masm32\include\kernel32.inc
include \masm32\include\gdi32.inc
includelib \masm32\lib\user32.lib
includelib \masm32\lib\kernel32.lib
includelib \masm32\lib\gdi32.lib

.data
ClassName db "SimpleWinClass",0
AppName db "Our First Window",0
char WPARAM 20h ; the character the program receives from keyboard

.data?
hInstance HINSTANCE ?
CommandLine LPSTR ?

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

WinMain proc hInst:HINSTANCE,hPrevInst:HINSTANCE,CmdLine:LPSTR,CmdShow:DWORD
LOCAL wc:WNDCLASSEX
LOCAL msg:MSG
LOCAL hwnd:HWND
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 hInst
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 wc
invoke CreateWindowEx,NULL,ADDR ClassName,ADDR AppName,\
WS_OVERLAPPEDWINDOW,CW_USEDEFAULT,\
CW_USEDEFAULT,CW_USEDEFAULT,CW_USEDEFAULT,NULL,NULL,\
hInst,NULL
mov hwnd,eax
invoke ShowWindow, hwnd,SW_SHOWNORMAL
invoke UpdateWindow, hwnd
.WHILE TRUE
invoke GetMessage, ADDR msg,NULL,0,0
.BREAK .IF (!eax)
invoke TranslateMessage, ADDR msg
invoke DispatchMessage, ADDR msg
.ENDW
mov eax,msg.wParam
ret
WinMain endp

WndProc proc hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM
LOCAL hdc:HDC
LOCAL ps:PAINTSTRUCT

.IF uMsg==WM_DESTROY
invoke PostQuitMessage,NULL
.ELSEIF uMsg==WM_CHAR
push wParam
pop char
invoke InvalidateRect, hWnd,NULL,TRUE
.ELSEIF uMsg==WM_PAINT
invoke BeginPaint,hWnd, ADDR ps
mov hdc,eax
invoke TextOut,hdc,0,0,ADDR char,1
invoke EndPaint,hWnd, ADDR ps
.ELSE
invoke DefWindowProc,hWnd,uMsg,wParam,lParam
ret
.ENDIF
xor eax,eax
ret
WndProc endp
end start

分析：

char WPARAM 20h ; the character the program receives from keyboard

这个变量将保存从键盘接收到的字符。因为它是在窗口过程中通过WPARAM型变量传送的，所以我们简单地把它定义为WPARAM型。由于我们的窗口在初次刷新时(也即刚被创建的那一次)是没有键盘输入的所以我们把他设成空格符（20h），这样显示时您就什么都看不见。

.ELSEIF uMsg==WM_CHAR
push wParam
pop char
invoke InvalidateRect, hWnd,NULL,TRUE

这一段是用来处理WM_CHAR消息的。它把接收到的字符放入变量char中，接着调用InvalidateRect，而InvalidateRect使得窗口的客户区无效，这样它会发出WM_PAINT消息，而WM_PAINT消息迫使WINDOWS重新绘制它的客户区。该函数的语法如下：

InvalidateRect proto hWnd:HWND,\
lpRect:DWORD,\
bErase:DWORD

lpRect是指向客户区我们想要其无效的一个正方形结构体的指针。如果该值等于NULL，则整个客户区都无效；布尔值bErase告诉WINDOWS是否擦除背景，如果是TRUE，则WINDOWS在调用BeginPaint函数时把背景擦掉。 所以我们此处的做法是：我们将保存所有有关重绘客户区的数据，然后发送WM_PAINT消息，处理该消息的程序段然后根据相关数据重新绘制客户区。尽管这么做事有点像走了弓背，但WINDOWS要处理那么庞大的消息群，没有一定的规矩可不行。 实际上我们完全可以通过调用GetDC 获得设备上下文句柄，然后绘制字符，然后再调用ReleaseDC释放设备上下文句柄，毫无疑问这样也能在客户区绘制出正确的字符。但是如果这之后接收到WM_PAINT消息要处理时，客户区会重新刷新，而我们这稍前所绘制的字符就会消失掉。所以为了让字符一直正确地显示，就必须把它们放到WM_PAINT的处理过程中处理。而在本消息处理中发送WM_PAINT消息即可。

invoke TextOut,hdc,0,0,ADDR char,1

在调用InvalidateRect时，WM_PAINT消息被发送到了WINDOWS窗口处理过程，程序流程转移到处理WM_PAINT消息的程序段，然后调用BeginPaint得到设备上下文的句柄，再调用TextOut在客户区的（0，0）处输出保存的按键字符。这样无论您按什么键都能在客户区的左上角显示，不仅如此，无论您怎么缩放窗口（迫使WINDOWS重新绘制它的客户区），字符都会在正确的地方显示，所以必须把所有重要的绘制动作都放到处理WM_PAINT消息的程序段中去。

翻译：Lxx，校对：LuoYunBin's Win32 ASM Page，http://asm.yeah.net


Iczelion's win32 asm tutorial

第七课 处理鼠标输入消息

本课中我们将学习如何在我们的窗口过程函数中处理鼠标按键消息。示例程序演示了如何等待左键按下消息，我们将在按下的位置显示一个字符串。

理论：

和处理键盘输入一样，WINDOWS将捕捉鼠标动作并把它们发送到相关窗口。这些活动包括左、右键按下、移动、双击等(译者注：新式鼠标还包括滚轮消息WM_WHEEL)。WINDOWS并不像处理键盘输入那样把所有的鼠标消息都导向有输入焦点的窗口，任何鼠标经过的窗口都将接收到鼠标消息，无论有否输入焦点。另外，窗口还会接收到鼠标在非客户区移动的消息（WM_NCMOVE），但大多数的情况下我们都会将其忽略掉。 对鼠标的每一个按钮都有两个消息：WM_LBUTTONDOWN,WM_RBUTTONDOWN 。对于三键鼠标还会有WM_MBUTTONDOWN和WM_MBUTTONUP消息，当鼠标在某窗口客户区移动时，该窗口将接收到WM_MOUSEMOVE消息。一个窗口若想处理WM_LBUTTONDBCLK或 WM_RBUTTONDBCLK，那么它的窗口类必须有CS_DBLCLKS风格，否则它就会接受到一堆的按键起落（WM_XBUTTONDOWN或WM_XBUTTONUP)的消息。 对于所有的消息，窗口过程函数传入的参数lParam包含了鼠标的位置，其中底位为x坐标，高位为y坐标，这些坐标值都是相对于窗口客户区的左上角的值，wParam中则包含了鼠标按钮的状态。

例子：

.386
.model flat,stdcall
option casemap:none

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

include \masm32\include\windows.inc
include \masm32\include\user32.inc
include \masm32\include\kernel32.inc
include \masm32\include\gdi32.inc
includelib \masm32\lib\user32.lib
includelib \masm32\lib\kernel32.lib
includelib \masm32\lib\gdi32.lib

.data
ClassName db "SimpleWinClass",0
AppName db "Our First Window",0
MouseClick db 0 ; 0=no click yet

.data?
hInstance HINSTANCE ?
CommandLine LPSTR ?
hitpoint POINT <>

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

WinMain proc hInst:HINSTANCE,hPrevInst:HINSTANCE,CmdLine:LPSTR,CmdShow:DWORD
LOCAL wc:WNDCLASSEX
LOCAL msg:MSG
LOCAL hwnd:HWND
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 hInst
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 wc
invoke CreateWindowEx,NULL,ADDR ClassName,ADDR AppName,\
WS_OVERLAPPEDWINDOW,CW_USEDEFAULT,\
CW_USEDEFAULT,CW_USEDEFAULT,CW_USEDEFAULT,NULL,NULL,\
hInst,NULL
mov hwnd,eax
invoke ShowWindow, hwnd,SW_SHOWNORMAL
invoke UpdateWindow, hwnd
.WHILE TRUE
invoke GetMessage, ADDR msg,NULL,0,0
.BREAK .IF (!eax)
invoke DispatchMessage, ADDR msg
.ENDW
mov eax,msg.wParam
ret
WinMain endp

WndProc proc hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM
LOCAL hdc:HDC
LOCAL ps:PAINTSTRUCT

.IF uMsg==WM_DESTROY
invoke PostQuitMessage,NULL
.ELSEIF uMsg==WM_LBUTTONDOWN
mov eax,lParam
and eax,0FFFFh
mov hitpoint.x,eax
mov eax,lParam
shr eax,16
mov hitpoint.y,eax
mov MouseClick,TRUE
invoke InvalidateRect,hWnd,NULL,TRUE
.ELSEIF uMsg==WM_PAINT
invoke BeginPaint,hWnd, ADDR ps
mov hdc,eax
.IF MouseClick
invoke lstrlen,ADDR AppName
invoke TextOut,hdc,hitpoint.x,hitpoint.y,ADDR AppName,eax
.ENDIF
invoke EndPaint,hWnd, ADDR ps
.ELSE
invoke DefWindowProc,hWnd,uMsg,wParam,lParam
ret
.ENDIF
xor eax,eax
ret
WndProc endp
end start

分析：

.ELSEIF uMsg==WM_LBUTTONDOWN
mov eax,lParam
and eax,0FFFFh
mov hitpoint.x,eax
mov eax,lParam
shr eax,16
mov hitpoint.y,eax
mov MouseClick,TRUE
invoke InvalidateRect,hWnd,NULL,TRUE

窗口过程处理了WM_LBUTTONDOWN消息，当接收到该消息时，lParam中包含了相对于窗口客户区左上角的坐标，我们把它保存下来，放到一个结构体变量（POINT）中，该结构体变量的定义如下：

POINT STRUCT
x dd ?
y dd ?
POINT ENDS

然后我们把标志量MouseClick设为TRUE，这表明至少有一次在客户区的左键按下消息。

mov eax,lParam
and eax,0FFFFh
mov hitpoint.x,eax

由于lParam是一个32位长的数，其中高、底16位分别包括了x、y坐标所以我们做一些小处理，以便保存它们。

shr eax,16
mov hitpoint.y,eax
保存完坐标后我们设标志MouseClick为TRUE，这是在处理WM_PAINT时用来判断是否有鼠标左键按下消息。然后我们调用InvalidateRect（）函数迫使WINDOWS重新绘制客户区。

.IF MouseClick
invoke lstrlen,ADDR AppName
invoke TextOut,hdc,hitpoint.x,hitpoint.y,ADDR AppName,eax
.ENDIF

绘制客户区的代码首先检测MouseClick标志位，再决定是否重绘。因为我们在首次显示窗口时还没有左键按下的消息，所以我们在初始时把该标志设为FALSE，告诉WINDOWS不要重绘客户区，当有左键按下的消息时，它会在鼠标按下的位置绘制字符串。注意在调用TextOut（）函数时，其关于字符串长度的参数是调用lstrlen（）函数来计算的。

翻译：Lxx，校对：LuoYunBin's Win32 ASM Page，http://asm.yeah.net


Iczelion's win32 asm tutorial

第八课 菜单

本课中我们将在我们的应用程序中加入一个菜单。

理论：

菜单可以说是WINDOWS最重要的元素之一。有了它，用户可以方便地选择操作命令.用户只要细读一下所有的菜单项就可以明了应用程序所提供的大概功能,而且可以立即操作,无须去阅读手册了.正因为菜单给了用户一种方便的方式,所以您在应用程序中加入菜单时就要遵守一般的标准.譬如:一般头两个菜单项是"File"和"Edit",最后是"Help",您可以在这中间插入您要定义的菜单项.如果所运行的菜单命令会弹出一个对话框,那么就要在该菜单项后加入省略符(...).菜单是一种资源,除菜单外还有其它像对话框,字符串,图标,位图资源等.在链接时链接程序将把资源加入到可执行程序中去,最后我们的执行程序中就既包括机器指令又包括了资源. 您可以在任何文本编辑器中编写脚本文件,在文件中您可以指定资源呈现出来的外观和其它的一些属性.当然更直观的方法是用资源编辑器,通常资源编辑器都打包在编译环境中,像Visual C++, Borland C++等都带了资源编辑器. 我们可以按以下方式来定义一个菜单资源：
    MyMenu MENU
    {
    [menu list here]
    }
这和C语言中的结构体的定义非常相似。 MyMenu类似于被定义的变量，而MENU则类似于关键字。当然您可以用另外一种办法，那就是用BEGIN和END来代替花括号，这和PASCAL语言中的风格相同。
在菜单项的列表中是一大串的MENUITEM和POPUP语句。MENUITEM定义了一个菜单项，当选择后不会激活对话框。它的语法如下：
    MENUITEM "&text", ID [,options]
它由关键字MENUITEM开头，紧跟在MENUITEM后的是指菜单项的名称字符串，符号“&“后的第一个字符将会带下画线，它也是该菜单项的快捷键。ID的作用当该菜单被选中时，WINDOWS的消息处理过程用来区分菜单项用的。毫无疑问，ID号必须唯一。options有以下可供选择的属性：
  • GRAYED 代表该菜单项处于非激活状态，即当其被选中时不会产生WM_COMMAND消息。该菜单以灰色显示。
  • INACTIVE 代表该菜单项处于非激活状态，即当其被选中时不会产生WM_COMMAND消息。该菜单以正常颜色显示。
  • MENUBREAK 该菜单项和随后的菜单项会显示在新列中。（译者注：比较难描述，请做实验。）
  • HELP 该菜单项和随后的菜单项右对齐。（译者注：我在WINDOWS2000下编译有该标志的菜单项，该标志好像没起作用）
您可以单独使用以上标志位，也可以把它们或在一起。当然INACTIVE和GRAYED不能同时使用。 POPUP的语法如下：
POPUP "&text" [,options]
{
[menu list]
}
POPUP定义了一个菜单项当该菜单项被选中时又会弹出一个子菜单。另外有一种特别类型的MENUITEM语句MENUITEM SEPARATOR，它表示在菜单项位置画一条分隔线。 定义完菜单后，您就可以在程序中使用脚本中定义的菜单资源了。 您可以在程序的两个地方（或叫做用两种方式）使用它们：
  • 在WNDCLASSEX结构体的成员lpszMenuName中。譬如，您有一个菜单“FirstMenu“，您可以按如下方法把它联系到您的窗口：
        • .DATA
          MenuName db "FirstMenu",0
        ...........................
        ...........................
        .CODE
          ...........................
          mov wc.lpszMenuName, OFFSET MenuName
          ...........................
  • 在CreateWindowEx函数中指明菜单的句柄：
        • .DATA
          MenuName db "FirstMenu",0
          hMenu HMENU ?
        ...........................
        ...........................
        .CODE
          ...........................
          invoke LoadMenu, hInst, OFFSET MenuName
          mov hMenu, eax
          invoke CreateWindowEx,NULL,OFFSET ClsName,\
          OFFSET Caption, WS_OVERLAPPEDWINDOW,\
          CW_USEDEFAULT,CW_USEDEFAULT,\
          CW_USEDEFAULT,CW_USEDEFAULT,\
          NULL,\
          hMenu,\
          hInst,\
          NULL\
          ...........................
您也许会问，这两着之间有什么不同呢？当您用第一种方法时，由于是在窗口类中指定，故所有由该窗口类派生的窗口都将有相同的菜单。如果您想要从相同的类中派生的窗口有不同的菜单那就要使用第二中方法，该方法中通过函数CreateWindowEx指定的菜单会“覆盖”WNDCLASSEX结构体中指定的菜单。 接下来我们看看当用户选择了一个菜单项时它是如何通知WINDOWS 窗口过程的： 当用户选择了一个菜单项时，WINDOWS窗口过程会接收到一个WM_COMMAND消息，传进来的参数wParam的底字节包含了菜单项的ID号。 好了，上面就是关于菜单项的一切，下面我们就来实践。

例子：

第一个例子显示了指定一个菜单项的第一种方法：

.386
.model flat,stdcall
option casemap:none

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

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

.data
ClassName db "SimpleWinClass",0
AppName db "Our First Window",0
MenuName db "FirstMenu",0 ; The name of our menu in the resource file.
Test_string db "You selected Test menu item",0
Hello_string db "Hello, my friend",0
Goodbye_string db "See you again, bye",0

.data?
hInstance HINSTANCE ?
CommandLine LPSTR ?

.const
IDM_TEST equ 1 ; Menu IDs
IDM_HELLO equ 2
IDM_GOODBYE equ 3
IDM_EXIT equ 4

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

WinMain proc hInst:HINSTANCE,hPrevInst:HINSTANCE,CmdLine:LPSTR,CmdShow:DWORD
LOCAL wc:WNDCLASSEX
LOCAL msg:MSG
LOCAL hwnd:HWND
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 hInst
pop wc.hInstance
mov wc.hbrBackground,COLOR_WINDOW+1
mov wc.lpszMenuName,OFFSET MenuName ; Put our menu name here
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 wc
invoke CreateWindowEx,NULL,ADDR ClassName,ADDR AppName,\
WS_OVERLAPPEDWINDOW,CW_USEDEFAULT,\
CW_USEDEFAULT,CW_USEDEFAULT,CW_USEDEFAULT,NULL,NULL,\
hInst,NULL
mov hwnd,eax
invoke ShowWindow, hwnd,SW_SHOWNORMAL
invoke UpdateWindow, hwnd
.WHILE TRUE
invoke GetMessage, ADDR msg,NULL,0,0
.BREAK .IF (!eax)
invoke DispatchMessage, ADDR msg
.ENDW
mov eax,msg.wParam
ret
WinMain endp

WndProc proc hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM
.IF uMsg==WM_DESTROY
invoke PostQuitMessage,NULL
.ELSEIF uMsg==WM_COMMAND
mov eax,wParam
.IF ax==IDM_TEST
invoke MessageBox,NULL,ADDR Test_string,OFFSET AppName,MB_OK
.ELSEIF ax==IDM_HELLO
invoke MessageBox, NULL,ADDR Hello_string, OFFSET AppName,MB_OK
.ELSEIF ax==IDM_GOODBYE
invoke MessageBox,NULL,ADDR Goodbye_string, OFFSET AppName, MB_OK
.ELSE
invoke DestroyWindow,hWnd
.ENDIF
.ELSE
invoke DefWindowProc,hWnd,uMsg,wParam,lParam
ret
.ENDIF
xor eax,eax
ret
WndProc endp
end start
**************************************************************************************************************************

Menu.rc
**************************************************************************************************************************

#define IDM_TEST 1
#define IDM_HELLO 2
#define IDM_GOODBYE 3
#define IDM_EXIT 4

FirstMenu MENU
{
POPUP "&PopUp"
{
MENUITEM "&Say Hello",IDM_HELLO
MENUITEM "Say &GoodBye", IDM_GOODBYE
MENUITEM SEPARATOR
MENUITEM "E&xit",IDM_EXIT
}
MENUITEM "&Test", IDM_TEST
}

分析:

我们先来分析资源文件：
#define IDM_TEST 1 /* equal to IDM_TEST equ 1*/
#define IDM_HELLO 2
#define IDM_GOODBYE 3
#define IDM_EXIT 4
上面的几行定义了菜单项的ID号。只要注意菜单项ID号必须唯一外，您可以给ID号任何值。

FirstMenu MENU

用关键字MENU定义菜单。

POPUP "&PopUp"
{
MENUITEM "&Say Hello",IDM_HELLO
MENUITEM "Say &GoodBye", IDM_GOODBYE
MENUITEM SEPARATOR
MENUITEM "E&xit",IDM_EXIT
}

定义一个有四个菜单项的子菜单，其中第三个菜单项是一个分隔线。

MENUITEM "&Test", IDM_TEST

定义主菜单中的一项。下面我们来看看源代码。

MenuName db "FirstMenu",0 ; The name of our menu in the resource file.
Test_string db "You selected Test menu item",0
Hello_string db "Hello, my friend",0
Goodbye_string db "See you again, bye",0
MenuName是资源文件中指定的菜单的名字。因为您可以在脚本文件中定义任意多个菜单，所以在使用前必须指定您要使用那一个，接下来的行是在选中菜单项时显示在相关对话框中的字符串。
IDM_TEST equ 1 ; Menu IDs
IDM_HELLO equ 2
IDM_GOODBYE equ 3
IDM_EXIT equ 4
定义用在WINDOWS窗口过程中的菜单项ID号。这些值必须和脚本文件中的相同。

.ELSEIF uMsg==WM_COMMAND
mov eax,wParam
.IF ax==IDM_TEST
invoke MessageBox,NULL,ADDR Test_string,OFFSET AppName,MB_OK
.ELSEIF ax==IDM_HELLO
invoke MessageBox, NULL,ADDR Hello_string, OFFSET AppName,MB_OK
.ELSEIF ax==IDM_GOODBYE
invoke MessageBox,NULL,ADDR Goodbye_string, OFFSET AppName, MB_OK
.ELSE
invoke DestroyWindow,hWnd
.ENDIF

在本窗口过程中我们处理WM_COMMAND消息。当用户选择了一个菜单项时，该菜单项的ID放入参数wParam中被同时送到WINDOWS的窗口过程，我们把它保存到eax寄存器中以便和预定义的菜单项ID比较用。前三种情况下，当我们选中Test、Say Hello、Say GoodBye菜单项时，会弹出一个对话框其中显示一个相关的字符串，选择Exit菜单项时，我们就调用函数DestroyWindow，其中的参数是我们窗口的句柄，这样就销毁了窗口。 就像您所看到的，通过在一个窗口类中指定菜单名的方法来给一个应用程序生成一个菜单是简单而直观的。除此方法外您还可以用另一种方法，其中资源文件是一样的，源文件中也只有少数的改动，这些改动如下：

.data?
hInstance HINSTANCE ?
CommandLine LPSTR ?
hMenu HMENU ? ; handle of our menu
定义了一个变量来保存我们的菜单的句柄，然后：

invoke LoadMenu, hInst, OFFSET MenuName
mov hMenu,eax
INVOKE CreateWindowEx,NULL,ADDR ClassName,ADDR AppName,\
WS_OVERLAPPEDWINDOW,CW_USEDEFAULT,\
CW_USEDEFAULT,CW_USEDEFAULT,CW_USEDEFAULT,NULL,hMenu,\
hInst,NULL

调用LoadMenu函数，该函数需要实例句柄和菜单名的字符串，调用的结果返回指向菜单的句柄，然后传给函数CreateWindowEx刚返回的菜单句柄就可以了。

翻译：Lxx，校对：LuoYunBin's Win32 ASM Page，http://asm.yeah.net


Iczelion's win32 asm tutorial

第九课 子窗口控件

本课中我们将探讨控件，这些控件是我们程序主要的输入输出设备。

理论：

WINDOWS 提供了几个预定义的窗口类以方便我们的使用。大多数时间内，我们把它们用在对话框中，所以我们一般就它们叫做子窗口控件。子窗口控件会自己处理消息，并在自己状态发生改变时通知父窗口。这样就大大地减轻了我们的编程工作，所以我们应尽可能地利用它们。本课中我们把这些控件放在窗口中以简化程序，但是大多数时间内子窗口控件都是放在对话框中的。 我们示例中演示的子窗口控件包括：按钮、下拉菜单、检查框、单选按钮、编辑框等。 使用子窗口控件时，先调用CreateWindow 或 CreateWindowEx。在这里由于WINDOWS 已经注册了这些子控件，所以无须我们再注册。当然我们不能改变它们的类名称。譬如：如果您想产生一个按钮，在调用上述两个函数时就必须指定类名为"button"。其他必须指定的参数还有父窗口的句柄和将要产生的子控件的ID号。子控件的ID号是用来标识子控件的，故也必须是唯一 的。子控件产生后，当其状态改变时将会向父窗口发送消息。一般我们应在父窗口的WM_CREATE消息中产生字控件。子控件向父窗口发送的消息是WM_COMMAND，并在传递的参数wPara的底位中包括控件的ID号，消息号在wParam的高位，lParam中则包括了子控件的窗口的句柄。各类控件有不同的消息代码集，详情请参见WIN32 API参考手册。 父窗口也可以通过调用函数SendMessage向子控件发送消息，其中第一个参数是子控件的窗口句柄，第二个参数是要发送的消息号，附加的参数可以在wParam和lParam中传递，其实只要知道了某个窗口的句柄就可以用该函数向其发送相关消息。 所以产生了子窗口后必须处理WM_COMMAND消息以便可以接收到子控件的消息。

例子：

我们将产生一个窗口，在该窗口中有一个编辑框和一个按钮。当您按下按钮时 ，会弹出一个对话框其中显示了您在编辑框中输入的内容。另外，该应用程序还有一个菜单，其中有四个菜单项：
  1. Say Hello -- 把一个字符串输入编辑控件；
  2. Clear Edit Box -- 清除编辑控件中的字符串；
  3. Get Text -- 弹出对话框显示编辑控件中的字符串；
  4. Exit -- 退出应用程序。
.386
.model flat,stdcall
option casemap:none

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

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

.data
ClassName db "SimpleWinClass",0
AppName db "Our First Window",0
MenuName db "FirstMenu",0
ButtonClassName db "button",0
ButtonText db "My First Button",0
EditClassName db "edit",0
TestString db "Wow! I'm in an edit box now",0

.data?
hInstance HINSTANCE ?
CommandLine LPSTR ?
hwndButton HWND ?
hwndEdit HWND ?
buffer db 512 dup(?) ; buffer to store the text retrieved from the edit box

.const
ButtonID equ 1 ; The control ID of the button control
EditID equ 2 ; The control ID of the edit control
IDM_HELLO equ 1
IDM_CLEAR equ 2
IDM_GETTEXT equ 3
IDM_EXIT equ 4

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

WinMain proc hInst:HINSTANCE,hPrevInst:HINSTANCE,CmdLine:LPSTR,CmdShow:DWORD
LOCAL wc:WNDCLASSEX
LOCAL msg:MSG
LOCAL hwnd:HWND
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 hInst
pop wc.hInstance
mov wc.hbrBackground,COLOR_BTNFACE+1
mov wc.lpszMenuName,OFFSET MenuName
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 wc
invoke CreateWindowEx,WS_EX_CLIENTEDGE,ADDR ClassName, \
ADDR AppName, WS_OVERLAPPEDWINDOW,\
CW_USEDEFAULT, CW_USEDEFAULT,\
300,200,NULL,NULL, hInst,NULL
mov hwnd,eax
invoke ShowWindow, hwnd,SW_SHOWNORMAL
invoke UpdateWindow, hwnd
.WHILE TRUE
invoke GetMessage, ADDR msg,NULL,0,0
.BREAK .IF (!eax)
invoke TranslateMessage, ADDR msg
invoke DispatchMessage, ADDR msg
.ENDW
mov eax,msg.wParam
ret
WinMain endp

WndProc proc hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM
.IF uMsg==WM_DESTROY
invoke PostQuitMessage,NULL
.ELSEIF uMsg==WM_CREATE
invoke CreateWindowEx,WS_EX_CLIENTEDGE, ADDR EditClassName,NULL,\
WS_CHILD or WS_VISIBLE or WS_BORDER or ES_LEFT or\
ES_AUTOHSCROLL,\
50,35,200,25,hWnd,8,hInstance,NULL
mov hwndEdit,eax
invoke SetFocus, hwndEdit
invoke CreateWindowEx,NULL, ADDR ButtonClassName,ADDR ButtonText,\
WS_CHILD or WS_VISIBLE or BS_DEFPUSHBUTTON,\
75,70,140,25,hWnd,ButtonID,hInstance,NULL
mov hwndButton,eax
.ELSEIF uMsg==WM_COMMAND
mov eax,wParam
.IF lParam==0
.IF ax==IDM_HELLO
invoke SetWindowText,hwndEdit,ADDR TestString
.ELSEIF ax==IDM_CLEAR
invoke SetWindowText,hwndEdit,NULL
.ELSEIF ax==IDM_GETTEXT
invoke GetWindowText,hwndEdit,ADDR buffer,512
invoke MessageBox,NULL,ADDR buffer,ADDR AppName,MB_OK
.ELSE
invoke DestroyWindow,hWnd
.ENDIF
.ELSE
.IF ax==ButtonID
shr eax,16
.IF ax==BN_CLICKED
invoke SendMessage,hWnd,WM_COMMAND,IDM_GETTEXT,0
.ENDIF
.ENDIF
.ENDIF
.ELSE
invoke DefWindowProc,hWnd,uMsg,wParam,lParam
ret
.ENDIF
xor eax,eax
ret
WndProc endp
end start

分析：

我们现在开始分析，
    .ELSEIF uMsg==WM_CREATE
    invoke CreateWindowEx,WS_EX_CLIENTEDGE, \
    ADDR EditClassName,NULL,\
    WS_CHILD or WS_VISIBLE or WS_BORDER or ES_LEFT\
    or ES_AUTOHSCROLL,\
    50,35,200,25,hWnd,EditID,hInstance,NULL
    mov hwndEdit,eax
    invoke SetFocus, hwndEdit
    invoke CreateWindowEx,NULL, ADDR ButtonClassName,\
    ADDR ButtonText,\
    WS_CHILD or WS_VISIBLE or BS_DEFPUSHBUTTON,\
    75,70,140,25,hWnd,ButtonID,hInstance,NULL
    mov hwndButton,eax
我们在WM_CREATE中产生子控件，其中在函数CreateWindowEx中给子控件窗口一个WS_EX_CLIENTEDGE风格，它使得子控件窗口看上去边界下凹，具有立体感。每一个子控件的类名都是预定义的，譬如：按钮的预定义类名是"button"，编辑框是"edit"。接下来的参数是窗口风格，除了通常的窗口风格外，每一个控件都有自己的扩展风格，譬如：按钮类的扩展风格前面加有BS_，编辑框类则是：ES_，WIN32 API 参考中有所有的扩展风格的描述。注意：您在CreateWindowsEx函数中本来要传递菜单句柄的地方传入子窗口空间的ID号不会有什么副作用，因为子窗口控件本身不能有菜单。产生控件后，我们保存它们的句柄，然后调用SetFocus把焦点设到编辑控件上以便用户立即可以输入。 接下来的是如何处理控件发送的通知消息WM_COMMAND：

.ELSEIF uMsg==WM_COMMAND
mov eax,wParam
.IF lParam==0

我们以前讲过选择菜单想也会发送WM_COMMAND 消息，那我们应如何区分呢？看了下表您就会一目了然：

 | Low word of wParam | High word of wParam | lParam
Menu | Menu ID | 0 | 0
Control | Control ID | Notification code | Child Window Handle

其中我们可以看到不能用wParam来区分，因为菜单和控件的ID号可能相同，而且子窗口空间的消息号也有可能为0。

.IF ax==IDM_HELLO
invoke SetWindowText,hwndEdit,ADDR TestString
.ELSEIF ax==IDM_CLEAR
invoke SetWindowText,hwndEdit,NULL
.ELSEIF ax==IDM_GETTEXT
invoke GetWindowText,hwndEdit,ADDR buffer,512
invoke MessageBox,NULL,ADDR buffer,ADDR AppName,MB_OK

您可以调用SetWindowText函数把一字符串繁缛到编辑控件中去，为了清0，传入NULL值。SetWindowText是一个通用函数，即可以用它来设定一个窗口的标题，也可以用它来改变一个按钮上的文字。如果是要得到按钮上的文字，则调用GetWindowText。

.IF ax==ButtonID
shr eax,16
.IF ax==BN_CLICKED
invoke SendMessage,hWnd,WM_COMMAND,IDM_GETTEXT,0
.ENDIF
.ENDIF

上面的片段是处理用户按钮事件的。他首先检查wParam的高字节看是否是按钮的ID 号，若是则检查低字节看发送的消息号是否BN_CLICKED，该消息是在按钮按下时发送的，如果一切都对，则转入处理该消息，我们可以从处理消息IDM_GETTEXT处复制全部的代码，但是更专业的办法是在发送一条IDM_GETTEXT消息让主窗口过程处理，这只要把传送的消息设置为WM_COMMAND，再把wParam的低字节中设置为IDM_GETTEXT即可。这样一来您的代码就简洁了许多，所以尽可能利用该技巧。 最后，当然不是或有或无，必须在消息循环中调用函数TranslateMessage，因为您的应用程序需要在编辑框中输入可读的文字。如果省略了该函数，就不能在编辑框中输入任何东西。

翻译：Lxx，校对：LuoYunBin's Win32 ASM Page，http://asm.yeah.net


Iczelion's win32 asm tutorial

第十课 以对话框为主要界面的应用程序

现在我们开始学习一些有关GUI编程的有趣的部分， 即：以对话框为主要界面的应用程序。我们将分两课来讲述这一过程

理论：

如果您仔细关注过前一个程序就会发现：您无法按TAB键从一个子窗口控件跳到另一个子窗口控件，要想转移的话只有 用鼠标一下一下地去点击。对用户来说这是不友好的。另一件事是如果您象前一课中那样把主窗口的背景色从白色改成 灰色，为了子窗口控件无缝地作相应地改变，您必须细分类所有子窗口。 造成上述诸多不便的原因是子窗口控件本来是为对话框而设计的，象子窗口控件的背景色是灰色的，而对话框的背景色也是 灰色的，这样它们本来就相互协调了，而无须程序员加入其他的处理。 在我们深入讨论对话框前我们必须知道何谓对话框。一个对话框其实是有很多的子窗口控件的一个窗口，WINDOWS在对话框 内部有一个管理程序，由其来处理象按下TAB键则输入焦点从一个子窗口空间条到另一个子窗口控件、按下ENTER键等于在当 前具有输入焦点的子窗口控件上点击了鼠标 等等这些杂事，这样程序员就可以集中精力于他们的逻辑事务了。对话框主要用 作输入输出接口，人们无须知道它们内部的工作原理，而只要知道如何和他们进行交互就可以了。这也是面向对象设计中的 所谓信息隐藏。只要这个黑盒子中的实现足够完美，我们就可以放心地使用，当然我们必须强调的是“黑盒子”必须完美。 WIN32 API 内部 的实现即是一个“黑盒子”。 噢，好象我们的讨论有些走题，现在让我们回到正题来，对话框的设计是为了减少程序员的工作量的，一般您如果在窗口中 自己放一个子窗口控件您就必须自己处理其中的按键逻辑和细分类它的窗口过程。如果您把它放到对话框中，则这些杂事 对话框会自己处理，您只要知道如何获得用户输入的数据和如何把数据放入到子窗口控件中去就可以了。 在程序中对话框和菜单一样被定义成一种资源，您可以在脚本文件中写一个对话框模板，其中包含该对话框和子窗口的特性， 然后用资源编辑器编辑。需要注意的是所有的资源必须放在同一个脚本文件中。 虽然可以用文本编辑器去编辑脚本文件，但是象要调整子窗口控件位置时要涉及到一些坐标值时最好还是用一些可视化的编 辑器，这样方便多了。一般在编译器的开发包中都会带资源编辑器，您可以用它们来产生一个模板然后增删一些子窗口控件。 有两种主要的对话框：模式对话框和无模式对话框。无模式对话框允许您把输入焦点切换到（同一个应用程序的）另一个窗口，而该对话框无须关闭 。比如MS WORD 中的FIND对话框。模式对话框又有两类：应用程序模式对话框和系统对话框。应用程序对话框不允许您在本 应用程序中切换输入焦点，但是可以切换到其它的应用程序中去，而系统对话框则必须您对该对话框做出响应否则不能切换到 任何的应用程序中去。要创建一个无模式对话框调用API函数CreateDialogParam，而创建一个模式对话框则调用API函数DialogBoxParam。 其中应用程序模式对话框和系统模式对话框之间的差别是style参数不同，要想创建一个系统模式对话框该参数必须“或”上 DS_SYSMODAL标志位。在对话框中若要和子窗口控件通讯则调用函数SendDlgItemMesage。该函数的语法如下：

SendDlgItemMessage proto hwndDlg:DWORD,\
idControl:DWORD,\
uMsg:DWORD,\
wParam:DWORD,\
lParam:DWORD
该PAI函数对于用在向子窗口控件发送方面是非常有用的。譬如:如果您想得到编辑控件中的字符串可以这么做:
call SendDlgItemMessage, hDlg, ID_EDITBOX, WM_GETTEXT, 256, ADDR text_buffer
具体要发送那些消息应当查询有关的WIN32 API 参考手册。 WINDOWS 还 提供几个快速存取控件数据的函数。譬如：GetDlgItemText、CheckDlgButton等。这样一来，您就可以不用去查询每个消息的wParam和lParam参数获得相关信息了。您应尽可能地使用这些API 函数，这样使得您的代码将来比较容易维护。 对话框的管理函数会把一些消息发送给一个特定的回调函数：对话框过程处理函数，该函数的格式为：
DlgProc proto hDlg:DWORD ,\
iMsg:DWORD ,\
wParam:DWORD ,\
lParam:DWORD
该函数的格式非常类似于窗口的过程函数，除了返回值是TURE和FALSE，而不是HRESULT，存在于WINDOWS内部的对话框管理器才是对话框真正的窗口过程函数。它会把某些消息传递给我们的窗口过程函数。所以当我们的窗口过程函数处理这些消息时就返回TTRUE，否则就在eax中返回FALSE。这也意味着我们的窗口过程函数在接受到自己不处理的消息时并不会调用DefWindowProc函数，因为它本身不是一个真正的窗口过程函数。 对于对话框有两种用法：一种是把它作为一个主窗口来用，一种是把它作为一种输入输出设备使用。本课中我们将示范第一种用法。“把对话框用作主窗口”有两种意思： 1。您可以调用RegisterClassEx函数把对话框模板注册为一个窗口类。这样该对话框的行为就类似于一个普通的窗口了：它通过在注册窗口时指定的窗口过程来处理所有的消息，通过这种方法来使用对话框的好处是您不需要显示地创建子窗口控件，WINDOWS本身会帮您创建好，另外还会帮您处理所有的按键逻辑，另外您还可以指定您窗口类结构中的光标和图标； 2。您的应用程序创建没有父窗口的对话框窗口，这种方法中，没有必要需要一段处理消息循环的代码，因为所有的消息被直接送到对话框过程处理函数，这样您也可以不要注册一个窗口类。 本课中我门将先使用第一种方法然后使用第二中方法。

例子：

dialog.asm

.386
.model flat,stdcall
option casemap:none
WinMain proto :DWORD,:DWORD,:DWORD,:DWORD
include \masm32\include\windows.inc
include \masm32\include\user32.inc
include \masm32\include\kernel32.inc
includelib \masm32\lib\user32.lib
includelib \masm32\lib\kernel32.lib

.data
ClassName db "DLGCLASS",0
MenuName db "MyMenu",0
DlgName db "MyDialog",0
AppName db "Our First Dialog Box",0
TestString db "Wow! I'm in an edit box now",0

.data?
hInstance HINSTANCE ?
CommandLine LPSTR ?
buffer db 512 dup(?)

.const
IDC_EDIT equ 3000
IDC_BUTTON equ 3001
IDC_EXIT equ 3002
IDM_GETTEXT equ 32000
IDM_CLEAR equ 32001
IDM_EXIT equ 32002

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

WinMain proc hInst:HINSTANCE,hPrevInst:HINSTANCE,CmdLine:LPSTR,CmdShow:DWORD
LOCAL wc:WNDCLASSEX
LOCAL msg:MSG
LOCAL hDlg:HWND
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,DLGWINDOWEXTRA
push hInst
pop wc.hInstance
mov wc.hbrBackground,COLOR_BTNFACE+1
mov wc.lpszMenuName,OFFSET MenuName
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 wc
invoke CreateDialogParam,hInstance,ADDR DlgName,NULL,NULL,NULL
mov hDlg,eax
invoke ShowWindow, hDlg,SW_SHOWNORMAL
invoke UpdateWindow, hDlg
invoke GetDlgItem,hDlg,IDC_EDIT
invoke SetFocus,eax
.WHILE TRUE
invoke GetMessage, ADDR msg,NULL,0,0
.BREAK .IF (!eax)
invoke IsDialogMessage, hDlg, ADDR msg
.IF eax ==FALSE
invoke TranslateMessage, ADDR msg
invoke DispatchMessage, ADDR msg
.ENDIF
.ENDW
mov eax,msg.wParam
ret
WinMain endp

WndProc proc hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM
.IF uMsg==WM_DESTROY
invoke PostQuitMessage,NULL
.ELSEIF uMsg==WM_COMMAND
mov eax,wParam
.IF lParam==0
.IF ax==IDM_GETTEXT
invoke GetDlgItemText,hWnd,IDC_EDIT,ADDR buffer,512
invoke MessageBox,NULL,ADDR buffer,ADDR AppName,MB_OK
.ELSEIF ax==IDM_CLEAR
invoke SetDlgItemText,hWnd,IDC_EDIT,NULL
.ELSE
invoke DestroyWindow,hWnd
.ENDIF
.ELSE
mov edx,wParam
shr edx,16
.IF dx==BN_CLICKED
.IF ax==IDC_BUTTON
invoke SetDlgItemText,hWnd,IDC_EDIT,ADDR TestString
.ELSEIF ax==IDC_EXIT
invoke SendMessage,hWnd,WM_COMMAND,IDM_EXIT,0
.ENDIF
.ENDIF
.ENDIF
.ELSE
invoke DefWindowProc,hWnd,uMsg,wParam,lParam
ret
.ENDIF
xor eax,eax
ret
WndProc endp
end start

Dialog.rc

#include "resource.h"

#define IDC_EDIT 3000
#define IDC_BUTTON 3001
#define IDC_EXIT 3002

#define IDM_GETTEXT 32000
#define IDM_CLEAR 32001
#define IDM_EXIT 32003

MyDialog DIALOG 10, 10, 205, 60
STYLE 0x0004 | DS_CENTER | WS_CAPTION | WS_MINIMIZEBOX |
WS_SYSMENU | WS_VISIBLE | WS_OVERLAPPED | DS_MODALFRAME | DS_3DLOOK
CAPTION "Our First Dialog Box"
CLASS "DLGCLASS"
BEGIN
EDITTEXT IDC_EDIT, 15,17,111,13, ES_AUTOHSCROLL | ES_LEFT
DEFPUSHBUTTON "Say Hello", IDC_BUTTON, 141,10,52,13
PUSHBUTTON "E&xit", IDC_EXIT, 141,26,52,13, WS_GROUP
END

MyMenu MENU
BEGIN
POPUP "Test Controls"
BEGIN
MENUITEM "Get Text", IDM_GETTEXT
MENUITEM "Clear Text", IDM_CLEAR
MENUITEM "", , 0x0800 /*MFT_SEPARATOR*/
MENUITEM "E&xit", IDM_EXIT
END
END

分析：

我们先来分析第一个例子：
该例显示了如何把一个对话框模板注册成一个窗口类，然后创建一个由该窗口类派生的窗口。由于您没有必要自己去创建子窗口控件，所以就简化了许多的工作。
我们先来分析对话框模板。

MyDialog DIALOG 10, 10, 205, 60

先是对话框的名字，然后是关键字“DAILOG”。接下来的四个数字中，前两个是对话框的坐标，后两个是对话框的宽和高（注意：它们的单位是对话框的单位，而不一定是像素点）。

STYLE 0x0004 | DS_CENTER | WS_CAPTION | WS_MINIMIZEBOX |
WS_SYSMENU | WS_VISIBLE | WS_OVERLAPPED | DS_MODALFRAME | DS_3DLOOK

上面定义了对话框的风格。

CAPTION "Our First Dialog Box"

这是显示在对话框标题条上的标题。

CLASS "DLGCLASS"

这一行非常关键。正是有了关键字CLASS，我们才可以用它来声明把一个对话框当成一个窗口来用。跟在关键字后面的是“窗口类”的名称。

BEGIN
EDITTEXT IDC_EDIT, 15,17,111,13, ES_AUTOHSCROLL | ES_LEFT
DEFPUSHBUTTON "Say Hello", IDC_BUTTON, 141,10,52,13
PUSHBUTTON "E&xit", IDC_EXIT, 141,26,52,13
END

上面的一块定义了对话框中的子窗口控件，它们是声明在一头一尾的两个关键字BEGIN和END之间的。

    control-type "text" ,controlID, x, y, width, height [,styles]
控件的类型是资源编辑器定义好了的常数，您可以查找有关的手册。
现在我们来看看汇编源代码。先看这部分：
    mov wc.cbWndExtra,DLGWINDOWEXTRA
    mov wc.lpszClassName,OFFSET ClassName
通常cbWndExtra被设成NULL，但我们想把一个对话框模板注册成一个窗口类，我们必须把该成员的值设成DLGWINDOWEXTRA。注意类的名称必须和模板中跟在CLASS关键字后面的名称一样。余下的成员变量和声明一般的窗口类相同。填写好窗口类结构变量后调用函数RegisterClassEx进行注册。看上去这一切和注册一个普通的窗口类是一样的。
    invoke CreateDialogParam,hInstance,ADDR DlgName,NULL,NULL,NULL
注册完毕后，我们就创建该对话框。在这个例子中，我们调用函数CreateDialogParam产生一个无模式对话框。这个函数共有5个参数，其中前两个参数是必须的：实例句柄和指向对话框模板名称的指针。注意第二个参数是指向模板名称而不是类名称的指针。这时，WINDOWS将产生对话框和子控件窗口。同时您的应用程序将接收到由WINDOWS传送的第一个消息WM_CREATE。
    invoke GetDlgItem,hDlg,IDC_EDIT
    invoke SetFocus,eax
在对话框产生后，我们把输入输出焦点设到编辑控件上。如果在WM_CREATE消息处理段中假如设置焦点的代码，GetDlgItem函数就会失败，因为此时空间窗口还未产生，为了在对话框和所有的子窗口控件都产生后调用该函数我们把它安排到了函数UpdatWindow后，GetDlgItem函数返回该控件的敞口句柄。

invoke IsDialogMessage, hDlg, ADDR msg
.IF eax ==FALSE
invoke TranslateMessage, ADDR msg
invoke DispatchMessage, ADDR msg
.ENDIF

现在程序进入消息循环，在我们翻译和派发消息前，该函数使得对话框内置的对话框管理程序来处理有关的键盘跳转逻辑。如果该函数返回TRUE，则表示消息是传给对话框的已经由该函数处理了。注意和前一课不同，当我们想得到控件的文本信息时调用GetDlgItemText函数而不是GetWindowText函数，前者接受的参数是一个控件的ID 号，而不是窗口的句柄，这使得在对话框中调用该函数更方便。

好我们现在使用第二种方法把一个对话框当成一个主窗口来使用。在接下来的例子中，我们将产生一个应用程序的模式对话框，您将会发现其中根本没有消息循环或窗口处理过程，因为它们根本没有必要！
dialog.asm (part 2)

.386
.model flat,stdcall
option casemap:none

DlgProc proto :DWORD,:DWORD,:DWORD,:DWORD

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

.data
DlgName db "MyDialog",0
AppName db "Our Second Dialog Box",0
TestString db "Wow! I'm in an edit box now",0

.data?
hInstance HINSTANCE ?
CommandLine LPSTR ?
buffer db 512 dup(?)

.const
IDC_EDIT equ 3000
IDC_BUTTON equ 3001
IDC_EXIT equ 3002
IDM_GETTEXT equ 32000
IDM_CLEAR equ 32001
IDM_EXIT equ 32002

.code
start:
invoke GetModuleHandle, NULL
mov hInstance,eax
invoke DialogBoxParam, hInstance, ADDR DlgName,NULL, addr DlgProc, NULL
invoke ExitProcess,eax

DlgProc proc hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM
.IF uMsg==WM_INITDIALOG
invoke GetDlgItem, hWnd,IDC_EDIT
invoke SetFocus,eax
.ELSEIF uMsg==WM_CLOSE
invoke SendMessage,hWnd,WM_COMMAND,IDM_EXIT,0
.ELSEIF uMsg==WM_COMMAND
mov eax,wParam
.IF lParam==0
.IF ax==IDM_GETTEXT
invoke GetDlgItemText,hWnd,IDC_EDIT,ADDR buffer,512
invoke MessageBox,NULL,ADDR buffer,ADDR AppName,MB_OK
.ELSEIF ax==IDM_CLEAR
invoke SetDlgItemText,hWnd,IDC_EDIT,NULL
.ELSEIF ax==IDM_EXIT
invoke EndDialog, hWnd,NULL
.ENDIF
.ELSE
mov edx,wParam
shr edx,16
.if dx==BN_CLICKED
.IF ax==IDC_BUTTON
invoke SetDlgItemText,hWnd,IDC_EDIT,ADDR TestString
.ELSEIF ax==IDC_EXIT
invoke SendMessage,hWnd,WM_COMMAND,IDM_EXIT,0
.ENDIF
.ENDIF
.ENDIF
.ELSE
mov eax,FALSE
ret
.ENDIF
mov eax,TRUE
ret
DlgProc endp
end start

dialog.rc (part 2)

#include "resource.h"

#define IDC_EDIT 3000
#define IDC_BUTTON 3001
#define IDC_EXIT 3002

#define IDR_MENU1 3003

#define IDM_GETTEXT 32000
#define IDM_CLEAR 32001
#define IDM_EXIT 32003

MyDialog DIALOG 10, 10, 205, 60
STYLE 0x0004 | DS_CENTER | WS_CAPTION | WS_MINIMIZEBOX |
WS_SYSMENU | WS_VISIBLE | WS_OVERLAPPED | DS_MODALFRAME | DS_3DLOOK
CAPTION "Our Second Dialog Box"
MENU IDR_MENU1
BEGIN
EDITTEXT IDC_EDIT, 15,17,111,13, ES_AUTOHSCROLL | ES_LEFT
DEFPUSHBUTTON "Say Hello", IDC_BUTTON, 141,10,52,13
PUSHBUTTON "E&xit", IDC_EXIT, 141,26,52,13
END

IDR_MENU1 MENU
BEGIN
POPUP "Test Controls"
BEGIN
MENUITEM "Get Text", IDM_GETTEXT
MENUITEM "Clear Text", IDM_CLEAR
MENUITEM "", , 0x0800 /*MFT_SEPARATOR*/
MENUITEM "E&xit", IDM_EXIT
END
END

分析如下：

DlgProc proto :DWORD,:DWORD,:DWORD,:DWORD

我们已经定义了DlgProc函数的原型，所以可以用操作符ADDR来获得它的地址（记得吗，它可以在运行时动态地获得标识符的有效地址）：

invoke DialogBoxParam, hInstance, ADDR DlgName,NULL, addr DlgProc, NULL

上面的几行调用了函数DialogBoxPAram，该函数有五个参数，分别是：实例句柄、对话框模板的名字、父窗口的句柄、对话框过程函数的地址、和对话框相关的数据。该函数产生一个模式对话框。如果不显示地关闭该函数不会返回。

.IF uMsg==WM_INITDIALOG
invoke GetDlgItem, hWnd,IDC_EDIT
invoke SetFocus,eax
.ELSEIF uMsg==WM_CLOSE
invoke SendMessage,hWnd,WM_COMMAND,IDM_EXIT,0

除了不处理WM_CREATE消息外对话框的窗口处理过程函数和一般的窗口处理过程相似。该过程函数接收到的第一个消息是WM_INITDIALOG。通常把初始化的代码放到此处。注意如果您处理该消息必须在eax中返回TRUE。内置的对话框管理函数不会把WM_DESTROY 消息发送到对话框的消息处理函数，所以如果我们想在对话框关闭时进行处理，就把它放到WM_CLOSE消息的处理中。在我们的例子中我们发送消息WM_COMMAND，并在参数wParam中放置IDM_EXIT，这和处理WM_CLOSE 消息效果一样，在处理IDM_EXIT 中我们调用EndDialog函数。 如果我们想要销毁一个对话框，必须调用EndDialog函数，该函数并不会立即销毁一个窗口，而是设置一个标志位，然后对话框管理器会处理接下去的销毁对话框动作。 好，现在我们来看看资源文件，其中最显著的变化是在指定菜单时我们不是用字符串指定该菜单的名称而是用了一个常量 IDR_MENU1。在调用DialogBoxParam产生的对话框中挂接一个菜单必须这么做，注意在该对话框模板中，在该标识符前必须加MENU关键字，这两个例子中的显著不同是后者没有图标，这可以在处理WM_INITDIALOG中发送消息WM_SETICON消息，然后在该消息处理代码中作适当的处理即可。

翻译：Lxx，校对：LuoYunBin's Win32 ASM Page，http://asm.yeah.net


Iczelion's win32 asm tutorial

第十一课 进一步学习对话框

本课中我们将进一步学习对话框。特别地我们将要探讨如何把对话框当成输入设备。如果您学习了上一课，那就会发现本课的例子只有少量的改动，就是把我们的对话框窗口附属到主窗口上。另外，我们还要学习通用对话框的用法。

理论：

把对话框当成一个输入设备来用确实是非常地简单，创建完主窗口后，您只要调用函数 CreatedialogParam 或 DialogBoxParam 就可以了，前一个函数只要在对话框的过程处理函数中处理相关的消息就可以，而后者你必须在消息循环段中插入函数 IsDialogMessage 的调用让它来处理键盘的按键逻辑。因为这两个程序段相对来说比较容易，我们就不详解。您可以下载并仔细研究。
下面我们来讨论通用对话框。WINDOWS已经为您准备好了预定义的对话框类，您可以拿来就用，这些通用对话框提供给用户以统一的界面。它们包括:打开文件、打印、选择颜色、字体和搜索等。您应该尽可能地用它们。处理这些对话框的代码在comdlg32.dll中，为了在您的应用程序中使用它们，就必须在链接阶段链接库文件 comdlg32.lib。然后调用其中的相关函数即可。对于打开文件通用对话框，该函数名为 GetOpenFileName，"保存为..."对话框为 GetSaveFileName，打印通用对话框是 PrintDlg， 等等。每一个这样的函数都接收一个指向一个结构体的指针的参数，您可以参考WIN32 API手册得到详细的资料，本课中我将讲解创建和使用打开文件对话框。
下面是打开对话框函数 GetOpenFileName 的原型:

GetOpenFileName proto lpofn:DWORD
您可以看到，该函数只有一个参数，即指向结构体OPENFILENAME的指针。当用户选择了一个文件并打开，该函数返回TRUE，否则返回FALSE。接下来我们看看结构体OPENFILENAME的定义:

OPENFILENAME STRUCT
lStructSize DWORD ?
hwndOwner HWND ?
hInstance HINSTANCE ?
lpstrFilter LPCSTR ?
lpstrCustomFilter LPSTR ?
nMaxCustFilter DWORD ?
nFilterIndex DWORD ?
lpstrFile LPSTR ?
nMaxFile DWORD ?
lpstrFileTitle LPSTR ?
nMaxFileTitle DWORD ?
lpstrInitialDir LPCSTR ?
lpstrTitle LPCSTR ?
Flags DWORD ?
nFileOffset WORD ?
nFileExtension WORD ?
lpstrDefExt LPCSTR ?
lCustData LPARAM ?
lpfnHook DWORD ?
lpTemplateName LPCSTR ?
OPENFILENAME ENDS
好，我们再来看看该结构体中常用的成员的意义:
lStructSize | 结构体OPENFILENAME的大小。
hwndOwner | 拥有打开对话框的窗口的句柄。
hInstance | 拥有该打开文件对话框的应用程序的实例句柄 。
lpstrFilter | 以NULL结尾的一个或多个通配符。通配符是成对出现的，前一部分是描述部分，后一部分则是通配符的格式，譬如: FilterString db "All Files (*.*)",0, "*.*",0 db "Text Files (*.txt)",0,"*.txt",0,0 注意:只有每一对中的第二部分是WINDOWS用来过滤所需选择的文件的，另外您必须在该部分后放置一个0，以示字符串的结束。
nFilterIndex | 用来指定打开文件对话框第一次打开时所用的过滤模式串，该索引是从1开始算的，即第一个通配符模式的索引是1，第二个是2，譬如上面的例子中，若指定该值为2，则缺省显示的模式串就是"*.txt"。
lpstrFile | 需要打开的文件的名称的地址，该名称将会出现在打开文件对话框的编辑控件中，该缓冲区不能超过260个字符长，当用户打开文件后，该缓冲区中包含该文件的全路径名，您可以从该缓冲区中抽取您所需要的路径或文件名等信息。
nMaxFile | lpstrFile的大小。
lpstrTitle | 指向对话框标题的字符串。
Flags | 该标志决定决定了对话框的风格和特点。
nFileOffset | 在用户打开了一个文件后该值是全路径名称中指向文件名第一个字符的索引。譬如:若全路径名为"c:\windows\system\lz32.dll"， 则该值为18。
nFileExtension | 在用户打开了一个文件后该值是全路径名称中指向个文件扩展名第一个字符的索引。

例子:

下例中，我们演示了当用户选择"File->Open"时，将弹出一个打开文件对话框，当用户选择了某个文件打开时，会弹出一个对话框，告知要打开的文件的全路径名，文件名和文件扩展名。

.386
.model flat,stdcall
option casemap:none
WinMain proto :DWORD,:DWORD,:DWORD,:DWORD
include \masm32\include\windows.inc
include \masm32\include\user32.inc
include \masm32\include\kernel32.inc
include \masm32\include\comdlg32.inc
includelib \masm32\lib\user32.lib
includelib \masm32\lib\kernel32.lib
includelib \masm32\lib\comdlg32.lib

.const
IDM_OPEN equ 1
IDM_EXIT equ 2
MAXSIZE equ 260
OUTPUTSIZE equ 512

.data
ClassName db "SimpleWinClass",0
AppName db "Our Main Window",0
MenuName db "FirstMenu",0
ofn OPENFILENAME <>
FilterString db "All Files",0,"*.*",0
db "Text Files",0,"*.txt",0,0
buffer db MAXSIZE dup(0)
OurTitle db "-=Our First Open File Dialog Box=-: Choose the file to open",0
FullPathName db "The Full Filename with Path is: ",0
FullName db "The Filename is: ",0
ExtensionName db "The Extension is: ",0
OutputString db OUTPUTSIZE dup(0)
CrLf db 0Dh,0Ah,0

.data?
hInstance HINSTANCE ?
CommandLine LPSTR ?

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

WinMain proc hInst:HINSTANCE,hPrevInst:HINSTANCE,CmdLine:LPSTR,CmdShow:DWORD
LOCAL wc:WNDCLASSEX
LOCAL msg:MSG
LOCAL hwnd:HWND
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 hInst
pop wc.hInstance
mov wc.hbrBackground,COLOR_WINDOW+1
mov wc.lpszMenuName,OFFSET MenuName
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 wc
invoke CreateWindowEx,WS_EX_CLIENTEDGE,ADDR ClassName,ADDR AppName,\
WS_OVERLAPPEDWINDOW,CW_USEDEFAULT,\
CW_USEDEFAULT,300,200,NULL,NULL,\
hInst,NULL
mov hwnd,eax
invoke ShowWindow, hwnd,SW_SHOWNORMAL
invoke UpdateWindow, hwnd
.WHILE TRUE
invoke GetMessage, ADDR msg,NULL,0,0
.BREAK .IF (!eax)
invoke TranslateMessage, ADDR msg
invoke DispatchMessage, ADDR msg
.ENDW
mov eax,msg.wParam
ret
WinMain endp

WndProc proc hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM
.IF uMsg==WM_DESTROY
invoke PostQuitMessage,NULL
.ELSEIF uMsg==WM_COMMAND
mov eax,wParam
.if ax==IDM_OPEN
mov ofn.lStructSize,SIZEOF ofn
push hWnd
pop ofn.hwndOwner
push hInstance
pop ofn.hInstance
mov ofn.lpstrFilter, OFFSET FilterString
mov ofn.lpstrFile, OFFSET buffer
mov ofn.nMaxFile,MAXSIZE
mov ofn.Flags, OFN_FILEMUSTEXIST or \
OFN_PATHMUSTEXIST or OFN_LONGNAMES or\
OFN_EXPLORER or OFN_HIDEREADONLY
mov ofn.lpstrTitle, OFFSET OurTitle
invoke GetOpenFileName, ADDR ofn
.if eax==TRUE
invoke lstrcat,offset OutputString,OFFSET FullPathName
invoke lstrcat,offset OutputString,ofn.lpstrFile
invoke lstrcat,offset OutputString,offset CrLf
invoke lstrcat,offset OutputString,offset FullName
mov eax,ofn.lpstrFile
push ebx
xor ebx,ebx
mov bx,ofn.nFileOffset
add eax,ebx
pop ebx
invoke lstrcat,offset OutputString,eax
invoke lstrcat,offset OutputString,offset CrLf
invoke lstrcat,offset OutputString,offset ExtensionName
mov eax,ofn.lpstrFile
push ebx
xor ebx,ebx
mov bx,ofn.nFileExtension
add eax,ebx
pop ebx
invoke lstrcat,offset OutputString,eax
invoke MessageBox,hWnd,OFFSET OutputString,ADDR AppName,MB_OK
invoke RtlZeroMemory,offset OutputString,OUTPUTSIZE
.endif
.else
invoke DestroyWindow, hWnd
.endif
.ELSE
invoke DefWindowProc,hWnd,uMsg,wParam,lParam
ret
.ENDIF
xor eax,eax
ret
WndProc endp
end start

分析:

mov ofn.lStructSize,SIZEOF ofn
push hWnd
pop ofn.hwndOwner
push hInstance
pop ofn.hInstance

我们在此填充结构体OPENFILENAME变量ofn的有关成员。

mov ofn.lpstrFilter, OFFSET FilterString

这里FilterString 是文件过滤模式的字符串地址，我们指定的过滤模式字符串如下:

FilterString db "All Files",0,"*.*",0
db "Text Files",0,"*.txt",0,0
注意:所有的模式串都是配对的，前一个是描述，后一个才是真正的模式，次处"*.*"和"*.txt"是WIONDOWS用来寻找匹配的欲打开的文件的。我们当能可以指定任何模式，但是不要忘记在结尾处加0以代表字符串已结束，否则您的对话框在操作时可能不稳定。

mov ofn.lpstrFile, OFFSET buffer
mov ofn.nMaxFile,MAXSIZE

这里是把缓冲区的地址放到结构体中，同时必须设定大小。以后我们可以随意编辑在该缓冲区中返回的信息。

mov ofn.Flags, OFN_FILEMUSTEXIST or \
OFN_PATHMUSTEXIST or OFN_LONGNAMES or\
OFN_EXPLORER or OFN_HIDEREADONLY

Flags 中放入的是对话框的风格和特性值。
其中OFN_FILEMUSTEXIST和 OFN_PATHMUSTEXIST要求用户在打开对话框的编辑控件中输入的文件名或路径名必须存在。
OFN_LONGNAMES 告诉对话框显示长文件名。
OFN_EXPLORER 告诉WINDOWS对话框的外观必须类似资源管理器。
OFN_HIDEREADONLY 指定不要显示只读文件(既使它的扩展名符合过滤模式)。
除此之外，还有许多其它的标志位，您可以参考有关WIN32 API手册。

mov ofn.lpstrTitle, OFFSET OurTitle

指定打开文件对话框的标题名。

invoke GetOpenFileName, ADDR ofn

调用GetOpenFileName函数，并传入指向结构体ofn的指针。
这时候，打开文件对话框就显示出来了，GetOpenFileName函数要一直等到用户选择了一个文件后才会返回，或者当用户按下了CANCEL键或关闭对话框时。
当用户选择了打开一个文件时，该函数返回TRUE， 否则返回FALSE。

.if eax==TRUE
invoke lstrcat,offset OutputString,OFFSET FullPathName
invoke lstrcat,offset OutputString,ofn.lpstrFile
invoke lstrcat,offset OutputString,offset CrLf
invoke lstrcat,offset OutputString,offset FullName

当用户选择打开一个文件时，我们就在一个对话框中显示一个字符串，我们先给OutputString变量分配内存，然后调用PAI 函数lstrcat，把所有的字符串连到一起，为了让这些字符串分行显示，我们必须在每个字符串后面加一个换行符。

mov eax,ofn.lpstrFile
push ebx
xor ebx,ebx
mov bx,ofn.nFileOffset
add eax,ebx
pop ebx
invoke lstrcat,offset OutputString,eax

上面这几行可能需要一些解释。nFileOffset的值等于被打开文件的全路径名中的文件名的第一个字符的索引，由于nFileOffset是一个WORD型变量，而lpstrFile是一个DWORD形的指针，所以我们就要作一转换把nFileOffset存入ebx寄存器的底字节，然后再加到eax寄存器中得到DWORD型的指针。

invoke MessageBox,hWnd,OFFSET OutputString,ADDR AppName,MB_OK

我们在对话框中显示该字符串。

invoke RtlZerolMemory,offset OutputString,OUTPUTSIZE

为了下一次能正确地显示，必须清除缓冲区，我们调用函数RtlZerolMemory来做这件事。

翻译：Lxx，阿龙.LuoYunBin's Win32 ASM Page， http://asm.yeah.net


Iczelion's win32 asm tutorial

第十二课 内存管理和文件输入/输出

本课中我们将学习基本的内存管理和文件输入/输出操作方面的知识。另外我们还将用上课学的通用对话框作为我们的显示“设备”。

理论：

从用户的角度来看，WIN32的内存管理是非常简单和明了的。每一个应用程序都有自己独立的4G地址空间，这种内存模式叫做“平坦”型地址模式，所有的段寄存器或描述符都指向同样的起始地址，所有的地址偏移都是32位的长度，这样一个应用程序无须变换选择符就可以存取自己的多达4G的地址空间。这种内存管理模式是非常简洁而便于管理的，而且我们再不用和那些令人讨厌的“near”和“far”指针打交道了。
在W16下有两种主要类型的API：全局和局部。“全局”的API 分配在其他的段中，这样从内存角度来看他们是一些“far”（远）函数或者叫远过程调用，“局部”API只要和进程的堆打交道，所以把它们叫做“near”（近）函数或者近过程调用。而在WIN32中，这两种内存模式是相同的，无论您调用GlobalAlloc还是LocalAlloc，结果都是一样。
至于分配和使用内存的过程都是一样的：

  1. 调用GlobalAlloc函数分配一块内存，该函数会返回分配的内存句柄。
  2. 调用GlobalLock函数锁定内存块，该函数接受一个内存句柄作为参数，然后返回一个指向被锁定的内存块的指针。
  3. 您可以用该指针来读写内存。
  4. 调用GlobalUnlock函数来解锁先前被锁定的内存，该函数使得指向内存块的指针无效。
  5. 调用GlobalFree函数来释放内存块。您必须传给该函数一个内存句柄。

在WIN32中您也可以用“Local”替代内存分配API函数带有“Global”字样的函数中的“Global”，也即用LocalAlloc、LocalLock等。
在调用函数GlobalAlloc时使用GMEM_FIXED标志位可以更进一步简化操作。使用了该标志后，Global/LocalAlloc返回的是指向已分配内存的指针而不是句柄，这样也就不用调用Global/LocalLock来锁定内存了，释放内存时只要直接调用Global/LocalFree就可以了。不过在本课中我们只使用传统的方法，因为其它地方有许多的源代码是用这种方法写的。

WIN32的文件输入/输出API和DOS下的从外表上看几乎一样（译者注：也许不管内部实现多么不同，可以想象所有的文件系统暴露给应用程序编写者的接口的功能应该基本相同），不同的只是把DOS下的中断方式处理文件输入/输出变成了对API函数的调用。以下是基本的步骤：

  1. 调用CreateFile函数生成一个文件，该函数可以应用在多方面，除了磁盘文件外，我们还可以用来打开通讯端口、管道、驱动程序或控制台。如果成功的话，会返回指向文件或设备的句柄。然后可以使用该句柄去完成对文件或设备操作。
    调用SetFilePointer来把文件指针移到想读写的地方。.
  2. 然后调用ReadFile 或 WriteFile来完成实际的读写。这些函数会自己处理文件和内存之间的数据传送，这样免得您自己去做分配内存等繁杂的琐事。
  3. 调用CloseHandle来关闭文件。该函数接受一个先前打开的文件句柄。

内容：

下面的代码段演示了：打开一个“打开文件”对话框，用户可以选择打开一个文本文件，然后在一个编辑控件中打开该文本文件的内容，另外用户还可以编辑该文本文件的内容并选择保存。

.386
.model flat,stdcall
option casemap:none
WinMain proto :DWORD,:DWORD,:DWORD,:DWORD
include \masm32\include\windows.inc
include \masm32\include\user32.inc
include \masm32\include\kernel32.inc
include \masm32\include\comdlg32.inc
includelib \masm32\lib\user32.lib
includelib \masm32\lib\kernel32.lib
includelib \masm32\lib\comdlg32.lib

.const
IDM_OPEN equ 1
IDM_SAVE equ 2
IDM_EXIT equ 3
MAXSIZE equ 260
MEMSIZE equ 65535

EditID equ 1 ; ID of the edit control

.data
ClassName db "Win32ASMEditClass",0
AppName db "Win32 ASM Edit",0
EditClass db "edit",0
MenuName db "FirstMenu",0
ofn OPENFILENAME <>
FilterString db "All Files",0,"*.*",0
db "Text Files",0,"*.txt",0,0
buffer db MAXSIZE dup(0)

.data?
hInstance HINSTANCE ?
CommandLine LPSTR ?
hwndEdit HWND ? ; Handle to the edit control
hFile HANDLE ? ; File handle
hMemory HANDLE ? ;handle to the allocated memory block
pMemory DWORD ? ;pointer to the allocated memory block
SizeReadWrite DWORD ? ; number of bytes actually read or write

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

WinMain proc hInst:HINSTANCE,hPrevInst:HINSTANCE,CmdLine:LPSTR,CmdShow:SDWORD
LOCAL wc:WNDCLASSEX
LOCAL msg:MSG
LOCAL hwnd:HWND
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 hInst
pop wc.hInstance
mov wc.hbrBackground,COLOR_WINDOW+1
mov wc.lpszMenuName,OFFSET MenuName
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 wc
invoke CreateWindowEx,WS_EX_CLIENTEDGE,ADDR ClassName,ADDR AppName,\
WS_OVERLAPPEDWINDOW,CW_USEDEFAULT,\
CW_USEDEFAULT,300,200,NULL,NULL,\
hInst,NULL
mov hwnd,eax
invoke ShowWindow, hwnd,SW_SHOWNORMAL
invoke UpdateWindow, hwnd
.WHILE TRUE
invoke GetMessage, ADDR msg,NULL,0,0
.BREAK .IF (!eax)
invoke TranslateMessage, ADDR msg
invoke DispatchMessage, ADDR msg
.ENDW
mov eax,msg.wParam
ret
WinMain endp

WndProc proc uses ebx hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM
.IF uMsg==WM_CREATE
invoke CreateWindowEx,NULL,ADDR EditClass,NULL,\
WS_VISIBLE or WS_CHILD or ES_LEFT or ES_MULTILINE or\
ES_AUTOHSCROLL or ES_AUTOVSCROLL,0,\
0,0,0,hWnd,EditID,\
hInstance,NULL
mov hwndEdit,eax
invoke SetFocus,hwndEdit
;==============================================
; Initialize the members of OPENFILENAME structure
;==============================================
mov ofn.lStructSize,SIZEOF ofn
push hWnd
pop ofn.hWndOwner
push hInstance
pop ofn.hInstance
mov ofn.lpstrFilter, OFFSET FilterString
mov ofn.lpstrFile, OFFSET buffer
mov ofn.nMaxFile,MAXSIZE
.ELSEIF uMsg==WM_SIZE
mov eax,lParam
mov edx,eax
shr edx,16
and eax,0ffffh
invoke MoveWindow,hwndEdit,0,0,eax,edx,TRUE
.ELSEIF uMsg==WM_DESTROY
invoke PostQuitMessage,NULL
.ELSEIF uMsg==WM_COMMAND
mov eax,wParam
.if lParam==0
.if ax==IDM_OPEN
mov ofn.Flags, OFN_FILEMUSTEXIST or \
OFN_PATHMUSTEXIST or OFN_LONGNAMES or\
OFN_EXPLORER or OFN_HIDEREADONLY
invoke GetOpenFileName, ADDR ofn
.if eax==TRUE
invoke CreateFile,ADDR buffer,\
GENERIC_READ or GENERIC_WRITE ,\
FILE_SHARE_READ or FILE_SHARE_WRITE,\
NULL,OPEN_EXISTING,FILE_ATTRIBUTE_ARCHIVE,\
NULL
mov hFile,eax
invoke GlobalAlloc,GMEM_MOVEABLE or GMEM_ZEROINIT,MEMSIZE
mov hMemory,eax
invoke GlobalLock,hMemory
mov pMemory,eax
invoke ReadFile,hFile,pMemory,MEMSIZE-1,ADDR SizeReadWrite,NULL
invoke SendMessage,hwndEdit,WM_SETTEXT,NULL,pMemory
invoke CloseHandle,hFile
invoke GlobalUnlock,pMemory
invoke GlobalFree,hMemory
.endif
invoke SetFocus,hwndEdit
.elseif ax==IDM_SAVE
mov ofn.Flags,OFN_LONGNAMES or\
OFN_EXPLORER or OFN_HIDEREADONLY
invoke GetSaveFileName, ADDR ofn
.if eax==TRUE
invoke CreateFile,ADDR buffer,\
GENERIC_READ or GENERIC_WRITE ,\
FILE_SHARE_READ or FILE_SHARE_WRITE,\
NULL,CREATE_NEW,FILE_ATTRIBUTE_ARCHIVE,\
NULL
mov hFile,eax
invoke GlobalAlloc,GMEM_MOVEABLE or GMEM_ZEROINIT,MEMSIZE
mov hMemory,eax
invoke GlobalLock,hMemory
mov pMemory,eax
invoke SendMessage,hwndEdit,WM_GETTEXT,MEMSIZE-1,pMemory
invoke WriteFile,hFile,pMemory,eax,ADDR SizeReadWrite,NULL
invoke CloseHandle,hFile
invoke GlobalUnlock,pMemory
invoke GlobalFree,hMemory
.endif
invoke SetFocus,hwndEdit
.else
invoke DestroyWindow, hWnd
.endif
.endif
.ELSE
invoke DefWindowProc,hWnd,uMsg,wParam,lParam
ret
.ENDIF
xor eax,eax
ret
WndProc endp
end start

分析：

invoke CreateWindowEx,NULL,ADDR EditClass,NULL,\
WS_VISIBLE or WS_CHILD or ES_LEFT or ES_MULTILINE or\
ES_AUTOHSCROLL or ES_AUTOVSCROLL,0,\
0,0,0,hWnd,EditID,\
hInstance,NULL
mov hwndEdit,eax

处理 WM_CREATE消息时，我们创建一个编辑控件。请注意，我们把该控件大小的有关参数都设成0，因为我们稍后将重新设置该编辑控件的大小，使得其覆盖父窗口的整个客户区。
注意：本例中我们没有必要调用ShowWindow来显示编辑控件，因为在创建时在其风格中已设置了WS_VISIBLE标志位，在创建父窗口时也可以使用这个小技巧。

;==============================================
; Initialize the members of OPENFILENAME structure
;==============================================
mov ofn.lStructSize,SIZEOF ofn
push hWnd
pop ofn.hWndOwner
push hInstance
pop ofn.hInstance
mov ofn.lpstrFilter, OFFSET FilterString
mov ofn.lpstrFile, OFFSET buffer
mov ofn.nMaxFile,MAXSIZE

创建完编辑控件后，我们初始话ofn变量的成员。因为稍后在保存文件时还要使用该结构体变量，所以此处只初始化要用到的公共部分。WM_CREATE 消息的处理部分是进行这种初始化的绝佳之处。

.ELSEIF uMsg==WM_SIZE
mov eax,lParam
mov edx,eax
shr edx,16
and eax,0ffffh
invoke MoveWindow,hwndEdit,0,0,eax,edx,TRUE

当主窗口的客户区部分大小改变时，我们的应用程序将接收到WM_SIZE 消息。当然该窗口第一次显示时，我们也将接收到该消息。要接收到该消息，主窗口必须有CS_VREDRAW和CS_HREDRAW风格。我们应该把缩放编辑控件的动作放到此处。我们要把编辑控件变成和我们的窗口客户区一样大，所以先得要得到父窗口客户区的大小。这些值包含在参数lParam中，lParam的高字部分是客户区的高，底字部分是客户区的宽。然后我们调用MoveWindow函数来重新调整编辑控件的大小，该函数不仅能够移动窗口的位置，而且能够改变窗口的大小。

.if ax==IDM_OPEN
mov ofn.Flags, OFN_FILEMUSTEXIST or \
OFN_PATHMUSTEXIST or OFN_LONGNAMES or\
OFN_EXPLORER or OFN_HIDEREADONLY
invoke GetOpenFileName, ADDR ofn

当用户选择了File/Open菜单项时，我们填充ofn的其他成员，然后调用GetOpenFileName函数显示一个“打开文件”对话框。

.if eax==TRUE
invoke CreateFile,ADDR buffer,\
GENERIC_READ or GENERIC_WRITE ,\
FILE_SHARE_READ or FILE_SHARE_WRITE,\
NULL,OPEN_EXISTING,FILE_ATTRIBUTE_ARCHIVE,\
NULL
mov hFile,eax

如果用户选择了一个文件时，我们调用CreateFile函数来打开。我们设置标志位来让该函数的文件能够读写。文件打开后我们把返回的文件句柄保存在一个全局变量中以便以后使用。CreateFile函数应用非常广泛，其原型如下：

CreateFile proto lpFileName:DWORD,\
dwDesiredAccess:DWORD,\
dwShareMode:DWORD,\
lpSecurityAttributes:DWORD,\
dwCreationDistribution:DWORD\,
dwFlagsAndAttributes:DWORD\,
hTemplateFile:DWORD

dwDesiredAccess 指定想要进行的操作。

  • 0 打开文件查询它的属性。
  • GENERIC_READ 打开文件读
  • GENERIC_WRITE 打开文件写.
dwShareMode 指定文件的共享模式。
  • 0 不让其他进程共享，即当您打开该文件后，其他进程欲打开该文件时将失败。
  • FILE_SHARE_READ 允许其他进程读。
  • FILE_SHARE_WRITE 允许其他进程写。

lpSecurityAttributes 该属性在WIN95下无效。
dwCreationDistribution 指定欲生成的文件在其已存在和未存在时应做的动作。

  • CREATE_NEW 生成一个新文件。如果文件已存在则失败。
  • CREATE_ALWAYS 无论文件是否存在都生成一个新文件。
  • OPEN_EXISTING 打开存在的文件。如果文件不存在则失败。
  • OPEN_ALWAYS 打开文件，如果该文件不存在则生成，这和在dwCreationDistribution 中设置 CREATE_NEW标志位一样。
  • TRUNCATE_EXISTING打开文件。打开时该文件的长度裁减到零（也即完全不要原来的文件了）。这要求调用进程必须有GENERIC_WRITE的权利，如果指定的文件不存在，该函数返回失败。

dwFlagsAndAttributes 指定文件的属性。

  • FILE_ATTRIBUTE_ARCHIVE 该文件具有一般的归档文件的属性。用户可以用该标志位来标记文件的删除和备份。
  • FILE_ATTRIBUTE_COMPRESSED 文件或目录是压缩的。对于文件来说是压缩其中的所有数据，而对于目录来说新生成的子目录和文件都要压缩。
  • FILE_ATTRIBUTE_NORMAL 该文件没有一般的属性集。该标志位只能单独使用。
  • FILE_ATTRIBUTE_HIDDEN 该文件是隐藏文件，当浏览一般的文件目录时将不显示它。
  • FILE_ATTRIBUTE_READONLY 该文件是只读文件。应用程序可以读其中的内容，但不可以写。
  • FILE_ATTRIBUTE_SYSTEM 该文件是系统文件。
invoke GlobalAlloc,GMEM_MOVEABLE or GMEM_ZEROINIT,MEMSIZE
mov hMemory,eax
invoke GlobalLock,hMemory
mov pMemory,eax

文件打开后，我们将分配一块内存供随后的API 函数ReadFile 和 WriteFile使用。我们使用标志GMEM_MOVEABLE来使得WINDOWS总是把内存块移到可靠的内存中去，GMEM_ZEROINIT告诉WINDOWS把刚刚分配的内存置为零。如果GlobalAlloc调用成功的话，会在eax中返回内存块的句柄，我们把该句柄传给GlobalLock函数以得到指向内存块的指针。

invoke ReadFile,hFile,pMemory,MEMSIZE-1,ADDR SizeReadWrite,NULL
invoke SendMessage,hwndEdit,WM_SETTEXT,NULL,pMemory

使内存块可用后，我们调用ReadFile函数从文件中读数据。对于第一次打开的文件，文件的指针放在偏移0处，像本例中我们从偏移0处往前读。ReadFile的第一个参数是文件句柄，第二个参数是指向内存块的指针，接下来的参数是要读的数据的长度，第四个参数是一个指向DWORD型的参数的指针，它用来存放实际读的数据的长度。读完了后，我们把这些内容存放到编辑控件中，这要用消息传递来完成，我们把消息WM_SETTEXT传给编辑控件，其中的参数lParam中包含指向内存块的指针。到此处，编辑控件就可以在它的客户区显示文件的内容了。

invoke CloseHandle,hFile
invoke GlobalUnlock,pMemory
invoke GlobalFree,hMemory
.endif

我们不再需要让文件打开了，因为我们的目的是把修改后的数据保存到另一个文件而不是先前的那一个文件中去。所以我们可以调用CloseHandle来关闭文件。接下来我们解锁内存块，再释放它。实际上我们可以暂不释放内存块，而在以后的操作中重新利用。我们为了演示的原由，选择了释放它。

invoke SetFocus,hwndEdit

当打开文件对话框显示在屏幕上时，输入的焦点切换到了该对话框上。所以在该对话框关闭后，我们必须把焦点切换到编辑控件上。 现在打开文件的阶段结束了，用户可以编辑他们打开的文件了。当用户想把修改后的内容保存到磁盘上时，必须选择File/Save菜单项，这时会显示一个保存文件对话框。显示保存文件对话框其实和打开打开文件对话框基本一样。您甚至可以认为他们的不同只是函数名称不一样而已。此处可以复用大多数ofn变量先前设置的成员的值。

mov ofn.Flags,OFN_LONGNAMES or\
OFN_EXPLORER or OFN_HIDEREADONLY

本例中我们将生成一个新文件，所以一定不能有 OFN_FILEMUSTEXIST 和 OFN_PATHMUSTEXIST标志位。dwCreationDistribution 参数应当有CREATE_NEW标志位。 接下来的代码和打开问对话框基本一样。最后调用：

invoke SendMessage,hwndEdit,WM_GETTEXT,MEMSIZE-1,pMemory
invoke WriteFile,hFile,pMemory,eax,ADDR SizeReadWrite,NULL

现在我们把修改后的数据从编辑控件中写回内存块，再从内存块写回新文件。

翻译：Lxx.阿龙，校对：LuoYunBin's Win32 ASM Page， http://asm.yeah.net/


Iczelion's win32 asm tutorial

第十三课 内存映射文件

本课中我们将要讲解内存映射文件并且演示如何运用它。您将会发现使用内存映射文件是非常简单的。

理论：

如果您仔细地研究了前一课的例子， 就会发现它有一个严重的缺陷：如果您想读的内容大于系统分配的内存块怎么办？如果您想搜索的字符串刚好超过内存块的边界又该如何处理？对于第一个问题，您也许会说，只要不断地读就不解决了吗。至于第二个问题，您又会说在内存块的边界处做一些特别的处理，譬如放上一些标志位就可以了。原理上确实是行得通，但是这随问题复杂程度加深而显得非常难以处理。其中的第二个问题是有名的边界判断问题，程序中许许多多的错误都是由此引起。想一想，如果我们能够分配一个能够容纳整个文件的大内存块该多好啊，这样这两个问题不都迎刃而解了吗？是的，WIN32的内存映射文件确实允许我们分配一个装得下现实中可能存在的足够大的文件的内存。

利用内存映射文件您可以认为操作系统已经为您把文件全部装入了内存，然后您只要移动文件指针进行读写即可了。这样您甚至不需要调用那些分配、释放内存块和文件输入/输出的API函数，另外您可以把这用作不同的进程之间共享数据的一种办法。运用内存映射文件实际上没有涉及实际的文件操作，它更象为每个进程保留一个看得见的内存空间。至于把内存映射文件当成进程间共享数据的办法来用，则要加倍小心，因为您不得不处理数据的同步问题，否则您的应用程序也许很可能得到过时或错误的数据甚至崩溃。本课中我们将主要讲述内存映射文件，将不涉及进程间的同步。WIN32中的内存映射文件应用非常广泛，譬如：即使是系统的核心模块---PE格式文件装载器也用到了内存映射文件，因为PE格式的文件并不是一次性加载到内存中来的，譬如他它在首次加载时只加载必需加载的部分，而其他部分在用到时再加载，这正好可以利用到内存映射文件的长处。实际中的大多数文件存取都和PE加载器类似，所以您在处理该类问题时也应该充分利用内存映射文件。

内存映射文件本身还是有一些局限性的，譬如一旦您生成了一个内存映射文件，那么您在那个会话期间是不能够改变它的大小的。所以内存映射文件对于只读文件和不会影响其大小的文件操作是非常有用的。当然这并不意味着对于会引起改变其大小的文件操作就一定不能用内存影射文件的方法，您可以事先估计操作后的文件的可能大小，然后生成这么大小一块的内存映射文件，然后文件的长度就可以增长到这么一个大小。 我们的解释够多的了，接下来我们就看看实现的细节：

  1. 调用CreateFile打开您想要映射的文件。
  2. 调用CreateFileMapping，其中要求传入先前CreateFile返回的句柄，该函数生成一个建立在CreateFile函数创建的文件对象基础上的内存映射对象。
  3. 调用MapViewOfFile函数映射整个文件的一个区域或者整个文件到内存。该函数返回指向映射到内存的第一个字节的指针。
  4. 用该指针来读写文件。
  5. 调用UnmapViewOfFile来解除文件映射。
  6. 调用CloseHandle来关闭内存映射文件。注意必须传入内存映射文件的句柄。
  7. 调用CloseHandle来关闭文件。注意必须传入由CreateFile创建的文件的句柄。

例子：

下面的例子允许用户通过“打开文件”对话框来打开一个文件，然后用内存映射文件来打开该文件，如果成功，窗口的标题条会显示打开的文件的名称，您可以通过选择“File/Save”菜单项来把换名保存。该程序将会把打开的文件的内容存到新文件中去。注意，这整个过程您根本就没有用到GlobalAlloc这样的分配内存的函数。

.386
.model flat,stdcall
WinMain proto :DWORD,:DWORD,:DWORD,:DWORD
include \masm32\include\windows.inc
include \masm32\include\user32.inc
include \masm32\include\kernel32.inc
include \masm32\include\comdlg32.inc
includelib \masm32\lib\user32.lib
includelib \masm32\lib\kernel32.lib
includelib \masm32\lib\comdlg32.lib

.const
IDM_OPEN equ 1
IDM_SAVE equ 2
IDM_EXIT equ 3
MAXSIZE equ 260

.data
ClassName db "Win32ASMFileMappingClass",0
AppName db "Win32 ASM File Mapping Example",0
MenuName db "FirstMenu",0
ofn OPENFILENAME <>
FilterString db "All Files",0,"*.*",0
db "Text Files",0,"*.txt",0,0
buffer db MAXSIZE dup(0)
hMapFile HANDLE 0 ; Handle to the memory mapped file, must be
;initialized with 0 because we also use it as
;a flag in WM_DESTROY section too

.data?
hInstance HINSTANCE ?
CommandLine LPSTR ?
hFileRead HANDLE ? ; Handle to the source file
hFileWrite HANDLE ? ; Handle to the output file
hMenu HANDLE ?
pMemory DWORD ? ; pointer to the data in the source file
SizeWritten DWORD ? ; number of bytes actually written by WriteFile

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

WinMain proc hInst:HINSTANCE,hPrevInst:HINSTANCE,CmdLine:LPSTR,CmdShow:DWORD
LOCAL wc:WNDCLASSEX
LOCAL msg:MSG
LOCAL hwnd:HWND
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 hInst
pop wc.hInstance
mov wc.hbrBackground,COLOR_WINDOW+1
mov wc.lpszMenuName,OFFSET MenuName
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 wc
invoke CreateWindowEx,WS_EX_CLIENTEDGE,ADDR ClassName,\
ADDR AppName, WS_OVERLAPPEDWINDOW,CW_USEDEFAULT,\
CW_USEDEFAULT,300,200,NULL,NULL,\
hInst,NULL
mov hwnd,eax
invoke ShowWindow, hwnd,SW_SHOWNORMAL
invoke UpdateWindow, hwnd
.WHILE TRUE
invoke GetMessage, ADDR msg,NULL,0,0
.BREAK .IF (!eax)
invoke TranslateMessage, ADDR msg
invoke DispatchMessage, ADDR msg
.ENDW
mov eax,msg.wParam
ret
WinMain endp

WndProc proc hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM
.IF uMsg==WM_CREATE
invoke GetMenu,hWnd ;Obtain the menu handle
mov hMenu,eax
mov ofn.lStructSize,SIZEOF ofn
push hWnd
pop ofn.hWndOwner
push hInstance
pop ofn.hInstance
mov ofn.lpstrFilter, OFFSET FilterString
mov ofn.lpstrFile, OFFSET buffer
mov ofn.nMaxFile,MAXSIZE
.ELSEIF uMsg==WM_DESTROY
.if hMapFile!=0
call CloseMapFile
.endif
invoke PostQuitMessage,NULL
.ELSEIF uMsg==WM_COMMAND
mov eax,wParam
.if lParam==0
.if ax==IDM_OPEN
mov ofn.Flags, OFN_FILEMUSTEXIST or \
OFN_PATHMUSTEXIST or OFN_LONGNAMES or\
OFN_EXPLORER or OFN_HIDEREADONLY
invoke GetOpenFileName, ADDR ofn
.if eax==TRUE
invoke CreateFile,ADDR buffer,\
GENERIC_READ ,\
0,\
NULL,OPEN_EXISTING,FILE_ATTRIBUTE_ARCHIVE,\
NULL
mov hFileRead,eax
invoke CreateFileMapping,hFileRead,NULL,PAGE_READONLY,0,0,NULL
mov hMapFile,eax
mov eax,OFFSET buffer
movzx edx,ofn.nFileOffset
add eax,edx
invoke SetWindowText,hWnd,eax
invoke EnableMenuItem,hMenu,IDM_OPEN,MF_GRAYED
invoke EnableMenuItem,hMenu,IDM_SAVE,MF_ENABLED
.endif
.elseif ax==IDM_SAVE
mov ofn.Flags,OFN_LONGNAMES or\
OFN_EXPLORER or OFN_HIDEREADONLY
invoke GetSaveFileName, ADDR ofn
.if eax==TRUE
invoke CreateFile,ADDR buffer,\
GENERIC_READ or GENERIC_WRITE ,\
FILE_SHARE_READ or FILE_SHARE_WRITE,\
NULL,CREATE_NEW,FILE_ATTRIBUTE_ARCHIVE,\
NULL
mov hFileWrite,eax
invoke MapViewOfFile,hMapFile,FILE_MAP_READ,0,0,0
mov pMemory,eax
invoke GetFileSize,hFileRead,NULL
invoke WriteFile,hFileWrite,pMemory,eax,ADDR SizeWritten,NULL
invoke UnmapViewOfFile,pMemory
call CloseMapFile
invoke CloseHandle,hFileWrite
invoke SetWindowText,hWnd,ADDR AppName
invoke EnableMenuItem,hMenu,IDM_OPEN,MF_ENABLED
invoke EnableMenuItem,hMenu,IDM_SAVE,MF_GRAYED
.endif
.else
invoke DestroyWindow, hWnd
.endif
.endif
.ELSE
invoke DefWindowProc,hWnd,uMsg,wParam,lParam
ret
.ENDIF
xor eax,eax
ret
WndProc endp

CloseMapFile PROC
invoke CloseHandle,hMapFile
mov hMapFile,0
invoke CloseHandle,hFileRead
ret
CloseMapFile endp

end start

分析：

invoke CreateFile,ADDR buffer,\
GENERIC_READ ,\
0,\
NULL,OPEN_EXISTING,FILE_ATTRIBUTE_ARCHIVE,\
NULL

当用户选择打开文件时，我们调用CreateFile来打开。注意我们指定GENERIC_READ（一般的读）来表示我们打开的文件只能够读出，把dwShareMode设成0，表示我们不想其他进程在我们操作文件时来存取该文件。

invoke CreateFileMapping,hFileRead,NULL,PAGE_READONLY,0,0,NULL

我们调用CreateFileMapping来在打开的文件的基础上生成内存映射文件。CreateFileMapping的语法如下：

CreateFileMapping proto hFile:DWORD,\
lpFileMappingAttributes:DWORD,\
flProtect:DWORD,\
dwMaximumSizeHigh:DWORD,\
dwMaximumSizeLow:DWORD,\
lpName:DWORD

您应当知道该函数并没有必要把整个文件映射到内存中去，您可以用该函数来只映射文件的一部分。您可以在参数dwMaximumSizeHigh和dwMaximumSizeLow中指定内存映射文件的大小，如果您指定的值大于实际的文件，则实际的文件将增长到指定的大小，如果想要映射的内存大小正好和文件的实际大小相等，则把两个参数中都设成为0。您可以设定lpFileMappingAttributes为NULL，让WINDOWS赋予该内存映射文件于缺省的安全属性。
flProtect定义了内存映射文件的保护属性，我们指定它为PAGE_READONLY来规定该内存映射文件只能够读。注意该属性不能和CreateFile中指定的属性相矛盾，否则就不能生成内存映射文件。
lpName指定内存映射文件的名称，如果您想要该内存映射文件同时可以供其它的进程使用，就必须给它取个名称。不过在我们的例子中，只有我们的进程使用该内存映射文件故我们忽略该参数。

mov eax,OFFSET buffer
movzx edx,ofn.nFileOffset
add eax,edx
invoke SetWindowText,hWnd,eax

如果函数CreateFileMapping调用成功，我们把窗口的标题条换成被打开文件的名称。保存在缓冲区中的文件名是带有路径的全文件名，所以为了只显示文件名我们需要利用OPENFILENAME结构体中的成员nFileOffset的值来找到文件名的起始地址。

invoke EnableMenuItem,hMenu,IDM_OPEN,MF_GRAYED
invoke EnableMenuItem,hMenu,IDM_SAVE,MF_ENABLED

为了避免用户一次性打开多个文件，我们让“打开文件”菜单项呈灰色显示，使得打开文件的菜单项失效。函数EnableMenuItem可以用来改变菜单项的属性。 之后用户可能保存文件或者直接关闭应用程序。如果用户选择关闭应用程序，则事先必须关闭内存映射文件和打开的文件， 代码如下:

.ELSEIF uMsg==WM_DESTROY
.if hMapFile!=0
call CloseMapFile
.endif
invoke PostQuitMessage,NULL

在上面的代码段中，当WINDOWS的消息处理过程接收到WM_DESTROY消息后，它首先检测hMapFile值是否为0。如果不为0则表示相关的文件未关闭，这样就需要调用CloseMapFile来关闭它们。

CloseMapFile PROC
invoke CloseHandle,hMapFile
mov hMapFile,0
invoke CloseHandle,hFileRead
ret
CloseMapFile endp

上述过程调用是用来关闭内存映射文件和原来打开的文件的，这样可以使得程序退出时没有资源泄漏。如果用户选择保存文件的话，就弹出一个“保存文件”对话框，当用户输入了新文件的名称后，我们调用CreateFile函数来创建新文件---输出文件。

invoke MapViewOfFile,hMapFile,FILE_MAP_READ,0,0,0
mov pMemory,eax

在输出文件创建后我们调用MapViewOfFile来映射希望映射到内存中的部分。该函数的语法如下:

MapViewOfFile proto hFileMappingObject:DWORD,\
dwDesiredAccess:DWORD,\
dwFileOffsetHigh:DWORD,\
dwFileOffsetLow:DWORD,\
dwNumberOfBytesToMap:DWORD

dwDesiredAccess用来指定我们想对文件进行的操作。在我们例子中，我们只想读，故指定标志FILE_MAP_READ。
dwFileOffsetHigh 和 dwFileOffsetLow 用来指定打开文件中欲映射的起始偏移位置。我们的例子中想映射整个的文件，故指定它们的值为0。
dwNumberOfBytesToMap 用来指定欲映射的字节数，如果想映射整个的文件，设定该值为0。
调用MapViewOfFile后，我们希望的部分就已经映射到内存中去了。您将得到一个指向起始内存块的指针。

invoke GetFileSize,hFileRead,NULL

调用该函数可以得到文件的大小，其值通过eax传送，如果文件的长度超过4G，那么文件长度DWORD的高值部分（也即超过4G的部分）保存在FileSizeHighWord中。因为我们估计一般的文件将没有这么大，故忽略该值。

invoke WriteFile,hFileWrite,pMemory,eax,ADDR SizeWritten,NULL

把内存映射文件中的数据写到输出文件中去。

invoke UnmapViewOfFile,pMemory

写完后，我们解除映射。

call CloseMapFile
invoke CloseHandle,hFileWrite

关闭内存映射文件和输出文件的句柄。

invoke SetWindowText,hWnd,ADDR AppName

恢复窗口的标题条到应用程序的名称。

invoke EnableMenuItem,hMenu,IDM_OPEN,MF_ENABLED
invoke EnableMenuItem,hMenu,IDM_SAVE,MF_GRAYED

恢复“打开文件”和“保存文件”菜单项使的可以重新开始新的打开、编辑和保存循环。

翻译：Lxx.阿龙，校对：LuoYunBin's Win32 ASM Page， http://asm.yeah.net/


Iczelion's win32 asm tutorial

第十四课 进程

本课中我们将学习：什么是进程？如何产生和终止一个进程？

初步知识：

进程是什么？下面是我从WIN32 API指南中节选的解释：
“一个进程是一个正在执行的应用程序，它包含有：私有的虚拟地址空间、代码、数据和其它的操作系统资源，譬如进程可以存取的管道、文件和同步对象等等。”

从上面的定义中您可以看到，一个进程拥有几个对象：地址空间、执行模块和其它该执行程序打开或创建的任何对象或资源。至少，一个进程必须包含可执行模块、私有的地址空间和一个以上的线程。什么是线程呢？一个线程实际上是一个执行单元。当WINDOWS产生一个进程时，它自动为该进程产生一个主线程。该线程通常从模块的第一条指令处开始执行。如果进程需要更多的线程，它可以随后显式地产生。
当WINDWOS 接收到产生进程的消息时，它会为进程生成私有内存地址空间，接着把可执行文件映射到该空间。在WIN32下为进程产生了主进程后，您还可以调用函数CreateProcess来为您的进程产生更多的线程。

CreateProcess的原型如下：

CreateProcess proto lpApplicationName:DWORD,\
lpCommandLine:DWORD,\
lpProcessAttributes:DWORD,\
lpThreadAttributes:DWORD,\
bInheritHandles:DWORD,\
dwCreationFlags:DWORD,\
lpEnvironment:DWORD,\
lpCurrentDirectory:DWORD,\
lpStartupInfo:DWORD,\
lpProcessInformation:DWORD

不要被这么多的参数吓倒，其实您可以忽略其中的大多数的参数（让它们有缺省值）。

lpApplicationName --> 可执行文件的名称（含或不含路径）。如果该参数为NULL，那必须在参数lpCommandLine中传递文件名称。
lpCommandLine --> 传递给欲执行的文件的命令行参数。如果lpApplicationName为NULL，那必须在该参数中指定，譬如："notepad.exe readme.txt" 。
lpProcessAttributes 和 lpthreadAttributes --> 指定进程和主线程的安全属性。您可以把它们都设成为NULL，这样就设置了缺省的安全属性。
bInheritHandles --> 标志位。用来设置新进程是否继承创建进程所有的打开句柄。
dwCreationFlags --> 有几个标志可以在此处设置以决定欲创建进程的行为，譬如：您可能想创建进程后并不想让它立刻运行，这样在它真正运行前可以作一些检查和修改工作。您还可以在此处设置新进程中的所有线程的优先级，通常我们把它设置为NORMAL_PRIORITY_CLASS。
lpEnvironment --> 指向环境块的指针，一般地环境块包含几个环境字符串。如果该参数为NULL，那么新进程继承创建进程的环境块。
lpCurrentDirectory --> 指向当前目录以及为子进程设置的“当前目录”的路径。如果为NULL， 则继承创建进程的“当前目录”路径。
lpStartupInfo --> 指向新进程的启动结构体STARTUPINFO的指针。STARTUPINFO告诉WINDOWS如何显示新进程的外观。该参数有许多的成员变量，如果您不想新进程有什么的特别之处，可以调用GetStartupInfo函数来用创建进程的启动参数来填充STARTUPINFO结构体变量。
lpProcessInformation --> 指向结构体PROCESS_INFORMATION的指针，该结构体变量包含了一些标识该进程唯一性的一些成员变量：

PROCESS_INFORMATION STRUCT
hProcess HANDLE ? ; handle to the child process
hThread HANDLE ? ; handle to the primary thread of the child process
dwProcessId DWORD ? ; ID of the child process
dwThreadId DWORD ? ; ID of the primary thread of the child process
PROCESS_INFORMATION ENDS

进程句柄和进程ID是两个不同的概念。进程ID好似一个唯一值，而进程句柄是调用相关的WINDOWS API 后得到的一个返回值。不能用进程句柄来标识一个进程的唯一性，因为这个值并不唯一。在调用CreateProcess产生新进程后，该进程就被创建，而且CerateProcess函数立即返回。您可以调用函数GetExitCodeProcess来检验进程是否结束。该函数的原型如下：

GetExitCodeProcess proto hProcess:DWORD, lpExitCode:DWORD

如果调用成功，lpExitCode中包含了所查询进程的状态码。如果等于STILL_ACTIVE就表明该进程依旧存在。 您可以调用函数TerminateProcess来强制终止一个进程。该函数的原型如下：

TerminateProcess proto hProcess:DWORD, uExitCode:DWORD

您可以指定任意一个退出值。用该函数结束一个进程并不好，因为该进程加载的动态连接库并不会得到进程正退出的消息。

例子：

在下面的例子中，当用户选择菜单项“crate process”时我们创建一个新进程。它会去执行“"msgbox.exe”。如果用户想要终止新进程，可以选择菜单项“terminate process”。这时，应用程序检查欲终止的进程是否仍存在，若存在则调用TerminateProcess函数来终止它。

.386
.model flat,stdcall
option casemap:none
WinMain proto :DWORD,:DWORD,:DWORD,:DWORD
include \masm32\include\windows.inc
include \masm32\include\user32.inc
include \masm32\include\kernel32.inc
includelib \masm32\lib\user32.lib
includelib \masm32\lib\kernel32.lib

.const
IDM_CREATE_PROCESS equ 1
IDM_TERMINATE equ 2
IDM_EXIT equ 3

.data
ClassName db "Win32ASMProcessClass",0
AppName db "Win32 ASM Process Example",0
MenuName db "FirstMenu",0
processInfo PROCESS_INFORMATION <>
programname db "msgbox.exe",0

.data?
hInstance HINSTANCE ?
CommandLine LPSTR ?
hMenu HANDLE ?
ExitCode DWORD ? ; contains the process exitcode status from GetExitCodeProcess call.

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

WinMain proc hInst:HINSTANCE,hPrevInst:HINSTANCE,CmdLine:LPSTR,CmdShow:DWORD
LOCAL wc:WNDCLASSEX
LOCAL msg:MSG
LOCAL hwnd:HWND
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 hInst
pop wc.hInstance
mov wc.hbrBackground,COLOR_WINDOW+1
mov wc.lpszMenuName,OFFSET MenuName
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 wc
invoke CreateWindowEx,WS_EX_CLIENTEDGE,ADDR ClassName,ADDR AppName,\
WS_OVERLAPPEDWINDOW,CW_USEDEFAULT,\
CW_USEDEFAULT,300,200,NULL,NULL,\
hInst,NULL
mov hwnd,eax
invoke ShowWindow, hwnd,SW_SHOWNORMAL
invoke UpdateWindow, hwnd
invoke GetMenu,hwnd
mov hMenu,eax
.WHILE TRUE
invoke GetMessage, ADDR msg,NULL,0,0
.BREAK .IF (!eax)
invoke TranslateMessage, ADDR msg
invoke DispatchMessage, ADDR msg
.ENDW
mov eax,msg.wParam
ret
WinMain endp

WndProc proc hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM
LOCAL startInfo:STARTUPINFO
.IF uMsg==WM_DESTROY
invoke PostQuitMessage,NULL
.ELSEIF uMsg==WM_INITMENUPOPUP
invoke GetExitCodeProcess,processInfo.hProcess,ADDR ExitCode
.if eax==TRUE
.if ExitCode==STILL_ACTIVE
invoke EnableMenuItem,hMenu,IDM_CREATE_PROCESS,MF_GRAYED
invoke EnableMenuItem,hMenu,IDM_TERMINATE,MF_ENABLED
.else
invoke EnableMenuItem,hMenu,IDM_CREATE_PROCESS,MF_ENABLED
invoke EnableMenuItem,hMenu,IDM_TERMINATE,MF_GRAYED
.endif
.else
invoke EnableMenuItem,hMenu,IDM_CREATE_PROCESS,MF_ENABLED
invoke EnableMenuItem,hMenu,IDM_TERMINATE,MF_GRAYED
.endif
.ELSEIF uMsg==WM_COMMAND
mov eax,wParam
.if lParam==0
.if ax==IDM_CREATE_PROCESS
.if processInfo.hProcess!=0
invoke CloseHandle,processInfo.hProcess
mov processInfo.hProcess,0
.endif
invoke GetStartupInfo,ADDR startInfo
invoke CreateProcess,ADDR programname,NULL,NULL,NULL,FALSE,\
NORMAL_PRIORITY_CLASS,\
NULL,NULL,ADDR startInfo,ADDR processInfo
invoke CloseHandle,processInfo.hThread
.elseif ax==IDM_TERMINATE
invoke GetExitCodeProcess,processInfo.hProcess,ADDR ExitCode
.if ExitCode==STILL_ACTIVE
invoke TerminateProcess,processInfo.hProcess,0
.endif
invoke CloseHandle,processInfo.hProcess
mov processInfo.hProcess,0
.else
invoke DestroyWindow,hWnd
.endif
.endif
.ELSE
invoke DefWindowProc,hWnd,uMsg,wParam,lParam
ret
.ENDIF
xor eax,eax
ret
WndProc endp
end start

分析：

应用程序创建主窗口，保存菜单句柄以备后用。当用户在主菜单中选择了“Process”菜单项后，消息处理过程中接收到WM_INITMENUPOPUP消息，我们在此处修改弹出式菜单中的菜单项的“使能”和“非使能”，以便同一菜单有不同的显示。

.ELSEIF uMsg==WM_INITMENUPOPUP
invoke GetExitCodeProcess,processInfo.hProcess,ADDR ExitCode
.if eax==TRUE
.if ExitCode==STILL_ACTIVE
invoke EnableMenuItem,hMenu,IDM_CREATE_PROCESS,MF_GRAYED
invoke EnableMenuItem,hMenu,IDM_TERMINATE,MF_ENABLED
.else
invoke EnableMenuItem,hMenu,IDM_CREATE_PROCESS,MF_ENABLED
invoke EnableMenuItem,hMenu,IDM_TERMINATE,MF_GRAYED
.endif
.else
invoke EnableMenuItem,hMenu,IDM_CREATE_PROCESS,MF_ENABLED
invoke EnableMenuItem,hMenu,IDM_TERMINATE,MF_GRAYED
.endif

我们之所以处理该消息的目的就是让菜单显示时有不同的外观以方便用户的使用。譬如；新进程尚未运行时，我们就变亮（使能）“菜单项“start process”，而变灰（非使能）菜单项“terminate process”。当新进程运行起来后，菜单的外观就应该是相反的。
首先我们调用GetExitCodeProcess函数，其中传入由CreateProcess返回的句柄。如果GetExitCodeProcess返回FALSE，则表示进程尚未运行，我们就让菜单项“terminate process”变灰；如果返回TRUE，表示新进程已经启动了，我们再检测是否正在运行，这通过比较ExitCode是否等于STILL_ACTIVE 来完成，如果相等，表示进程仍在运行，我们就让菜单项“start process”变灰，因为在我们的简单的应用程序中不提供同时运行多个进程的能力。

.if ax==IDM_CREATE_PROCESS
.if processInfo.hProcess!=0
invoke CloseHandle,processInfo.hProcess
mov processInfo.hProcess,0
.endif
invoke GetStartupInfo,ADDR startInfo
invoke CreateProcess,ADDR programname,NULL,NULL,NULL,FALSE,\
NORMAL_PRIORITY_CLASS,\
NULL,NULL,ADDR startInfo,ADDR processInfo
invoke CloseHandle,processInfo.hThread

当用户选择了菜单项“start process”时，我们先检测结构体PROCESS_INFORMATION中的成员变量hPRocess是否已经关闭。如果是第一次启动应用程序，那该变量为0，因为我们在.data分段定义结构体时已经初始化该值为0。如果该值不为0，则表明新进程已经结束，但是我们尚未关闭该进程的句柄（以减少该进程的引用记数），我们在此处完成该动作。
我们调用GetStartupInfo函数来填充启动信息的结构体变量，而该变量将被传递到CreateProcess函数中去。调用CreateProcess生成新进程，我们不检查该函数的返回值为的是让问题简化一些，在实际应用中，必须做该项工作。在调用CreateProcess后，我们立即关闭在进程信息结构体参数中返回的主线程句柄，关闭线程句柄为的是减少该内核对象的引用记数，否则即使该线程退出后，其内核对象仍惨存在内核中得不到释放，这会引起资源泄露。进程其实也是一样，之所以我们不在该处关闭进程的句柄是因为稍后我们还要用该句柄去得到一些和进程相关的信息，至于线程，我们的应用程序不需要其相关信息。

.elseif ax==IDM_TERMINATE
invoke GetExitCodeProcess,processInfo.hProcess,ADDR ExitCode
.if ExitCode==STILL_ACTIVE
invoke TerminateProcess,processInfo.hProcess,0
.endif
invoke CloseHandle,processInfo.hProcess
mov processInfo.hProcess,0

当用户选择了菜单项“terminate process”后，我们调用函数GetExitCodeProcess来检查新进程是否还存在，如果还存在我们就调用函数TerminateProcess来结束它。另外我们把它的句柄关闭掉，因为我们再也不用它了。

翻译：Lxx.阿龙，校对：LuoYunBin's Win32 ASM Page， http://asm.yeah.net/


Iczelion's win32 asm tutorial

第十五课 多线程编程

本课中，我们将学习如何进行多线程编程。另外我们还将学习如何在不同的线程间进行通信。

理论：

前一课中，我们学习了进程，其中讲到每一个进程至少要有一个主线程。这个线程其实是进程执行的一条线索，除此主线程外您还可以给进程增加其它的线程，也即增加其它的执行线索，由此在某种程度上可以看成是给一个应用程序增加了多任务功能。当程序运行后，您可以根据各种条件挂起或运行这些线程，尤其在多CPU的环境中，这些线程是并发运行的。这些是在W32下才有的概念，在WIN16下并没有等同的概念。
在同一进程中运行不同的线程的好处是这些线程可以共享进程的资源，如全局变量、资源等。当然各个线程也可以有自己的私有栈用于保存私有数据。另外每个线程需要保存其运行上下文以便在线程切换时能够记住或恢复其上下文，当然这是由操作系统来完成的，对于用户是透明的。
我们大体上可以把线程分成两大类：
  1. 处理用户界面的线程：该类线程产生自己的窗口并负责处理相关的窗口消息。用户界面线程遵守WIN16下的互斥原则，即没一刻仅有一个用户界面线程使用USER和GDI库中的内核函数，也就是说当一个用户界面程序在进入GDI或USER中时，内核不允许重入。由此我们可以推论出WIN95的该部分内核的代码是遵守16位模式的。而WINOWS NT是纯的32位操作系统，所以不存在这个问题。
  2. 工作者线程：该类线程不用处理窗口界面，当然也就不用处理消息了。它一般都运行在后台干一些计算之类的粗，这大概也是把它叫做工作者线程的原因吧。

运用W32的多线程模式来编程，我们可以遵循某种策略：即让主线程仅来做用户界面的工作，而其它繁重的工作则交由工作者线程在后台完成。这就好比我们日常生活中的许多例子。譬如：政府管理者好比是用户界面线程，它负责听取民意，给职能部门分配工作，然后把工作成果汇报给公众。而具体的职能部门就是工作者线程，它负责完成下达的具体工作。如果让政府管理这来具体地做每一件事，它必须作一件事后再做另一项，那它就不能及时来听取和反馈民意。这样就无法管理好一个国家了。当然即使采用多线程制，政府管理部门也不一定就能管理好国家，但是程序却可以采用多线程机制来管理好她自己的工作。我们可以调用CreateThread函数来生成新线程。该函数的语法如下：

CreateThread proto lpThreadAttributes:DWORD,\
dwStackSize:DWORD,\
lpStartAddress:DWORD,\
lpParameter:DWORD,\
dwCreationFlags:DWORD,\
lpThreadId:DWORD

生成一个线程的函数和生成一个进程基本相同。
lpThreadAttributes -->如果您想要线程有缺省的安全属性，可以置该值为NULL。
dwStackSize --> 指定线程的堆栈大小。如果为0，那线程的大小和进程相同。
lpStartAddress--> 线程函数的起始地址。注意该函数仅接收一个32位的参数和返回一个32位的值。（该参数可以是一个指针，而且进程的线程可以直接存取进程定义全局变量，所以您大可不必担心不能如何把大量的参数传递给线程）。
lpParameter --> 传递给线程的上下文。
dwCreationFlags -->如果是0的话则表示创线程建后立即启动，相反的是标志位CREATE_SUSPENDED，这样您需要稍后显示地让该线程运行。
lpThreadId --> 内核给新生成的线程分配的线程ID。

如果生成线程成功的话，CreateThread函数就返回新线程的句柄。否则返回NULL。
如果没有给参数dwCreationFlags指定CREATE_SUSPENDED的话，该线程就会立即运行。如果不这样，我们上面说了，需要显示地启动该线程，要这样做您需要调用ResumeThread函数。
在线程返回后（线程的执行类似与执行一个函数，如果它调用了最后一条指令后，在汇编中是ret，那么该线程就结束了，除非您让它进入一个循环，譬如我们讲的用户界面线程就是如此，只不过它不退出的原因是进入的循环是在{while ( GetMessage（...）)...}中，如果您没有给它传递一个值为0的消息，那它可不会退出），系统会自动调用ExitThread函数透明地处理线程一些退出时的清理工作。当然您可以自己调用该函数，但似乎没有什么意义。要得到退出时的退出码，您可以调用GetExitCodeThread函数。
如果您想结束一个程序，可以调用TerminateThread函数，不过使用该函数要小心行事，因为该函数一旦被调用线程就会退出，这样它就没有机会来做清理自己的工作了。

现在我们来看看线程间的通讯机制。
总的说来一共有三种方法：

  • 使用全局变量
  • 使用Windows消息传递机制
  • 使用事件
上面我们说了线程会共享进程的资源,其中全局变量也包括在内，所以线程可以通过使用全局变量来通讯。但是这种办法的明显的缺点是在有多个线程存取同一个全局变量时，必须考虑同步的问题。譬如：有一个有十个成员变量的结构体，其中一个线程在对起赋值时，假设只更新了五个成员变量的值，这时内核的调度线程剥夺其运行权给另一个线程，这样接下来的线程如果想要用该全局结构体变量，它的值就显然不对了。另外多线程的程序也很难调试，尤其这些错误很隐蔽和很难复现时。如果两个线程都是用户界面线程时，用WINDOWS的消息机制来进行线程间的通讯是比较方便的.
您所要做的只是自定义一些windows消息（注意不要和windows的预定义的消息冲突），然后在线程之间传递可以了。您可以这样来定义消息，把WM_USER（它的值等于0x0400）当作基数，然后顺序地去加序号，譬如：

WM_MYCUSTOMMSG equ WM_USER+100h

小于WM_USER 的值是Windows系统的保留值，大于该值留给用户来使用。
如果其中有一个线程是工作者线程的话，那就不能用该种方法来进行通讯了，这是因为工作者线程没有消息队列。您应当用下面这种策略来进行工作者线程和用户界面线程之间的通讯：

User interface Thread ------> global variable(s)----> Worker thread
Worker Thread ------> custom window message(s) ----> User interface Thread

稍后我们的例子中将讲解这种通讯办法。
最后的办法是事件对象。您可以把事件对象看作是一种标志。如果事件对象的状态是无信号的话，说明该线程正在睡眠或挂起，在该种状态下系统是不会给该线程分配CPU时间片的。当一个线程的状态转成有信号时，WINDOWS就会唤醒该线程并且让它正常运行。

例子：

您可以下载例子并运行thread1.exe，然后激活菜单项"Savage Calculation"，然后程序开始执行指令"add eax,eax "，一共执行600,000,000次，您会发现在这个过程当中，用户界面将停止响应，您既不能使用菜单，也不能使用移动窗口。等到计算完成后，会弹出一个对话框，关闭掉对话框后窗口才可以和当初一样正常运行了。
为了避免这种不便，我们把计算的工作放入到一个单独的工作者线程中去，而主窗口仅仅响应用户的活动。您可以看到虽然用户界面的反应比平常时慢了，但还是可以工作的。

.386
.model flat,stdcall
option casemap:none
WinMain proto :DWORD,:DWORD,:DWORD,:DWORD
include \masm32\include\windows.inc
include \masm32\include\user32.inc
include \masm32\include\kernel32.inc
includelib \masm32\lib\user32.lib
includelib \masm32\lib\kernel32.lib

.const
IDM_CREATE_THREAD equ 1
IDM_EXIT equ 2
WM_FINISH equ WM_USER+100h

.data
ClassName db "Win32ASMThreadClass",0
AppName db "Win32 ASM MultiThreading Example",0
MenuName db "FirstMenu",0
SuccessString db "The calculation is completed!",0

.data?
hInstance HINSTANCE ?
CommandLine LPSTR ?
hwnd HANDLE ?
ThreadID DWORD ?

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

WinMain proc hInst:HINSTANCE,hPrevInst:HINSTANCE,CmdLine:LPSTR,CmdShow:DWORD
LOCAL wc:WNDCLASSEX
LOCAL msg:MSG
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 hInst
pop wc.hInstance
mov wc.hbrBackground,COLOR_WINDOW+1
mov wc.lpszMenuName,OFFSET MenuName
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 wc
invoke CreateWindowEx,WS_EX_CLIENTEDGE,ADDR ClassName,ADDR AppName,\
WS_OVERLAPPEDWINDOW,CW_USEDEFAULT,\
CW_USEDEFAULT,300,200,NULL,NULL,\
hInst,NULL
mov hwnd,eax
invoke ShowWindow, hwnd,SW_SHOWNORMAL
invoke UpdateWindow, hwnd
.WHILE TRUE
invoke GetMessage, ADDR msg,NULL,0,0
.BREAK .IF (!eax)
invoke TranslateMessage, ADDR msg
invoke DispatchMessage, ADDR msg
.ENDW
mov eax,msg.wParam
ret
WinMain endp

WndProc proc hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM
.IF uMsg==WM_DESTROY
invoke PostQuitMessage,NULL
.ELSEIF uMsg==WM_COMMAND
mov eax,wParam
.if lParam==0
.if ax==IDM_CREATE_THREAD
mov eax,OFFSET ThreadProc
invoke CreateThread,NULL,NULL,eax,\
0,\
ADDR ThreadID
invoke CloseHandle,eax
.else
invoke DestroyWindow,hWnd
.endif
.endif
.ELSEIF uMsg==WM_FINISH
invoke MessageBox,NULL,ADDR SuccessString,ADDR AppName,MB_OK
.ELSE
invoke DefWindowProc,hWnd,uMsg,wParam,lParam
ret
.ENDIF
xor eax,eax
ret
WndProc endp

ThreadProc PROC USES ecx Param:DWORD
mov ecx,600000000
Loop1:
add eax,eax
dec ecx
jz Get_out
jmp Loop1
Get_out:
invoke PostMessage,hwnd,WM_FINISH,NULL,NULL
ret
ThreadProc ENDP

end start

分析：

主程序的主线程是一个用户界面线程，它有一个普通窗口。用户选择菜单项"Create Thread"，程序就会产生一个线程：

.if ax==IDM_CREATE_THREAD
mov eax,OFFSET ThreadProc
invoke CreateThread,NULL,NULL,eax,\
NULL,0,\
ADDR ThreadID
invoke CloseHandle,eax

上面的代码段产生一个线程，线程的主体代码是函数ThreadProc，该函数和主线程并行运行。在调用成功后，CreateThread函数立即返回，ThreadProc也开始运行。因为我们不再用线程句柄，我们立即关闭它以避免内存泄漏。我们前面讲过关闭句柄不会终止线程的执行，而只是减少起引用计数。

ThreadProc PROC USES ecx Param:DWORD
mov ecx,600000000
Loop1:
add eax,eax
dec ecx
jz Get_out
jmp Loop1
Get_out:
invoke PostMessage,hwnd,WM_FINISH,NULL,NULL
ret
ThreadProc ENDP

我们看到上面的线程的代码仅仅是做简单的计数工作，因为我们设了一个很大的基数，所以该线程会持续一段您能感觉得到的时间，当结束后它会向主线程发送WM_FINISH消息。WM_FINISH消息是我们自己定义的，它的定义如下：

    WM_FINISH equ WM_USER+100h
WM_USER消息是我们能够使用的最小消息值。
显然我们一看到WM_FINISH，就能从字面上理解该消息的意义。主线程接收到该消息后，会弹出一个对话框告诉用户，计算线程已经结束了。
通过线程之间的通讯，用户可以多次选择"Create Thread"，那样就可以运行多个计算线程了。
本例子中，线程之间的通讯是单向的。如果您想让主线程也能向工作者线程发送消息的话，譬如加入一个菜单项来控制工作者线程的结束，您可以这样做：
  • add a menu item saying something like "Kill Thread" in the menu
  • a global variable which is used as a command flag. TRUE=Stop the thread, FALSE=continue the thread
  • Modify ThreadProc to check the value of the command flag in the loop.
设立一个全局变量，当线程启动前，我们设置它的值为FALSE，当用户激活了我们加的菜单项时，该值变成TRUE。在线程的代码段ThreadProc中每次减1前，判断该值，如果为TRUE的话线程就结束循环体中的计算并退出线程。

翻译：Lxx.阿龙，校对：LuoYunBin's Win32 ASM Page， http://asm.yeah.net/


Iczelion's win32 asm tutorial

第十六课 事件对象

本课中我们将要学习事件对象以及如何在多线程编程中如何使用同步对象。

理论：

上一课中我们演示了如何用WINDOWS消息在不同的线程之间进行通讯。另外的两种，即：使用全局变量和事件对象，将在本课中讲解。
事件对象就像一个开关：它只有两种状态---开和关。当一个事件处于”开”状态，我们称其为”有信号”否则称为”无信号”。您可以在一个线程的执行函数中创建一个事件对象，然后观察它的状态，如果是”无信号”就让该线程睡眠，这样该线程占用的CPU时间就比较少。
产生事件对象的函数如下：

CreateEvent proto lpEventAttributes:DWORD,\
bManualReset:DWORD,\
bInitialState:DWORD,\
lpName:DWORD

lpEventAttribute--> 如果是NULL值，产生的事件对象有缺省的安全属性。
bManualReset--> 如果想在每次调用WaitForSingleObject 后让WINDOWS为您自动地把事件地状态恢复为”无信号”状态，必须把该参数设为FALSE,否则，您必须每次调用ResetEvent函数来清除事件的信号。
bInitialState--> 刚刚产生事件对象时的状态。如果设为TRUE是”有信号”，否则是”无信号”。
lpName --> 事件对象的名称。您在OpenEvent函数中可能使用。

如果CreateEvent调用成功的话，会返回新生成的对象的句柄，否则返回NULL。
这里有两个API函数用来修改事件对象的信号状态：SetEvent和ResetEvent。前者把事件对象设为”有信号”状态，而后者正好相反。
在事件对象生成后，必须调用WaitForSingleObject来让线程进入等待状态，该函数的语法如下：

WaitForSingleObject proto hObject:DWORD, dwTimeout:DWORD

hObject -->指向同步对象的指针。事件对象其实是同步对象的一种。
dwTimeout --> 等待同步对象变成”有信号”前等待的时间，以毫秒计。当等待的时间超过该值后无信号同步对象仍处于”无信号”状态，线程不再等待，WaitForSingleObject函数会返回。如果想要线程一直等待，请把该参数设为INFINITE（该值等于0xffffffff)。

例子：

下面的例子显示了一个窗口，当用户选择了菜单项”run thread”后，线程开始简单的计数运算。结束后弹出一个对话框通知用户。在整个的计数期间，您可以选择菜单项”stop thread”来随时终止线程。

.386
.model flat,stdcall
option casemap:none
WinMain proto :DWORD,:DWORD,:DWORD,:DWORD
include \masm32\include\windows.inc
include \masm32\include\user32.inc
include \masm32\include\kernel32.inc
includelib \masm32\lib\user32.lib
includelib \masm32\lib\kernel32.lib

.const
IDM_START_THREAD equ 1
IDM_STOP_THREAD equ 2
IDM_EXIT equ 3
WM_FINISH equ WM_USER+100h

.data
ClassName db "Win32ASMEventClass",0
AppName db "Win32 ASM Event Example",0
MenuName db "FirstMenu",0
SuccessString db "The calculation is completed!",0
StopString db "The thread is stopped",0
EventStop BOOL FALSE

.data?
hInstance HINSTANCE ?
CommandLine LPSTR ?
hwnd HANDLE ?
hMenu HANDLE ?
ThreadID DWORD ?
ExitCode DWORD ?
hEventStart HANDLE ?

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

WinMain proc hInst:HINSTANCE,hPrevInst:HINSTANCE,CmdLine:LPSTR,CmdShow:DWORD
LOCAL wc:WNDCLASSEX
LOCAL msg:MSG
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 hInst
pop wc.hInstance
mov wc.hbrBackground,COLOR_WINDOW+1
mov wc.lpszMenuName,OFFSET MenuName
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 wc
invoke CreateWindowEx,WS_EX_CLIENTEDGE,ADDR ClassName,\
ADDR AppName,\
WS_OVERLAPPEDWINDOW,CW_USEDEFAULT,\
CW_USEDEFAULT,300,200,NULL,NULL,\
hInst,NULL
mov hwnd,eax
invoke ShowWindow, hwnd,SW_SHOWNORMAL
invoke UpdateWindow, hwnd
invoke GetMenu,hwnd
mov hMenu,eax
.WHILE TRUE
invoke GetMessage, ADDR msg,NULL,0,0
.BREAK .IF (!eax)
invoke TranslateMessage, ADDR msg
invoke DispatchMessage, ADDR msg
.ENDW
mov eax,msg.wParam
ret
WinMain endp

WndProc proc hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM
.IF uMsg==WM_CREATE
invoke CreateEvent,NULL,FALSE,FALSE,NULL
mov hEventStart,eax
mov eax,OFFSET ThreadProc
invoke CreateThread,NULL,NULL,eax,\
NULL,0,\
ADDR ThreadID
invoke CloseHandle,eax
.ELSEIF uMsg==WM_DESTROY
invoke PostQuitMessage,NULL
.ELSEIF uMsg==WM_COMMAND
mov eax,wParam
.if lParam==0
.if ax==IDM_START_THREAD
invoke SetEvent,hEventStart
invoke EnableMenuItem,hMenu,IDM_START_THREAD,MF_GRAYED
invoke EnableMenuItem,hMenu,IDM_STOP_THREAD,MF_ENABLED
.elseif ax==IDM_STOP_THREAD
mov EventStop,TRUE
invoke EnableMenuItem,hMenu,IDM_START_THREAD,MF_ENABLED
invoke EnableMenuItem,hMenu,IDM_STOP_THREAD,MF_GRAYED
.else
invoke DestroyWindow,hWnd
.endif
.endif
.ELSEIF uMsg==WM_FINISH
invoke MessageBox,NULL,ADDR SuccessString,ADDR AppName,MB_OK
.ELSE
invoke DefWindowProc,hWnd,uMsg,wParam,lParam
ret
.ENDIF
xor eax,eax
ret
WndProc endp

ThreadProc PROC USES ecx Param:DWORD
invoke WaitForSingleObject,hEventStart,INFINITE
mov ecx,600000000
.WHILE ecx!=0
.if EventStop!=TRUE
add eax,eax
dec ecx
.else
invoke MessageBox,hwnd,ADDR StopString,ADDR AppName,MB_OK
mov EventStop,FALSE
jmp ThreadProc
.endif
.ENDW
invoke PostMessage,hwnd,WM_FINISH,NULL,NULL
invoke EnableMenuItem,hMenu,IDM_START_THREAD,MF_ENABLED
invoke EnableMenuItem,hMenu,IDM_STOP_THREAD,MF_GRAYED
jmp ThreadProc
ret
ThreadProc ENDP
end start

分析：

本例中，我们演示另一种技巧：

.IF uMsg==WM_CREATE
invoke CreateEvent,NULL,FALSE,FALSE,NULL
mov hEventStart,eax
mov eax,OFFSET ThreadProc
invoke CreateThread,NULL,NULL,eax,\
NULL,0,\
ADDR ThreadID
invoke CloseHandle,eax

在WM_CREATE 消息的处理中我们生成事件同步对象并创建线程。我们设置了相关的值让同步对象生成时处于”无信号”状态而且在调用了WaitForSingleObject后可以自动把事件对象的状态设为”无信号”。然后我们创建线程。 线程的代码开始执行后立即被阻塞：

ThreadProc PROC USES ecx Param:DWORD
invoke WaitForSingleObject,hEventStart,INFINITE
mov ecx,600000000

您可以看到线程的执行体的第一条代码就是调用WaitForSingleObject函数，该函数使得线程阻塞并且一直处于等待事件对象变成”有信号”。这也就是说，我们以开始就让该线程进入了睡眠状态。 当用户选择了菜单项”run thread”后，我们把事件对象得状态变成”有信号”：

.if ax==IDM_START_THREAD
invoke SetEvent,hEventStart

函数SetEvent可以让同步对象变成”有信号”状态，那么下一次线程得到时间片运行时，WaitForSingleObject函数就会返回，线程余下的代码就可以得到执行了。当用户选择了菜单项”stop thread” 时，我们把全局变量EventStop设为TRUE。

.if EventStop==FALSE
add eax,eax
dec ecx
.else
invoke MessageBox,hwnd,ADDR StopString,ADDR AppName,MB_OK
mov EventStop,FALSE
jmp ThreadProc
.endif

这样线程得计数工作结束，然后跳转到重新执行WaitForSingleObject函数的地方。注意：我们不用手动清除事件对象的信号，因为在调用CreateEvent函数时把参数bManualReset的值设为了FALSE。

翻译：Lxx.阿龙，校对：LuoYunBin's Win32 ASM Page， http://asm.yeah.net/


Iczelion's win32 asm tutorial

第十七课 动态链接库

本课中，我们将学习DLLs，它们到底是什么和如何创建它们。

理论：

如果您编程的时间非常长，就会发现很多的程序之间其实有相当多的重复代码。每编一个程序就重写一遍这些代码既没必要又浪费时间。在DOS时代，一般的做法是把这些重复的代码写成一个个的函数，然后把它们按类别放到不同的库文件中去。当要使用这些函数时，只要把您的目标文件（.obj)文件和先前存放在库文件中的函数进行链接，链接时链接器会从库文件中抽取相关的信息并把它们插入到可执行文件中去。这个过程叫做静态链接。C运行时库就是一个好例子。这样的库的缺点是您在每一个调用库函数的程序中都必须嵌入同一函数的拷贝，这显然很浪费磁盘。在DOS时代毕竟每一时刻仅有一个程序在运行，所以浪费的还只是磁盘而已，在多任务的WINDOWS时代就不仅浪费磁盘，还要浪费宝贵的内存了。

在WINDOWS中，由于有多个程序同时运行，如果您的程序非常大的话，那将消耗相当多的内存。WINDOWS的解决办法是：使用动态链接库。动态链接库从表面上看也是一大堆的通用函数，不过即使有多个程序调用了它，在内存中也仅仅只有动态链接库的唯一一份拷贝。WINDOWS是通过分页机制来作到这一点的。当然，库的代码只有一份，但是每一个应用程序要有自己单独的数据段，要么就会乱掉。
不象旧时的静态链接库，它并不会把这些函数的可执行代码放入到应用程序中去，而是当程序已经在内存中运行时，如果需要调用该函数时才调入内存也即链接。这也就是为什么把它叫做“动态”的原因所在。另外您还可以动态地卸载动态链接库，当然要求这时没有其它的应用程序在使用它，否则就要一直等到最后一个使用它的函数也不再使用该动态链接库时才能去卸载它。
为了正确的调用库和给库函数分配内存空间，在编译和链接应用程序时，必须把重定位等一些消息插入到执行代码中去，以便载入正确的库，并给库函数分配正确的地址。
那么这些信息从哪里得到呢？引入库。引入库包含足够的信息，链接器从中抽取足够的信息（注意区别：静态链接库放入的是可执行代码）把它们放入到可执行文件中去。当WINDOWS的加载器装入应用程序查看到有DLL时，它会查找该库文件，如果没有查到，就报错退出，否则就把它映射进进程的地址空间，并修正函数调用语句的地址。
如果没有引入库呢？当然我们也可以调用动态链接库中的任意函数。只不过您必须知道调用的函数是否在库中而且是否在库的引出名字表中，另外还需要知道该函数的参数个数和参数的类型。

（译者加：说到这里，让我想起了一件很有名的事。<<Undocumented Windows>>一书的作者Angel Schudleman 曾经利用此方法来跟踪微软Win3x系统动态链接库中未公开的函数，因为在微软给程序员提供的系统动态链接库的引入库中没有提供这些函数的原型，所以您无法在链接时把这些函数的信息链接到可执行文件中去，而为了某种目的您又要使用这些函数，您就可以在执行时加载动态链接库并得到这些函数的地址，从而和调用其它的库函数一样使用这些未公开的函数。由于这本书的巨大影响，当时许多程序员纷纷在它们的程序中调用未公开函数，甚至在写商业程序时也这么做。这种走偏峰的做法引起了微软的反感，后来微软在它Win3x的改进版中不再把那些未公开函数列入系统动态链接库的引出名字表，这样也就无法再利用这种方法来调用未公开的函数了。）

  • 当您让系统的加载器为您加载动态库时，如果不能找到库文件，它就会提示一条“A required .DLL file, xxxxx.dll is missing”，这样您的应用程序就无法运行，即使该库对您的应用程序来说并不重要。
  • 如果您选择在程序运行时自己加载该库，就没有这种问题了。
  • 如果您知道足够的信息，就可以调用系统未公开的函数。
  • 如果您调用LoadLibrary函数加载库，就必须再调用GetProcAddress函数来得到每一个您想调用的函数的地址，GetProcAddress会在动态链接库中查找函数的入口地址。由于多余的步骤，这样您的程序执行起来会慢一点，但是并不明显。
明白了LoadLibrary函数的优缺点，下面我们就来看看如何产生一个动态链接库。下面的代码是一个动态链接库的框架：

;--------------------------------------------------------------------------------------
; DLLSkeleton.asm
;--------------------------------------------------------------------------------------
.386
.model flat,stdcall
option casemap:none
include \masm32\include\windows.inc
include \masm32\include\user32.inc
include \masm32\include\kernel32.inc
includelib \masm32\lib\user32.lib
includelib \masm32\lib\kernel32.lib

.data
.code
DllEntry proc hInstDLL:HINSTANCE, reason:DWORD, reserved1:DWORD
mov eax,TRUE
ret
DllEntry Endp
;---------------------------------------------------------------------------------------------------
;下面是一个空函数，您可以象下面一样插入您的函数。
;----------------------------------------------------------------------------------------------------
TestFunction proc
ret
TestFunction endp

End DllEntry

;-------------------------------------------------------------------------------------
; DLLSkeleton.def
;-------------------------------------------------------------------------------------
LIBRARY DLLSkeleton
EXPORTS TestFunction

上面是一个动态链接库的框架，每一个DLL必须有一个入口点函数，WINDOWS每一次在做下面的动作时会调用该入口点函数：

  • 当动态链接库被加载时
  • 当动态链接库卸载时
  • 同一进程的线程生成时
  • 同一进程的线程退出时

DllEntry proc hInstDLL:HINSTANCE, reason:DWORD, reserved1:DWORD
mov eax,TRUE
ret
DllEntry Endp

入口点函数的名称无所谓只要您让语句“END<函数名>”中的函数名和前面的相同就可以了。该函数共有三个参数，只有前面两个是重要的。
hInstDLL是该动态链接库模块的句柄。它和进程的实例句柄不一样。如果您以后要用，可以保存它，因为以后再要获得它不容易。
根据不同的时机，reason传入的值可能是下面的四个值中的一个：

  • DLL_PROCESS_ATTACH 动态链接库第一次插入进程的地址空间时。当传入的参数是该值时，您可以做一些初始化的工作。
  • DLL_PROCESS_DETACH 动态链接库从进程的地址空间卸出时。您可以在此做一些清理的工作。譬如：释放内存等。
  • DLL_THREAD_ATTACH 新线程生成。
  • DLL_THREAD_DETACH 线程销毁。

如果想要库中的代码继续执行，返回TRUE，否则返回FALSE，那样动态链接库就不会加载了。譬如：您想分配一块内存，如果不成功的话就退出，这时您就可以返回FALSE。那样动态链接库就不会加载了。
您可以加入的函数，它们的位置并不重要，把它们放在入口点函数的前面或后面都可以。只是如果您想要它们能被其它的程序调用的话，就必须把它们的名字放到模块定义文件（.def）中去。
动态链接库在它们自己的编译过程就需要，而不只是提供给其它要引用它的程序参考。他们如下：

LIBRARY DLLSkeleton
EXPORTS TestFunction

第一行是必须的。LIBRARY 定义了DLL的模块名称。它必须和动态链接库的名称相同。
EXPORTS关键字告诉链接器该DLL的引出函数，也就是其它程序可以调用的函数。举个例子：其它的程序想要调用函数TestFunction ，我们就把它放到EXPORTS中。
还有就是，链接器的选项中必须放入开关项：/DLL 和/DEF<DLL文件名>，就像下面这样：

link /DLL /SUBSYSTEM:WINDOWS /DEF:DLLSkeleton.def /LIBPATH:c:\masm32\lib DLLSkeleton.obj

编译器的开关选项是一样的，即：/c /coff /Cp。在您链接好后，链接器会生成.lib 和.dll文件。前者是引入库，当其它的程序要调用您的动态链接库中的函数时就需要该引入库，以便把必要的信息加入到其可执行文件中去。
接下来我们来看看如何使用LoadLibrary函数来加载一个DLL。

;---------------------------------------------------------------------------------------------
; UseDLL.asm
;----------------------------------------------------------------------------------------------
.386
.model flat,stdcall
option casemap:none
include \masm32\include\windows.inc
include \masm32\include\user32.inc
include \masm32\include\kernel32.inc
includelib \masm32\lib\kernel32.lib
includelib \masm32\lib\user32.lib

.data
LibName db "DLLSkeleton.dll",0
FunctionName db "TestHello",0
DllNotFound db "Cannot load library",0
AppName db "Load Library",0
FunctionNotFound db "TestHello function not found",0

.data?
hLib dd ? ; 动态链接库的句柄 (DLL)
TestHelloAddr dd ? ; TestHello 函数的地址

.code
start:
invoke LoadLibrary,addr LibName
;---------------------------------------------------------------------------------------------------------
; 调用LoadLibrary，其参数是欲加载的动态链接库的名称。如果调用成功，将返回该DLL的句柄。 否则返回NULL。该句柄可以传给 :library函数和其它需要动态链接库句柄的函数。
;-----------------------------------------------------------------------------------------------------------
.if eax==NULL
invoke MessageBox,NULL,addr DllNotFound,addr AppName,MB_OK
.else
mov hLib,eax
invoke GetProcAddress,hLib,addr FunctionName
;-----------------------------------------------------------------------------------------------------------
; 当您得到了动态链接库的句柄后，把它传给GetProcAddress函数，再把您要调用的函数的名称 也传给该函数。如果成功的话，它:会返回想要的函数的地址，失败的话返回NULL。除非卸载该 动态链接库否则函数的地址是不会改变的，所以您可以把它保存到一个:全局变量中以备后用。
;-----------------------------------------------------------------------------------------------------------
.if eax==NULL
invoke MessageBox,NULL,addr FunctionNotFound,addr AppName,MB_OK
.else
mov TestHelloAddr,eax
call [TestHelloAddr]
;-----------------------------------------------------------------------------------------------------------
; 以后您就可以和调用其它函数一样调用该函数了。其中要把包含函数地址信息的变量用方括号括起来。
;-----------------------------------------------------------------------------------------------------------
.endif
invoke FreeLibrary,hLib
;-----------------------------------------------------------------------------------------------------------
;调用FreeLibrary卸载动态链接库。
;-----------------------------------------------------------------------------------------------------------
.endif
invoke ExitProcess,NULL
end start

使用LoadLibrary函数加载动态链接库，可能要自己多做一些工作，但是这种方法确实是提供了许多的灵活性。

翻译：ZhangJun，校对：LuoYunBin's Win32 ASM Page， http://asm.yeah.net


Iczelion's win32 asm tutorial

第十八课 通用控件

本课中我们将学习什么是通用控件和如何使用它们。

理论：

WIN95相对于WIN3X有几个加强的用户界面控件。其实在WIN95正式发行前这些控件就在使用，譬如：状态条、工具条等。以前程序员要自己去编程使用它们，现在微软已经把它们包含到了WIN9X和WINNT中了。
  • Toolbar ---工具条
  • Tooltip ---提示文本
  • Status bar ---状态条
  • Property sheet ---属性页单
  • Property page ---属性页
  • Tree view ---树型视图
  • List view ---列表视图
  • Animation ---动画
  • Drag list ---能够处理Drag-Drop的列表框
  • Header ---
  • Hot-key ---热键
  • Image list ---图象链表
  • Progress bar ---进程状态条
  • Right edit ---
  • Tab ---跳格表
  • Trackbar ---跟踪条
  • Up-down ---滚动条
因为通用控件的数量非常多，把它们全部装入内存并注册它们是非常浪费内存的。除了“RTF文本编辑”控件外其他控件的可执行代码都放在comctl32.dll中，这样其他的应用程序就可以使用它们了。“RTF文本编辑”控件在richedXX.dll中，由于该控件非常的复杂，所以也比其它控件大。
要加载comctl32.dll可以在您的应用程序中调用函数InitCommonControls。InitCommonControls函数是动态链接库comctl32.dll中的一个函数，只要在您的程序中的任意地方引用了该函数就、会使得WINDOWS的程序加载器PE Loader加载该库。函数InitCommonControls其实只有一条指令“ret”，它的唯一目的是为了使得在调用了个该函数的应用程序的可执行文件的PE头中的“引入”段中包含有comctl32.dll，这样无论什么时候该应用程序都会为您加载该库。所以真正初始化的工作是在该库的入口点处做的，在这里会注册所有的通用控件类，然后所有的通用控件就可以在这些类上进行创建，这就象创建其它的子窗口控件一样。
RTF文本编辑控件则不同。如果您要使用它，就必须调用LoadLibrary函数来动态加载，并调用FreeLibrary来动态地卸载。
现在我们学习如何创建这些通用控件。您可以用资源编辑器把它们放到一个对话框中，或者您也可以自己调用相关的函数来手动创建它们。几乎所有的通用控件都是调用函数CreateWindowEx或CreateWindow来创建的，您只要在其中传递通用控件的类名即可。有一些通用控件有一些特别的创建函数，但是其实这些函数在内部都调用了CreateWindowEx，只是包装后的函数更方便使用而已。经过包装的函数有：
  • CreateToolbarEx
  • CreateStatusWindow
  • CreatePropertySheetPage
  • PropertySheet
  • ImageList_Create
为了创建通用控件您必须要知道它们的类名，我们把类名列于如下：
类名 | 通用控件
ToolbarWindow32 | Toolbar
tooltips_class32 | Tooltip
msctls_statusbar32 | Status bar
SysTreeView32 | Tree view
SysListView32 | List view
SysAnimate32 | Animation
SysHeader32 | Header
msctls_hotkey32 | Hot-key
msctls_progress32 | Progress bar
RICHEDIT | Rich edit
msctls_updown32 | Up-down
SysTabControl32 | Tab

Property sheets、property pages和image list控件有它们自己的创建函数。Drag list其实是可以伸缩的listbox控件，所以它没有自己的类名。上面的类名是VC++的资源编辑器提供的，它们和Borland公司的WIN32 API指南中提出的不一样，和Petzold的书《Programming Windows 95》也不一样。可以肯定的是我们上面列出的类名绝对准确。 这些通用控件可以有通用的窗口类的一些风格，譬如WS_CHILD等。它们当然还有其他的特殊风格，譬如树型视图控件就有TVS_XXXXX风格，列表控件就有LVS_xxxx风格。具体的最好查找有关的WIN32 API函数指南。 既然我们已经知道了如何创建一个通用控件，我们就可以讨论这些通用控件之间以及和它们的父窗口之间是如何通讯的了。不象子窗口控件，通用控件在某些状态发生变化时不是通过发送WM_COMMAND而是发送WM_NOTIFY消息和父窗口通讯的。父窗口可以通过发送消息来控制子窗口的行为。对于那些新的通用控件，还有一些新的消息类型。您可以参考您的WIN32 API手册。

在下面的例子中我们将要实验一下进度条和状态条。

例子代码：

.386
.model flat,stdcall
option casemap:none
include \masm32\include\windows.inc
include \masm32\include\user32.inc
include \masm32\include\kernel32.inc
include \masm32\include\comctl32.inc
includelib \masm32\lib\comctl32.lib
includelib \masm32\lib\user32.lib
includelib \masm32\lib\kernel32.lib

WinMain PROTO :DWORD,:DWORD,:DWORD,:DWORD

.const
IDC_PROGRESS equ 1 ; control IDs
IDC_STATUS equ 2
IDC_TIMER equ 3

.data
ClassName db "CommonControlWinClass",0
AppName db "Common Control Demo",0
ProgressClass db "msctls_progress32",0 ; the class name of the progress bar
Message db "Finished!",0
TimerID dd 0

.data?
hInstance HINSTANCE ?
hwndProgress dd ?
hwndStatus dd ?
CurrentStep dd ?
.code
start:
invoke GetModuleHandle, NULL
mov hInstance,eax
invoke WinMain, hInstance,NULL,NULL, SW_SHOWDEFAULT
invoke ExitProcess,eax
invoke InitCommonControls

WinMain proc hInst:HINSTANCE,hPrevInst:HINSTANCE,CmdLine:LPSTR,CmdShow:DWORD
LOCAL wc:WNDCLASSEX
LOCAL msg:MSG
LOCAL hwnd:HWND

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 hInst
pop wc.hInstance
mov wc.hbrBackground,COLOR_APPWORKSPACE
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 wc
invoke CreateWindowEx,WS_EX_CLIENTEDGE,ADDR ClassName,ADDR AppName,\
WS_OVERLAPPED+WS_CAPTION+WS_SYSMENU+WS_MINIMIZEBOX+WS_MAXIMIZEBOX+WS_VISIBLE,CW_USEDEFAULT,\
CW_USEDEFAULT,CW_USEDEFAULT,CW_USEDEFAULT,NULL,NULL,\
hInst,NULL
mov hwnd,eax
.while TRUE
invoke GetMessage, ADDR msg,NULL,0,0
.BREAK .IF (!eax)
invoke TranslateMessage, ADDR msg
invoke DispatchMessage, ADDR msg
.endw
mov eax,msg.wParam
ret
WinMain endp

WndProc proc hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM
.if uMsg==WM_CREATE
invoke CreateWindowEx,NULL,ADDR ProgressClass,NULL,\
WS_CHILD+WS_VISIBLE,100,\
200,300,20,hWnd,IDC_PROGRESS,\
hInstance,NULL
mov hwndProgress,eax
mov eax,1000 ; the lParam of PBM_SETRANGE message contains the range
mov CurrentStep,eax
shl eax,16 ; the high range is in the high word
invoke SendMessage,hwndProgress,PBM_SETRANGE,0,eax
invoke SendMessage,hwndProgress,PBM_SETSTEP,10,0
invoke CreateStatusWindow,WS_CHILD+WS_VISIBLE,NULL,hWnd,IDC_STATUS
mov hwndStatus,eax
invoke SetTimer,hWnd,IDC_TIMER,100,NULL ; create a timer
mov TimerID,eax
.elseif uMsg==WM_DESTROY
invoke PostQuitMessage,NULL
.if TimerID!=0
invoke KillTimer,hWnd,TimerID
.endif
.elseif uMsg==WM_TIMER ; when a timer event occurs
invoke SendMessage,hwndProgress,PBM_STEPIT,0,0 ; step up the progress in the progress bar
sub CurrentStep,10
.if CurrentStep==0
invoke KillTimer,hWnd,TimerID
mov TimerID,0
invoke SendMessage,hwndStatus,SB_SETTEXT,0,addr Message
invoke MessageBox,hWnd,addr Message,addr AppName,MB_OK+MB_ICONINFORMATION
invoke SendMessage,hwndStatus,SB_SETTEXT,0,0
invoke SendMessage,hwndProgress,PBM_SETPOS,0,0
.endif
.else
invoke DefWindowProc,hWnd,uMsg,wParam,lParam
ret
.endif
xor eax,eax
ret
WndProc endp
end start

分析:

    invoke WinMain, hInstance,NULL,NULL, SW_SHOWDEFAULT
    invoke ExitProcess,eax
    invoke InitCommonControls
我故意把函数InitCommonControls放到ExitProcess后，这样就可以验证调用该函数仅仅是为了在我们程序的可执行文件的PE头中的引入段中放入引用了comctl32.dll的信息。您可以看到，即使该函数什么都没有做，我们的通用控件对话框依旧可以正常工作。
    .if uMsg==WM_CREATE
    invoke CreateWindowEx,NULL,ADDR ProgressClass,NULL,\
    WS_CHILD+WS_VISIBLE,100,\
    200,300,20,hWnd,IDC_PROGRESS,\
    hInstance,NULL
    mov hwndProgress,eax
在这里我们创建了通用控件。注意CreateWindowEx函数中的参数hWnd是父窗口的句柄。另外它也指定了通用控件的ID号。因为我们直接使用控件的窗口句柄，所以就没有使用该ID号。所有的窗口都必须具有WS_CHILD风格。
    mov eax,1000
    mov CurrentStep,eax
    shl eax,16
    invoke SendMessage,hwndProgress,PBM_SETRANGE,0,eax
    invoke SendMessage,hwndProgress,PBM_SETSTEP,10,0
在创建了进度条后我们先设定它的范围。缺省的范围是0-100。如果您不满意，可以重新设置，这通过传递PBM_SETRANGE消息来实现。参数lParam中包含了范围值，其中底字和高字分别是范围的起始和终了的值。您可以指定进度条每移动一格的步长。本例子中把步长设置成10，意味着每发送一次PBM_STEPIT消息给进度条，它的显示指针就会移动10。当然您可以调用PBM_SETPOS 来直接设定进度条上的指针的位置。用该消息您可以更方便地设定进度条了。
    invoke CreateStatusWindow,WS_CHILD+WS_VISIBLE,NULL,hWnd,IDC_STATUS
    mov hwndStatus,eax
    invoke SetTimer,hWnd,IDC_TIMER,100,NULL ; create a timer
    mov TimerID,eax
下面我们调用CreateStatusWindow来创建状态条。这个调用很好理解，无需我多解释。在状态条创建后我们创建一个计时器。在本例中我们每隔100毫秒就更新一次进度条。下面时创建记时器的函数原型：
    SetTimer PROTO hWnd:DWORD, TimerID:DWORD, TimeInterval:DWORD, lpTimerProc:DWORD
hWnd : 父窗口的句柄。
TimerID : 计时器的ID号。您可以指定一个唯一的非零值。
TimerInterval : 以毫秒计的时间间隔。
lpTimerProc : 计时器回调函数的地址。每当时间间隔到了的时候，该函数就会被系统调用。如果该值为NULL，计时器就会把WM_TIMER消息发送到父窗口。

如果SetTimer调用成功的话就会返回计时器的ID号值，否则返回0。这也是为什么计时器的ID号必须为非零值的原因。

    .elseif uMsg==WM_TIMER
    invoke SendMessage,hwndProgress,PBM_STEPIT,0,0
    sub CurrentStep,10
    .if CurrentStep==0
    invoke KillTimer,hWnd,TimerID
    mov TimerID,0
    invoke SendMessage,hwndStatus,SB_SETTEXT,0,addr Message
    invoke MessageBox,hWnd,addr Message,addr AppName,MB_OK+MB_ICONINFORMATION
    invoke SendMessage,hwndStatus,SB_SETTEXT,0,0
    invoke SendMessage,hwndProgress,PBM_SETPOS,0,0
    .endif
当指定的时间到了的时候，计时器将发送WM_TIMER消息。您可以在处理该消息时作适当的处理。本例中我们将更新进度条，并检查进度条是否超过最大的值。如果超过了的话，我们通过发送SB_SETTEXT消息来在状态条中设置文本。这时，弹出一个对话框，当用户关闭掉对话框后，我们去除掉进度条和状态条中的文本。

翻译：Lxx.阿龙，校对：LuoYunBin's Win32 ASM Page， http://asm.yeah.net/


Iczelion's win32 asm tutorial

第十九课 树型视图控件

本课中，我们将学习如何使用树型视图控件。另外还要学习如何在树型视图中完成拖-拉动作，以及如何使用图象列表。

理论：

树型视图是一种特别的窗口，我们可以使用它一目了然地表示某种层次关系。譬如象在资源管理器中左边窗口中的就是树型视图。您可以调用CreateWindowEx来创建树型视图，传递一个类名“"SysTreeView32"”，或者您也可以把它放到一个对话框中去。不要忘了在您的代码中加入InitCommonControls函数。
树型视图有几种特有的风格。下面是几种经常使用的。
    TVS_HASBUTTONS == 在父项目中显示（+）或（-）。用户可以通过点击该符号来展开或收起该父项目下的子项目。如果想在根目录下也有这个符号必须指定TVS_LINESATROOT风格。
    TVS_HASLINES == 在层次中用线条来连接各个项目名称。
    TVS_LINESATROOT == 在根目录下的项目也用线连接。如果没有指定TVS_HASLINES风格，该风格也就会被忽略。
像其它的通用控件一样，树型视图用消息来完成通信。父窗口发送一系列的消息给树型视图，而树型视图发送"notification"消息给它的父窗口。在这方面，树型视图和其它的通用控制没什么两样。
当有事件发生时，树型视图发送一个WM_NOTIFY消息个父窗口，并在消息中附带传递一些附加信息。
    WM_NOTIFY
    wParam ==控件的ID。因为该值不是唯一的，故我们不用它。我们使用NMHDR结构体中的hwndFrom或IDFrom成员变量。
    lParam == 指向NMHDR结构体的指针。有一些控件可能传递一个指向更大一点的结构体的指针。但该结构体必须保证它的第一个成员变量是一个NMHDR型的变量。这样，您在处理lParam变量时，至少可以得到一个NMHDR型的变量。
下面我们来看NMHDR：
    NMHDR struct DWORD
    hwndFrom DWORD ?
    idFrom DWORD ?
    code DWORD ?
    NMHDR ends
hwndFrom是发送WM_NOTIFY消息的控件的窗口句柄。
idFrom是发送WM_NOTIFY消息的控件的ID。
code是控件发送给父窗口的数据。
树型视图发送给父窗口的通知消息以TVN_打头。 树型视图接收到的消息以TVM_打头，譬如：TVM_CREATEDRAGIMAGE。 树型视图发送TVN_XXX消息时在code变量中放入NMHDR型变量。父窗口发送TVM_消息来控制树型视图。

在树型视图中加入项目

在创建完树型视图后可以通过发送TVM_INSERTITEM消息往其中加入项目了。

    TVM_INSERTITEM
    wParam = 0;
    lParam = 指向结构体TV_INSERTSTRUCT的指针;

    您应当知道一些关于树型视图中的项目之间关系的一些术语。 一个项目可能是一个父亲、儿子或两者都是。父项目下含有子项目，而该父项目又有可能是其它项目的子项目。一个没有父项目的项目叫根项目。在树型视图中可能有多个根项目。现在我们来看看TV_INSERTSTRUCT结构体：

    TV_INSERTSTRUCT STRUCT DWORD
    hParent DWORD ?
    hInsertAfter DWORD ?
    ITEMTYPE <>
    TV_INSERTSTRUCT ENDS
hParent = 父项目的句柄。如果该值为TVI_ROOT value或NULL，该项目插在树型视图的根部。
hInsertAfter = 应该插入在起后面的项目的句柄或下面的值：
  • TVI_FIRST ==> 插在列表的头部。
  • TVI_LAST ==> 插在列表的尾部。
  • TVI_SORT ==> 按字母顺序插入。
    ITEMTYPE UNION
    itemex TVITEMEX <>
    item TVITEM <>
    ITEMTYPE ENDS
我们仅使用TVITEM。
    TV_ITEM STRUCT DWORD
    imask DWORD ?
    hItem DWORD ?
    state DWORD ?
    stateMask DWORD ?
    pszText DWORD ?
    cchTextMax DWORD ?
    iImage DWORD ?
    iSelectedImage DWORD ?
    cChildren DWORD ?
    lParam DWORD ?
    TV_ITEM ENDS

该结构体根据消息类型，用来发送或接收关于一个树型视图的项目的有关信息。譬如：对于消息TVM_INSERTITEM，它用来指定插入树型视图控件的项目的属性。而对于消息TVM_GETITEM，该结构体用来填充关于选定项目的信息。
imask 用来指定TV_ITEM的那些成员变量有效。譬如，如果指定了TVIF_TEXT，这意味着pszText成员变量是有效的。您可以同时指定几个标志位。
hItem 是树型视图项目的句柄。每一个项目都有它自己的句柄，就像窗口一样。如果您想要操作一个项目，就必须选择它的句柄。
pszText 是一个字符串指针。它是项目的标签名。
cchTextMax仅在查询项目的名称时使用。由于在pszText中指定了指针，WINDOWS还要知道该缓冲去的大小。所以您必须给出该值。
iImage 和 iSelectedImage用来指定图象列表以及一个索引号。这样就知道当项目被选中或没被选中时用哪个图象来表示该项目。像资源管理器中左边窗口中的文件夹等小图表就是有这两个参数来决定的。
为了在树型视图中插入一个项目，您必须至少设定hParent, hInsertAfter，另外您还要设定imask和pszText值。

把图形加到图形视图中

如果您想要在项目的名称左边显示图标的话，您必须创建一个图形列表，并且把它和树形视图相关联起来。您可以调用ImageList_Create来创建一个图形列表。
    ImageList_Create PROTO cx:DWORD, cy:DWORD, flags:DWORD, \
    cInitial:DWORD, cGrow:DWORD

如果创建成功的话，该函数返回一个空的图象列表的句柄。
cx == 以像素为单位的图象的宽度。
cy == 以像素为单位的图象的高度。图象列表中的每一幅的高度都必须相同。否则WINDOWS会对您的图象进行裁剪，如果过大的话就可能裁剪成几小块。所以您必须指定相同大小的图象。
flags == 指定图象列表的图象的颜色深度。详细情况请参考WIN32 API 指南。
cInitial == 指定包含的图象的数目。WINDWOS将依此来分配合适的内存。
cGrow == 在增加新图象是一次增加的数目。

图象列表不是窗口。仅仅是保存在那给其它的窗口使用的一种资源。 在图象列表产生后，您可以调用ImageList_Add来向其中加入图象。

    ImageList_Add PROTO himl:DWORD, hbmImage:DWORD, hbmMask:DWORD
如果该函数调用失败的话，返回-1。
himl == 图象列表的句柄。它是调用ImageList_Create时返回的值。
hbmImage == 加入图象列表的位图的句柄。您通常把位图保存在资源中，然后调用LoadBitmap来把它加载进来。 注意您没有必要指定该位图中包含的图象的数目。WINDOWS会根据它的大小，自动计算。
hbmMask == 掩码位图的句柄。如果没有使用掩码位图，可以忽略该值。 通常我们加入两种图象到图象列表中。一种时被选中时显示的图象，另一种时没被选中时显示的。
当图象列表准备就绪后，您可以发送消息TVM_SETIMAGELIST给树型视图来让图象列表和树型视图联系起来。
    TVM_SETIMAGELIST
    wParam = 图象列表的状态，一共有两种：
    • TVSIL_NORMAL 包含被选中和没有被选中两种状态的图象。
    • TVSIL_STATE 包含了用户自定义的状态的图象。
    lParam = 图象列表的句柄。

检索树型视图的信息

您可以通过发送消息TVM_GETITEM来检索图形视图的信息。

    TVM_GETITEM
    wParam = 0
    lParam =指向结构体TV_ITEM的指针。该结构体将用来得到相关的信息。

    在发送该消息前必须设置成员变量imask的值，以便WINDOWS能告诉相关的信息。当然，最重要的是，您必须传递您想得到信息的项目的句柄。这就引起了一个问题，您如何得到项目的句柄？要保存所有项目的句柄吗？
    答案是很简单的：没有必要。您可以发送消息TVM_GETNEXTITEM到树型视图以检索您想要得到其属性的项目的句柄。譬如：您可以查询第一个子项目的句柄、根目录的句柄、选中的项目的句柄等等。

    TVM_GETNEXTITEM
    wParam = 标志
    lParam = 树型视图的句柄（仅仅当wParam的值是某些标志位时才是必须的）。
wParam中的值非常重要， 我解释如下：
    • TVGN_CARET 选中的项目
    • TVGN_CHILD hitem参数指定项目的第一个子项目
    • TVGN_DROPHILITE 拖-拉操作的目的项目
    • TVGN_FIRSTVISIBLE 第一个可见项目
    • TVGN_NEXT 下一个同级项目
    • TVGN_NEXTVISIBLE 下一个可见项目，指定的项目必须可见。发送消息TVM_GETITEMRECT 来决定项目是否可见
    • TVGN_PARENT 指定项目的父项目
    • TVGN_PREVIOUS 前一个同级项目
    • TVGN_PREVIOUSVISIBLE 前一个可见项目，指定的项目必须可见。发送消息TVM_GETITEMRECT 来决定项目是否可见
    • TVGN_ROOT 根项目
由此您可以通过发送该消息来得到项目的句柄，然后在发送消息TVM_GETITEM时在结构体变量TV_ITEM的成员变量hItem中放入该项目的句柄就可以得到关于该项目的有关信息了。

在树型视图中进行拖-拉操作

也就是因为这一部分我才决定写这课教程。当我按照InPrise公司的WIN32帮助来运行例子时，发现它的帮助中缺少真正重要的信息。我只有通过自己做实验，最后总算弄明白来个中来由。希望您不要和我一样再去走这些弯路，下面我把我所知的在树型视图中进行拖-拉操作的步骤描述如下：
  1. 当用户要拖动一个项目时，树型视图控件会给它的父窗口发送TVN_BEGINDRAG通知消息。您可以在此处创建表示项目处在拖动操作中的图象，这可以通过发送TVM_CREATEDRAGIMAGE消息给树型视图，让其为目前使用的图象产生一副缺省的图象来实现。树型视图控件将创建一个图象列表，其中仅包含一副在拖动中显示的图象，图象列表创建后，您可以得到它的句柄。
  2. 在拖拉的图象生成后，您可以通过调用ImageList_BeginDrag来指定拖动图象的热点位置。
      ImageList_BeginDrag PROTO himlTrack:DWORD, \
      iTrack:DWORD , \
      dxHotspot:DWORD, \
      dyHotspot:DWORD
      himlTrack 是包含了拖拉时显示的图象的图象列表的句柄
      iTrack 是选中的图象在图象列表中的索引号。
      dxHotspot 因为在拖动中该图象被用来取代光标，所以我们必须指定图象中的哪一点是光标的左上角的位置。dxHotspot是水平相对位置。
      dyHotspot 是垂直相对位置。
      iTrack等于0。如果您要想光标的热点在拖拉中显示的图象的左上角，把dxHotspot和dyHotspot都设成0。
  3. 当拖拉的图象要显示时，我们调用ImageList_DragEnter 在树型视图中显示该图象。
      ImageList_DragEnter PROTO hwndLock:DWORD, x:DWORD, y:DWORD
      hwndLock 是进行拖拉中的窗口的句柄，拖拉的动作限制在该窗口中。
      x 和 y是在拖拉时显示图象的初始位置的坐标值。这些值是相对于窗口的左上角而不是客户区的左上角。
  4. 既然可以显示拖动中的图象了，我们就要处理拖动操作了。在这里有一个小问题。我们监视拖动是通过监视鼠标光标的移动来实现的，譬如在移动时我们通过捕获WM_MOUSEMOVE消息来得到移动中的坐标位置，通过捕获WM_LBUTTONUP消息来获知用户的放下操作。但这时如果鼠标光标移过子窗口时父窗口就无法再得到鼠标光标的移动以及鼠标的按键消息了。解决办法是调用SetCapture函数了锁定鼠标事件，这样无论鼠标移到那里和有什么动作，我们的窗口都可以知道了。
  5. 在处理WM_MOUSEMOVE消息时，您可以调用ImageList_DragMove来更新图象移动的轨迹。 该函数可以移动拖放操作中的图象位置。另外，如果您想让移动中的图象经过某些项目时高量度显示，可以调用TVM_HITTEST 来确定是否经过某个项目的上面。如果是的话，您可以发送TVM_SELECTITEM消息并设置 TVGN_DROPHILITE标志位使得那个项目高亮度显示。注意：在发送消息TVM_SELECTITEM前，您必须先隐藏图象列表，否则会留下非常难看的轨迹。要隐藏拖动中的图象可以调用ImageList_DragShowNolock，在显示完高亮度的图象后再调用该函数以让拖动中的图象再正常显示。
  6. 当用户释放主键后，您必须做几件事。 如果您在高亮度显示的时候释放鼠标主键（表示您想把该项目加到此处），您必须使该项目变成正常地显示，这可以通过发送消息TVM_SELECTITEM消息并设置标志位TVGN_DROPHILITE来实现，只是这时lParam必须为0。如果您不让高亮度显示的项目恢复正常，那就会发生一个奇怪的现象：当您再选择另外的项目时，那个项目的图象会包含在一个正方形中，当时高亮度显示的项目依旧是上一个项目。接下来必须调用ImageList_EndDrag和ImageList_DragLeave。还有调用ReleaseCapture来释放捕获的鼠标。如果您创建了一个图象列表，那还要调用calling ImageList来将它销毁，在拖放操作结束后您可以进行另外其它的操作。

例子代码：

.386
.model flat,stdcall
option casemap:none
include \masm32\include\windows.inc
include \masm32\include\user32.inc
include \masm32\include\kernel32.inc
include \masm32\include\comctl32.inc
include \masm32\include\gdi32.inc
includelib \masm32\lib\gdi32.lib
includelib \masm32\lib\comctl32.lib
includelib \masm32\lib\user32.lib
includelib \masm32\lib\kernel32.lib

WinMain PROTO :DWORD,:DWORD,:DWORD,:DWORD
.const
IDB_TREE equ 4006 ; ID of the bitmap resource
.data
ClassName db "TreeViewWinClass",0
AppName db "Tree View Demo",0
TreeViewClass db "SysTreeView32",0
Parent db "Parent Item",0
Child1 db "child1",0
Child2 db "child2",0
DragMode dd FALSE ; a flag to determine if we are in drag mode

.data?
hInstance HINSTANCE ?
hwndTreeView dd ? ; handle of the tree view control
hParent dd ? ; handle of the root tree view item
hImageList dd ? ; handle of the image list used in the tree view control
hDragImageList dd ? ; handle of the image list used to store the drag image

.code
start:
invoke GetModuleHandle, NULL
mov hInstance,eax
invoke WinMain, hInstance,NULL,NULL, SW_SHOWDEFAULT
invoke ExitProcess,eax
invoke InitCommonControls

WinMain proc hInst:HINSTANCE,hPrevInst:HINSTANCE,CmdLine:LPSTR,CmdShow:DWORD
LOCAL wc:WNDCLASSEX
LOCAL msg:MSG
LOCAL hwnd:HWND
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 hInst
pop wc.hInstance
mov wc.hbrBackground,COLOR_APPWORKSPACE
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 wc
invoke CreateWindowEx,WS_EX_CLIENTEDGE,ADDR ClassName,ADDR AppName,\ WS_OVERLAPPED+WS_CAPTION+WS_SYSMENU+WS_MINIMIZEBOX+WS_MAXIMIZEBOX+WS_VISIBLE,CW_USEDEFAULT,\
CW_USEDEFAULT,200,400,NULL,NULL,\
hInst,NULL
mov hwnd,eax
.while TRUE
invoke GetMessage, ADDR msg,NULL,0,0
.BREAK .IF (!eax)
invoke TranslateMessage, ADDR msg
invoke DispatchMessage, ADDR msg
.endw
mov eax,msg.wParam
ret
WinMain endp

WndProc proc uses edi hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM
LOCAL tvinsert:TV_INSERTSTRUCT
LOCAL hBitmap:DWORD
LOCAL tvhit:TV_HITTESTINFO
.if uMsg==WM_CREATE
invoke CreateWindowEx,NULL,ADDR TreeViewClass,NULL,\
WS_CHILD+WS_VISIBLE+TVS_HASLINES+TVS_HASBUTTONS+TVS_LINESATROOT,0,\
0,200,400,hWnd,NULL,\
hInstance,NULL ; Create the tree view control
mov hwndTreeView,eax
invoke ImageList_Create,16,16,ILC_COLOR16,2,10 ; create the associated image list
mov hImageList,eax
invoke LoadBitmap,hInstance,IDB_TREE ; load the bitmap from the resource
mov hBitmap,eax
invoke ImageList_Add,hImageList,hBitmap,NULL ; Add the bitmap into the image list
invoke DeleteObject,hBitmap ; always delete the bitmap resource
invoke SendMessage,hwndTreeView,TVM_SETIMAGELIST,0,hImageList
mov tvinsert.hParent,NULL
mov tvinsert.hInsertAfter,TVI_ROOT
mov tvinsert.item.imask,TVIF_TEXT+TVIF_IMAGE+TVIF_SELECTEDIMAGE
mov tvinsert.item.pszText,offset Parent
mov tvinsert.item.iImage,0
mov tvinsert.item.iSelectedImage,1
invoke SendMessage,hwndTreeView,TVM_INSERTITEM,0,addr tvinsert
mov hParent,eax
mov tvinsert.hParent,eax
mov tvinsert.hInsertAfter,TVI_LAST
mov tvinsert.item.pszText,offset Child1
invoke SendMessage,hwndTreeView,TVM_INSERTITEM,0,addr tvinsert
mov tvinsert.item.pszText,offset Child2
invoke SendMessage,hwndTreeView,TVM_INSERTITEM,0,addr tvinsert
.elseif uMsg==WM_MOUSEMOVE
.if DragMode==TRUE
mov eax,lParam
and eax,0ffffh
mov ecx,lParam
shr ecx,16
mov tvhit.pt.x,eax
mov tvhit.pt.y,ecx
invoke ImageList_DragMove,eax,ecx
invoke ImageList_DragShowNolock,FALSE
invoke SendMessage,hwndTreeView,TVM_HITTEST,NULL,addr tvhit
.if eax!=NULL
invoke SendMessage,hwndTreeView,TVM_SELECTITEM,TVGN_DROPHILITE,eax
.endif
invoke ImageList_DragShowNolock,TRUE
.endif
.elseif uMsg==WM_LBUTTONUP
.if DragMode==TRUE
invoke ImageList_DragLeave,hwndTreeView
invoke ImageList_EndDrag
invoke ImageList_Destroy,hDragImageList
invoke SendMessage,hwndTreeView,TVM_GETNEXTITEM,TVGN_DROPHILITE,0
invoke SendMessage,hwndTreeView,TVM_SELECTITEM,TVGN_CARET,eax
invoke SendMessage,hwndTreeView,TVM_SELECTITEM,TVGN_DROPHILITE,0
invoke ReleaseCapture
mov DragMode,FALSE
.endif
.elseif uMsg==WM_NOTIFY
mov edi,lParam
assume edi:ptr NM_TREEVIEW
.if [edi].hdr.code==TVN_BEGINDRAG
invoke SendMessage,hwndTreeView,TVM_CREATEDRAGIMAGE,0,[edi].itemNew.hItem
mov hDragImageList,eax
invoke ImageList_BeginDrag,hDragImageList,0,0,0
invoke ImageList_DragEnter,hwndTreeView,[edi].ptDrag.x,[edi].ptDrag.y
invoke SetCapture,hWnd
mov DragMode,TRUE
.endif
assume edi:nothing
.elseif uMsg==WM_DESTROY
invoke PostQuitMessage,NULL
.else
invoke DefWindowProc,hWnd,uMsg,wParam,lParam
ret
.endif
xor eax,eax
ret
WndProc endp
end start

分析：

在处理消息WM_CREATE的代码中，您可以创建树型视图控件。
    invoke CreateWindowEx,NULL,ADDR TreeViewClass,NULL,\
    WS_CHILD+WS_VISIBLE+TVS_HASLINES+TVS_HASBUTTONS+TVS_LINESATROOT,0,\
    0,200,400,hWnd,NULL,\
    hInstance,NULL
注意： TVS_xxxx 是树型视图所特有的风格。
    invoke ImageList_Create,16,16,ILC_COLOR16,2,10
    mov hImageList,eax
    invoke LoadBitmap,hInstance,IDB_TREE
    mov hBitmap,eax
    invoke ImageList_Add,hImageList,hBitmap,NULL
    invoke DeleteObject,hBitmap
    invoke SendMessage,hwndTreeView,TVM_SETIMAGELIST,0,hImageList
接下来，您可以创建一个空的图像列表，该图像列表容纳的是以像素为单位16x16大小和16位深度的图像，该图像列表初始包含两幅图像，最大可以容纳10幅。然后我们从资源中加载图像，并把它们放到图像列表中去。随后我们删除掉图像的句柄，因为我们不需要再用到它。设置好图像列表后，我们通过发送消息TVM_SETIMAGELIST把它和树型视图控件联系起来。
    mov tvinsert.hParent,NULL
    mov tvinsert.hInsertAfter,TVI_ROOT
    mov tvinsert.u.item.imask,TVIF_TEXT+TVIF_IMAGE+TVIF_SELECTEDIMAGE
    mov tvinsert.u.item.pszText,offset Parent
    mov tvinsert.u.item.iImage,0
    mov tvinsert.u.item.iSelectedImage,1
    invoke SendMessage,hwndTreeView,TVM_INSERTITEM,0,addr tvinsert
现在把项目插入到树型视图控件中去，首先我们从根项目开始。因为是根项目，所以成员变量hParent是NULL，hInsertAfter是TVI_ROOT。imask指定TV_ITEM结构体变量中的pszText，iImage和iSelectedImage三个成员变量的值是有效的。我们应该给这三个成员变量赋上正确的值。其中pszText显示项目的名称，iImage是图像列表中图像的索引号，该图像显示在未选中的项目名称的左边，iSelectedImage是选中的项目的图像索引号。设置好了这些值后，我们发送TVM_INSERTITEM消息给树型视图控件来把根项目加入到树型视图控件中去。
    mov hParent,eax
    mov tvinsert.hParent,eax
    mov tvinsert.hInsertAfter,TVI_LAST
    mov tvinsert.u.item.pszText,offset Child1
    invoke SendMessage,hwndTreeView,TVM_INSERTITEM,0,addr tvinsert
    mov tvinsert.u.item.pszText,offset Child2
    invoke SendMessage,hwndTreeView,TVM_INSERTITEM,0,addr tvinsert
加入完根项目后，我们再加入子项目。这时的成员变量hParent是其父项目的句柄，hInsertAfter的值是TVI_LAST。至于选中和非选中时用的图像是一样的，所以我们无需更改其它变量的值。
    .elseif uMsg==WM_NOTIFY
    mov edi,lParam
    assume edi:ptr NM_TREEVIEW
    .if [edi].hdr.code==TVN_BEGINDRAG
    invoke SendMessage,hwndTreeView,TVM_CREATEDRAGIMAGE,0,[edi].itemNew.hItem
    mov hDragImageList,eax
    invoke ImageList_BeginDrag,hDragImageList,0,0,0
    invoke ImageList_DragEnter,hwndTreeView,[edi].ptDrag.x,[edi].ptDrag.y
    invoke SetCapture,hWnd
    mov DragMode,TRUE
    .endif
    assume edi:nothing
当用户拖动项目时，树型视图控件将发送WM_NOTIFY消息给它的父窗口，子消息号是TVN_BEGINDRAG。在lPAram中是指向结构体NM_TREEVIEW 的指针，该结构体包含了一些附加信息。我们把lParam的值放到edi寄存器中，这样就可以把edi作为一个指针来使用。“assume edi:ptr NM_TREEVIEW ”语句用来告诉编译器MASM把edi作为指向NM_TREEVIEW 的结构体的变量使用。我们通过发送消息TVM_CREATEDRAGIMAGE来创建一个拖动的图像。它将返回一个新创建的图像列表的句柄，该图像列表中包含拖动中的图像。我们调用ImageList_BeginDrag函数设置拖动图像的热点。调用ImageList_DragEnter函数进入操作。该函数会在特定位置显示拖动中的图像。起初显示的位置我们设在结构体NM_TREEVIEW中的成员变量ptDrag所指的位置。我们锁定鼠标的输入，并设置标志变量，表示我们进入了拖拉操作。
    .elseif uMsg==WM_MOUSEMOVE
    .if DragMode==TRUE
    mov eax,lParam
    and eax,0ffffh
    mov ecx,lParam
    shr ecx,16
    mov tvhit.pt.x,eax
    mov tvhit.pt.y,ecx
    invoke ImageList_DragMove,eax,ecx
    invoke ImageList_DragShowNolock,FALSE
    invoke SendMessage,hwndTreeView,TVM_HITTEST,NULL,addr tvhit
    .if eax!=NULL
    invoke SendMessage,hwndTreeView,TVM_SELECTITEM,TVGN_DROPHILITE,eax
    .endif
    invoke ImageList_DragShowNolock,TRUE
    .endif
现在我们来看看WM_MOUSEMOVE消息的处理过程。当用户拖动图像时，我们的父窗口将接收到WM_MOUSEMOVE。为了响应这些消息，我们调用ImageList_DragMove来更新更新图像的位置。然后我们发送消息TVM_HITTEST给列表视图控件看看拖拉中的图像是否正好经过某些项目的上面，当然还要附带传递坐标位置等信息。如果经过的话，我们发送消息TVM_SELECTITEM并附带TVGN_DROPHILITE标志给树型视图控件，后者将会高亮度显示正被经过的项目。在高亮度显示的过程中，我们隐藏掉拖动中的图像免得显示的图像难看。
    .elseif uMsg==WM_LBUTTONUP
    .if DragMode==TRUE
    invoke ImageList_DragLeave,hwndTreeView
    invoke ImageList_EndDrag
    invoke ImageList_Destroy,hDragImageList
    invoke SendMessage,hwndTreeView,TVM_GETNEXTITEM,TVGN_DROPHILITE,0
    invoke SendMessage,hwndTreeView,TVM_SELECTITEM,TVGN_CARET,eax
    invoke SendMessage,hwndTreeView,TVM_SELECTITEM,TVGN_DROPHILITE,0
    invoke ReleaseCapture
    mov DragMode,FALSE
    .endif
当用户释放鼠标左键后，拖拉操作就可以结束了。我们调用ImageList_DragLeave，ImageList_EndDrag和ImageList_Destroy来结束拖拉操作模式。为了使得树形视图控件好看，我们检查最后高亮度显示的项目，并且选中它。我们还必须使得其不高亮度显示，否则其它的项目被选中时就不能高亮度显示了。最后我们释放对鼠标输入事件的捕获。

翻译：Lxx.阿龙，校对：LuoYunBin's Win32 ASM Page， http://asm.yeah.net/


Iczelion's win32 asm tutorial

第二十课 窗口子类化

在这一讲，我们将学习什么是窗口子类化和怎样按你所想要的方式方便地使用它。

理论：

如果你曾经在 Windows 环境下编过程序，有时候就会发现：有一个现成的窗口，几乎有你所需要的全部功能，但还不完全一样（否则就没有必要讲这一节了）。你曾遇到过这样的处境吗，如果你需要一个具有过滤特殊字符功能的 Edit 控件。当然最直接的方法就是自己用代码来实现，但这的确是一个费时又很困难的任务，而窗口子类化就可以用来做这种事情。

窗口子类化允许你接管被子类化的窗口，使你对它有绝对的控制权。举个例子了来阐明一下：例如你需要一个只接受十六进制数字输入的文本编辑框，如果使用一个简单的 Edit控件，当用户输入十六进制以外的字符时，你既不知道也无计可施。也就是说，当用户进文本框中输入字符串 "zb+q*" 时，如果除了拒绝接受整个字符串以外几乎什么也不能做，至少这显得特别不专业。重要的是，你需要具有输入检测的能力，即每当用户输入一个字符到编辑框中时要能检测这个字符。

现在来解释实现细节：当用户往文本框中输入字符时，Windows 会给Edit控件的窗口函数发送 WM_CHAR 消息。这个窗口函数本身寄生于 Windows 中，因此不能直接修改它。但是我们可以重定向这个消息使之发送到我们自己编写的窗口处理函数。如果自定义窗口要处理这个消息那就可以处理它，如果不处理就可以把这个消息转发到它原来窗口处理函数。通过这种方式，自定义的窗口处理函数就把它自己插入到 Windows 系统和 Edit 控件之间。

看下面的流程：
窗口子类化之前
Windows ==>Edit 控件的窗口处理函数。

子类化之后
Windows ==>自定义的窗口处理函数==> Edit 控件的窗口处理函数。

注意子类化并不局限于控件，可以子类化任何窗口，现在我们要把精力集中到怎样实现子类化一个窗口上。让我们想想Windows 怎样知道 Edit 控件的窗口处理函数放在什么地方。猜的？…肯定不是。原来 WNDCLASSEX 结构的成员 lpfnWndProc 指出了窗口函数地址。如果能用自己编写的窗口函数的地址来替换这个成员变量，那 Windows 不就把消息发到自定义的窗口函数了吗! 我们通过调用函数SetWindowLong 来实现这个任务，此函数的原型为：

SetWindowLong PROTO hWnd:DWORD, nIndex:DWORD, dwNewLong:DWORD

  • hWnd = 将要实施子类化的窗口的句柄
  • nIndex = 函数了功能索引
    GWL_EXSTYLE 设置窗口的扩展风格.
    GWL_STYLE 设置新的窗口风格
    GWL_WNDPROC 设置新的窗口处理函数地址
    GWL_HINSTANCE 设置新的应用程序句柄
    GWL_ID 设置新的窗口标识
    GWL_USERDATA 设置一个与这个窗口相关的给用户使用的32位的数据
  • dwNewLong = 用来更新的数据

我们的工作还是比较简单的：

  1. 写一个窗口函数用于处理发给 Edit 控件的消息。
  2. 用参数GWL_WNDPROC调用SetWindowLong 函数，如果调用成功那么返回值就是与调用功能相联系的一个32位的整数

在我们的程序中，返回值就是原先窗口函数的地址。我们要保存这个值以便以后使用。 记住：有一些我们不处理的消息，需要把它们派遣给原来的窗口函数来处理，这就用到另外一个函数 CallWindowProc， 函数原型为：

CallWindowProc PROTO lpPrevWndFunc:DWORD, hWnd:DWORD, Msg:DWORD, wParam:DWORD, lParam:DWORD

lpPrevWndFunc = 窗口原来函数的地址. 剩下的四个参数就是发给自定义函数的参数，直接把它们传给函数 CallWindowProc 就行了。

代码举例：

.386
.model flat,stdcall
option casemap:none
include \masm32\include\windows.inc
include \masm32\include\user32.inc
include \masm32\include\kernel32.inc
include \masm32\include\comctl32.inc
includelib \masm32\lib\comctl32.lib
includelib \masm32\lib\user32.lib
includelib \masm32\lib\kernel32.lib
WinMain PROTO :DWORD,:DWORD,:DWORD,:DWORD
EditWndProc PROTO :DWORD,:DWORD,:DWORD,:DWORD

.data
ClassName  db "SubclassWinClass",0
AppName    db "Subclassing Demo",0
EditClass  db "EDIT",0
Message  db "You pressed Enter in the text box!",0

.data?
hInstance  HINSTANCE ?
hwndEdit dd ?
OldWndProc dd ?

.code
start:
    invoke GetModuleHandle, NULL
    mov    hInstance,eax
    invoke WinMain, hInstance,NULL,NULL, SW_SHOWDEFAULT
    invoke ExitProcess,eax

WinMain proc hInst:HINSTANCE,hPrevInst:HINSTANCE,CmdLine:LPSTR,CmdShow:DWORD
    LOCAL wc:WNDCLASSEX
    LOCAL msg:MSG
    LOCAL hwnd:HWND
    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  hInst
    pop   wc.hInstance
    mov   wc.hbrBackground,COLOR_APPWORKSPACE
    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 wc
    invoke CreateWindowEx,WS_EX_CLIENTEDGE,ADDR ClassName,ADDR AppName,\
 WS_OVERLAPPED+WS_CAPTION+WS_SYSMENU+WS_MINIMIZEBOX+WS_MAXIMIZEBOX+WS_VISIBLE,CW_USEDEFAULT,\
           CW_USEDEFAULT,350,200,NULL,NULL,\
           hInst,NULL
    mov   hwnd,eax
    .while TRUE
        invoke GetMessage, ADDR msg,NULL,0,0
        .BREAK .IF (!eax)
        invoke TranslateMessage, ADDR msg
        invoke DispatchMessage, ADDR msg
    .endw
    mov eax,msg.wParam
    ret
WinMain endp

WndProc proc hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM
    .if uMsg==WM_CREATE
        invoke CreateWindowEx,WS_EX_CLIENTEDGE,ADDR EditClass,NULL,\
            WS_CHILD+WS_VISIBLE+WS_BORDER,20,\
            20,300,25,hWnd,NULL,\
            hInstance,NULL
        mov hwndEdit,eax
        invoke SetFocus,eax
        ;-----------------------------------------
        ; Subclass it!
        ;-----------------------------------------
        invoke SetWindowLong,hwndEdit,GWL_WNDPROC,addr EditWndProc
        mov OldWndProc,eax
    .elseif uMsg==WM_DESTROY
        invoke PostQuitMessage,NULL
    .else
        invoke DefWindowProc,hWnd,uMsg,wParam,lParam
        ret
    .endif
    xor eax,eax
    ret
WndProc endp

EditWndProc PROC hEdit:DWORD,uMsg:DWORD,wParam:DWORD,lParam:DWORD
    .if uMsg==WM_CHAR
        mov eax,wParam
        .if (al>="0" && al<="9") || (al>="A" && al<="F") || (al>="a" && al<="f") || al==VK_BACK
            .if al>="a" && al<="f"
                sub al,20h
            .endif
            invoke CallWindowProc,OldWndProc,hEdit,uMsg,eax,lParam
            ret
        .endif
    .elseif uMsg==WM_KEYDOWN
        mov eax,wParam
        .if al==VK_RETURN
            invoke MessageBox,hEdit,addr Message,addr AppName,MB_OK+MB_ICONINFORMATION
            invoke SetFocus,hEdit
        .else
            invoke CallWindowProc,OldWndProc,hEdit,uMsg,wParam,lParam
            ret
        .endif
    .else
        invoke CallWindowProc,OldWndProc,hEdit,uMsg,wParam,lParam
        ret
    .endif
    xor eax,eax
    ret
EditWndProc endp
end start

分析：

invoke SetWindowLong,hwndEdit,GWL_WNDPROC,addr EditWndProc
mov OldWndProc,eax

在创建 Edit 控件后，通过调用 SetWindowLong 把原来的窗口函数地址替换为自定义函数的地址，从而对它实施了窗口子类化，要注意 为了调用函数 CallWindowProc，我们存储了原窗口函数地址，自已编写的EditWndProc 仅仅是个普普通通的窗口函数。当然也可以再调用一次 SetWindowLong 函数来存储这个32位的值，

invoke SetWindowLong ,hwndEdit,GWL_USERDATA,eax 。

当然用的时候就要调用GetWindowLong 来取回这个值。

  .if uMsg==WM_CHAR
        mov eax,wParam
        .if (al>="0" && al<="9") || (al>="A" && al<="F") || (al>="a" && al<="f") || al==VK_BACK
            .if al>="a" && al<="f"
                sub al,20h
            .endif
            invoke CallWindowProc,OldWndProc,hEdit,uMsg,eax,lParam
            ret
        .endif
在函数 EditWndProc 中，我们自己处理了WM_CHAR消息： 如果输入的字符是'0'--'9'、'A'-'F'或者是'a'--'f'就接受，并且把此消息转发给原窗口函数，其中若输入的是小写的'a'--'f'就把它变为大写。如果输入的不是十六进制字符，就丢掉它，并且也不转发此消息。因此当输入是非十六进制字符时，这个字符就不会显示在 Edit 控件中。

    .elseif uMsg==WM_KEYDOWN
        mov eax,wParam
        .if al==VK_RETURN
            invoke MessageBox,hEdit,addr Message,addr AppName,MB_OK+MB_ICONINFORMATION
            invoke SetFocus,hEdit
        .else
            invoke CallWindowProc,OldWndProc,hEdit,uMsg,wParam,lParam
            ret
        .end
在这里我们通过处理 回车(Enter) 键进一步示范了子类化的能力。EditWndProc 通过检查 WM_KEYDONW 消息来判断是否是 回车键，若是显示提示消息框，否则转发此消息。 你可以用窗口子类化来控制另外的窗口，这是必须掌握的十分有用的技术之一。

翻译：ZhangJun，校对：LuoYunBin's Win32 ASM Page， http://asm.yeah.net


Iczelion's win32 asm tutorial

第二十一课 管道

这一讲将探索一下管道，看看它是什么、有什么用。为使之更加生动有趣，我将用怎样改变 Edit 控件的背景色和文本颜色来说明此技术。

理论：

管道，顾名思义就是有两个端的通道。可以使用管道在进程间、同一进程内进行数据交换，就像手提式无线电话机一样。把管道的一端给另一方，他就可以借助管道和你通讯了。

有两种管道，即有名管道和匿名管道。匿名管道就是没有名字的管道了，也就是说在使用它们时不需要知道其名字。而有名管道正好相反，在使用前必须知道其名字。

也可以根据管道的特性来分类，即是单向的还是双向的。单向管道，数据只能沿一个方向移动，从一端流向另一端，而双向管道数据可以在两端间自由交换。匿名管道通常是单向的而有名管道通常是双向的。有名管道常用于一个服务器联络多个客户端的网络环境。

这一讲将详细讨论一下匿名管道。匿名管道主要目的是作为父进程与子进程、子进程之间通讯的联结通路。在处理控制台问题时，匿名管道是相当有用的。控制台应用程序就是使用控制台作为输入和输出的一种 Win32 应用程序。一个控制台就像一个 DOS 窗口。但控制台应用程序的的确确是32位的应用程序，它可以向其它图形程序一样使用 GUI 函数，只不过它碰巧使用了控制台罢了。

控制台应用程序有三个用于输入输出的标准句柄，它们是标准输入、标准输出和标准错误句柄。标准输入用于从控制台读或取信息而标准输出用于往控制台写或打印信息。标准错误用于汇报输出不能重定向的错误。

控制台应用程序可以通过调用函数 GetStdHandle 来获得这三个句柄。一个图形应用程序没有控制台，如果在其中调用GetStdHandle 就会返回错误；如果的确要使用控制台，可以调用AllocConsole 来分配一个新的控制台以使用，但当处理完成时，别忘了调用 FreeConsole 来释放控制台。

匿名管道用得最多的功能就是 重定向子进程的标准输入和标准输出。父进程可以是一个控制台或者是图形程序，而子进程必须是控制台应用程序。众所周知，控制台应用使用标准输入输出句柄。若要重定向输入输出，就得用指向管道一端的句柄来替换这个标准句柄。控制台应用程序不会知道我们使用了指向管道任一端的句柄，它会把这个句柄作为标准句柄来看待。借用面向对象的术语，这就是多态性的一种。因为子进程不需作任何改动，因此这种方法是非常有用的。

关于控制台应用程序应该掌握的另一点就是它从哪获得标准句柄。当一个控制台应用程序被创建时，父进程有两种选择：为子进程创建一个新的控制台或者是让子进程继承自己的控制台。若使用后者，那父进程本身必须是一个控制台应用程序，或者如果是 GUI 应用程序，它必须首先调用 AllocConsole 分配了一个控制台。

通过调用 CreatePipe 来创建一个匿名管道，它的原型为：

CreatePipe proto pReadHandle:DWORD, \
pWriteHandle:DWORD,\
pPipeAttributes:DWORD,\
nBufferSize:DWORD

  • pReadHandle 双字指针变量，指向管道读端的句柄。
  • pWriteHandle 双字指针变量，指向管道写端的句柄
  • pPipeAttributes 双字指针变量，指向SECURITY_ATTRIBUTES 结构，其用于决定读写句柄是否可以被子进程继承
  • nBufferSize 建议管道留给用户使用的缓冲区的大小，这仅仅是个建议值，可以用 NULL 来使用缺省值

如果函数调用成功返回值为非零，否则为零。成功调用之后，就会得到两个句柄，一个指向管道的读出端，另一个指向管道的写入端。现在我将要把重点放到重定向子控制台程序的标准输出到自己进程的所需的步骤上。注意我的这个方法不同于Borland 公司的 API 参考上的例子。Win32 API 参考上假设父进程是控制台应用程序，因此子进程可以继承它的标准句柄。然而大多数情况下我们需要重定向控制台应用程序的输出到 GUI 应用程序。

创建匿名管道使用 CreatePipe ，同时别忘了把 SECURITY_ATTRIBUTES 结构成员bInheritable 设置为TRUE，这样才可以继承句柄。

现在要准备好创建进程的函数即CreateProcess 的参数，只有它才可以装载子控制台应用程序。STARTUPINFO 是一个重要的结构，它决定了子进程出现时主窗口的外观，它对于我们的目标也是至关重要的。通过这个结构就可以隐藏主窗口并且把管道句柄传递给子进程。

以下就是必须要填写的成员：

  • cb STARTUPINFO结构的大小
  • dwFlags 二进制标志位，它决定本结构的哪些成员有效，也决定主窗口是显示还是隐藏的状态。在我们的程序中使用STARTF_USESHOWWINDOW 和 STARTF_USESTDHANDLES的组合
  • hStdOutput 和hStdError 你想要子进程使用的标准输出和标准错误句柄，对我们来说，我们将把管道的写端作为子进程的标准输出和错误。因此当子进程往标准输出或标准错误发送信息时，它实际上把这些信息通过管道传给了父进程
  • wShowWindow 决定主窗口是显示还是隐藏。我们不希望显示子进程的主窗口，因此把该成员置成SW_HIDE

调用CreateProcess 来创建子进程，但调用成功后子进程仍然不处于激活状态。它被装进了内存但并没有立即运行。

在父进程中关闭管道的写端也是必须的。这是因为父进程并不使用管道的写句柄，而且如果一个管道有两个写入端也就不会工作，因此我们在从管道往外读数据之前必须关闭管道的写端。但是不能在调用CreateProcess 之前关闭，否则管道就坏了。你应当在CreateProcess 刚刚返回并且在读数据之前关闭管道的写端。

现在就可以通过函数ReadFile 在管道的读端读数据了。通过使用ReadFile ，可以使子进程处于运行状态。它将开始执行，并且当它往标准输出( 实际上是管道的写端 )上写数据时，数据就会被送至管道的读端。应当不停调用ReadFile 直至它的返回值为 0 ，也就是说再也没有数据可读了。对从管道读来的数据你可以进行任何处理，在我们的例子中它被显示在 Edit 控件中。

记得用完后关闭管道的读句柄。

代码举例：

.386
.model flat,stdcall
option casemap:none
include \masm32\include\windows.inc
include \masm32\include\user32.inc
include \masm32\include\kernel32.inc
include \masm32\include\gdi32.inc
includelib \masm32\lib\gdi32.lib
includelib \masm32\lib\user32.lib
includelib \masm32\lib\kernel32.lib
WinMain PROTO :DWORD,:DWORD,:DWORD,:DWORD

.const
IDR_MAINMENU equ 101         ; the ID of the main menu
IDM_ASSEMBLE equ 40001

.data
ClassName            db "PipeWinClass",0
AppName              db "One-way Pipe Example",0 EditClass db "EDIT",0
CreatePipeError     db "Error during pipe creation",0
CreateProcessError     db "Error during process creation",0
CommandLine     db "ml /c /coff /Cp test.asm",0

.data?
hInstance HINSTANCE ?
hwndEdit dd ?

.code
start:
    invoke GetModuleHandle, NULL
    mov hInstance,eax
    invoke WinMain, hInstance,NULL,NULL, SW_SHOWDEFAULT
    invoke ExitProcess,eax

WinMain proc hInst:DWORD,hPrevInst:DWORD,CmdLine:DWORD,CmdShow:DWORD
    LOCAL wc:WNDCLASSEX
    LOCAL msg:MSG
    LOCAL hwnd:HWND
    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 hInst
    pop wc.hInstance
    mov wc.hbrBackground,COLOR_APPWORKSPACE
    mov wc.lpszMenuName,IDR_MAINMENU
    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 wc
    invoke CreateWindowEx,WS_EX_CLIENTEDGE,ADDR ClassName,ADDR AppName,\ WS_OVERLAPPEDWINDOW+WS_VISIBLE,CW_USEDEFAULT,\ CW_USEDEFAULT,400,200,NULL,NULL,\ hInst,NULL
    mov hwnd,eax
    .while TRUE
        invoke GetMessage, ADDR msg,NULL,0,0
        .BREAK .IF (!eax)
        invoke TranslateMessage, ADDR msg
        invoke DispatchMessage, ADDR msg
    .endw
    mov eax,msg.wParam
    ret
WinMain endp

WndProc proc hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM
    LOCAL rect:RECT
    LOCAL hRead:DWORD
    LOCAL hWrite:DWORD
    LOCAL startupinfo:STARTUPINFO
    LOCAL pinfo:PROCESS_INFORMATION
    LOCAL buffer[1024]:byte
    LOCAL bytesRead:DWORD
    LOCAL hdc:DWORD
    LOCAL sat:SECURITY_ATTRIBUTES
    .if uMsg==WM_CREATE
        invoke CreateWindowEx,NULL,addr EditClass, NULL, WS_CHILD+ WS_VISIBLE+ ES_MULTILINE+ ES_AUTOHSCROLL+ ES_AUTOVSCROLL, 0, 0, 0, 0, hWnd, NULL, hInstance, NULL
        mov hwndEdit,eax
    .elseif uMsg==WM_CTLCOLOREDIT
        invoke SetTextColor,wParam,Yellow
        invoke SetBkColor,wParam,Black
       invoke GetStockObject,BLACK_BRUSH
        ret
    .elseif uMsg==WM_SIZE
        mov edx,lParam
        mov ecx,edx
        shr ecx,16
        and edx,0ffffh
        invoke MoveWindow,hwndEdit,0,0,edx,ecx,TRUE
    .elseif uMsg==WM_COMMAND
       .if lParam==0
            mov eax,wParam
            .if ax==IDM_ASSEMBLE
                mov sat.niLength,sizeof SECURITY_ATTRIBUTES
                mov sat.lpSecurityDescriptor,NULL
                mov sat.bInheritHandle,TRUE
                invoke CreatePipe,addr hRead,addr hWrite,addr sat,NULL
                .if eax==NULL
                    invoke MessageBox, hWnd, addr CreatePipeError, addr AppName, MB_ICONERROR+ MB_OK
                .else
                    mov startupinfo.cb,sizeof STARTUPINFO
                    invoke GetStartupInfo,addr startupinfo
                    mov eax, hWrite
                    mov startupinfo.hStdOutput,eax
                    mov startupinfo.hStdError,eax
                    mov startupinfo.dwFlags, STARTF_USESHOWWINDOW+ STARTF_USESTDHANDLES
                    mov startupinfo.wShowWindow,SW_HIDE
                    invoke CreateProcess, NULL, addr CommandLine, NULL, NULL, TRUE, NULL, NULL, NULL, addr startupinfo, addr pinfo
                    .if eax==NULL
                        invoke MessageBox,hWnd,addr CreateProcessError,addr         AppName,MB_ICONERROR+MB_OK
                    .else
                        invoke CloseHandle,hWrite
                        .while TRUE
                            invoke RtlZeroMemory,addr buffer,1024
                            invoke ReadFile,hRead,addr buffer,1023,addr bytesRead,NULL
                            .if eax==NULL
                                .break
                            .endif
                            invoke SendMessage,hwndEdit,EM_SETSEL,-1,0
                            invoke SendMessage,hwndEdit,EM_REPLACESEL,FALSE,addr buffer
                        .endw
                    .endif
                    invoke CloseHandle,hRead
                .endif
            .endif
        .endif
    .elseif uMsg==WM_DESTROY
        invoke PostQuitMessage,NULL
    .else
        invoke DefWindowProc,hWnd,uMsg,wParam,lParam ret
    .endif
    xor eax,eax
    ret
WndProc endp
end start

分析：

这个例子调用 ml.exe 来汇编一个名为 test.asm 的程序，并且重定向 ml.exe 的输出到客户区的 Edit 控件中。当程序被加载时，象往常一样要注册窗口类和创建主窗口。在主窗口被创建的过程中要做的第一件事就是创建用于显示程序 ml.exe 输出的 Edit 控件。

现在有趣的事来了，我们将改变此 Edit 控件的文本颜色和背景色。当 Edit 控件将要重画客户区时，它会给父窗口发送 WM_CTLCOLOREDIT 消息。参数 wParam 包含了用于画控件自己的客户区设备描述符的句柄 (HDC) 。我们可以利用这种机制来修改 HDC 的特性。

    .elseif uMsg==WM_CTLCOLOREDIT
        invoke SetTextColor,wParam,Yellow
        invoke SetTextColor,wParam,Black
        invoke GetStockObject,BLACK_BRUSH
        ret

SetTextColor 把文本颜色变为黄色，背景颜色变为黑色。最后我们返回一个通过调用GetStockObject 而得到黑色刷子的句柄。处理WM_CTLCOLOREDIT 必须返回一个刷子的句柄，因为 Windows 将要使用这个刷子来重画 Edit 控件的背景。在这个例子中，我希望背景是黑色，所以返回了一个黑色刷子的句柄。

现在当用户选择 Assemble 子菜单时，就会创建一个匿名管道。

            .if ax==IDM_ASSEMBLE
                mov sat.niLength,sizeof SECURITY_ATTRIBUTES
                mov sat.lpSecurityDescriptor,NULL
                mov sat.bInheritHandle,TRUE

在调用CreatePipe 之前，必须要填写SECURITY_ATTRIBUTES 结构。如果我们不关心安全性的话，可以在lpSecurityDescriptor 成员中填入 NULL 。bInheritHandle 则必须为 TRUE ，这样管道的句柄才可以被子进程继承。

invoke CreatePipe,addr hRead,addr hWrite,addr sat,NULL

在此之后，我们调用CreatePipe 来创建管道，如果成功，那么变量hRead 和 hWrite 将分别被填入相应的管道的读出端和写入端的句柄。

                    mov startupinfo.cb,sizeof STARTUPINFO
                    invoke GetStartupInfo,addr startupinfo
                    mov eax, hWrite
                    mov startupinfo.hStdOutput,eax
                    mov startupinfo.hStdError,eax
                    mov startupinfo.dwFlags, STARTF_USESHOWWINDOW+ STARTF_USESTDHANDLES
                    mov startupinfo.wShowWindow,SW_HIDE

下一步就是填写STARTUPINFO 结构了。调用 GetStartupinfo 用父进程的缺省值来填写STARTUPINFO 结构。如果要使程序同时工作在 Windows9x 和 Windows NT 下，就必须调用GetStartupInfo 来填写STARTUPINFO 结构。调用返回后，就可以修改重要的成员了。因为我们要子进程输出到父进程而不是缺省的标准输出和标准错误，所以我们把hStdOutput 和 hStdError 都赋成管道写端的句柄。为了隐藏子进程的主窗口，必须把成员变量wShowWidow 赋值为SW_HIDE 。最后通过把成员 dwFlags 赋值为STARTF_USESHOWWINDOW 和 STARTF_USESTDHANDLES 来指明成员hStdOutput, hStdError 和 wShowWindow 是有效的。

invoke CreateProcess, NULL, addr CommandLine, NULL, NULL, TRUE, NULL, NULL, NULL, addr startupinfo, addr pinfo

现在调用CreateProcess 来创建子进程。注意为使管道工作，参数bInheritHandles 必须设置为TRUE。 invoke CloseHandle,hWrite 成功创建子进程之后，在父进程中必须关闭管道的写端。我们已经把写端的句柄通过结构STARTUPINFO 传给了子进程。如果不关闭，那么管道就有两个写入端，而这样的管道是不会工作的。所以必须在创建子进程后但在读数据前关闭这个句柄。

                        .while TRUE
                            invoke RtlZeroMemory,addr buffer,1024
                            invoke ReadFile,hRead,addr buffer,1023,addr bytesRead,NULL
                            .if eax==NULL
                                .break
                            .endif
                            invoke SendMessage,hwndEdit,EM_SETSEL,-1,0
                            invoke SendMessage,hwndEdit,EM_REPLACESEL,FALSE,addr buffer
                        .endw

现在已经准备好从子进程的标准输出读数据了。直到再也没有数据了，即 ReadFile 返回为 NULL时才会退出循环，否则一直会等待数据。我们调用ReadFile 之前先调用RtlZeroMemory 来清空内存，并且用管道的读句柄代替文件句柄。注意读数据的最大长度为 1023 ( sizeof(buffer)-1 )，因为我们需要把接受的字符变为一个 Edit 控件可以处理的 ASCII 串。当ReadFile 返回时，就把此数据传给 Edit 控件。然而这有一个小小的问题，如果使用SetWindowText API 往 Edit 控件中写数据，新数据就会覆盖已存在的旧数据，而我们想把新数据添加在已有数据的后面。为达此目的，首先通过发送一个 wParam 为-1的 EM_SETSEL 消息，把 Edit 控件的输入焦点移动到文本的末端；然后发送一个 EM_REPLACESEL 消息把数据添加后面。

invoke CloseHandle,hRead

当ReadFile 返回为NULL时，就跳出循环并关闭管道的读句柄。

翻译：ZhangJun，校对：LuoYunBin's Win32 ASM Page， http://asm.yeah.net


Iczelion's win32 asm tutorial

第22课 超类化

在这一讲我们将学习什么是超类化以及它有什么作用；同时你还会学到怎样在自己的窗口中用Tab键在控件中切换这一技巧。

理论：

在你的程序生涯中你肯定遇到过这样的情况，你需要一系列的控件，但它们之间却只有一点点的不同。例如，你可能需要10个只接受数字的 Edit 控件，当然你可以通过多种方法来达到这个目的。

  1. 创建自己的类并用它实例化为那些控件
  2. 创建那些 Edit 控件并把它们全部子类化
  3. 超类化Edit 控件

第一种方法太乏味了，因为你必须自己实现Edit 控件的每个功能，但这项工作不是轻松就能完成的。第二种方法好于第一种，但仍然要做许多工作，子类化几个Edit 控件还可以接受，但若要子类化十几二十个，这项工作简直就是一场恶梦。在这种情况下就应该使用超类化这个技巧，它是用于控制某一个特定窗口类的特殊方法。通过这种控制就可以修改窗口类的特性使之符合你的要求，然后再创建那一堆控件就可以了。

超类化有如下几个步骤：

  1. 通过调用 GetClassInfoEx 来获得想要进行超类化操作的窗口类的信息。函数GetClassInfoEx 需要一个指向 WNDCLASSEX 结构的指针，用于当成功返回时填入窗口类的信息。
  2. 按需要修改 WNDCLASSEX 结构的成员，其中有两个成员必须修改：
    hInstance 存放程序的实例句柄
    lpszClassName 指向一个新类名的指针
    不必修改成员 lpfnWndProc，但大多数情况下还是需要的。但要记住如果要使用函数 CallWindowProc 调用老窗口的过程，那就必须保存成员 lpfnWndProc 的原值。
  3. 注册修改完的 WNDCLASSEX 结构，得到一个具有旧窗口类某些特性的新窗口类。
  4. 用新窗口类创建窗口

如果要创建具有相同特性的多个控件，超类化就比子类化要好。

举例：

.386
.model flat,stdcall
option casemap:none
include \masm32\include\windows.inc
include \masm32\include\user32.inc
include \masm32\include\kernel32.inc
includelib \masm32\lib\user32.lib
includelib \masm32\lib\kernel32.lib
WM_SUPERCLASS equ WM_USER+5
WinMain PROTO :DWORD,:DWORD,:DWORD,:DWORD
EditWndProc PROTO :DWORD,:DWORD,:DWORD,:DWORD

.data
ClassName db "SuperclassWinClass",0
AppName db "Superclassing Demo",0
EditClass db "EDIT",0
OurClass db "SUPEREDITCLASS",0
Message db "You pressed the Enter key in the text box!",0

.data?
hInstance dd ?
hwndEdit dd 6 dup(?) ；存放6个窗口句柄的数组
OldWndProc dd ? ；原来的窗口过程

.code
start:
invoke GetModuleHandle, NULL
mov hInstance,eax
invoke WinMain, hInstance,NULL,NULL, SW_SHOWDEFAULT
invoke ExitProcess,eax

WinMain proc hInst:HINSTANCE,hPrevInst:HINSTANCE,CmdLine:LPSTR,CmdShow:DWORD
LOCAL wc:WNDCLASSEX
LOCAL msg:MSG
LOCAL hwnd:HWND

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 hInst
pop wc.hInstance
mov wc.hbrBackground,COLOR_APPWORKSPACE
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 wc
invoke CreateWindowEx,WS_EX_CLIENTEDGE+WS_EX_CONTROLPARENT,ADDR ClassName,ADDR AppName,\
WS_OVERLAPPED+WS_CAPTION+WS_SYSMENU+WS_MINIMIZEBOX+WS_MAXIMIZEBOX+WS_VISIBLE,CW_USEDEFAULT,\
CW_USEDEFAULT,350,220,NULL,NULL,\
hInst,NULL
mov hwnd,eax

.while TRUE
invoke GetMessage, ADDR msg,NULL,0,0
.BREAK .IF (!eax)
invoke TranslateMessage, ADDR msg
invoke DispatchMessage, ADDR msg
.endw
mov eax,msg.wParam
ret
WinMain endp

WndProc proc uses ebx edi hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM
LOCAL wc:WNDCLASSEX
.if uMsg==WM_CREATE
mov wc.cbSize,sizeof WNDCLASSEX
invoke GetClassInfoEx,NULL,addr EditClass,addr wc
push wc.lpfnWndProc
pop OldWndProc
mov wc.lpfnWndProc, OFFSET EditWndProc
push hInstance
pop wc.hInstance
mov wc.lpszClassName,OFFSET OurClass
invoke RegisterClassEx, addr wc
xor ebx,ebx
mov edi,20
.while ebx<6
invoke CreateWindowEx,WS_EX_CLIENTEDGE,ADDR OurClass,NULL,\
WS_CHILD+WS_VISIBLE+WS_BORDER,20,\
edi,300,25,hWnd,ebx,\
hInstance,NULL
mov dword ptr [hwndEdit+4*ebx],eax
add edi,25
inc ebx
.endw
invoke SetFocus,hwndEdit
.elseif uMsg==WM_DESTROY
invoke PostQuitMessage,NULL
.else
invoke DefWindowProc,hWnd,uMsg,wParam,lParam
ret
.endif
xor eax,eax
ret
WndProc endp

EditWndProc PROC hEdit:DWORD,uMsg:DWORD,wParam:DWORD,lParam:DWORD
.if uMsg==WM_CHAR
mov eax,wParam
.if (al>="0" && al<="9") || (al>="A" && al<="F") || (al>="a" && al<="f") || al==VK_BACK
；处理字符0~9，A~F，a~f，这几个十六进制数
.if al>="a" && al<="f"
sub al,20h
如果是字符a~f，则把它们变为大写
.endif
invoke CallWindowProc,OldWndProc,hEdit,uMsg,eax,lParam
ret
.endif
.elseif uMsg==WM_KEYDOWN
mov eax,wParam
.if al==VK_RETURN
invoke MessageBox,hEdit,addr Message,addr AppName,MB_OK+MB_ICONINFORMATION
invoke SetFocus,hEdit
.elseif al==VK_TAB
invoke GetKeyState,VK_SHIFT
test eax,80000000
.if ZERO?
invoke GetWindow,hEdit,GW_HWNDNEXT
.if eax==NULL
invoke GetWindow,hEdit,GW_HWNDFIRST
.endif
.else
invoke GetWindow,hEdit,GW_HWNDPREV
.if eax==NULL
invoke GetWindow,hEdit,GW_HWNDLAST
.endif
.endif
invoke SetFocus,eax
xor eax,eax
ret
.else
invoke CallWindowProc,OldWndProc,hEdit,uMsg,wParam,lParam
ret
.endif
.else
invoke CallWindowProc,OldWndProc,hEdit,uMsg,wParam,lParam
ret
.endif
xor eax,eax
ret
EditWndProc endp
end start

分析

这个程序创建了一个在其客户区有六个被修改的 Edit 控件的简单窗口，这些 Edit控件只接受十六进制的数字。实际上，这个例子是通过修改窗口了类化的例子得来的。这个程序开始和其它程序一样，有趣的部分出现在主窗口被创建的时候：

.if uMsg==WM_CREATE
mov wc.cbSize,sizeof WNDCLASSEX
invoke GetClassInfoEx,NULL,addr EditClass,addr wc

必须用想进行超类化操作的类数据填充 WNDCLASSEX 结构，在我们的例子中就是类 Edit ，记住在调用函数 GetClassInfoEx 之前必须填写成员 cbSize，否则函数调用 GetClassInfoEx不会在 WNDCLASSEX 结构中填入正确的返回值。成功返回后，变量 wc中保存的就是想要创建一个新类所需要的所有信息。

push wc.lpfnWndProc
pop OldWndProc
mov wc.lpfnWndProc, OFFSET EditWndProc
push hInstance
pop wc.hInstance
mov wc.lpszClassName,OFFSET OurClass

现在必须修改变量 wc 的一些属性：第一个要修改的就是指向窗口过程的指针。因为在新窗口过程中函数 CallWindowProx 要用到老窗口过程，因此得把它保存到一个变量中以便使用。这个技巧和在子类化中用到的一样，只不过不是调用 SetWindowLong 而是直接修改 WNDCLASSEX 结构罢了。接下来必须得为这个新类取个名字。

invoke RegisterClassEx, addr wc

当所有这些都完成时，注册这个新类就会得到一个具有旧类某些特征的新类了。

xor ebx,ebx
mov edi,20
.while ebx<6
invoke CreateWindowEx,WS_EX_CLIENTEDGE,ADDR OurClass,NULL,\
WS_CHILD+WS_VISIBLE+WS_BORDER,20,\
edi,300,25,hWnd,ebx,\
hInstance,NULL
mov dword ptr [hwndEdit+4*ebx],eax
add edi,25
inc ebx
.endw
invoke SetFocus,hwndEdit

注册完新类就可以创建基于它的窗口了：
在上面的程序片断中，用寄存器 ebx 来保存已创建的窗口数目，用寄存器 edi 来保存窗口左上角的 y 坐标。创建一个新窗口时，把它的句柄保存在一个双字的数组中，当创建完所有的窗口后，设定输入焦点为所创建的第一个窗口。

这时已经有6个只能接受十六进制数字的 edit 窗口控件了，替换的窗口过程处理了字符过滤，这实际上和在子类化中的例子是一样的。但不必做子类化那些窗口的额外工作了。

在此程序中，通过使用 Tabs 键来在各个 Edit 控件中切换来使得这个程序更加有趣。一般来说，如果使用对话框，对话框管理器会处理好所有这些问题，即：
按下 Tabs 输入焦点切换到下一个控件窗口中，按下 Shift-Tabs 输入焦点切换到上一个控件窗口中；但一个简单的窗口不具有这个功能，必须子类化它们以处理 Tabs 键。在这个例子中，不必一个一个去子类化已经进行过超类化操作的这些控件，可以使用一种集中控制切换策略。

.elseif al==VK_TAB
invoke GetKeyState,VK_SHIFT
test eax,80000000
.if ZERO?
invoke GetWindow,hEdit,GW_HWNDNEXT
.if eax==NULL
invoke GetWindow,hEdit,GW_HWNDFIRST
.endif
.else
invoke GetWindow,hEdit,GW_HWNDPREV
.if eax==NULL
invoke GetWindow,hEdit,GW_HWNDLAST
.endif
.endif
invoke SetFocus,eax
xor eax,eax
ret

上面是摘自于 EditWndClass 过程的程序片断，它检查用户是否按下了 Tabs 键，若是就调用函数 GetKeyState 来检查 SHIFT 键是否也被同时按下了。函数 GetKeyState 在寄存器 eax 中设立一个返回值，用于判断某个特定的键是否被按下了，若按下了，则把 eax 的的最高位置1，否则把最高位清0。所以只要用 80000000h 来测试返回值就行了，若最高位是1则说明用户按下了 SHIFT-Tabs，这需要单独处理；否则说明只按下 Tabs 键，调用函数 GetWindow 来获得 hEdit 所指向窗口的下一个窗口句柄，若该函数返回 NULL ，说明这是当前窗口是窗口链中最后一个窗口了，应该通过以参数 GW_HWNDFIRST 调用函数 GetWindow 来卷回到窗口链中的第一个窗口控件。SHIFT-Tabs 的处理过程和这正好相反。

翻译：ZhangJun，校对：LuoYunBin's Win32 ASM Page， http://asm.yeah.net


Iczelion's win32 asm tutorial

第二十三课 系统托盘中的快捷图标

本课中，我们将学习如何把小图标放到系统托盘中去以及如何创建和使用弹出式菜单。

理论：

系统托盘是指任务条中的一个方形区域，在该区域中可以放入一些小图标，通常您可以在此处看到系统提供的最新时间。您自己当然也可以把快捷小图标放到此处。下面是这么做的步骤：
  1. 设置NOTIFYICONDATA型的结构体变量的成员变量的值：
    • cbSize 该结构体的大小。
    • hwnd 窗口的句柄。当鼠标滑过该小图标时，该窗口将接收到相关的消息。
    • uID 小图标的ID号。您可以取任意值，只是当您的应用程序有不止一个小图标时，您要能够区分出到底是那一个小图标接收到了鼠标的消息，也即ID号必须唯一。
    • uFlags 指定该结构体变量的那些成员变量有效。
      • NIF_ICON 有效。
      • NIF_MESSAGE 有效。
      • NIF_TIP 有效。
    • uCallbackMessage 自定义的消息。当鼠标对小图标动作时，WINDOWS外壳将把该消息发送到您的应用程序。该消息的值您可以自己定义。
    • hIcon 放入系统托盘中的图标的句柄。
    • szTip 64字节的缓冲区，它用来放入提示字符串，当鼠标停留在小图标上时，就会显示该字符串。
  2. 调用Shell_NotifyIcon函数。该函数在shell32.inc中定义，其原型如下：

    Shell_NotifyIcon PROTO dwMessage:DWORD ,pnid:DWORD

    dwMessage 是发送到WINDOWS外壳的消息：
    NIM_ADD 把小图标加到系统托盘区。
    NIM_DELETE 从系统托盘中删除小图标。
    NIM_MODIFY 修改小图标。
    pnid 是指向NOTIFYICONDATA型结构体变量的指针。
    如果您想要加入一个小图标就用NIM_ADD，删除时使用NIM_DELETE消息。

基本上的消息就是这些。但是大多数的情况下，您不会仅仅满足把一个小图标放到那里。您还必须要对鼠标事件作出适当的反应。您可以在NOTIFYICONDATA型的结构体变量的成员变量uCallbackMessage 中设置您要处理的消息，然后WINDOWS外壳将在发生这些事件时通知您的应用程序。随着消息传送的参数wParam和lParam的值如下：
  • wParam 小图标的ID号。它和您在NOTIFYICONDATA型结构体变量中的成员变量uID中设置的值一样。
  • lParam 低字包含鼠标消息。譬如，用户在小图标上按下了右键时，lParam中将包含WM_RBUTTONDOWN消息。
大多数的系统托盘中的小图标，在用户用鼠标右击时都会弹出一个菜单以方便用户选择。我们可先创建菜单，然后调用TrackPopupMenu函数来显示它。步骤如下：
  1. 调用CreatePopupMenu函数来创建菜单。该函数创建一个空的菜单。如果成功，将在eax中返回该菜单的句柄。
  2. 调用AppendMenu, InsertMenu 或 InsertMenuItem来向菜单中加入菜单项。
  3. 当您想在当前鼠标位置显示该菜单时，调用GetCursorPosition函数来得到鼠标当前的屏幕位置，然后调用TrackPopupMenu来显示菜单。当用户从弹出式菜单中选择了一个菜单项时，WINDOWS将发送WM_COMMAND消息给您应用程序的消息处理过程，这和通常的菜单选择是一样的。.
注意：当您使用系统托盘中的小图标时有两件比较讨厌的事：
  1. 该菜单可能不会像通常那样马上消失掉。这是因为从弹出式接收消息的窗口必须是前景窗口。调用SetForegroundWindow函数就可以纠正该错误;
  2. 在调用了SetForegroundWindow函数后，您会发现第一次该弹出式菜单会正常弹出而且工作的很好。但是随后，该菜单只是一弹出就立即消失。根据MSDN，这么做是故意的。为了使得弹出菜单保持住，必须要求下一个切换到的是程序的主窗口。您可以通过邮寄任何消息给该程序的窗口来强行进行任务切换。注意要使用PostMessage而不是SendMessage。

例子：

.386
.model flat,stdcall
option casemap:none
include \masm32\include\windows.inc
include \masm32\include\user32.inc
include \masm32\include\kernel32.inc
include \masm32\include\shell32.inc
includelib \masm32\lib\user32.lib
includelib \masm32\lib\kernel32.lib
includelib \masm32\lib\shell32.lib

WM_SHELLNOTIFY equ WM_USER+5
IDI_TRAY equ 0
IDM_RESTORE equ 1000
IDM_EXIT equ 1010
WinMain PROTO :DWORD,:DWORD,:DWORD,:DWORD

.data
ClassName db "TrayIconWinClass",0
AppName db "TrayIcon Demo",0
RestoreString db "&Restore",0
ExitString db "E&xit Program",0

.data?
hInstance dd ?
note NOTIFYICONDATA <>
hPopupMenu dd ?

.code
start:
invoke GetModuleHandle, NULL
mov hInstance,eax
invoke WinMain, hInstance,NULL,NULL, SW_SHOWDEFAULT
invoke ExitProcess,eax

WinMain proc hInst:HINSTANCE,hPrevInst:HINSTANCE,CmdLine:LPSTR,CmdShow:DWORD
LOCAL wc:WNDCLASSEX
LOCAL msg:MSG
LOCAL hwnd:HWND
mov wc.cbSize,SIZEOF WNDCLASSEX
mov wc.style, CS_HREDRAW or CS_VREDRAW or CS_DBLCLKS
mov wc.lpfnWndProc, OFFSET WndProc
mov wc.cbClsExtra,NULL
mov wc.cbWndExtra,NULL
push hInst
pop wc.hInstance
mov wc.hbrBackground,COLOR_APPWORKSPACE
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 wc
invoke CreateWindowEx,WS_EX_CLIENTEDGE,ADDR ClassName,ADDR AppName,\
WS_OVERLAPPED+WS_CAPTION+WS_SYSMENU+WS_MINIMIZEBOX+WS_MAXIMIZEBOX+WS_VISIBLE,CW_USEDEFAULT,\
CW_USEDEFAULT,350,200,NULL,NULL,\
hInst,NULL
mov hwnd,eax
.while TRUE
invoke GetMessage, ADDR msg,NULL,0,0
.BREAK .IF (!eax)
invoke TranslateMessage, ADDR msg
invoke DispatchMessage, ADDR msg
.endw
mov eax,msg.wParam
ret
WinMain endp

WndProc proc hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM
LOCAL pt:POINT
.if uMsg==WM_CREATE
invoke CreatePopupMenu
mov hPopupMenu,eax
invoke AppendMenu,hPopupMenu,MF_STRING,IDM_RESTORE,addr RestoreString
invoke AppendMenu,hPopupMenu,MF_STRING,IDM_EXIT,addr ExitString
.elseif uMsg==WM_DESTROY
invoke DestroyMenu,hPopupMenu
invoke PostQuitMessage,NULL
.elseif uMsg==WM_SIZE
.if wParam==SIZE_MINIMIZED
mov note.cbSize,sizeof NOTIFYICONDATA
push hWnd
pop note.hwnd
mov note.uID,IDI_TRAY
mov note.uFlags,NIF_ICON+NIF_MESSAGE+NIF_TIP
mov note.uCallbackMessage,WM_SHELLNOTIFY
invoke LoadIcon,NULL,IDI_WINLOGO
mov note.hIcon,eax
invoke lstrcpy,addr note.szTip,addr AppName
invoke ShowWindow,hWnd,SW_HIDE
invoke Shell_NotifyIcon,NIM_ADD,addr note
.endif
.elseif uMsg==WM_COMMAND
.if lParam==0
invoke Shell_NotifyIcon,NIM_DELETE,addr note
mov eax,wParam
.if ax==IDM_RESTORE
invoke ShowWindow,hWnd,SW_RESTORE
.else
invoke DestroyWindow,hWnd
.endif
.endif
.elseif uMsg==WM_SHELLNOTIFY
.if wParam==IDI_TRAY
.if lParam==WM_RBUTTONDOWN
invoke GetCursorPos,addr pt
invoke SetForegroundWindow,hWnd
invoke TrackPopupMenu,hPopupMenu,TPM_RIGHTALIGN,pt.x,pt.y,NULL,hWnd,NULL
invoke PostMessage,hWnd,WM_NULL,0,0
.elseif lParam==WM_LBUTTONDBLCLK
invoke SendMessage,hWnd,WM_COMMAND,IDM_RESTORE,0
.endif
.endif
.else
invoke DefWindowProc,hWnd,uMsg,wParam,lParam
ret
.endif
xor eax,eax
ret
WndProc endp

end start

分析：

该程序将显示一个简单的窗口。当您按下最小化按钮时，该窗口将隐藏，然后放一个小图标到系统托盘中。当您双击小图标时，应用程序将恢复自己，并把小图标从系统托盘中删除。当您右击小图标时，会显示一个弹出式菜单。您可以在菜单中选择是恢复窗口还是退出应用程序。

.if uMsg==WM_CREATE
invoke CreatePopupMenu
mov hPopupMenu,eax
invoke AppendMenu,hPopupMenu,MF_STRING,IDM_RESTORE,addr RestoreString
invoke AppendMenu,hPopupMenu,MF_STRING,IDM_EXIT,addr ExitString

当主窗口创建时，将会创建一个弹出式菜单，并且加入两个菜单项。 AppendMenu的语法如下：

AppendMenu PROTO hMenu:DWORD, uFlags:DWORD, uIDNewItem:DWORD, lpNewItem:DWORD
  • hMenu 是将要加入菜单项的菜单的句柄。
  • uFlags 告诉WINDOWS要加入的菜单项是位图、字符串或自画的项目以及是可用、不可用或灰色显示等。您可以从WIN32 API 指南中得到全部的标志位的信息。在我们的例子中使用标志位MF_STRING，它表示我们加入的菜单项是字符串。
  • uIDNewItem 是菜单项的ID号。这是一个用户自定义的值，它用来唯一地代表菜单项。.
  • lpNewItem 用来指定菜单项的内容，具体代表什么取决于uFlags中指定的标志。我们前面指定了MF_STRING标志，所以此处代表一个字符串
主窗口创建完成后，用户就可以开始测试了。这时按下最小化键。
当一个窗口被最小化时将接收到WM_SIZE消息，其中wParam参数中的值为SIZE_MINIMIZED。

.elseif uMsg==WM_SIZE
.if wParam==SIZE_MINIMIZED
mov note.cbSize,sizeof NOTIFYICONDATA
push hWnd
pop note.hwnd
mov note.uID,IDI_TRAY
mov note.uFlags,NIF_ICON+NIF_MESSAGE+NIF_TIP
mov note.uCallbackMessage,WM_SHELLNOTIFY
invoke LoadIcon,NULL,IDI_WINLOGO
mov note.hIcon,eax
invoke lstrcpy,addr note.szTip,addr AppName
invoke ShowWindow,hWnd,SW_HIDE
invoke Shell_NotifyIcon,NIM_ADD,addr note
.endif

这时我们来给NOTIFYICONDATA型结构体变量赋值。IDI_TRAY是在代码开始处定义的一个数值常量，您可以任意设定它的值。由于我们仅有一个图标，所以这一点并不重要，如果要同时加入几个系统图标的话，那么每个图标都要有一个唯一的ID号。由于我们指定了一个图标NIF_ICON，所以我们要在uFlags成员变量中指定所有的标志位，我们还指定了一个自定义的消息NIF_MESSAGE和帮助文本NIF_TIP。 WM_SHELLNOTIFY 被定义为WM_USER+5，只要是唯一的值，就无所谓是多少了，只要大于WM_USER。我们这里用的是WINDOWS登录时的图标，当然您可以使用任意您想要用的图标，您可以用LoadIcon函数从资源中装载，该函数返回一个图标的句柄。最后我们在szTip中放入当鼠标放在图标时显示的提示文本。为了达到“最小化然后只显示图标的效果”，我们在这时隐藏掉主窗口。
接下来，我们调用Shell_NotifyIcon函数并指定标志位NIM_ADD把图标加到系统托盘中去。

现在我们的主窗口隐藏了，图标显示在系统托盘中。如果您让鼠标从图标上滑过，将看到提示文本。如果您双击小图标，主窗口就会显示，图标将消失。

.elseif uMsg==WM_SHELLNOTIFY
.if wParam==IDI_TRAY
.if lParam==WM_RBUTTONDOWN
invoke GetCursorPos,addr pt
invoke SetForegroundWindow,hWnd
invoke TrackPopupMenu,hPopupMenu,TPM_RIGHTALIGN,pt.x,pt.y,NULL,hWnd,NULL
invoke PostMessage,hWnd,WM_NULL,0,0
.elseif lParam==WM_LBUTTONDBLCLK
invoke SendMessage,hWnd,WM_COMMAND,IDM_RESTORE,0
.endif
.endif

当在系统托盘中的图标发生鼠标事件时，您的窗口将接收到WM_SHELLNOTIFY消息，该消息是在uCallbackMessage成员变量中指定的。在接收到该消息时，wParam中包含了图标的ID号，lParam中包含了鼠标动作的原始数据。在上面的代码中，我们首先检测是否是我们感兴趣的消息。如果是的话，我们在看看是什么消息。因为我们只对右击和双击事件感兴趣，所以我们仅仅处理WM_RBUTTONDOWN和WM_LBUTTONDBLCLK消息。
如果是WM_RBUTTONDOWN，我们调用GetCursorPos来得到鼠标光标所在的当前屏幕位置。注意我指的是屏幕位置，即，其坐标是相对于整个的屏幕的。譬如，如果屏幕的解析读640*480，那么它的右下角的坐标是x==639 ，y==479。如果您想要把屏幕位置转换成窗口的坐标，可以调用ScreenToClient函数
我们想要在当前的位置显示弹出式菜单，我们就调用TrackPopupMenu函数，该函数需要屏幕的坐标，由GetCursorPos函数返回的坐标就可以原封不动的拿过来用。
TrackPopupMenu的原型如下：

    TrackPopupMenu PROTO hMenu:DWORD, uFlags:DWORD, x:DWORD, y:DWORD, nReserved:DWORD, hWnd:DWORD, prcRect:DWORD
  • hMenu 是弹出式菜单的句柄。
  • uFlags 功能的选择。像在哪里放置（相对于随后将指定的坐标）菜单，那一个鼠标按钮用来跟踪弹出式菜单。在我们的例子中，我们用TPM_RIGHTALIGN标志位来指定弹出式菜单放在坐标的左边。
  • x 和 y 指定放置菜单的屏幕坐标。
  • nReserved 必须为NULL。
  • hWnd 是将要接收消息的窗口的句柄。
  • prcRect 指定一个矩形区域。如果在该矩形区域外面按下鼠标的话，菜单将消失。一般我们把该值设为NULL，这样当用户只要在菜单外面按下鼠标，菜单立即消失。

当用户双击图标时，我们给我们自己的窗口发送WM_COMMAND消息，并指定消息为IDM_RESTORE，这样可以达到和在弹出式菜单中选择“Restore”菜单项同样的效果。为了能够接收到双击消息，主窗口必须要有的CS_DBLCLKS 风格。

invoke Shell_NotifyIcon,NIM_DELETE,addr note
mov eax,wParam
.if ax==IDM_RESTORE
invoke ShowWindow,hWnd,SW_RESTORE
.else
invoke DestroyWindow,hWnd
.endif

当用户选择恢复主窗口时，我们调用Shell_NotifyIcon函数来删除掉系统托盘中的图标，这一次我们要指定NIM_DELETE消息。接下来我们把主窗口恢复到原始的状态。如果用户选择了Exit菜单项，我们不但把图标给删除掉，也从整个的应用程序中退出。

翻译：Lxx.阿龙，校对：LuoYunBin's Win32 ASM Page， http://asm.yeah.net/


Iczelion's win32 asm tutorial

第二十四课 WINDOWS钩子函数

本课中我们将要学习WINDOWS钩子函数的使用方法。WINDOWS钩子函数的功能非常强大，有了它您可以探测其它进程并且改变其它进程的行为。

理论：

WINDOWS的钩子函数可以认为是WINDOWS的主要特性之一。利用它们，您可以捕捉您自己进程或其它进程发生的事件。通过“钩挂”，您可以给WINDOWS一个处理或过滤事件的回调函数，该函数也叫做“钩子函数”，当每次发生您感兴趣的事件时，WINDOWS都将调用该函数。一共有两种类型的钩子：局部的和远程的。
  • 局部钩子仅钩挂您自己进程的事件。
  • 远程的钩子还可以将钩挂其它进程发生的事件。远程的钩子又有两种：
    • 基于线程的 它将捕获其它进程中某一特定线程的事件。简言之，就是可以用来观察其它进程中的某一特定线程将发生的事件。
    • 系统范围的 将捕捉系统中所有进程将发生的事件消息。
安装钩子函数将会影响系统的性能。监测“系统范围事件”的系统钩子特别明显。因为系统在处理所有的相关事件时都将调用您的钩子函数，这样您的系统将会明显的减慢。所以应谨慎使用，用完后立即卸载。还有，由于您可以预先截获其它进程的消息，所以一旦您的钩子函数出了问题的话必将影响其它的进程。记住：功能强大也意味着使用时要负责任。
在正确使用钩子函数前，我们先讲解钩子函数的工作原理。当您创建一个钩子时，WINDOWS会先在内存中创建一个数据结构，该数据结构包含了钩子的相关信息，然后把该结构体加到已经存在的钩子链表中去。新的钩子将加到老的前面。当一个事件发生时，如果您安装的是一个局部钩子，您进程中的钩子函数将被调用。如果是一个远程钩子，系统就必须把钩子函数插入到其它进程的地址空间，要做到这一点要求钩子函数必须在一个动态链接库中，所以如果您想要使用远程钩子，就必须把该钩子函数放到动态链接库中去。当然有两个例外：工作日志钩子和工作日志回放钩子。这两个钩子的钩子函数必须在安装钩子的线程中。原因是：这两个钩子是用来监控比较底层的硬件事件的，既然是记录和回放，所有的事件就当然都是有先后次序的。所以如果把回调函数放在DLL中，输入的事件被放在几个线程中记录，所以我们无法保证得到正确的次序。故解决的办法是：把钩子函数放到单个的线程中，譬如安装钩子的线程。
钩子一共有14种，以下是它们被调用的时机：
  • WH_CALLWNDPROC 当调用SendMessage时
  • WH_CALLWNDPROCRET 当SendMessage的调用返回时
  • WH_GETMESSAGE 当调用GetMessage 或 PeekMessage时
  • WH_KEYBOARD 当调用GetMessage 或 PeekMessage 来从消息队列中查询WM_KEYUP 或 WM_KEYDOWN 消息时
  • WH_MOUSE 当调用GetMessage 或 PeekMessage 来从消息队列中查询鼠标事件消息时
  • WH_HARDWARE 当调用GetMessage 或 PeekMessage 来从消息队列种查询非鼠标、键盘消息时
  • WH_MSGFILTER 当对话框、菜单或滚动条要处理一个消息时。该钩子是局部的。它时为那些有自己的消息处理过程的控件对象设计的。
  • WH_SYSMSGFILTER 和WH_MSGFILTER一样，只不过是系统范围的
  • WH_JOURNALRECORD 当WINDOWS从硬件队列中获得消息时
  • WH_JOURNALPLAYBACK 当一个事件从系统的硬件输入队列中被请求时
  • WH_SHELL 当关于WINDOWS外壳事件发生时，譬如任务条需要重画它的按钮.
  • WH_CBT 当基于计算机的训练(CBT)事件发生时
  • WH_FOREGROUNDIDLE 由WINDOWS自己使用，一般的应用程序很少使用
  • WH_DEBUG 用来给钩子函数除错
现在我们知道了一些基本的理论，现在开始讲解如何安装/卸载一个钩子。
要安装一个钩子，您可以调用SetWindowHookEx函数。该函数的原型如下：
SetWindowsHookEx proto HookType:DWORD, pHookProc:DWORD, hInstance:DWORD, ThreadID:DWORD
  • HookType 是我们上面列出的值之一,譬如： WH_MOUSE, WH_KEYBOARD
  • pHookProc 是钩子函数的地址。如果使用的是远程的钩子，就必须放在一个DLL中，否则放在本身代码中
  • hInstance 钩子函数所在DLL的实例句柄。如果是一个局部的钩子，该值为NULL
  • ThreadID 是您安装该钩子函数后想监控的线程的ID号。该参数可以决定该钩子是局部的还是系统范围的。如果该值为NULL，那么该钩子将被解释成系统范围内的，那它就可以监控所有的进程及它们的线程。如果您指定了您自己进程中的某个线程ID 号，那该钩子是一个局部的钩子。如果该线程ID是另一个进程中某个线程的ID，那该钩子是一个全局的远程钩子。这里有两个特殊情况：WH_JOURNALRECORD 和 WH_JOURNALPLAYBACK总是代表局部的系统范围的钩子，之所以说是局部，是因为它们没有必要放到一个DLL中。WH_SYSMSGFILTER 总是一个系统范围内的远程钩子。其实它和WH_MSGFILTER钩子类似，如果把参数ThreadID设成0的话，它们就完全一样了。
如果该函数调用成功的话，将在eax中返回钩子的句柄，否则返回NULL。您必须保存该句柄，因为后面我们还要它来卸载钩子。
要卸载一个钩子时调用UnhookWidowHookEx函数，该函数仅有一个参数，就是欲卸载的钩子的句柄。如果调用成功的话，在eax中返回非0值，否则返回NULL。
现在您知道了如何安装和卸载一个钩子了，接下来我们将看看钩子函数。.
只要您安装的钩子的消息事件类型发生，WINDOWS就将调用钩子函数。譬如您安装的钩子是WH_MOUSE类型，那么只要有一个鼠标事件发生时，该钩子函数就会被调用。不管您安装的时那一类型钩子，钩子函数的原型都时是一样的：
    HookProc proto nCode:DWORD, wParam:DWORD, lParam:DWORD
    • nCode 指定是否需要处理该消息
    • wParam 和 lParam 包含该消息的附加消息
HookProc 可以看作是一个函数名的占位符。只要函数的原型一致，您可以给该函数取任何名字。至于以上的几个参数及返回值的具体含义各种类型的钩子都不相同。譬如：
WH_CALLWNDPROC
  • nCode 只能是HC_ACTION，它代表有一个消息发送给了一个窗口
  • wParam 如果非0，代表正被发送的消息
  • lParam 指向CWPSTRUCT型结构体变量的指针
  • return value: 未使用，返回0
WH_MOUSE
  • nCode 为HC_ACTION 或 HC_NOREMOVE
  • wParam 包含鼠标的事件消息
  • lParam 指向MOUSEHOOKSTRUCT型结构体变量的指针
  • return value: 如果不处理返回0，否则返回非0值
所以您必须查询您的WIN32 API 指南来得到不同类型的钩子的参数的详细定义以及它们返回值的意义。这里还有一个问题需要注意：所有的钩子都串在一个链表上，最近加入的钩子放在链表的头部。当一个事件发生时，WINDOWS将按照从链表头到链表尾调用的顺序。所以您的钩子函数有责任把消息传到下一个链中的钩子函数。当然您可以不这样做，但是您最好明白这时这么做的原因。在大多数的情况下，最好把消息事件传递下去以便其它的钩子都有机会获得处理这一消息的机会。调用下一个钩子函数可以调用函数CallNextHookEx。该函数的原型如下：
CallNextHookEx proto hHook:DWORD, nCode:DWORD, wParam:DWORD, lParam:DWORD
  • hHook 时是您自己的钩子函数的句柄。利用该句柄可以遍历钩子链。
  • nCode, wParam and lParam 您只要把传入的参数简单传给CallNextHookEx即可。
请注意：对于远程钩子，钩子函数必须放到DLL中，它们将从DLL中映射到其它的进程空间中去。当WINDOWS映射DLL到其它的进程空间中去时，不会把数据段也进行映射。简言之，所有的进程仅共享DLL的代码，至于数据段，每一个进程都将有其单独的拷贝。这是一个很容易被忽视的问题。您可能想当然的以为，在DLL中保存的值可以在所有映射该DLL的进程之间共享。在通常情况下，由于每一个映射该DLL的进程都有自己的数据段，所以在大多数的情况下您的程序运行得都不错。但是钩子函数却不是如此。对于钩子函数来说，要求DLL的数据段对所有的进程也必须相同。这样您就必须把数据段设成共享的，这可以通过在链接开关中指定段的属性来实现。在MASM中您可以这么做：
/SECTION:<section name>, S
已初期化的段名是.data，未初始化的段名是.bss。`加入您想要写一个包含钩子函数的DLL，而且想使它的未初始化的数据段在所有进程间共享，您必须这么做：
link /section:.bss,S /DLL /SUBSYSTEM:WINDOWS ..........
S 代表该段是共享段。

例子：

一共有两个模块：一个是GUI部分，另一个是安装和卸载钩子的DLL。

;--------------------------------------------- 主程序的源代码部分--------------------------------------
.386
.model flat,stdcall
option casemap:none
include \masm32\include\windows.inc
include \masm32\include\user32.inc
include \masm32\include\kernel32.inc
include mousehook.inc
includelib mousehook.lib
includelib \masm32\lib\user32.lib
includelib \masm32\lib\kernel32.lib

wsprintfA proto C :DWORD,:DWORD,:VARARG
wsprintf TEXTEQU <wsprintfA>

.const
IDD_MAINDLG equ 101
IDC_CLASSNAME equ 1000
IDC_HANDLE equ 1001
IDC_WNDPROC equ 1002
IDC_HOOK equ 1004
IDC_EXIT equ 1005
WM_MOUSEHOOK equ WM_USER+6

DlgFunc PROTO :DWORD,:DWORD,:DWORD,:DWORD

.data
HookFlag dd FALSE
HookText db "&Hook",0
UnhookText db "&Unhook",0
template db "%lx",0

.data?
hInstance dd ?
hHook dd ?
.code
start:
invoke GetModuleHandle,NULL
mov hInstance,eax
invoke DialogBoxParam,hInstance,IDD_MAINDLG,NULL,addr DlgFunc,NULL
invoke ExitProcess,NULL

DlgFunc proc hDlg:DWORD,uMsg:DWORD,wParam:DWORD,lParam:DWORD
LOCAL hLib:DWORD
LOCAL buffer[128]:byte
LOCAL buffer1[128]:byte
LOCAL rect:RECT
.if uMsg==WM_CLOSE
.if HookFlag==TRUE
invoke UninstallHook
.endif
invoke EndDialog,hDlg,NULL
.elseif uMsg==WM_INITDIALOG
invoke GetWindowRect,hDlg,addr rect
invoke SetWindowPos, hDlg, HWND_TOPMOST, rect.left, rect.top, rect.right, rect.bottom, SWP_SHOWWINDOW
.elseif uMsg==WM_MOUSEHOOK
invoke GetDlgItemText,hDlg,IDC_HANDLE,addr buffer1,128
invoke wsprintf,addr buffer,addr template,wParam
invoke lstrcmpi,addr buffer,addr buffer1
.if eax!=0
invoke SetDlgItemText,hDlg,IDC_HANDLE,addr buffer
.endif
invoke GetDlgItemText,hDlg,IDC_CLASSNAME,addr buffer1,128
invoke GetClassName,wParam,addr buffer,128
invoke lstrcmpi,addr buffer,addr buffer1
.if eax!=0
invoke SetDlgItemText,hDlg,IDC_CLASSNAME,addr buffer
.endif
invoke GetDlgItemText,hDlg,IDC_WNDPROC,addr buffer1,128
invoke GetClassLong,wParam,GCL_WNDPROC
invoke wsprintf,addr buffer,addr template,eax
invoke lstrcmpi,addr buffer,addr buffer1
.if eax!=0
invoke SetDlgItemText,hDlg,IDC_WNDPROC,addr buffer
.endif
.elseif uMsg==WM_COMMAND
.if lParam!=0
mov eax,wParam
mov edx,eax
shr edx,16
.if dx==BN_CLICKED
.if ax==IDC_EXIT
invoke SendMessage,hDlg,WM_CLOSE,0,0
.else
.if HookFlag==FALSE
invoke InstallHook,hDlg
.if eax!=NULL
mov HookFlag,TRUE
invoke SetDlgItemText,hDlg,IDC_HOOK,addr UnhookText
.endif
.else
invoke UninstallHook
invoke SetDlgItemText,hDlg,IDC_HOOK,addr HookText
mov HookFlag,FALSE
invoke SetDlgItemText,hDlg,IDC_CLASSNAME,NULL
invoke SetDlgItemText,hDlg,IDC_HANDLE,NULL
invoke SetDlgItemText,hDlg,IDC_WNDPROC,NULL
.endif
.endif
.endif
.endif
.else
mov eax,FALSE
ret
.endif
mov eax,TRUE
ret
DlgFunc endp

end start

;----------------------------------------------------- DLL的源代码部分 --------------------------------------
.386
.model flat,stdcall
option casemap:none
include \masm32\include\windows.inc
include \masm32\include\kernel32.inc
includelib \masm32\lib\kernel32.lib
include \masm32\include\user32.inc
includelib \masm32\lib\user32.lib

.const
WM_MOUSEHOOK equ WM_USER+6

.data
hInstance dd 0

.data?
hHook dd ?
hWnd dd ?

.code
DllEntry proc hInst:HINSTANCE, reason:DWORD, reserved1:DWORD
.if reason==DLL_PROCESS_ATTACH
push hInst
pop hInstance
.endif
mov eax,TRUE
ret
DllEntry Endp

MouseProc proc nCode:DWORD,wParam:DWORD,lParam:DWORD
invoke CallNextHookEx,hHook,nCode,wParam,lParam
mov edx,lParam
assume edx:PTR MOUSEHOOKSTRUCT
invoke WindowFromPoint,[edx].pt.x,[edx].pt.y
invoke PostMessage,hWnd,WM_MOUSEHOOK,eax,0
assume edx:nothing
xor eax,eax
ret
MouseProc endp

InstallHook proc hwnd:DWORD
push hwnd
pop hWnd
invoke SetWindowsHookEx,WH_MOUSE,addr MouseProc,hInstance,NULL
mov hHook,eax
ret
InstallHook endp

UninstallHook proc
invoke UnhookWindowsHookEx,hHook
ret
UninstallHook endp

End DllEntry

;---------------------------------------------- DLL的Makefile文件 ----------------------------------------------

NAME=mousehook
$(NAME).dll: $(NAME).obj
Link /SECTION:.bss,S /DLL /DEF:$(NAME).def /SUBSYSTEM:WINDOWS /LIBPATH:c:\masm\lib $(NAME).obj
$(NAME).obj: $(NAME).asm
ml /c /coff /Cp $(NAME).asm

分析:

该应用程序的主窗口中包括三个编辑控件，它们将分别显示当前鼠标光标所在位置的窗口类名、窗口句柄和窗口过程的地址。还有两个按钮：“Hook”和“Eixt”。当您按下Hook时，应用程序将钩挂鼠标输入的事件消息，该按钮的文本将变成“Unhook”。当您把鼠标关标滑过一个窗口时，该窗口的有关消息将显示在主窗口中。当您按下“Unhook”时，应用程序将卸载钩子。 主窗口使用一个对话框来作为它的主窗口。它自定义了一个消息WM_MOUSEHOOK，用来在主窗口和DLL之间传递消息。当主窗口接收到该消息时，wParam中包含了光标所在位置的窗口的句柄。当然这是我们做的安排。我这么做只是为了方便。您可以使用您自己的方法在主应用程序和DLL之间进行通讯。

.if HookFlag==FALSE
invoke InstallHook,hDlg
.if eax!=NULL
mov HookFlag,TRUE
invoke SetDlgItemText,hDlg,IDC_HOOK,addr UnhookText
.endif

该应用程序有一个全局变量，HookFlag，它用来监视钩子的状态。如果安装来钩子它就是TRUE，否则是FALSE。 当用户按下Hook按钮时，应用程序检查钩子是否已经安装。如果还没有的话，它将调用DLL中引出的函数InstallHook来安装它。注意我们把主对话框的句柄传递给了DLL，这样这个钩子DLL就可以把WM_MOUSEHOOK消息传递给正确的窗口了。当应用程序加载时，钩子DLL也同时加载。时机上当主程序一旦加载到内存中后，DLL就立即加载。DLL的入口点函数载主程序的第一条语句执行前就前执行了。所以当主程序执行时，DLL已经初始化好了。我们载入口点处放入如下代码：

.if reason==DLL_PROCESS_ATTACH
push hInst
pop hInstance
.endif

该段代码把DLL自己的实例句柄放到一个全局变量中保存。由于入口点函数是在所有函数调用前被执行的，所以hInstance总是有效的。我们把该变量放到.data中，使得每一个进程都有自己一个该变量的值。因为当鼠标光标停在一个窗口上时，钩子DLL被映射进进程的地址空间。加入在DLL缺省加载的地址处已经加载其它的DLL，那钩子DLL将要被映射到其他的地址。hInstance将被更新成其它的值。当用户按下Unhook再按下Hook时，SetWindowsHookEx将被再次调用。这一次，它将把新的地址作为实例句柄。而在例子中这是错误的，DLL装载的地址并没有变。这个钩子将变成一个局部的，您只能钩挂发生在您窗口中的鼠标事件，这是很难让人满意的 。

InstallHook proc hwnd:DWORD
push hwnd
pop hWnd
invoke SetWindowsHookEx,WH_MOUSE,addr MouseProc,hInstance,NULL
mov hHook,eax
ret
InstallHook endp

InstallHook 函数非常简单。它把传递过来的窗口句柄保存在hWnd中以备后用。接着调用SetWindowsHookEx函数来安装一个鼠标钩子。该函数的返回值放在全局变量hHook中，将来在UnhookWindowsHookEx中还要使用。在调用SetWindowsHookEx后，鼠标钩子就开始工作了。无论什么时候发生了鼠标事件，MouseProc函数都将被调用：

MouseProc proc nCode:DWORD,wParam:DWORD,lParam:DWORD
invoke CallNextHookEx,hHook,nCode,wParam,lParam
mov edx,lParam
assume edx:PTR MOUSEHOOKSTRUCT
invoke WindowFromPoint,[edx].pt.x,[edx].pt.y
invoke PostMessage,hWnd,WM_MOUSEHOOK,eax,0
assume edx:nothing
xor eax,eax
ret
MouseProc endp

钩子函数首先调用CallNextHookEx函数让其它的钩子处理该鼠标事件。然后，调用WindowFromPoint函数来得到给定屏幕坐标位置处的窗口句柄。注意：我们用lParam指向的MOUSEHOOKSTRUCT型结构体变量中的POINT成员变量作为当前的鼠标位置。在我们调用PostMessage函数把WM_MOUSEHOOK消息发送到主程序。您必须记住的一件事是：在钩子函数中不要使用SendMessage函数，它会引起死锁。MOUSEHOOKSTRUCT的定义如下：

MOUSEHOOKSTRUCT STRUCT DWORD
pt POINT <>
hwnd DWORD ?
wHitTestCode DWORD ?
dwExtraInfo DWORD ?
MOUSEHOOKSTRUCT ENDS

  • pt 是当前鼠标所在的屏幕位置。
  • hwnd 是将接收鼠标消息的窗口的句柄。通常它是鼠标所在处的窗口，但是如果窗口调用了SetCapture，鼠标的输入将到向到这个窗口。因我们不用该成员变量而是用WindowFromPoint函数。
  • wHitTestCode 指定hit-test值，该值给出了更多的鼠标位置值。它指定了鼠标在窗口的那个部位。该值的完全列表，请参考WIN32 API 指南中的WM_NCHITTEST消息。
  • dwExtraInfo 该值包含了相关的信息。一般该值由mouse_event函数设定，可以调用GetMessageExtraInfo来获得。

当主窗口接收到WM_MOUSEHOOK 消息时，它用wParam参数中的窗口句柄来查询窗口的消息。

.elseif uMsg==WM_MOUSEHOOK
invoke GetDlgItemText,hDlg,IDC_HANDLE,addr buffer1,128
invoke wsprintf,addr buffer,addr template,wParam
invoke lstrcmpi,addr buffer,addr buffer1
.if eax!=0
invoke SetDlgItemText,hDlg,IDC_HANDLE,addr buffer
.endif
invoke GetDlgItemText,hDlg,IDC_CLASSNAME,addr buffer1,128
invoke GetClassName,wParam,addr buffer,128
invoke lstrcmpi,addr buffer,addr buffer1
.if eax!=0
invoke SetDlgItemText,hDlg,IDC_CLASSNAME,addr buffer
.endif
invoke GetDlgItemText,hDlg,IDC_WNDPROC,addr buffer1,128
invoke GetClassLong,wParam,GCL_WNDPROC
invoke wsprintf,addr buffer,addr template,eax
invoke lstrcmpi,addr buffer,addr buffer1
.if eax!=0
invoke SetDlgItemText,hDlg,IDC_WNDPROC,addr buffer
.endif

为了避免重绘文本时的抖动，我们把已经在编辑空间中线时的文本和我们将要显示的对比。如果相同，就可以忽略掉。得到类名调用GetClassName，得到窗口过程调用GetClassLong并传入GCL_WNDPROC标志，然后把它们格式化成文本串并放到相关的编辑空间中去。

invoke UninstallHook
invoke SetDlgItemText,hDlg,IDC_HOOK,addr HookText
mov HookFlag,FALSE
invoke SetDlgItemText,hDlg,IDC_CLASSNAME,NULL
invoke SetDlgItemText,hDlg,IDC_HANDLE,NULL
invoke SetDlgItemText,hDlg,IDC_WNDPROC,NULL

当用户按下Unhook后，主程序调用DLL中的UninstallHook函数。该函数调用UnhookWindowsHookEx函数。然后，它把按钮的文本换回“Hook”，HookFlag的值设成FALSE再清除掉编辑控件中的文本。
链接器的开关选项如下：

Link /SECTION:.bss,S /DLL /DEF:$(NAME).def /SUBSYSTEM:WINDOWS

它指定.bss段作为一个共享段以便所有映射该DLL的进程共享未初始化的数据段。如果不用该开关，您DLL中的钩子就不能正常工作了。

翻译：Lxx.阿龙，校对：LuoYunBin's Win32 ASM Page， http://asm.yeah.net/


Iczelion's win32 asm tutorial

第二十五课:位图初步

在这一课,我们将学习怎样在程序中使用位图。 更准确地说,我们要学习的是怎样在一个窗口的客户区中显示位图。

理论

位图就是存贮于电脑中的图片。位图文件有相当多的格式（译者：如.BMP.JPG.GIF.PIC 等）但Windows仅 支持 Windows Bitmap Graphics 格式，即BMP文件。 本课所指的位图也是BMP文件。 使用位图最简单的方法就是把它定义在资源文件（.rc)中。 定义的方法有两种。第一种方法是把它定义为整数宏，具体如下：
#define IDB_MYBITMAP 100
IDB_MYBITMAP BITMAP "c:\project\example.bmp"
第一行我们定义了一个值为100的整数宏。 第二行我们把这个整数宏指向所要定义的位图， 这样，编译器就能知道位图所在的路径。
另一种方法是给它起一个名字，也就是把它定义为字符串，具体如下:
MyBitMap BITMAP "c:\project\example.bmp"
两种方法效果是一样的。 选择哪一种方法，视乎在程序中你喜欢用整数宏还是用字符串来指向位图。
现在我们已经把位图定义在资源文件中，下一步就是把它显示在窗口的客户区上。
  1. 在程序中，我们使用API函数 LoadBitmap 取得位图句柄。 下面是 LoadBitmap 函数的 完型：
      LoadBitmap proto hInstance:HINSTANCE, lpBitmapName:LPSTR

    该函数返回一个位图句柄。函数有两个参数，其中 hInstance 是程序句柄。 lpBitmapName 是位图名字的指针（适用于第二种定义方法）。如果你使用了第一种 定义方法，你可以填入指向位图的值或整数宏 （对应上例这个值就是100，整数宏是IDB_MYBITMAP)。下面是简单的例子：

      第一种方法:

      .386
      .model flat, stdcall
      ................
      .const
      IDB_MYBITMAP equ 100
      ...............
      .data?
      hInstance dd ?
      ..............
      .code
      .............
      invoke GetModuleHandle,NULL
      mov hInstance,eax
      ............
      invoke LoadBitmap,hInstance,IDB_MYBITMAP
      ...........

      第二种方法:

      .386
      .model flat, stdcall
      ................
      .data
      BitmapName db "MyBitMap",0
      ...............
      .data?
      hInstance dd ?
      ..............
      .code
      .............
      invoke GetModuleHandle,NULL
      mov hInstance,eax
      ............
      invoke LoadBitmap,hInstance,addr BitmapName
      ...........

  1. 获得一个设备文本（DC）句柄。你可以在响应WM_PAINT消息时通过API函数BeginPaint获得。 如果在其它消息中则可以用API函数GetDC获得。
  2. 创建这个DC的内存映像。这样做的目的是建立一张“隐藏的画纸”，把位图 “画”在上面，作缓冲之用。完成这项工作后，我们就通过一个函数把“画纸”上的位图复制 到真正的DC中。这就是在屏幕上快速显示图象的双缓冲技术。（译者：可以减少图象抖动） 这张“画纸”用API函数 CreateCompatibleDC 建立，下面是它的完型：
      CreateCompatibleDC proto hdc:HDC

    如果函数执行成功，将返回DC内存映像也即“画纸”的句柄。

  1. 现在我们已经有了“画纸”，可以把位图画在上面了。这可以通过API函数 SelectObject 完成， 其中第一个参数是“画纸”的句柄，第二个参数则是位图的句柄，下面是函数的完型:
      SelectObject proto hdc:HDC, hGdiObject:DWORD
  1. 现在位图已经画在“画纸”上了。下一步我们要把位图复制到真正的DC中。 有很多API函数都能完成这项工作，例如 BitBlt 和 StretchBlt。 函数 BitBlt 仅仅将一个DC的内容简单地复制到另一个DC中，而函数 StretchBlt 则能够自动调整源DC复制内容的大小已适应目的DC的输出区域大小，因此前者比后者速度更快。 在这里我们只使用函数 BitBlt ，下面是它的完型：
      BitBlt proto hdcDest:DWORD, nxDest:DWORD, nyDest:DWORD, nWidth:DWORD, nHeight:DWORD, hdcSrc:DWORD, nxSrc:DWORD, nySrc:DWORD, dwROP:DWORD
    hdcDest 目的DC的句柄。
    nxDest, nyDest 目的DC输出区域的左上角坐标。
    nWidth, nHeight 目的DC输出区域的长和宽。
    hdcSrc 源DC的句柄。
    nxSrc, nySrc 源DC中所要复制区域的左上角坐标。
    dwROP 屏面运算码（ROP）。该参数用以确定复制内容的颜色与输出区域原来的颜色按哪种运算 方式处理。通常，只需要简单地用复制内容把输出区域覆盖掉。
  1. 一切办妥后，就用API函数 DeleteObject 释放位图对象，也就是把位图“抹掉”。
大功告成! 现在再来回顾一下整个过程：首先，你需要把位图定义在资源文件中。 然后，你需要在程序中载入位图资源，并取得位图句柄。随后，你需要获得位图输出区域的DC， 以及创建这个DC的内存映像，并把位图放进这个DC内存映像中。最后把位图从DC内存映像复制到 真正的DC中。

例子:

.386
.model flat,stdcall
option casemap:none
include \masm32\include\windows.inc
include \masm32\include\user32.inc
include \masm32\include\kernel32.inc
include \masm32\include\gdi32.inc
includelib \masm32\lib\user32.lib
includelib \masm32\lib\kernel32.lib
includelib \masm32\lib\gdi32.lib

WinMain proto :DWORD,:DWORD,:DWORD,:DWORD
IDB_MAIN equ 1

.data
ClassName db "SimpleWin32ASMBitmapClass",0
AppName db "Win32ASM Simple Bitmap Example",0

.data?
hInstance HINSTANCE ?
CommandLine LPSTR ?
hBitmap dd ?

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

WinMain proc hInst:HINSTANCE,hPrevInst:HINSTANCE,CmdLine:LPSTR,CmdShow:DWORD
LOCAL wc:WNDCLASSEX
LOCAL msg:MSG
LOCAL hwnd:HWND
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 wc
INVOKE CreateWindowEx,NULL,ADDR ClassName,ADDR AppName,\
WS_OVERLAPPEDWINDOW,CW_USEDEFAULT,\
CW_USEDEFAULT,CW_USEDEFAULT,CW_USEDEFAULT,NULL,NULL,\
hInst,NULL
mov hwnd,eax
invoke ShowWindow, hwnd,SW_SHOWNORMAL
invoke UpdateWindow, hwnd
.while TRUE
invoke GetMessage, ADDR msg,NULL,0,0
.break .if (!eax)
invoke TranslateMessage, ADDR msg
invoke DispatchMessage, ADDR msg
.endw
mov eax,msg.wParam
ret
WinMain endp

WndProc proc hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM
LOCAL ps:PAINTSTRUCT
LOCAL hdc:HDC
LOCAL hMemDC:HDC
LOCAL rect:RECT
.if uMsg==WM_CREATE
invoke LoadBitmap,hInstance,IDB_MAIN
mov hBitmap,eax
.elseif uMsg==WM_PAINT
invoke BeginPaint,hWnd,addr ps
mov hdc,eax
invoke CreateCompatibleDC,hdc
mov hMemDC,eax
invoke SelectObject,hMemDC,hBitmap
invoke GetClientRect,hWnd,addr rect
invoke BitBlt,hdc,0,0,rect.right,rect.bottom,hMemDC,0,0,SRCCOPY
invoke DeleteDC,hMemDC
invoke EndPaint,hWnd,addr ps
.elseif uMsg==WM_DESTROY
invoke DeleteObject,hBitmap
invoke PostQuitMessage,NULL
.ELSE
invoke DefWindowProc,hWnd,uMsg,wParam,lParam
ret
.ENDIF
xor eax,eax
ret
WndProc endp
end start

;---------------------------------------------------------------------
; 资源定义
;---------------------------------------------------------------------
#define IDB_MAIN 1
IDB_MAIN BITMAP "tweety78.bmp"

分析:

#define IDB_MAIN 1
IDB_MAIN BITMAP "tweety78.bmp"
定义整数宏IDB_MAIN的值为1，然后把它指向一个与资源文件处于相同目录文件名为“tweety.bmp"的位图。

.if uMsg==WM_CREATE
invoke LoadBitmap,hInstance,IDB_MAIN
mov hBitmap,eax

在处理 WM_CREATE 消息时, 我们通过API函数 LoadBitmap 载入位图资源，并通过函数 返回值取得位图句柄。
然后，我们就可以把位图画在窗口客户区上。

.elseif uMsg==WM_PAINT
invoke BeginPaint,hWnd,addr ps
mov hdc,eax
invoke CreateCompatibleDC,hdc
mov hMemDC,eax
invoke SelectObject,hMemDC,hBitmap
invoke GetClientRect,hWnd,addr rect
invoke BitBlt,hdc,0,0,rect.right,rect.bottom,hMemDC,0,0,SRCCOPY
invoke DeleteDC,hMemDC
invoke EndPaint,hWnd,addr ps

在本例中，我们选用在响应WM_PAINT消息时画出位图。首先我们通过API函数l BeginPaint 获得窗口客户区的DC句柄。 接着我们通过API函数 CreateCompatibleDC 创建该DC 的内存映像，并通过API函数 SelectObject 把位图放进内存映像中。下一步，我们通过API函数 GetClientRect 取得窗口客户区的大小。最后，我们通过API函数 BitBlt 把位图从DC内存映像复制到真正的客户区DC中。 完成显示工作后，我们通过API函数 DeleteDC 释放DC内存映像，并用API函数 EndPaint 释放客户区DC， 结束画图工作。

.elseif uMsg==WM_DESTROY
invoke DeleteObject,hBitmap
invoke PostQuitMessage,NULL
当我们不再需要位图时，通过API函数 DeleteObject 把它释放。
翻译：赵昆 Welcom to http://asm.yeah.net


Iczelion's win32 asm tutorial

第26章: 启动画面

上一章我们学习了位图的使用.在这一章我们要用 上帝赋予我们的创造力来融会贯通上一章我们学到的知识.那就是研究如何用位图来创建启动画面. 你可以在这里下载示范: the example.

理论

首先,我们先要搞清楚什么是启动画面.举个简单的例子:我们启动某些作 的专业一点的程序时(比如Netscape,Adobe Acrobat等)会先跳出一个启动画面.上面通常有一 些版权信息,版本号等.与此同时,程序后台正做着一些程序的加载或初始化工作.这个启动画面有别于 一般的窗口.它没有标题栏,没有系统菜单,也没有边框.只有一张位图在屏幕上显示一会儿,然后消失 .在这一章我们来试试自己做一个.
第一步你可能会想到把要显示的位图包含到资源文件中去.但是这样做 有一个缺点.你的程序只在启动的时候显示这张位图,可是它却至始至终存在于你的内存中,直到你 把程序关掉.这不能不说是对内存的极大浪费.好办法是:创建一个"资源"DLL(动态连接库)来包含位图 和它独特的显示代码.这样,你就可以在想显示启动画面的时候加载他,用完了就卸载它.所以,我们的 程序需要2个模块:主程序和启动画面DLL.我们要把位图放到这个DLL的资源中去.
基本步骤如下:
  1. 把位图作为一个位图资源放到DLL中去.
  2. 主程序调用 LoadLibrary 把 dll 加载到内存中去.
  3. DLL 被调用后,它会建立一个定时器用于管理启动画面显示的时间.然后,注册并创建一个没有 标题和边框的窗口.同时在窗口的客户区显示位图.
  4. 等启动画面的显示时间长度到达你预先的设定值,启动画面消失,控制权回到主程序手中.
  5. 主程序调用 FreeLibrary 从内存中卸载 DLL .然后,做它该做的事去.
下面我们来研究细节部分

加载/卸载 DLL

你可以用 LoadLibrary 灵活的加载一个 DLL ,它的格式如下:
LoadLibrary proto lpDLLName:DWORD
它只有一个参数: 你想要加载的 DLL 的名称所在的地址.调用成功返回指向该DLL模块的句柄,反之返回NULL.
要卸载 DLL, 则调用 FreeLibrary:
FreeLibrary proto hLib:DWORD
它也只有一个参数: 你想要卸载的 DLL 模块的句柄(通常就是上面那个函数返回的啦).

怎样使用定时器

首先, 你要用 SetTimer 创建一个定时器:
SetTimer proto hWnd:DWORD, TimerID:DWORD, uElapse:DWORD, lpTimerFunc:DWORD

hWnd接受这个定时器消息的窗口的句柄.如果,你的定时器不需要窗口接受它的消息,你也可以 用NULL作为参数
TimerID定时器的 ID 值. 由你自己定义.
uElapse 定时器定的时间.以ms(千分之一秒)为单位.
lpTimerFunc 处理该定时器消息的函数所在的地址.如果你用NULL作为该参数,那么定时器的消息会被送给 hWnd 参数所指定的窗口.

SetTimer 如果成功则返回定时器的 ID 否则返回 NULL. 所以最好不要把定时器的ID设为0(Pheadnius:NULL代表0, 记得吗?).

你可以用2种方法创建定时器:
  • 如果你有一个窗口并且定时器把消息传给这个窗口.那么你需要把所有的4个参数都传送给 Settimer 函数 (lpTimerFunc参数必须为NULL).
  • 如果你没有窗口或者你不想让窗口处理定时器的消息,那么你必须在窗口句柄中传送一个NULL.同时你要指定 用于处理定时器消息的函数的地址.
在这个例子中我们要使用第一种方法.
当你设定的时间到了, 与定时器相连的窗口会收到 WM_TIMER 消息.例如,你指定 uElapse 的值为 1000, 你的窗口每 过一秒都会收到 WM_TIMER 消息.
等到你再也不需要这个定时器了,就用 KillTimer 来去除定时器.
KillTimer proto hWnd:DWORD, TimerID:DWORD

例子:

;-----------------------------------------------------------------------
; 主程序
;-----------------------------------------------------------------------
.386
.model flat,stdcall
option casemap:none
include \masm32\include\windows.inc
include \masm32\include\user32.inc
include \masm32\include\kernel32.inc
includelib \masm32\lib\user32.lib
includelib \masm32\lib\kernel32.lib

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

.data
ClassName db "SplashDemoWinClass",0
AppName db "Splash Screen Example",0
Libname db "splash.dll",0

.data?
hInstance HINSTANCE ?
CommandLine LPSTR ?
.code
start:
invoke LoadLibrary,addr Libname
.if eax!=NULL
invoke FreeLibrary,eax
.endif
invoke GetModuleHandle, NULL
mov hInstance,eax
invoke GetCommandLine
mov CommandLine,eax
invoke WinMain, hInstance,NULL,CommandLine, SW_SHOWDEFAULT
invoke ExitProcess,eax

WinMain proc hInst:HINSTANCE,hPrevInst:HINSTANCE,CmdLine:LPSTR,CmdShow:DWORD
LOCAL wc:WNDCLASSEX
LOCAL msg:MSG
LOCAL hwnd:HWND
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 wc
INVOKE CreateWindowEx,NULL,ADDR ClassName,ADDR AppName,\
WS_OVERLAPPEDWINDOW,CW_USEDEFAULT,\
CW_USEDEFAULT,CW_USEDEFAULT,CW_USEDEFAULT,NULL,NULL,\
hInst,NULL
mov hwnd,eax
invoke ShowWindow, hwnd,SW_SHOWNORMAL
invoke UpdateWindow, hwnd
.while TRUE
invoke GetMessage, ADDR msg,NULL,0,0
.break .if (!eax)
invoke TranslateMessage, ADDR msg
invoke DispatchMessage, ADDR msg
.endw
mov eax,msg.wParam
ret
WinMain endp

WndProc proc hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM
.IF uMsg==WM_DESTROY
invoke PostQuitMessage,NULL
.ELSE
invoke DefWindowProc,hWnd,uMsg,wParam,lParam
ret
.ENDIF
xor eax,eax
ret
WndProc endp
end start

;--------------------------------------------------------------------
; 位图 DLL
;--------------------------------------------------------------------
.386
.model flat, stdcall
include \masm32\include\windows.inc
include \masm32\include\user32.inc
include \masm32\include\kernel32.inc
include \masm32\include\gdi32.inc
includelib \masm32\lib\user32.lib
includelib \masm32\lib\kernel32.lib
includelib \masm32\lib\gdi32.lib
.data
BitmapName db "MySplashBMP",0
ClassName db "SplashWndClass",0
hBitMap dd 0
TimerID dd 0

.data
hInstance dd ?

.code

DllEntry proc hInst:DWORD, reason:DWORD, reserved1:DWORD
.if reason==DLL_PROCESS_ATTACH ; When the dll is loaded
push hInst
pop hInstance
call ShowBitMap
.endif
mov eax,TRUE
ret
DllEntry Endp
ShowBitMap proc
LOCAL wc:WNDCLASSEX
LOCAL msg:MSG
LOCAL hwnd:HWND
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,0
invoke LoadCursor,NULL,IDC_ARROW
mov wc.hCursor,eax
invoke RegisterClassEx, addr wc
INVOKE CreateWindowEx,NULL,ADDR ClassName,NULL,\
WS_POPUP,CW_USEDEFAULT,\
CW_USEDEFAULT,250,250,NULL,NULL,\
hInstance,NULL
mov hwnd,eax
INVOKE ShowWindow, hwnd,SW_SHOWNORMAL
.WHILE TRUE
INVOKE GetMessage, ADDR msg,NULL,0,0
.BREAK .IF (!eax)
INVOKE TranslateMessage, ADDR msg
INVOKE DispatchMessage, ADDR msg
.ENDW
mov eax,msg.wParam
ret
ShowBitMap endp
WndProc proc hWnd:DWORD,uMsg:DWORD,wParam:DWORD,lParam:DWORD
LOCAL ps:PAINTSTRUCT
LOCAL hdc:HDC
LOCAL hMemoryDC:HDC
LOCAL hOldBmp:DWORD
LOCAL bitmap:BITMAP
LOCAL DlgHeight:DWORD
LOCAL DlgWidth:DWORD
LOCAL DlgRect:RECT
LOCAL DesktopRect:RECT

.if uMsg==WM_DESTROY
.if hBitMap!=0
invoke DeleteObject,hBitMap
.endif
invoke PostQuitMessage,NULL
.elseif uMsg==WM_CREATE
invoke GetWindowRect,hWnd,addr DlgRect
invoke GetDesktopWindow
mov ecx,eax
invoke GetWindowRect,ecx,addr DesktopRect
push 0
mov eax,DlgRect.bottom
sub eax,DlgRect.top
mov DlgHeight,eax
push eax
mov eax,DlgRect.right
sub eax,DlgRect.left
mov DlgWidth,eax
push eax
mov eax,DesktopRect.bottom
sub eax,DlgHeight
shr eax,1
push eax
mov eax,DesktopRect.right
sub eax,DlgWidth
shr eax,1
push eax
push hWnd
call MoveWindow
invoke LoadBitmap,hInstance,addr BitmapName
mov hBitMap,eax
invoke SetTimer,hWnd,1,2000,NULL
mov TimerID,eax
.elseif uMsg==WM_TIMER
invoke SendMessage,hWnd,WM_LBUTTONDOWN,NULL,NULL
invoke KillTimer,hWnd,TimerID
.elseif uMsg==WM_PAINT
invoke BeginPaint,hWnd,addr ps
mov hdc,eax
invoke CreateCompatibleDC,hdc
mov hMemoryDC,eax
invoke SelectObject,eax,hBitMap
mov hOldBmp,eax
invoke GetObject,hBitMap,sizeof BITMAP,addr bitmap
invoke StretchBlt,hdc,0,0,250,250,\
hMemoryDC,0,0,bitmap.bmWidth,bitmap.bmHeight,SRCCOPY
invoke SelectObject,hMemoryDC,hOldBmp
invoke DeleteDC,hMemoryDC
invoke EndPaint,hWnd,addr ps
.elseif uMsg==WM_LBUTTONDOWN
invoke DestroyWindow,hWnd
.else
invoke DefWindowProc,hWnd,uMsg,wParam,lParam
ret
.endif
xor eax,eax
ret
WndProc endp

End DllEntry

分析:

我们首先要再主程序中检验这段代码.
invoke LoadLibrary,addr Libname
.if eax!=NULL
invoke FreeLibrary,eax
.endif
我们调用 LoadLibrary 读入名称为 "splash.dll" 的 DLL. 然后, 用 FreeLibrary 卸载. 一直到 DLL 完成初始化, LoadLibrary才会返回.
主程序的任务到此为止. 更有趣的部分再 DLL里.

.if reason==DLL_PROCESS_ATTACH ; When the dll is loaded
push hInst
pop hInstance
call ShowBitMap

DLL 被加载后, Windows 调用它的有 DLL_PROCESS_ATTACH 标记的入口函数. 我们借这个机会显示启动画面. 首先,我们 保存 DLL 事例的句柄以供将来使用. 然后, 调用一个叫 ShowBitMap 的函数进行真正的工作. ShowBitMap 注册一个窗口, 创建这个窗口和显示它.就像我们以前创建窗口一样. 有趣的是这个 CreateWindowEx 调用:

INVOKE CreateWindowEx,NULL,ADDR ClassName,NULL,\
WS_POPUP,CW_USEDEFAULT,\
CW_USEDEFAULT,250,250,NULL,NULL,\
hInstance,NULL

注意, 这里的窗口风格仅仅使用了 WS_POPUP . 所以窗口即没有标题栏,也没有边界. 我们同时也限定窗口的宽高为 250x250个像素.
现在窗口创建好了. 在 WM_CREATE 的消息处理代码里我们把这个窗口移到屏幕的中央.代码如下:

invoke GetWindowRect,hWnd,addr DlgRect
invoke GetDesktopWindow
mov ecx,eax
invoke GetWindowRect,ecx,addr DesktopRect
push 0
mov eax,DlgRect.bottom
sub eax,DlgRect.top
mov DlgHeight,eax
push eax
mov eax,DlgRect.right
sub eax,DlgRect.left
mov DlgWidth,eax
push eax
mov eax,DesktopRect.bottom
sub eax,DlgHeight
shr eax,1
push eax
mov eax,DesktopRect.right
sub eax,DlgWidth
shr eax,1
push eax
push hWnd
call MoveWindow

它先找到桌面和窗口的大小. 然后,计算出一个窗口左上角的坐标. 使这个窗口能位于屏幕中央.

invoke LoadBitmap,hInstance,addr BitmapName
mov hBitMap,eax
invoke SetTimer,hWnd,1,2000,NULL
mov TimerID,eax

下一步,它用 LoadBitmap 从资源中读入位图并且创建一个定时器.定时器的 ID 为 1 时间间隔为 2 秒. 定时器 将每 2 秒 向窗口发送 WM_TIMER 消息.

.elseif uMsg==WM_PAINT
invoke BeginPaint,hWnd,addr ps
mov hdc,eax
invoke CreateCompatibleDC,hdc
mov hMemoryDC,eax
invoke SelectObject,eax,hBitMap
mov hOldBmp,eax
invoke GetObject,hBitMap,sizeof BITMAP,addr bitmap
invoke StretchBlt,hdc,0,0,250,250,\
hMemoryDC,0,0,bitmap.bmWidth,bitmap.bmHeight,SRCCOPY
invoke SelectObject,hMemoryDC,hOldBmp
invoke DeleteDC,hMemoryDC
invoke EndPaint,hWnd,addr ps

当窗口收到 WM_PAINT 消息, 它创建一个内存DC(Pheadnius:还记得DC吗. 在win32编程中你会经常遇到DC这个词. 它是 Device Context 的缩写, 官方译为"设备描述表". 如果你研究过vc, 你应该对它不陌生. 不过如果你不明白它是什么 也不要紧. 你可以把它看作一个句柄. 就是某个设备或某块内存的名称.),然后把位图选进内存DC. 再用 GetObject 函数获得 位图的尺寸, 然后用 StretchBlt 把位图显示在窗口上. StretchBlt的作用和 BitBlt 一样,但它可以拉伸或压缩位图到我们 希望的大小. 在这里我们希望位图能适合窗口的大小,所以我们 StretchBlt 代替 BitBlt. 之后我们删除内存DC.

.elseif uMsg==WM_LBUTTONDOWN
invoke DestroyWindow,hWnd

如果你的程序的使用者每次都要看到启动画面消失才能用, 他们一定会厌烦. 我们可以为用户提供多一种选择. 当他单击启动画面, 它就会消失. 这就是为什么我们要在DLL里处理 WM_LBUTTONDOWN 消息. 收到这个消息后立即就用 DestroyWindow 关掉窗口.

.elseif uMsg==WM_TIMER
invoke SendMessage,hWnd,WM_LBUTTONDOWN,NULL,NULL
invoke KillTimer,hWnd,TimerID

如果用户选择等待, 那么启动画面会在定时器到了指定的时间后消失. (在本例中, 是 2 秒). 我们可以通过处理 WM_TIMER 消息达到这一目的. 在收到这一消息后,我们可以对窗口传送 WM_LBUTTONDOWN 消息来关掉窗口. 这是为了避免 代码重复. 现在, 我们不再需要这个定时器了,所以我们用 KillTimer 删除它.
窗口关闭后,DLL 把控制权还给主程序.

这篇文章来自于 Iczelion 的 asm 网页,本章由Pheadnius为大家翻译. Welcom to http://asm.yeah.net


Iczelion 的 Win32asm 教程

第27课 工具提示控件

我们将学习工具提示控件:它是什么如何创建和使用.下载例子

理论:

工具提示是当鼠标在某特定区域上停留时显示的一个矩形窗口.工具提示窗口包含一些编程者想要显示的文本.在这点上,工具提示同状态栏的作用是一样的,所不同的是工具提示当单击或者远离指定区域的时候就会消逝,你可能熟悉与工具栏相关联的工具提示,那些"提示"是工具栏控件提供的便利.如果你想要在其它窗口、控件中显示工具提示的话,就不得不自己创建他们.

既然已经了解了什么是工具提示,就让我们来看看如何创建他们.大致步骤如下:

  1. 用CreateWindowEx函数创建工具提示控件.
  2. 定义一个工具提示控件将要监视鼠标移动的区域.
  3. 将区域传递给工具提示控件
  4. 将传递区域的鼠标消息转送给工具提示控件.(这步或许更早,具体依据转播消息的方法)
下面我们就来详细的讨论每一步.

工具提示控件的创建

工具提示控件是一种通用控件.同样,要在源代码某处调用InitCommonControls以便MASM能够将你的程序和comctl32.dll连接. 用CreateWindowEx创建工具提示控件,典型代码如下:
.data
TooltipClassName db "Tooltips_class32",0
.code
.....
invoke InitCommonControls
invoke CreateWindowEx, NULL, addr TooltipClassName, NULL, TIS_ALWAYSTIP, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, NULL, NULL, hInstance, NULL
注意窗口风格:TIS_ALWAYSTIP指定了工具提示不管包含指定区域的窗口状态如何,当鼠标移过指定区域的时候,工具提示总是显示.简单的说就是,即使窗口处于非激活状态,鼠标移过工具提示指定区域的时候,工具提示也会出现.
你不必在CreateWindowEx中包括WS_POPUP 和 WS_EX_TOOLWINDOW风格,因为工具提示处理过程会自动加上,你也不必指定工具提示窗口的坐标和宽高,控件会依据要显示的文字自动调节.四个参数,均使用CW_USEDEFAULT ,其余的参数都不太重要.

指定工具

工具提示控件创建了但还没有显示,我们想要当鼠标指针在某个区域之上时显示工具提示窗口.现在需要指定这个区域.我们称这样的区域为"工具",“工具”就是工具提示控件监视鼠标指针是否移过的位于窗口客户区的一个方形区域.如果鼠标指针移过"工具",工具提示窗口就显示."工具"可覆盖整个客户区或者仅仅是它的一部分.因此我们把"工具"分成两种类型,一种是作为一个窗口,另一种则是某窗口客户区的一部分.两种各有所用.覆盖整个客户区的"工具"通常用于按钮、编辑控件等,你不必指定焦点域的坐标和大小:它被假定为窗口的整个客户区.仅覆盖窗口客户区一部分的"工具"在你想把窗口客户区分成几个部分但又不想使用子窗口时特别有用,但需要指定左上角的坐标和宽高.

使用如下的 TOOLINFO 结构定义"工具":

TOOLINFO STRUCT
cbSize DWORD ?
uFlags DWORD ?
hWnd DWORD ?
uId DWORD ?
rect RECT <>
hInst DWORD ?
lpszText DWORD ?
lParam LPARAM ?
TOOLINFO ENDS
域名 | 说明
cbSize | TOOLINFO结构的大小.必须填充, 如果这个区域不被正确填充Windows并不会报错,但你会得到不可预料的奇怪结果.
uFlags 指定焦点域的属性,可以是如下标志的联合:
  • TTF_IDISHWND "ID is hWnd".如果你指定了这个标志,就意味着你要使用覆盖整个客户区的"工具" (上面第一种"工具"). 如果你使用了这个标志,你必须用你要使用的窗口句柄填充uId成员,如果你不指定这个成员,就意味着你要使用第二种"工具"、客户区窗口的一方形区域.在这种情况下,你就必须以方形区域的大小填充rect成员.
  • TTF_CENTERTIP 通常工具提示窗口显示在鼠标的右下方,如果你指定了这个标志,不管鼠标的位置如何,工具提示总显示在焦点域总的中下方.
  • TTF_RTLREADING .如果你的程序不是为阿拉伯或者希伯来语系统设计的,你完全可以不理它,它使得提示文本以从右至左的顺序显示,在其它系统中无效.
  • TTF_SUBCLASS 如果你使用了这个标志,工具提示控件将子类化"工具"所在窗口以便截取发送给它的的鼠标消息,这个标志非常有用,否则你将不得不做更多的工作来向工具提示控件转发消息.
hWnd 包含"工具"的窗口句柄,如果你指定了TTF_IDISHWND标志,Windows将忽略该值,而使用uId成员的值作为窗口句柄.你需要填充这个域域如果:
  • 你不使用 TTF_IDISHWND标志 (换句话说,你使用局部"工具")
  • 你在 lpszText 成员中指定了LPSTR_TEXTCALLBACK .这个值告诉工具提示控件当需要显示提示窗口时,必须向包含"工具"的窗口查询应该显示什么. 这是一种实时的控件文本更新.如果你需要动态改变提示文本,你应当在 lpszText成员中指定LPSTR_TEXTCALLBACK值,控件就会向hWnd指定的窗口发送TTN_NEEDTEXT 消息.
uId 这个域的值可能有两种含义,依 uFlags 是否包含TTF_IDISHWND.
  • 如果TTF_IDISHWND标志没有被指定就代表应用程序定义的"工具"ID,由于这意味着你使用仅覆盖客户区一部分的"工具",逻辑的推出一个客户区可能存在多个同样的焦点域(不存在交迭),Hwnd成员的一个窗口句柄就不够了,应用程序定义ID以区分他们因此而显得必要,只要唯一ID可以是任何值.
  • 如果TTF_IDISHWND标志被指定就表示整个客户区都作为焦点域的窗口句柄,你或许会奇怪为什么不用上面提到的hWnd成员的值来储存窗口句柄.答案是:如果lpszText指定为LPSTR_TEXTCALLBACK,Hwnd 可能已经被填充了.还有提供提示文本的窗口和包含"工具"的窗口可能不是同一个!(你可以设计一个提供两种服务的窗口程序,但太严格了,在这点上,微软为我们提供了更大的自由,欢呼吧!)
rect | 指定"工具"大小的rect结构.这个结构定义了一个以hWnd指定窗口客户区左上角为基点的方形大小,简言之,如果你想指定客户区的一部分作为"工具"就得填充这个结构,如果你指定了TTF_IDISHWND标志 ,控件就会忽略这个值.(你已经选择整个客户区作为"工具")
hInst | 如果lpszText指定了字符串资源的标识,包含将作为工具提文本字符串资源的实例句柄.听起来有点费解,阅读一下lpszText的说明就可以明白这个域是干什么用的了.若lpszText不包含字符串资源标识,控件会忽略这个域.
lpszText 这个域可以有如下几个值:
  • 如果指定为LPSTR_TEXTCALLBACK, 工具提示控件就会向HWnd窗口发送TTN_NEEDTEXT消息以获得将要显示的字符串.提示文本的动态更新方法:每次显示提示窗口都改变提示文本.
  • 如果在这个域中指定字符串资源标识,当控件要在提示窗口中显示提示文本时,就搜索hInst成员标识的实例的字符串资源列表.由于字符串资源列表标识总是16位值,这个域的高字节将永远为0,这种方法在你想移植程序时非常有用,由于字符串资源以脚本形式定义,你不必修改源代码.只需要修改字符串列表提示文本就会相应改变,而不必担心引进bugs.
  • 如果这个域的值不是LPSTR_TEXTCALLBACK并且高字节不为零, 控件截取这个值作为提示文本的指针,这是最简单的方法但也最不稳定.通过检查高字节区分字符串资源标识.

总言之,你需要将TOOLINFO结构传递给工具提示控件之前填充填充好,它描述了你期望的"工具"属性.

向工具提示控件注册"工具"

填充完TOOLINFO结构后, 必须将其传递给控件 . 一个工具提示控件可以控制很多"工具",因此不必为一个窗口创建很多控件,为了注册"工具",向控件发送TTM_ADDTOOL消息 wParam不使用,lParam必须包含要注册的TOOLINFO结构的指针
.data?
ti TOOLINFO <>
.......
.code
.......
<fill the TOOLINFO structure>
.......
invoke SendMessage, hwndTooltip, TTM_ADDTOOL, NULL, addr ti
成功返回 TRUE,否则返回 FALSE.
发送 TTM_DELTOOL消息取消注册.

向工具提示控件转发鼠标消息

以上步骤完毕之后,控件知道了应当监视那一块区域和应该在提示窗口显示什么.唯一缺乏的就是激发机制. 想想看:"工具"指定的其它窗口的客户区的区域.控件如何截取发送向该窗口的消息呢?实际中需要截取消息以便了解鼠标停留了多长时间,当指定时间流逝以后,控件显示提示窗口.有两种方法: 一种需要包含"工具"窗口的合作,另一种则不需要.
  • 包含"工具"的窗口必须向控件发送 TTM_RELAYEVENT 以转发消息. lParam是指向要转发消息MSG的指针 控件仅处理如下鼠标消息 :
    • WM_LBUTTONDOWN
    • WM_MOUSEMOVE
    • WM_LBUTTONUP
    • WM_RBUTTONDOWN
    • WM_MBUTTONDOWN
    • WM_RBUTTONUP
    • WM_MBUTTONUP
    其它消息全被忽略,因此包含"工具"的窗口的处理过程必须包含像这样的选择:

    WndProc proc hWnd:DWORD, uMsg:DWORD, wParam:DWORD, lParam:DWORD
    .......
    if uMsg==WM_CREATE
    .............
    elseif uMsg==WM_LBUTTONDOWN || uMsg==WM_MOUSEMOVE || uMsg==WM_LBUTTONUP || uMsg==WM_RBUTTONDOWN || uMsg==WM_MBUTTONDOWN || uMsg==WM_RBUTTONUP || uMsg==WM_MBUTTONUP
    invoke SendMessage, hwndTooltip, TTM_RELAYEVENT, NULL, addr msg
    ..........

  • .你可以在TOOLINFO结构的uFlags成员指定 TTF_SUBCLASS标志。此标志告诉控件子类化包含"工具"的窗口以便无需窗口的协作便可捕获鼠标消息。由于除了控件自己处理截获的鼠标消息和指定TTF_SUBCLASS标志之外不用编写多余的代码，因此很易于使用。
就是这些了,到这步为止,控件已经全功能了.还有几个你应当知道的相关消息.
  • TTM_ACTIVATE.如果你想动态的允许或者禁止工具提示控件,这个小消息就是为你而备.wParam值为TRUE,允许控件.若为FALSE,禁止控件.控件初始创建的时候无需发送消息激活他,便被自动设为允许状态.
  • TTM_GETTOOLINFO and TTM_SETTOOLINFO. 如果你想在把TOOLINFO结构传递给控件之后获得或者改变其值,使用此消息.你需要用正确的uId and hWnd值指定要改变的"工具".如果你只想改变rect成员的值,使用TTM_NEWTOOLRECT 消息,如果仅想改变提示文本,使用TTM_UPDATETIPTEXT消息.
  • TTM_SETDELAYTIME. 使用此消息指定控件显示提示文本时的时间延迟.

例子:

例子是一个有两个按钮的对话框,对话框的客户区分为4部分:左上、右上、左下、右下.每个区域都指定为有自己提示文本的"工具",两个按钮也有自己的提示文本.

.386
.model flat,stdcall
option casemap:none
include \masm32\include\windows.inc
include \masm32\include\kernel32.inc
include \masm32\include\user32.inc
include \masm32\include\comctl32.inc
includelib \masm32\lib\comctl32.lib
includelib \masm32\lib\user32.lib
includelib \masm32\lib\kernel32.lib
DlgProc proto :DWORD,:DWORD,:DWORD,:DWORD
EnumChild proto :DWORD,:DWORD
SetDlgToolArea proto :DWORD,:DWORD,:DWORD,:DWORD,:DWORD
.const
IDD_MAINDIALOG equ 101
.data
ToolTipsClassName db "Tooltips_class32",0
MainDialogText1 db "This is the upper left area of the dialog",0
MainDialogText2 db "This is the upper right area of the dialog",0
MainDialogText3 db "This is the lower left area of the dialog",0
MainDialogText4 db "This is the lower right area of the dialog",0
.data?
hwndTool dd ?
hInstance dd ?
.code
start:
invoke GetModuleHandle,NULL
mov hInstance,eax
invoke DialogBoxParam,hInstance,IDD_MAINDIALOG,NULL,addr DlgProc,NULL
invoke ExitProcess,eax

DlgProc proc hDlg:DWORD,uMsg:DWORD,wParam:DWORD,lParam:DWORD
LOCAL ti:TOOLINFO
LOCAL id:DWORD
LOCAL rect:RECT
.if uMsg==WM_INITDIALOG
invoke InitCommonControls
invoke CreateWindowEx,NULL,ADDR ToolTipsClassName,NULL,\
TTS_ALWAYSTIP,CW_USEDEFAULT,\
CW_USEDEFAULT,CW_USEDEFAULT,CW_USEDEFAULT,NULL,NULL,\
hInstance,NULL
mov hwndTool,eax
mov id,0
mov ti.cbSize,sizeof TOOLINFO
mov ti.uFlags,TTF_SUBCLASS
push hDlg
pop ti.hWnd
invoke GetWindowRect,hDlg,addr rect
invoke SetDlgToolArea,hDlg,addr ti,addr MainDialogText1,id,addr rect
inc id
invoke SetDlgToolArea,hDlg,addr ti,addr MainDialogText2,id,addr rect
inc id
invoke SetDlgToolArea,hDlg,addr ti,addr MainDialogText3,id,addr rect
inc id
invoke SetDlgToolArea,hDlg,addr ti,addr MainDialogText4,id,addr rect
invoke EnumChildWindows,hDlg,addr EnumChild,addr ti

.elseif uMsg==WM_CLOSE
invoke EndDialog,hDlg,NULL
.else
mov eax,FALSE
ret
.endif
mov eax,TRUE
ret
DlgProc endp

EnumChild proc uses edi hwndChild:DWORD,lParam:DWORD
LOCAL buffer[256]:BYTE
mov edi,lParam
assume edi:ptr TOOLINFO
push hwndChild
pop [edi].uId
or [edi].uFlags,TTF_IDISHWND
invoke GetWindowText,hwndChild,addr buffer,255
lea eax,buffer
mov [edi].lpszText,eax
invoke SendMessage,hwndTool,TTM_ADDTOOL,NULL,edi
assume edi:nothing
ret
EnumChild endp

SetDlgToolArea proc uses edi esi hDlg:DWORD,lpti:DWORD,lpText:DWORD,id:DWORD,lprect:DWORD
mov edi,lpti
mov esi,lprect
assume esi:ptr RECT
assume edi:ptr TOOLINFO
.if id==0
mov [edi].rect.left,0
mov [edi].rect.top,0
mov eax,[esi].right
sub eax,[esi].left
shr eax,1
mov [edi].rect.right,eax
mov eax,[esi].bottom
sub eax,[esi].top
shr eax,1
mov [edi].rect.bottom,eax
.elseif id==1
mov eax,[esi].right
sub eax,[esi].left
shr eax,1
inc eax
mov [edi].rect.left,eax
mov [edi].rect.top,0
mov eax,[esi].right
sub eax,[esi].left
mov [edi].rect.right,eax
mov eax,[esi].bottom
sub eax,[esi].top
mov [edi].rect.bottom,eax
.elseif id==2
mov [edi].rect.left,0
mov eax,[esi].bottom
sub eax,[esi].top
shr eax,1
inc eax
mov [edi].rect.top,eax
mov eax,[esi].right
sub eax,[esi].left
shr eax,1
mov [edi].rect.right,eax
mov eax,[esi].bottom
sub eax,[esi].top
mov [edi].rect.bottom,eax
.else
mov eax,[esi].right
sub eax,[esi].left
shr eax,1
inc eax
mov [edi].rect.left,eax
mov eax,[esi].bottom
sub eax,[esi].top
shr eax,1
inc eax
mov [edi].rect.top,eax
mov eax,[esi].right
sub eax,[esi].left
mov [edi].rect.right,eax
mov eax,[esi].bottom
sub eax,[esi].top
mov [edi].rect.bottom,eax
.endif
push lpText
pop [edi].lpszText
invoke SendMessage,hwndTool,TTM_ADDTOOL,NULL,lpti
assume edi:nothing
assume esi:nothing
ret
SetDlgToolArea endp

end start

分析:

创建主对话框窗口之后,使用CreateWindowEx创建工具提示控件.

invoke InitCommonControls
invoke CreateWindowEx,NULL,ADDR ToolTipsClassName,NULL,\
TTS_ALWAYSTIP,CW_USEDEFAULT,\
CW_USEDEFAULT,CW_USEDEFAULT,CW_USEDEFAULT,NULL,NULL,\
hInstance,NULL
mov hwndTool,eax

之后,我们继续定义对话框四个角作为焦点域.

mov id,0 ; 焦点域ID
mov ti.cbSize,sizeof TOOLINFO
mov ti.uFlags,TTF_SUBCLASS ; 告诉控件子类化窗口.
push hDlg
pop ti.hWnd ; 包含焦点域的窗口句柄
invoke GetWindowRect,hDlg,addr rect ; 获得客户区的大小
invoke SetDlgToolArea,hDlg,addr ti,addr MainDialogText1,id,addr rect

我们初始化TOOLINFO结构. 注意我们要把客户区分成4个焦点域,因此我们需要知道客户区的大小,所以调用GetWindowRect.因为我们不想自己向控件转发消息,因此指定TIF_SUBCLASS 标志.
SetDlgToolArea 是计算焦点域矩形范围的并向控件注册的函数,我不详细解释计算过程.只说明它把对话框分成4个焦点域.然后向控件发送TTM_ADDTOOL 消息, 在lParam参数中传递TOOLINFO结构的地址.

invoke SendMessage,hwndTool,TTM_ADDTOOL,NULL,lpti

在四个控件注册之后,我们来看看对话框的按钮,我们可以用ID来处理每个按钮,但是实在太乏味了.我们使用EnumChildWindows函数列举对话框上的所有控件并把他们注册给控件,EnumChildWindows原型如下:

EnumChildWindows proto hWnd:DWORD, lpEnumFunc:DWORD, lParam:DWORD

hWnd 是父窗口句柄.

lpEnumFunc 是每个控件将调用的EnumChildProc函数地址.lParam 是应用程序定义的要传给EnumChildProc 函数的值. EnumChildProc 函数定义如下:

EnumChildProc proto hwndChild:DWORD, lParam:DWORD
hwndChild是EnumChildWindows函数枚举的句柄. lParam 就是你传递给EnumChildWindows函数的同一个lParam.
在例子中.我们如此调用 EnumChildWindows 函数:
invoke EnumChildWindows,hDlg,addr EnumChild,addr ti
我们把TOOLINFO结构的地址放在lParam参数中传递,是因为我们要在EnumChild函数中注册每个子控件.如果我们不使用这种方法,就需要将ti声明为全局变量,但这可能会引入很多bug.
当我们调用 EnumChildWindows时, Windows会枚举出对话框上所有的子控件并为每个子控件调用一次EnumChild f函数. 这样如果我们的对话框有两个控件,EnumChild将被调用两次.
EnumChild 函数填充TOOLINFO 结构的相应成员并向控件注册.
EnumChild proc uses edi hwndChild:DWORD,lParam:DWORD
LOCAL buffer[256]:BYTE
mov edi,lParam
assume edi:ptr TOOLINFO
push hwndChild
pop [edi].uId ; we use the whole client area of the control as the tool
or [edi].uFlags,TTF_IDISHWND
invoke GetWindowText,hwndChild,addr buffer,255
lea eax,buffer ; use the window text as the tooltip text
mov [edi].lpszText,eax
invoke SendMessage,hwndTool,TTM_ADDTOOL,NULL,edi
assume edi:nothing
ret
EnumChild endp
注意在例子中,我们使用了一种不同"工具":覆盖整个客户区的"工具",因此我们需要用包含"工具"窗口的句柄来填充uID成员,也必须在uFlags 成员中指定TTF_IDISHWND标志.

This article come from Iczelion's asm page, Welcom to http://asm.yeah.net/


Iczelion 的 Win32asm 教程

第二十八课: Win32调试API 第一部分

在本教程中,我们将学习Win32提供给开发者的用于调试的原语. 在教程的结尾,我们将学习如何调试一个进程.
下载 例子程序.

理论:

Win32有一些供程序员使用的API,它们提供相当于调试器的功能. 他们被称作Win32调试API(或原语).利用这些API,我们可以:

  • 加载一个程序或捆绑到一个正在运行的程序上以供调试
  • 获得被调试的程序的低层信息,例如进程ID,进入地址,映像基址等.
  • 当发生与调试有关的事件时被通知,例如进程/线程的开始/结束, DLL的加载/释放等.
  • 修改被调试的进程或线程

简而言之,我们可以用这些API写一个简单的调试器.由于这个题目有些过大,我把它分为几部分,而本教程就是它的第一部分.在本教程中,我将讲解一些基本概念及Win32调试API的大致框架.
使用Win32调试API的步骤如下:

  1. 创建一个进程或捆绑到一个运行中的进程上. 这是使用Win32调试API的第一步.由于我们的程序要扮演调试器的角色,我们要找一个供调试的程序.一个被调试的程序被称为debuggee.可以通过以下两种方式获得debuggee:
    • 通过CreateProcess创建debuggee进程.为了创建被调试的进程,必须指定DEBUG_PROCESS标志.这一标志告诉Windows我们要调试该进程. 当debuggee中发生重要的与调试有关的事件(调试事件)时,Windows 会向我们的程序发送通知.debuggee会立即挂起以等待我们的程序准备好.如果debuggee还创建了子进程,Windows还会为每个子进程中的调试事件向我们的程序发送通知.这一特性通常是不必要的.我们可以通过指定DEBUG_ONLY_THIS_PROCESS与 DEBUG_PROCESS的组合标志来禁止它.
    • 我们也可以用 DebugActiveProcess标志捆绑到一个运行中的进程上.
  2. 等待调试事件. 在获得了一个debuggee进程后,debuggee的主线程被挂起,这种状况将持续到我们的程序调用WaitForDebugEvent为止.这个函数和其他的WaitForXXX函数相似,比如说,它阻塞调用线程直到等待的事件发生.对这个函数来说, 它等待由Windows发送的调试事件.下面是它的定义:

    WaitForDebugEvent proto lpDebugEvent:DWORD, dwMilliseconds:DWORD

    lpDebugEvent is the address of a DEBUG_EVENT这个结构将被填入关于debuggee中发生的调试事件的信息.

    dwMilliseconds 该函数等待调试事件的时间,以毫秒为单位.如果这段时间没有调试事件发生, WaitForDebugEvent返回调用者.另一方面,如果将该参数指定为 INFINITE 常数,函数将一直等待直到调试事件发生.

    现在我们看一下DEBUG_EVENT 结构.

    DEBUG_EVENT STRUCT
    dwDebugEventCode dd ?
    dwProcessId dd ?
    dwThreadId dd ?
    u DEBUGSTRUCT <>
    DEBUG_EVENT ENDS

    dwDebugEventCode 该值指定了等待发生的调试事件的类型.因为有很多种类型的事件发生,我们的程序要检查该值,知道要发生事件的类型并做出响应. 该值可能的取值如下:

    3. 取值 | 含义
CREATE_PROCESS_DEBUG_EVENT | 进程被创建.当debuggee进程刚被创建(还未运行) 或我们的程序刚以DebugActiveProcess被捆绑到一个运行中的进程时事件发生. 这是我们的程序应该获得的第一个事件.
EXIT_PROCESS_DEBUG_EVENT | 进程退出.
CREATE_THEAD_DEBUG_EVENT | 当一个新线程在deuggee进程中创建或我们的程序首次捆绑到运行中的进程时事件发生.要注意的是当debugge的主线程被创建时不会收到该通知.
EXIT_THREAD_DEBUG_EVENT | debuggee中的线程退出时事件发生.debugee的主线程退出时不会收到该通知.我们可以认为debuggee的主线程与debugge进程是同义词. 因此, 当我们的程序看到CREATE_PROCESS_DEBUG_EVENT标志时,对主线程来说,就是CREATE_THREAD_DEBUG_EVENT标志.
LOAD_DLL_DEBUG_EVENT | debuggee装入一个DLL.当PE装载器第一次分解指向DLL的链接时,我们将收到这一事件. (当调用CreateProcess装入 debuggee时)并且当debuggee调用LoadLibrary时也会发生.
UNLOAD_DLL_DEBUG_EVENT | 一个DLL从debuggee中卸载时事件发生.
EXCEPTION_DEBUG_EVENT | 在debuggee中发生异常时事件发生. 注意: 该事件仅在debuggee开始它的第一条指令之前发生一次.异常实际上是一个调试中断(int 3h).如果想恢复debuggee事,以 DBG_CONTINUE 标志调用ContinueDebugEvent 函数. 不要使用DBG_EXCEPTION_NOT_HANDLED 标志否则debuggee会在NT下拒绝运行(Win98下运行得很好).
OUTPUT_DEBUG_STRING_EVENT | 当debuggee调用DebugOutputString函数向我们的程序发送消息字符串时该事件发生.
RIP_EVENT | 系统调试发生错误

    dwProcessId 和dwThreadId发生调试事件的进程和线程Id.我们可以用这些值作为我们感兴趣的进程或线程的标志符.记住如果我们使用CreateProcess来装载debuggee,我们仍可在PROCESS_INFO结构中获得debuggee的进程和线程.我们可以用这些值来区别调试事件是发生在debuggee中还是它的子进程中(当没有指定 DEBUG_ONLY_THIS_PROCESS 标志时).

    u 是一个联合,包含了调试事件的更多信息.根据上面dwDebugEventCode的不同,它可以是以下结构:

    dwDebugEventCode | u的解释
CREATE_PROCESS_DEBUG_EVENT | 名为CreateProcessInfo的CREATE_PROCESS_DEBUG_INFO结构
EXIT_PROCESS_DEBUG_EVENT | 名为ExitProcess的EXIT_PROCESS_DEBUG_INFO结构
CREATE_THREAD_DEBUG_EVENT | 名为CreateThread的CREATE_THREAD_DEBUG_INFO结构
EXIT_THREAD_DEBUG_EVENT | 名为ExitThread的EXIT_THREAD_DEBUG_EVENT 结构
LOAD_DLL_DEBUG_EVENT | 名为LoadDll的LOAD_DLL_DEBUG_INFO 结构
UNLOAD_DLL_DEBUG_EVENT | 名为UnloadDll的UNLOAD_DLL_DEBUG_INFO结构
EXCEPTION_DEBUG_EVENT | 名为Exception的EXCEPTION_DEBUG_INFO结构
OUTPUT_DEBUG_STRING_EVENT | 名为DebugString的OUTPUT_DEBUG_STRING_INFO 结构
RIP_EVENT | 名为RipInfo的RIP_INFO 结构

    我不会在这一个教程里讲所有这些结构的细节,这里只详细讲一下CREATE_PROCESS_DEBUG_INFO 结构.
    假设我们的程序调用了WaitForDebugEvent函数并返回,我们要做的第一件事就是检查dwDebugEventCode中的值来看debuggee进程中发生了那种类型的调试事件.比如说,如果dwDebugEventCode的值为 CREATE_PROCESS_DEBUG_EVENT,就可认为u的成员为CreateProcessInfo 并用u.CreateProcessInfo来访问.

  3. 在我们的程序中做对调试事件的响应. 当WaitForDebugEvent 返回时,这意味着在debuggee进程中发生了调试事件或者发生了超时.所以我们的程序要检查dwDebugEventCode 来作出适当的反应.这里有些象处理Windows消息:由用户来选择和忽略消息.
  4. 继续运行debuggee. 当调试事件发生时, Windows挂起了debuggee,所以当我们处理完调试事件,还要让debuggee继续运行.调用ContinueDebugEvent 函数来完成这一过程.

    ContinueDebugEvent proto dwProcessId:DWORD, dwThreadId:DWORD, dwContinueStatus:DWORD

    该函数恢复由于调试事件而挂起的线程.
    dwProcessId和dwThreadId是要恢复的线程的进程ID和线程ID,通常这两个值从 DEBUG_EVENT结构的dwProcessId 和dwThreadId成员获得.
    dwContinueStatus显示了如何继续报告调试事件的线程.可能的取值有两个: DBG_CONTINUE 和DBG_EXCEPTION_NOT_HANDLED. 对大多数调试事件,这两个值都一样:恢复线程.唯一的例外是EXCEPTION_DEBUG_EVENT,如果线程报告发生了一个异常调试事件,这意味着在debuggee的线程中发生了一个异常.如果指定了DBG_CONTINUE,线程将忽略它自己的异常处理部分并继续执行.在这种情况下,我们的程序必须在以DBG_CONTINUE恢复线程之前检查并处理异常,否则异常将生生不息地不断发生....如果我们指定了 DBG_EXCEPTION_NOT_HANDLED值,就是告诉Windows我们的程序并不处理异常:Windows将使用debuggee的默认异常处理函数来处理异常.
    总而言之,如果我们的程序没有考虑异常,而调试事件又指向debuggee进程中的一个异常的话,就应调用含DBG_CONTINUE标志的ContinueDebugEvent函数.否则,我们的程序就必须以DBG_EXCEPTION_NOT_HANDLED调用 ContinueDebugEvent.但在下面这种情况下必须使用DBG_CONTINUE标志:第一个在ExceptionCode成员中有值EXCEPTION_BREAKPOINT的 EXCEPTION_DEBUG_EVENT事件.当debuggee开始执行它的第一条指令时,我们的函数将接受到异常调试事件.它事实上是一个调试中断(int 3h).如果我们以DBG_EXCEPTION_NOT_HANDLED调用ContinueDebugEvent 来响应调试事件, Windows NT会拒绝执行debuggee(因为它没有异常处理).所以在这种情况下,要用DBG_CONTINUE标志告诉Windows我们希望该线程继续执行.

  5. 继续上面的步骤循环直到debuggee进程退出. 我们的程序必须在一个很象消息循环的无限循环中直到debuggee结束.该循环大体如下:

    .while TRUE
    invoke WaitForDebugEvent, addr DebugEvent, INFINITE
    .break .if DebugEvent.dwDebugEventCode==EXIT_PROCESS_DEBUG_EVENT
    <调试事件处理>
    invoke ContinueDebugEvent, DebugEvent.dwProcessId, DebugEvent.dwThreadId, DBG_EXCEPTION_NOT_HANDLED
    .endw

    就是说,当开始调试程序时,我们的程序不能和debuggee分开直到它结束.

我们再来总结一下这些步骤:

  1. 创建一个进程或捆绑我们的程序到运行中的进程上.
  2. 等待调试事件
  3. 响应调试事件.
  4. 继续执行debuggee.
  5. 继续这一无尽循环直到debuggee进程结束

例子:

这个例子调试一个win32程序并显示诸如进程句柄,进程Id,映象基址等.

.386
.model flat,stdcall
option casemap:none
include \masm32\include\windows.inc
include \masm32\include\kernel32.inc
include \masm32\include\comdlg32.inc
include \masm32\include\user32.inc
includelib \masm32\lib\kernel32.lib
includelib \masm32\lib\comdlg32.lib
includelib \masm32\lib\user32.lib
.data
AppName db "Win32 Debug Example no.1",0
ofn OPENFILENAME <>
FilterString db "Executable Files",0,"*.exe",0
db "All Files",0,"*.*",0,0
ExitProc db "The debuggee exits",0
NewThread db "A new thread is created",0
EndThread db "A thread is destroyed",0
ProcessInfo db "File Handle: %lx ",0dh,0Ah
db "Process Handle: %lx",0Dh,0Ah
db "Thread Handle: %lx",0Dh,0Ah
db "Image Base: %lx",0Dh,0Ah
db "Start Address: %lx",0
.data?
buffer db 512 dup(?)
startinfo STARTUPINFO <>
pi PROCESS_INFORMATION <>
DBEvent DEBUG_EVENT <>
.code
start:
mov ofn.lStructSize,sizeof ofn
mov ofn.lpstrFilter, offset FilterString
mov ofn.lpstrFile, offset buffer
mov ofn.nMaxFile,512
mov ofn.Flags, OFN_FILEMUSTEXIST or OFN_PATHMUSTEXIST or OFN_LONGNAMES or OFN_EXPLORER or OFN_HIDEREADONLY
invoke GetOpenFileName, ADDR ofn
.if eax==TRUE
invoke GetStartupInfo,addr startinfo
invoke CreateProcess, addr buffer, NULL, NULL, NULL, FALSE, DEBUG_PROCESS+ DEBUG_ONLY_THIS_PROCESS, NULL, NULL, addr startinfo, addr pi
.while TRUE
invoke WaitForDebugEvent, addr DBEvent, INFINITE
.if DBEvent.dwDebugEventCode==EXIT_PROCESS_DEBUG_EVENT
invoke MessageBox, 0, addr ExitProc, addr AppName, MB_OK+MB_ICONINFORMATION
.break
.elseif DBEvent.dwDebugEventCode==CREATE_PROCESS_DEBUG_EVENT
invoke wsprintf, addr buffer, addr ProcessInfo, DBEvent.u.CreateProcessInfo.hFile, DBEvent.u.CreateProcessInfo.hProcess, DBEvent.u.CreateProcessInfo.hThread, DBEvent.u.CreateProcessInfo.lpBaseOfImage, DBEvent.u.CreateProcessInfo.lpStartAddress
invoke MessageBox,0, addr buffer, addr AppName, MB_OK+MB_ICONINFORMATION
.elseif DBEvent.dwDebugEventCode==EXCEPTION_DEBUG_EVENT
.if DBEvent.u.Exception.pExceptionRecord.ExceptionCode==EXCEPTION_BREAKPOINT
invoke ContinueDebugEvent, DBEvent.dwProcessId, DBEvent.dwThreadId, DBG_CONTINUE
.continue
.endif
.elseif DBEvent.dwDebugEventCode==CREATE_THREAD_DEBUG_EVENT
invoke MessageBox,0, addr NewThread, addr AppName, MB_OK+MB_ICONINFORMATION
.elseif DBEvent.dwDebugEventCode==EXIT_THREAD_DEBUG_EVENT
invoke MessageBox,0, addr EndThread, addr AppName, MB_OK+MB_ICONINFORMATION
.endif
invoke ContinueDebugEvent, DBEvent.dwProcessId, DBEvent.dwThreadId, DBG_EXCEPTION_NOT_HANDLED
.endw
invoke CloseHandle,pi.hProcess
invoke CloseHandle,pi.hThread
.endif
invoke ExitProcess, 0
end start

分析:

程序首先填充OPENFILENAME结构,调用GetOpenFileName让用户选择要调试的程序.

invoke GetStartupInfo,addr startinfo
invoke CreateProcess, addr buffer, NULL, NULL, NULL, FALSE, DEBUG_PROCESS+ DEBUG_ONLY_THIS_PROCESS, NULL, NULL, addr startinfo, addr pi

当接收用户选择后,调用CreateProcess装载程序.并调用GetStartupInfo以默认值填充STARTUPINFO结构.注意我们将DEBUG_PROCESS标志与DEBUG_ONLY_THIS_PROCESS标志组合来仅调试这个程序,不包括子进程.

.while TRUE
invoke WaitForDebugEvent, addr DBEvent, INFINITE

在debuggee被装入后,我们调用WaitForDebugEvent进入无尽的调试循环,WaitForDebugEvent在debuggee中发生调试事件时返回,因为我们指定了INFINITE作为第二个参数.当调试事件发生时, WaitForDebugEvent 返回并填充DBEvent结构.

.if DBEvent.dwDebugEventCode==EXIT_PROCESS_DEBUG_EVENT
invoke MessageBox, 0, addr ExitProc, addr AppName, MB_OK+MB_ICONINFORMATION
.break

我们要先检查dwDebugEventCode的值, 如果是EXIT_PROCESS_DEBUG_EVENT,用一个消息框显示"The debuggee exits" 并退出调试循环.

.elseif DBEvent.dwDebugEventCode==CREATE_PROCESS_DEBUG_EVENT
invoke wsprintf, addr buffer, addr ProcessInfo, DBEvent.u.CreateProcessInfo.hFile, DBEvent.u.CreateProcessInfo.hProcess, DBEvent.u.CreateProcessInfo.hThread, DBEvent.u.CreateProcessInfo.lpBaseOfImage, DBEvent.u.CreateProcessInfo.lpStartAddress
invoke MessageBox,0, addr buffer, addr AppName, MB_OK+MB_ICONINFORMATION

如果dwDebugEventCode 的值为CREATE_PROCESS_DEBUG_EVENT,我们就在消息框中显示一些感兴趣的底层信息.这些信息从u.CreateProcessInfo获得. CreateProcessInfo是一个CREATE_PROCESS_DEBUG_INFO类型的结构体.你可以查阅Win32 API获得它的更多信息e.

.elseif DBEvent.dwDebugEventCode==EXCEPTION_DEBUG_EVENT
.if DBEvent.u.Exception.pExceptionRecord.ExceptionCode==EXCEPTION_BREAKPOINT
invoke ContinueDebugEvent, DBEvent.dwProcessId, DBEvent.dwThreadId, DBG_CONTINUE
.continue
.endif

如果dwDebugEventCode 的值为EXCEPTION_DEBUG_EVENT,我们就要更进一步检查异常类型.它是一大堆的结构嵌套,但我们可以从ExceptionCode成员获得异常类型.如果ExceptionCode的值为 EXCEPTION_BREAKPOINT并且是第一次发生(或者我们已知道deuggee中没有int 3h指令),我们可以安全地假定在debuggee要执行第一条指令时发生这一异常.在我们完成这些处理后,就可以用 DBG_CONTINUE调用ContinueDebugEvent来继续执行debuggee.接着我们继续等待下一个调试事件的发生.

.elseif DBEvent.dwDebugEventCode==CREATE_THREAD_DEBUG_EVENT
invoke MessageBox,0, addr NewThread, addr AppName, MB_OK+MB_ICONINFORMATION
.elseif DBEvent.dwDebugEventCode==EXIT_THREAD_DEBUG_EVENT
invoke MessageBox,0, addr EndThread, addr AppName, MB_OK+MB_ICONINFORMATION
.endif

如果dwDebugEventCode 的值为CREATE_THREAD_DEBUG_EVENT或EXIT_THREAD_DEBUG_EVENT, 我们的程序显示一个消息框.

invoke ContinueDebugEvent, DBEvent.dwProcessId, DBEvent.dwThreadId, DBG_EXCEPTION_NOT_HANDLED
.endw

除了上面讨论过的 EXCEPTION_DEBUG_EVENT,用DBG_EXCEPTION_NOT_HANDLED标志调用ContinueDebugEvent函数恢复debuggee的执行.

invoke CloseHandle,pi.hProcess
invoke CloseHandle,pi.hThread

当debuggee结束时,我们就跳出了调试循环,这时要关闭 debuggee的线程和进程句柄.关闭这些句柄并不意味着要关闭这些进程和线程.只是说不再用这些句柄罢了.

校对：LuoYunBin's Win32 ASM Page， http://asm.yeah.net


Iczelion's win32 asm tutorial

第29课: Win32调试API 第二部分

我们继续Win32调试API的话题。在本章中，我们将要学习如何修改被调试程序。
下载 the example

理论:

在前面一章中，我们学会了如何装载被调试的进程以及如何处理进程中发生的事件。 为了有实际用途，我们的程序应具有修改被调试程序的能力。有好几个API函数用于这 一目的。

  • ReadProcessMemory该函数允许你去读指定的进程的内存。函数原型如下:

    ReadProcessMemory proto hProcess:DWORD, lpBaseAddress:DWORD, lpBuffer:DWORD, nSize:DWORD, lpNumberOfBytesRead:DWORD

    hProcess 待读进程的句柄.
    lpBaseAddress 目标进程中待读内存起始地址。例如，如果你想要读目标 进程中从地址401000h开始的4个字节，该参数值应置为401000h。
    lpBuffer 接收缓冲区地址
    nSize 想要读的字节数。
    lpNumberOfBytesRead 记录实际读取的字节数的变量地址。如果对这个值 不关心，填入NULL即可。

  • WriteProcessMemory 是对应于ReadProcessMemory的函数，通过它 可以写目标进程的内存。其参数和ReadProcessMemory 相同。

    理解接下去的两个函数需要一些进程上下文的有关背景知识。在象Windows这样的 多任务操作系统中，同一时间里可能运行着几个程序。Windows分配给每个线程一个 时间片，当时间片结束后，Windows将冻结当前线程并切换到下一具有最高优先级的 线程。在切换之前，Windows将保存当前进程的寄存器的 内容，这样当在该线程再 次恢复运行时，Windows可以恢复最近一次线程运行的*环境*。保存的寄存器内容总 称为进程上下文。
    现在回到我们的主题。当一个调试事件发生时，Windows暂停被调试进程，并保存其 进程上下文。由于进程被暂停运行，我们可以确信其进程上下文内容将保持不变。 可以用GetThreadContext来获取进程上下文内容，并且也可以用GetThreadContext 来修改进程上下文内容。
    这两个函数威力非凡。有了他们，对被调试进程你就具有象VxD的能力: 如改变其寄 存器内容，而在被调试程序恢复运行前，这些值将会写回寄存器中。在进程上下文中 所做的任何改动，将都会反映到被调试程序中。想象一下: 甚至可以改变eip寄存器 的内容，这样你可以让程序运行到你想要的任何地方! 在正常情况下是不可能做到这 一点的。

    GetThreadContext proto hThread:DWORD, lpContext:DWORD

    hThread 你想要获得上下文的线程句柄
    lpContext 函数成功返回时用来保存上下文内容的结构指针。

    SetThreadContext 参数相同。让我们来看看上下文的结构:

  • CONTEXT STRUCT
  • ContextFlags dd ?
    ;----------------------------------------------------------------------------------------------------------
    ;当ContextFlags包含CONTEXT_DEBUG_REGISTERS，返回本部分
  • ;-----------------------------------------------------------------------------------------------------------
    iDr0 dd ?
    iDr1 dd ?
    iDr2 dd ?
    iDr3 dd ?
    iDr6 dd ?
    iDr7 dd ?
  • ;----------------------------------------------------------------------------------------------------------
    ;当ContextFlags包含CONTEXT_FLOATING_POINT，返回本部分
  • ;-----------------------------------------------------------------------------------------------------------
  • FloatSave FLOATING_SAVE_AREA <>
  • ;----------------------------------------------------------------------------------------------------------
    ;当ContextFlags包含CONTEXT_SEGMENTS，返回本部分
  • ;-----------------------------------------------------------------------------------------------------------
  • regGs dd ?
    regFs dd ?
    regEs dd ?
    regDs dd ?
  • ;----------------------------------------------------------------------------------------------------------
    ;当ContextFlags包含CONTEXT_INTEGER，返回本部分
  • ;-----------------------------------------------------------------------------------------------------------
  • regEdi dd ?
    regEsi dd ?
    regEbx dd ?
    regEdx dd ?
    regEcx dd ?
    regEax dd ?
  • ;----------------------------------------------------------------------------------------------------------
    ;当ContextFlags包含CONTEXT_CONTROL，返回本部分
  • ;-----------------------------------------------------------------------------------------------------------
  • regEbp dd ?
    regEip dd ?
    regCs dd ?
    regFlag dd ?
    regEsp dd ?
    regSs dd ?
  • ;----------------------------------------------------------------------------------------------------------
    ;当ContextFlags包含CONTEXT_EXTENDED_REGISTERS，返回本部分
  • ;-----------------------------------------------------------------------------------------------------------
  • ExtendedRegisters db MAXIMUM_SUPPORTED_EXTENSION dup(?) CONTEXT ENDS

    可以看出，该结构中的成员是对实际处理器的寄存器的模仿。在使用该结构之前 要在ContextFlags 中指定哪些寄存器组用来读写。如要访问所有的寄存器， 你可以置ContextFlags 为CONTEXT_FULL 。或者只访问regEbp, regEip, regCs, regFlag, regEsp 或 regSs, 应置ContextFlags 为 CONTEXT_CONTROL 。

    在使用结构CONTEXT 时还应记住: 它必须是双字对齐的，否则在NT下将得 到奇怪的结果。可以在定义前加上"align dword"。例如:

    align dword
    MyContext CONTEXT <>

例:

第一个例子演示DebugActiveProcess的使用。首先，需要在Windows显示在屏 幕上以前运行一个待调试程序win.exe，该程序将处于无限循环运行状态中。然后你运行 例子程序，它将把自己与win.exe连接起来，并且修改win.exe的代码，这样win.exe将退 出无限循环状态而显示自己的窗口。

.386
.model flat,stdcall
option casemap:none
include \masm32\include\windows.inc
include \masm32\include\kernel32.inc
include \masm32\include\comdlg32.inc
include \masm32\include\user32.inc
includelib \masm32\lib\kernel32.lib
includelib \masm32\lib\comdlg32.lib
includelib \masm32\lib\user32.lib

.data
AppName db "Win32 Debug Example no.2",0
ClassName db "SimpleWinClass",0
SearchFail db "Cannot find the target process",0
TargetPatched db "Target patched!",0
buffer dw 9090h

.data?
DBEvent DEBUG_EVENT <>
ProcessId dd ?
ThreadId dd ?
align dword
context CONTEXT <>

.code
start:
invoke FindWindow, addr ClassName, NULL
.if eax!=NULL
invoke GetWindowThreadProcessId, eax, addr ProcessId
mov ThreadId, eax
invoke DebugActiveProcess, ProcessId
.while TRUE
invoke WaitForDebugEvent, addr DBEvent, INFINITE
.break .if DBEvent.dwDebugEventCode==EXIT_PROCESS_DEBUG_EVENT
.if DBEvent.dwDebugEventCode==CREATE_PROCESS_DEBUG_EVENT
mov context.ContextFlags, CONTEXT_CONTROL
invoke GetThreadContext,DBEvent.u.CreateProcessInfo.hThread, addr context
invoke WriteProcessMemory, DBEvent.u.CreateProcessInfo.hProcess, context.regEip ,addr buffer, 2, NULL
invoke MessageBox, 0, addr TargetPatched, addr AppName, MB_OK+MB_ICONINFORMATION
.elseif DBEvent.dwDebugEventCode==EXCEPTION_DEBUG_EVENT
.if DBEvent.u.Exception.pExceptionRecord.ExceptionCode==EXCEPTION_BREAKPOINT
invoke ContinueDebugEvent, DBEvent.dwProcessId,DBEvent.dwThreadId, DBG_CONTINUE
.continue
.endif
.endif
invoke ContinueDebugEvent, DBEvent.dwProcessId, DBEvent.dwThreadId, DBG_EXCEPTION_NOT_HANDLED
.endw
.else
invoke MessageBox, 0, addr SearchFail, addr AppName,MB_OK+MB_ICONERROR .endif
invoke ExitProcess, 0
end start

;--------------------------------------------------------------------
; The partial source code of win.asm, our debuggee. It's actually
; the simple window example in tutorial 2 with an infinite loop inserted
; just before it enters the message loop.
;----------------------------------------------------------------------

......
mov wc.hIconSm,eax
invoke LoadCursor,NULL,IDC_ARROW
mov wc.hCursor,eax
invoke RegisterClassEx, addr wc
INVOKE CreateWindowEx,NULL,ADDR ClassName,ADDR AppName,\ WS_OVERLAPPEDWINDOW,CW_USEDEFAULT,\ CW_USEDEFAULT,CW_USEDEFAULT,CW_USEDEFAULT,NULL,NULL,\ hInst,NULL
mov hwnd,eax
jmp $ <---- Here's our infinite loop. It assembles to EB FE
invoke ShowWindow, hwnd,SW_SHOWNORMAL
invoke UpdateWindow, hwnd
.while TRUE
invoke GetMessage, ADDR msg,NULL,0,0
.break .if (!eax)
invoke TranslateMessage, ADDR msg
invoke DispatchMessage, ADDR msg
.endw
mov eax,msg.wParam
ret
WinMain endp

分析:

invoke FindWindow, addr ClassName, NULL

我们的程序需要用DebugActiveProcess将自己绑定到被调试程序，这需要知道 被调试程序的进程Id。用GetWindowThreadProcessId 可以得到该Id，该函数需要 窗口句柄作为参数，因此首先需要知道窗口句柄。
用FindWindow, 我们先指定窗口类的名称，返回的是该类创建的窗口句柄。如 果返回NULL，则表明当前没有该类的窗口。

.if eax!=NULL
invoke GetWindowThreadProcessId, eax, addr ProcessId
mov ThreadId, eax
invoke DebugActiveProcess, ProcessId

得到进程Id后，我们调用DebugActiveProcess。这样就进入等待调试事件的循 环中。

.if DBEvent.dwDebugEventCode==CREATE_PROCESS_DEBUG_EVENT
mov context.ContextFlags, CONTEXT_CONTROL
invoke GetThreadContext,DBEvent.u.CreateProcessInfo.hThread, addr context

当得到 CREATE_PROCESS_DEBUG_INFO, 这意味着被调试进程已经被暂停运行了。 我们就可以对该进程动手术了。本例中，我们将用NOPs ( 90h 90h)覆盖被调试进程中的无 限循环指令(0EBh 0FEh) 。
首先，需要得到该指令的地址。由于在我们的程序绑定到被调试程序时，被调试程序已经 处于循环语句中了，eip总是指向该指令。我们所要做的是得到eip的值。我们将使用 GetThreadContext来达到此目的。将上下文结构成员中ContextFlags设置 为CONTEXT_CONTROL ，这样告诉GetThreadContext我们需要它去填充上下 文结构的成员中的"控制"寄存器。

invoke WriteProcessMemory, DBEvent.u.CreateProcessInfo.hProcess, context.regEip ,addr buffer, 2, NULL

得到eip的值以后，可以调用WriteProcessMemory来用NOPs覆盖"jmp $" 指令，这样将使被调试程序退出无限循环。在向用户显示了信息之后，调用ContinueDebugEvent 来恢复被调试程序的运行。由于指令"jmp $"已被Nops覆盖，被调试程序将继续 显示窗口，并进入消息循环。证据是我们在屏幕上观察到了次窗口。

另一个例子与此稍有不同，它是将被调试程序从无限循环中中断。

.......
.......
.if DBEvent.dwDebugEventCode==CREATE_PROCESS_DEBUG_EVENT
mov context.ContextFlags, CONTEXT_CONTROL
invoke GetThreadContext,DBEvent.u.CreateProcessInfo.hThread, addr context
add context.regEip,2
invoke SetThreadContext,DBEvent.u.CreateProcessInfo.hThread, addr context
invoke MessageBox, 0, addr LoopSkipped, addr AppName, MB_OK+MB_ICONINFORMATION
.......
.......

这里仍调用GetThreadContext来获取eip值，但没有去覆盖"jmp $" 指令，而是将 regEip加2，从而"跳过"该指令。结果是当被调试程序 重新获得控制权时，将恢复执行在"jmp $"后的指令。

现在你可以体会到Get/SetThreadContext的威力了。你也可以修改其他寄存器映象，这 些值将直接反映到被调试程序中。甚至你可以把int 3h指令插入到被调试进程中。产生断点。

This article come from Iczelion's asm page, 翻译 xjg1124@163.net. Welcom to http://asm.yeah.net


Iczelion's win32 asm tutorial

第30课: Win32调试API 第三部分

在本章中，我们将继续探讨win32调试api。特别地，我们将学习如何去跟踪被调试程序.
下载 例子.

理论:

如果你以前使用过调试器，那么你应对跟踪比较熟悉。当"跟踪"一个程序时， 程序在每执行一条指令后将会停止，这使你有机会去检查寄存器/内存中的值。这种单步运 行的官方定义为跟踪(tracing)。
单步运行的特色是由CPU本身提供的。标志寄存器的第8位称为陷阱标志trap flag。 如果该位设置，则CPU运行于单步模式。CPU将在每条指令后产生一个debug异常。当debug 异常产生后，陷阱标志自动清除。利用win32调试api，我们也可以单步运行被调试程序。 方法如下:

  1. 调用GetThreadContext, 指定 ContextFlags为CONTEXT_CONTROL， 来获得标志寄存器的值
  2. 设置CONTEXT结构成员标志寄存器regFlag中的陷阱标志位
  3. 调用 SetThreadContext
  4. 等待调式事件。被调试程序将按单步模式执行，在每执行一条指令后，我们将得到调试 事件，u.Exception.pExceptionRecord.ExceptionCode值为EXCEPTION_SINGLE_STEP
  5. 如果要跟踪下一条指令，需要再次设置陷阱标志位。

例:

.386
.model flat,stdcall
option casemap:none
include \masm32\include\windows.inc
include \masm32\include\kernel32.inc
include \masm32\include\comdlg32.inc
include \masm32\include\user32.inc
includelib \masm32\lib\kernel32.lib
includelib \masm32\lib\comdlg32.lib
includelib \masm32\lib\user32.lib

.data
AppName db "Win32 Debug Example no.4",0
ofn OPENFILENAME <>
FilterString db "Executable Files",0,"*.exe",0
db "All Files",0,"*.*",0,0
ExitProc db "The debuggee exits",0Dh,0Ah
db "Total Instructions executed : %lu",0
TotalInstruction dd 0

.data?
buffer db 512 dup(?)
startinfo STARTUPINFO <>
pi PROCESS_INFORMATION <>
DBEvent DEBUG_EVENT <>
context CONTEXT <>

.code
start:
mov ofn.lStructSize,SIZEOF ofn
mov ofn.lpstrFilter, OFFSET FilterString
mov ofn.lpstrFile, OFFSET buffer
mov ofn.nMaxFile,512
mov ofn.Flags, OFN_FILEMUSTEXIST or OFN_PATHMUSTEXIST or OFN_LONGNAMES or OFN_EXPLORER or OFN_HIDEREADONLY
invoke GetOpenFileName, ADDR ofn
.if eax==TRUE
invoke GetStartupInfo,addr startinfo
invoke CreateProcess, addr buffer, NULL, NULL, NULL, FALSE, DEBUG_PROCESS+ DEBUG_ONLY_THIS_PROCESS, NULL, NULL, addr startinfo, addr pi
.while TRUE
invoke WaitForDebugEvent, addr DBEvent, INFINITE
.if DBEvent.dwDebugEventCode==EXIT_PROCESS_DEBUG_EVENT
invoke wsprintf, addr buffer, addr ExitProc, TotalInstruction
invoke MessageBox, 0, addr buffer, addr AppName, MB_OK+MB_ICONINFORMATION
.break
.elseif DBEvent.dwDebugEventCode==EXCEPTION_DEBUG_EVENT .if DBEvent.u.Exception.pExceptionRecord.ExceptionCode==EXCEPTION_BREAKPOINT
mov context.ContextFlags, CONTEXT_CONTROL
invoke GetThreadContext, pi.hThread, addr context
or context.regFlag,100h
invoke SetThreadContext,pi.hThread, addr context
invoke ContinueDebugEvent, DBEvent.dwProcessId, DBEvent.dwThreadId, DBG_CONTINUE
.continue
.elseif DBEvent.u.Exception.pExceptionRecord.ExceptionCode==EXCEPTION_SINGLE_STEP
inc TotalInstruction
invoke GetThreadContext,pi.hThread,addr context or context.regFlag,100h
invoke SetThreadContext,pi.hThread, addr context
invoke ContinueDebugEvent, DBEvent.dwProcessId, DBEvent.dwThreadId,DBG_CONTINUE
.continue
.endif
.endif
invoke ContinueDebugEvent, DBEvent.dwProcessId, DBEvent.dwThreadId, DBG_EXCEPTION_NOT_HANDLED
.endw
.endif
invoke CloseHandle,pi.hProcess
invoke CloseHandle,pi.hThread
invoke ExitProcess, 0
end start

分析:

该程序先显示一个打开文件对话框，当用户选择了一个可执行文件，它将单步执行该程序， 并记录执行的指令数，直到被调试程序退出运行。

.elseif DBEvent.dwDebugEventCode==EXCEPTION_DEBUG_EVENT .if DBEvent.u.Exception.pExceptionRecord.ExceptionCode==EXCEPTION_BREAKPOINT

利用该机会来设置被调试程序为单步运行模式。记住，在执行被调试程序的第一条指令前 windows将发送一个EXCEPTION_BREAKPOINT消息。

mov context.ContextFlags, CONTEXT_CONTROL
invoke GetThreadContext, pi.hThread, addr context

调用GetThreadContext，以被调试程序的当前寄存器内容来填充CONTEXT 结构 特别地，我们需要标志寄存器的当前值。

or context.regFlag,100h

设置标志寄存器映象的陷阱位(第8位)

invoke SetThreadContext,pi.hThread, addr context
invoke ContinueDebugEvent, DBEvent.dwProcessId, DBEvent.dwThreadId, DBG_CONTINUE
.continue

然后调用SetThreadContext去覆盖CONTEXT的值。再以DBG_CONTINUE调用 ContinueDebugEvent 来恢复被调试程序的运行。

.elseif DBEvent.u.Exception.pExceptionRecord.ExceptionCode==EXCEPTION_SINGLE_STEP
inc TotalInstruction

当调试程序中一条指令执行后，我们将接收到EXCEPTION_DEBUG_EVENT的调试事件， 必须要检查u.Exception.pExceptionRecord.ExceptionCode的值。如果该值为 EXCEPTION_SINGLE_STEP，那么，该调试事件是单步运行模式造成的。在这种情况 下，TotalInstruction加一，因为我们确切地知道此时被调试程序执行了一条指令。

invoke GetThreadContext,pi.hThread,addr context or context.regFlag,100h
invoke SetThreadContext,pi.hThread, addr context
invoke ContinueDebugEvent, DBEvent.dwProcessId, DBEvent.dwThreadId,DBG_CONTINUE
.continue

由于陷阱标志在debug异常后自动清除了，如果我们需要继续保持单步运行模式，则 必须设置陷阱标志位。
警告: 不要用本教程中的此例子来调试大程序: 跟踪是很慢的。你或许需要等待10 多分钟才能关闭被调试程序。

This article come from Iczelion's asm page, 翻译 xjg1124@163.net. Welcom to http://asm.yeah.net


Iczelion's win32 asm tutorial

第三十一课 列表视图控件

本课中我们将学习如何创建和使用列表视图控件。

理论：

列表视图控件和树型视图、丰富文本编辑控件一样是通用控件的一种。可能您都已经知道了列表视图控件，只不过是不知道它的确切名字而已。列表视图控件可以用来很好地显示项目。在这方面它和列表框相同，只不过它的性能更强。
有两种方法创建一个列表视图控件。第一种也是最简单的方法是：用资源编辑器来创建它。用该种方法只是不要忘记在您的代码（的任何位置处）加入对InitCommonControls函数的调用（记得吗，调用该函数只是为了隐式地加载包含通用控件的DLL）。另一种方法是调用CreateWindowEx函数，这里您必须指定合适的类名，譬如：SysListView32，WC_LISTVIEW不是正确的类名
在列表视图种有四种方法来显示数据：大图标，小图标，列表和报告方式。这些方法和在资源管理器种选择View->Large Icons，Small Icons ， List 和 Details 相对应。各种不同的显示方式只是显示了不同的外观而已。譬如，您可能有许多的数据，只是并不想全部显示。报告方式提供的消息最完全，其它的方式则要少得多。在刚创建一个列表视图时您可以选择一种初始显示方法，随后您可以调用SetWinodwLong函数并设置GWL_STYLE标志位来改变显示方式

既然我们已经知道了如何创建列表控件，接下来我们学习如何使用它们。我们将主要集中在报告方式的显示上，因为该种方式演示了最多的列表控制的特性。使用列表控制的步骤如下：

  1. 调用CreateWindowEx函数来创建一个列表控件，指定它的类名为SysListView32。您还可以在此处指定控件初次显示时的方式。
  2. 创建和初始化用在列表控件中显示项目的图象列表（如果存在）。
  3. 向列表控件中插入列，如果显示的方式是报告方式这一步是必须的。
  4. 向控件中插入项目和自项目。

列：

在报告方式中，有不止一个列。您可以把放入到列表控件中的数据看作是一张表单：这时数据是按行列排列的。在控件中至少有一列。在其它的显示方式中则无所谓，因为这些显示方式有仅有一列。
加入列要通过向列表控件发送LVM_INSERTCOLUMN消息来实现。

LVM_INSERTCOLUMN
wParam = iCol
lParam =指向LV_COLUMN型结构体变量的指针

iCol 列数，从0开始编号。
LV_COLUMN 包含了将插入的列的信息。它的定义如下：

LV_COLUMN STRUCT
imask dd ?
fmt dd ?
lx dd ?
pszText dd ?
cchTextMax dd ?
iSubItem dd ?
iImage dd ?
iOrder dd ?
LV_COLUMN ENDS

Field name | Meanings
imask

一组标志位，它指示了该结构体中的那些成员变量是有效的。该结构体中的成员变量并不是同时有效的。在某些时候，可能只有某些成员变量是有效的。结构体可以用来输入和输出。这样让WINDOWS知道那些成员变量是有效的是非常重要的。可能的标志有：

LVCF_FMT = fmt有效
LVCF_SUBITEM = iSubItem有效
LVCF_TEXT = pszText有效.
LVCF_WIDTH = lx有效

您可以一次使用几个标志。譬如，如果您向指定列的文本标签（列名），您必须在pszText成员变量中提供列名，然后指定标志LVCF_TEXT告诉WINDOWS成员变量pszText中的值是有效的，否则WINDOWS将忽略掉pszText中的值。

fmt

指定了项目/子项目的对齐方式。可能的值有：

LVCFMT_CENTER = 文本居中
LVCFMT_LEFT = 文本左对齐
LVCFMT_RIGHT = 文本右对齐

lx | lx 是列的宽度（以像素点为单位）。以后您可以发送消息LVM_SETCOLUMNWIDTH来改变列的宽度。
pszText | 如果用来设定列的属性时，该成员变量为指向列名的指针。如果是查询列名，该成员变量指向一个足够大的缓冲区，用来接收返回的列名，这是您必须在成员cchTextMax中指定缓冲区的大小。如果是设定列名时，可以忽略该变量，因为该指针指向的是一个ASCII码的字符串，而WINDOWS可以解析出ASCII串的长度。
cchTextMax | cchTextMax 以字节计的上面一个成员变量指向的缓冲区的小。该成员变量只在您查询列的属性时使用。如果是设定列的属性，那该变量将被忽略。
iSubItem | 指定和该列相连的子项目的索引号。该成员变量的值用来标识和列相连系的子项目。该列的使用最好地说明了如何把列号和子项目相连。要查询列的属性时可以发送LVM_GETCOLUMN消息，并在成员变量imask中指定LVCF_SUBITEM标志，列表控件将在iSubItem中返回插入时设定的iSubItem值。为了使用该办法，您需要在该成员变量中放入正确的值。
iImage and iOrder | 为了和IE3.0以上版本兼容。目前我没有这方面的资料。

在列表视图控件创建后，您必须至少向其中插入一列。当然如果不打算使用报告方式显示，那倒是没有必要插入列。为了插入列，您需要定义一个LV_COLUMN型的结构体变量，给其成员变量赋上正确的值，指定列号，然后向列表视图控件发送LVM_INSERTCOLUMN消息并把该结构体变量的值传过去。

LOCAL lvc:LV_COLUMN
mov lvc.imask,LVCF_TEXT+LVCF_WIDTH
mov lvc.pszText,offset Heading1
mov lvc.lx,150
invoke SendMessage,hList, LVM_INSERTCOLUMN,0,addr lvc

上面的代码段显示了该过程。当发送LVM_INSERTCOLUMN消息时，他指定了列的标题条文本和它的宽度。

项目和子项目

项目是列表视图中主要的内容。除报告方式显示的外，在列表视图您只能看到项目。子项目是项目的详细信息。一个项目可能有不止一个相关的子项目。举个例子，譬如项目是文件名，那其相关的子项目可能有文件属性、大小、创建日期等。在报告方式的视图中，最左边一列是项目，其它列是子项目。从数据库记录的角度看，项目类似主键，子项目类似记录。
至少您的列表视图需要一些项目：子项目是可选的。如果您想要给用户提供更多的信息，可以把子项目和项目相连，然后放到列表视图中以报告的方式显示。
您可以通过向列表视图发送LVM_INSERTITEM消息来向其中添加项目，这时还需要把一个指向LV_ITEM型的结构体的变量的指针放到lParam一同传给列表视图。LV_ITEM的定义如下：

LV_ITEM STRUCT
imask dd ?
iItem dd ?
iSubItem dd ?
state dd ?
stateMask dd ?
pszText dd ?
cchTextMax dd ?
iImage dd ?
lParam dd ?
iIndent dd ?
LV_ITEM ENDS

Field name | Meanings
imask | 一组标志位标明该结构体中那些成员变量中的值有效。它的意义和上面我们提到的LV_COLUMN型结构体中向对应的成员变量基本相同。更详细的信息，可以查询WIN32 API 手册。
iItem | 该结构体代表的项目的索引号。索引号是从0开始编号的。该值和表单的“行”类似。
iSubItem | 和上一个成员变量指定的项目相连的子项目的索引号。您可以把它当作表单的“列”。譬如您想要把一个项目插入到新创建的列表视图控件，iItem的值应为0（因为该项目是第一个项目），iSubItem的值也应当为0（我们想把该项目插到第一列）。如果你想指定一个子项目和该项目相连，iItem中应该是您想要相连的项目的索引号，iSubItem的值应当是大于0的值，具体的值取决于您想把该子项目插在那一列。如果你的列表视图控件一共有4列的化，第一列包含了项目，其余3列是留给子项目的。如果您想把子项目插在第四列，应当指定该值为3。
state | 该成员变量包含的标志位反应了项目的状态。状态的改变可能是由用户的操作引起的或是程序改变的。这些状态包括：是否有焦点/高亮度显示/被选中（由于被剪切）/被选中等。另外还包括，以1为基数的索引用来代表是否处使用重叠/状态图标。
stateMask | 由于上面的成员变量包含状态标志位、重叠的位图索引号、和状态位图的索引号，我们需要告诉WINDOWS我们到底需要设定或查询那一个值。该成员变量就是用来做这项工作的。
pszText | 当我们想设定项目的属性时，它包含项目名称的ASCII码的字符串的地址。当查询项目的属性时，该成员变量将用来接收查询返回的项目的名称。
cchTextMax | 仅当您用来查询项目的属性时才需要使用该值，这时它包含上一个成员变量的大小。
iImage | 图标在列表视图中的图象链表中的索引号。
lParam | 用户定义的值，当您给项目排序时使用。当您告诉列表视图对项目排序时，列表视图将成对地比较项目。 它将会把两个项目的lParam的值传给您，这样您就可以进行比较先列出那一个了。如果您现在还不太明白的话，没有系，我们稍后还要讲关于排序的问题。

现在让我们来总结想列表控件中插入项目/子项目的步骤：

  1. 定义一个LV_ITEM型的结构体变量。
  2. 给该变量赋给合适的值
  3. 如果要插入一个项目，就向列表视图控件发送LVM_INSERTITEM值。 如果要插入一个子项目，发送LVM_SETITEM。如果您不明白项目和子项目之间的关系的话，可能会有一些疑惑。子项目仅是项目的属性而已，也就是说您可以插入一个项目但是不能插入一个子项目。所以添加一个子项目十只能发送LVM_SETITEM消息而不能发送LVM_INSERTITEM消息。

列表视图控件的消息/通知

既然您知道了如何创建和往其中添加内容，下一步就是如何和它通讯。列表视图控件和它的父窗口之间的通讯是通过消息/通知来进行的。父窗口通过发送消息来控制列表视图控件，列表视图控件通过发送WM_NOTIFY消息来通知它的父窗口。这一点和其它的通用控件没有什么不同。

排序项目/子项目

您可以在调用CreateWindowEx函数时指定LVS_SORTASCENDING 或 LVS_SORTDESCENDING风格来指定缺省的排序方式。这两种风格仅仅排序项目的名称。如果想要排序项目的其它属性，您可以通过发送LVM_SORTITEMS消息来完成

LVM_SORTITEMS
wParam = lParamSort
lParam = pCompareFunction

lParamSort 用户定义的值，该值将传递给用来比较的函数。
pCompareFunction 用户定义的用来比较排序的函数的地址。该函数的原型如下：

CompareFunc proto lParam1:DWORD, lParam2:DWORD, lParamSort:DWORD

lParam1 和 lParam2 是 LV_ITEM型的结构体中的成员变量lParam的值。
lParamSort 是发送LVM_SORTITEMS消息时参数wParam中的值

当列表视图控件接收到LVM_SORTITEMS消息时，当需要比较项目时它会调用在lParam中指定的比较函数。比较函数将决定那一个项目排在前面。方法很简单：如果函数返回一个负值，由（lParam代表的）第一个项目排在前，如果返回正值，第二个项目排在前。如果相等，必须返回0 。

真正使得该方法能够运行的是LV_ITEM型结构体中的成员变量lParam值。当您需要排序时（譬如当您点击列的标题条时），您需要考虑好排序方案。在本例中，我们把项目的索引放到该成员变量中，这样我们可以通过发送LVM_GETITEM消息来得到项目的其它信息。注意：当项目重排序后，它们的索引也就变了。所以当重排序后，我需要在lParam参数中反应出新的索引。如果您想在用户点击列的标题条时重新排序，您需要在您的窗口过程函数中处理LVN_COLUMNCLICK通知消息。LVN_COLUMNCLICK消息是随同WM_NOTIFY消息一起发送的。

例子：

该例子创建了一个列表视图控件，并在其中显示了当前文件夹中的文件大小和文件名。缺省的视图是报告方式的，如果您点击列标题条，标题将按升/降序重新排列。您可以通过菜单选择不同的显示方式（大图标、小图标等）。当您双击一个项目时，项目的名称将显示在一个对话框中。

.386
.model flat,stdcall
option casemap:none
include \masm32\include\windows.inc
include \masm32\include\user32.inc
include \masm32\include\kernel32.inc
include \masm32\include\comctl32.inc
includelib \masm32\lib\comctl32.lib
includelib \masm32\lib\user32.lib
includelib \masm32\lib\kernel32.lib

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

IDM_MAINMENU equ 10000
IDM_ICON equ LVS_ICON
IDM_SMALLICON equ LVS_SMALLICON
IDM_LIST equ LVS_LIST
IDM_REPORT equ LVS_REPORT

RGB macro red,green,blue
xor eax,eax
mov ah,blue
shl eax,8
mov ah,green
mov al,red
endm

.data
ClassName db "ListViewWinClass",0
AppName db "Testing a ListView Control",0
ListViewClassName db "SysListView32",0
Heading1 db "Filename",0
Heading2 db "Size",0
FileNamePattern db "*.*",0
FileNameSortOrder dd 0
SizeSortOrder dd 0
template db "%lu",0

.data?
hInstance HINSTANCE ?
hList dd ?
hMenu dd ?

.code
start:
invoke GetModuleHandle, NULL
mov hInstance,eax
invoke WinMain, hInstance,NULL, NULL, SW_SHOWDEFAULT
invoke ExitProcess,eax
invoke InitCommonControls
WinMain proc hInst:HINSTANCE,hPrevInst:HINSTANCE,CmdLine:LPSTR,CmdShow:DWORD
LOCAL wc:WNDCLASSEX
LOCAL msg:MSG
LOCAL hwnd:HWND

mov wc.cbSize,SIZEOF WNDCLASSEX
mov wc.style, NULL
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,IDM_MAINMENU
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 wc
invoke CreateWindowEx,NULL,ADDR ClassName,ADDR AppName, WS_OVERLAPPEDWINDOW,CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, NULL, NULL, hInst,NULL
mov hwnd,eax
invoke ShowWindow, hwnd,SW_SHOWNORMAL
invoke UpdateWindow, hwnd
.while TRUE
invoke GetMessage, ADDR msg,NULL,0,0
.break .if (!eax)
invoke TranslateMessage, ADDR msg
invoke DispatchMessage, ADDR msg
.endw
mov eax,msg.wParam
ret
WinMain endp

InsertColumn proc
LOCAL lvc:LV_COLUMN

mov lvc.imask,LVCF_TEXT+LVCF_WIDTH
mov lvc.pszText,offset Heading1
mov lvc.lx,150
invoke SendMessage,hList, LVM_INSERTCOLUMN, 0, addr lvc
or lvc.imask,LVCF_FMT
mov lvc.fmt,LVCFMT_RIGHT
mov lvc.pszText,offset Heading2
mov lvc.lx,100
invoke SendMessage,hList, LVM_INSERTCOLUMN, 1 ,addr lvc
ret
InsertColumn endp

ShowFileInfo proc uses edi row:DWORD, lpFind:DWORD
LOCAL lvi:LV_ITEM
LOCAL buffer[20]:BYTE
mov edi,lpFind
assume edi:ptr WIN32_FIND_DATA
mov lvi.imask,LVIF_TEXT+LVIF_PARAM
push row
pop lvi.iItem
mov lvi.iSubItem,0
lea eax,[edi].cFileName
mov lvi.pszText,eax
push row
pop lvi.lParam
invoke SendMessage,hList, LVM_INSERTITEM,0, addr lvi
mov lvi.imask,LVIF_TEXT
inc lvi.iSubItem
invoke wsprintf,addr buffer, addr template,[edi].nFileSizeLow
lea eax,buffer
mov lvi.pszText,eax
invoke SendMessage,hList,LVM_SETITEM, 0,addr lvi
assume edi:nothing
ret
ShowFileInfo endp

FillFileInfo proc uses edi
LOCAL finddata:WIN32_FIND_DATA
LOCAL FHandle:DWORD

invoke FindFirstFile,addr FileNamePattern,addr finddata
.if eax!=INVALID_HANDLE_VALUE
mov FHandle,eax
xor edi,edi
.while eax!=0
test finddata.dwFileAttributes,FILE_ATTRIBUTE_DIRECTORY
.if ZERO?
invoke ShowFileInfo,edi, addr finddata
inc edi
.endif
invoke FindNextFile,FHandle,addr finddata
.endw
invoke FindClose,FHandle
.endif
ret
FillFileInfo endp

String2Dword proc uses ecx edi edx esi String:DWORD
LOCAL Result:DWORD

mov Result,0
mov edi,String
invoke lstrlen,String
.while eax!=0
xor edx,edx
mov dl,byte ptr [edi]
sub dl,"0"
mov esi,eax
dec esi
push eax
mov eax,edx
push ebx
mov ebx,10
.while esi > 0
mul ebx
dec esi
.endw
pop ebx
add Result,eax
pop eax
inc edi
dec eax
.endw
mov eax,Result
ret
String2Dword endp

CompareFunc proc uses edi lParam1:DWORD, lParam2:DWORD, SortType:DWORD
LOCAL buffer[256]:BYTE
LOCAL buffer1[256]:BYTE
LOCAL lvi:LV_ITEM

mov lvi.imask,LVIF_TEXT
lea eax,buffer
mov lvi.pszText,eax
mov lvi.cchTextMax,256
.if SortType==1
mov lvi.iSubItem,1
invoke SendMessage,hList,LVM_GETITEMTEXT,lParam1,addr lvi
invoke String2Dword,addr buffer
mov edi,eax
invoke SendMessage,hList,LVM_GETITEMTEXT,lParam2,addr lvi
invoke String2Dword,addr buffer
sub edi,eax
mov eax,edi
.elseif SortType==2
mov lvi.iSubItem,1
invoke SendMessage,hList,LVM_GETITEMTEXT,lParam1,addr lvi
invoke String2Dword,addr buffer
mov edi,eax
invoke SendMessage,hList,LVM_GETITEMTEXT,lParam2,addr lvi
invoke String2Dword,addr buffer
sub eax,edi
.elseif SortType==3
mov lvi.iSubItem,0
invoke SendMessage,hList,LVM_GETITEMTEXT,lParam1,addr lvi
invoke lstrcpy,addr buffer1,addr buffer
invoke SendMessage,hList,LVM_GETITEMTEXT,lParam2,addr lvi
invoke lstrcmpi,addr buffer1,addr buffer
.else
mov lvi.iSubItem,0
invoke SendMessage,hList,LVM_GETITEMTEXT,lParam1,addr lvi
invoke lstrcpy,addr buffer1,addr buffer
invoke SendMessage,hList,LVM_GETITEMTEXT,lParam2,addr lvi
invoke lstrcmpi,addr buffer,addr buffer1
.endif
ret
CompareFunc endp

UpdatelParam proc uses edi
LOCAL lvi:LV_ITEM

invoke SendMessage,hList, LVM_GETITEMCOUNT,0,0
mov edi,eax
mov lvi.imask,LVIF_PARAM
mov lvi.iSubItem,0
mov lvi.iItem,0
.while edi>0
push lvi.iItem
pop lvi.lParam
invoke SendMessage,hList, LVM_SETITEM,0,addr lvi
inc lvi.iItem
dec edi
.endw
ret
UpdatelParam endp

ShowCurrentFocus proc
LOCAL lvi:LV_ITEM
LOCAL buffer[256]:BYTE

invoke SendMessage,hList,LVM_GETNEXTITEM,-1, LVNI_FOCUSED
mov lvi.iItem,eax
mov lvi.iSubItem,0
mov lvi.imask,LVIF_TEXT
lea eax,buffer
mov lvi.pszText,eax
mov lvi.cchTextMax,256
invoke SendMessage,hList,LVM_GETITEM,0,addr lvi
invoke MessageBox,0, addr buffer,addr AppName,MB_OK
ret
ShowCurrentFocus endp

WndProc proc hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM
.if uMsg==WM_CREATE
invoke CreateWindowEx, NULL, addr ListViewClassName, NULL, LVS_REPORT+WS_CHILD+WS_VISIBLE, 0,0,0,0,hWnd, NULL, hInstance, NULL
mov hList, eax
invoke InsertColumn
invoke FillFileInfo
RGB 255,255,255
invoke SendMessage,hList,LVM_SETTEXTCOLOR,0,eax
RGB 0,0,0
invoke SendMessage,hList,LVM_SETBKCOLOR,0,eax
RGB 0,0,0
invoke SendMessage,hList,LVM_SETTEXTBKCOLOR,0,eax
invoke GetMenu,hWnd
mov hMenu,eax
invoke CheckMenuRadioItem,hMenu,IDM_ICON,IDM_LIST, IDM_REPORT,MF_CHECKED
.elseif uMsg==WM_COMMAND
.if lParam==0
invoke GetWindowLong,hList,GWL_STYLE
and eax,not LVS_TYPEMASK
mov edx,wParam
and edx,0FFFFh
push edx
or eax,edx
invoke SetWindowLong,hList,GWL_STYLE,eax
pop edx
invoke CheckMenuRadioItem,hMenu,IDM_ICON,IDM_LIST, edx,MF_CHECKED
.endif
.elseif uMsg==WM_NOTIFY
push edi
mov edi,lParam
assume edi:ptr NMHDR
mov eax,[edi].hwndFrom
.if eax==hList
.if [edi].code==LVN_COLUMNCLICK
assume edi:ptr NM_LISTVIEW
.if [edi].iSubItem==1
.if SizeSortOrder==0 || SizeSortOrder==2
invoke SendMessage,hList,LVM_SORTITEMS,1,addr CompareFunc
invoke UpdatelParam
mov SizeSortOrder,1
.else
invoke SendMessage,hList,LVM_SORTITEMS,2,addr CompareFunc
invoke UpdatelParam
mov SizeSortOrder,2
.endif
.else
.if FileNameSortOrder==0 || FileNameSortOrder==4
invoke SendMessage,hList,LVM_SORTITEMS,3,addr CompareFunc
invoke UpdatelParam
mov FileNameSortOrder,3
.else
invoke SendMessage,hList,LVM_SORTITEMS,4,addr CompareFunc
invoke UpdatelParam
mov FileNameSortOrder,4
.endif
.endif
assume edi:ptr NMHDR
.elseif [edi].code==NM_DBLCLK
invoke ShowCurrentFocus
.endif
.endif
pop edi
.elseif uMsg==WM_SIZE
mov eax,lParam
mov edx,eax
and eax,0ffffh
shr edx,16
invoke MoveWindow,hList, 0, 0, eax,edx,TRUE
.elseif uMsg==WM_DESTROY
invoke PostQuitMessage,NULL
.else
invoke DefWindowProc,hWnd,uMsg,wParam,lParam
ret
.endif
xor eax,eax
ret
WndProc endp
end start

分析：

当主窗口创建后要做的第一件事是创建一个列表视图控件应用程序。

.if uMsg==WM_CREATE
invoke CreateWindowEx, NULL, addr ListViewClassName, NULL, LVS_REPORT+WS_CHILD+WS_VISIBLE, 0,0,0,0,hWnd, NULL, hInstance, NULL
mov hList, eax

我们调用CreateWindowEx来创建窗口，并把窗口类的名称“SysListView32”传给它。缺省的显示方式是报告方式，因为您指定了LVS_REPORT标志作为它的风格。

invoke InsertColumn

创建列表视图控件后，我们向其中插入列。

LOCAL lvc:LV_COLUMN

mov lvc.imask,LVCF_TEXT+LVCF_WIDTH
mov lvc.pszText,offset Heading1
mov lvc.lx,150
invoke SendMessage,hList, LVM_INSERTCOLUMN, 0, addr lvc

我们指定第一列的宽度和列的标题条，为了在该列中显示文件的名称，我们需要在LV_COLUMN 型结构体变量的成员变量iMask中设定标志位LVCF_TEXT 或 LVCF_WIDTH。我们设定pszText为列标题条文本字符串的值，lx设定为列的宽度（以像素点为单位）。然后我们发送LVM_INSERTCOLUMN消息给列表视图控件，并把该结构体变量传递给它。

or lvc.imask,LVCF_FMT
mov lvc.fmt,LVCFMT_RIGHT

插入完第一列后，我们再插入第二列，单击该列的标题条可以按文件的大小排序。因为我们需要右对齐文本，我们需要在成员变量fmt中指定标志位LVCFMT_RIGHT。我们还必须在成员变量iMask中除了标志位LVCF_TEXT 和 LVCF_WIDTH外还需要指定标志位LVCF_FMT。

mov lvc.pszText,offset Heading2
mov lvc.lx,100
invoke SendMessage,hList, LVM_INSERTCOLUMN, 1 ,addr lvc

剩余的代码比较简单。在pszText中放入文本字符串的地址，在lx中放入列的宽度。然后发送消息LVM_INSERTCOLUMN 给列表视图控件，在参数中同时传递列号和结构体变量的地址。

当插入完列后，我们向列表控件中加入项目。

invoke FillFileInfo

FillFileInfo 的代码如下：

FillFileInfo proc uses edi
LOCAL finddata:WIN32_FIND_DATA
LOCAL FHandle:DWORD

invoke FindFirstFile,addr FileNamePattern,addr finddata

我们调用FindFirstFile来得到第一个符合搜索标准的的文件的信息。FindFirstFile函数的原型如下

FindFirstFile proto pFileName:DWORD, pWin32_Find_Data:DWORD

pFileName 是用来匹配搜索的文件名的地址。该字符串包含了通配符。在我们的例子中是*.*，这样会搜索当前文件夹中所有的文件。
pWin32_Find_Data 是WIN32_FIND_DATA 型的结构体变量的地址，WIN32_FIND_DATA型的结构体变量将用来保存返回的文件的信息。

如果没有找到匹配的文件，该函数将在eax中返回INVALID_HANDLE_VALUE 。否则将返回一个搜索句柄，您可以用该句柄在FindNextFile函数中来搜索下一个符合条件的文件。

.if eax!=INVALID_HANDLE_VALUE
mov FHandle,eax
xor edi,edi

如果找到了一个文件，我们在一个变量中保存搜索句柄，并把寄存器edi清零，该寄存器将用作项目的索引号。

.while eax!=0
test finddata.dwFileAttributes,FILE_ATTRIBUTE_DIRECTORY
.if ZERO?

在本课中，我们将不处理文件夹，所以我们检查dwFileAttributes成员变量的值是否有FILE_ATTRIBUTE_DIRECTORY 标志，如果有，我们就忽略掉它，然后调用FindNextFile。

invoke ShowFileInfo,edi, addr finddata
inc edi
.endif
invoke FindNextFile,FHandle,addr finddata
.endw

我们调用ShowFileInfo函数包文件的名称和大小信息加到列表视图控件中去。然后让edi寄存器加一来增加项目的行号。最后我们调用FindNextFile函数在当前文件夹中继续搜索文件一直到该函数返回0为止（这意味着没有可供搜索的文件了）。

invoke FindClose,FHandle
.endif
ret
FillFileInfo endp

当前文件夹中的文件枚举完毕后，我们必须关闭搜索句柄。

先在我们看一下ShowFileInfo函数。该函数由两个参数，一个是项目的索引号（也即行号），另一个是WIN32_FIND_DATA型结构体变量的地址。

ShowFileInfo proc uses edi row:DWORD, lpFind:DWORD
LOCAL lvi:LV_ITEM
LOCAL buffer[20]:BYTE
mov edi,lpFind
assume edi:ptr WIN32_FIND_DATA

把WIN32_FIND_DATA 型结构体变量的值放到寄存器edi中。

mov lvi.imask,LVIF_TEXT+LVIF_PARAM
push row
pop lvi.iItem
mov lvi.iSubItem,0

我们将传递项目的名称和lParam的值，所以我们在iMask中放入标志位LVIF_TEXT 和LVIF_PARAM。接下来我们在iItem中放入传递进来的行号，另外由于这是主项目我们必须设置iSubItem的值等于0。

lea eax,[edi].cFileName
mov lvi.pszText,eax
push row
pop lvi.lParam

我们现在要把标签字符串的地址，在这里也就是WIN32_FIND_DATA 型结构体变量中的文件的名称放到pszText中。由于我们要完成对项目的排序，所以必须设置lParam的值，我把它设成行号值，这样我们可以根据索引值来查询项目。

invoke SendMessage,hList, LVM_INSERTITEM,0, addr lvi

设置完所有LV_ITEM型变量中的值后，我们发送LVM_INSERTITEM消息给列表视图控件来把项目插入到其中。

mov lvi.imask,LVIF_TEXT
inc lvi.iSubItem
invoke wsprintf,addr buffer, addr template,[edi].nFileSizeLow
lea eax,buffer
mov lvi.pszText,eax

我们将把子项目插入到第二列。一个子项目只能有一个标签。这样我们在iMask中指定LVIF_TEXT标志位。接着我们指定子项目所在的列，本例中我们通过将iSubItem加一使得该值等于1。标签值是文件的大小，为了转换成文本我们调用wsprintf函数，然后把文本的地址放到pszText中去。

invoke SendMessage,hList,LVM_SETITEM, 0,addr lvi
assume edi:nothing
ret
ShowFileInfo endp

当LV_ITEM型变量中的值设定好之后，我们向列表视图控件发送LVM_SETITEM消息，并一同把LV_ITEM变量的地址传过去。注意：发送的消息是LV_ITEM而不是LVM_INSERTITEM，因为我们插入的是子项目，子项目不是真正的项目而是主项目的属性。所以我们这时是在设定项目的属性，而不是加入一个项目。

当所有的项目都插入到列表视图控件后，我们设定它的文本和背景颜色。

RGB 255,255,255
invoke SendMessage,hList,LVM_SETTEXTCOLOR,0,eax
RGB 0,0,0
invoke SendMessage,hList,LVM_SETBKCOLOR,0,eax
RGB 0,0,0
invoke SendMessage,hList,LVM_SETTEXTBKCOLOR,0,eax

我们用RGB（R---Red G---Green B---Blue）来把三色转换并放到eax中。我们通过发送LVM_SETTEXTCOLOR 和 LVM_SETTEXTBKCOLOR 消息来设定文本的前景和背景色。

invoke GetMenu,hWnd
mov hMenu,eax
invoke CheckMenuRadioItem,hMenu,IDM_ICON,IDM_LIST, IDM_REPORT,MF_CHECKED

我们将让用户通过菜单来选择它想要的显示方式。这样我们必须先得到菜单的句柄。我了让用户跟踪当前的视图，我们在菜单中放入一组单选按钮。我们可以调用CheckMenuRadioItem函数，该函数将把一个单选按钮放到一个菜单项前。

注意我们创建列表视图控件时把它的宽度和高度都设成为0。当父窗口改变大小时，它将同时改变大小。这样我们可以让列表视图总是随着主窗口改变。子我们的例子中，我们让列表视图填充整个客户区。

.elseif uMsg==WM_SIZE
mov eax,lParam
mov edx,eax
and eax,0ffffh
shr edx,16
invoke MoveWindow,hList, 0, 0, eax,edx,TRUE

当父窗口接收到了WM_SIZE消息后，lParam的底字部分包含了客户区新的宽和高。让后我们调用MoveWindow来改变列表视图控件的大小使得它覆盖整个的客户区。

当用户通过菜单选择了一种选择方式，我们必须相应地改变列表视图中的显示方式。我们调用SetWindowLong函数来设定新的风格。

.elseif uMsg==WM_COMMAND
.if lParam==0
invoke GetWindowLong,hList,GWL_STYLE
and eax,not LVS_TYPEMASK

首先得到当前的风格，然后清除旧的风格。LVS_TYPEMASK 是LVS_ICON+LVS_SMALLICON+LVS_LIST+LVS_REPORT四种风格的集合。这样当我们用当前的风格“与”“not LVS_TYPEMASK”就等于清除了当前的显示风格。

在设计菜单时，我们使用了一些小技巧。我们包显示风格的常数串当作菜单的ID号。

IDM_ICON equ LVS_ICON
IDM_SMALLICON equ LVS_SMALLICON
IDM_LIST equ LVS_LIST
IDM_REPORT equ LVS_REPORT

这样当父窗口接收到WM_COMMAND消息时，希望显示的风格值会当成菜单的ID号传递过来。

mov edx,wParam
and edx,0FFFFh

在wParam中的低字部分是欲显示的风格，我们所需要做的只是把高字部分清0。

push edx
or eax,edx

我们把希望显示的风格加到列表视图的风格中去（已经去除了旧的风格）。

invoke SetWindowLong,hList,GWL_STYLE,eax

调用SetWindowLong函数来设定新的风格。

pop edx
invoke CheckMenuRadioItem,hMenu,IDM_ICON,IDM_LIST, edx,MF_CHECKED
.endif

我们需要在被选择的显示方式前放入单选按钮。如果要排序，我们必须处理WM_NOTIFY消息。

.elseif uMsg==WM_NOTIFY
push edi
mov edi,lParam
assume edi:ptr NMHDR
mov eax,[edi].hwndFrom
.if eax==hList

当我们接收到了WM_NOTIFY 消息后，lParam包含了指向NMHDR型结构体变量的指针。我们通过把列表视图控件的值和NMHDR型结构体变量中的hwndFrom成员变量的值比较来判断，如果相等的话我们就可以确定消息是列表视图控件发送的。

.if [edi].code==LVN_COLUMNCLICK
assume edi:ptr NM_LISTVIEW

如果通知消息是列表视图控件发送的，我们检测该消息是否是LVN_COLUMNCLICK。如果是，它意味着用户点击了列标题条。在接收到LVN_COLUMNCLICK消息后，我们假设lParam参数包含NM_LISTVIEW型结构体变量的指针，NM_LISTVIEW型结构体是NMHDR型结构体的扩展。我们需要知道用户单击了那一列，在iSubItem中的值即是列号，列的编号是从0开始的。

.if [edi].iSubItem==1
.if SizeSortOrder==0 || SizeSortOrder==2

在这里iSubItem的值是1，它表示用户点击的是第二列，即文件的大小。我们用状态变量来保持当前的排序顺序。0代表不用排序，1代表升序，2代表降序。如果该列中的项目/子项目以前没有排序或为降序，我们就把它设成升序。

invoke SendMessage,hList,LVM_SORTITEMS,1,addr CompareFunc

我们发送消息LVM_SORTITEMS给列表视图控件，在wParam中传递1，在lParam中传递比较函数的参数。注意wParam中的值是用户定义的，用户可以按自己的需要来解释，这里我们把它用作排序的方法。我们先来看看比较函数：

CompareFunc proc uses edi lParam1:DWORD, lParam2:DWORD, SortType:DWORD
LOCAL buffer[256]:BYTE
LOCAL buffer1[256]:BYTE
LOCAL lvi:LV_ITEM

mov lvi.imask,LVIF_TEXT
lea eax,buffer
mov lvi.pszText,eax
mov lvi.cchTextMax,256

列表视图控件将传递需要比较的两个项目的lParam（LV_ITEM型结构体变量的成员变量）比较函数。您还记得吗？我们在lParam中放置了醒目的索引号。这样我们利用这些索引号查询列表视图来得到项目信息。我们需要的消息是项目/子项目的标签文本。为此我们准备好LV_ITEM 型结构体变量并在iMask中设置标志位LVIF_TEXT ，在pszText中设置缓冲区的地址，在cchTextMax中设置缓冲区的大小。

.if SortType==1
mov lvi.iSubItem,1
invoke SendMessage,hList,LVM_GETITEMTEXT,lParam1,addr lvi

如果SortType的值为1或2，我们知道点击了那一列，1代表根据文件的大小按升序排列所有的项目。2的意思相反。这样我们指定iSubItem为1（代表文件大小列）然后发送LVM_GETITEMTEXT 消息给列表视图控件来得到在项目的标签文本串。

invoke String2Dword,addr buffer
mov edi,eax

调用子定义的String2Dword函数来把字符串转换成一个DWORD值。它将在eax中返回转换后的值，我们把它保存在edi中以便以后比较用。

invoke SendMessage,hList,LVM_GETITEMTEXT,lParam2,addr lvi
invoke String2Dword,addr buffer
sub edi,eax
mov eax,edi

对lParam2 中的值做同样的操作。当我们得到了两个文件的大小后，就可以比较它们了。比较的规则如下：

  • 如果第一个项目放在前面，在eax中返回一个负值
  • 如果第二个项目放在前面，在eax中返回一个正值
  • 如果相等，在eax中返回0

在我们这里，我们想按升序排列，所以我们只要简单地将第二个项目的文件大小减去第一个项目的文件大小，然后返回放在eax中的值。

.elseif SortType==3
mov lvi.iSubItem,0
invoke SendMessage,hList,LVM_GETITEMTEXT,lParam1,addr lvi
invoke lstrcpy,addr buffer1,addr buffer
invoke SendMessage,hList,LVM_GETITEMTEXT,lParam2,addr lvi
invoke lstrcmpi,addr buffer1,addr buffer

当用户点击文件名字列时，我们必须比较文件的名字。我们先得到文件的名字，然后调用lstrcmpi函数来比较，然后只要简单返回lstrcmpi的值，因为该函数比较使用的规则和我们的相同。

当项目排序后，我们调用UpdateParam函数来更新所有项目的lParam的值来反应出最新的改变。

invoke UpdatelParam
mov SizeSortOrder,1

该函数简单地枚举列表视图中所有的项目并且把它们lParam更新成项目的索引号。

.elseif [edi].code==NM_DBLCLK
invoke ShowCurrentFocus
.endif

如果用户双击某个项目时，我们将显示一个消息框，上面有该项目的有关标签值。我们必须检查NMHDR 中的code值是否是 NM_DBLCLK。如果是，我们就得到它的标签值并显示在一个消息框中。

ShowCurrentFocus proc
LOCAL lvi:LV_ITEM
LOCAL buffer[256]:BYTE

invoke SendMessage,hList,LVM_GETNEXTITEM,-1, LVNI_FOCUSED

我们是增么怎么知道某个项目被双击的呢？当单击或双击某个项目时，它的状态被设成“焦点”。即使有多个项目被选中，也仅有一个项目有焦点。我们的工作就是去找到那个有焦点的项目。我们发送LVM_GETNEXTITEM消息给列表视图控件，在lParam中指定期望的状态。如果wParam中时-1的话，表示要搜索所有的项目。有焦点的项目第索引号在eax中返回。

mov lvi.iItem,eax
mov lvi.iSubItem,0
mov lvi.imask,LVIF_TEXT
lea eax,buffer
mov lvi.pszText,eax
mov lvi.cchTextMax,256
invoke SendMessage,hList,LVM_GETITEM,0,addr lvi

发送LVM_GETITEM消息来得到标签。

invoke MessageBox,0, addr buffer,addr AppName,MB_OK

最后我们在一个消息框中显示标签。

如果想在列表视图控件中显示图标，您可以阅读关于树型视图控件的课程。它们的步骤基本上是一样的。

翻译：Lxx.阿龙，校对：LuoYunBin's Win32 ASM Page， http://asm.yeah.net/


Iczelion's win32 asm tutorial

教程32:多文档界面(MDI)

本教程告诉你怎样创建MDI应用程序.事实上并不是很困难.下载例子.

理论:

多文档界面(MDI)是同一时刻处理多个文档的应用程序的一个规范. 你很熟悉记事本.它是单文档界面(SDI)的一个例子.记事本在一个时候只能处理一个文档.假如你希望打开另一个文档,你首先必须关闭你前面打开的那一个.你可以想象这有多麻烦. 和Microsoft Word相比:Word可以随心所欲的在同一时刻打开任意多个文档,而且可以让用户选择使用哪一个文档.Microsoft Word 是多文档界面(MDI)的一个例子.

MDI应用程序有几个显著的特征:我列举其中的一些:

  • 有主窗口,在客户区可以有多个子窗口.所有的子窗口都位于客户区.
  • 最小化一个子窗口,它最小化到了主窗口的客户区的左下角.
  • 最大化一个子窗口,它的标题和主窗口的标题合并到了一起.
  • 你可以通过按Ctrl+F4键来关闭子窗口,还可以通过按Ctrl+Tab键在子窗口之间来切换.

包含子窗口的主窗口被称为框架窗口.主窗口的客户区是子窗口活动的地方,因此有了'框架'这个名字.主窗口的任务要比普通窗口精细一些,因为它需要为MDI处理一些协调工作.

为了在你的客户区控制任意多个数目的子窗口,你需要一个特殊的窗口:客户窗口.你可以把客户窗口看成是覆盖框架窗口的整个客户区的一个透明的窗口.客户窗口才是所有MDI子窗口的实际的父亲.客户窗口是MDI子窗口的真实的监督者.

框架窗口

|

客户窗口

|

|

|

|

|

|

MDI 自窗口1

MDI 自窗口 2

MDI自窗口3

MDI自窗口4

MDI 自窗口 n

图1.一个MDI应用程序的层次结构

创建框架窗口

现在我们将注意力放到细节上来.首先你需要创建框架窗口. 创建框架窗口的方法和普通窗口是相同的:调用CreateWindowEx. 和普通窗口相比,有两个主要的不同.

第一个不同是你必须调用DefFrameProc来处理你的窗口不想处理的窗口信息而不是调用DefWindowProc.这是让Windows为你作的保持一个MDI应用程序的垃圾任务的一个方法.假如你忘记使用DefFrameProc,你的应用程序将不具有MDI的功能. DefFrameProc具有下列语法:

DefFrameProc proc hwndFrame:DWORD,
hwndClient:DWORD,
 uMsg:DWORD,
wParam:DWORD,
lParam:DWORD

假如你将 DefFrameProc 和 DefWindowProc作一个对比,你将会注意到它们之间的唯一的不同在于DefFrameProc有5个参数,而DefWindowProc只有4个.这个增加的参数是客户窗口的句柄.这个句柄是必须的,有了它Windows才可以发送MDI-相关的消息给客户窗口.

第二个不同是你必须在你的框架窗口的消息循环中调用 TranslateMDISysAccel .假如你希望Windows为你处理MDI相关的加速键,比如Ctrl+F4,Ctrl+Tab,那么这是必须的.它具有下列语法:

TranslateMDISysAccel proc hwndClient:DWORD,
                                                 lpMsg:DWORD

第一个参数是客户窗口的句柄.对此你应该不会觉得惊讶.因为客户窗口是所有MDI子窗口的父亲. 第二个参数是你通过调用GetMessage获得的MSG框架的地址. 我们的想法是传递MSG结构给客户窗口,这样客户窗口可以检测在MSG结构中所包含的MDI相关的按键是不是按下去了.假如是的话, 客户窗口处理这个信息,然后返回一个非零值,否则返回一个假值..

创建框架窗口的步骤总结如下:

  1. 像平常一样填写 WNDCLASSEX 结构.
  2. 通过调用 RegisterClassEx注册框架窗口类.
  3. 通过调用CreateWindowEx创建框架窗口.
  4. 在消息循环中调用TranslateMDISysAccel.
  5. 在窗口过程中,将未处理的消息传递给 DefFrameProc 而不是DefWindowProc.

创建客户窗口

现在我们有了框架窗口,我们可以开始创建客户窗口了. 客户窗口类是由Windows预先注册的. 类的名称为"MDICLIENT". 你同样也需要将 CLIENTCREATESTRUCT 的地址传递给 CreateWindowEx. 这个结构具有以下定义:

CLIENTCREATESTRUCT struct
        hWindowMenu    dd ?
        idFirstChild    dd ?
CLIENTCREATESTRUCT ends

hWindowMenu 是子菜单的句柄,这个子菜单显示Windows将要添加的MDI子窗口名称列表. 我们需要对这一功能进行一点解释.假如你以前曾经使用过类似Microsoft Word的MDI 应用程序,你将会注意到有一个名称为"窗口"的子菜单. 这个菜单一旦激活的话,将会在底部显示出和窗口管理相关的各种各样的菜单项, 还有当前打开的MDI子窗口的列表. 这个列表是由Windows自己内部保持的. 你不需要为它作任何特殊的事情. 仅仅只在需要在hWindowMenu 中传递你所希望显示列表的子菜单的句柄, Windows 将会处理剩下的事情. 注意这个子菜单可以是任何的子菜单:它并不一定要是名称为"窗口"的子菜单. 重要的是你应该传递你希望显示窗口列表的子菜单的句柄. 假如你不想要这个列表,你就给 hWindowMenu 赋一个NULL的值就行了. 你可以通过调用GetSubMenu来获得子菜单的句柄.

idFirstChild 是第一个MDI子窗口的标识号. Windows为应用程序所创建的每一个新的MDI子窗口相应的增加标识号. 举个例子, 假如你传递100给这个域, 第一个MDI子窗口将会有一个值为100的标识符, 那么第二个MDI子窗口也就会有一个值为101的标识符, 如此这样下去. 当从窗口列表中选择MDI子窗口时, 被选择的MDI子窗口的标识符通过WM_COMMAND传递给框架窗口. 正常情况下,你将传递"未处理"的WM_COMMAND消息给DefFrameProc. 我用"未处理"这个词语,是因为窗口列表中的菜单项不是由你的应用程序创建的, 这样你的应用程序不知道它们的标识符,而且也没有它们的句柄. 这是MDI框架窗口又一个特殊的地方. 假如你有窗口列表的话,你必须像这样来修改你的WM_COMMAND句柄:

.elseif uMsg==WM_COMMAND
      .if lParam==0			; 这条消息是由菜单产生的
          mov eax,wParam
          .if ax==IDM_CASCADE
                 .....
          .elseif ax==IDM_TILEVERT
                .....
          .else
                invoke DefFrameProc, hwndFrame, hwndClient, uMsg,wParam, lParam
                ret
          .endif

一般来说,你可以忽略未处理的消息. 但是在MDI的情况下,如果你忽略它们, 当用户点击窗口列表中的一个MDI子窗口的名称时,,这个窗口不会被激活. 你需要将这些消息传递给DefFrameProc 这样它们才会得到适当的处理.

idFirstChild 赋值的注意之处: 你不能使用0. 你窗口列表将会表现的不正常. 举个例子, 即使某一个MDI子窗口被激活的话, 窗口列表中的这个MDI子窗口名字前的复选标记也不会显现. 我们可以选择一个安全的值,比如100或是一个比100大的值.

给 CLIENTCREATESTRUCT 结构赋值后,你可以通过调用 CreateWindowEx 用预先注册好的类名"MDICLIENT", 在lParam中传递CLIENTCREATESTRUCT结构的地址来创建客户窗口. 你同样需要在hWndParent参数中指定框架窗口的句柄, 这样Windows才可以知道框架窗口和客户窗口之间的父-子关系. 你可以使用的窗口风格有:WS_CHILD ,WS_VISIBLEHE WS_CLIPCHILDREN . 假如你忘了WS_VISIBLE的话, 即使MDI子窗口成功地创建了,你也看不到它们.

以下是创建客户窗口的步骤:

  1. 获取你所希望显示窗口列表的子菜单的句柄.
  2. 将这个菜单句柄的值和你希望作为第一个MDI子窗口标识符的值一起传送给CLIENTCREATESTRUCT 结构.
  3. 调用 CreateWindowEx 用类名"MDICLIENT" ,lParam参数为CLIENTCREATESTRUCT结构的地址,

创建MDI子窗口

现在我们既有了框架窗口,也有了客户窗口. 下一阶段可以开始创建MDI子窗口了.有两种方法:

  • 你可以发送 WM_MDICREATE消息给客户窗口,在wParam参数中传递类型MDICREATESTRUCT的结构的地址. 这是常用的也是最简单的MDI子窗口的创建方法.
    .data?
    mdicreate MDICREATESTRUCT <>
    ....
.code
    .....
    [fill the members of mdicreate]
    ......
    invoke SendMessage, hwndClient, WM_MDICREATE,addr mdicreate,0

    假如创建成功的话, SendMessage 将会返回新创建的MDI子窗口的句柄. 你并不需要保存这个句柄. 如果你需要的话, 你可以通过其它的方法来获得它. MDICREATESTRUCT有如下定义.

    MDICREATESTRUCT STRUCT
    szClass DWORD ?
    szTitle DWORD ?
    hOwner DWORD ?
    x DWORD ?
    y DWORD ?
    lx DWORD ?
    ly DWORD ?
    style DWORD ?
    lParam DWORD ?
    MDICREATESTRUCT ENDS

szClass | 你作为MDI自窗口模板的窗口类的地址
szTitle | 你希望出现在子窗口的标题栏的文本的地址
hOwner | 应用程序的例程句柄
x,y,lx,ly | 子窗口的左上角的坐标以及宽度和高度
style | 子窗口风格. 假若你用MDIS_ALLCHILDSTYLES创建子窗口的话,你可以使用任何窗口风格.
lParam | 一个应用程序定义的32位值. 这是在MDI窗口中共享值的一种方法. 如果你不需要它, 将它设为NULL.
  • 你可以调用 CreateMDIWindow. 这一个功能具有下列语法:
    CreateMDIWindow proto lpClassName:DWORD
                                           lpWindowName:DWORD
                                           dwStyle:DWORD
                                           x:DWORD
                                           y:DWORD
                                           nWidth:DWORD
                                           nHeight:DWORD
                                           hWndParent:DWORD
                                           hInstance:DWORD
                                           lParam:DWORD

如果你仔细地看一下这些参数, 你将会发现它们和MDICREATESTRUCT结构的成员是相同的, 除了 hWndParent.以外. 本质上它和你用WM_MDICREATE传送的参数数目是相同的. MDICREATESTRUCT不需要hWndParent 域, 因为你必须用Sendmessage传送整个结构给正确的子窗口. .

在这里,你也许会有一些问题: 我应该使用哪一种方法? 在这两者之间有什么区别? 答案如下:

WM_MDICREATE方法创建的MDI子窗口作为调用代码是同一个线程.这意味这假如这个应用程序只有一个主线程的话, 所有的MDI子窗口都在这个主线程中运行. 这并不是一个大的问题. 但是如果一个或是多个你的MDI子窗口执行一些较长的操作的话, 问题就来了. 想象一下你的整个的应用程序突然之间停止了,对任何事情都没有反应, 一直持续到MDI子窗口的操作结束.

这个问题正是CreateMDIWindow 设计了所要解决的. CreateMDIWindow 为每一个MDI子窗口创建了一个单独的线程. 这样假如一个MDI子窗口忙的话, 它不会拖累整个应用程序..

有关MDI子窗口的窗口过程还有一点需要说明的地方. 对于框架窗口, 你不能调用DefWindowProc来处理未处理的消息. 与之相反你必须在自窗口的窗口过程中使用DefMDIChildProc . 这个函数具有和DefWindowProc相同的参数.

除了WM_MDICREATE以外,还有其它的MDI相关的窗口消息. 列表如下:

WM_MDIACTIVATE | 这条消息由应用程序发送给客户窗口,告诉客户窗口激活所选择的MDI子窗口. 当客户窗口受到消息后, 它将激活所选择的MDI子窗口和发送WM_MDIACTIVATE消息给将被激活的子窗口和将变为非活动窗口的子窗口. 这条消息的用途是双方面的:应用程序可以用它来激活所希望的子窗口.同时它又可以被MDI子窗口本身用作活动/非活动窗口的指示器.举个例子,假如每一个MDI子窗口都有不同的菜单, 那么当它变为活动或是非活动窗口的时候,它可以利用这个机会来改变框架窗口的菜单
WM_MDICASCADE WM_MDITILE WM_MDIICONARRANGE | 这些消息处理如何排列MDI子窗口. 举个例子, 假如你希望MDI子窗口排列成层叠的样式,发送WM_MDICASCADE消息给客户窗口.
WM_MDIDESTROY | 发送这条消息给客户窗口来关闭一个MDI子窗口. 你应该使用这条消息而不是调用DestroyWindow 因为假如这个MDI子窗口最大化的话, th这条消息将会恢复框架窗口的标题. 假如你使用DestroyWindow, 框架窗口的标题将不会被恢复.
WM_MDIGETACTIVE | 发送这条消息检索当前活动MDI子窗口的句柄.
WM_MDIMAXIMIZE WM_MDIRESTORE | 发送 WM_MDIMAXIMIZE来最大化MDI子窗口和WM_MDIRESTORE来将它恢复成以前的状态. 对于这些操作总是使用这些消息. 假如你使用参数为SW_MAXIMIZE来调用ShowWindow时,MDI子窗口最大化并没有问题, 但是当你试图将它恢复成以前的状态时,问题就来了. 但是你可以用调用ShowWindow来最小化MDI子窗口.
WM_MDINEXT | 发送这条消息给客户窗口,根据wParam和lParam里相应的值来激活下一个或是前一个MDI子窗口.
WM_MDIREFRESHMENU | 发送这条消息给客户窗口来刷新框架窗口的菜单. 注意在发送了这条消息之后, 你必须调用DrawMenuBar 来更新菜单条.
WM_MDISETMENU | 发送这条消息给客户窗口来取代框架窗口的整个菜单或是窗口子菜单. 你必须使用这条消息而不是用SetMenu. 在发送了这条消息之后, 你必须调用DrawMenuBar来更新菜单条. 正常情况下当活动的MDI子窗口有它自己的菜单而且你希望用这个活动的子窗口自身的菜单来取代框架窗口的菜单时,你将使用这条消息.

我们将创建一个MDI应用程序的步骤回顾一遍.

  1. 注册窗口类, 既有框架窗口类也有MDI子窗口类.
  2. 调用CreateWindowEx创建框架窗口.
  3. 在消息循环中调用TranslateMDISysAccel 来处理MDI相关的加速键.
  4. 在框架窗口的窗口过程中, 调用DefFrameProc 处理所有你的代码没有处理的消息.
  5. 用预选定义好的窗口类名 "MDICLIENT"调用CreateWindowEx来创建客户窗口, 在lParam参数中传递CLIENTCREATESTRUCT结构的地址. 正常情况下,你可以用框架窗口过程中的WM_CREATE句柄来创建一个客户窗口.
  6. 相应的要创建MDI子窗口,你可以通过调用CreateMDIWindow 来发送WM_MDICREATE消息给客户窗口.
  7. 在MDI子窗口的窗口过程中, 我们把所有未处理的消息发送给传递给DefMDIChildProc.
  8. 假如某一条消息有它的MDI的版本,那我们就使用它的MDI版本. 举个例子, 我们使用WM_MDIDESTORY消息, 而不是使用DestroyWindow来关闭一个MDI子窗口.

例子:

.386
.model flat,stdcall
option casemap:none
include \masm32\include\windows.inc
include \masm32\include\user32.inc
include \masm32\include\kernel32.inc
includelib \masm32\lib\user32.lib
includelib \masm32\lib\kernel32.lib
WinMain proto :DWORD,:DWORD,:DWORD,:DWORD

.const
IDR_MAINMENU 	equ 101
IDR_CHILDMENU	equ 102
IDM_EXIT 		equ 40001
IDM_TILEHORZ	equ 40002
IDM_TILEVERT	equ 40003
IDM_CASCADE	equ 40004
IDM_NEW 		equ 40005
IDM_CLOSE	equ 40006

.data
ClassName 	db "MDIASMClass",0
MDIClientName	db "MDICLIENT",0
MDIChildClassName	db "Win32asmMDIChild",0
MDIChildTitle	db "MDI Child",0
AppName		db "Win32asm MDI Demo",0
ClosePromptMessage	db "Are you sure you want to close this window?",0

.data?
hInstance 	dd ?
hMainMenu 	dd ?
hwndClient 	dd ?
hChildMenu 	dd ?
mdicreate		MDICREATESTRUCT <>
hwndFrame 	dd ?

.code
start:
    invoke GetModuleHandle, NULL
    mov hInstance,eax
    invoke WinMain, hInstance,NULL,NULL, SW_SHOWDEFAULT
    invoke ExitProcess,eax

WinMain proc hInst:HINSTANCE,hPrevInst:HINSTANCE,CmdLine:LPSTR,CmdShow:DWORD
    LOCAL wc:WNDCLASSEX
    LOCAL msg:MSG
    ;=============================================
    ; 注册框架窗口类
    ;=============================================
    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_APPWORKSPACE
    mov wc.lpszMenuName,IDR_MAINMENU
    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 wc
    ;================================================
    ; 注册MDI子窗口类
    ;================================================
    mov wc.lpfnWndProc,offset ChildProc
    mov wc.hbrBackground,COLOR_WINDOW+1
    mov wc.lpszClassName,offset MDIChildClassName
    invoke RegisterClassEx,addr wc
    invoke CreateWindowEx,NULL,ADDR ClassName,ADDR AppName,\
            WS_OVERLAPPEDWINDOW or WS_CLIPCHILDREN,CW_USEDEFAULT,\
            CW_USEDEFAULT,CW_USEDEFAULT,CW_USEDEFAULT,NULL,0,\
            hInst,NULL
    mov hwndFrame,eax
    invoke LoadMenu,hInstance, IDR_CHILDMENU
    mov hChildMenu,eax
    invoke ShowWindow,hwndFrame,SW_SHOWNORMAL
    invoke UpdateWindow, hwndFrame
    .while TRUE
        invoke GetMessage,ADDR msg,NULL,0,0
        .break .if (!eax)
        invoke TranslateMDISysAccel,hwndClient,addr msg
        .if !eax
            invoke TranslateMessage, ADDR msg
            invoke DispatchMessage, ADDR msg
        .endif
    .endw
    invoke DestroyMenu, hChildMenu
    mov eax,msg.wParam
    ret
WinMain endp

WndProc proc hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM
    LOCAL ClientStruct:CLIENTCREATESTRUCT
    .if uMsg==WM_CREATE
        invoke GetMenu,hWnd
        mov hMainMenu,eax
        invoke GetSubMenu,hMainMenu,1
        mov ClientStruct.hWindowMenu,eax
        mov ClientStruct.idFirstChild,100
        INVOKE CreateWindowEx,NULL,ADDR MDIClientName,NULL,\
                WS_CHILD or WS_VISIBLE or WS_CLIPCHILDREN,CW_USEDEFAULT,\
                CW_USEDEFAULT,CW_USEDEFAULT,CW_USEDEFAULT,hWnd,NULL,\
                hInstance,addr ClientStruct
        mov hwndClient,eax
        ;=======================================
        ; 初始化MDICREATESTRUCT结构
        ;=======================================
        mov mdicreate.szClass,offset MDIChildClassName
        mov mdicreate.szTitle,offset MDIChildTitle
        push hInstance
        pop mdicreate.hOwner
        mov mdicreate.x,CW_USEDEFAULT
        mov mdicreate.y,CW_USEDEFAULT
        mov mdicreate.lx,CW_USEDEFAULT
        mov mdicreate.ly,CW_USEDEFAULT
    .elseif uMsg==WM_COMMAND
        .if lParam==0
            mov eax,wParam
            .if ax==IDM_EXIT
                invoke SendMessage,hWnd,WM_CLOSE,0,0
            .elseif ax==IDM_TILEHORZ
                invoke SendMessage,hwndClient,WM_MDITILE,MDITILE_HORIZONTAL,0
            .elseif ax==IDM_TILEVERT
                invoke SendMessage,hwndClient,WM_MDITILE,MDITILE_VERTICAL,0
            .elseif ax==IDM_CASCADE
                invoke SendMessage,hwndClient,WM_MDICASCADE,MDITILE_SKIPDISABLED,0
            .elseif ax==IDM_NEW
                invoke SendMessage,hwndClient,WM_MDICREATE,0,addr mdicreate
            .elseif ax==IDM_CLOSE
                invoke SendMessage,hwndClient,WM_MDIGETACTIVE,0,0
                invoke SendMessage,eax,WM_CLOSE,0,0
            .else
                invoke DefFrameProc,hWnd,hwndClient,uMsg,wParam,lParam
                ret
            .endif
        .endif
    .elseif uMsg==WM_DESTROY
        invoke PostQuitMessage,NULL
    .else
        invoke DefFrameProc,hWnd,hwndClient,uMsg,wParam,lParam
        ret
    .endif
    xor eax,eax
    ret
WndProc endp

ChildProc proc hChild:DWORD,uMsg:DWORD,wParam:DWORD,lParam:DWORD
    .if uMsg==WM_MDIACTIVATE
        mov eax,lParam
        .if eax==hChild
            invoke GetSubMenu,hChildMenu,1
            mov edx,eax
            invoke SendMessage,hwndClient,WM_MDISETMENU,hChildMenu,edx
        .else
            invoke GetSubMenu,hMainMenu,1
            mov edx,eax
            invoke SendMessage,hwndClient,WM_MDISETMENU,hMainMenu,edx
        .endif
        invoke DrawMenuBar,hwndFrame
    .elseif uMsg==WM_CLOSE
        invoke MessageBox,hChild,addr ClosePromptMessage,addr AppName,MB_YESNO
        .if eax==IDYES
            invoke SendMessage,hwndClient,WM_MDIDESTROY,hChild,0
        .endif
    .else
        invoke DefMDIChildProc,hChild,uMsg,wParam,lParam
        ret
    .endif
    xor eax,eax
    ret
ChildProc endp
end start

分析:

程序所做的第一件事情就是注册框架窗口类和MDI子窗口类. 作完这个以后, 程序调用CreateWindowEx来创建框架窗口.用框架窗口的WM_CREATE句柄来创建客户窗口:

    LOCAL ClientStruct:CLIENTCREATESTRUCT
    .if uMsg==WM_CREATE
        invoke GetMenu,hWnd
        mov hMainMenu,eax
        invoke GetSubMenu,hMainMenu,1
        mov ClientStruct.hWindowMenu,eax
        mov ClientStruct.idFirstChild,100
        invoke CreateWindowEx,NULL,ADDR MDIClientName,NULL,\
            WS_CHILD or WS_VISIBLE or WS_CLIPCHILDREN,CW_USEDEFAULT,\
            CW_USEDEFAULT,CW_USEDEFAULT,CW_USEDEFAULT,hWnd,NULL,\
            hInstance,addr ClientStruct
        mov hwndClient,eax

我们调用GetMenu来获得框架窗口的菜单的句柄, 这个句柄在调用GetSubMenu时将会被用到. 注意我们将1传递给 GetSubMenu ,因为我们希望显示窗口列表的子菜单是第二个子菜单. 然后我们给CLIENTCREATESTRUCT结构的成员赋值.
下一步我们初始化MDICLIENTSTRUCT 结构. 注意我们不需要在这里做.要初始化MDICLIENTSTRUCT结构的话,用WM_CREATE消息比较方便.

    mov mdicreate.szClass,offset MDIChildClassName
    mov mdicreate.szTitle,offset MDIChildTitle
    push hInstance
    pop mdicreate.hOwner
    mov mdicreate.x,CW_USEDEFAULT
    mov mdicreate.y,CW_USEDEFAULT
    mov mdicreate.lx,CW_USEDEFAULT
    mov mdicreate.ly,CW_USEDEFAULT

在框架窗口创建之后(也包括客户窗口), 我们调用LoadMenu从资源中获取子窗口的菜单.我们需要获取这个菜单的句柄,这样当一个MDI子菜单出现时,我们就可以用这个句柄来取代框架窗口的菜单. 在这个应用程序退出到Windows之前不要忘记调用DestroyMenu来去掉这个句柄. 正常情况下当一个应用程序退出的时候,Windows将会自动释放和窗口相关的菜单. 但是在这种情况下, 子窗口的菜单没有和任何窗口相关联, 这样即使当应用程序退出后半部 子窗口的菜单仍然会占用宝贵的内存资源.

    invoke LoadMenu,hInstance, IDR_CHILDMENU
    mov hChildMenu,eax
    ........
    invoke DestroyMenu, hChildMenu

在消息循环中我们调用TranslateMDISysAccel.

    .while TRUE
        invoke GetMessage,ADDR msg,NULL,0,0
        .break .if (!eax)
        invoke TranslateMDISysAccel,hwndClient,addr msg
        .if !eax
            invoke TranslateMessage, ADDR msg
            invoke DispatchMessage, ADDR msg
        .endif
    .endw

假如TranslateMDISysAccel 返回一个非零值, 它以为着Windows已经在处理这条消息.这样你就不需要为这条消息做任何事情了.假如返回的值为零, 那么这条消息就不是MDI相关的消息, 因此就必须按照通常情况来处理.

WndProc proc hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM
    .....
    .else
        invoke DefFrameProc,hWnd,hwndClient,uMsg,wParam,lParam
        ret
    .endif
    xor eax,eax
    ret
WndProc endp

注意到在框架窗口的窗口过程中, 我们调用DefFrameProc来处理我们不感兴趣的消息.

窗口过程的重要之处在 WM_COMMAND句柄. 当用户从文件菜单中选择 "New"时, 我们就创建了一个MDI子窗口.

    .elseif ax==IDM_NEW
        invoke SendMessage,hwndClient,WM_MDICREATE,0,addr mdicreate

在我们的例子中,我们通过发送WM_MDICREATE消息给客户窗口, 同时还要在lParam参数中传递MDICREATESTRUCT结构的地址来创建一个MDI子窗口.

ChildProc proc hChild:DWORD,uMsg:DWORD,wParam:DWORD,lParam:DWORD
    .if uMsg==WM_MDIACTIVATE
        mov eax,lParam
        .if eax==hChild
            invoke GetSubMenu,hChildMenu,1
            mov edx,eax
            invoke SendMessage,hwndClient,WM_MDISETMENU,hChildMenu,edx
        .else
            invoke GetSubMenu,hMainMenu,1
            mov edx,eax
            invoke SendMessage,hwndClient,WM_MDISETMENU,hMainMenu,edx
        .endif
        invoke DrawMenuBar,hwndFrame

当MDI子窗口创建后, 我们可以监视 WM_MDIACTIVATE以察看它是不是一个活动窗口.具体的方法是比较将某一个MDI子窗口的句柄和参数lParam的值进行比较, 参数lParam中包含的是活动子窗口的句柄. 这样如果两者匹配的话, 证明这个MDI子窗口就是活动子窗口. 下一步就是将框架窗口的菜单替换成MDI子窗口自身的菜单. 因为最初的菜单将会被取代, 你比学赶帮超再一次告讼Windows窗口列表将显示在哪一个子菜单. 这就是我们必须再一次调用GetSubMenu 来检索子菜单的句柄的原因. 我们发送 WM_MDISETMENU消息给客户窗口来获得想要的结果. WM_MDISETMENU中的wParam参数包含了你希望取代最初的菜单的菜单句柄. lParam参数包含的是你希望用来显示窗口列表的子菜单的句柄.在发送了WM_MDISETMENU之后, 我们调用l DrawMenuBar 来刷新菜单, 否则的话你的菜单将会是一片混乱.

    .else
        invoke DefMDIChildProc,hChild,uMsg,wParam,lParam
        ret
    .endif
在MDI子窗口的窗口过程中, 你必须传送所有未处理的消息给DefMDIChildProc而不是DefWindowProc.
    .elseif ax==IDM_TILEHORZ
        invoke SendMessage,hwndClient,WM_MDITILE,MDITILE_HORIZONTAL,0
    .elseif ax==IDM_TILEVERT
        invoke SendMessage,hwndClient,WM_MDITILE,MDITILE_VERTICAL,0
    .elseif ax==IDM_CASCADE
        invoke SendMessage,hwndClient,WM_MDICASCADE,MDITILE_SKIPDISABLED,0

当用户在窗口子菜单中选择一个菜单项时, 我们发送相应的消息给客户窗口. 假如用户选择平铺窗口, 我们发送 WM_MDITILE 给客户窗口, 在wParam参数中指定哪一种类型的平铺. 选择重叠的话是类似的, 相应的发送WM_MDICASCADE..

    .elseif ax==IDM_CLOSE
        invoke SendMessage,hwndClient,WM_MDIGETACTIVE,0,0
        invoke SendMessage,eax,WM_CLOSE,0,0

假如用户选择 "Close" 菜单项, 我们首先必须通过发送WM_MDIGETACTIVE给客户窗口来获得当前活动的MDI子窗口的句柄, 返回的值保存在eax寄存器中, 这个值就是当前活动MDI子窗口的句柄. 获得句柄之后, 我们就可以发送WM_CLOSE给那个窗口了.

    .elseif uMsg==WM_CLOSE
        invoke MessageBox,hChild,addr ClosePromptMessage,addr AppName,MB_YESNO
        .if eax==IDYES
            invoke SendMessage,hwndClient,WM_MDIDESTROY,hChild,0
        .endif

在MDI子窗口的窗口过程中, 当收到WM_CLOSE的消息时, 就会显示一个消息框询问用户是否确实想关闭着这个窗口. 假如回答是"是"的话, 我们发送WM_MDIDESTROY给客户窗口. WM_MDIDESTROY关闭MDI子窗口,然后恢复框架窗口的标题.

翻译：XieJacky，校对：LuoYunBin's Win32 ASM Page， http://asm.yeah.net/


ODBC 编程教程:基础

ODBC 基础

这是使用 win32asm进行数据库编程系列的第一份教程。 在如今的IT界，数据库编程变的越来越重要，所以我们不能再忽视它。但如今有很多种数据库在使用，如果 我们为了实现win32下数据库汇编语言编程而学习各种数据库文件格式，所花时间大概称得上“永恒”。

幸运的是，Microsoft的一项技术使得我们得以摆脱这个 大麻烦。它被称为ODBC，是开放式数据库互连（Open Database Connectivity）的缩写,这是一族API,与Windows API相似。它主要与数据库打交道。就是说,利用ODBC API,你可通过统一界面 和好多各不相同的数据库打交道。

ODBC是如何工作的?它的结构式怎样的？在使用ODBC之前， 你应对它的结构有一个清楚的了解。 ODBC有四个组成部分:

  • 应用程序 (Application，你的程序)
  • ODBC 管理器 (ODBC manager)
  • ODBC 驱动程序(ODBC Drivers)
  • 数据源 (Data Sources,数据库)

这四个组件的核心是ODBC 管理器。 你可把它想象成你的监工。你告诉它你希望他作什么，然后它把你的要求传达给它的工人(ODBC 驱动程序)并完成工作。如果工人有什么想告诉你的，它会与监工(ODBC 管理器)说，由监工传达给你。工人们很明白他们应作什么，因此他们会为你很好的完成工作。

通过这样的模式，我们并不与数据库驱动程序直接通信。 你只需告诉数据库管理器你想要做什么。而使用恰当的ODBC驱动程序来实现你的目的则是ODBC管理器的事。 每个ODBC 驱动程序对于它所对应的数据库均有足够了解。各部件各司其职,极大的简化了工作量。

你的程序<----> ODBC管理器<----> ODBC驱动程序 <----> 数据库

ODBC管理器由Microsoft提供。看一下你的控制面板。如果你正确地安装了ODBC你会找到ODBC数据源(32位) 项目。 至于ODBC驱动程序, Microsoft随他们的产品提供了好几种。并且你总可从数据库提供商那里获得新的ODBC 驱动程序。只要简单地安装新的ODBC驱动程序,你的机器就可使用新的它以前不知道的数据库。

ODBC APIs 使用很简单,但你需要知道一些关于SQL和数据库的知识。例如字段(field),主键(primary key),记录(record),列(column),行(row)等。我须假定你已知道数据库理论的一些基础知识,这样我才能讨论win32下用汇编语言进行ODBC编程的细节问题。正如你所看到的,ODBC 管理器试图在你的程序里隐藏实现的细节。这意味着它必须提供某些基本界面来与你的程序和ODBC驱动程序进行通讯。 由于ODBC驱动程序在某些性能方面存在着差异,因此必须存在一种方法,以使得我们的程序能够知道某个ODBC驱动程序是否支持某一特性。 ODBC定义了被称为Interface Conformance Levels的三层服务界面。第三层是核心层。任何ODBC驱动程序都要象在第一层和第二层实现功能一样实现核心层表中的所有特性。从我们的程序的眼光来看, ODBC APIs被分割为这样的三层。如果某个函数被标为核心的，就意味着你可放心使用而不必担心它是否为你正使用的ODBC驱动程序支持。如果它是一个第一层或第二层的函数,你就得确认ODBC驱动程序是否支持，然后再使用。你可通过MSDN获得ODBC APIs的详细资料。

在编写代码之前你应了解一些ODBC的名词。

  • 环境(Environment). 和字面意思一样,是一个全局文本用来存取数据。如果你熟悉DAO的话,你可把它想象为一个workspace。它包含应用于所有ODBC session的信息，例如一个session的connections句柄。在用ODBC之前你必须从环境中获得这个句柄。
  • 连接(Connection). 指定ODBC驱动程序和数据源(数据库)。你可以在同一环境中同时连接不同的数据库
  • 语句(Statement). ODBC使用SQL作为自己的语言。 因而只要简单的认为语句就是你希望ODBC执行的SQL命令就行了。

以下是使用ODBC编程的一般步骤:

  1. 连接数据源
  2. 创建并执行一条或多条SQL语句
  3. 检查结果记录(如果有的话)
  4. 断开数据源

在接下来的教程中我们来学习如何来实现这几步。

[Iczelion's Win32 Assembly Homepage]

[由orochi于2000年8月1日翻译]


ODBC Programming Tutorial:连接数据源

连接数据源

本教程中，我们将学习使用ODBC APIs的细节.

因为我们的程序并不与ODBC驱动程序直接通信，而是通过ODBC管理器来定义一系列APIs供你的程序调用以完成工作，所以我们需要包含odbc32.inc和odbc32.lib文件，当然还有windows.inc。

连接数据源需要以下几步:

  1. 分配一个环境句柄(environment handle). 在进行每个ODBC任务(session)时仅需这样做一次.一旦获得了句柄,我们就可修改环境属性来适合我们的需要。你可以把这想象为在DB工作中创建一个workspace.
  2. 确认将使用的ODBC的版本. 你可在ODBC 2.x版和3.x版间选择.他们在很多方面存在不同，因此本步骤是必须的以使得ODBC管理器它将用何种语法与用户程序通讯，及如何解释用户程序的命令.
  3. 分配一个连接句柄.这个步骤可看作创建一个空连接.我们还没有指定使用那一个驱动程序，连接那一个数据库.这些信息将在稍后来写入.
  4. 建立一个连接.可通过调用ODBC函数来建立连接.

当连接完成时，必须通过以下步骤来关闭和销毁它:

  1. 断开与数据源的连接.
  2. 释放连接句柄.
  3. 释放环境句柄 (如果不再需要在这个环境中作更多连接)

分配一个句柄

在ODBC 3.x版本以前,我们需要调用很多独立的函数来分配环境、连接和语句句柄(SQLAllocEnv, SQLAllocConnect, SQLAllocStmt).而在ODBC 3.x中, 这些函数被SQLAllocHandle所代替，语法如下:

SQLRETURN SQLAllocHandle( SQLSMALLINT HandleType,
                                         SQLHANDLE InputHandle,
                                         SQLHANDLE * OutputHandlePtr
                                       );

看上去挺麻烦，简化一下看看：

SQLAllocHandle proto HandleType:DWORD,
                                    InputHandle:DWORD,
                                    OutputHandlePtr:DWORD

SQLRETURN 被定义为SQLSMALLINT类型.而 SQLSMALLINT被定义为短整型,例如一个字(16 bits). 所以该函数的返回值在ax中,而不是 eax. 这是很重要的.但是Win32下函数的参数是通过32位堆栈来传送的.即使这个参数只是一个字长(16位)，它也应被扩展为32位.这就是为什么HandleType被说明为双字(dword)而不是字(word).看一下导入库 odbc32.lib，SQLAllocHandle的入口是_SQLAllocHandle@12. 就是说这个函数的参数的组合长度为12字节(3 dwords).然而，这不是说C函数的原型不对. SQLAllocHandle会只用HandleType的底位字并忽略高位字.因此C函数原型是功能上(functionally)正确而我们的汇编函数原型反映了实际.

结束了SQL类型的讨论，我们来看一看函数的参数和返回值。.

  • HandleType 是一个常数，定义了希望分配的句柄类型.可能值如下:
SQL_HANDLE_ENV | 环境句柄(Environment handle)
SQL_HANDLE_DBC | 连接句柄(Connection handle)
SQL_HANDLE_STMT | 语句句柄(Statement handle)
SQL_HANDLE_DESC | 描述符句柄(Descriptor handle)

描述符是一个数据集合描述了一个SQL语句的参数或一个结果集的列数, 视应用程序或驱动程序而定。

  • InputHandle 是指向父"文本"的句柄.就是说,如果你想分配一个连接句柄, 需要通过一个环境句柄因为连接将在那个环境的文本中建立.如果你想分配一个环境句柄,这个参数必须为SQL_HANDLE_NULL (注意SQL_HANDLE_NULL在windows.inc版本1.18及其以前版本中被不正确的定义为0L.你需要删除掉"L"否则程序不会被编译通过.这是我的错，因为我负责修订windows.inc中的 SQL/ODBC部分.) 因为环境没有父文本.对于语句和描述符句柄，我们需要将连接句柄作为这个参数。
  • OutputHandlePtr 如果调用成功，将指向一个双字，其中包含了被分配的句柄.

SQLAllocHandle 可能的返回值如下:

SQL_SUCCESS | 函数成功完成.
SQL_SUCCESS_WITH_INFO | 函数成功完成，但带回非致命错误或警告.
SQL_ERROR | 函数调用失败.
SQL_INVALID_HANDLE | 传送给函数的句柄非法.

无论函数的调用成功还是失败，我们都可通过调用SQLGetDiagRec或SQLGetDiagField函数来获得更多的信息.它们与Win32 API中的GetLastError很相似.

例子:

.data?
hEnv dd ?

.code
invoke SQLAllocHandle, SQL_HANDLE_ENV, SQL_HANDLE_NULL, addr hEnv
.if ax==SQL_SUCCESS || ax==SQL_SUCCESS_WITH_INFO

选择ODBC的版本

分配完环境句柄后,我们需要设置一个环境属性SQL_ATTR_ODBC_VERSION以适当的值.设置环境属性可通过调用函数SQLSetEnvAttr.你也许猜到了，还有类似的函数如 SQLSetConnectAttr和SQLSetStmtAttr. SQLSetEnvAttr原型如下:

SQLSetEnvAttr proto EnvironmentHandle:DWORD,
                                   Attribute:DWORD,
                                   ValuePtr:DWORD,
                                   StringLength:DWORD
  • EnvironmentHandle. 与字面意思一样, 它包含了要设置属性的环境句柄.
  • Attribute. 这是一个常数，表示用户需要设置的属性.对我们而言,是SQL_ATTR_ODBC_VERSION.可以从MSDN中查看全部列表.
  • ValuePtr. 这个参数的意义由希望设置的属性值决定.如果属性值是32位的, 这个参数将被认为是想要设置的属性值.如果属性值是一个字符串或二进制缓冲区,它就被解释为指向字符串或缓冲区的指针.如果我们指定了要设置的属性为SQL_ATTR_ODBC_VERSION, 这个参数我们可以填入SQL_OV_ODBC3和SQL_OV_ODBC2这两个可能值,分别对应ODBC 3.x和2.x.
  • StringLength. 由ValuePtr指向的值的长度. 如果这个值是字符串或二进制缓冲区,这个参数一定是合法的. 如果想设置的属性是一个双字，这个参数被忽略.因为 SQL_ATTR_ODBC_VERSION属性包含一个双字的值,我们可以只给它赋为NULL.

这个函数的返回值与SQLAllocHandle相同.

例子:

.data?
hEnv dd ?

.code
invoke SQLAllocHandle, SQL_HANDLE_ENV, SQL_HANDLE_NULL, addr hEnv
.if ax==SQL_SUCCESS || ax==SQL_SUCCESS_WITH_INFO
invoke SQLSetEnvAttr, hEnv, SQL_ATTR_ODBC_VERSION, SQL_OV_ODBC3, NULL
.if ax==SQL_SUCCESS || ax==SQL_SUCCESS_WITH_INFO

分配连接句柄

这一步与分配环境句柄相似，我们可以通过调用SQLAllocHandle函数并赋以不同的参数值来完成.

例子:

.data?
hEnv dd ?
hConn dd ?

.code
invoke SQLAllocHandle, SQL_HANDLE_ENV, SQL_HANDLE_NULL, addr hEnv
.if ax==SQL_SUCCESS || ax==SQL_SUCCESS_WITH_INFO
invoke SQLSetEnvAttr, hEnv, SQL_ATTR_ODBC_VERSION, SQL_OV_ODBC3, NULL
.if ax==SQL_SUCCESS || ax==SQL_SUCCESS_WITH_INFO
invoke SQLAllocHandle, SQL_HANDLE_DBC, hEnv, addr hConn
.if ax==SQL_SUCCESS || ax==SQL_SUCCESS_WITH_INFO

建立一个连接

我们现在要通过特定的ODBC驱动程序来连接数据源.通过这三个ODBC函数来达成这个目标.它们为我们提供了好几层"选择".

SQLConnect | Core | 这是最简单的函数。它只需要数据源名(DSN,Data source name)和可选的用户名和密码.它不提供任何GUI选项例如向用户显示一个对话框来提供更多信息。如果你已经有了需要使用的数据库的DSN就可以使用这个函数.
SQLDriverConnect | Core | 这个函数提供了较SQLConnect更多的选择.我们可以连接一个没有在系统信息内定义的数据源。如没有DSN.另外,我们可以指定这个函数是否需要显示一个对话框来为用户提供更多信息.例如,如果用户遗漏了数据库的名字,它会指导ODBC驱动程序显示一个对话框，让用户来选择想连接的数据库.
SQLBrowseConnect | Level 1 | 这个函数允许在运行时(RunTime)枚举数据源.比SQLDriverConnect更加灵活。因为可以多次顺序调用SQLBrowseConnect，而每次提供给使用者更多的专用信息直到最后获得需要的连接句柄.

我将先检查SQLConnect函数.要使用 SQLConnect,你应先知道什么是DSN. DSN是数据源名(Data Source Name)的缩写,是一个唯一标识某数据源的字符串。一个DSN标识了一个包含了如何连接某一特定的数据源的信息的数据结构.这个信息包括要使用何种ODBC驱动程序及要连接哪个数据库.我们可以通过控制面板中的32位ODBC数据源来创建、修改及删除DSN.

SQLConnect的语法如下:

SQLConnect proto ConnectionHandle:DWORD
                              pDSN:DWORD,
                              DSNLength:DWORD,
                              pUserName:DWORD,
                              NameLength:DWORD,
                              pPassword:DWORD,
                              PasswordLength:DWORD
  • ConnectionHandle. 要使用的连接句柄.
  • pDSN. 指向DSN的指针.
  • DSNLength. DSN的长度
  • pUserName. 指向用户名的指针
  • NameLength. 用户名的长度
  • pPassword. 指向该用户名所使用密码的指针
  • PasswordLength. 密码的长度

在最小情况下, SQLConnect 需要连接句柄,DSN和DSN的长度。如果数据源不需要的话，用户名和密码就不是必须的.函数的返回值与SQLAllocHandle的返回值相同.

假设我们的系统中有一个叫做"Sales" 的DSN并且我们想连接这个数据源.我们可以这样做:

.data
DSN db "Sales",0

.code
......
invoke SQLConnect, hConn, addr DSN, sizeof DSN,0,0,0,0

SQLConnect 的缺点是：在连接一个数据源之前,我们必须创建它的DSN. SQLDriverConnect 提供了更大的灵活性.它的语法如下:

SQLDriverConnect proto ConnectionHandle:DWORD,
                                        hWnd:DWORD,
                                        pInConnectString:DWORD,
                                        InStringLength:DWORD,
                                        pOutConnectString:DWORD,
                                        OutBufferSize:DWORD,
                                        pOutConnectStringLength:DWORD,
                                        DriverCompletion:DWORD
  • ConnectionHandle 连接句柄
  • hWnd 应用程序窗口句柄.如果这个参数被置为NULL,驱动程序将不会为用户显示一个对话框来显示更多信息(如果有的话).
  • pInConnectString 指向连接字符串的指针. 这是一个ASCIIZ字符串,格式由要连接的具体ODBC驱动程序描述.它描述了驱动程序名、数据源及其他附加属性.连接字符串的具体描述请参见MSDN，这里不再细述.
  • InStringLength 连接字符串的长度.
  • pOutConnectString 指向将要被填入完整连接字符串的缓冲区的指针.这个缓冲区将至少有1,024字节长.这听上去使人困惑。事实上，我们提供的连接字符串会不完整，这时，ODBC驱动程序会提示用户更多信息.接下来ODBC驱动程序会根据所有可能的信息创建一个完整的连接字符串并将其放入缓冲区。即使我们提供的连接字符串已经可以工作了，这个缓冲区也会填入更多的属性值.这个参数的目的是我们可以保存完整连接字符串来为接下来的连接做准备。
  • OutBufferSize 由pOutConnectString指向的缓冲区的长度.
  • pOutConnectStringLength 指向一个双字的指针，用来接收由ODBC驱动程序返回的完整连接字符串的长度。
  • DriverCompletion 一个标志用来指示ODBC管理器/驱动程序是否将提示用户更多信息.但是,这个标志取决于是否在调用本函数时传送了hWnd 参数一个窗口句柄。如果没有,即使该标志被设置，ODBC管理器/驱动程序也不会提示用户.可能值如下：
    SQL_DRIVER_PROMPT | ODBC驱动程序提示用户输入信息。驱动程序将利用这些信息来创建连接字符串.
    SQL_DRIVER_COMPLETE
    SQL_DRIVER_COMPLETE_REQUIRED     		仅当用户提供的连接字符串不完全时， ODBC驱动程序才会提示用户.
    SQL_DRIVER_NOPROMPT | ODBC驱动程序将不会提示用户.

例子:

.data
strConnect db "DBQ=c:\data\test.mdb;DRIVER={Microsoft Access Driver (*.mdb)};",0

.data?
buffer db 1024 dup(?)
OutStringLength dd ?

.code
.....
invoke SQLDriverConnect, hConn, hWnd, addr strConnect, sizeof strConnect, addr buffer, sizeof buffer, addr OutBufferLength, SQL_DRIVER_COMPLETE

断开与数据源的连接

在连接顺利完成后,我们就可以对数据源进行查询及其他操作.这些将在下一节讨论.现在假设我们已完成了对数据源的操作，就可以通过调用SQLDisconnect来断开与它的连接. 这个函数非常简单(就象那悲伤而冷酷的现实：毁灭总比创造容易的多).它只需要一个参数：连接句柄。

invoke SQLDisconnect, hConn

释放连接与环境句柄

在顺利地断开连接后,我们现在可以调用SQLFreeHandle函数来销毁连接句柄和环境句柄. 这是由ODBC 3.x提供的新函数.它替代了SQLFreeConnect, SQLFreeEnv及SQLFreeStmt函数. SQLFreeHandle语法如下:

SQLFreeHandle proto HandleType:DWORD, Handle:DWORD
  • HandleType 标识要销毁句柄的类别的常数.可能值与 SQLAllocHandle中相同
  • Handle要销毁的句柄.

For example:

invoke SQLFreeHandle, SQL_HANDLE_DBC, hConn
invoke SQLFreeHandle, SQL_HANDLE_ENV, hEnv

[<<] [index] [>>]

[Iczelion's Win32 Assembly Homepage]

[The Chinese Portion Is Translated By Orochi ,2000.8.21 ]


ODBC Programming Tutorial:准备与使用语句

准备与使用语句

继续我们的学习,在这一章，我们来看看如何通过ODBC来操作数据源.

在上一章,我们已学习了第一步，怎样连接一个数据源。一个连接定义了用户与数据源的数据通道.它是静态的.如果要想操作数据源，我们就必须使用语句(statement).可以认为语句就是发给数据源的命令.这个"命令"必须用SQL写成.通过使用语句,我们就可以修改数据源的结构、在其中执行查询、修改及删除数据.

准备及使用语句可分为以下几个步骤:

  1. 分配一个语句句柄(statement handle)
  2. 创建SQL语句
  3. 执行语句
  4. 销毁语句

分配一个语句句柄

可以通过调用函数 SQLAllocHandle来分配一个语句句柄。例子如下：

.data?
hStmt dd ?

.code
......
invoke SQLAllocHandle, SQL_HANDLE_STMT, hConn, addr hStmt

创建SQL语句

这部分你就得靠自己了你必须知道SQL的语法.比如说,当你想创建一个表时，你就得知道CREATE TABLE语句.

执行语句

执行语句有四种方法,由它们是否被数据库引擎编译过（准备好）及被怎样定义有关.

直接执行(Direct Execution) | 由用户程序定义SQL语句.语句在运行时(runtime)被编译并执行.
编译执行(Prepared Execution) | 也是由用户程序定义SQL语句，但编译和执行被分为两步：首先SQL被准备好(被编译)，接下来被执行.通过这种方法,我们可以只编译SQL语句一次但执行相同的语句多次以节省时间.
存储过程(Procedures) | SQL语句被编译并存放在数据源内，用户程序可以在运行时调用这些语句.
目录(Catalog) | SQL被硬编码(hardcoded)在ODBC驱动程序内. 目录函数的目的是返回预定义的结果集例如数据库中的所有表名. 总的来说,目录函数用来获得数据源的信息的用户程序可以在运行时调用它们.

这四种方法各有优缺点.当你只运行某一SQL语句一次时，直接执行是一个很好的选择；如果你要经常运行某一语句,则应首选编译执行因为SQL语句仅在第一次运行时被编译，在接下来的运行中，它将运行更快，因为已被编译过了；存储过程是注重速度的最佳选择，因为它已被编译过并存放在数据源中了。缺点是并不是所有的数据存储都支持存储过程.目录主要是是用来获得数据源结构的一些信息。

在本章中,我们主要看一下直接执行和编译执行，因为它们是由我们的程序来完成的.编写存储过程是DBMS(数据库管理系统)的事.而目录将在稍后讨论.

直接执行

要直接并快速执行SQL语句,以如下语法调用函数SQLExecDirect:

SQLExecDirect proto StatementHandle:DWORD,
                                  pStatementText:DWORD,
                                  TextLength:DWORD
  • StatementHandle. 要使用的语句句柄
  • pStatementText. 指向要执行的SQL语句的指针
  • TextLength. SQL语句的长度.

可能的返回值如下:

SQL_SUCCESS | 操作顺利
SQL_SUCCESS_WITH_INFO | 操作顺利但可能产生非致命错误
SQL_ERROR | 操作失败
SQL_INVALID_HANDLE | 使用的语句句柄非法
SQL_NEED_DATA | 如果在执行SQL语句前没有提供足够的参数将会获得这个返回值.这时需调用SQLParamData 或 SQLPutData函数来提交参数.
SQL_NO_DATA | 如果SQL语句不返回结果集，例如只是一个查询动作,将获得这个返回值。使用户知道动作成功，但没有结果集被返回。
SQL_STILL_EXECUTING | 如果异步执行SQL语句, SQLExecDirect 会立刻返回这个值,表明语句正在执行。在通常情况下，如果你使用的一个多线程操作系统，异步执行是一个好主意。如果你希望异步执行,旧可以通过SQLSetStmtAttr来设置语句属性。

例子:

.data
SQLStmt db "select * from Sales",0

.data?
hStmt dd ?

.code
.....
 invoke SQLAllocHandle, SQL_HANDLE_STMT, hConn, addr hStmt
.if ax==SQL_SUCCESS || ax==SQL_SUCCESS_WITH_INFO
invoke SQLExecDirect, hStmt, addr SQLStmt, sizeof SQLStmt

编译执行

SQL的执行被分为两步：第一步，必须通过调用函数SQLPrepare来*准备*语句。第二步,通过调用SQLExecute函数来执行语句.在使用编译执行时,我们可以多次调用SQLExecute来执行同一语句. 结合使用SQL参数,这个方法对执行同一语句极为有效。

SQLPrepare 与SQLExecDirect使用相同的三个参数，所以这里不再写出函数原型。 SQLExecute 语法:

SQLExecute proto StatementHandle:DWORD

只须这一个参数,我想不再需要解释了;)

例子:

.data
SQLStmt db "select * from Sales",0

.data?
hStmt dd ?

.code
.....
 invoke SQLAllocHandle, SQL_HANDLE_STMT, hConn, addr hStmt
.if ax==SQL_SUCCESS || ax==SQL_SUCCESS_WITH_INFO
invoke SQLPrepare, hStmt, addr SQLStmt, sizeof SQLStmt
invoke SQLExecute, hStmt

你也许会想，编译执行没什么强于直接执行的。上面的例子还不明显。 我们需要知道SQL语句的参数来仔细研究它。

语句参数(Statement Parameters)

这里的参数是指由SQL语句使用的变量.比如说我们有一个叫做 "employee"的表，它有三个字段："name", "surname"和 "TelephoneNo" 。现在我们要找一个叫做"Bob"的职员的电话号码, 就可以使用以下SQL语句：

select telephoneNo from employee where name='Bob'

这条SQL语句象我们希望的那样工作了。但是，如果我们又想找另一个职员的电话号码怎么办？如果不使用参数,那只好再写一条SQL语句，然后再一次编译、执行它。

现在我们不会允许这种低效率的行为了。我们可以使用参数来实现目标。在上面的例子中，我们必须将字符串/值替换为 '?' (被称为参数标志符(parameter marker)).SQL 语句将变成这样:

select telephoneNo from employee where name=?

现在想一下这个问题:ODBC驱动程序如何知道用什么值来替换参数标志符'?'?答案是: 我们必须提供需要的值.这种方法被称为参数绑定(parameter binding).简单点说,就是将一个参数标志符与用户程序中的变量建立连接的过程.在上面的例子中,我们需要创建一个缓冲区来告诉ODBC驱动程序，当它需要一个参数的具体值时，将从我们提供的字符串缓冲区中获得。一旦一个参数与一个变量绑定,它将一直保持绑定，直到被绑定到另一变量，或直到所有参数都被函数 SQLFreeStmt以(函数)参数 SQL_RESET_PARAMS释放，或直到该语句被释放.

将一个参数绑定到一个变量是通过调用函数 SQLBindParameter实现，语法如下:

  • SQLBindParameter proto StatementHandle:DWORD,
                                             ParameterNumber:DWORD,
                                             InputOutputType:DWORD,
                                             ValueType:DWORD,
                                             ParameterType:DWORD,
                                             ColumnSize:DWORD,
                                             DecimalDigits:DWORD,
                                             ParameterValuePtr:DWORD,
                                             BufferLength:DWORD,
                                             pStrLenOrIndPtr:DWORD
  • StatementHandle 语句句柄
  • ParameterNumber 参数个数，由1开始。这就是ODBC用来判断参数描述符的方法。如果有三个参数，则最左边是第一个参数，最右边是第三个参数。
  • InputOutputType 表明参数是用来输入还是输出的标志.这里的输入是指ODBC驱动程序将使用参数中获得的值，输出是指ODBC驱动程序将在操作结束时将结果放入参数中.大多数情况下,我们使用参数作为输入。而输出参数经常与存储过程有关.这个参数的两个可能值为: SQL_PARAM_INPUT、 SQL_PARAM_INPUT_OUTPUT和SQL_PARAM_OUTPUT(译者:似应为三个参数，但原文如此)
  • ValueType 指明用户程序将要绑定到参数的值或缓冲区的类型。可能的类型为一组常数，以SQL_C_开头。
  • ParameterType 参数的SQL类型。例如,如果SQL参数是文本字段,我们就在这里填入值SQL_CHAR.查看MSDN中的ODBC程序员指南来获得完整列表(ODBC Programmer's reference)。
  • ColumnSize 参数的长度。换句话说，可认为是与参数标志符相连接的列(字段)的长度.在我们的例子中, 我们的参数标志符对列"name"使用了标准值.如果该列被定义了20字节长,我们就该在ColumnSize中填入20.
  • DecimalDigits 与参数描述符连接的列的小数位.
  • ParameterValuePtr 指向包含参数数据的缓冲区的指针.
  • BufferLength 由ParameterValuePtr指向的缓冲区的长度.
  • pStrLenOrIndPtr 指向一个双字的指针，包含以下之一:
    • 由ParameterValuePtr指向的缓冲区中包含的参数长度. 除非参数的类型是字符串或二进制值，否则这个值被忽略.别把它与BufferLength混淆，看一下这个例子就会明白：假设该参数是一个字符串，该列有20字节宽.所以我们分配了一个21字节长的缓冲区，并将其地址传送到ParameterValuePtr。在调用函数SQLExecute前, 我们在缓冲区中放入了字符串"Bob".这个字符串有3字节长，因此我们需要在pStrLenOrIndPtr指向的双字中放入3这个值.
    • SQL_NTS. 这个参数是一个0结尾字符串(null-terminated string).
    • SQL_NULL_DATA. 参数值为NULL.
    • SQL_DEFAULT_PARAM. 存储过程将使用参数的默认值,而不是从用户程序中获得的值. 它仅适用于已定义了默认参数值的存储过程.
    • SQL_DATA_AT_EXEC. 参数的数据将由SQLPutData传送. 由于数据可能太大无法放入内存(比如整个文件的数据),我们可以告诉ODBC驱动程序我们将用SQLPutData替代.

      可能你会说 pStrLenOrIndPtr的参数太多了,但通常情况下，我们只会用到第一或第三个选项。

例子:

.data
SQLString db "select telephoneNo from employee where name=?",0
Sample1 db "Bob",0
Sample2 db "Mary",0

.data?
buffer db 21 dup(?)
StrLen dd ?

.code
........
invoke SQLPrepare, hStmt, addr SQLString,sizeof SQLString
invoke SQLBindParameter, hStmt, 1, SQL_PARAM_INPUT, SQL_C_CHAR, SQL_CHAR, 20, 0, addr buffer, sizeof buffer, addr StrLen
;===========================================
; First run
;===========================================
invoke lstrcpy, addr buffer, addr Sample1
mov StrLen, sizeof Sample1
invoke SQLExecute, hStmt
;===========================================
; Second run
;===========================================
invoke lstrcpy, addr buffer, addr Sample2
mov StrLen, sizeof Sample2
invoke SQLExecute, hStmt

注意我们仅将参数与缓冲区仅绑定了一次，当我们修改了缓冲区的内容并调用了SQLExecute 很多次.不必再调用SQLPrepare.ODBC驱动程序知道去那里找它需要的参数因为已通过SQLBindParameter函数告诉了它.

但现在我们还不能获得查询结果.访问和适用结果集是下一章的内容。

假设我们已完成了很多SQL语句，并要执行一个新的语句，那么没必要重新分配一个语句句柄.只要使用SQL_UNBIND与SQL_RESET_PARAMS参数调用SQLFreeStmt函数来解除与参数的绑定就可以了. 接下来就可以接着使用原来的语句句柄来执行SQL语句了.

释放语句

由调用SQLFreeHandle函数实现.

  • 译注：原标题为Preparing and Using Statements,因为ODBC驱动程序繁多，有解释、编译执行之分，所以Preparing不仅是指准备，也有预编译的意思。
  • 另:关于作者提到的MSDN,其ODBC部分可在Visual Studio及其部件中找到，如
    C:\program Files\DevStudio\SharedIDE\Help\ODBC.hlp.

    [<<] [index] [>>]

    [Iczelion's Win32 Assembly Homepage]

    [The Chinese Portion Is Translated By Orochi ,2000.8.23]


ODBC Programming Tutorial: 检索结果

获得结果

在本章中,我们来学习如何获得SQL语句的执行结果。

我们称由查询返回的一组记录为一个 结果集(result set) (或在VB中被称为recordset).检索结果通常分为以下几个步骤:

  1. 确认一个结果集是否可用。
  2. 将结果集的列绑定在适当的变量上。
  3. 取得行

完成对结果集的操作后,我们必须调用SQLCloseCursor来销毁它.

确认一个结果集是否可用

有时在执行SQL语句后，我们就已经知道结果集是否可用了。如果执行的语句并不是返回结果集的类型,我们就知道结果集不可用了。但有时我们不太清楚使用的SQL语句是什么类型，比如说，让用户自行输入SQL语句。这时，我们必须确认是否有结果集被建立，这可以通过SQLNumResultCols函数实现。如果结果集存在，则该函数返回结果集中的列数(字段数)。语法如下:

SQLNumResultCols proto StatementHandle:DWORD, pNumCols:DWORD
  • StatementHandle 语句句柄
  • pNumCols 指向一个双字的指针，其中包含了结果集的列数。

如果 pNumCols指向的值为0,那么表明结果集不存在。

绑定列

与绑定一个变量到SQL语句的参数相同,我们连接(绑定)一个变量到结果集中的某一列。这里要用到的函数是SQLBindCol，语法如下:

SQLBindCol proto StatementHandle:DWORD,
                            ColumnNumber:DWORD,
                            TargetType:DWORD,
                            TargetValuePtr:DWORD,
                            BufferLength:DWORD,
                            pStrLenOrIndPtr:DWORD
  • StatementHandle 语句句柄
  • ColumnNumber 结果集中要绑定的列序数.列序数从1开始.列0是书签(bookmark)列.
  • TargetType The 指示 TargetValuePtr指向的变量(缓冲区)的类型的常数。
  • TargetValuePtr 指向将要捆绑到列的变量或缓冲区的指针。当调用SQLFetch来获得结果集中的行时,本参数指向的变量或缓冲区将被填入被绑定的列的值。
  • BufferLength 由TargetValuePtr指向的缓冲区的长度。
  • pStrLenOrIndPtr 参见前章SQLBindParameter

例子:

.data?
buffer db 21 dup(?)
DataLength dd ? ;调用SQLFetch后，被填入缓冲区的字符串的长度

.code
.....
invoke SQLBindCol, hStmt, 1, SQL_C_CHAR, addr buffer, 21, addr DataLength

取得一行

非常简单.先调用SQLFetch 检索结果集的一列到绑定的变量中。当SQLFetch调用完成,游标(cursor)被更新(updated).可以认为游标就是一个记录指针(record pointer). 它指明了SQLFetch调用后将返回哪一行。比如说,如果结果集有4列，当结果集建立时，游标指向第一行.当调用了SQLFetch 后,游标被加1。所以如果调用了SQLFetch 4次,就没有更多的行可被提交了。游标会显示指向文件尾(EOF). SQLFetch语法如下:

SQLFetch proto StatementHandle:DWORD

当没有行可提交时，这个函数返回 SQL_NO_DATA.

例子:

.data?
buffer db 21 dup(?)
DataLength dd ?

.code
.....
invoke SQLBindCol, hStmt, 1, SQL_C_CHAR, addr buffer, 21, addr DataLength
invoke SQLFetch, hStmt

译者废话:还记得地球的经纬吗?我们通过SQLBindCol来定位经度(列),用游标来定义纬度(行),用SQLFetch来获得坐标，而结果集就是地球。大航海时代２的同好们不应忘记呀!(船长,我们找不到水......顺便问一句,这是地球吗%$#@&*^:-)

[<<] [index] [>>]

[Iczelion's Win32 Assembly Homepage]

[The Chinese Portion Is Translated By Orochi ,2000.8.24]


ODBC Programming Tutorial:一个例子

一个例子

这一章,我们要把我们已学的知识集合起来。具体来讲,我们来写一个使用ODBC APIs的程序.为简单起见,这个程序中我使用Microsoft的Access数据库(Microsoft Access 97) .

下载例子源程序.

注意:如果你使用的windows.inc 是1.18及其以下版本,在开始编译之前要修改其中的一个小bug.在windows.inc中查找 "SQL_NULL_HANDLE"，将得到下面这行:

SQL_NULL_HANDLE equ 0L

将0后面的"L"删除,象这样:

SQL_NULL_HANDLE equ 0

这个程序是一个基于对话框的程序，有一个简单的菜单.当用户选择"connect"时,它将试图连接test.mdb数据库,如果连接成功,将显示由ODBC驱动程序返回的完整连接字符串.接下来,用户可选择"View All Records"命令,程序会使用listview control来显示数据库中的所有数据.用户还可以选择"Query"命令来查询特定的记录.例子程序将会显示一个小对话框提示用户输入想找的人名.当用户按下OK钮或回车键,程序将执行一个查询来查找符合条件的记录.当用户完成对数据库的操作时,可以选择"disconnect"命令与数据库断开连接.

现在看一下源程序:

.386
.model flat,stdcall
include \masm32\include\windows.inc
include \masm32\include\kernel32.inc
include \masm32\include\odbc32.inc
include \masm32\include\comctl32.inc
include \masm32\include\user32.inc
includelib \masm32\lib\odbc32.lib
includelib \masm32\lib\comctl32.lib
includelib \masm32\lib\kernel32.lib
includelib \masm32\lib\user32.lib

IDD_MAINDLG equ 101
IDR_MAINMENU equ 102
IDC_DATALIST equ 1000
IDM_CONNECT equ 40001
IDM_DISCONNECT equ 40002
IDM_QUERY equ 40003
IDC_NAME equ 1000
IDC_OK equ 1001
IDC_CANCEL equ 1002
IDM_CUSTOMQUERY equ 40004
IDD_QUERYDLG equ 102

DlgProc proto hDlg:DWORD, uMsg:DWORD, wParam:DWORD, lParam:DWORD
QueryProc proto hDlg:DWORD, uMsg:DWORD, wParam:DWORD, lParam:DWORD
SwitchMenuState proto :DWORD
ODBCConnect proto :DWORD
ODBCDisconnect proto :DWORD
RunQuery proto :DWORD

.data?
hInstance dd ?
hEnv dd ?
hConn dd ?
hStmt dd ?
Conn db 256 dup(?)
StrLen dd ?
hMenu dd ? ; 主菜单句柄
hList dd ? ; listview control句柄
TheName db 26 dup(?)
TheSurname db 26 dup(?)
TelNo db 21 dup(?)
NameLength dd ?
SurnameLength dd ?
TelNoLength dd ?
SearchName db 26 dup(?)
ProgPath db 256 dup(?)
ConnectString db 1024 dup(?)

.data
SQLStatement db "select * from main",0
WhereStatement db " where name=?",0
strConnect db "DRIVER={Microsoft Access Driver (*.mdb)};DBQ=",0
DBName db "test.mdb",0
ConnectCaption db "Complete Connection String",0
Disconnect db "Disconnect successful",0
AppName db "ODBC Test",0
AllocEnvFail db "Environment handle allocation failed",0
AllocConnFail db "Connection handle allocation failed",0
SetAttrFail db "Cannot set desired ODBC version",0
NoData db "You must type the name in the edit box",0
ExecuteFail db "Execution of SQL statement failed",0
ConnFail db "Connection attempt failed",0
AllocStmtFail db "Statement handle allocation failed",0
Heading1 db "Name",0
Heading2 db "Surname",0
Heading3 db "Telephone No.",0

.code
start:
invoke GetModuleHandle, NULL
mov hInstance,eax
call GetProgramPath
invoke DialogBoxParam, hInstance, IDD_MAINDLG,0,addr DlgProc,0
invoke ExitProcess,eax
invoke InitCommonControls
DlgProc proc hDlg:DWORD, uMsg:DWORD, wParam:DWORD, lParam:DWORD
.if uMsg==WM_INITDIALOG
invoke GetMenu, hDlg
mov hMenu,eax
invoke GetDlgItem, hDlg, IDC_DATALIST
mov hList,eax
call InsertColumn
.elseif uMsg==WM_CLOSE
invoke GetMenuState, hMenu, IDM_CONNECT,MF_BYCOMMAND
.if eax==MF_GRAYED
invoke ODBCDisconnect, hDlg
.endif
invoke EndDialog,hDlg, 0
.elseif uMsg==WM_COMMAND
.if lParam==0
mov eax,wParam
.if ax==IDM_CONNECT
invoke ODBCConnect,hDlg
.elseif ax==IDM_DISCONNECT
invoke ODBCDisconnect,hDlg
.elseif ax==IDM_QUERY
invoke RunQuery,hDlg
.elseif ax==IDM_CUSTOMQUERY
invoke DialogBoxParam, hInstance, IDD_QUERYDLG,hDlg, addr QueryProc, 0
.endif
.endif
.else
mov eax,FALSE
ret
.endif
mov eax,TRUE
ret
DlgProc endp

GetProgramPath proc
invoke GetModuleFileName, NULL,addr ProgPath,sizeof ProgPath
std
mov edi,offset ProgPath
add edi,sizeof ProgPath-1
mov al,"\"
mov ecx,sizeof ProgPath
repne scasb
cld
mov byte ptr [edi+2],0
ret
GetProgramPath endp

SwitchMenuState proc Flag:DWORD
.if Flag==TRUE
invoke EnableMenuItem, hMenu, IDM_CONNECT, MF_GRAYED
invoke EnableMenuItem, hMenu, IDM_DISCONNECT, MF_ENABLED
invoke EnableMenuItem, hMenu, IDM_QUERY, MF_ENABLED
invoke EnableMenuItem, hMenu, IDM_CUSTOMQUERY, MF_ENABLED
.else
invoke EnableMenuItem, hMenu, IDM_CONNECT, MF_ENABLED
invoke EnableMenuItem, hMenu, IDM_DISCONNECT, MF_GRAYED
invoke EnableMenuItem, hMenu, IDM_QUERY, MF_GRAYED
invoke EnableMenuItem, hMenu, IDM_CUSTOMQUERY, MF_GRAYED
.endif
ret
SwitchMenuState endp

ODBCConnect proc hDlg:DWORD
invoke SQLAllocHandle, SQL_HANDLE_ENV, SQL_NULL_HANDLE, addr hEnv
.if ax==SQL_SUCCESS || ax==SQL_SUCCESS_WITH_INFO
invoke SQLSetEnvAttr, hEnv,SQL_ATTR_ODBC_VERSION, SQL_OV_ODBC3,0
.if ax==SQL_SUCCESS || ax==SQL_SUCCESS_WITH_INFO
invoke SQLAllocHandle, SQL_HANDLE_DBC, hEnv, addr hConn
.if ax==SQL_SUCCESS || ax==SQL_SUCCESS_WITH_INFO
invoke lstrcpy,addr ConnectString,addr strConnect
invoke lstrcat,addr ConnectString, addr ProgPath
invoke lstrcat, addr ConnectString,addr DBName
invoke SQLDriverConnect, hConn, hDlg, addr ConnectString, sizeof ConnectString, addr Conn, sizeof Conn,addr StrLen, SQL_DRIVER_COMPLETE
.if ax==SQL_SUCCESS || ax==SQL_SUCCESS_WITH_INFO
invoke SwitchMenuState,TRUE
invoke MessageBox,hDlg, addr Conn,addr ConnectCaption,MB_OK+MB_ICONINFORMATION
.else
invoke SQLFreeHandle, SQL_HANDLE_DBC, hConn
invoke SQLFreeHandle, SQL_HANDLE_ENV, hEnv
invoke MessageBox, hDlg, addr ConnFail, addr AppName, MB_OK+MB_ICONERROR
.endif
.else
invoke SQLFreeHandle, SQL_HANDLE_ENV, hEnv
invoke MessageBox, hDlg, addr AllocConnFail, addr AppName, MB_OK+MB_ICONERROR
.endif
.else
invoke SQLFreeHandle, SQL_HANDLE_ENV, hEnv
invoke MessageBox, hDlg, addr SetAttrFail, addr AppName, MB_OK+MB_ICONERROR
.endif
.else
invoke MessageBox, hDlg, addr AllocEnvFail, addr AppName, MB_OK+MB_ICONERROR
.endif
ret
ODBCConnect endp

ODBCDisconnect proc hDlg:DWORD
invoke SQLDisconnect, hConn
invoke SQLFreeHandle, SQL_HANDLE_DBC, hConn
invoke SQLFreeHandle, SQL_HANDLE_ENV, hEnv
invoke SwitchMenuState, FALSE
invoke ShowWindow,hList, SW_HIDE
invoke MessageBox,hDlg,addr Disconnect, addr AppName,MB_OK+MB_ICONINFORMATION
ret
ODBCDisconnect endp

InsertColumn proc
LOCAL lvc:LV_COLUMN
mov lvc.imask,LVCF_TEXT+LVCF_WIDTH
mov lvc.pszText,offset Heading1
mov lvc.lx,150
invoke SendMessage,hList, LVM_INSERTCOLUMN,0,addr lvc
mov lvc.pszText,offset Heading2
invoke SendMessage,hList, LVM_INSERTCOLUMN, 1 ,addr lvc
mov lvc.pszText,offset Heading3
invoke SendMessage,hList, LVM_INSERTCOLUMN, 3 ,addr lvc
ret
InsertColumn endp

FillData proc
LOCAL lvi:LV_ITEM
LOCAL row:DWORD

invoke SQLBindCol, hStmt,1,SQL_C_CHAR, addr TheName, sizeof TheName,addr NameLength
invoke SQLBindCol, hStmt,2,SQL_C_CHAR, addr TheSurname, sizeof TheSurname,addr SurnameLength
invoke SQLBindCol, hStmt,3,SQL_C_CHAR, addr TelNo, sizeof TelNo,addr TelNoLength
mov row,0
.while TRUE
mov byte ptr ds:[TheName],0
mov byte ptr ds:[TheSurname],0
mov byte ptr ds:[TelNo],0
invoke SQLFetch, hStmt
.if ax==SQL_SUCCESS || ax==SQL_SUCCESS_WITH_INFO
mov lvi.imask,LVIF_TEXT+LVIF_PARAM
push row
pop lvi.iItem
mov lvi.iSubItem,0
mov lvi.pszText, offset TheName
push row
pop lvi.lParam
invoke SendMessage,hList, LVM_INSERTITEM,0, addr lvi
mov lvi.imask,LVIF_TEXT
inc lvi.iSubItem
mov lvi.pszText,offset TheSurname
invoke SendMessage,hList,LVM_SETITEM, 0,addr lvi
inc lvi.iSubItem
mov lvi.pszText,offset TelNo
invoke SendMessage,hList,LVM_SETITEM, 0,addr lvi
inc row
.else
.break
.endif
.endw
ret
FillData endp

RunQuery proc hDlg:DWORD
invoke ShowWindow, hList, SW_SHOW
invoke SendMessage, hList, LVM_DELETEALLITEMS,0,0
invoke SQLAllocHandle, SQL_HANDLE_STMT, hConn, addr hStmt
.if ax==SQL_SUCCESS || ax==SQL_SUCCESS_WITH_INFO
invoke SQLExecDirect, hStmt, addr SQLStatement, sizeof SQLStatement
.if ax==SQL_SUCCESS || ax==SQL_SUCCESS_WITH_INFO
invoke FillData
.else
invoke ShowWindow, hList, SW_HIDE
invoke MessageBox,hDlg,addr ExecuteFail, addr AppName, MB_OK+MB_ICONERROR
.endif
invoke SQLCloseCursor, hStmt
invoke SQLFreeHandle, SQL_HANDLE_STMT, hStmt
.else
invoke ShowWindow, hList, SW_HIDE
invoke MessageBox,hDlg,addr AllocStmtFail, addr AppName, MB_OK+MB_ICONERROR
.endif
ret
RunQuery endp
QueryProc proc hDlg:DWORD, uMsg:DWORD, wParam:DWORD, lParam:DWORD
.if uMsg==WM_CLOSE
invoke SQLFreeHandle, SQL_HANDLE_STMT, hStmt
invoke EndDialog, hDlg,0
.elseif uMsg==WM_INITDIALOG
invoke ShowWindow, hList, SW_SHOW
invoke SQLAllocHandle, SQL_HANDLE_STMT, hConn, addr hStmt
.if ax==SQL_SUCCESS || ax==SQL_SUCCESS_WITH_INFO
invoke lstrcpy, addr Conn, addr SQLStatement
invoke lstrcat, addr Conn, addr WhereStatement
invoke SQLBindParameter,hStmt, 1, SQL_PARAM_INPUT, SQL_C_CHAR, SQL_CHAR,25,0, addr SearchName,25,addr StrLen
invoke SQLPrepare, hStmt, addr Conn, sizeof Conn
.else
invoke ShowWindow, hList, SW_HIDE
invoke MessageBox,hDlg,addr AllocStmtFail, addr AppName, MB_OK+MB_ICONERROR
invoke EndDialog, hDlg,0
.endif
.elseif uMsg==WM_COMMAND
mov eax, wParam
shr eax,16
.if ax==BN_CLICKED
mov eax,wParam
.if ax==IDC_OK
invoke GetDlgItemText, hDlg, IDC_NAME, addr SearchName, 25
.if ax==0
invoke MessageBox, hDlg,addr NoData, addr AppName, MB_OK+MB_ICONERROR
invoke GetDlgItem, hDlg, IDC_NAME
invoke SetFocus, eax
.else
invoke lstrlen,addr SearchName
mov StrLen,eax
invoke SendMessage, hList, LVM_DELETEALLITEMS,0,0
invoke SQLExecute, hStmt
invoke FillData
invoke SQLCloseCursor, hStmt
.endif
.else
invoke SQLFreeHandle, SQL_HANDLE_STMT, hStmt
invoke EndDialog, hDlg,0
.endif
.endif
.else
mov eax,FALSE
ret
.endif
mov eax,TRUE
ret
QueryProc endp
end start

分析

start:
invoke GetModuleHandle, NULL
mov hInstance,eax
call GetProgramPath

当程序开始时,将获得实例句柄并获得所在路径.默认情况下数据库 test.mdb应与程序处于同一文件夹.

GetProgramPath proc
invoke GetModuleFileName, NULL,addr ProgPath,sizeof ProgPath
std
mov edi,offset ProgPath
add edi,sizeof ProgPath-1
mov al,"\"
mov ecx,sizeof ProgPath
repne scasb
cld
mov byte ptr [edi+2],0
ret
GetProgramPath endp

GetProgramPath调用GetModuleFileName来获得程序的全路径名.接着在路径中查找最后一个"\"符",通过将文件名的第一个字符置为0获得(truncate)" 文件名. 因此我们在ProgPath中获得了程序的路径名.

然后程序将用DialogBoxParam显示主对话框.当主对话框第一次被载入时,它将获得菜单句柄和listview control句柄.接下来在listview control中插入三列(因为我们已经知道结果集将包含三列.因为是我们先建的表.)

现在,它就等待用户的动作了.如果用户在菜单中选择"connect",将会调用ODBCConnect函数.

ODBCConnect proc hDlg:DWORD
invoke SQLAllocHandle, SQL_HANDLE_ENV, SQL_NULL_HANDLE, addr hEnv
.if ax==SQL_SUCCESS || ax==SQL_SUCCESS_WITH_INFO

它做的第一件事是调用SQLAllocHandle来分配一个环境句柄.

 invoke SQLSetEnvAttr, hEnv,SQL_ATTR_ODBC_VERSION, SQL_OV_ODBC3,0
.if ax==SQL_SUCCESS || ax==SQL_SUCCESS_WITH_INFO

获得环境句柄后,程序调用SQLSetEnvAttr来表示将要使用ODBC 3.x的语法.

 invoke SQLAllocHandle, SQL_HANDLE_DBC, hEnv, addr hConn
.if ax==SQL_SUCCESS || ax==SQL_SUCCESS_WITH_INFO

如果一切顺利,程序将通过调用SQLAllocHandle获得连接句柄来实现连接.

 invoke lstrcpy,addr ConnectString,addr strConnect
invoke lstrcat,addr ConnectString, addr ProgPath
invoke lstrcat, addr ConnectString,addr DBName

接着填写连接字符串.完整的连接字符串将被用在ConnectionString

 invoke SQLDriverConnect, hConn, hDlg, addr ConnectString, sizeof ConnectString, addr Conn, sizeof Conn,addr StrLen, SQL_DRIVER_COMPLETE
.if ax==SQL_SUCCESS || ax==SQL_SUCCESS_WITH_INFO
invoke SwitchMenuState,TRUE
invoke MessageBox,hDlg, addr Conn,addr ConnectCaption,MB_OK+MB_ICONINFORMATION

当连接字符串完成,程序将调用SQLDriverConnect来通过MS Access ODBC 驱动程序连接test.mdb数据库.如果文件test.mdb不存在,ODBC driver将提示用户输入该文件的位置,因为我们已经设定了SQL_DRIVER_COMPLETE标志.当SQLDriverConnect成功返回时, Conn 被填入由ODBC驱动程序创建的完整连接字符串.我们通过一个message box来将其显示给用户. SwitchMenuState是一个单纯切换菜单选项可用的函数.

现在,到数据库的连接已经建立并被打开,并一直保持打开状态直到用户选择关闭.

当用户选择了"View All Records"命令, 对话框过程将调用RunQuery.函数

RunQuery proc hDlg:DWORD
invoke ShowWindow, hList, SW_SHOW
invoke SendMessage, hList, LVM_DELETEALLITEMS,0,0

由于listview control在创建时是不可见的,现在我们把它显示出来.还有 要把其中的所有元素(如果有的话)删掉.

 invoke SQLAllocHandle, SQL_HANDLE_STMT, hConn, addr hStmt
.if ax==SQL_SUCCESS || ax==SQL_SUCCESS_WITH_INFO

接下来,程序将获得一个语句句柄.

 invoke SQLExecDirect, hStmt, addr SQLStatement, sizeof SQLStatement
.if ax==SQL_SUCCESS || ax==SQL_SUCCESS_WITH_INFO

通过 SQLExecDirect执行已准备好的SQL语句.我这里选择SQLExecDirect 的原因是只须执行一次.

 invoke FillData

执行SQL语句后,将返回一个结果集.我们使用 FillData函数来从结果集中解出数据并将其放入listview control中.

FillData proc
LOCAL lvi:LV_ITEM
LOCAL row:DWORD

invoke SQLBindCol, hStmt,1,SQL_C_CHAR, addr TheName, sizeof TheName,addr NameLength
invoke SQLBindCol, hStmt,2,SQL_C_CHAR, addr TheSurname, sizeof TheSurname,addr SurnameLength
invoke SQLBindCol, hStmt,3,SQL_C_CHAR, addr TelNo, sizeof TelNo,addr TelNoLength

现在,结果集被返回.我们要绑定结果集的所有三列到我们提供的缓冲区中.这是调用SQLBindCol来实现的.注意我们要对每一列分别调用.并且我们并不需要绑定所有的列:只要绑定要获得数据的列就行了.

 mov row,0
.while TRUE
mov byte ptr ds:[TheName],0
mov byte ptr ds:[TheSurname],0
mov byte ptr ds:[TelNo],0

当列中没有数据时,我们初始化缓冲区为NULLs.更好的方法是用SQLBindCol指定的变量中数据的长度.在我们的例子中,我们可以检查NameLength, SurnameLength和TelNoLength中的值的确切长度.

 invoke SQLFetch, hStmt
.if ax==SQL_SUCCESS || ax==SQL_SUCCESS_WITH_INFO
mov lvi.imask,LVIF_TEXT+LVIF_PARAM
push row
pop lvi.iItem
mov lvi.iSubItem,0
mov lvi.pszText, offset TheName
push row
pop lvi.lParam
invoke SendMessage,hList, LVM_INSERTITEM,0, addr lvi

其它都很简单了.调用SQLFetch 来获得结果集的一行,并将其存入listview control的缓冲区中.当没有更多的行供检索时(已到达文件尾), SQLFetch返回SQL_NO_DATA并且程序跳出循环.

 invoke SQLCloseCursor, hStmt
invoke SQLFreeHandle, SQL_HANDLE_STMT, hStmt

当完成对结果集的操作时,调用SQLCloseCursor关闭结果集并调用SQLFreeHandle释放语句句柄.

当用户选择"Query"命令,程序显示另一个对话框供用户输入要查询的名字.

 .elseif uMsg==WM_INITDIALOG
invoke ShowWindow, hList, SW_SHOW
invoke SQLAllocHandle, SQL_HANDLE_STMT, hConn, addr hStmt
.if ax==SQL_SUCCESS || ax==SQL_SUCCESS_WITH_INFO
invoke lstrcpy, addr Conn, addr SQLStatement
invoke lstrcat, addr Conn, addr WhereStatement
invoke SQLBindParameter,hStmt, 1, SQL_PARAM_INPUT, SQL_C_CHAR, SQL_CHAR,25,0, addr SearchName,25,addr StrLen
invoke SQLPrepare, hStmt, addr Conn, sizeof Conn

对话框做的第一件事是显示listview control.接下来分配一个语句句柄以创建SQL语句.这个SQL语句有一个"where"子句及一个参数标志符"?". 完整的SQL语句是:

select * from main where name=?

接着程序调用SQLBindParameter 来建立参数标志符与缓冲区SearchName的连接,这样当SQL语句被执行时,ODBC驱动程序就可从SearchName中获得需要的字符串.接下来,程序调用SQLPrepare来编译SQL语句. 这样我们只要准备/编译SQL语句一次就可多次使用.因为SQL语句已被编译过,接下来的执行过程会快一些.

 .if ax==IDC_OK
invoke GetDlgItemText, hDlg, IDC_NAME, addr SearchName, 25
.if ax==0
invoke MessageBox, hDlg,addr NoData, addr AppName, MB_OK+MB_ICONERROR
invoke GetDlgItem, hDlg, IDC_NAME
invoke SetFocus, eax
.else

当用户在编辑框(edit control)中填入了一些名字并按下回车键, 程序将获得编辑框中的文本并检查是否是空字符串.如果是,则显示一个message box并将键盘焦点设在编辑框上,提示用户输入名字.

 invoke lstrlen,addr SearchName
mov StrLen,eax
invoke SendMessage, hList, LVM_DELETEALLITEMS,0,0
invoke SQLExecute, hStmt
invoke FillData
invoke SQLCloseCursor, hStmt

如果编辑框中已有字符串,程序会获得它的长度并将其放入StrLen中供ODBC驱动程序使用(记住我们已将StrLen的地址传送给了SQLBindParameter). 接下来程序使用获得的语句句柄调用SQLExecute执行已准备好的SQL语句.当 SQLExecute返回时,程序调用FillData在listview control显示结果.因为我们不会再用到结果集,调用SQLCloseCursor来关闭它.

[<<] [index] [>>]

[Iczelion's Win32 Assembly Homepage]

[The Chinese Portion Is Translated By Orochi ,2000.8.24]


Virtual Device Driver Basic

虚拟设备驱动程序初步

在本教程里，我假定读者对诸如虚8086模式，调页，GDT,LDT,IDT之类的INTEL 80x86保护模式的操作比较熟悉。如果你不了解这些，那你要先在 http://developer.intel.com/design/pentium/manuals/阅读INTEL的文档。

内容：

Windows95是一个运行在最高级特权，第0层级别的多线程操作系统。所有的应用程序都运行在最低级特权，第3层级别上。这样就限制了应用程序对系统的操作。它们不能使用cpu特权指令，不能直接访问I/O端口，等等。你对gdi32,kernal32和user32这三个大的系统组件一定很熟悉。你肯定会认为这样重要的代码段一定是在第0层级别下运行的。但是实际上，它们和其他的应用程序一样，是在第三层级别下运行的。这就是说它们并不比Windows计算器，或者扫雷游戏有更多的权限。系统的控制实权掌握在虚拟级管理器(VMM) 和虚拟设备驱动程序(VxD)手中。
这一切都是由dos引起的。在Window 3.x的时代，在市场上有很多成功的dos软件。Windows 3.x必须同时运行普通的Windows程序和dos程序，否则，它就会失去市场。
这个局面是很难处理的，因为dos程序和Windows程序有本质的不同。dos程序认为它们拥有系统的一切：键盘，cpu，内存，硬盘等等。dos程序不知道怎样和其他程序合作，而Windows程序(从那时候起)是可靠的多任务合作系统。也就是每个Windows程序都必须通过GetMessage或PeekMessage来和其他程序进行交流。
解决办法就是，在一个8086虚拟机上运行所有的dos程序，而在另一个叫做系统虚拟机的虚拟机上运行其他所有的Windows程序。Windows负责把cpu运算时间轮流的分给每个虚拟机。这样，在Windows 3.x里。Windows程序之间用的是合作多任务，而虚拟机之间用的是优先级多任务。
什么是一个虚拟机？一个虚拟机是被软件创建的一个假象。一个虚拟机和在它上面运行的程序交互，就像这个程序是在真正的机器上运行一样。这样，一个程序不知道也不关心自己是否是在虚拟机上运行。只要虚拟机准确的像一个真的机器一样响应程序，我们就可以把它当成一个真正的机器。
你可以把虚拟机这种实机器和软件之间的接口看作一种API。这种不寻常的API由中断，BIOS调用和I/O端口组成。如果Windows能够以某种方法完美的模拟这个API，那么在虚拟机上运行的程序就会表现的和它们在实际器上运行时完全一样。
这就是为什么会出现VMM和VxD的原因。为了协调和监视虚拟机(VMs)，Windows需要一个程序来分配任务。这个程序就是虚拟机管理器(VMM)。

虚拟机管理器

VMM是一个32位的保护模式程序。它的主要任务是建立和维护一个支持虚拟机的框架。例如，它要创建，运行和结束一个虚拟机。VMM是众多的系统VxD程序之一，它被放在你的系统目录下的VMM32.VxD文件中。VMM本身是一个VxD程序，但它被当作一个监视其他VxD程序的监视器。让我们来看一下Windows95的启动次序：
  1. 加载io.sys。
  2. 执行config.sys和autoexec.bat。
  3. 调用win.com。
  4. win.com运行VMM32.VxD，VMM32.VxD实际上是个简单的dos的exe文件。
  5. VMM32.VxD用xms驱动程序把VMM加载到扩展内存。
  6. VMM初始化自身及其它的默认VxD。
  7. VMM把机器转入到保护模式并创建系统虚拟机。
  8. 最后被加载的虚拟外壳设备在系统虚拟机上通过运行krnl386.exe来启动Windows。
  9. krnl386.exe加载所有的文件，最后是Windows95外壳。
正如你所看到的，VMM是第一个被加载到内存的VxD程序。它创建系统虚拟机并初始化其他的VxD程序。它也为这些VxD程序提供许多服务。
VMM和VxD的操作模式和真正的程序不同。在大多数时候，它们是潜伏的。当应用程序在系统中运行时，这些VxD程序没有被激活。当某些需要它们处理的中断/错误/事件发生时，它们才被唤醒。
VMM是不可重入的。这意味着VxD程序必须使它们的访问和VMM服务同步。在有些情况下调用VMM服务是不安全的，比如VMM正在处理一个硬件中断。在这段时间内，VMM是不允许重进入的。作为一个VxD编写者，你必须对你的所作所为极度的小心。记住，你是在最高特权级别，第0层级别，如果你代码有错的话，谁也管不到。

虚拟设备驱动程序

虚拟设备驱动程序被简称为VxD。x 代表各种设备的名字，如虚拟键盘驱动程序(vkd)，虚拟鼠标驱动程序(vmd)等等。VxD程序是硬件成功初始化的途径。记得dos程序认为它们拥有系统的一切，当它们在虚拟机中运行时，Windows需要给它们一个实机器的替身。VxD程序就是这些替身。VxD程序通常虚拟一些硬件设备，所以，例如当一个dos程序认为它在同键盘通讯时，实际是虚拟键盘驱动程序在和dos程序通讯。一个VxD程序通常控制真正的硬件设备并对该设备在各个虚拟机之间的共享进行管理。
尽管如此，并不是说每个VxD程序必须和一个硬件设备相连。虽然VxD程序是用来虚拟硬件设备的，但是我们也可以把VxD程序看作是在第0级别的dll。例如，如果你需要做一些只有在第0级别才能做的工作，你就可以编一个VxD程序来为你完成这个工作。这样，由于此VxD程序并没有虚拟任何设备，你就可以把它仅仅看作是你的程序的扩展。
在我们更深入的讨论VxD和创建我们的VxD程序之前，让我先说一些有关于VxD的事情。
  • VxD程序是Windows 9x特有的，它在Windows NT下不能运行。所以如果你的程序是依靠VxD的，它就不能被移植到Windows NT平台上去。
  • VxD是系统中权力最大的实体。由于它们可以对系统作任何事情，所以它们是极度危险的。一个恶意的/错误的VxD程序可以毁掉整个系统。对于恶意的/错误的VxD程序没有任何的保护措施。
  • 通常的，不用VxD也有很多办法能达到你的目的。在采用VxD的解决办法之前一定要三思。如果用其他的可以在第三层级别实施的办法，就使用这个办法。
Windows 95下有两种VxD:
  • 静态VxD
  • 动态VxD
静态VxD是那些从系统启动就被加载，在系统关闭之前一直存在于内存中的VxD程序。这种VxD可以追溯至Windows 3.x的时代。动态VxD时只有Windows 9x下才有的。动态VxD程序可以在需要的时候被加载/卸载。这些程序大多数都是用来控制设置管理器和输入输出监视器加载的即插即用设备的。你可以在你的win32应用程序里加载或卸载动态VxD程序。

VxD程序之间的通讯

VxD程序，包括VMM，通过以下三种途径在相互之间进行通讯：
  • 控制消息
  • 服务API
  • 回调
控制消息: 当有VMM感兴趣的事件发生时，它就向系统中所有载入的VxD程序发送控制消息。控制消息就像是第三层级别的Windows应用程序的消息。每个VxD程序都有一个接受和处理控制消息的函数，叫做设备控制函数。系统控制消息总共有50多个。控制消息不多的原因是系统中通常加载了很多VxD程序，而每个VxD程序在收到一个控制消息时都要进行处理。如果控制消息太多，就会导致系统停滞。所以控制消息只包括那些与虚拟机有关的重要消息，如：一个虚拟机被创建，被销毁等等。作为对系统控制消息的附加，一个VxD程序可以定义自己的控制消息，这些消息可以用来和那些能响应这些消息的VxD程序通讯。

服务函数: 一个VxD程序，包括VMM在内，通常要导出一系列的被别的VxD程序调用的公共函数，这些函数被称为VxD服务。调用这些服务的机制和在第三层级别运行的的应用程序有很大的不同：每个导出VxD服务的VxD程序必须有一个唯一的ID，你可以从Microsoft得到一个这样的ID。这个ID是一个包含了一个VxD唯一的身份验证的16位的数字，例如：

    UNDEFINED_DEVICE_ID    EQU 00000H
    VMM_DEVICE_ID          EQU 00001H
    DEBUG_DEVICE_ID        EQU 00002H
    VPICD_DEVICE_ID        EQU 00003H
    VDMAD_DEVICE_ID        EQU 00004H
    VTD_DEVICE_ID          EQU 00005H
你可以看到VMM的ID是1，VPICD的ID是3，等等。VMM用这些ID来找到导出所需VxD服务的VxD程序。当一个VxD程序导出VxD服务时，它把所有服务的地址存在一个表里面。所以，你还需要通过服务分支表里面服务的索引来找到你所要的服务。例如，如果你要调用第一个服务，GetVersion服务，你就要指定0(这个索引是从0开始的)。调用VxD服务的实机制包括中断20h，你的代码产生一个中断20h，并带有一个双字的值，这个值包含了设备ID和服务索引。例如，如果你要调用一个VxD程序导出的VxD服务，假设VxD程序设备ID是000DH，服务号码是1，那么代码应该是：
    int 20h
    dd 000D0001h
跟在中断20H后的双字的高字包含设备ID。低字是在服务列表中的索引。
当20H中断执行时，VMM得到了控制权，并马上检测跟着的双字。然后它提出设备ID用来找到VxD程序，用服务索引来定位在那个VxD程序中的所要求的服务的地址。
你可以看到这个操作时很费时的。VMM必须浪费很多时间来定位VxD程序和所要服务的地址，所以VMM作了个小小的弊 。当中断20H操作成功后，VMM抓取链接。这就是说，VMM用直接的服务调用来替代20H中断和它后面的双字。所以上面的20H中断代码片断就被改变成:
    call dword ptr [VxD_Service_Address]
这个把戏是成功的，因为int 20h+dword加一个双字用6个字节，正好和call dword ptr结构相等。所以接下来的服务调用是快速而有效的。这个方法具有直接性，简洁性。在好的一方面，它减轻了VMM和VxD载入器的工作量，因为它们不用定位VxD中所有的服务，那些没有执行过的服务将会保持原样。再不那么好的一方面，一旦一个静态VxD程序导出的服务被调用，那么就不可能把这个静态的VxD程序卸载了。由于VMM把调用锁定到VxD服务的实际地址上，如果提供这个服务的VxD程序从内存中被卸载了，其他VxD程序调用这个服务时就会很快的因为调用无效的内存地址而导致系统崩溃。没有办法来消除抓取的链接。这个问题的结论是动态VxD不适合作为服务提供者。

回调: 回调或者回调函数是在VxD程序中给其他的VxD程序调用的函数，不要把回调函数和VxD服务搞混淆了。回调函数不像服务那样是公共的，它们是私有函数，VxD在特定的情况下把它们的地址送给其他的VxD程序。例如，当一个VxD程序在处理一个硬件中断时，由于VMM是不可重入的，这个VxD程序不能使用VxD服务，否则会引起页面错误(重入VMM)。这个VxD程序可以把它自己的一个回调函数的地址给VMM，这样VMM就可以在能忍受页面错误时调用这个函数。回调函数的想法不是VxD独有的。许多Windows API都在用。最好的例子也许是窗口函数，你把窗口函数的地址填在WINDCLASS或WINDCLASSEX结构里并把它当作函数来调用RegisterClass或者RegisterClassEx。当有这个窗口的消息传来时，Windows就会调用你的窗口函数。另一个例子是窗口接管函数。你的程序把接管函数的地址送给Windows，这样当你感兴趣的事件发生时，Windows就会调用你的接管函数。
上述三种方法是VxD之间通讯的，我们还要讲对V86，保护模式和Win32应用程序的接口。在下一章里，我们要学习VxD对Win32应用程序的接口。

由 Ryo 翻译，发表于 http://asm.yeah.net，英文版本来自 [Iczelion's Win32 Assembly Homepage]


虚拟机管理器

虚拟机管理器(VMM)是Windows 95的实际操作系统，它建立和维护一个管理虚拟机的框架，同时为其他vxd程序提供许多重要的服务。其中三种重要的服务是：
  • 内存管理
  • 中断处理
  • 线程调度

内存管理

VMM使用Intel 80386或更新的处理器的内存调页能力来为系统虚拟机创建一个32位的虚地址空间。它把这个地址空间分为四个不同的部分：
  • V86区 地址从0H到10FFEFH，这个区属于当前执行的虚拟机。
  • 应用程序私有区地址从4MB到2GB。这是Win32应用程序运行的空间。每个Win32的进程都有它自己的2GB(要减去4MB)。
  • 应用程序共享区地址从2GB到3GB。这个区域是在虚拟机内的所有 应用程序共享的。系统DLL(user32,kernel32,gid32)都驻存在这里。所有的Win16程序也放在这里，因为它们行为都是不规范的的：它们对内存中的其他Win16程序进行读写。只有在这个区域里，Win16程序才可以看到其他所有的Win16程序。内存映射文件和分配给DPMI的内存也被存放在这里。
  • 系统共享区地址从3GB到4GB。这里是VMM和VXM存放的地方。
VMM为VxD程序提供三种VxD服务：
  • 页面内存服务 这种服务分配/管理页面大小为4KB的内存。这是提供的最低级的服务，其他所有的服务都是建立在页面内存服务上的。
  • 堆内存服务 管理小的内存块。这种高级别的内存管理服务建立在页面内存服务的基础上。
  • 表服务管理可用来实行链结表的固定大小的内存块。

处理中断

在保护模式下，中断指向中断描述表(IDT)。VMM通过VxD的帮助监视虚拟机的IDT。通常VMM处理IDT内几乎所有的中断入口。它进行第一级的中断处理：保存被中断程序的状态，把控制传送到第二级的中断处理，第二级的中断处理通常由各种VxD程序来进行实际的处理。当第二级中断处理程序完成了它的工作之后，它把控制转交给重分派程序，由重分派程序来恢复被中断程序的状态并从先前被中断的地方继续执行。
上面的描述太过简单。由于被中断的虚拟机的时间片可能已过，重分派也许不会马上执行。VxD程序通过VMM服务如：Set_PM_Int或Hook_V86_Int_Chain来安装中断处理。VxD程序不应该直接改动IDT中的中断入口(除非你很确切的知道将发生的后果)。

线程调度

VMM使用两个调度器组件来在虚拟机之间实现有优先级的多线程处理：
  • 主调度器
  • 时间片管理器或副调度器
主调度器的任务是选择有最高优先级的线程来执行。这种选择在VMM处理一个中断(如计时器中断)时进行。选择的结果决定了当VMM从中断服务返回时由哪一个线程/虚拟机获得控制权。主调度器工作的结果是确定的，一个线程要么获得控制权，要么没有，只有一个线程可以得到控制权。VMM和其他的VxD可以通过VMM服务来调整线程的执行优先级。例如，当一个硬中断发生时，VMM就会增加中断处理的执行优先级以便让它在尽可能短的时间内有更高的机会被调用。
副调度器通过主调度器提供的服务来给享有最高优先级的线程分配cpu时间。副调度器给每个线程一个时间片。当一个线程执行到它的时间片完结时，副调度器就增加下一个线程的优先级，这样它就会被主调度器选中并执行。

你可以从Walter Oney's Systems Programming for Windows 95和Windows 95 DDK 文档里面得到关于这个问题的细节。

由 Ryo 翻译，发表于 http://asm.yeah.net，英文版本来自 [Iczelion's Win32 Assembly Homepage]


虚拟设备驱动程序结构

现在大家对vmm和vxd有了一定的了解，接下来我们来看一看如何编写vxd代码。首先，你必须具备Windows 95/98 Device Driver Development Kit。Window95 ddk只有MSDN 订户才能拿到，但Windows98 ddk却可以免费从Microsoft公司取得。尽管Windows 98 ddk是面向WDM的，但你还是可以用它来开发VxD程序。你可以从 http://www.microsoft.com/hwdev/ddk/install98ddk.htm?下载Window98 ddk。
你可以下载整个软件包(大约30M)，也可以只下载你感兴趣的部分。如果你没有下载整个软件包，那么别忘了下载other.exe
里面的Window95 ddk documentation。Windows98 ddk 包含了6.11d版的MASM。你需要把它升级为最新版。如果你不知道到哪里去下载最新的版本，可以去我的主页上查一查。
Window9x DDK包含了一些Masm32包所不具有的重要库文件。
你可以在这里下载这一章的例子。

LE文件格式

VxD采用线性可执行文件格式(LE)。这种文件格式是为OS/2 2.0版设计的。它同时包含16位和32位代码，这点也是VxD程序的需要。回想VxD在Windows3.x的时代，在那时，从Dos启动Windows,Windows在把机器转到保护模式之前需要在实模式下做一些初始化。实模式的16位代码必须和32位代码一起放在可执行文件中。所以LE文件格式理所当然的选择。幸运的，Windows NT驱动程序不必在实模式下初始化，所以它们不必使用LE文件格式。它们用的是PE文件格式。

在LE文件中，代码和数据被存放在几类运行属性不同的段中。以下是一些可用的段类。

  • LCODE 页面锁定的代码和数据段 这种段被锁定在内存里。换句话说，这段永远不会被放到硬盘上去，所以你一定要谨慎的使用这种段类以免浪费宝贵的内存。那些每时每刻都必须放在内存中的代码和数据应该放在这个段里。尤其是那些硬件中断处理程序。
  • PCODE 可调页代码段 VMM可以对这种段实行调页处理，在这种段里的代码不必时刻放在内存里，当VMM需要物理内存的时候，它就会把这段放到硬盘上去。
  • PDATA 可调页数据段
  • ICODE 仅用于的初始化段 这种段里的代码仅仅用来进行VxD的初始化。当初始化完成后，VMM就把这段从内存中释放。
  • DBOCODE 仅用于调试的代码数据段 当你要调试VxD程序时，就要用到这种段里的代码和数据，例如，它包含要调试的消息的处理代码。
  • SCODE 静态代码和数据段 这种段时刻存在于内存中，即使VxD已经卸载，这种段对某些动态的VxD程序很有用，这些VxD程序需要在某一Windows进程里不停的加载/卸载而又要纪录上次的环境和状态。
  • RCODE 实模式初始化代码数据段 这种段包含实模式初始化需要的16位代码和数据。
  • 16ICODE 16ICODE USE16保护模式初始化数据段 这是一个16位的段，它包含VxD要从保护模式拷贝到V86模式的代码。例如，如果你要把一些V86的代码拷贝到一个虚拟机上时，你想拷贝的代码就要放在这里。如果你把它放在其他的段里，编译程序就会产生错误的代码，例如，它会产生32位代码而不是16位代码。
  • MCODE 锁定的消息字串 这种段包含了由VMM消息宏帮助编译的消息字串，这有助于你构造你的驱程的国际版本。
这并不意味着你的VxD程序必须包含以上所有的段，你可以选择你的VxD程序需要的段。例如，如果你的VxD程序不进行实模式初始化，那么就不必包含 RCODE 段。
大多数时候，你要用到 LCODE, PCODE 和 PDATA 段。作为一个VxD程序编写者，为你的代码和数据选择合适的段取决于你自己的判断。总的来说，你应该尽可能多的使用 PCODE 和 PDATA 因为这样VMM就可以在需要的时候把段调入调出内存。另外，硬件中断程序及其所用到的服务必须放在 LCODE 段里。
你不能直接地使用这些段类，你要用这些段类来定义段，这些段的定义被存放在模块定义文件(.def)中。下面是一个标准的模块定义文件：
VXD FIRSTVXD
SEGMENTS
    _LPTEXT     CLASS 'LCODE'    PRELOAD NONDISCARDABLE
    _LTEXT      CLASS 'LCODE'    PRELOAD NONDISCARDABLE
    _LDATA      CLASS 'LCODE'    PRELOAD NONDISCARDABLE
    _TEXT       CLASS 'LCODE'    PRELOAD NONDISCARDABLE
    _DATA       CLASS 'LCODE'    PRELOAD NONDISCARDABLE
    CONST       CLASS 'LCODE'    PRELOAD NONDISCARDABLE
    _TLS        CLASS 'LCODE'    PRELOAD NONDISCARDABLE
    _BSS        CLASS 'LCODE'    PRELOAD NONDISCARDABLE
    _LMGTABLE   CLASS 'MCODE'    PRELOAD NONDISCARDABLE IOPL
    _LMSGDATA   CLASS 'MCODE'    PRELOAD NONDISCARDABLE IOPL
    _IMSGTABLE  CLASS 'MCODE'    PRELOAD DISCARDABLE IOPL
    _IMSGDATA   CLASS 'MCODE'    PRELOAD DISCARDABLE IOPL
    _ITEXT      CLASS 'ICODE'    DISCARDABLE
    _IDATA      CLASS 'ICODE'    DISCARDABLE
    _PTEXT      CLASS 'PCODE'    NONDISCARDABLE
    _PMSGTABLE  CLASS 'MCODE'    NONDISCARDABLE IOPL
    _PMSGDATA   CLASS 'MCODE'    NONDISCARDABLE IOPL
    _PDATA      CLASS 'PDATA'    NONDISCARDABLE SHARED
    _STEXT      CLASS 'SCODE'    RESIDENT
    _SDATA      CLASS 'SCODE'    RESIDENT
    _DBOSTART   CLASS 'DBOCODE'  PRELOAD NONDISCARDABLE CONFORMING
    _DBOCODE    CLASS 'DBOCODE'  PRELOAD NONDISCARDABLE CONFORMING
    _DBODATA    CLASS 'DBOCODE'  PRELOAD NONDISCARDABLE CONFORMING
    _16ICODE    CLASS '16ICODE'  PRELOAD DISCARDABLE
    _RCODE      CLASS 'RCODE'
EXPORTS
    FIRSTVXD_DDB  @1
第一个声明定义了VxD的名称，一个VxD的名称必须是全部大写的，我曾经试过用小写，结果VxD除了把自己载入内存外什么也不干。
接下来是段的定义，段的定义包括三个部分：段的名称，段类和要求的段的运行属性。你可以看到很多段都基于相同的段类，例如，_LPTEXT, _LTEXT, _LDATA 都是基于 LCODE 段类而且属性也完全一样。这样定义段有利于使代码更容易理解。如：LCODE 可以包含代码和数据，对于一个程序员来说，如果他能把数据放到_LDATA 段里，把代码放到_LTEXT 段里，就会显得很容易理解。最后，这两个段都会被编译到最后的可执行程序的同一个段内。
一个VxD程序导出且仅导出一个标记：它的设备描述块(DDB)。DDB实际上是一个结构，它包含了VMM需要知道的所有的VxD信息。你必须在模块定义文件中导出DDB。
在大多数时候，你可以把上面的.DEF文件用到你的新建的VxD项目中去。你只要把.DEF文件里第一行和最后一行的VxD名字改掉就可以了。在一个汇编的VxD项目中，段的定义是不必要的，段的定义主要用于C的VxD项目编写，但用在汇编里也是可以的。你会得到一大堆警告的信息但是它能汇编成功。你也可以删掉你在你的项目里没有用到的段定义从而去掉这些讨厌的警告信息。
vmm.inc包含了许多用于定义你的源文件中的段的宏：
_LTEXT | VxD_LOCKED_CODE_SEG
_PTEXT | VxD_PAGEABLE_CODE_SEG
_DBOCODE | VxD_DEBUG_ONLY_CODE_SEG
_ITEXT | VxD_INIT_CODE_SEG
_LDATA | VxD_LOCKED_DATA_SEG
_IDATA | VxD_IDATA_SEG
_PDATA | VxD_PAGEABLE_DATA_SEG
_STEXT | VxD_STATIC_CODE_SEG
_SDATA | VxD_STATIC_DATA_SEG
_DBODATA | VxD_DEBUG_ONLY_DATA_SEG
_16ICODE | VxD_16BIT_INIT_SEG
_RCODE | VxD_REAL_INIT_SEG

每个宏都有它相对应的结束宏，例如，如果你要在你的源文件中定义一个_LTEXT 段，你应该这样写：

VxD_LOCKED_CODE_SEG

(把你的代码写在这里)

VxD_LOCKED_CODE_ENDS

VxD结构

现在你了解了LE文件里的段，我们可以继续来看一下源文件。你会发现VxD程序有一个特点，那就是它用了很多的宏。你可以看到在VxD中宏几乎无处不在，这都成为一个习惯了。这些宏用来隐藏一些底层的细节，也增加了源程序的可移植性。如果你有兴趣，你可以看一看像vmm.inc这一类的库文件中的这些宏的定义。
下面是VxD源文件结构：
.386p
include vmm.inc

DECLARE_VIRTUAL_DEVICE FIRSTVXD,1,0, FIRSTVXD_Control, UNDEFINED_DEVICE_ID, UNDEFINED_INIT_ORDER

Begin_control_dispatch FIRSTVXD
End_control_dispatch FIRSTVXD

end

这段源程序给人的第一印象就是：它并不像一个汇编源程序。那是因为它用了很多宏。让我们来分析一下源程序以便你能很快理解它。

.386p
告诉编译器我们要使用包括CPU特权指令的80386指令系统。你也可以使用.486p 或者.586p.
include vmm.inc
你的每个VxD源程序都必须包含imm.inc，因为它包含了你在源程序里所要用到的宏的定义。你还可以根据需要包含其他的库文件。
DECLARE_VIRTUAL_DEVICE FIRSTVXD,1,0, FIRSTVXD_Control, UNDEFINED_DEVICE_ID, UNDEFINED_INIT_ORDER
正如我们刚才说的，VMM通过VxD程序的设备描述块(DDB)来获取它所需要知道的关于VxD的所有信息。一个设备描述块是一个结构，它包含了许多关于VxD的重要信息，比如VxD的名字，它的设备ID，它的VxD服务函数入口(如果有的话)，等等。你可以在imm.inc里查一查这个结构，它被定义为 VxD_Desc_Block。你必须在.DEF 文件里导出这个结构。这个结构有22个数据，但是你只用填写其中的几个。然后vmm.inc包含的一个宏会为你初始化并填写这些数据。这个宏叫做 DECLARE_VIRTUAL_DEVICE。它的格式如下：

Declare_Virtual_Device Name, MajorVer, MinorVer, CtrlProc, DeviceID, InitOrder, V86Proc, PMProc, RefData

你可以看到：VxD源程序中的标号是不区分大小写的，你可以用大写，小写或者混合起来用都可以。让我们来看一下 Declare_virtual_device 里的每一个参数。

  • Name VxD的名字 最多8个字符。它必须是大写！在系统中的所有VxD程序里，它们的名字不能重复，每个VxD的名字应该是唯一的。这个宏同时也会根据这个名字产生DDB的名字，产生的办法就是：在这个名字的后面加上_DDB。所以如果你的VxD的名字是 FIRSTVXD, Declare_Virtual_Device 这个宏就会把DDB的名字定为 FIRSTVXD_DDB。记住，你还要在.DEF文件里导出DDB。所以你必须使DDB的名字和.DEF文件定义中的相同。
  • MajorVer 和 MinorVer 你的VxD的主要的和次要的版本。
  • CtrlProc 你的VxD程序的设备控制函数的名字。设备控制函数是一个接受和处理VxD程序的控制消息的函数。你可以把设备控制函数看作Window函数的等价物。既然我们要用 Begin_Control_Dispatch 这个宏来生成我们的设备控制函数，那么我们应该使用一个标准格式的名字，那就是 VxD的名字_Control。 Begin_Control_Dispatch 这个宏把_Control 加到它后面的那个名字上(而我们又通常把VxD的名字写在它后面)作为设备控制函数的名字，所以我们就应该把VxD的名字加上_Control 作为 CtrlProc 参数的值。
  • DeviceID 你的VxD程序的16位唯一标识符 当且仅当你的VxD程序需要处理以下情况时你需要用到这个ID:
    • 你的VxD程序导出一些供其他VxD程序使用的VxD服务。因为20H中断接口用设备ID来定位/区分VxD程序，所以一个唯一的ID对你的VxD程序是必要的。
    • Your VxD 你的VxD程序要在初始化中断2FH,1607H时通知实模式程序它的存在。
    • Some 一些实模式软件(TSR)要用中断2FH,1605H来加载你的VxD程序。
    如果你的VxD程序不需要一个唯一的设备ID，你可以把这一项设为 UNDEFINED_DEVICE_ID ，如果你需要它，你可以去Microsoft要一个。
  • InitOrder 初始化的顺序，简单的说，就是加载的顺序。VMM就按照这个次序来加载VxD程序。每个VxD程序都有一个加载次序号，例如：
      VMM_INIT_ORDER        EQU 000000000H
      DEBUG_INIT_ORDER      EQU 000000000H
      DEBUGCMD_INIT_ORDER   EQU 000000000H
      PERF_INIT_ORDER       EQU 000900000H
      APM_INIT_ORDER        EQU 001000000H

    你可以看到：VMM, DEBUG 和 DEBUGCMD 是首先加载的VxD程序，然后是 PERF 和 APM。初始化顺序值越低的VxD程序越先被加载。如果你的VxD程序在初始化时需要用到其他VxD程序提供的服务，那么你必须把初始化顺序的值设得比你所要调用的那个VxD程序的大，这样，当你的VxD程序加载时，你所要的VxD就已经在内存中为你准备好了。如果不想去管你的VxD的初始化顺序，就把这个参数填写为 UNDEFINED_INIT_ORDER 。

  • V86Proc 和 PMProc 你的程序可以导出供V86和保护模式程序使用的API，这两个参数就是用来填写这些API的地址。记住，VxD程序除了监控系统虚拟机外，还要监控一个或多个运行在DOS或者保护模式下的虚拟机程序。理所当然的，VxD程序要为DOS和保护模式程序提供API支持。如果你不导出这些API，你可以不填这两个参数。
  • RefData 输入输出监视器(IOS)要用到的参考数据。只有一种情况下你要用到这个参数：当你在为IOS编写一个层驱动程序时。否则，你可以不填这个参数。
接下来是 Begin_Control_Dispatch 宏。
Begin_control_dispatch FIRSTVXD
End_control_dispatch FIRSTVXD
这两个宏定义了设备控制函数，当VxD的控制消息发生时，VMM就调用这个函数。你必须填写设备控制函数名字的前半部分，在本例中，我们用的是 FIRSTVXD。这个宏会在你输入的前半部分后加上_Control 作为设备控制函数的名字。这个名字一定要和你在 Declare_virtual_device 宏中给参数 CtrlProc 填的名字一致。设备控制函数总是放在锁定段(VxD_LOCKED_CODE_SEG)内的。上面定义的设备控制函数什么也不干。你需要说明你的VxD程序要响应什么控制消息，以及处理这个消息的函数，你可以用 Control_Dispatch 宏来实现这一点。
Control_Dispatch message, function
例如，如果你的VxD程序只要处理 Device_Init 消息，你的设备控制程序要这样写：
Begin_Control_Dispatch FIRSTVXD
Control_Dispatch Device_Init, OnDeviceInit
End_Control_DispatchFIRSTVXD
OnDeviceInit 就是要处理 Device_Init 消息的函数的名字。你可以给你的函数取任何你想取的名字。
你可以用 end 直接地结束你的VxD源程序。
综上所述，一个VxD程序至少包含一个设备控制块和一个设备控制函数。你要用 Declare_Virtual_Device 宏来定义一个设备控制块，用 Begin_Control_Dispatch 宏来定义一个设备控制程序。你必须在.def文件中的 EXPORTS 下面填写设备控制块的名字，从而导出该设备控制块。

编译VxD

编译的过程和编译普通的win32程序一样。先调用ml.exe编译asm源文件，然后用link.exe来连接object文件。不同的地方是ml.exe和link.exe后所带的命令行参数不同。

ml -coff -c -Cx -DMASM6 -DBLD_COFF -DIS_32 firstvxd.asm

-coff 表明COFF数据格式
-c 只汇编，不调用连接程序来连接，这样我们就可以在调用link.exe的时候使用跟多的参数。
-Cx 保存公共，外部标记。
-D<text> 定义一个文本宏，例如，-DBLD_COFF定义了一个文本宏BLD_COFF，这个宏用来作为编译的条件。如果你有兴趣，你可以在库文件中查找BLD_COFF，自己亲眼看看它对汇编过程起什么作用。上面的命令行定义了三个文本宏：BLD_COFF，IS_32和MASM6。如果你对C编程熟悉的话，你会知道这些定义相当于完成以下功能：

#define BLD_COFF
#define IS_32
#define MASM6
link -vxd -def:firstvxd.def firstvxd.obj

-vxd 表明我们要根据obj文件来生成一个VxD文件。
-def:<.DEF file> 指定该VxD文件的模式定义文件。

我觉得用makefile很方便，如果你不喜欢用makefile，你也可以创建批处理文件来自动完成编译过程。我的makefile如下：

NAME=firstvxd

$(NAME).vxd:$(NAME).obj
link -vxd -def:$(NAME).def $(NAME).obj

$(NAME).obj:$(NAME).asm
ml -coff -c -Cx -DMASM6 -DBLD_COFF -DIS_32 $(NAME).asm

由 Ryo 翻译，发表于 http://asm.yeah.net，英文版本来自 [Iczelion's Win32 Assembly Homepage]


VxD 程序设计入门

We 我们在上一节学会了如何编写一个什么事也不做的VxD程序。在这一节里，我们要给它增加处理控制消息的功能。

VxD的初始化和结束

VxD程序分为两种：静态的和动态的。每种的加载方法都不同，接受到的初始化和结束的控制消息也不同。

静态VxD:

下列情况下，VMM加载一个静态VxD:
  • 一个实模式常驻程序通过调用中断2FH，1605H,来调用此VxD。
  • 此VxD在注册表中的如下位置有定义：
      HKEY_LOCAL_MACHINE\System\CurrentControlSet\Services\VxD\key\StaticVxD=VxD带路径文件名
  • 此VxD在system.ini中的[386enh]行下有定义：[386enh] section:

      device=VxD带路径文件名
在开发的时候，我建议你从system.ini载入VxD程序，因为这样如果你的VxD程序有错而导致Windows不能启动的话，你可以在Dos下修改system.ini，而如果你使用的注册表载入的办法，就无法修改了。
当VMM加载你的静态VxD程序时，你的VxD程序会按以下顺序接收到三个系统控制消息：
  • Sys_Critical_Init VMM在转入到保护模式后，开放中断前发出这个控制消息。大多数VxD程序到不要用到这个消息，除非：
    • 你的VxD程序要接管一些其他VxD程序或者保护模式程序要用到的中断。既然你处理这个消息的时候这个中断还没有打开，你就可以确定在你接管这个中断的时候此中断不会被调用。
    • 你的VxD程序为其他的VxD程序提供了一些VxD服务。例如，一些在你的VxD程序后加载的VxD程序在处理 Device_Init 控制消息时需要调用一些你的VxD服务，既然 Sys_Critical_Init 控制消息在 Device_Init 消息之前被发送，所以你应该在 Sys_Critical_Init 消息发送时初始化你的程序。
    如果你要对这消息进行处理，你应该尽可能快的做完初始化工作，以免太长的执行时间导致的硬中断丢失。(记住：中断还没打开)
  • Device_Init VMM在开放中断后发送此信息。大多数VxD程序都在得到这个消息时初始化。因为中断都开放了，所以耗时的操作也可以在这里执行而不必怕会导致硬中断的丢失。你可以在这时进行初始化(如果你需要的话）。
  • Init_Complete 在所有的VxD程序处理完 Device_Init 消息之后,VMM释放初始化段（ICODE和RCODE段类）之前，VMM发出这个控制消息。只有少数几个VxD要处理这个消息。
你的VxD程序在成功地初始化后，必须将返回标志清零，反之，必须在返回之前把返回标志设为出错信息。如果你的VxD不需要初始化，你就不必对这些消息进行处理。
当要结束静态VxD的时候，VMM发送如下的控制消息：
  • System_Exit2 当你的VxD程序收到这个消息，Windows95正在关闭系统，除了系统虚拟机所有其他虚拟机都已经退出了。尽管如此，CPU仍然处于保护模式下，在系统虚拟机上执行实模式编码也是安全的。在这时Kernel32.dll也已经被卸载了。
  • Sys_Critical_Exit2 当所有的VxD完成对System_Exit2的响应处理并且中断都被关闭后，你的VxD收到到这个消息。
许多VxD程序并不要响应这两个消息，除非你要为系统做转换到实模式的准备。要知道，当Window95关闭时，它进入到实模式。所以如果你的VxD程序对实模式影像做了一些会导致它不稳定的操作，它就需要在这时进行恢复。
你也许会感到奇怪：为什么这两个消息后面都跟着个“2" ”。这是因为：在VMM加载VxD程序的时候，它是按照初始化顺序值小的VxD先加载的顺序加载的，这样VxD程序就可以使用那些在它们之前加载的VxD程序提供的服务。例如，VxD2要用到VxD1中的服务，它就必须把它的初始化顺序值定义的比VxD小。加载的顺序是：
..... VxD1 ===> VxD2 ===> VxD3 .....
那么卸载的时候，理所当然的是初始化顺序值大的VxD程序先被卸载，这样他们仍然可以使用比它们后加载的那些VxD程序提供的服务。如上面的例子，次序是：
.... VxD3 ===> VxD2 ===> VxD1.....
在上边的例子中，如果VxD2在初始化时调用了VxD1中的某些服务，那么卸载时它可能也要再次用到一些VxD1中的服务。System_Exit2 和 Sys_Critical_Exit2 是反初始化顺序发送的。这表示，当VxD2接受到这些消息时，VxD1还没有被卸载，它仍可以调用VxD1的服务，而 System_Exit 和 Sys_Critical_Exit 消息不是按照反初始化顺序发送的。这意味着，你不能肯定你是否仍能调用在你之前加载的VxD提供的VxD服务。新一代的VxD程序不应该使用这些消息。
还有两种退出消息：
  • Device_Reboot_Notify2 告诉VxD程序VMM正在准备重新启动系统。这时候中断还是开放的。
  • Crit_Reboot_Notify2 告诉VxD程序VMM正在准备重新启动系统。这时候中断已经被关闭了。
到这里，你可以猜到还有 Device_Reboot_Notify 和 Crit_Reboot_Notify 消息，但它们并不是像“2”版本的消息一样按反初始化顺序发送的。

动态VxD:

动态VxD在Windows9x里可以动态的被加载和卸载。这个特点在Window3.x下是没有的。动态VxD程序的主要作用是用来支持某些动态的硬件设备的重装，比如：即插即用设备。尽管如此，你可以从你的Win32程序中加载/卸载它，也可以把它看作是你的程序的一个到ring-0的扩展。
上一节我们提到的例子是一个静态的VxD，你可以把它转换成一个动态的VxD，只要在.def文件中 VxD 标记的后面加上关键字 DYNAMIC。
VxD FIRSTVxD DYNAMIC
这就是你把一个静态VxD转换成一个动态的VxD所要做的一切。
一个动态的VxD可以按以下的方法被加载：
  • 把它放到你的Windows目录下的\SYSTEM\IOSUBSYS 目录中。在这个目录里的VxD会被输入输出监视器(ios)加载。这些VxD必须支持层设备驱动。所以用这种方法加载你的动态VxD并不是一个好办法。
  • 用VxD加载服务。 VxDLDR 是一个可以加载动态VxD的静态VxD。你可以在其他VxD里面或者在16位代码里面调用它的服务。
  • 用Win32应用程序里的 CreateFile API。你在调用CreateFile时，你的动态VxD要以下面的格式填写：

      \\.\VxD完整路径名

    例如，如果你要加载一个在当前目录下名为FirstVxD的动态VxD，你需要做如下的工作：

    .data
    VxDName db "\\.\FirstVxD.VxD",0
    ......
    .data?
    hDevice dd ?
    .....
    .code
    .....
    invoke CreateFile, addr VxDName,0,0,0,0, FILE_FLAG_DELETE_ON_CLOSE,0
    mov hDevice,eax
    ......
    invoke CloseHandle,hDevice
    ......

FILE_FLAG_DELETE_ON_CLOSE 这个标志用来说明该VxD在 CreateFile 返回的句柄关闭时被卸载。
如果你用CreateFile来加载一个动态VxD，那么这个动态VxD 必须处理 w32_DeviceIoControl 消息。当你的动态VxD第一次被CreateFile函数加载的时候，VWIN32 向你的VxD发出这个消息。你的VxD响应这个消息，返回时eax中的值必须为零。当应用程序调用DeviceIoControl API来与一个动态VxD通讯时，w32_DeviceIoControl 消息也被发送。我们会在下一章讲到DeviceIoControl 接口。
一个动态VxD在初始化时收到一个消息：
  • Sys_Dynamic_Device_Init
在结束时也收到一个控制消息：
  • Sys_Dynamic_Device_Exit
动态VxD不会收到 Sys_Critical_Init, Device_Init 和 Init_Complete 控制消息，因为这些消息是在系统虚拟机初始化时发送的。除了这三个消息，动态VxD能收到所有的控制消息，只要它还在内存里。它可以做静态VxD可以做的所有事情。简单的说，动态VxD除了加载机制和接收到的初始化/结束消息跟静态VxD不同以外，它能做静态VxD所能做的一切。

其它系统控制消息

当VxD在内存里的时候，除了接收和初始化及结束相关的消息外，它还要收到许多别的控制消息。有些消息是关于虚拟机管理器的，有的是关于各种事件的。例如，关于虚拟机的消息如下：
  • Create_VM
  • VM_Critical_Init
  • VM_Suspend
  • VM_Resume
  • Close_VM_Notify
  • Destroy_VM
选择地响应你所感兴趣的消息是你自己的责任。

在VxD内创建函数

你要在一个段里面定义你的函数。你应该首先定义一个段，然后把你的函数放进去。例如，如果你要把你的函数放到一个可调页段中。你应该先定义一个可调页段，像这样：
VxD_PAGEABLE_CODE_SEG

（你的函数写在这里）

VxD_PAGEABLE_CODE_ENDS

你可以在一个段里面插入多个的函数。作为一个VxD编写者，你必须决定每一个函数应该放到哪个段里面去。如果你的函数必须时刻存在于内存中，如某些硬件中断处理程序，就把它们放到锁定页面段里面，否则，你应该把它们放到可调页段。
你要用BeginProc和EndProc 宏来定义你的函数：
BeginProc 函数名

EndProc 函数名

使用BeginProc 宏还可以加上一些参数，想了解这些细节，你可以看看Win95 DDK的文档。大多数时候，你只用填写函数的名字就够了。
因为BeginProc-EndProc 宏比proc-endp 指令的功能要强，所以你应该用BeginProc-EndProc宏来代替proc-endp指令

VxD编程约定

寄存器的使用

你的VxD程序可以使用所有的寄存器，FS和GS。但是在改动段寄存器的时候一定要小心。尤其是，一定不要改动CS和SS的内容，除非你对将发生的事情有绝对的把握。你可以使用DS和ES，但一定要记住在返回时恢复它们初值。有两个特征位尤其重要：方向和中断特征位。不要长时间的屏蔽中断。还有如果你要改动方向特征位，不要忘了在返回之前恢复它的初值。

参数传递约定

VxD服务函数有两种调用约定：寄存器法和堆栈法。调用寄存器法服务函数时，你通过各种寄存器来传递服务函数的参数。并且，在调用完成后检查寄存器的值来看操作是否成功。不要总是以为在调用服务函数后主要寄存器的值还和以前一样。当调用堆栈法服务函数时，你把要传递的参数压栈，在eax得到返回值。堆栈调用法的服务函数保存ebx，esi，edi和ebp的值。许多寄存器调用法服务函数都源于Windows3.x的时代。在大多数时候，你可以通过名字来区分这两种服务函数，如果一个函数的名字一下划线开头，如_HeapAllocate，它就是一个堆栈法的服务函数（除了少数从 VWIN32.VxD 导出的函数）。如果函数名不是一下划线开头，它就是一个寄存器法的服务函数。

调用VxD服务函数

你可以通过 VMMCall 和 VxDCall 宏来调用VMM和VxD服务。这两个宏的语法是一样的。当你要调用VMM导出的VxD服务函数时，用 VMMCall。当你要用其它VxD程序导出的VxD服务函数时，用 VxDCall。
VMMCall service ; 调用寄存器法服务函数e
VMMCall _service, <argument list> ; 调用堆栈法服务函数
正如我在前面所讲的，VMMCall 和 VxDCall 分解出一个跟着一个双字的20h中断，这样用起来很方便。 当你调用堆栈法服务时，你必须用角括号把你的参数列括起来。
VMMCall _HeapAllocate, <<size mybuffer>, HeapLockedIfDP>
_HeapAllocate 是一个堆栈法服务函数。它有两个参数，我们必须用角括号把它们括起来。由于第一个参数是一个这个宏不能正确解释的表达式，所以我们又要用一个角括号把它括起来。

Flat地址

在老的编译工具里，当你使用 offset 操作符时，编译器和联接器会生成错误地址，所以VxD编写者用 offset flat:来代替 offset。imm.inc包括了一个使这更简单的宏：OFFSET32 来代替 offset flat:。所以如果你要用地址操作时，用 OFFSET32 来代替offset操作符。

注意： 当我写这篇教程的时候，我试了一下用 offset 操作符。它可以生成正确的地址。所以我想MASM6.14修正了这个bug。但是为了安全起见，你还是应该用 OFFSET32 宏来代替 offset。

由 Ryo 翻译，发表于 http://asm.yeah.net，英文版本来自 [Iczelion's Win32 Assembly Homepage]


VxD例程：消息框

在上一节教程里，我们讲了编写一个VxD程序的方法。现在我们要学以致用。 在这一节里，我们要编写一个静态VxD，这个静态VxD在一个虚拟机创建或销毁时就会弹出一个消息框。
在这里下载例子。

捕获虚拟机创建和结束事件

当一个虚拟机创建时，VMM发送Create_VM控制消息给所有的VxD程序。当一个虚拟机退出时，它也发送VM_Terminate和VM_Terminate2消息给所有的VxD程序。我们的工作很简单：在设备控制程序里处理Create_VM and VM_Terminate2消息。当我们的VxD程序收到这两个控制消息时，它在屏幕上弹出一个消息框。
当VxD程序收到Create_VM或者 VM_Terminate2消息时，该虚拟机的句柄保存在ebx中。一个虚拟记的句柄可以看作它的唯一的ID。每一个虚拟机都有它自己唯一的ID(虚拟机句柄)。你可以像使用进程ID一样使用虚拟机ID：调用函数时，把它当作参数传送。
更进一步的来看，一个虚拟机句柄实际上是一个指向虚拟机控制块(VMCB)的32位线性地址。
一个虚拟机控制块是一个包括了许多关于该虚拟机的重要属性的结构。它的定义如下：
    cb_s STRUC
    CB_VM_Status        DD ?
    CB_High_Linear      DD ?
    CB_Client_Pointer   DD ?
    CB_VMID             DD ?
    CB_Signature        DD ?
    cb_s ENDS
  • CB_VM_Status 包含了反映虚拟机状态的一些标志位。
  • CB_High_Linear 是一个指向在系统共享区（约3GB)的虚拟机镜像的一个线性地址。这个概念需要解释一下。在Window95下，一个VxD不能直接访问到V86区域，代替地，VMM把每个虚拟机的V86区域都映射到系统共享区。当一个VxD程序要访问或修改虚拟机中的V86区时，它就在虚拟机的高线性区域进行操作。例如，如果显存的地址是0B8000H，而你的VxD程序要访问这个区域。它就要把CB_High_Linear中的值上0B8000H，然后访问那个区域。你在高线性镜像区域所作的修改都会被保存到虚拟机中去，因为这两个区域共享一个页面目录入口。使用高线性镜像在它多数情况下非常有效，因为你甚至可以修改一个不是当前虚拟机的虚拟机。
  • CB_Client_Pointer 包含了客户寄存器结构的地址。一个客户寄存器结构包含了在一个虚拟机中被中断的V86或保护模式程序的所有寄存器的值。如果你的VxD程序要读取/改动V86或PM程序的状态，它可以改动客户寄存器结构里的值，当VMM返回执行该程序时，这些改动会被保存到程序里去。
  • CB_VMID 虚拟机的身份验证数字。当VMM创建一个虚拟机时，就给该虚拟机分配一个数字。系统虚拟机的VMID是1。
  • CB_Signature 包含字串“VMcb”。这个元素是用来检测虚拟机句柄是否有效的。

显示一个对话框

一个VxD程序可以通过Virtual Shell Device服务来同用户通讯。在这个例子里我们要用到其中的一个：SHELL_Message.
SHEll_Message是一个寄存器法的服务函数，通过寄存器来传送参数：
  • ebx 显示这个消息的虚拟机的句柄。
  • eax 消息框的标志位。你可以在shell.inc中查查它们的详细信息，它们都是以MB_开头的。
  • ecx 要显示的消息的32位线性地址。
  • edi 消息框的标题的32位线性地址。
  • esi 如果你要知道用户对你的消息框作的反应操作，就在这里填写返回函数的32位线性地址。如果你不想知道，就用NULL。
  • edx 用来传送你的返回函数所需要的参数（如果你在esi中填了这个函数的地址）。
返回后，如果调用成功，返回标志被清零，否则，返回标志被置位。

例子：

.386p
include VMM.inc
include shell.inc

DECLARE_VIRTUAL_DEVICE MESSAGE,1,0, MESSAGE_Control, UNDEFINED_DEVICE_ID, UNDEFINED_INIT_ORDER

Begin_control_dispatch MESSAGE
Control_Dispatch Create_VM, OnVMCreate
Control_Dispatch VM_Terminate2, OnVMClose
End_control_dispatch MESSAGE

VxD_PAGEABLE_DATA_SEG
MsgTitle db "VxD MessageBox",0
VMCreated db "A VM is created",0
VMDestroyed db "A VM is destroyed",0
VxD_PAGEABLE_DATA_ENDS

VxD_PAGEABLE_CODE_SEG
BeginProc OnVMCreate
mov ecx, OFFSET32 VMCreated
CommonCode:
VMMCall Get_sys_vm_handle
mov eax,MB_OK+MB_ICONEXCLAMATION
mov edi, OFFSET32 MsgTitle
xor esi,esi
xor edx,edx
VxDCall SHELL_Message
ret
EndProc OnVMCreate

BeginProc OnVMClose
mov ecx,OFFSET32 VMDestroyed
jmp CommonCode
EndProc OnVMClose
VxD_PAGEABLE_CODE_ENDS

end

分析:

Begin_control_dispatch MESSAGE
Control_Dispatch Create_VM, OnVMCreate
Control_Dispatch VM_Terminate2, OnVMClose
End_control_dispatch MESSAGE
此VxD程序处理两个控制消息，Create_VM 和 VM_Terminate2 当收到 Create_VM 控制消息时，它调用 OnVMCreate 函数。当收到 VM_Terminate2 消息时，它调用 OnVMClose 函数。
VxD_PAGEABLE_DATA_SEG
MsgTitle db "VxD MessageBox",0
VMCreated db "A VM is created",0
VMDestroyed db "A VM is destroyed",0
VxD_PAGEABLE_DATA_ENDS
我们把这些数据放在可调页段里面。
BeginProc OnVMCreate
mov ecx, OFFSET32 VMCreated
CommonCode:
VMMCall Get_sys_vm_handle
mov eax,MB_OK+MB_ICONEXCLAMATION
mov edi, OFFSET32 MsgTitle
xor esi,esi
xor edx,edx
VxDCall SHELL_Message
ret
EndProc OnVMCreate
我们用 BeginProc 和 EndProc 宏来创建OnVMCreate。OnVMCreate函数把调用 SHELL_Message 服务所需要的参数放到寄存器里面去。因为我们要在系统虚拟机上显示消息框，所以不能使用ebx中的值（ebx包含了在创建的虚拟机的句柄，而我们要的是系统虚拟机的句柄）。于是，我们用另一个VMM服务，Get_Sys_VM_Handle 来得到系统虚拟机的虚拟机句柄。我们分别把消息的地址和消息框标题的地址放在ecx和edi里面。我们不需要知道客户的反应，所以我们把esi和edx置零。当每个参数都在相应的寄存器内后，我们就调用 SHELL_Message 来显示消息框。
BeginProc OnVMClose
mov ecx,OFFSET32 VMDestroyed
jmp CommonCode
EndProc OnVMClose
OnVMClose OnVMClose函数本身是很简单的。因为它要使用的代码和 OnVMCreate 相同，所以它在用另一个消息的地址初始化ecx后，就转到 OnVMCreate 中的代码去了。

改变定义文件(.def)

VxD MESSAGE

SEGMENTS
    _LPTEXT      CLASS 'LCODE'    PRELOAD NONDISCARDABLE
    _LTEXT       CLASS 'LCODE'    PRELOAD NONDISCARDABLE
    _LDATA       CLASS 'LCODE'    PRELOAD NONDISCARDABLE
    _TEXT        CLASS 'LCODE'    PRELOAD NONDISCARDABLE
    _DATA        CLASS 'LCODE'    PRELOAD NONDISCARDABLE
    CONST        CLASS 'LCODE'    PRELOAD NONDISCARDABLE
    _TLS         CLASS 'LCODE'    PRELOAD NONDISCARDABLE
    _BSS         CLASS 'LCODE'    PRELOAD NONDISCARDABLE
    _LMGTABLE    CLASS 'MCODE'    PRELOAD NONDISCARDABLE IOPL
    _LMSGDATA    CLASS 'MCODE'    PRELOAD NONDISCARDABLE IOPL
    _IMSGTABLE   CLASS 'MCODE'    PRELOAD DISCARDABLE IOPL
    _IMSGDATA    CLASS 'MCODE'    PRELOAD DISCARDABLE IOPL
    _ITEXT       CLASS 'ICODE'    DISCARDABLE
    _IDATA       CLASS 'ICODE'    DISCARDABLE
    _PTEXT       CLASS 'PCODE'    NONDISCARDABLE
    _PMSGTABLE   CLASS 'MCODE'    NONDISCARDABLE IOPL
    _PMSGDATA    CLASS 'MCODE'    NONDISCARDABLE IOPL
    _PDATA       CLASS 'PDATA'    NONDISCARDABLE SHARED
    _STEXT       CLASS 'SCODE'    RESIDENT
    _SDATA       CLASS 'SCODE'    RESIDENT
    _DBOSTART    CLASS 'DBOCODE'  PRELOAD NONDISCARDABLE CONFORMING
    _DBOCODE     CLASS 'DBOCODE'  PRELOAD NONDISCARDABLE CONFORMING
    _DBODATA     CLASS 'DBOCODE'  PRELOAD NONDISCARDABLE CONFORMING
    _16ICODE     CLASS '16ICODE'  PRELOAD DISCARDABLE
    _RCODE       CLASS 'RCODE'

EXPORTS

    MESSAGE_DDB  @1

汇编过程：

ml -coff -c -Cx -DMASM6 -DBLD_COFF -DIS_32 message.asm

link -VxD -def:message.def message.obj

VxD 的安装：
  • 把message.VxD放到\system目录下。
  • 在system.ini文件里的[386enh]部分里加上如下的一行：
      device=message.VxD
  • 重启你的计算机。

    • 测试这个VxD：

    打开一dos窗口，你会看到弹出的消息框，显示“A VM is created."。当你关闭一个dos窗口，又会弹出一个消息框显示“A VM is destroyed"。

    由 Ryo 翻译，发表于 http://asm.yeah.net，英文版本来自 [Iczelion's Win32 Assembly Homepage]


DeviceIoControl Interface

DeviceIoControl接口

在这一节中我们将要关于学习动态VXD，特别是如何创建，加载和使用。

点击这里下载例子

VxD接口

VxD总共提供了4种接口。

l VxD services VxD服务

l V86 Interface V86接口

l Protected-mode (PM) Interface 保护模式接口

l Win32 DeviceIoControl Interface Win32设备输入输出控制接口

我们已经知道了VxD服务，V86和保护模式接口是由V86和保护模式程序调用的。因为V86和保护模式程序是16位的，我们不能在Win32应用程序中使用那两种接口。在Windows 95中，微软给Win32应用程序加了另外一个接口所以Win32应用程序可以调用VxD的服务：DeviceIoControl接口（设备输入输出控制接口）

DeviceIoControl接口

简单的说，DeviceIoControl接口是一种为Win32程序准备的调用VxD内部函数的方法。不要混淆DeviceIoControl接口调用函数和用VxD服务调用函数，这两种方法是不一样的。比如说，DeviceIoControl function1 也许和Vxd service1是不一样的。你应给把DeviceIoControl函数作为一种只为Win32应用程序提供的单独的函数。

在Win32程序方面：

首先用CreateFile来打开/加载一个VxD。如果调用成功的话，VxD将会创建/加再到内存中并且CreateFile把VxD的句柄返回到eax中。

接着你调用DeviceIoControlAPI函数来选择要运行的函数。DeviceIoControl函数遵循下面的语法：

DeviceIoControl PROTO hDevice:DWORD,\

dwIoControlCode:DWORD,\

lpInBuffer:DWORD,\

nInBufferSize:DWORD,\

lpOutBuffer:DWORD,\

nOutBufferSize:DWORD,\

lpBytesReturned:DWORD,\

lpOverlapped:DWORD

l hDevice 是从CreateFile返回的VxD句柄。

l dwIoControlCode 是用来制定VxD将要进行的操作。你应该在你要选用那种操作之前得到可能的dwIoControlCode值得列表。

l lpInBuffer 是包含了VxD完成dwIoControlCode所制定操作的数据的缓冲区地址。如果这个操作不需要数据，你可以传为NULL。

l nInBufferSize 是由 lpInBuffer 所指向的缓冲区的地址的大小（byte）。

l lpOutBuffer 是VxD程序在操作成功之后要将输出数据输出到的缓冲区。如果这个操作没有任何返回值，这个值可以为NULL。

l nOutBufferSize 是 lpOutBuffer 所指向的缓冲区的大小（byte）。

l lpBytesReturned是一个dword型变量的地址。这个变量用来接收VxD在 lpOutBuffer中写入数据的大小。

l 如果你想要把操作设成异步的，lpOverlapped 是一个OVERLAPPED结构的指针。如果你要一直等直到操作完成，这个值为NULL。

在VxD方面：

VxD程序必须处理w32_deviceIoControl消息。当VxD收到w32_deviceIoControl消息，它的寄存器是如下值：

l ebx 是VM的句柄。

l esi 是指向 DIOCParams结构的指针。DIOCParams包含了从win32程序传送的信息。

DIOCParams是按照如下定义的：

DIOCParams STRUC

Internal1 DD ?

VMHandle DD ?

Internal2 DD ?

dwIoControlCode DD ?

lpvInBuffer DD ?

cbInBuffer DD ?

lpvOutBuffer DD ?

cbOutBuffer DD ?

lpcbBytesReturned DD ?

lpoOverlapped DD ?

hDevice DD ?

tagProcess DD ?

DIOCParams ENDS

l Internal1 是指向Win32应用应用程序用户寄存器结构的指针。

l VMHandle 虚拟机句柄

l Internal2 是指向设备描述块(DDB)的句柄。

l dwIoControlCode, lpvInBuffer, cbInBuffer, lpvOutBuffer, cbOutBuffer, lpcbBytesReturned, lpOverlapped是传送到DeviceIoControl API的参数。

l hDevice是 ring-3级设备句柄。

l tagProces 是过程的标签。

在DIOCParams结构中有所有从Win32应用程序传送到你的VxD的信息。

你的VxD至少要处理DIOC_Open(传送到dwIoControlCode),那是当Win32程序调用CreateFile打开你的VxD时VWIN32发送给你的VxD的。如果你的VxD准备好了，它必须在eax中返回0而且CreateFile也会成功。如果你的VxD没有准备好，它必须在eas中返回一个非零值而且CreateFile也会失败。除了DIOC_Open,当Win32程序关闭这个设备句柄时，你的VxD将会从VWIN32收到DIOC_Closehandle。

能由CreateFile加载的最小的动态VxD框架：

.386p
include vmm.inc
include vwin32.inc

DECLARE_VIRTUAL_DEVICE DYNAVXD,1,0, DYNAVXD_Control,\
UNDEFINED_DEVICE_ID, UNDEFINED_INIT_ORDER

Begin_control_dispatch DYNAVXD
Control_Dispatch w32_DeviceIoControl, OnDeviceIoControl
End_control_dispatch DYNAVXD

VxD_PAGEABLE_CODE_SEG
BeginProc OnDeviceIoControl
assume esi:ptr DIOCParams
.if [esi].dwIoControlCode==DIOC_Open
xor eax,eax
.endif
ret
EndProc OnDeviceIoControl
VxD_PAGEABLE_CODE_ENDS

end

;--------------------------------------------------------------------------------------------------------------------------------
; Module Definition File
;---------------------------------------------------------------------------------------------------------------------------------

VXD DYNAVXD DYNAMIC

SEGMENTS
    _LPTEXT      CLASS 'LCODE'    PRELOAD NONDISCARDABLE
    _LTEXT       CLASS 'LCODE'    PRELOAD NONDISCARDABLE
    _LDATA       CLASS 'LCODE'    PRELOAD NONDISCARDABLE
    _TEXT        CLASS 'LCODE'    PRELOAD NONDISCARDABLE
    _DATA        CLASS 'LCODE'    PRELOAD NONDISCARDABLE
    CONST        CLASS 'LCODE'    PRELOAD NONDISCARDABLE
    _TLS         CLASS 'LCODE'    PRELOAD NONDISCARDABLE
    _BSS         CLASS 'LCODE'    PRELOAD NONDISCARDABLE
    _LMGTABLE    CLASS 'MCODE'    PRELOAD NONDISCARDABLE IOPL
    _LMSGDATA    CLASS 'MCODE'    PRELOAD NONDISCARDABLE IOPL
    _IMSGTABLE   CLASS 'MCODE'    PRELOAD DISCARDABLE IOPL
    _IMSGDATA    CLASS 'MCODE'    PRELOAD DISCARDABLE IOPL
    _ITEXT       CLASS 'ICODE'    DISCARDABLE
    _IDATA       CLASS 'ICODE'    DISCARDABLE
    _PTEXT       CLASS 'PCODE'    NONDISCARDABLE
    _PMSGTABLE   CLASS 'MCODE'    NONDISCARDABLE IOPL
    _PMSGDATA    CLASS 'MCODE'    NONDISCARDABLE IOPL
    _PDATA       CLASS 'PDATA'    NONDISCARDABLE SHARED
    _STEXT       CLASS 'SCODE'    RESIDENT
    _SDATA       CLASS 'SCODE'    RESIDENT
    _DBOSTART    CLASS 'DBOCODE'  PRELOAD NONDISCARDABLE CONFORMING
    _DBOCODE     CLASS 'DBOCODE'  PRELOAD NONDISCARDABLE CONFORMING
    _DBODATA     CLASS 'DBOCODE'  PRELOAD NONDISCARDABLE CONFORMING
    _16ICODE     CLASS '16ICODE'  PRELOAD DISCARDABLE
    _RCODE       CLASS 'RCODE'

EXPORTS
    DYNAVXD_DDB  @1

完整例子：

下面是一段加载动态VxD并且通过DeviceIoControl API 来调用VxD内部函数的Win32应用程序的源代码。

; VxDLoader.asm

.386
.model flat,stdcall
include windows.inc
include kernel32.inc
includelib kernel32.lib
include user32.inc
includelib user32.lib

.data
AppName db "DeviceIoControl",0
VxDName db "\\.\shellmsg.vxd",0
Success db "The VxD is successfully loaded!",0
Failure db "The VxD is not loaded!",0
Unload db "The VxD is now unloaded!",0
MsgTitle db "DeviceIoControl Example",0
MsgText db "I'm called from a VxD!",0
InBuffer dd offset MsgTitle
dd offset MsgText
.data?
hVxD dd ?
.code
start:
invoke CreateFile,addr VxDName,0,0,0,0,FILE_FLAG_DELETE_ON_CLOSE,0
.if eax!=INVALID_HANDLE_VALUE
mov hVxD,eax
invoke MessageBox,NULL,addr Success,addr AppName,MB_OK+MB_ICONINFORMATION
invoke DeviceIoControl,hVxD,1,addr InBuffer,8,NULL,NULL,NULL,NULL
invoke CloseHandle,hVxD
invoke MessageBox,NULL,addr Unload,addr AppName,MB_OK+MB_ICONINFORMATION
.else
invoke MessageBox,NULL,addr Failure,NULL,MB_OK+MB_ICONERROR
.endif
invoke ExitProcess,NULL
end start

下面这段源代码是由 vxdloader.asm 调用的动态VxD。
; ShellMsg.asm

.386p
include vmm.inc
include vwin32.inc
include shell.inc

DECLARE_VIRTUAL_DEVICE SHELLMSG,1,0, SHELLMSG_Control,\
UNDEFINED_DEVICE_ID, UNDEFINED_INIT_ORDER

Begin_control_dispatch SHELLMSG
Control_Dispatch w32_DeviceIoControl, OnDeviceIoControl
End_control_dispatch SHELLMSG

VxD_PAGEABLE_DATA_SEG
pTitle dd ?
pMessage dd ?
VxD_PAGEABLE_DATA_ENDS

VxD_PAGEABLE_CODE_SEG
BeginProc OnDeviceIoControl
assume esi:ptr DIOCParams
.if [esi].dwIoControlCode==DIOC_Open
xor eax,eax
.elseif [esi].dwIoControlCode==1
mov edi,[esi].lpvInBuffer
;-----------------------------------
; copy the message title to buffer
;-----------------------------------
VMMCall _lstrlen, <[edi]>
inc eax
push eax
VMMCall _HeapAllocate,<eax,HEAPZEROINIT>
mov pTitle,eax
pop eax
VMMCall _lstrcpyn,<pTitle,[edi],eax>
;-----------------------------------
; copy the message text to buffer
;-----------------------------------
VMMCall _lstrlen, <[edi+4]>
inc eax
push eax
VMMCall _HeapAllocate,<eax,HEAPZEROINIT>
mov pMessage,eax
pop eax
VMMCall _lstrcpyn,<pMessage,[edi+4],eax>
mov edi,pTitle
mov ecx,pMessage
mov eax,MB_OK
VMMCall Get_Sys_VM_Handle
VxDCall SHELL_sysmodal_Message
VMMCall _HeapFree,pTitle,0
VMMCall _HeapFree,pMessage,0
xor eax,eax
.endif
ret
EndProc OnDeviceIoControl
VxD_PAGEABLE_CODE_ENDS

end

分析：

我们从VxDLoader.asm开始。

Invoke CreateFile,addrVxDName,0,0,0,0,FILE_FLAG_DELETE_ON_CLOSE,0
.if eax!=INVALID_HANDLE_VALUE
mov hVxD,eax
....
.else
invoke MessageBox,NULL,addr Failure,NULL,MB_OK+MB_ICONERROR
.endif

我们调用CreateFile来加载动态VxD。注意 FILE_FLAG_DELETE_ON_CLOSE 标记。当从CreateFile返回的VxD句柄被关闭的时候，这个标志通知Windows卸载VxD。如果CreateFile成功，我们把VxD句柄保存起来。

invoke MessageBox,NULL,addr Success,addr AppName,MB_OK+MB_ICONINFORMATION
invoke DeviceIoControl,hVxD,1,addr InBuffer,8,NULL,NULL,NULL,NULL
invoke CloseHandle,hVxD
invoke MessageBox,NULL,addr Unload,addr AppName,MB_OK+MB_ICONINFORMATION

当VxD加载/卸载的时候，这个程序会显示一个消息框。它令dwIoControlCode=1然后调用DeviceIoControl。将InBuffer的地址传给lpInBuffer，将InBuffer的大小传给nInBufferSize。InBuffer是一个包括两个元素的数组：每个元素都是一个字符串的地址。

MsgTitle db "DeviceIoControl Example",0
MsgText db "I'm called from a VxD!",0
InBuffer dd offset MsgTitle
dd offset MsgText

现在我们看一下这段VxD。

它只处理w32_deviceIoControl消息。当w32_deviceIoControl消息发送的时候，调用OnDeviceIoControl函数。

BeginProc OnDeviceIoControl
assume esi:ptr DIOCParams
.if [esi].dwIoControlCode==DIOC_Open
xor eax,eax

OnDeviceIoControl 处理DIOC_Open，再eas中返回0。

.elseif [esi].dwIoControlCode==1
mov edi,[esi].lpvInBuffer

它也处理control code 等于1。它做的第一件事是取出在lpyInBuffer中的数据。这个数据是传送到DeviceIoControl API 的lpInBuffer中的两个dword值。它把指向dword数组的地址放到edi中。第一个dword是作为消息框标题的字符串地址。第二个dword是作为消息框文本的字符串地址。

;-----------------------------------
; copy the message title to buffer
;-----------------------------------
VMMCall _lstrlen, <[edi]>
inc eax
push eax
VMMCall _HeapAllocate,<eax,HEAPZEROINIT>
mov pTitle,eax
pop eax
VMMCall _lstrcpyn,<pTitle,[edi],eax>

它调用VMM服务lstrlen来计算消息框标题的长度。lstrlen在eax中返回字符串的长度。我们把这个长度加1来包括结束标记NULL。下一步我们通过调用HeapAllocate来分配一块足够大可以容纳字符串和它的结束标记NULL内存。加上HEAPZEROINIT标记使HeapAllocate将这块内存清零。HeapAllocate在eax中返回这块内存的地址。我们然后从win32 app的地址空间把字符串拷贝到我们申请的内存中。我们对要做消息框文本的字符串做同样的操作。

mov edi,pTitle
mov ecx,pMessage
mov eax,MB_OK
VMMCall Get_Sys_VM_Handle
VxDCall SHELL_sysmodal_Message

我们把标题和文本的地址分别存在edi和ecx中。把想要的标记放在eax中，通过调用Get_Sys_VM_handle得到系统VM的VM 句柄。然后调用SHELL_sysbodal_Message 。SHELL_sysModal_Message是系统SHELL_Message的模式版本。它冻结系统直到用户对消息框做出反应。

VMMCall _HeapFree,pTitle,0
VMMCall _HeapFree,pMessage,0

当SHELL_sysmodal_Message返回时，我们用_HeapFree释放内存。

总结：

DeviceIoControl接口使你的win32应用程序使用动态VxD作为一个ring-0 DLL扩展非常理想。

[Iczelion's Win32 Assembly Homepage]


Iczelion's win32 asm tutorial

请求执行时间段与Shell函数

下载例子程序

理论:

请求执行时间段通常被简称为"appy time"。是指当系统VM稳定到充许VxDs和ring-3级别的应用软件（特别是16-bit的应用软件）交互时的时间段。例如，在一个特定时间段，VxDs能加载并调用在16-bit Dlls中的函数。这个appy time在Windows 3.x中是无效的。在Windows3.x，一个VxD能包含在16-bit DLLs中的任意函数的地址，并模拟一个远调用到这个地址。然而，因为造成了VMM重入，这个操作将中断所有正在ring-3中执行的任务。所以能被VxDs能调用的APIs被要求是中断安全的，象PostMessage。在Windows 95，一个VxD在appy time的帮助下能调用任意一个在16-bit DLLs中的函数。

假如你的VxDs被通知正处在appy time，它就能加载16-bit DLLs并调用其中的函数。VxDs怎么知道appy time到来了呢？这就要使用Shell VxD请求一个appy time。当系统VM在稳定状态，Shell VxD将调用某VxD的一个回调函数，此函数是在VxD请求appy time时指定的。Shell VxD发生一次appy time事件仅仅调用一次你的回调函数。这就象找工作。你到职业介绍所，登记你的名字和电话号码。当你回到家，如有一个工作适合你，职业介绍所将电话通知你这个好消息。当你收到了这个消息，他们就不再通知你了。

在一个appy time起作用前要花上一些时间进行相关处理。appy time事件以下环境中将不起作用：

1、系统启动或关机时。
2、当系统VM在临界段或等待一个信号量时。

管理一个appy time事件

你可以通过调用_SHELL_CallAtAppyTime来注册一个appy time事件，它的定义如下：

VxDCall _SHELL_CallAtAppyTime, <<OFFSET32 pfnCallback>, dwRefData, dwFlags, dwTimeout>

  • pfnCallBack -- 当appy time事件发生时你要Shell VxD调用的回调函数的平板地址。这个函数接收两个参数，dwRefData和dwFlags，与你传送给_SHELL_CallAtAppyTime的两个一样。记住，Shell VxD采用C调用顺序调用你的回调函数。总而言之，你要象这样定义你的回调函数：

    BeginProc OnAppyTime, CCALL, PUBLIC
    ArgVar dwRefData,DWORD ; declare argument name and type
    ArgVar dwFlags, DWORD
    EnterProc
    你的代码...
    LeaveProc
    Return
    EndProc OnAppyTime
  • dwRefData -- 你要Shell VxD传送给你的回调函数的参考数据。可以是你想要的任何东西。
  • dwFlags -- 事件标志。

    如下值之一：
    CAAFL_RING0 ring-0事件
    CAAFL_TIMEOUT 由dwTimeout指定的时间到期事件。

    假如你只想在一定的时间内等待appy time事件，使用CAAFL_TIMEOUT标志。如你想一直等待appy time事件，使用NULL。CAAFL_RING0作用不明。
  • dwTimeout -- 在appy time事件发生前，VxD能等待时间段长度。时间段的单位不明。

这个服务是异步的，意味着你为appy time事件注册回调函数之后立即返回。

如果这个服务调用是成功的，在eax中返回appy time事件句柄。假如调用失败，在eax返回0。

你可以调用_SHELL_CancelAppyTimeEvent来撤消appy time事件注册，它仅有一个参数，就是由_SHELL_CallAtAppyTime返回的appy time事件句柄。

你应当在appy time事件到来时检查系统。例如，当你在系统关闭时注册appy time会发生什么？你的VxD的回调函数将不会得到调用！当appy time事件到来时，你应当调用_SHELL_QueryAppyTimeAvailable来查询系统状态。这个服务没有参数。如果appy time无效，在eax中返回0，例如：当系统关闭时或消息服务程序产生一个一般保护性错误时。

这个服务不会告诉你现在是不是appy time：它仅仅告诉你可能有一个appy time事件到来。简而言之，如果你想安全运行，首先调用_SHELL_QueryAppyTimeAvailable并检查eax中的值是否非零值，然后才可继续调用_SHELL_CallAtAppyTime。

请求执行时间段Shell服务

当appy time到达时，你可以使用几个Shell服务调用：

  • _SHELL_CallDll
  • _SHELL_FreeLibrary
  • _SHELL_GetProcAddress
  • _SHELL_LoadLibrary
  • _SHELL_LocalAllocEx
  • _SHELL_LocalFree

使用提供的这6个服务，VxDs可以调用在16-bit DLLs/EXE中的16-bit函数，象WinHelp。然而，我们马上要进步到32-bit时代（未来是64-bit） ，所以我不会仔细研究它们。如你对此感兴趣，你可以在Windows 95/98DDK文档中了解它们。

另外一些只请求执行时间段服务我想更有用处：_SHELL_ShellExecute和 _SHELL_BroadcastSystemMessage。使用_SHELL_BroadcastSystemMessage，你能在一次调用中发送消息到顶端的窗口和所有的VxDs。如果appy time有效，你可以发送消息到窗口和VxDs。如果appy time无效，你只能发送消息到VxDs。

_SHELL_ShellExecute是在ring-3中的函数ShellExecute在ring-0中的对应函数。实际上，它调用ring-3中的ShellExecute完成这个工作。使用外壳服务，你可以运行/打开/打印任意文件。

_SHELL_ShellExecute的定义如下所示：

VxDCall _SHELL_ShellExecute,<OFFSET32 ShexPacket>

它仅有一个参数，SHEXPACKET结构的平板地址。它从ShellExecute函数中返回一个值到eax中。让我们研究一下SHEXPACKET结构：

shex_dwTotalSize SHEXPACKET结构的字节数加上可选的参数rgchBaggage的大小，它直接跟随在这个结构之后。我等一下描述rgchBaggage。

  • shex_dwTotalSize SHEXPACKET结构的字节数加上可选的参数rgchBaggage的大小，它直接跟随在这个结构之后。我等一下描述rgchBaggage。
  • shex_dwSize SHEXPACKET结构的字节数，不包括rgchBaggage。结合上面的shex_dwTotalSize值，外壳VxD能计算任意长度的rgchBaggage的大小。
  • shex_ibOp 你要完成的操作。假如你指定0，意味着你想要打开一个文件。如果是一个可执行文件，就运行它。如果你想要完成其他的操作，你必须在rgchBaggage中指定操作的名字，在这个域中，必须包括从这个SHEXPACKET结构开始到一个ASCII字符串的距离，此距离大小以字节计数，字符串指定你要完成的操作的名称。SHEXPACKET结构的大小是32字节。如果操作字符串紧跟在SHEXPACKET结构后，shex_ibOp中的值必须是32。要知道你可以完成的操作，请查看ShellExecute服务。有三个操作被定义，"open"、"print"和"explore"。
  • shex_ibFile 从此结构开始到一个ASCII字符串的距离，这个字符串是你想要传递给ShellExecute的文件名，就象Shex_ibOp成员。
  • shex_ibParams 你想传递到由shex_ibFile指定的文件的可选参数。假如此文件是一个文档文件，你不想传递任何参数给它，就用0。假如你想传递参数给那个文件，在把从此结构开始到此成员指定的字符串的距离放在这个成员中。简而言之，就象shex_ibOp和shex_ibFile成员。
  • shex_ibDir 工作目录。假如你想使用Windows目录就指定0，否则指定其为在此结构后的首选目录名字符串，并把从此结构开始到目录名字符串的距离值放到这个成员中。
  • shex_dwReserved 如其名字所指，它是保留的。不要理它。
  • shex_nCmdShow 应用程序窗口怎样被显示。这是一个你正常传递给ShowWindow的值，比如，SW_XXXX值。可在windows.inc中查看这些值。

所有成员的大小都是双字的。在这里我介绍刚才我承诺的rgchBaggage成员。仅有一点不同，因为其大小是可变的，所以它作为SHEXPACKET结构的一员却不能包括在此结构的定义中。查看shell.inc，你看到rgchBaggage并不在SHEXPACKET结构的定义中，尽管在Windows 9x DDK文档中，声明其是SHEXPACKET结构的一员。

rgchBaggage是什么？简单说来其是跟在SHEXPACKET结构后的一个字符串数组。在这个数组中，你可以把你要对文件完成的操作名称、文件的名字、你要传递给文件的参数和工作目录放在其中。首先得到从SHEXPACKET结构到这些字符串的第一个字符之间的距离（即此结构的一些成员值），再把这些值加上SHEXPACKET结构的平板编移量，Shell VxD就能得到这些字符串在rgchBaggage数组中的偏移量。例如，假如SHEXPACKET结构从60000h开始，字符串紧跟其后，结构与字符串之间的距离是结构本身的大小，32字节（20h）。所以Shell VxD知道字符串定位在60020h。

例子

这是一个显示如何注册一个appy time事件并且使用_SHELL_ShellExecute的例子。VxD是动态的，被一个简单的Win32应用程序使用。当一个用户按下"run Calculator"按纽，win32应用程序调用DeviceIoControl去要求VxD注册一个appy time事件并且运行在Windows目录中的calc.exe。

;---------------------------------------------------------------------------------
;                                   VxD Source Code
;---------------------------------------------------------------------------------
.386p
include \masm\include\vmm.inc
include \masm\include\vwin32.inc
include \masm\include\shell.inc
VxDName TEXTEQU
ControlName TEXTEQU
VxDMajorVersion TEXTEQU <1>
VxDMinorVersion TEXTEQU <0>

VxD_STATIC_DATA_SEG
VxD_STATIC_DATA_ENDS

VXD_LOCKED_CODE_SEG
;----------------------------------------------------------------------------
; Remember: The name of the vxd MUST be uppercase else it won't work/unload
;----------------------------------------------------------------------------
DECLARE_VIRTUAL_DEVICE %VxDName,%VxDMajorVersion,%VxDMinorVersion, %ControlName,UNDEFINED_DEVICE_ID,UNDEFINED_INIT_ORDER

Begin_control_dispatch %VxDName
        Control_Dispatch W32_DEVICEIOCONTROL, OnDeviceIoControl
End_control_dispatch %VxDName

BeginProc OnDeviceIoControl
 assume esi:ptr DIOCParams
 .if [esi].dwIoControlCode==1
      VxDCall _SHELL_CallAtAppyTime,<,0,0,0>
 .endif
 xor eax,eax
 ret
EndProc OnDeviceIoControl
VXD_LOCKED_CODE_ENDS

VXD_PAGEABLE_CODE_SEG
BeginProc OnAppyTime, CCALL
 ArgVar RefData,DWORD
 ArgVar TheFlag,DWORD
 EnterProc
 mov File.shex_dwTotalSize,sizeof SHEXPACKET
 add File.shex_dwTotalSize,sizeof EXEName
 mov File.shex_dwSize,sizeof SHEXPACKET
 mov File.shex_ibOp,0
 mov File.shex_ibFile,sizeof SHEXPACKET
 mov File.shex_ibParams,0
 mov File.shex_ibDir,0
 mov File.shex_dwReserved,0
 mov File.shex_nCmdShow,1
 VxDCall _SHELL_ShellExecute,
 LeaveProc
 Return
EndProc OnAppyTime
VXD_PAGEABLE_CODE_ENDS

VXD_PAGEABLE_DATA_SEG
 File SHEXPACKET <>
 EXEName db "calc.exe",0
VXD_PAGEABLE_DATA_ENDS

end

讲解

VxD等待一个DeviceIoControl消息：第1号服务。当它收到上述消息，它将注册一个appy time事件。

VxDCall _SHELL_CallAtAppyTime,<<OFFSET32 OnAppyTime>,0,0,0>

它将传送OnAppyTime函数的平板地址到_SHELL_CallAtAppyTime，这样，当appy time事件发生时，Shell VxD将调用它。因我们不需要使用任何参考数据并且不需要处理过期情况，所以紧跟OnAppyTime函数的三个参数都是0。

当appy time事件发生时， Shell VxD调用OnAppyTime函数。

BeginProc OnAppyTime, CCALL

我们用BeginProc描述一个函数。因为Shell VxD用C调用顺序调用OnAppyTime，我们需要指定CCALL属性。

ArgVar RefData,DWORD
ArgVar TheFlag,DWORD
EnterProc
...
LeaveProc
Return

因为Shell VxD用两个参数调用OnAppyTime，我们必须设置堆栈结构。ArgVar宏用来调整每个要传递到函数的参数的堆栈结构。它的语法如下：

ArgVar varname, size, used

varname是参数的名字。你可以使用你喜欢的任意名字。size是参数的大小。你可以使用BYTE、WORD、DWORD或1,2,4。used通常被忽略。

紧接着ArgVar宏，我们需要使用EnterProc和LeaveProc宏来标志在程序中存放变量和参数的结构的开始与结束，使其能被正确访问。使用Return宏返回到调用者。

    mov File.shex_dwTotalSize,sizeof SHEXPACKET
    add File.shex_dwTotalSize,sizeof EXEName
    mov File.shex_dwSize,sizeof SHEXPACKET
    mov File.shex_ibOp,0
    mov File.shex_ibFile,sizeof SHEXPACKET
    mov File.shex_ibParams,0
    mov File.shex_ibDir,0
    mov File.shex_dwReserved,0
    mov File.shex_nCmdShow,1
    VxDCall _SHELL_ShellExecute,

在这个程序中的指令是简单的：初始化SHEXPACKET结构并且调用_SHELL_ShellExecute服务。记住 shex_dwTotalSize包含SHEXPACKET结构自身的和跟着它的字符串的组合大小。这是一个简单的事情。假如字符串不紧跟在此结构之后的，你必须计算从结构第一个字节到到字符串最后一个字节之间的距离。shex_ibFile包含此结构自身的大小，因为程序名紧跟在此结构之后。shex_ibDir是0，意味着把Windows目录作为工作目录。这并不意味着程序必须在Windows目录中。程序可以在Windows能找到的任意地方。

shex_nCmdShow是1，即SW_SHOWNORMAL的值。

File SHEXPACKET <>
EXEName db "calc.exe",0

我们定义一个SHEXPACKET结构，其后紧跟着想要运行的程序名。

翻译：吕骏，整理：LuoYunBin's Win32 ASM Page， http://asm.yeah.net


Iczelion's win32 asm tutorial

客户寄存器结构

我们将学习本教程中另外一个重要的结构，叫客户寄存器结构。在本文中，V86指虚拟8086模式。在这里下载例子程序

理论

VxDs与正常的win32/win16/DOS应用程序有很大不同。大多数情况下，当其他应用程序正常工作时，它们是休眠的。它们象一个监管者一样工作，其作用是监视ring-3应用程序并在其出错时改正它们。下面是其工作时的典型的情况：

1、中断发生时
2、VMM得到控制权时
3、VMM存贮寄存器组的值时
4、VMM服务于中断或调用其他VxDs完成此工作时
5、VMM交还控制权给被中断的程序时

在以上过程中令人感兴趣的是，VMM只有这一种方式能影响被中断的应用程序，即修改存储的寄存器映象。例如，VMM认为被中断的程序应该返回到另外一个地址，它就修改存储的寄存器映象中CS：IP的值，当这个程序被重新分派时，它将在新的CS：IP处开始执行。

VMM在客户寄存器结构中存储中断点处的寄存器值。

Client_Reg_Struc STRUC
Client_EDI DD ?
Client_ESI DD ?
Client_EBP DD ?
Client_res0 DD ?
Client_EBX DD ?
Client_EDX DD ?
Client_ECX DD ?
Client_EAX DD ?
Client_Error DD ?
Client_EIP DD ?
Client_CS DW ?
Client_res1 DW ?
Client_EFlags DD ?
Client_ESP DD ?
Client_SS DW ?
Client_res2 DW ?
Client_ES DW ?
Client_res3 DW ?
Client_DS DW ?
Client_res4 DW ?
Client_FS DW ?
Client_res5 DW ?
Client_GS DW ?
Client_res6 DW ?
Client_Alt_EIP DD ?
Client_Alt_CS DW ?
Client_res7 DW ?
Client_Alt_EFlags DD ?
Client_Alt_ESP DD ?
Client_Alt_SS DW ?
Client_res8 DW ?
Client_Alt_ES DW ?
Client_res9 DW ?
Client_Alt_DS DW ?
Client_res10 DW ?
Client_Alt_FS DW ?
Client_res11 DW ?
Client_Alt_GS DW ?
Client_res12 DW ?
Client_Reg_Struc ENDS

你可以看到这个结构分为两个部分：Client_xxx和Client_Alt_xxx。在这稍作说明，在一个给定的VM中，可能有两个运行的线程：V86和保护模式。当V86程序运行时，假如一个中断产生，Client_xxx将包含V86程序的寄存器映象，Client_Alt_xxx将包含保护模式程序的寄存器映象。相应的，当保护模式程序运行时，假如一个中断产生，Client_xxx将包含保护模式程序的寄存器映象，Client_Alt_xxx将包含V86程序的寄存器映象。Client_resX被保留而没有使用。

在查看过这个结构后，你可能有一问题：怎样改变寄存器中的一个字节，比如al？上面的结构仅仅描述了字和双字大小的寄存器组。不用担心，在vmm.inc找一找。那有两个为此附加的结构：Client_Word_Reg_Struc和Client_Byte_Reg_Struc。假如你想以字或字节大小来访问寄存器，根据你的需要转换Client_Reg_Struc到Client_Word_Reg_Struc或Client_Byte_Reg_Struc。

下一个问题：我们如何得到一个指向客户寄存器结构的指针？

这相当简单：一般地，当VMM调用我们的VxD时，把客户寄存器结构的地址放在ebp中。在这里的客户寄存器结构是当前VM的。你可以从VM的句柄中得到这个指针。记住，VM的句柄是VM控制块的线性地址。

cb_s STRUC

CB_VM_Status DD ?
CB_High_Linear DD ?
CB_Client_Pointer DD ?
CB_VMID DD ?
CB_Signature DD ?

cb_s ENDS
CB

CB_Client_Pointer包含指向VM的客户寄存器结构的指针。例如：你可用下边的代码得到指向当前VM中的客户寄存器结构的指针：

VMMCall Get_Cur_VM_Handle ; return the current VM handle in ebx
assume ebx:ptr cb_s
mov ebp,[ebx+CB_Client_Pointer] ; pointer to client reg struct

现在我们了解了客户寄存器结构，我们可以用它来开始工作了。我们将使用客户寄存器结构去传送寄存器组的值到一个DOS中断中，也就是，int 21h,功能2h,显示一个字符。这个DOS服务把要显示的字符放在dl中。假如我们传送响铃字符（07h)到这个服务，将通过PC喇叭发出一声响。

记住，int 21h是一个DOS服务，因而其在V86模式下是可用的，我们如何在VxD中调用一个V86中断？一个方法是使用Exec_Int服务。这个VMM服务把要调用的中断号放在eax中。它模拟指定的中断然后返回到调用的VM。然而，它必须在一个嵌套执行块中被调用。嵌套执行块被Begin_Nest_V86_Exec (或 Begin_Nest_Exec)和End_Nest_Exec包括起来。如果我们要调用int 21h功能2h，我们需要在嵌套执行块内转换Client_Byte_Reg_Struc结构的Client_ah和Client_Dl，然后把值21h放在eax中。当一切准备好了，就调用Exec_Int。

例子：

例子是一个动态VxD，它调用int 21h的功能2使PC喇叭发声。

.386p
include \masm\include\vmm.inc
include \masm\include\vwin32.inc
include \masm\include\v86mmgr.inc
VxDName TEXTEQU
ControlName TEXTEQU
VxDMajorVersion TEXTEQU <1>
VxDMinorVersion TEXTEQU <0>

VxD_STATIC_DATA_SEG
VxD_STATIC_DATA_ENDS

VXD_LOCKED_CODE_SEG
;----------------------------------------------------------------------------
; Remember: The name of the vxd MUST be uppercase else it won't work/unload
;----------------------------------------------------------------------------
DECLARE_VIRTUAL_DEVICE %VxDName,%VxDMajorVersion,%VxDMinorVersion, %ControlName,UNDEFINED_DEVICE_ID,UNDEFINED_INIT_ORDER

Begin_control_dispatch %VxDName
        Control_Dispatch W32_DEVICEIOCONTROL, OnDeviceIoControl
End_control_dispatch %VxDName

VXD_LOCKED_CODE_ENDS

VXD_PAGEABLE_CODE_SEG
BeginProc OnDeviceIoControl
 assume esi:ptr DIOCParams
 .if [esi].dwIoControlCode==1
  Push_Client_State
  VMMCall Begin_Nest_V86_Exec
  assume ebp:ptr Client_Byte_Reg_Struc
  mov [ebp].Client_dl,7
  mov [ebp].Client_ah,2
  mov eax,21h
  VMMCall Exec_Int
  VMMCall End_Nest_Exec
  Pop_Client_State
EndI:
 .endif
 xor eax,eax
 ret
EndProc OnDeviceIoControl
VXD_PAGEABLE_CODE_ENDS

end

讲解

Push_Client_State

这没什么好讲解的。当一个VxD接收到一个DeviceIoControl消息，ebp已经指向了当前VM的客户寄存器结构。我们调用Push_Client_State宏在堆栈中存储客户寄存器结构的状态。然后用Pop_Client_State宏恢复客户寄存器。

VMMCall Begin_Nest_V86_Exec

通过调用Begin_Nest_V86_Exec开始嵌套执行块。

assume ebp:ptr Client_Byte_Reg_Struc
mov [ebp].Client_dl,7
mov [ebp].Client_ah,2

改变在客户寄存器中的dl和ah寄存器的映象。这个改变的值将由中断使用。

mov eax,21h
VMMCall Exec_Int

Exec_Int要求在eax存有一个中断号。我们想使用int 21h。等会我们调用Exec_Int去模拟中断。

VMMCall End_Nest_Exec
Pop_Client_State

当Exec_Int返回，我们完成了嵌套执行块，并且由堆栈中恢复了客户寄存器结构的值。 你将听到你的PC喇叭发出一声响。

翻译：吕骏，整理：LuoYunBin's Win32 ASM Page， http://asm.yeah.net


Iczelion's win32 asm tutorial

虚拟8086模式的内存管理

下边我们用到的V86即指虚拟8086模式。 在以前的教程中，你学习了怎样模拟V86中断，但还有一个问题没有解决：在VxD和V86代码之间交换数据。我们将在此学习如何使用V86内存管理器来实现这个功能。在这里下载例子程序

理论

假如你的VxD和一些V86程序一起运行，如何传送大量数据到V86程序中或从V86程序中传送大量数据迟早是一个大问题。通过寄存器传送大量数据是不现实的。可能你的下一个想法是在ring0中分配一大块内存，并且通过一些寄存器传送其指针到V86程序，使其能访问这些数据。假如你这样做，可能会破坏你的系统，因为V86的地址定位方式需要segment:offset对，而不是线性定位方式。对这个问题，有很多解决的方法。然而，我选择了一个由V86内存管理器提供的一种简便的方法。

如你能在你可使用的V86内存范围内找到一个空闲的内存块作为通讯缓冲区，这将解决其中的一个问题。然而，指针传送的问题依然存在。你可以通过V86内存管理器的服务来解决这两个问题。V86内存管理器是为V86应用管理内存的静态VxD。它还为V86应用提供EMS和XMS服务和为其他VxD提供API传送服务。API传送是一个从ring0拷贝数据到V86范围内的缓冲区并且传送V86缓冲区地址到V86代码的过程。V86内存管理器有一个在V86内存范围内的传送缓冲区，其含有VxD拷贝到V86内存范围内的数据，反之亦然。初始的缓冲区是4K。你以调用V86MMGR_Set_Mapping_Info来增加它的大小。

现在你知道了传送缓冲区，我们如何拷入或拷出数据呢？这个问题通过调用两个服务来解决：V86MMGR_Allocate_Buffer和V86MMGR_Free_Buffer。

V86MMGR_Allocate_Buffer从传送缓冲区分配一块内存并且从ring0拷贝一些数据到分配的V86缓冲区。V86MMGR_Free_Buffer正好相反：它从分配的V86内存块拷贝一些数据到ring0缓冲区并且释放由V86MMGR_Allocate_Buffer分配的内存块。

记住，V86在内存管理器象堆栈一样管理被分配的缓冲区。这意味着分配／释放必须按先进后出的规则。所以如你调用了两次V86MMGR_Allocate_Buffer，第一个V86MMGR_Free_Buffer将释放由第二个V86MMGR_Allocate_Buffer调用而分配的缓冲区。

我们来看一下V86MMGR_Allocate_Buffer的定义，它是一个基本寄存器传送参数的服务。

  • EBX 当前VM的句柄
  • EBP 指向当前VM的客户寄存器结构的指针
  • ECX 从传送缓冲区分配的字节数 CARRY FLAG 进位标志位，如你不想从ring0缓冲区拷贝数据到分配的内存块就清零， 如你想从ring0缓冲区拷贝数据到分配的内存块就置1
  • FS:ESI 指向ring0缓冲区的selector:offset指针，缓冲区中有要被拷贝到被分配的 缓冲区中的数据如果进位标志位被清零，则忽略它。

假如调用成功，进位标志位被清零并且ECX包含在传送缓冲区中的字节数。这个数值应小于你要求的数值，所以你应保持这个数值，V86MMGR_Free_Buffer待会要用到它。EDI的高字包含被分配的内存块的V86段地址，偏移地址在在低字中。进位标志位当错误发生时被置位。

V86MMGR_Free_Buffer和V86MMGR_Allocate_Buffer接受同样的参数。

当你调用V86MMGR_Allocate_Buffer时，你在当前VM的V86内存范围内分配了一块内存，并且把其地址放到了EDI中。你可以使用这些服务传送数据到V86中断中或从V86中断中取得数据。

在附加的API传送中，V86内存管理器也给其他VxDs提供了API映射服务。API映射服务是映射一些在扩展内存中的页到每个VM的V86内存范围。你可以使用V86MMGR_Map_Pages执行API映射。使用这个服务，页被映射到每个VM的同一线性地址空间上。如你仅仅工作在一个VM上，这将浪费地址空间。因为API映射比API传送要慢，所以你尽可能使用API传送方式。API映射仅仅使用在一些要访问同一线性地址空间并作用到所有VM的V86操作上。

例子：

这个例子演示了API传送方式，使用了int 21h的440Dh功能（从代码66h）。这个中断调用得到媒体ID，你的第一个固定磁盘的卷标号。

;---------------------------------------------------------------
;                            VxDLabel.asm
;---------------------------------------------------------------
.386p
include \masm\include\vmm.inc
include \masm\include\vwin32.inc
include \masm\include\v86mmgr.inc
VxDName TEXTEQU
ControlName TEXTEQU
VxDMajorVersion TEXTEQU <1>
VxDMinorVersion TEXTEQU <0>

VxD_STATIC_DATA_SEG
VxD_STATIC_DATA_ENDS

VXD_LOCKED_CODE_SEG
;----------------------------------------------------------------------------
; Remember: The name of the vxd MUST be uppercase else it won't work/unload
;----------------------------------------------------------------------------
DECLARE_VIRTUAL_DEVICE %VxDName,%VxDMajorVersion,%VxDMinorVersion, %ControlName,UNDEFINED_DEVICE_ID,UNDEFINED_INIT_ORDER

Begin_control_dispatch %VxDName
        Control_Dispatch W32_DEVICEIOCONTROL, OnDeviceIoControl
End_control_dispatch %VxDName

VXD_LOCKED_CODE_ENDS

VXD_PAGEABLE_CODE_SEG
BeginProc OnDeviceIoControl
 assume esi:ptr DIOCParams
 .if [esi].dwIoControlCode==1
  VMMCall Get_Sys_VM_Handle
  mov Handle,ebx
  assume ebx:ptr cb_s
  mov ebp,[ebx+CB_Client_Pointer]
  mov ecx,sizeof MID
  stc
  push esi
  mov esi,OFFSET32 MediaID
  push ds
  pop fs
  VxDCall V86MMGR_Allocate_Buffer
  pop esi
  jc EndI
  mov AllocSize,ecx
  Push_Client_State
  VMMCall Begin_Nest_V86_Exec
  assume ebp:ptr Client_Byte_Reg_Struc
  mov [ebp].Client_ch,8
  mov [ebp].Client_cl,66h
  assume ebp:ptr Client_word_reg_struc
  mov edx,edi
  mov [ebp].Client_bx,3 ; drive A
  mov [ebp].Client_ax,440dh
  mov [ebp].Client_dx,dx
  shr edx,16
  mov [ebp].Client_ds,dx
  mov eax,21h
  VMMCall Exec_Int
  VMMCall End_Nest_Exec
  Pop_Client_State
  ;-------------------------------
  ; retrieve the data
  ;-------------------------------
  mov ecx,AllocSize
  stc
  mov ebx,Handle
  push esi
  mov esi,OFFSET32 MediaID
  push ds
  pop fs
  VxDCall V86MMGR_Free_Buffer
  pop esi
  mov edx,esi
  assume edx:ptr DIOCParams
  mov edi,[edx].lpvOutBuffer
  mov esi,OFFSET32 MediaID.midVolLabel
  mov ecx,11
  rep movsb
  mov byte ptr [edi],0
  mov ecx,[edx].lpcbBytesReturned
  mov dword ptr [edx],11
EndI:
 .endif
 xor eax,eax
 ret
EndProc OnDeviceIoControl
VXD_PAGEABLE_CODE_ENDS

VXD_PAGEABLE_DATA_SEG
 MID struct
  midInfoLevel dw 0
  midSerialNum dd ?
  midVolLabel db 11 dup(?)
  midFileSysType db 8 dup(?)
 MID ends
 MediaID MID <>
 Handle dd ?
 AllocSize dd ?
VXD_PAGEABLE_DATA_ENDS

end

;------------------------------------------------------------
;                        Label.asm
; The win32 VxD loader.
;------------------------------------------------------------
.386
.model flat,stdcall
option casemap:none

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

DlgProc PROTO :DWORD,:DWORD,:DWORD,:DWORD
.data
Failure db "Cannot load VxDLabel.VXD",0
AppName db "Get Disk Label",0
VxDName db "\\.\vxdLabel.vxd",0
OutputTemplate db "Volume Label of Drive C",0

.data?
hInstance HINSTANCE ?
hVxD dd ?
DiskLabel db 12 dup(?)
BytesReturned dd ?

.const
IDD_VXDRUN    equ 101
IDC_LOAD      equ 1000

.code
start:
 invoke GetModuleHandle, NULL
 mov    hInstance,eax
 invoke DialogBoxParam, hInstance, IDD_VXDRUN ,NULL,addr DlgProc,NULL
 invoke ExitProcess,eax

DlgProc proc hDlg:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM
 .IF uMsg==WM_INITDIALOG
  invoke CreateFile,addr VxDName,0,0,0,0,FILE_FLAG_DELETE_ON_CLOSE,0
  .if eax==INVALID_HANDLE_VALUE
   invoke MessageBox,hDlg,addr Failure,addr AppName,MB_OK+MB_ICONERROR
   mov hVxD,0
   invoke EndDialog,hDlg,NULL
  .else
   mov hVxD,eax
  .endif
 .elseif uMsg==WM_CLOSE
  .if hVxD!=0
   invoke CloseHandle,hVxD
  .endif
  invoke EndDialog,hDlg,0
 .ELSEIF uMsg==WM_COMMAND
  mov eax,wParam
  mov edx,wParam
  shr edx,16
  .if dx==BN_CLICKED
   .IF ax==IDC_LOAD
     invoke DeviceIoControl,hVxD,1,NULL,0,addr DiskLabel,12,addr BytesReturned,NULL
     invoke MessageBox,hDlg,addr DiskLabel,addr OutputTemplate,MB_OK+MB_ICONINFORMATION
   .endif
  .endif
 .ELSE
  mov eax,FALSE
  ret
 .ENDIF
 mov eax,TRUE
 ret
DlgProc endp
end start

讲解

我们首先分析lable.asm，它是一个加载了VxD的WIN32应用程序。

invoke DeviceIoControl,hVxD,1,NULL,0,addr DiskLabel,12,addr BytesReturned,NULL

它调用DeviceIoControl，设备代码是1，没有输入缓冲区，一个指向输出缓冲区的指针及其大小。DiskLable是一个接收由VxD返回的卷标号的缓冲区。BytesReturned变量存有返回的字节数。这个例子说明了怎样传送数据和从VxD接收数据：你传送输入／输出缓冲区给VxD并且VxD读取／写入数据到指定的缓冲区。

我们下面看看VxD代码。

    VMMCall Get_Sys_VM_Handle
    mov Handle,ebx
    assume ebx:ptr cb_s
    mov ebp,[ebx+CB_Client_Pointer]

当一个VxD接收W32_DeviceIoControl消息，它调用Get_Sys_VM_Handle得到系统VM的句柄并把它存在一个叫Handle的变量中。下面将从VM控制块中提取指向客户寄存器结构的指针到EBP。

    mov ecx,sizeof MID
    stc
    push esi
    mov esi,OFFSET32 MediaID
    push ds
    pop fs
    VxDCall V86MMGR_Allocate_Buffer
    pop esi
    jc EndI
    mov AllocSize,ecx

下面，准备传送到V86MMGR_Allocate_Buffer的参数。我们必须初始化被分配的缓冲区。我们把MediaID的偏移量送到ESI中，并且把选择子放在FS中，然后调用V86MMGR_Allocate_Buffer。你等会要恢复指向DIOCParams的指针，所以我们必须通过push esi 和pop esi来保护它。

    Push_Client_State
    VMMCall Begin_Nest_V86_Exec
    assume ebp:ptr Client_Byte_Reg_Struc
    mov [ebp].Client_ch,8
    mov [ebp].Client_cl,66h
    assume ebp:ptr Client_word_reg_struc
    mov edx,edi
    mov [ebp].Client_bx,3 ; drive C
    mov [ebp].Client_ax,440dh

我们在客户寄存器结构中准备参数值来执行int 21h的440Dh功能（从代码66h），得到盘C的媒体ID。我们拷贝edi的值到edx中（edi中有由V86MMGR_Allocate_Buffer分配的内存块的V86地址）。

    mov [ebp].Client_dx,dx
    shr edx,16
    mov [ebp].Client_ds,dx

调用了int 21h的440Dh功能（从代码66h）后，在ds:dx中得到一指向一个MID结构的指针，我们必须把在edx中的segment:offset对转换成两个部分并把它们放到合适的寄存器映象中。

    mov eax,21h
    VMMCall Exec_Int
    VMMCall End_Nest_Exec
    Pop_Client_State

当一切都准备好了，我们将运行Exec_Int去模拟一个中断。

    mov ecx,AllocSize
    stc
    mov ebx,Handle
    push esi
    mov esi,OFFSET32 MediaID
    push ds
    pop fs
    VxDCall V86MMGR_Free_Buffer
    pop esi

当Exec_Int返回时，分配的缓冲已经由我们想要的信息填满了。下一步是检索信息。我们使用 V86MMGR_Free_Buffer来完成这个目标。这个服务释放由V86MMGR_Allocate_Memory分配的内存块，并且从分配的内存块中拷贝数据到ring0中的内存块。象V86MMGR_Allocate_Memory，如果你想要进行拷贝操作，你必须先把进位标志位置1，再调用服务。

    mov edx,esi
    assume edx:ptr DIOCParams
    mov edi,[edx].lpvOutBuffer
    mov esi,OFFSET32 MediaID.midVolLabel
    mov ecx,11
    rep movsb
    mov byte ptr [edi],0
    mov ecx,[edx].lpcbBytesReturned
    mov dword ptr [edx],11

我们在ring0中得到这个信息后，拷贝这个卷标值到Win32应用程序提供的缓冲区中。我们可以用DIOCParams的成员lpvOutBuffer来访问它。

翻译：吕骏，整理：LuoYunBin's Win32 ASM Page， http://asm.yeah.net


汇编中参数的传递和堆栈修正

by 罗云彬

在 Win32汇编中，我们经常要和 Api 打交道，另外也会常常使用自己编制的类似于 Api 的带参数的子程序，本文要讲述的是在子程序调用的过程中进行参数传递的概念和分析。一般在程序中，参数的传递是通过堆栈进行的，也就是说，调用者把要传递给子程序（或者被调用者）的参数压入堆栈，子程序在堆栈取出相应的值再使用，比如说，如果你要调用 SubRouting(Var1,Var2,Var3)，编译后的最终代码可能是

push Var3
push Var2
push Var1
call SubRouting
add esp,12

也就是说，调用者首先把参数压入堆栈，然后调用子程序，在完成后，由于堆栈中先前压入的数不再有用，调用者或者被调用者必须有一方把堆栈指针修正到调用前的状态。参数是最右边的先入堆栈还是最左边的先入堆栈、还有由调用者还是被调用者来修正堆栈都必须有个约定，不然就会产生不正确的结果，这就是我在前面使用“可能”这两个字的原因：各种语言中调用子程序的约定是不同的，它们的不同点见下表：

 | C | SysCall | StdCall | Basic | Fortran | Pascal
参数从左到右 |  |  |  | 是 | 是 | 是
参数从右到左 | 是 | 是 | 是 |  |  |
调用者清除堆栈 | 是 |  |  |  |  |
允许使用:VARARG | 是 | 是 | 是 |  |  |

VARARG 表示参数的个数可以是不确定的，有一个例子就是 C 中的 printf 语句，在上表中，StdCall 的定义有个要说明的地方，就是如果 StdCall 使用 :VARARG 时，是由调用者清除堆栈的，而在没有:VARARG时是由被调用者清除堆栈的。
在 Win32 汇编中，约定使用 StdCall 方式，所以我们要在程序开始的时候使用 .model stdcall 语句。也就是说，在 API 或子程序中，最右边的参数先入堆栈，然后子程序在返回的时候负责校正堆栈，举例说明，如果我们要调用 MessageBox 这个 API，因为它的定义是 MessageBox(hWnd,lpText,lpCaption,UType) 所以在程序中要这样使用：

push MB_OK
push offset szCaption
push offset szText
push hWnd
call MessageBox
...

我们不必在 API 返回的时候加上一句 add sp,4*4 来修正堆栈，因为这已经由 MessageBox 这个子程序做了。在 Windows API 中，唯一一个特殊的 API 是 wsprintf，这个 API 是 C 约定的，它的定义是 wsprintf(lpOut,lpFormat,Var1,Var2...)，所以在使用时就要：

push 1111
push 2222
push 3333
push offset szFormat
push offset szOut
call wsprintf
add esp,4*5

下面要讲的是子程序如何存取参数，因为缺省对堆栈操作的寄存器有 ESP 和 EBP，而 ESP是堆栈指针，无法暂借使用，所以一般使用 EBP 来存取堆栈，假定在一个调用中有两个参数，而且在 push 第一个参数前的堆栈指针 ESP 为 X，那么压入两个参数后的 ESP 为 X-8，程序开始执行 call 指令，call 指令把返回地址压入堆栈，这时候 ESP 为 X-C，这时已经在子程序中了，我们可以开始使用 EBP 来存取参数了，但为了在返回时恢复 EBP 的值，我们还是再需要一句 push ebp 来先保存 EBP 的值，这时 ESP 为 X-10，再执行一句 mov ebp,esp，根据右图可以看出，实际上这时候 [ebp + 8] 就是参数1，[ebp + c]就是参数2。另外，局部变量也是定义在堆栈中的，它们的位置一般放在 push ebp 保存的 EBP 数值的后面，局部变量1、2对应的地址分别是 [ebp-4]、[ebp-8],下面是一个典型的子程序，可以完成第一个参数减去第二个参数，它的定义是：

MyProc proto Var1,Var2 ；有两个参数
local lVar1,lVar2 ；有两个局部变量

注意，这里的两个 local 变量实际上没有被用到，只是为了演示用，具体实现的代码是：

MyProc proc

push ebp
mov ebp,esp

sub esp,8

mov eax,dword ptr [ebp + 8]
sub eax,dword ptr [ebp + c]

add esp,8

pop ebp
ret 8

MyProc endp

现在对这个子程序分析一下，push ebp/mov ebp,esp 是例行的保存和设置 EBP 的代码，sub esp,8 在堆栈中留出两个局部变量的空间，mov /add 语句完成相加，add esp,8 修正两个局部变量使用的堆栈，ret 8 修正两个参数使用的堆栈，相当于 ret / add esp,8 两句代码的效果。可以看出，这是一个标准的 Stdcall 约定的子程序，使用时最后一个参数先入堆栈，返回时由子程序进行堆栈修正。当然，这个子程序为了演示执行过程，使用了手工保存 ebp 并设置局部变量的方法，实际上，386 处理器有两条专用的指令是完成这个功能用的，那就是 Enter 和 Leave，Enter 语句的作用就是 push ebp/mov ebp,esp/sub esp,xxx，这个 xxx 就是 Enter 的，Leave 则完成 add esp,xxx/pop ebp 的功能，所以上面的程序可以改成：

MyPorc proc
enter 8,0

mov eax,dword ptr [ebp + 8]
sub eax,dword ptr [ebp + c]

leave
ret 8
MyProc endp

好了，说到这儿，参数传递的原理也应该将清楚了，还要最后说的是，在使用 Masm32 编 Win32 汇编程序的时候，我们并不需要记住 [ebp + xx] 等麻烦的地址，或自己计算局部变量需要预留的堆栈空间，还有在 ret 时计算要加上的数值，Masm32 的宏指令都已经把这些做好了，如在 Masm32 中，上面的程序只要写成为：

MyProc proc Var1,Var2
local lVar1,lVar2

mov eax,Var1
sub eax,Var2
ret

MyProc endp

编译器会自动的在 mov eax,Var1 前面插上一句 Enter 语句，它的参数会根据 local 定义的局部变量的多少自动指定，在 ret 前会自动加上一句 Leave，同样，编译器会根据参数的多少把 ret 替换成 ret xxx，把 mov eax,Var1 换成 mov eax,dword ptr [ebp + 8] 等等。

最后是使用 Masm32 的 invoke 宏指令，在前面可以看到，调用带参数的子程序时，我们需要用 push 把参数压入堆栈，如果不小心把参数个数搞错了，就会使堆栈不平衡，从而使程序从堆栈中取出错误的返回地址引起不可预料的后果，所以有必要有一条语句来完成自动检验的任务，invoke 就是这样的语句，实际上，它是自动 push 所有参数，检测参数个数、类型是否正确，并使用 call 来调用的一个宏指令，对于上面的 push/push/call MyProc 的指令，可以用一条指令完成就是：

invoke MyProc,Var1,Var2

当然，当程序编译好以后你去看机器码会发现它被正确地换成了同样的 push/push/call 指令。但是，在使用 invoke 之前，为了让它进行正确的参数检验，你需要对函数进行申明，就象在 C 中一样，申明的语句是：

MyProc proto ：DWORD，：DWORD

语句中 proto 是关键字，表示申明，：DWORD 表示参数的类型是 double word 类型的，有几个就表示有几个参数，在 Win32 中参数都是 double word 型的，申明语句要写在 invoke 之前，所以我们一般把它包括在 include 文件中，好了，综合一下，在 Masm32 中使用一个带参数的子程序或者 Api ，我们只需用：

...
MyProc proto :dword,:dword
...
.data
x dd ?
y dd ?
dwResult dd ?
...
mov x,1
mov y,2
invoke MyProc x,y
mov dwResult,eax
...

就行了，如何，是不是很简单啊？不过我可苦了，这篇文章整整花了我一个晚上 ... ##%$^&(*&^(*&(^&(* ...
(C) Copyright by LuoYunBin's Win32 ASM Page，http://asm.yeah.net


用汇编编写屏幕保护程序

屏幕保护程序是什么，相信大家都用过，但对于它的结构也许就不那么熟悉了。屏幕保护程序是一种特使的 .exe 文件，实际上它是一个标准的 PE 文件，除了有扩展名 .scr，当然这个扩展名也是用连接程序产生的 .exe 文件改名得到的。但在编程中，屏幕保护程序又有它的特殊的地方，说穿了就是它的编程规范。

屏幕保护程序有以下特点：

  1. 屏幕保护程序是Win32 API 支持一种特殊的应用程序并由系统自动激活。其机制是当条件满足时，系统向当前活动窗口发出字参数 wParam 值为 SC_SCREENSAVE 的 WM_SYSCOMMAND 消息，然后由当前活动窗口执行 SYSTEM.INI 文件中 [boot] 区指定的屏幕保护程序。
  2. 屏幕保护程序激活的条件是：在规定时间内没有鼠标或键盘输入、当前的活动窗口是标准的 WINDOWS 应用程序。因为非 WINDOWS 应用，不会理睬 WM_SYSCOMMAND 消息。显然，如果当前活动的程序接管了字参数 wParam 值为 SC_SCREENSAVE 的 WM_SYSCOMMAND 消息并且不传递到 DefWindowProc 函数就可以禁止屏幕保护程序。这对某些运行中不愿意被打断的程序如视频播放，光盘刻录程序特别有用。
  3. 可以在控制面板的显示器中选择需要的屏幕保护程序，并可以配置屏幕保护程序的参数。配置的对话框由屏幕保护程序提供。

下面是编写屏幕保护程序的要点：

  1. 屏幕保护程序的编写由静态链接库 SCRNSAVE.LIB 支持，它包含了建立屏幕保护程序的主程序和缺省功能，如建立一个缺省的大小为全屏幕的窗口供用户使用，并提供缺省的消息处理程序，它对下面消息的缺省处理是：

    WM_SETCURSOR -- 将光标设置为无
    WM_PAINT -- 画屏幕背景
    WM_LBUTTONDOWN、WM_MBUTTONDOWN、WM_RBUTTONDOWN、WM_KEYDOWN、WM_MOUSEMOVE -- 终止执行
    WM_ACTIVATE -- 如果 wParam 是 FALSE，则终止执行

  2. 程序的入口代码已经包括在 scrnsave.lib 中，名称为 WinMain，所以程序尾包括 end WinMain 即可。
  3. 用户只需编写三个基本函数必须名为 ScreenSaverConfigureDialog、ScreenSaverProc 和 RegisterDialogClasses，这 3 个函数必须在.DEF 文件中指定 export

    ScreenSaverProc - 主过程，也就是自动建立的主窗口的窗口过程，所有对屏幕的处理就是由它完成的。可以把未处理的消息传递到 DefScreenSaverProc函数，由系统处理上面说到的缺省处理。缺省 DefScreenSaverProc 过程处理 WM_LBUTTONDOWN、WM_MBUTTONDOWN ; WM_RBUTTONDOWN、WM_KEYDOWN、WM_MOUSEMOVE 消息并结束程序，如果在这些消息时不想退出，可以自行处理，不要传递到 DefScreenSaverProc。

    ScreenSaverConfigureDialog - 处理屏幕保护程序配置对话框过程，这个过程并不是由主程序调用的，而是由控制面板的显示器设置程序调用。用户输入的配置数据应该输出到.INI 或注册表中。

    RegisterDialogClasses - 登记屏幕保护程序配置对话框的窗口类，如果使用标准的对话框，可以简单地返回 TRUE。

  4. 在 ScreenSaverProc 窗口过程中，有个专用消息 WM_ERASEBKGND -- 可以在这时擦除背景，如果把这个消息传到 DefScreenSaverProc，会得到一个全黑的背景。
  5. 使用时必须将编译完成的 .exe 文件改名为 .scr 文件，然后拷贝到 Windows 或 Windows\System 目录下。
  6. 为使控制面板能够识别，屏幕保护程序的图标（ICON）在资源文件中必须定义为 100，资源文件中必须包含一描述字符串。该字符串用于控制面板显示屏幕保护程序的名字。它必须位于字符串表的首位，ID 为 100。资源文件中屏幕保护程序配置对话框的 ID 必须为2003。

综上所述，屏幕保护程序的两个部分即窗口过程和设置对话框过程是在不同的地方被执行的，所以在编程中要注意不要在两个过程中传递变量，这就意味着屏幕保护程序的设置要被输出到 .ini 或注册表中保存，在窗口过程中的 WM_CREATE 消息中再读入，因为你无法把它保留在内存中。同样窗口过程和设置对话框过程中的变量或资源的初始化也要分别进行，比如说两者都要用到同一个图片，那就在自己的 WM_CREATE 消息中分别 LoadBitmap，总之要时刻认为你是在编写两个“独立的程序”。

下面是一个屏幕保护程序的资源定义例子：

#include	<resource.h>

#define	ICO_MAIN		100
#define	DLG_SETUP		2003

ICO_MAIN	ICON		"Resource\Main.ico"

DLG_SETUP	DIALOG DISCARDABLE  0, 0, 300, 120
STYLE		DS_MODALFRAME | WS_POPUP | WS_CAPTION | WS_SYSMENU
CAPTION		"屏幕保护程序"
FONT		9, "宋体"
BEGIN
    ...
    ...
    DEFPUSHBUTTON   "确定(&O)",		IDOK,		240,10,50,14
    PUSHBUTTON      "取消(&C)",		IDCANCEL,	240,29,50,14
END

STRINGTABLE	DISCARDABLE
BEGIN
    100	"保护程序"
END

.def 文件例子：

EXPORTS
ScreenSaverProc
ScreenSaverConfigureDialog
RegisterDialogClasses

源代码例子：

;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
;	Programmed by 罗云彬, bigluo@telekbird.com.cn
;	Website: http://asm.yeah.net
;	LuoYunBin's Win32 ASM page (罗云彬的编程乐园)
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
;	版本信息
;	屏幕保护程序模板
;	Ver 1.0 - 2000.07.15
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>

        .386
        .model flat, stdcall
        option casemap :none   ; case sensitive

;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
;	Include 数据
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
include		windows.inc
include		user32.inc
include		kernel32.inc
include		comctl32.inc
include		comdlg32.inc
include		gdi32.inc
include		advapi32.inc
include		shell32.inc
include		scrnsave.inc

includelib	user32.lib
includelib	kernel32.lib
includelib	comctl32.lib
includelib	comdlg32.lib
includelib	gdi32.lib
includelib	advapi32.lib
includelib	shell32.lib
includelib	scrnsave.lib
includelib	msvcrt.lib
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
;	Equ 数据
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
ICO_MAIN	equ		100	;Must be 100
DLG_SETUP	equ		2003	;Must be 2003

;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
;	数据段
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>

        .data?

hInstance	dd		?
hWinMain	dd		?

;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
;	代码段
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>

        .code

;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
; 设置对话框过程
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
ScreenSaverConfigureDialog	proc	uses ebx edi esi, \
        hWnd:DWORD,wMsg:DWORD,wParam:DWORD,lParam:DWORD

        mov	eax,wMsg
;********************************************************************
        .elseif	eax == WM_CLOSE
            invoke	EndDialog,hWnd,NULL
;********************************************************************
        .elseif	eax == WM_COMMAND
            mov	eax,wParam
            .if	eax ==	IDOK
                ...
                在此保存设置
                ...
                invoke	EndDialog,hWnd,NULL
            .elseif eax ==	IDCANCEL
                invoke	EndDialog,hWnd,NULL
            .endif
        .else
            mov	eax,FALSE
            ret
        .endif
        mov	eax,TRUE
        ret

ScreenSaverConfigureDialog	endp

;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
; 主程序窗口过程
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
ScreenSaverProc	proc	uses ebx edi esi, \
        hWnd:DWORD,uMsg:DWORD,wParam:DWORD,lParam:DWORD

        mov	eax,uMsg
        .if	eax ==	WM_CREATE
               invoke	GetModuleHandle,NULL
            mov	hInstance,eax
            mov	eax,hWnd
            mov	hWinMain,eax
            ...
            在此初始化，包括定义一个定时器
            ...
               invoke	SetTimer,hWinMain,TIMER_MOON,100,NULL
        .elseif	eax ==	WM_DESTROY
            call	_Quit
;********************************************************************
        .elseif	eax ==	WM_TIMER
            ...
            在此画屏幕动画
            ...
            xor	eax,eax
            ret
;********************************************************************
;		.elseif	eax ==	WM_ERASEBKGND
;********************************************************************
; 以下黑屏的代码在 DefScreenSaverProc 中已经包括，如果自己要处理
; 屏幕，可以把它去掉。
;********************************************************************
;			invoke	GetDC,hWnd
;			mov	@hDc,eax
;			invoke	GetClientRect,hWnd,addr @stRc
;			invoke	GetStockObject,BLACK_BRUSH
;			invoke	FillRect,@hDc,addr @stRc,eax
;			invoke	ReleaseDC,hWnd,@hDc
;			xor	eax,eax
;			ret
        .endif
;********************************************************************
        invoke	DefScreenSaverProc,hWnd,uMsg,wParam,lParam
        ret

ScreenSaverProc	endp

;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
; 注册设置对话框窗口Class过程
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
RegisterDialogClasses	proc	uses ebx edi esi, hInst:DWORD

        mov	eax,TRUE
        ret

RegisterDialogClasses	endp
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
        end	WinMain
以上是一个屏幕保护程序的框架结构，其实编写一个屏幕保护程序的大部分工作量在于 WM_TIMER 消息的处理，也就是说对动画的处理，如果就把上面的程序编译，那么你就会得到一个最简单的“黑屏”屏保。这里是我编写的一个屏保月下情人，是用 Win32Asm 编写的。


Win32 Serial Port 编程

在 Win32 下 Serial Port 的通訊

以下的東西是我在閱讀 "Communications Programming for Windows 95" 時做的一點筆記, 所以在 topic 上大致上都和這書上的第三章一樣. 一些 structure 和 API 的宣告都是 copy from VC 5.0 的 on-line help.

這些東西主要談的是如何在 Win32 的平台下對 serial port 的通訊. 可能可以帶來幫助的是那些了解 serial port 的通訊, 但是不清楚在 Win32 平台究竟有那些 API 可用的 programer (就像我? :p) 我只是做做整理罷了, 更清楚的內容可能要再翻翻 on-line help 或是書本. 如果你要嘗試在 Win32 下做像是對 modem 做控制(例如撥號, 通訊)的程式, 但是對於 serial port 並不了解, 那你應該再去找一本有講到這些東西的書看看.

開啟一個 Serial Port

利用一般開啟檔案的 CreatFile() 即可開啟 serial port device

HANDLE CreateFile(
LPCTSTR lpFileName, // pointer to name of the file
DWORD dwDesiredAccess, // access (read-write) mode
DWORD dwShareMode, // share mode
LPSECURITY_ATTRIBUTES lpSecurityAttributes, // pointer to security attributes
DWORD dwCreationDistribution, // how to create
DWORD dwFlagsAndAttributes, // file attributes
HANDLE hTemplateFile // handle to file with attributes to copy
);
  • 用 CreateFile() API.
  • lpFileName 為 "COM1" 或是 "COM2"
  • dwDersiredAccess 一般為 GENERIC_READ|GENERIC_WRITE
  • dwShareMode "必須"為 0, 即不能共享, 但同一個 process 中的不同 thread 在一開啟之後就可以共享.
  • lpSecurityAttributes 一般為 NULL
  • dwCreateionDistributon 在這裡"必須"為 OPEN_EXISTING
  • dwFlagsAndAttributes 定義了開啟的屬性, 若是設成 FILE_FLAG_OVERLAPPED 則可使用非同步的 I/O.
  • hTemplateFile "必須"為 NULL
  • 傳回檔案 handle
設定 Serial Port 傳送及接收緩衝區的大小

在開啟完 serial port 之後, 可以藉由呼叫 SetupComm() 來進行配置傳送時的緩衝區及接收時的緩衝區. 如果沒有呼叫 SetupComm() 的話, Win95 會配置內定的緩衝區.

BOOL SetupComm(
HANDLE hFile, // handle of communications device
DWORD dwInQueue, // size of input buffer
DWORD dwOutQueue // size of output buffer
);
關閉 Serial Port file

利用一般的 CloseHandle() 即可.

BOOL CloseHandle(
HANDLE hObject // handle to object to close
)
取得 Seial Port 的資訊

在 Win32 裡頭, 將一些通訊時會用到的資訊用 COMMPROP 這個結構來表示. (當然不僅僅是 Serial Port) 可以用 GetCommProperties() 來取得:

BOOL GetCommProperties(
HANDLE hFile, // handle of communications device
LPCOMMPROP lpCommProp // address of communications properties structure
);

COMMPROP 長的像這個樣子:

typedef struct _COMMPROP { // cmmp
    WORD  wPacketLength;       // packet size, in bytes
    WORD  wPacketVersion;      // packet version
    DWORD dwServiceMask;       // services implemented
    DWORD dwReserved1;         // reserved
    DWORD dwMaxTxQueue;        // max Tx bufsize, in bytes
    DWORD dwMaxRxQueue;        // max Rx bufsize, in bytes
    DWORD dwMaxBaud;           // max baud rate, in bps
    DWORD dwProvSubType;       // specific provider type
    DWORD dwProvCapabilities;  // capabilities supported
    DWORD dwSettableParams;    // changable parameters
    DWORD dwSettableBaud;      // allowable baud rates
    WORD  wSettableData;       // allowable byte sizes
    WORD  wSettableStopParity; // stop bits/parity allowed
    DWORD dwCurrentTxQueue;    // Tx buffer size, in bytes
    DWORD dwCurrentRxQueue;    // Rx buffer size, in bytes
    DWORD dwProvSpec1;         // provider-specific data
    DWORD dwProvSpec2;         // provider-specific data
    WCHAR wcProvChar[1];       // provider-specific data
} COMMPROP;

在這裡, lpCommProp 需要 programmer 自行配置空間. 有趣的問題是, 系統在這個結構之後會需要額外的空間. 但是配置者也就是 programmer 卻不知道系統會需要多少. 很簡單的做法是配置一大塊絕對會夠的空間. 另一個聰明的做法是執行兩次 GetCommProperties() , 第一次只配置 sizeof(COMMPROP) 這麼大的空間, 因為還沒有開始執行一些動作, 所以系統並不會嘗試著在後面填東西, 所以不會出問題. 接著執行第一次的 GetCommProperties(), 得到結果, 取出結構中的 wPacketLength, 這個 member 代表實際上需要的大小, 然後依據這個大小重新配置一個新的. 這樣的話 , 就不會有浪費任何空間的問題了.

至於上述 COMMPROP 結構的成員所代表的意思, on-line help 中應該寫的都滿清楚的 .

設定及取得通訊狀態

你可以利用 COMMPROP 來取得一些狀態, 但是當你想改變目前的設定時你需要兩個 API 來完成:

BOOL GetCommState(
HANDLE hFile, // handle of communications device
LPDCB lpDCB // address of device-control block structure
);

BOOL SetCommState(
HANDLE hFile, // handle of communications device
LPDCB lpDCB // address of device-control block structure
);

你可以用 GetCommState() 來取得目前 Serial Port 的狀態, 也可以用 SetCommState() 來設定 Serial Port 的狀態.

DCB 的結構就請自行翻閱 help 囉.

另外, programmer 最常控制的幾個設定就是 baud rate, parity method, data bits, 還有 stop bit. BuildCommDCB() 提供了對於這幾個常見設定的控制.

BOOL BuildCommDCB(
LPCTSTR lpDef, // pointer to device-control string
LPDCB lpDCB // pointer to device-control block
);

lpDef 長的像這樣: "baud=2400 parity=N data=8 stop=1"

通訊設定對話盒

Win32 API 中提供了一個開啟通訊設定對話盒的 API: CommConfigDialog(), 當呼叫這個 API 時, 會蹦現一個可供設定 Baud Rate, Data Bits, Parity .. 等資訊的對話盒, programmer 可以利用它來讓使用者設定一些資訊, 並且取得結果.

BOOL CommConfigDialog(
LPTSTR lpszName, // pointer to device name string
HWND hWnd, // handle to window
LPCOMMCONFIG lpCC // pointer to comm. configuration structure
);

其中 lpCC 被用來存放設定值的結果.

typedef struct _COMM_CONFIG {
    DWORD dwSize;
    WORD  wVersion;
    WORD  wReserved;
    DCB   dcb;
    DWORD dwProviderSubType;
    DWORD dwProviderOffset;
    DWORD dwProviderSize;
    WCHAR wcProviderData[1];
} COMMCONFIG, *LPCOMMCONFIG;

在我們呼叫 CommConfigDialog() 之前, dwSize 要設為 sizeof(COMMCONFIG), wVersion 的值在這邊似乎不重要(我不清楚, VC5 的 on-line help 說可以設為 1, 我手中的 book 的範例是設為 0x100), 呼叫完 CommConfigDialog() 之後, 成員 dcb 中的 BaudRate, ByteSize, StopBits, Parity 就是使用者的設定.

Timeout 的機制

因為傳輸時並不會維持一個絕對穩定的速率. 因為傳輸品質的關係, programer 會需要 timeout 的機制來協助他們做一些控制. 在 Win32 通訊 Timeout 的機制中, timeout 的性質共分為兩類, 先來看看 COMMTIMEOUTS 這個結構:

typedef struct _COMMTIMEOUTS {   // ctmo
    DWORD ReadIntervalTimeout;

    DWORD ReadTotalTimeoutMultiplier;
    DWORD ReadTotalTimeoutConstant;
    DWORD WriteTotalTimeoutMultiplier;
    DWORD WriteTotalTimeoutConstant;
} COMMTIMEOUTS,*LPCOMMTIMEOUTS;

programmer 可以利用 GetCommTimeouts() 和 SetCommTimeouts() 來讀取或是設定目前的 timeout 值.

BOOL GetCommTimeouts(
HANDLE hFile, // handle of communications device
LPCOMMTIMEOUTS lpCommTimeouts // address of comm. time-outs structure
);

BOOL SetCommTimeouts(
HANDLE hFile, // handle of communications device
LPCOMMTIMEOUTS lpCommTimeouts // address of communications time-out structure
);

第一種 timeout 的機制稱為 interval timeout, 從字面上的意義很容易可以理解這種 timeout 的機制是讀取字元之間的間隔時間的 timeout, 只有讀取字元時才能夠使用interval timeout. 也就是在這個結構中的 ReadIntervalTimeout, 單位為 ms, 當讀取完一個字元後, 超過了 ReadIntervalTimeout 的值, 卻還沒有讀到下一個字元時, timeout 就發生了.

第二種 timeout 的機制稱為 total timeout, 顧名思義即是傳輸的總時間的 timeout . 在這種 timeout 的機制下, Win32 提供了一個具有彈性的方式來設定 total timeout. 以讀取的 total timeout 為例, 利用 ReadTotalTimeoutMultiplier 和 ReadTotalTimeoutConstant 構成了一個線性的上限值. 什麼意思呢? 實際上的 total timeout 應該是這樣的一個式子:

ReadTotalTimeout = ReadTotalTimeOutMultiplier*BytesToRead+ReadTotalTimeoutConstant

WriteTotalTimeout 用同樣的公式來計算. 這樣的話, 不僅可以用一個固定的值來做為 timeout 值, 也可以用條線來做為 timeout 的值, 而隨著要讀取或是要寫的 bytes 數而變動.

如果不想使用 timeout, 就把 COMMTIMEOUTS 裡頭的資料成員都填為 0.

如果你將 ReadIntervalTimeout 設為 MAXDWORD, 且將 ReadTotalTimeOutMultiplier 和 ReadTotalTimeoutConstant 都設為 0 的話, 那麼讀取時, 如果 receive queue 裡頭並沒有資料, 讀取的動作將會馬上返回, 而不會停滯在讀取的動作.

這裡有一個和 BuildCommDCB() 很像的 API 叫 BuildCommDCBAndTimeouts():

BOOL BuildCommDCBAndTimeouts(
LPCTSTR lpDef, // pointer to the device-control string
LPDCB lpDCB, // pointer to the device-control block
LPCOMMTIMEOUTS lpCommTimeouts // pointer to comm. time-out structure
);

lpDef 一樣是控制字串, 可以給像 BuildCommDCB() 中的 lpDef 那樣格式的字串, 但是多了 "TO=XXX" 這個設定. 如果 "TO=ON", 這個 API 會依據 lpCommTimeouts 裡頭的值來設定讀和寫的 timeout 值. 如果 "TO=OFF", 則會設定這個 device 沒有 timeout. 如果是 "ON" 和 "OFF" 之外的其它值, 則 lpCommTimeouts 的設定將會被忽略.

對了, 在設定完 timeout 值之後, 記得要檢查 COMMPROP 裡的 dwProvCapabilities 中的 PCF_INTTIMEOUTS 和 PCF_TOTALTIMEOUTS 兩個 flags 是否有被 set, 以確認 interval timeout 和 total timeout 是否有支援.

讀取資料

從 serial port 裡頭讀取資料就跟讀取一般的檔案一樣, 使用 ReadFile() 來達成.

BOOL ReadFile(
HANDLE hFile, // handle of file to read
LPVOID lpBuffer, // address of buffer that receives data
DWORD nNumberOfBytesToRead, // number of bytes to read
LPDWORD lpNumberOfBytesRead, // address of number of bytes read
LPOVERLAPPED lpOverlapped // address of structure for data
);

要注意的是, nNumberOfBytesToRead 設定的是一次最多的讀取量, 很有可能所讀取的值(檢查 lpNumberOfBytesRead)小於這個值. 通常在錯誤發生或是 timeout 發生時這個 API 就會返回.

PurgeComm() 這個 API 可以用來終止目前正在進行的讀或寫的動作, 也可以 flush 掉 I/O buffer 內等待讀或寫的資料.

BOOL PurgeComm(
HANDLE hFile, // handle of communications resource
DWORD dwFlags // action to perform
);

其中 dwFlags 共有四種 flags:

  • PURGE_TXABORT: 終止目前正在進行的(背景)寫入動作
  • PURGE_RXABORT: 終正目前正在進行的(背景)讀取動作
  • PURGE_TXCLEAR: flush 寫入的 buffer
  • PURGE_TXCLEAR: flush 讀取的 buffer

而使用 FlushFileBuffers() 可以確保所有的資料都被送出, 這個 API 才會返回.

另外一個有趣的 API 是 ClearCommError(), 從字面上的意思看來, 它是用來清除錯誤情況用的, 但是實際上它還可以拿來取得目前通訊設備的一些資訊.

BOOL ClearCommError(
HANDLE hFile, // handle to communications device
LPDWORD lpErrors, // pointer to variable to receive error codes
LPCOMSTAT lpStat // pointer to buffer for communications status
);

呼叫這個 API 之後, 關於通訊設備的一些資訊會被儲存在 lpStat 中, COMSTAT 的結構如下:

typedef struct _COMSTAT { // cst
    DWORD fCtsHold : 1;   // Tx waiting for CTS signal
    DWORD fDsrHold : 1;   // Tx waiting for DSR signal
    DWORD fRlsdHold : 1;  // Tx waiting for RLSD signal
    DWORD fXoffHold : 1;  // Tx waiting, XOFF char rec'd
    DWORD fXoffSent : 1;  // Tx waiting, XOFF char sent
    DWORD fEof : 1;       // EOF character sent
    DWORD fTxim : 1;      // character waiting for Tx
    DWORD fReserved : 25; // reserved
    DWORD cbInQue;        // bytes in input buffer
    DWORD cbOutQue;       // bytes in output buffer
} COMSTAT, *LPCOMSTAT

藉由 fCtsHold, fDsrHold, fRlsdHold, fXoffHold, fXoffSent 可以知道目前因為什麼因素而使通訊阻礙住了.( 跟 handshaking 和 flow control 有關) cbInque 和 cbOutQue 則可以顯示出還有多少 bytes 在讀取或是寫入 queue 中.

寫入資料

和讀取資料一樣, programmer 可以使用 WriteFile() 來將資料寫入 serial port.

BOOL WriteFile(
HANDLE hFile, // handle to file to write to
LPCVOID lpBuffer, // pointer to data to write to file
DWORD nNumberOfBytesToWrite, // number of bytes to write
LPDWORD lpNumberOfBytesWritten, // pointer to number of bytes written
LPOVERLAPPED lpOverlapped // pointer to structure needed for overlapped I/O
);

關於通訊設備的寫入有三個很有趣的 API, 它們分別是 SetCommBreak(), ClearCommBreak, 和 TransmitCommChar().

BOOL SetCommBreak(
HANDLE hFile // handle of communications device
);

BOOL ClearCommBreak(
HANDLE hFile // handle to communications device
);

BOOL TransmitCommChar(
HANDLE hFile, // handle of communications device
char cChar // character to transmit
);

SetCommBreak() 是用來暫停目前的傳輸作業, 它會使 buffer 中的資料都不再被送出, 這個時候, program 可以去做些雜七雜八的事, 之後, 再利用 ClearCommBreak() 回復傳輸作業.

TransmitCommChar() 是用來立即性的趕在所有 buffer 資料被送出去之前, 傳輸一個字元的資料出去, 即使 buffer 中還有資料. 換句話說, 這個字元有最高的優先權被送出去.

事件驅動式的 I/O

在 Win32 裡頭, 對於通訊設備的 I/O 可以用像是事件驅動式的方法來達成. 主要是利用一個叫 WaitCommEvent() 的 API. 呼叫這個 API 之後, 會一直 block 到設定的事件發生之後才會返回. 我們先來看看如何設定事件, 再回過頭來看 WaitCommEvent() .

programer 可以用 GetCommMask() 和 SetCommMask() 來取得或是設定目前設定的通訊事件.

BOOL GetCommMask(
HANDLE hFile, // handle of communications device
LPDWORD lpEvtMask // address of variable to get event mask
);

BOOL SetCommMask(
HANDLE hFile, // handle of communications device
DWORD dwEvtMask // mask that identifies enabled events
);

可以設定的事件有 EV_BREAK, EV_CTS, EV_DSR, EV_ERR, EV_RING, EV_RLSD, EV_RXCHAR, EV_RXFLAG, EV_TXEMPTY.(其意義請自行參考 help), 當然, 你可以把它們 or 起來成為組合的事件.

在設定完想要處理的事件之後, 可以使用 WaitCommEvent()

BOOL WaitCommEvent(
HANDLE hFile, // handle of communications device
LPDWORD lpEvtMask, // address of variable for event that occurred
LPOVERLAPPED lpOverlapped, // address of overlapped structure
);

WaitCommEvent() 會一直 block 到你所設定的通訊事件發生為止. 所以當 WaitCommEvent() 返回時, 你可以由 lpEvtMask 取得究竟是那一事件發生, 再來決定要如何處理.

舉例來說, 可以用 SetCommMask() 設定事件為 EV_RXCHAR, 那麼在呼叫 WaitCommEvent() 時, 它會等到有字元可供讀取時才會返回, 那麼在它返回之後, 可以檢查一下 lpEvtMask 中是否 set 了 EV_RXCHAR, 如果是的話就可以用 ReadFile() 去讀取. 這樣的話, 可以避免掉某些情形之下, 需要做 polling 所引起效率不彰的問題.

錯誤的處理

前面提過的 ClearnCommError() 可以用來取得目前發生錯誤的原因.(請參見 help)

硬體的控制命令

Win32 中提供了 EscapeCommFunction() 允許 programer 對幾個硬體訊號做控制.

BOOL EscapeCommFunction(
HANDLE hFile, // handle to communications device
DWORD dwFunc // extended function to perform
);

其中 dwFunc 可以是:

  • CLRDTR : 讓 DTR OFF
  • CLRRTS : 讓 RTS OFF
  • SETDTR : 讓 DTR ON
  • SETRTS : 讓 RTS ON
  • SETXOFF : "模擬" 接收到 XOFF 字元
  • SETXON : "模擬" 接收到 XON 字元
  • SETBREAK : 和 SetCommBreak() 的意思相同
  • CLRBREAK : 和 ClearCommBreak() 的意思相同
Reference

[1]Charles A. Mirho and Andre Terrisse, "Communications Programming for Windows 95" , Microsoft Press

From LuoYunBin's Win32 ASM Page，http://asm.yeah.net


实战串行通讯
by 罗云彬，2001.1

很长时间没有写编程文章了，最近为了编一个串行通讯的终端程序忙了近一个月，发现了在 Win32 的通讯 API 中有不少的问题，还有许多在其他串行通讯文章中没有谈到的问题，在这里准备集中谈一下，大家请下载我这一个月的“成果”：迷你终端测试一下，看看还有没有什么问题。有关串行通讯方面的基础文章，可以参考《Serial communications in Microsoft Win32》和《串行通讯编程点滴》。本文不是全面的讲述如何编写串行通讯程序，而是讨论一些实际遇到的问题。

1 选择通讯方式 -- 同步还是非同步

正如在《Serial communications in Microsoft Win32》等文章中提到的，同步（NonOverLapped）方式是比较简单的一种方式，编写起来代码的长度要明显少于异步（OverLapped）方式，我开始用同步方式编写了整个子程序，在 Windows98 下工作正常，但后来在 Windows2000下测试，发现接收正常，但一发送数据，程序就会停在那里，原因应该在于同步方式下如果有一个通讯 Api 在操作中，另一个会阻塞直到上一个操作完成，所以当读数据的线程停留在 WaitCommEvent 的时候，WriteFile 就停在那里。我又测试了我手上所有有关串行通讯的例子程序，发现所有使用同步方式的程序在 Windows 2000 下全部工作不正常，对这个问题我一直找不到解决的办法，后来在 Iczelion 站点上发现一篇文章提到 NT 下对串行通讯的处理和 9x 有些不同，根本不要指望在 NT 或 Windows 2000 下用同步方式同时收发数据，我只好又用异步方式把整个通讯子程序重新写了一遍。

所以对于这个问题的建议是：如果程序只打算工作在 Win9x 下，为了简单起见，可以用同步方式写程序，如果程序打算在 NT 下也可以工作的话，就必须用异步方式写。

2 Win32 通讯 API Bug 之一 --- CommConfigDialog

CommConfigDialog 是弹出系统内置串口设置对话框的 API，我们在设备管理器中设置串口参数的对话框就是这个，使用这个 API 时不用先打开端口，它并不针对一个已打开的端口，而是仅仅是把 DCB 的内容填写到对话框中，当按了 OK 后把输入的结果存回到 DCB 数据结构中，至于什么时候把结果设置到串口上，那就是你自己要做的事情了。

CommCinfigDialog 的定义如下：

BOOL CommConfigDialog(

LPTSTR lpszName, // pointer to device name string
HWND hWnd, // handle to window
LPCOMMCONFIG lpCC // pointer to comm. configuration structure
);

但在使用中发现，对话框有时能出来，有时出不来，最后总结的经验是问题出在 COMMCONFIG 结构的 dwSize 字段上，COMMCONFIG 的定义如下：

typedef struct _COMM_CONFIG {
DWORD dwSize;
WORD wVersion;
WORD wReserved;
DCB dcb;
DWORD dwProviderSubType;
DWORD dwProviderOffset;
DWORD dwProviderSize;
WCHAR wcProviderData[1];
} COMMCONFIG, *LPCOMMCONFIG;

在参数中，wVersion 要填 100h，dwProviderSubType 要填 1，但 dwSize 就不能填 sizeof COMMCONFIG 了，我发现好象一定要把 dwSize 设置为比 sizeof COMMCONFIG 对话框才能出来，所以我用的代码中定义了一个足够大的缓冲区作为结构的地址：

_CommConfigDialog proc
local @stCC[256]:BYTE

pushad
invoke RtlZeroMemory,addr @stCC,sizeof @stCC
mov (COMMCONFIG ptr @stCC).dwSize,256
mov (COMMCONFIG ptr @stCC).wVersion,100h
mov (COMMCONFIG ptr @stCC).dwProviderSubType,1
invoke CommConfigDialog,addr [esi].szPortName,[esi].hWnd,addr @stCC
popad
ret

_CommConfigDialog endp

3 Win32 通讯 API Bug 之二--- BuildCommDCB

BuildCommDCB 的功能是把一个字符串如 com1:9600,n,8,1 这样的转换到具体的数据填写到 DCB 中，但使用中也存在问题，我发现我用它转换象 com1:9600,e,7,1 之类的带校验位的字符串，它总是无法把这个 e 给我转换过去，设置好串口一看，成了 9600,n,7,1，而上面提到的 CommConfigDialog 返回的结果用来设置串口却是正确的，经过比较，发现问题出在 DCB.fbits.fParity 这个 bit 上，只有把这个 bit 置 1，校验位才是有效的，而 BuildCommDCB 恰恰是漏了这个 bit，所有如果你要使用 BuildCommDCB，别忘了补充把 DCB.fbits.fParity 设置回去，我用的代码是：

_BuildCommDCB proc _lpszPara,_lpstDCB

pushad
mov esi,_lpstDCB
assume esi:ptr DCB

invoke RtlZeroMemory,esi,sizeof DCB
invoke BuildCommDCB,_lpszPara,esi
;********************************************************************
; 根据校验位补充设置 DCB 中的 DCB.fbits.fParity 字段
;********************************************************************
mov dword ptr [esi].fbits,0010b

cld
@@:
lodsb
or al,al
jz @F
cmp al,'='
jz _BCD_Check
cmp al,','
jnz @B
_BCD_Check:
lodsb
or al,al
jz @F
or al,20h
cmp al,'n'
jnz @B
;********************************************************************
; 扫描到 =n 或 ,n 则取消校验位
;********************************************************************
mov esi,_lpstDCB
and dword ptr [esi].fbits,not 0010b
@@:
popad
ret

_BuildCommDCB endp

4 Win32 通讯编程的一般流程

由于同步方式相对比较简单，在这里讲述的是异步方式的流程，在其他的很多文章里提到了 Windows 通讯 API 有二十多个，它们是：

BuildCommDCB
BuildCommDCBAndTimeouts
ClearCommBreak
ClearCommError
CommConfigDialog
EscapeCommFunction
GetCommConfig
GetCommMask
GetCommModemStatus
GetCommProperties
GetCommState
GetCommTimeouts
GetDefaultCommConfig
PurgeComm
SetCommBreak
SetCommConfig
SetCommMask
SetCommState
SetCommTimeouts
SetDefaultCommConfig
SetupComm
TransmitCommChar
WaitCommEvent

我刚看到这些 API 的时候，都不知道如何使用它们，但并不是所有这些 API 都是必须用的，比如说你要检测当前串口的设置可以只用 SetCommState 而不用 GetCommProperties 和 GetCommConfig，虽然它们返回的信息可能更多。同样，如果有些值你想用缺省的，比如缓冲区的大小和超时的时间等等，那么 SetupComm 和 BuildCommDCBAndTimeouts、SetCommTimeouts 也可以不用，TransmitCommChar 是马上在发送序列中优先插入发送一个字符用的，平时也很少用到，下面讲的是必须用到的 API 和使用步骤：

  1. 建立 Event -- 用 CreateEvent

    invoke CreateEvent,NULL,TRUE,FALSE,NULL
    用异步方式操作串口必须要定义 OVERLAPPED 结构，其中的 hEvent 必须自己建立，你要定义两个 OVERLAPPED 结构，一个用于读一个用于写，当然也必须建立两个 Event，把它们放入 OVERLAPPED.hEvent

  2. 打开串口 -- 用 CreateFile

    invoke CreateFile,addr szPortName,GENERIC_READ or GENERIC_WRITE,0,NULL,OPEN_EXISTING,FILE_FLAG_OVERLAPPED,NULL
    注意用异步方式必须指定 FILE_FLAG_OVERLAPPED，而文件方式必须 OPEN_EXISTING，读写必须是 GENERIC_READ or GENERIC_WRITE

  3. 设置串口参数 -- 用 SetCommState

    invoke SetCommState,hCom,addr dcbx
    hCom 是前面打开成功后返回的句柄，dcbx 是数据结构 DCB，里面包括了通讯的具体参数，至于这个参数的建立，你可以自己填写，也可以用前面提到的 BuildCommDCB 或 CommConfigDialog 填写

  4. 建立读数据的线程

    到这里，就可以开始读数据了，一般我们是在主线程中写数据，因为写是我们可以控制的，而读的时候我们不知道数据什么时候会到，所以要建立一个线程专门用来读数据，在这个线程中，我们循环地用 ReadFile 读串口，同时用 WaitCommEvent 检测线路状态。

  5. 如果要检测通讯状态，如 CTS 信号，RingIn 等等 -- 用 SetCommMask、WaitCommEvent、ClearCommError、GetCommModemStatus

    invoke SetCommMask,hCom,EV_BREAK or EV_CTS or EV_DSR or EV_ERR or EV_RING or EV_RLSD or EV_RXCHAR or EV_RXFLAG or EV_TXEMPTY
    SetCommMask 指定 WaitCommEvent 要等待的事件名称，具体的参数请查手册

    invoke WaitCommEvent,hCom,addr dwEvent,NULL
    WaitCommEvent 等待一直到 SetCommMask 指定事件之一发生

    invoke ClearCommError,hCom,addr dwError,addr stComStat
    在 WaitCommEvent 以后，要用 ClearCommError 清除事件的 Flag，以便进行下一轮 WaitCommEvent，同时这个 API 可以获得更详细的事件信息

    invoke GetCommModemStatus,hCom,addr dwModemStatus
    同样，GetCommModemStatus 是用来获得串口线路状态的，如 CTS、RING 等等，当 WaitCommEvent 返回时，只是指出了如 CTS 等等状态有变化，但具体是变成 On 还是 Off 了还要靠这个 API 去取得更详细的信息

  6. 读数据 -- 用 ReadFile

    invoke ReadFile,hCom,addr szBuffer,sizeof szBuffer,addr dwBytesRead,addr stReadState
    最后一个参数是开头定义的 OVERLAPPED 结构的地址，指定了它就表示是用异步方式的读方式，这个 API 会马上返回，接下去要用

    invoke GetOverlappedResult,hCom,addr stReadState,addr dwBytesRead,FALSE
    将其余的数据读完

  7. 结束时关闭端口 -- 停止 WaitCommEvent 的等待以及关闭端口 CloseHandle

    平时程序会停留在 WaitCommEvent 的等待中，当要终止线程的时候，必须是程序从 WaitCommEvent 中退出来，这时候要用

    按照 Win32 手册上的说明，参数为 NULL 的 SetCommMask 会使另一个线程中的 WaitCommEvent 马上返回，然后就是用 CloseHandle 关闭端口
    invoke CloseHandle,hCom

5 Win32 通讯 API Bug 之二--- SetCommMask 和 WaitCommEvent

严格的说这不应该是 Bug，而是偶然的情况，我发现有些时候我的读线程无法结束，跟踪发现是停在了 WaitCommEvent 上，这说明有时候 invoke SetCommMask,hCom,NULL 并不能使 WaitCommEvent 退出，我最后使用的办法是： 在 SetCommMask 以后再执行 invoke SetEvent,stReadState.hEvent，把读的 OVERLAPPED 结构中的 Event 置位，让 WaitCommEvent 认为有 Event 发生，它就会马上返回，也许这并不是普遍的情况，但如果你的程序也是停在了 WaitCommEvent 的地方，不妨一试。

6 如何编写读线程中的循环

按照《Serial communications in Microsoft Win32》一文中的例程，读循环可以用：

#define READ_TIMEOUT 500 // milliseconds

DWORD dwRes;
DWORD dwRead;
BOOL fWaitingOnRead = FALSE;
OVERLAPPED osReader = {0};

// Create the overlapped event. Must be closed before exiting
// to avoid a handle leak.
osReader.hEvent = CreateEvent(NULL, TRUE, FALSE, NULL);

if (osReader.hEvent == NULL)
// Error creating overlapped event; abort.

if (!fWaitingOnRead) {
// Issue read operation.
if (!ReadFile(hComm, lpBuf, READ_BUF_SIZE, &dwRead, &osReader)) {
if (GetLastError() != ERROR_IO_PENDING) // read not delayed?
// Error in communications; report it.
else
fWaitingOnRead = TRUE;
}
else {
// read completed immediately
HandleASuccessfulRead(lpBuf, dwRead);
}
}

if (fWaitingOnRead) {
dwRes = WaitForSingleObject(osReader.hEvent, READ_TIMEOUT);
switch(dwRes)
{
// Read completed.
case WAIT_OBJECT_0:
if (!GetOverlappedResult(hComm, &osReader, &dwRead, FALSE))
// Error in communications; report it.
else
// Read completed successfully.
HandleASuccessfulRead(lpBuf, dwRead);

// Reset flag so that another opertion can be issued.
fWaitingOnRead = FALSE;
break;

case WAIT_TIMEOUT:
// Operation isn't complete yet. fWaitingOnRead flag isn't
// changed since I'll loop back around, and I don't want
// to issue another read until the first one finishes.
//
// This is a good time to do some background work.
break;

default:
// Error in the WaitForSingleObject; abort.
// This indicates a problem with the OVERLAPPED structure's
// event handle.
break;
}
}

这一段程序在 98 下正常，但非常不幸的是在 Win2000 下，ReadFile 总是返回读正确，并不返回 ERROR_IO_PENDING，使下面的 WaitForSingleObject 的循环形同虚设，要命的是，ReadFile 返回读正确却每次只读一个字节，结果程序工作得很奇怪，即使缓冲区中有很多的字符，程序也每次只能读一个字符，要等到发送字符或做其他的操作使线路状态改变了，才能读下一个字符，我不知道这个奇怪的现象是如何发生的，反正我解决的办法是在 ReadFile 前加 WaitCommEvent，真正等到 EV_RXCHAR 以后才去 ReadFile，到最后，我用的循环是这样的，虽然没有一篇文章中的例子是这样的，但它却同时在 windows9x 和 windows2000 下工作得很好：

.while dwFlag & IF_CONNECT
;********************************************************************
; 检测其它的通信事件
; 如果检测到且定义了 lpProcessEvent 则调用 lpProcessEvent
;********************************************************************
invoke WaitCommEvent,hCom,addr @dwEvent,NULL ;addr stReadState
push eax
invoke ClearCommError,hCom,addr @dwError,addr @stComStat
pop eax
.if eax == 0
invoke GetLastError
.if eax == ERROR_IO_PENDING
or dwFlag,IF_WAITING
.endif
.else
;这里是线路状态的处理
.endif
;********************************************************************
; 如果没有在等待异步读的过程中，则读端口
;********************************************************************
.if ! (dwFlag & IF_WAITING)
mov @dwBytesRead,0
invoke ReadFile,hCom,addr @szBuffer,sizeof @szBuffer,\
addr @dwBytesRead,addr stReadState
.if eax == FALSE
or dwFlag,IF_WAITING
invoke GetLastError
.if eax != ERROR_IO_PENDING
;这里是错误处理
.endif
.else
and dwFlag,not IF_WAITING
mov eax,@dwBytesRead
.if eax != 0
;这里是接收到的数据处理
.endif
.endif
.endif
;********************************************************************
; 如果在异步读端口中，则等待一段时间
;********************************************************************
.if dwFlag & IF_WAITING
invoke WaitForSingleObject,stReadState.hEvent,200
.if eax == WAIT_OBJECT_0
and dwFlag,not IF_WAITING
invoke GetOverlappedResult,hCom,addr stReadState,\
addr @dwBytesRead,FALSE
.if eax != 0
mov eax,@dwBytesRead
.if eax != 0
;这里是接收到的数据处理
.endif
.else
;这里是错误处理
invoke ClearCommError,hCom,addr @dwError,addr @stComStat
.endif
.else
;这里是错误处理
.endif
.endif
.endw

7 流控制的问题

在流控制方式为“无”和“软件控制”的情况下，基本上没有什么问题，但在“硬件控制”下，win32 手册中说明 RTS_CONTROL_HANDSHAKE 控制方式的含义是：

Enables RTS handshaking. The driver raises the RTS line when the "type-ahead" (input) buffer is less than one-half full and lowers the RTS line when the buffer is more than three-quarters full. If handshaking is enabled, it is an error for the application to adjust the line by using the EscapeCommFunction function.

也就是说，当缓冲区快满的时候 RTS 会自动 OFF 通知对方暂停发送，当缓冲区重新空出来的时候， RTS 会自动 ON，但我发现当 RTS 变 OFF 以后即使你已经清空了缓冲区， RTS 也不会自动的 ON，造成对方停在那里不发送了，所以，如果要用硬件流控制的话，还要在接收后最好加上检测缓冲区大小的判断，具体是使用 ClearCommError 后返回的 COMSTAT.cbInQue，当缓冲区已经空出来的时候，要使用 invoke EscapeCommFunction,hCom,SETRTS 重新将 RTS 设置为 ON。

以上是我在串行通讯编程中发现的实际问题，如果有和实际不符的地方或者别的问题，欢迎到编程论坛探讨。


Serial Communications in Win32

Allen Denver
Microsoft Windows Developer Support

December 11, 1995

Allen seldom eats breakfast, but if he had to pick a favorite, Win32 serial communications would be the top choice.

Click to open or copy the files in the MTTTY sample application for this technical article.

Abstract

Serial communications in Microsoft?Win32?is significantly different from serial communications in 16-bit Microsoft Windows? Those familiar with 16-bit serial communications functions will have to relearn many parts of the system to program serial communications properly. This article will help to accomplish this. Those unfamiliar with serial communications will find this article a helpful foundation for development efforts.

This article assumes the reader is familiar with the fundamentals of multiple threading and synchronization in Win32. In addition, a basic familiarity of the Win32 heap functions is useful to fully comprehend the memory management methods used by the sample, MTTTY, included with this article. For more information regarding these functions, consult the Platform SDK documentation, the Microsoft Win32 SDK Knowledge Base, or the Microsoft Developer Network Library. Application programming interfaces (APIs) that control user interface features of windows and dialog boxes, though not discussed here, are useful to know in order to fully comprehend the sample provided with this article. Readers unfamiliar with general Windows programming practices should learn some of the fundamentals of general Windows programming before taking on serial communications. In other words, get your feet wet before diving in head first.

Introduction

The focus of this article is on application programming interfaces (APIs) and methods that are compatible with Microsoft?Windows NT?and Windows 95; therefore, APIs supported on both platforms are the only ones discussed. Windows 95 supports the Win32?Telephony API (TAPI) and Windows NT 3.x does not; therefore, this discussion will not include TAPI. TAPI does deserve mention, however, in that it very nicely implements modem interfacing and call controlling. A production application that works with modems and makes telephone calls should implement these features using the TAPI interface. This will allow seamless integration with the other TAPI-enabled applications that a user may have. Furthermore, this article does not discuss some of the configuration functions in Win32, such as GetCommProperties.

The article is broken into the following sections: Opening a port, reading and writing (nonoverlapped and overlapped), serial status (events and errors), and serial settings (DCB, flow control, and communications time-outs).

The sample included with this article, MTTTY: Multithreaded TTY, implements many of the features discussed here. It uses three threads in its implementation: a user interface thread that does memory management, a writer thread that controls all writing, and a reader/status thread that reads data and handles status changes on the port. The sample employs a few different data heaps for memory management. It also makes extensive use of synchronization methods to facilitate communication between threads.

Opening a Port

The CreateFile function opens a communications port. There are two ways to call CreateFile to open the communications port: overlapped and nonoverlapped. The following is the proper way to open a communications resource for overlapped operation:

HANDLE hComm;
hComm = CreateFile( gszPort,
                    GENERIC_READ | GENERIC_WRITE,
                    0,
                    0,
                    OPEN_EXISTING,
                    FILE_FLAG_OVERLAPPED,
                    0);
if (hComm == INVALID_HANDLE_VALUE)
   // error opening port; abort

Removal of the FILE_FLAG_OVERLAPPED flag from the call to CreateFile specifies nonoverlapped operation. The next section discusses overlapped and nonoverlapped operations.

The Platform SDK documentation states that when opening a communications port, the call to CreateFile has the following requirements:

  • fdwShareMode must be zero. Communications ports cannot be shared in the same manner that files are shared. Applications using TAPI can use the TAPI functions to facilitate sharing resources between applications. For Win32 applications not using TAPI, handle inheritance or duplication is necessary to share the communications port. Handle duplication is beyond the scope of this article; please refer to the Platform SDK documentation for more information.

  • fdwCreate must specify the OPEN_EXISTING flag.

  • hTemplateFile parameter must be NULL.

One thing to note about port names is that traditionally they have been COM1, COM2, COM3, or COM4. The Win32 API does not provide any mechanism for determining what ports exist on a system. Windows NT and Windows 95 keep track of installed ports differently from one another, so any one method would not be portable across all Win32 platforms. Some systems even have more ports than the traditional maximum of four. Hardware vendors and serial-device-driver writers are free to name the ports anything they like. For this reason, it is best that users have the ability to specify the port name they want to use. If a port does not exist, an error will occur (ERROR_FILE_NOT_FOUND) after attempting to open the port, and the user should be notified that the port isn’t available.

Reading and Writing

Reading from and writing to communications ports in Win32 is very similar to file input/output (I/O) in Win32. In fact, the functions that accomplish file I/O are the same functions used for serial I/O. I/O in Win32 can be done either of two ways: overlapped or nonoverlapped. The Platform SDK documentation uses the terms asynchronous and synchronous to connote these types of I/O operations. This article, however, uses the terms overlapped and nonoverlapped.

Nonoverlapped I/O is familiar to most developers because this is the traditional form of I/O, where an operation is requested and is assumed to be complete when the function returns. In the case of overlapped I/O, the system may return to the caller immediately even when an operation is not finished and will signal the caller when the operation completes. The program may use the time between the I/O request and its completion to perform some “background?work.

Reading and writing in Win32 is significantly different from reading and writing serial communications ports in 16-bit Windows. 16-bit Windows only has the ReadComm and WriteComm functions. Win32 reading and writing can involve many more functions and choices. These issues are discussed below.

Nonoverlapped I/O

Nonoverlapped I/O is very straightforward, though it has limitations. An operation takes place while the calling thread is blocked. Once the operation is complete, the function returns and the thread can continue its work. This type of I/O is useful for multithreaded applications because while one thread is blocked on an I/O operation, other threads can still perform work. It is the responsibility of the application to serialize access to the port correctly. If one thread is blocked waiting for its I/O operation to complete, all other threads that subsequently call a communications API will be blocked until the original operation completes. For instance, if one thread were waiting for a ReadFile function to return, any other thread that issued a WriteFile function would be blocked.

One of the many factors to consider when choosing between nonoverlapped and overlapped operations is portability. Overlapped operation is not a good choice because most operating systems do not support it. Most operating systems support some form of multithreading, however, so multithreaded nonoverlapped I/O may be the best choice for portability reasons.

Overlapped I/O

Overlapped I/O is not as straightforward as nonoverlapped I/O, but allows more flexibility and efficiency. A port open for overlapped operations allows multiple threads to do I/O operations at the same time and perform other work while the operations are pending. Furthermore, the behavior of overlapped operations allows a single thread to issue many different requests and do work in the background while the operations are pending.

In both single-threaded and multithreaded applications, some synchronization must take place between issuing requests and processing the results. One thread will have to be blocked until the result of an operation is available. The advantage is that overlapped I/O allows a thread to do some work between the time of the request and its completion. If no work can be done, then the only case for overlapped I/O is that it allows for better user responsiveness.

Overlapped I/O is the type of operation that the MTTTY sample uses. It creates a thread that is responsible for reading the port’s data and reading the port’s status. It also performs periodic background work. The program creates another thread exclusively for writing data out the port.

Note Applications sometimes abuse multithreading systems by creating too many threads. Although using multiple threads can resolve many difficult problems, creating excessive threads is not the most efficient use of them in an application. Threads are less a strain on the system than processes but still require system resources such as CPU time and memory. An application that creates excessive threads may adversely affect the performance of the entire system. A better use of threads is to create a different request queue for each type of job and to have a worker thread issue an I/O request for each entry in the request queue. This method is used by the MTTTY sample provided with this article.

An overlapped I/O operation has two parts: the creation of the operation and the detection of its completion. Creating the operation entails setting up an OVERLAPPED structure, creating a manual-reset event for synchronization, and calling the appropriate function (ReadFile or WriteFile). The I/O operation may or may not be completed immediately. It is an error for an application to assume that a request for an overlapped operation always yields an overlapped operation. If an operation is completed immediately, an application needs to be ready to continue processing normally. The second part of an overlapped operation is to detect its completion. Detecting completion of the operation involves waiting for the event handle, checking the overlapped result, and then handling the data. The reason that there is more work involved with an overlapped operation is that there are more points of failure. If a nonoverlapped operation fails, the function just returns an error-return result. If an overlapped operation fails, it can fail in the creation of the operation or while the operation is pending. You may also have a time-out of the operation or a time-out waiting for the signal that the operation is complete.

Reading

The ReadFile function issues a read operation. ReadFileEx also issues a read operation, but since it is not available on Windows 95, it is not discussed in this article. Here is a code snippet that details how to issue a read request. Notice that the function calls a function to process the data if the ReadFile returns TRUE. This is the same function called if the operation becomes overlapped. Note the fWaitingOnRead flag that is defined by the code; it indicates whether or not a read operation is overlapped. It is used to prevent the creation of a new read operation if one is outstanding.

DWORD dwRead;
BOOL fWaitingOnRead = FALSE;
OVERLAPPED osReader = {0};

// Create the overlapped event. Must be closed before exiting
// to avoid a handle leak.
osReader.hEvent = CreateEvent(NULL, TRUE, FALSE, NULL);

if (osReader.hEvent == NULL)
   // Error creating overlapped event; abort.

if (!fWaitingOnRead) {
   // Issue read operation.
   if (!ReadFile(hComm, lpBuf, READ_BUF_SIZE, &dwRead, &osReader)) {
      if (GetLastError() != ERROR_IO_PENDING)     // read not delayed?
         // Error in communications; report it.
      else
         fWaitingOnRead = TRUE;
   }
   else {
      // read completed immediately
      HandleASuccessfulRead(lpBuf, dwRead);
    }
}

The second part of the overlapped operation is the detection of its completion. The event handle in the OVERLAPPED structure is passed to the WaitForSingleObject function, which will wait until the object is signaled. Once the event is signaled, the operation is complete. This does not mean that it was completed successfully, just that it was completed. The GetOverlappedResult function reports the result of the operation. If an error occurred, GetOverlappedResult returns FALSE and GetLastError returns the error code. If the operation was completed successfully, GetOverlappedResult will return TRUE.

Note GetOverlappedResult can detect completion of the operation, as well as return the operation’s failure status. GetOverlappedResult returns FALSE and GetLastError returns ERROR_IO_INCOMPLETE when the operation is not completed. In addition, GetOverlappedResult can be made to block until the operation completes. This effectively turns the overlapped operation into a nonoverlapped operation and is accomplished by passing TRUE as the bWait parameter.

Here is a code snippet that shows one way to detect the completion of an overlapped read operation. Note that the code calls the same function to process the data that was called when the operation completed immediately. Also note the use of the fWaitingOnRead flag. Here it controls entry into the detection code, since it should be called only when an operation is outstanding.

#define READ_TIMEOUT      500      // milliseconds

DWORD dwRes;

if (fWaitingOnRead) {
   dwRes = WaitForSingleObject(osReader.hEvent, READ_TIMEOUT);
   switch(dwRes)
   {
      // Read completed.
      case WAIT_OBJECT_0:
          if (!GetOverlappedResult(hComm, &osReader, &dwRead, FALSE))
             // Error in communications; report it.
          else
             // Read completed successfully.
             HandleASuccessfulRead(lpBuf, dwRead);

          //  Reset flag so that another opertion can be issued.
          fWaitingOnRead = FALSE;
          break;

      case WAIT_TIMEOUT:
          // Operation isn't complete yet. fWaitingOnRead flag isn't
          // changed since I'll loop back around, and I don't want
          // to issue another read until the first one finishes.
          //
          // This is a good time to do some background work.
          break;

      default:
          // Error in the WaitForSingleObject; abort.
          // This indicates a problem with the OVERLAPPED structure's
          // event handle.
          break;
   }
}

Writing

Transmitting data out the communications port is very similar to reading in that it uses a lot of the same APIs. The code snippet below demonstrates how to issue and wait for a write operation to be completed.

BOOL WriteABuffer(char * lpBuf, DWORD dwToWrite)
{
   OVERLAPPED osWrite = {0};
   DWORD dwWritten;
   DWORD dwRes;
   BOOL fRes;

   // Create this write operation's OVERLAPPED structure's hEvent.
   osWrite.hEvent = CreateEvent(NULL, TRUE, FALSE, NULL);
   if (osWrite.hEvent == NULL)
      // error creating overlapped event handle
      return FALSE;

   // Issue write.
   if (!WriteFile(hComm, lpBuf, dwToWrite, &dwWritten, &osWrite)) {
      if (GetLastError() != ERROR_IO_PENDING) {
         // WriteFile failed, but isn't delayed. Report error and abort.
         fRes = FALSE;
      }
      else
         // Write is pending.
         dwRes = WaitForSingleObject(osWrite.hEvent, INFINITE);
         switch(dwRes)
         {
            // OVERLAPPED structure's event has been signaled.
            case WAIT_OBJECT_0:
                 if (!GetOverlappedResult(hComm, &osWrite, &dwWritten, FALSE))
                       fRes = FALSE;
                 else
                  // Write operation completed successfully.
                  fRes = TRUE;
                 break;

            default:
                 // An error has occurred in WaitForSingleObject.
                 // This usually indicates a problem with the
                // OVERLAPPED structure's event handle.
                 fRes = FALSE;
                 break;
         }
      }
   }
   else
      // WriteFile completed immediately.
      fRes = TRUE;

   CloseHandle(osWrite.hEvent);
   return fRes;
}

Notice that the code above uses the WaitForSingleObject function with a time-out value of INFINITE. This causes the WaitForSingleObject function to wait forever until the operation is completed; this may make the thread or program appear to be “hung?when, in fact, the write operation is simply taking a long time to complete or flow control has blocked the transmission. Status checking, discussed later, can detect this condition, but doesn’t cause the WaitForSingleObject to return. Three things can alleviate this condition:

  • Place the code in a separate thread. This allows other threads to execute any functions they desire while our writer thread waits for the write to be completed. This is what the MTTTY sample does.

  • Use COMMTIMEOUTS to cause the write to be completed after a time-out period has passed. This is discussed more fully in the “Communications Time-outs?section later in this article. This is also what the MTTTY sample allows.

  • Change the WaitForSingleObject call to include a real time-out value. This causes more problems because if the program issues another operation while an older operation is still pending, new OVERLAPPED structures and overlapped events need to be allocated. This type of recordkeeping is difficult, particularly when compared to using a “job queue?design for the operations. The “job queue?method is used in the MTTTY sample.

    Note: The time-out values in synchronization functions are not communications time-outs. Synchronization time-outs cause WaitForSingleObject or WaitForMultipleObjects to return WAIT_TIMEOUT. This is not the same as a read or write operation timing out. Communications time-outs are described later in this article.

Because the WaitForSingleObject function in the above code snippet uses an INFINITE time-out, it is equivalent to using GetOverlappedResult with TRUE for the fWait parameter. Here is equivalent code in a much simplified form:

BOOL WriteABuffer(char * lpBuf, DWORD dwToWrite)
{
   OVERLAPPED osWrite = {0};
   DWORD dwWritten;
   BOOL fRes;

   // Create this writes OVERLAPPED structure hEvent.
   osWrite.hEvent = CreateEvent(NULL, TRUE, FALSE, NULL);
   if (osWrite.hEvent == NULL)
      // Error creating overlapped event handle.
      return FALSE;

   // Issue write.
   if (!WriteFile(hComm, lpBuf, dwToWrite, &dwWritten, &osWrite)) {
      if (GetLastError() != ERROR_IO_PENDING) {
         // WriteFile failed, but it isn't delayed. Report error and abort.
         fRes = FALSE;
      }
      else {
         // Write is pending.
         if (!GetOverlappedResult(hComm, &osWrite, &dwWritten, TRUE))
            fRes = FALSE;
         else
            // Write operation completed successfully.
            fRes = TRUE;
      }
   }
   else
      // WriteFile completed immediately.
      fRes = TRUE;

   CloseHandle(osWrite.hEvent);
   return fRes;
}

GetOverlappedResult is not always the best way to wait for an overlapped operation to be completed. For example, if an application needs to wait on another event handle, the first code snippet serves as a better model than the second. The call to WaitForSingleObject is easy to change to WaitForMultipleObjects to include the additional handles on which to wait. This is what the MTTTY sample application does.

A common mistake in overlapped I/O is to reuse an OVERLAPPED structure before the previous overlapped operation is completed. If a new overlapped operation is issued before a previous operation is completed, a new OVERLAPPED structure must be allocated for it. A new manual-reset event for the hEvent member of the OVERLAPPED structure must also be created. Once an overlapped operation is complete, the OVERLAPPED structure and its event are free for reuse.

The only member of the OVERLAPPED structure that needs modifying for serial communications is the hEvent member. The other members of the OVERLAPPED structure should be initialized to zero and left alone. Modifying the other members of the OVERLAPPED structure is not necessary for serial communications devices. The documentation for ReadFile and WriteFile state that the Offset and OffsetHigh members of the OVERLAPPED structure must be updated by the application, or else results are unpredictable. This guideline should be applied to OVERLAPPED structures used for other types of resources, such as files.

Serial Status

There are two methods to retrieve the status of a communications port. The first is to set an event mask that causes notification of the application when the desired events occur. The SetCommMask function sets this event mask, and the WaitCommEvent function waits for the desired events to occur. These functions are similar to the 16-bit functions SetCommEventMask and EnableCommNotification, except that the Win32 functions do not post WM_COMMNOTIFY messages. In fact, the WM_COMMNOTIFY message is not even part of the Win32 API. The second method for retrieving the status of the communications port is to periodically call a few different status functions. Polling is, of course, neither efficient nor recommended.

Communications Events

Communications events can occur at any time in the course of using a communications port. The two steps involved in receiving notification of communications events are as follows:

  • SetCommMask sets the desired events that cause a notification.

  • WaitCommEvent issues a status check. The status check can be an overlapped or nonoverlapped operation, just as the read and write operations can be.

    Note: The word event in this context refers to communications events only. It does not refer to an event object used for synchronization.

Here is an example of the SetCommMask function:

DWORD dwStoredFlags;

dwStoredFlags = EV_BREAK | EV_CTS   | EV_DSR | EV_ERR | EV_RING |\
                EV_RLSD | EV_RXCHAR | EV_RXFLAG | EV_TXEMPTY ;
if (!SetCommMask(hComm, dwStoredFlags))
   // error setting communications mask

A description of each type of event is in Table 1.

Table 1. Communications Event Flags

Event Flag | Description
EV_BREAK | A break was detected on input.
EV_CTS | The CTS (clear-to-send) signal changed state. To get the actual state of the CTS line, GetCommModemStatus should be called.
EV_DSR | The DSR (data-set-ready) signal changed state. To get the actual state of the DSR line, GetCommModemStatus should be called.
EV_ERR | A line-status error occurred. Line-status errors are CE_FRAME, CE_OVERRUN, and CE_RXPARITY. To find the cause of the error, ClearCommError should be called.
EV_RING | A ring indicator was detected.
EV_RLSD | The RLSD (receive-line-signal-detect) signal changed state. To get the actual state of the RLSD line, GetCommModemStatus should be called. Note that this is commonly referred to as the CD (carrier detect) line.
EV_RXCHAR | A new character was received and placed in the input buffer. See the “Caveat?section below for a discussion of this flag.
EV_RXFLAG | The event character was received and placed in the input buffer. The event character is specified in the EvtChar member of the DCB structure discussed later. The “Caveat?section below also applies to this flag.
EV_TXEMPTY | The last character in the output buffer was sent to the serial port device. If a hardware buffer is used, this flag only indicates that all data has been sent to the hardware. There is no way to detect when the hardware buffer is empty without talking directly to the hardware with a device driver.

After specifying the event mask, the WaitCommEvent function detects the occurrence of the events. If the port is open for nonoverlapped operation, then the WaitCommEvent function does not contain an OVERLAPPED structure. The function blocks the calling thread until the occurrence of one of the events. If an event never occurs, the thread may block indefinitely.

Here is a code snippet that shows how to wait for an EV_RING event when the port is open for nonoverlapped operation:

   DWORD dwCommEvent;

   if (!SetCommMask(hComm, EV_RING))
      // Error setting communications mask
      return FALSE;

   if (!WaitCommEvent(hComm, &dwCommEvent, NULL))
      // An error occurred waiting for the event.
      return FALSE;
   else
      // Event has occurred.
      return TRUE;

Note The Microsoft Win32 SDK Knowledge Base documents a problem with Windows 95 and the EV_RING flag. The above code never returns in Windows 95 because the EV_RING event is not detected by the system; Windows NT properly reports the EV_RING event. Please see the Win32 SDK Knowledge Base for more information on this bug.

As noted, the code above can be blocked forever if an event never occurs. A better solution would be to open the port for overlapped operation and wait for a status event in the following manner:

   #define STATUS_CHECK_TIMEOUT      500   // Milliseconds

   DWORD      dwRes;
   DWORD      dwCommEvent;
   DWORD      dwStoredFlags;
   BOOL      fWaitingOnStat = FALSE;
   OVERLAPPED osStatus = {0};

   dwStoredFlags = EV_BREAK | EV_CTS | EV_DSR | EV_ERR | EV_RING |\
                  EV_RLSD | EV_RXCHAR | EV_RXFLAG | EV_TXEMPTY ;
   if (!SetCommMask(comHandle, dwStoredFlags))
      // error setting communications mask; abort
      return 0;

   osStatus.hEvent = CreateEvent(NULL, TRUE, FALSE, NULL);
   if (osStatus.hEvent == NULL)
      // error creating event; abort
      return 0;

   for ( ; ; ) {
      // Issue a status event check if one hasn't been issued already.
      if (!fWaitingOnStat) {
         if (!WaitCommEvent(hComm, &dwCommEvent, &osStatus)) {
            if (GetLastError() == ERROR_IO_PENDING)
               bWaitingOnStatusHandle = TRUE;
            else
               // error in WaitCommEvent; abort
               break;
         }
         else
            // WaitCommEvent returned immediately.
            // Deal with status event as appropriate.
            ReportStatusEvent(dwCommEvent);
      }

      // Check on overlapped operation.
      if (fWaitingOnStat) {
         // Wait a little while for an event to occur.
         dwRes = WaitForSingleObject(osStatus.hEvent, STATUS_CHECK_TIMEOUT);
         switch(dwRes)
         {
             // Event occurred.
             case WAIT_OBJECT_0:
                 if (!GetOverlappedResult(hComm, &osStatus, &dwOvRes, FALSE))
                    // An error occurred in the overlapped operation;
                    // call GetLastError to find out what it was
                    // and abort if it is fatal.
                 else
                    // Status event is stored in the event flag
                    // specified in the original WaitCommEvent call.
                    // Deal with the status event as appropriate.
                    ReportStatusEvent(dwCommEvent);

                 // Set fWaitingOnStat flag to indicate that a new
                 // WaitCommEvent is to be issued.
                 fWaitingOnStat = FALSE;
                 break;

             case WAIT_TIMEOUT:
                 // Operation isn't complete yet. fWaitingOnStatusHandle flag
                 // isn't changed since I'll loop back around and I don't want
                 // to issue another WaitCommEvent until the first one finishes.
                 //
                 // This is a good time to do some background work.
                DoBackgroundWork();
                 break;

             default:
                 // Error in the WaitForSingleObject; abort
                 // This indicates a problem with the OVERLAPPED structure's
                 // event handle.
                CloseHandle(osStatus.hEvent);
                return 0;
         }
      }
   }

   CloseHandle(osStatus.hEvent);

The code above very closely resembles the code for overlapped reading. In fact, the MTTTY sample implements its reading and status checking in the same thread using WaitForMultipleObjects to wait for either the read event or the status event to become signaled.

There are two interesting side effects of SetCommMask and WaitCommEvent. First, if the communications port is open for nonoverlapped operation, WaitCommEvent will be blocked until an event occurs. If another thread calls SetCommMask to set a new event mask, that thread will be blocked on the call to SetCommMask. The reason is that the original call to WaitCommEvent in the first thread is still executing. The call to SetCommMask blocks the thread until the WaitCommEvent function returns in the first thread. This side effect is universal for ports open for nonoverlapped I/O. If a thread is blocked on any communications function and another thread calls a communications function, the second thread is blocked until the communications function returns in the first thread. The second interesting note about these functions is their use on a port open for overlapped operation. If SetCommMask sets a new event mask, any pending WaitCommEvent will complete successfully, and the event mask produced by the operation is NULL.

Caveat

Using the EV_RXCHAR flag will notify the thread that a byte arrived at the port. This event, used in combination with the ReadFile function, enables a program to read data only after it is in the receive buffer, as opposed to issuing a read that waits for the data to arrive. This is particularly useful when a port is open for nonoverlapped operation because the program does not need to poll for incoming data; the program is notified of the incoming data by the occurrence of the EV_RXCHAR event. Initial attempts to code this solution often produce the following pseudocode, including one oversight covered later in this section:

DWORD dwCommEvent;
DWORD dwRead;
char  chRead;

if (!SetCommMask(hComm, EV_RXCHAR))
   // Error setting communications event mask.

for ( ; ; ) {
   if (WaitCommEvent(hComm, &dwCommEvent, NULL)) {
      if (ReadFile(hComm, &chRead, 1, &dwRead, NULL))
         // A byte has been read; process it.
      else
         // An error occurred in the ReadFile call.
         break;
   }
   else
      // Error in WaitCommEvent.
      break;
}

The above code waits for an EV_RXCHAR event to occur. When this happens, the code calls ReadFile to read the one byte received. The loop starts again, and the code waits for another EV_RXCHAR event. This code works fine when one or two bytes arrive in quick succession. The byte reception causes the EV_RXCHAR event to occur. The code reads the byte. If no other byte arrives before the code calls WaitCommEvent again, then all is fine; the next byte to arrive will cause the WaitCommEvent function to indicate the occurrence of the EV_RXCHAR event. If another single byte arrives before the code has a chance to reach the WaitCommEvent function, then all is fine, too. The first byte is read as before; the arrival of the second byte causes the EV_RXCHAR flag to be set internally. When the code returns to the WaitCommEvent function, it indicates the occurrence of the EV_RXCHAR event and the second byte is read from the port in the ReadFile call.

The problem with the above code occurs when three or more bytes arrive in quick succession. The first byte causes the EV_RXCHAR event to occur. The second byte causes the EV_RXCHAR flag to be set internally. The next time the code calls WaitCommEvent, it indicates the EV_RXCHAR event. Now, a third byte arrives at the communications port. This third byte causes the system to attempt to set the EV_RXCHAR flag internally. Because this has already occurred when the second byte arrived, the arrival of the third byte goes unnoticed. The code eventually will read the first byte without a problem. After this, the code will call WaitCommEvent, and it indicates the occurrence of the EV_RXCHAR event (from the arrival of the second byte). The second byte is read, and the code returns to the WaitCommEvent function. The third byte waits in the system’s internal receive buffer. The code and the system are now out of sync. When a fourth byte finally arrives, the EV_RXCHAR event occurs, and the code reads a single byte. It reads the third byte. This will continue indefinitely.

The solution to this problem seems as easy as increasing the number of bytes requested in the read operation. Instead of requesting a single byte, the code could request two, ten, or some other number of bytes. The problem with this idea is that it still fails when two or more extra bytes above the size of the read request arrive at the port in quick succession. So, if two bytes are read, then four bytes arriving in quick succession would cause the problem. Ten bytes requested would still fail if twelve bytes arrived in quick succession.

The real solution to this problem is to read from the port until no bytes are remaining. The following pseudocode solves the problem by reading in a loop until zero characters are read. Another possible method would be to call ClearCommError to determine the number of bytes in the buffer and read them all in one read operation. This method requires more sophisticated buffer management, but it reduces the number of reads when a lot of data arrives at once.

DWORD dwCommEvent;
DWORD dwRead;
char  chRead;

if (!SetCommMask(hComm, EV_RXCHAR))
   // Error setting communications event mask

for ( ; ; ) {
   if (WaitCommEvent(hComm, &dwCommEvent, NULL)) {
      do {
         if (ReadFile(hComm, &chRead, 1, &dwRead, NULL))
            // A byte has been read; process it.
         else
            // An error occurred in the ReadFile call.
            break;
      } while (dwRead);
   }
   else
      // Error in WaitCommEvent
      break;
}

The above code does not work correctly without setting the proper time-outs. Communications time-outs, discussed later, affect the behavior of the ReadFile operation in order to cause it to return without waiting for bytes to arrive. Discussion of this topic occurs later in the “Communications Time-outs?section of this article.

The above caveat regarding EV_RXCHAR also applies to EV_RXFLAG. If flag characters arrive in quick succession, EV_RXFLAG events may not occur for all of them. Once again, the best solution is to read all bytes until none remain.

The above caveat also applies to other events not related to character reception. If other events occur in quick succession some of the notifications will be lost. For instance, if the CTS line voltage starts high, then goes low, high, and low again, an EV_CTS event occurs. There is no guarantee of how many EV_CTS events will actually be detected with WaitCommEvent if the changes in the CTS line happen quickly. For this reason, WaitCommEvent cannot be used to keep track of the state of the line. Line status is covered in the “Modem Status?section later in this article.

Error Handling and Communications Status

One of the communications event flags specified in the call to SetCommMask is possibly EV_ERR. The occurrence of the EV_ERR event indicates that an error condition exists in the communications port. Other errors can occur in the port that do not cause the EV_ERR event to occur. In either case, errors associated with the communications port cause all I/O operations to be suspended until removal of the error condition. ClearCommError is the function to call to detect errors and clear the error condition.

ClearCommError also provides communications status indicating why transmission has stopped; it also indicates the number of bytes waiting in the transmit and receive buffers. The reason why transmission may stop is because of errors or to flow control. The discussion of flow control occurs later in this article.

Here is some code that demonstrates how to call ClearCommError:

    COMSTAT comStat;
    DWORD   dwErrors;
    BOOL    fOOP, fOVERRUN, fPTO, fRXOVER, fRXPARITY, fTXFULL;
    BOOL    fBREAK, fDNS, fFRAME, fIOE, fMODE;

    // Get and clear current errors on the port.
    if (!ClearCommError(hComm, &dwErrors, &comStat))
        // Report error in ClearCommError.
        return;

    // Get error flags.
    fDNS = dwErrors & CE_DNS;
    fIOE = dwErrors & CE_IOE;
    fOOP = dwErrors & CE_OOP;
    fPTO = dwErrors & CE_PTO;
    fMODE = dwErrors & CE_MODE;
    fBREAK = dwErrors & CE_BREAK;
    fFRAME = dwErrors & CE_FRAME;
    fRXOVER = dwErrors & CE_RXOVER;
    fTXFULL = dwErrors & CE_TXFULL;
    fOVERRUN = dwErrors & CE_OVERRUN;
    fRXPARITY = dwErrors & CE_RXPARITY;

    // COMSTAT structure contains information regarding
    // communications status.
    if (comStat.fCtsHold)
        // Tx waiting for CTS signal

    if (comStat.fDsrHold)
        // Tx waiting for DSR signal

    if (comStat.fRlsdHold)
        // Tx waiting for RLSD signal

    if (comStat.fXoffHold)
        // Tx waiting, XOFF char rec'd

    if (comStat.fXoffSent)
        // Tx waiting, XOFF char sent

    if (comStat.fEof)
        // EOF character received

    if (comStat.fTxim)
        // Character waiting for Tx; char queued with TransmitCommChar

    if (comStat.cbInQue)
        // comStat.cbInQue bytes have been received, but not read

    if (comStat.cbOutQue)
        // comStat.cbOutQue bytes are awaiting transfer

Modem Status (a.k.a. Line Status)

The call to SetCommMask may include the flags EV_CTS, EV_DSR, EV_RING, and EV_RLSD. These flags indicate changes in the voltage on the lines of the serial port. There is no indication of the actual status of these lines, just that a change occurred. The GetCommModemStatus function retrieves the actual state of these status lines by returning a bit mask indicating a 0 for low or no voltage and 1 for high voltage for each of the lines.

Please note that the term RLSD (Receive Line Signal Detect) is commonly referred to as the CD (Carrier Detect) line.

Note The EV_RING flag does not work in Windows 95 as mentioned earlier. The GetCommModemStatus function, however, does detect the state of the RING line.

Changes in these lines may also cause a flow-control event. The ClearCommError function reports whether transmission is suspended because of flow control. If necessary, a thread may call ClearCommError to detect whether the event is the cause of a flow-control action. Flow control is covered in the “Flow Control?section later in this article.

Here is some code that demonstrates how to call GetCommModemStatus:

   DWORD dwModemStatus;
   BOOL  fCTS, fDSR, fRING, fRLSD;

   if (!GetCommModemStatus(hComm, &dwModemStatus))
      // Error in GetCommModemStatus;
      return;

   fCTS = MS_CTS_ON & dwModemStatus;
   fDSR = MS_DSR_ON & dwModemStatus;
   fRING = MS_RING_ON & dwModemStatus;
   fRLSD = MS_RLSD_ON & dwModemStatus;

   // Do something with the flags.

Extended Functions

The driver will automatically change the state of control lines as necessary. Generally speaking, changing status lines is under the control of a driver. If a device uses communications port control lines in a manner different from RS-232 standards, the standard serial communications driver will not work to control the device. If the standard serial communications driver will not control the device, a custom device driver is necessary.

There are occasions when standard control lines are under the control of the application instead of the serial communications driver. For instance, an application may wish to implement its own flow control. The application would be responsible for changing the status of the RTS and DTR lines. EscapeCommFunction directs a communications driver to perform such extended operations. EscapeCommFunction can make the driver perform some other function, such as setting or clearing a BREAK condition. For more information on this function, consult the Platform SDK documentation, the Microsoft Win32 SDK Knowledge Base, or the Microsoft Developer Network (MSDN) Library.

Serial Settings

DCB Settings

The most crucial aspect of programming serial communications applications is the settings in the Device-Control Block (DCB) structure. The most common errors in serial communications programming occur in initializing the DCB structure improperly. When the serial communications functions do not behave as expected, a close examination of the DCB structure usually reveals the problem.

There are three ways to initialize a DCB structure. The first method is to use the function GetCommState. This function returns the current DCB in use for the communications port. The following code shows how to use the GetCommState function:

   DCB dcb = {0};

   if (!GetCommState(hComm, &dcb))
      // Error getting current DCB settings
   else
      // DCB is ready for use.

The second method to initialize a DCB is to use a function called BuildCommDCB. This function fills in the baud, parity type, number of stop bits, and number of data bits members of the DCB. The function also sets the flow-control members to default values. Consult the documentation of the BuildCommDCB function for details on which default values it uses for flow-control members. Other members of the DCB are unaffected by this function. It is the program's duty to make sure the other members of the DCB do not cause errors. The simplest thing to do in this regard is to initialize the DCB structure with zeros and then set the size member to the size, in bytes, of the structure. If the zero initialization of the DCB structure does not occur, then there may be nonzero values in the reserved members; this produces an error when trying to use the DCB later. The following function shows how to properly use this method:

   DCB dcb;

   FillMemory(&dcb, sizeof(dcb), 0);
   dcb.DCBlength = sizeof(dcb);
   if (!BuildCommDCB("9600,n,8,1", &dcb)) {
      // Couldn't build the DCB. Usually a problem
      // with the communications specification string.
      return FALSE;
   }
   else
      // DCB is ready for use.

The third method to initialize a DCB structure is to do it manually. The program allocates the DCB structure and sets each member with any value desired. This method does not deal well with changes to the DCB in future implementations of Win32 and is not recommended.

An application usually needs to set some of the DCB members differently than the defaults or may need to modify settings in the middle of execution. Once proper initialization of the DCB occurs, modification of individual members is possible. The changes to the DCB structure do not have any effect on the behavior of the port until execution of the SetCommState function. Here is a section of code that retrieves the current DCB, changes the baud, and then attempts to set the configuration:

   DCB dcb;

   FillMemory(&dcb, sizeof(dcb), 0);
   if (!GetCommState(hComm, &dcb))     // get current DCB
      // Error in GetCommState
      return FALSE;

   // Update DCB rate.
   dcb.BaudRate = CBR_9600 ;

   // Set new state.
   if (!SetCommState(hComm, &dcb))
      // Error in SetCommState. Possibly a problem with the communications
      // port handle or a problem with the DCB structure itself.

Here is an explanation of each of the members of the DCB and how they affect other parts of the serial communications functions.

Note Most of this information is from the Platform SDK documentation. Because documentation is the official word in what the members actually are and what they mean, this table may not be completely accurate if changes occur in the operating system.

Table 2. The DCB Structure Members

Member | Description
DCBlength | Size, in bytes, of the structure. Should be set before calling SetCommState to update the settings.
BaudRate | Specifies the baud at which the communications device operates. This member can be an actual baud value, or a baud index.
fBinary | Specifies whether binary mode is enabled. The Win32 API does not support nonbinary mode transfers, so this member should be TRUE. Trying to use FALSE will not work.
fParity | Specifies whether parity checking is enabled. If this member is TRUE, parity checking is performed and parity errors are reported. This should not be confused with the Parity member, which controls the type of parity used in communications.
fOutxCtsFlow | Specifies whether the CTS (clear-to-send) signal is monitored for output flow control. If this member is TRUE and CTS is low, output is suspended until CTS is high again. The CTS signal is under control of the DCE (usually a modem), the DTE (usually the PC) simply monitors the status of this signal, the DTE does not change it.
fOutxDsrFlow | Specifies whether the DSR (data-set-ready) signal is monitored for output flow control. If this member is TRUE and DSR is low, output is suspended until DSR is high again. Once again, this signal is under the control of the DCE; the DTE only monitors this signal.
fDtrControl | Specifies the DTR (data-terminal-ready) input flow control. This member can be one of the following values:
 | Value |  | Meaning
 | DTR_CONTROL_DISABLE |  | Lowers the DTR line when the device is opened. The application can adjust the state of the line with EscapeCommFunction.
 | DTR_CONTROL_ENABLE |  | Raises the DTR line when the device is opened. The application can adjust the state of the line with EscapeCommFunction.
 | DTR_CONTROL_HANDSHAKE |  | Enables DTR flow-control handshaking. If this value is used, it is an error for the application to adjust the line with EscapeCommFunction.
fDsrSensitivity | Specifies whether the communications driver is sensitive to the state of the DSR signal. If this member is TRUE, the driver ignores any bytes received, unless the DSR modem input line is high.
fTXContinueOnXoff | Specifies whether transmission stops when the input buffer is full and the driver has transmitted the XOFF character. If this member is TRUE, transmission continues after the XOFF character has been sent. If this member is FALSE, transmission does not continue until the input buffer is within XonLim bytes of being empty and the driver has transmitted the XON character.
fOutX | Specifies whether XON/XOFF flow control is used during transmission. If this member is TRUE, transmission stops when the XOFF character is received and starts again when the XON character is received.
fInX | Specifies whether XON/XOFF flow control is used during reception. If this member is TRUE, the XOFF character is sent when the input buffer comes within XoffLim bytes of being full, and the XON character is sent when the input buffer comes within XonLim bytes of being empty.
fErrorChar | Specifies whether bytes received with parity errors are replaced with the character specified by the ErrorChar member. If this member is TRUE and the fParity member is TRUE, replacement occurs.
fNull | Specifies whether null bytes are discarded. If this member is TRUE, null bytes are discarded when received.
fRtsControl | Specifies the RTS (request-to-send) input flow control. If this value is zero, the default is RTS_CONTROL_HANDSHAKE. This member can be one of the following values:
 | Value |  | Meaning
 | RTS_CONTROL_DISABLE |  | Lowers the RTS line when the device is opened. The application can use EscapeCommFunction to change the state of the line.
 | RTS_CONTROL_ENABLE |  | Raises the RTS line when the device is opened. The application can use EscapeCommFunction to change the state of the line.
 | RTS_CONTROL_HANDSHAKE |  | Enables RTS flow-control handshaking. The driver raises the RTS line, enabling the DCE to send, when the input buffer has enough room to receive data. The driver lowers the RTS line, preventing the DCE to send, when the input buffer does not have enough room to receive data. If this value is used, it is an error for the application to adjust the line with EscapeCommFunction.
 | RTS_CONTROL_TOGGLE |  | Specifies that the RTS line will be high if bytes are available for transmission. After all buffered bytes have been sent, the RTS line will be low. If this value is set, it would be an error for an application to adjust the line with EscapeCommFunction. This value is ignored in Windows 95; it causes the driver to act as if RTS_CONTROL_ENABLE were specified.
fAbortOnError | Specifies whether read and write operations are terminated if an error occurs. If this member is TRUE, the driver terminates all read and write operations with an error status (ERROR_IO_ABORTED) if an error occurs. The driver will not accept any further communications operations until the application has acknowledged the error by calling the ClearCommError function.
fDummy2 | Reserved; do not use.
wReserved | Not used; must be set to zero.
XonLim | Specifies the minimum number of bytes allowed in the input buffer before the XON character is sent.
XoffLim | Specifies the maximum number of bytes allowed in the input buffer before the XOFF character is sent. The maximum number of bytes allowed is calculated by subtracting this value from the size, in bytes, of the input buffer.
Parity | Specifies the parity scheme to be used. This member can be one of the following values:
 | Value | Meaning
 | EVENPARITY | Even
 | MARKPARITY | Mark
 | NOPARITY | No parity
 | ODDPARITY | Odd
StopBits | Specifies the number of stop bits to be used. This member can be one of the following values:
 | Value | Meaning
 | ONESTOPBIT | 1 stop bit
 | ONE5STOPBITS | 1.5 stop bits
 | TWOSTOPBITS | 2 stop bits
XonChar | Specifies the value of the XON character for both transmission and reception.
XoffChar | Specifies the value of the XOFF character for both transmission and reception.
ErrorChar | Specifies the value of the character used to replace bytes received with a parity error.
EofChar | Specifies the value of the character used to signal the end of data.
EvtChar | Specifies the value of the character used to cause the EV_RXFLAG event. This setting does not actually cause anything to happen without the use of EV_RXFLAG in the SetCommMask function and the use of WaitCommEvent.
wReserved1 | Reserved; do not use.

Flow Control

Flow control in serial communications provides a mechanism for suspending communications while one of the devices is busy or for some reason cannot do any communication. There are traditionally two types of flow control: hardware and software.

A common problem with serial communications is write operations that actually do not write the data to the device. Often, the problem lies in flow control being used when the program did not specify it. A close examination of the DCB structure reveals that one or more of the following members may be TRUE: fOutxCtsFlow, fOutxDsrFlow, or fOutX. Another mechanism to reveal that flow control is enabled is to call ClearCommError and examine the COMSTAT structure. It will reveal when transmission is suspended because of flow control.

Before discussing the types of flow control, a good understanding of some terms is in order. Serial communications takes place between two devices. Traditionally, there is a PC and a modem or printer. The PC is labeled the Data Terminal Equipment (DTE). The DTE is sometimes called the host. The modem, printer, or other peripheral equipment is identified as the Data Communications Equipment (DCE). The DCE is sometimes referred to as the device.

Hardware flow control

Hardware flow control uses voltage signals on control lines of the serial cable to control whether sending or receiving is enabled. The DTE and the DCE must agree on the types of flow control used for a communications session. Setting the DCB structure to enable flow control just configures the DTE. The DCE also needs configuration to make certain the DTE and DCE use the same type of flow control. There is no mechanism provided by Win32 to set the flow control of the DCE. DIP switches on the device, or commands sent to it typically establish its configuration. The following table describes the control lines, the direction of the flow control, and the line's effect on the DTE and DCE.

Table 3. Hardware Flow-control Lines

Line and Direction Effect on DTE/DCE
CTS
(Clear To Send)
Output flow control 		DCE sets the line high to indicate that it can receive data. DCE sets the line low to indicate that it cannot receive data.

If the fOutxCtsFlow member of the DCB is TRUE, then the DTE will not send data if this line is low. It will resume sending if the line is high.

If the fOutxCtsFlow member of the DCB is FALSE, then the state of the line does not affect transmission.
DSR
(Data Set Ready)
Output flow control 		DCE sets the line high to indicate that it can receive data. DCE sets the line low to indicate that it cannot receive data.

If the fOutxDsrFlow member of the DCB is TRUE, then the DTE will not send data if this line is low. It will resume sending if the line is high.

If the fOutxDsrFlow member of the DCB is FALSE, then the state of the line does not affect transmission.
DSR
(Data Set Ready)
Input flow control 		If the DSR line is low, then data that arrives at the port is ignored. If the DSR line is high, data that arrives at the port is received.

This behavior occurs if the fDsrSensitivity member of the DCB is set to TRUE. If it is FALSE, then the state of the line does not affect reception.
RTS
(Ready To Send)
Input flow control 		The RTS line is controlled by the DTE.

If the fRtsControl member of the DCB is set to RTS_CONTROL_HANDSHAKE, the following flow control is used: If the input buffer has enough room to receive data (at least half the buffer is empty), the driver sets the RTS line high. If the input buffer has little room for incoming data (less than a quarter of the buffer is empty), the driver sets the RTS line low.

If the fRtsControl member of the DCB is set to RTS_CONTROL_TOGGLE, the driver sets the RTS line high when data is available for sending. The driver sets the line low when no data is available for sending. Windows 95 ignores this value and treats it the same as RTS_CONTROL_ENABLE.

If the fRtsControl member of the DCB is set to RTS_CONTROL_ENABLE or RTS_CONTROL_DISABLE, the application is free to change the state of the line as it needs. Note that in this case, the state of the line does not affect reception.

The DCE will suspend transmission when the line goes low. The DCE will resume transmission when the line goes high.
DTR
(Data Terminal Ready)
Input flow control 		The DTR line is controlled by the DTE.

If the fDtrControl member of the DCB is set to DTR_CONTROL_HANDSHAKE, the following flow control is used: If the input buffer has enough room to receive data (at least half the buffer is empty), the driver sets the DTR line high. If the input buffer has little room for incoming data (less than a quarter of the buffer is empty), the driver sets the DTR line low.

If the fDtrControl member of the DCB is set to DTR_CONTROL_ENABLE or DTR_CONTROL_DISABLE, the application is free to change the state of the line as it needs. In this case, the state of the line does not affect reception.

The DCE will suspend transmission when the line goes low. The DCE will resume transmission when the line goes high.

The need for flow control is easy to recognize when the CE_RXOVER error occurs. This error indicates an overflow of the receive buffer and data loss. If data arrives at the port faster than it is read, CE_RXOVER can occur. Increasing the input buffer size may cause the error to occur less frequently, but it does not completely solve the problem. Input flow control is necessary to completely alleviate this problem. When the driver detects that the input buffer is nearly full, it will lower the input flow-control lines. This should cause the DCE to stop transmitting, which gives the DTE enough time to read the data from the input buffer. When the input buffer has more room available, the voltage on flow-control lines is set high, and the DCE resumes sending data.

A similar error is CE_OVERRUN. This error occurs when new data arrives before the communications hardware and serial communications driver completely receives old data. This can occur when the transmission speed is too high for the type of communications hardware or CPU. This can also occur when the operating system is not free to service the communications hardware. The only way to alleviate this problem is to apply some combination of decreasing the transmission speed, replacing the communications hardware, and increasing the CPU speed. Sometimes third-party hardware drivers that are not very efficient with CPU resources cause this error. Flow control cannot completely solve the problems that cause the CE_OVERRUN error, although it may help to reduce the frequency of the error.

Software flow control

Software flow control uses data in the communications stream to control the transmission and reception of data. Because software flow control uses two special characters, XOFF and XON, binary transfers cannot use software flow control; the XON or XOFF character may appear in the binary data and would interfere with data transfer. Software flow control befits text-based communications or data being transferred that does not contain the XON and XOFF characters.

In order to enable software flow control, the fOutX and fInX members of the DCB must be set to TRUE. The fOutX member controls output flow control. The fInX member controls input flow control.

One thing to note is that the DCB allows the program to dynamically assign the values the system recognizes as flow-control characters. The XoffChar member of the DCB dictates the XOFF character for both input and output flow control. The XonChar member of the DCB similarly dictates the XON character.

For input flow control, the XoffLim member of the DCB specifies the minimum amount of free space allowed in the input buffer before the XOFF character is sent. If the amount of free space in the input buffer drops below this amount, then the XOFF character is sent. For input flow control, the XonLim member of the DCB specifies the minimum number of bytes allowed in the input buffer before the XON character is sent. If the amount of data in the input buffer drops below this value, then the XON character is sent.

Table 4 lists the behavior of the DTE when using XOFF/XON flow control.

Table 4. Software flow-control behavior

Flow-control character | Behavior
XOFF received by DTE | DTE transmission is suspended until XON is received. DTE reception continues. The fOutX member of the DCB controls this behavior.
XON received by DTE | If DTE transmission is suspended because of a previous XOFF character being received, DTE transmission is resumed. The fOutX member of the DCB controls this behavior.
XOFF sent from DTE | XOFF is automatically sent by the DTE when the receive buffer approaches full. The actual limit is dictated by the XoffLim member of the DCB. The fInX member of the DCB controls this behavior. DTE transmission is controlled by the fTXContinueOnXoff member of the DCB as described below.
XON sent from the DTE | XON is automatically sent by the DTE when the receive buffer approaches empty. The actual limit is dictated by the XonLim member of the DCB. The fInX member of the DCB controls this behavior.

If software flow control is enabled for input control, then the fTXContinueOnXoff member of the DCB takes effect. The fTXContinueOnXoff member controls whether transmission is suspended after the XOFF character is automatically sent by the system. If fTXContinueOnXoff is TRUE, then transmission continues after the XOFF is sent when the receive buffer is full. If fTXContinueOnXoff is FALSE, then transmission is suspended until the system automatically sends the XON character. DCE devices using software flow control will suspend their sending after the XOFF character is received. Some equipment will resume sending when the XON character is sent by the DTE. On the other hand, some DCE devices will resume sending after any character arrives. The fTXContinueOnXoff member should be set to FALSE when communicating with a DCE device that resumes sending after any character arrives. If the DTE continued transmission after it automatically sent the XOFF, the resumption of transmission would cause the DCE to continue sending, defeating the XOFF.

There is no mechanism available in the Win32 API to cause the DTE to behave the same way as these devices. The DCB structure contains no members for specifying suspended transmission to resume when any character is received. The XON character is the only character that causes transmission to resume.

One other interesting note about software flow control is that reception of XON and XOFF characters causes pending read operations to complete with zero bytes read. The XON and XOFF characters cannot be read by the application, since they are not placed in the input buffer.

A lot of programs on the market, including the Terminal program that comes with Windows, give the user three choices for flow control: Hardware, Software, or None. The Windows operating system itself does not limit an application in this way. The settings of the DCB allow for Software and Hardware flow control simultaneously. In fact, it is possible to separately configure each member of the DCB that affects flow control, which allows for several different flow-control configurations. The limits placed on flow-control choices are there for ease-of-use reasons to reduce confusion for end users. The limits placed on flow-control choices may also be because devices used for communications may not support all types of flow control.

Communications Time-outs

Another major topic affecting the behavior of read and write operations is time-outs. Time-outs affect read and write operations in the following way. If an operation takes longer than the computed time-out period, the operation is completed. There is no error code that is returned by ReadFile, WriteFile, GetOverlappedResult, or WaitForSingleObject. All indicators used to monitor the operation indicate that it completed successfully. The only way to tell that the operation timed out is that the number of bytes actually transferred are fewer than the number of bytes requested. So, if ReadFile returns TRUE, but fewer bytes were read than were requested, the operation timed out. If an overlapped write operation times out, the overlapped event handle is signaled and WaitForSingleObject returns WAIT_OBJECT_O. GetOverlappedResult returns TRUE, but dwBytesTransferred contains the number of bytes that were transferred before the time-out. The following code demonstrates how to handle this in an overlapped write operation:

BOOL WriteABuffer(char * lpBuf, DWORD dwToWrite)
{
   OVERLAPPED osWrite = {0};
   DWORD dwWritten;
   DWORD dwRes;
   BOOL  fRes;

   // Create this write operation's OVERLAPPED structure hEvent.
   osWrite.hEvent = CreateEvent(NULL, TRUE, FALSE, NULL);
   if (osWrite.hEvent == NULL)
      // Error creating overlapped event handle.
      return FALSE;

   // Issue write
   if (!WriteFile(hComm, lpBuf, dwToWrite, &dwWritten, &osWrite)) {
      if (GetLastError() != ERROR_IO_PENDING) {
         // WriteFile failed, but it isn't delayed. Report error.
         fRes = FALSE;
      }
      else
         // Write is pending.
         dwRes = WaitForSingleObject(osWrite.hEvent, INFINITE);
         switch(dwRes)
         {
            // Overlapped event has been signaled.
            case WAIT_OBJECT_0:
                 if (!GetOverlappedResult(hComm, &osWrite, &dwWritten, FALSE))
                       fRes = FALSE;
                 else {
                  if (dwWritten != dwToWrite) {
                     // The write operation timed out. I now need to
                     // decide if I want to abort or retry. If I retry,
                     // I need to send only the bytes that weren't sent.
                     // If I want to abort, I would just set fRes to
                     // FALSE and return.
                     fRes = FALSE;
                  }
                  else
                     // Write operation completed successfully.
                     fRes = TRUE;
                }
                 break;

            default:
                 // An error has occurred in WaitForSingleObject. This usually
                // indicates a problem with the overlapped event handle.
                 fRes = FALSE;
                 break;
         }
      }
   }
   else {
      // WriteFile completed immediately.

      if (dwWritten != dwToWrite) {
          // The write operation timed out. I now need to
          // decide if I want to abort or retry. If I retry,
          // I need to send only the bytes that weren't sent.
          // If I want to abort, then I would just set fRes to
          // FALSE and return.
          fRes = FALSE;
      }
      else
          fRes = TRUE;
   }

   CloseHandle(osWrite.hEvent);
   return fRes;
}

The SetCommTimeouts function specifies the communications time-outs for a port. To retrieve the current time-outs for a port, a program calls the GetCommTimeouts function. An applications should retrieve the communications time-outs before modifying them. This allows the application to set time-outs back to their original settings when it finishes with the port. Following is an example of setting new time-outs using SetCommTimeouts:

COMMTIMEOUTS timeouts;

timeouts.ReadIntervalTimeout = 20;
timeouts.ReadTotalTimeoutMultiplier = 10;
timeouts.ReadTotalTimeoutConstant = 100;
timeouts.WriteTotalTimeoutMultiplier = 10;
timeouts.WriteTotalTimeoutConstant = 100;

if (!SetCommTimeouts(hComm, &timeouts))
   // Error setting time-outs.

Note Once again, communications time-outs are not the same as time-out values supplied in synchronization functions. WaitForSingleObject, for instance, uses a time-out value to wait for an object to become signaled; this is not the same as a communications time-out.

Setting the members of the COMMTIMEOUTS structure to all zeros causes no time-outs to occur. Nonoverlapped operations will block until all the requested bytes are transferred. The ReadFile function is blocked until all the requested characters arrive at the port. The WriteFile function is blocked until all requested characters are sent out. On the other hand, an overlapped operation will not finish until all the characters are transferred or the operation is aborted. The following conditions occur until the operation is completed:

  • WaitForSingleObject always returns WAIT_TIMEOUT if a synchronization time-out is supplied. WaitForSingleObject will block forever if an INFINITE synchronization time-out is used.

  • GetOverlappedResult always returns FALSE and GetLastError returns ERROR_IO_INCOMPLETE if called directly after the call to GetOverlappedResult.

Setting the members of the COMMTIMEOUTS structure in the following manner causes read operations to complete immediately without waiting for any new data to arrive:

COMMTIMEOUTS timeouts;

timeouts.ReadIntervalTimeout = MAXDWORD;
timeouts.ReadTotalTimeoutMultiplier = 0;
timeouts.ReadTotalTimeoutConstant = 0;
timeouts.WriteTotalTimeoutMultiplier = 0;
timeouts.WriteTotalTimeoutConstant = 0;

if (!SetCommTimeouts(hComm, &timeouts))
   // Error setting time-outs.

These settings are necessary when used with an event-based read described in the “Caveat?section earlier. In order for ReadFile to return 0 bytes read, the ReadIntervalTimeout member of the COMMTIMEOUTS structure is set to MAXDWORD, and the ReadTimeoutMultiplier and ReadTimeoutConstant are both set to zero.

An application must always specifically set communications time-outs when it uses a communications port. The behavior of read and write operations is affected by communications time-outs. When a port is initially open, it uses default time-outs supplied by the driver or time-outs left over from a previous communications application. If an application assumes that time-outs are set a certain way, while the time-outs are actually different, then read and write operations may never complete or may complete too often.

Conclusion

This article serves as a discussion of some of the common pitfalls and questions that arise when developing a serial communications application. The Multithreaded TTY sample that comes with this article is designed using many of the techniques discussed here. Download it and try it out. Learning how it works will provide a thorough understanding of the Win32 serial communications functions.

Bibliography

Brain, Marshall. Win32 System Services: The Heart of Windows NT. Englewood Cliffs, NJ: Prentice Hall, 1994.

Campbell, Joe. C Programmer’s Guide to Serial Communications. 2d ed. Indianapolis, IN: Howard W. Sams & Company, 1994.

Mirho, Charles, and Andy Terrice. “Create Communications Programs for Windows 95 with the Win32 Comm API.?Microsoft Systems Journal 12 (December 1994). (MSDN Library, Books and Periodicals)

Send feedback on this article. Find support options.

© 2000 Microsoft Corporation. All rights reserved. Terms of use.

LuoYunBin's Win32 ASM Page,http://asm.yeah.net


硬件参考资料

各种硬件接口规范、定义

  • PC 机外接输入输出接口引脚定义 （串口、并口、鼠标、键盘、显示、游戏接口等）
  • PC 机内部设备接口引脚定义 （硬盘、软驱、SCSI 接口等）
  • PC 机总线引脚定义
  • PC 机内存条引脚定义
  • PC 各种转换口，转接器
  • PC 各种连接电缆引脚定义
  • PC 电源线引脚定义
  • Mac 苹果机接口定义
PC 机外接输入输出接口引脚定义 （串口、并口、鼠标、键盘、显示、游戏接口等）
PC 机 RS-232 串行接口定义
PC 机 RS-422 串行接口定义
PC 机 9 针及 25 针串行接口引脚定义
PC 机并行接口定义
PC 机 ECP 并行接口定义
PC 机 PS/2 鼠标接口引脚定义
PC 机键盘接口引脚定义
PC 机 PS/2 键盘接口引脚定义
PC 机游戏杆接口引脚定义
PC 机游戏杆和 MIDI 接口引脚定义
PC 机单色显示 MDA 接口定义
PC 机 16 色显示 CGA 接口定义
PC 机 16/64 色显示 EGA 接口定义
PC 机 9 针 VGA 接口定义
PC 机 15 针 VGA 接口定义
PC 机 Vesa DDC VGA 接口定义
以太网卡 10/100 BaseT (RJ45) 接口定义
以太网卡 100 BaseT4 (RJ45) 接口定义
PC 机内部设备接口引脚定义 （硬盘、软驱、SCSI 接口等）
8" 软驱接口定义
3.5"/5" 软驱接口定义
IDE 接口定义
2.5" 便携式计算机用 44 针扩展 IDE (ATA) 接口定义
老式硬盘 ESDI 接口定义
Mitsumi CD-ROM (非 IDE)接口定义
Panasonic CD-ROM (非 IDE)接口定义
Sony CD-ROM (非 IDE)接口定义
内置式 SCSI卡(单端信号)接口定义
内置式 SCSI卡(差分信号)接口定义
外置式 SCSI 25针接口定义
外置式 SCSI (单端信号) 50 针接口定义
外置式 SCSI (差分信号) 50 针接口定义
外置式 SCSI (单端信号) 50 针高密度接口定义
外置式 SCSI (差分信号) 50 针高密度接口定义
扩展 IDE (ATA) 接口定义
PC 机总线引脚定义
ISA 总线定义
EISA 总线定义
VESA 总线定义
PCI 总线定义
AGP 总线定义
PCMCIA 接口卡引线定义
USB 接口引线定义
PC 机内存条引脚定义
30 线 SIMM 内存条定义
72 线 SIMM 内存条定义
72 线 ECC SIMM 内存条定义
72 线 SO DIMM 内存条定义
144 线 SO DIMM 内存条定义
168 线 DRAM DIMM 内存条定义
168 线 SDRAM DIMM 内存条定义
PC 各种转换口，转接器
9 针串行口到 25 针转接器
PS/2 键盘口到普通键盘转接器
普通键盘口到 PS/2 键盘转接器
串行口到 PS/2 鼠标转接器
PS/2 鼠标口到串行鼠标转接器
Mac 苹果机显示输出到 PC VGA 显示器转接器
串行直连电缆 (NullModem) 9 针-9 针
串行直连电缆 (NullModem) 9 针-25 针
串行直连电缆 (NullModem) 25 针-25 针
PC 各种连接电缆引脚定义
9 针串行口到 25 针 Modem 连接电缆
25 针串行口到 25 针 Modem 连接电缆
打印机连接电缆
9 针串行口到 25 针串行打印机连接电缆
25 针串行口到 25 针串行打印机连接电缆
InterLink/LapLink 连接电缆
以太网 10/100Base-T 连接电缆 (RJ45)
以太网 10/100Base-T 对绞连接电缆 (RJ45)
以太网 100Base-T4 对绞连接电缆 (RJ45)
PC 电源线引脚定义
PC 机 ATX 电源引脚定义
PC 机主板电源引脚定义
PC 机 5" 驱动器 (硬盘，5"软驱等) 电源引脚定义
PC 机软驱电源引脚定义
Mac 苹果机接口定义
Mac 早期苹果机 RS-232 串行接口定义
Mac 苹果机 RS-422 串行接口定义
Mac 苹果机键盘接口引脚定义
Mac 苹果机鼠标接口引脚定义
Mac 苹果机显示接口定义
(C) Copyright by LuoYunBin's Win32 ASM Page，http://asm.yeah.net/


ISA bus

ISA 总线定义

ISA 是 Industry Standard Architecture 的缩写

接口卡的外观

62+36 PIN EDGE CONNECTOR MALE

插槽的外观

62+36 PIN EDGE CONNECTOR FEMALE

引脚定义

引脚 | 定义 | 方向 | 说明
A1 | /I/O CH CK | <-- | I/O channel check; active low=parity error
A2 | D7 | <-> | Data bit 7
A3 | D6 | <-> | Data bit 6
A4 | D5 | <-> | Data bit 5
A5 | D4 | <-> | Data bit 4
A6 | D3 | <-> | Data bit 3
A7 | D2 | <-> | Data bit 2
A8 | D1 | <-> | Data bit 1
A9 | D0 | <-> | Data bit 0
A10 | I/O CH RDY | <-- | I/O Channel ready, pulled low to lengthen memory cycles
A11 | AEN | --> | Address enable; active high when DMA controls bus
A12 | A19 | --> | Address bit 19
A13 | A18 | --> | Address bit 18
A14 | A17 | --> | Address bit 17
A15 | A16 | --> | Address bit 16
A16 | A15 | --> | Address bit 15
A17 | A14 | --> | Address bit 14
A18 | A13 | --> | Address bit 13
A19 | A12 | --> | Address bit 12
A20 | A11 | --> | Address bit 11
A21 | A10 | --> | Address bit 10
A22 | A9 | --> | Address bit 9
A23 | A8 | --> | Address bit 8
A24 | A7 | --> | Address bit 7
A25 | A6 | --> | Address bit 6
A26 | A5 | --> | Address bit 5
A27 | A4 | --> | Address bit 4
A28 | A3 | --> | Address bit 3
A29 | A2 | --> | Address bit 2
A30 | A1 | --> | Address bit 1
A31 | A0 | --> | Address bit 0
B1 | GND |  | Ground
B2 | RESET | --> | Active high to reset or initialize system logic
B3 | +5V |  | +5 VDC
B4 | IRQ2 | <-- | Interrupt Request 2
B5 | -5VDC |  | -5 VDC
B6 | DRQ2 | <-- | DMA Request 2
B7 | -12VDC |  | -12 VDC
B8 | /NOWS | <-- | No WaitState
B9 | +12VDC |  | +12 VDC
B10 | GND |  | Ground
B11 | /SMEMW | --> | System Memory Write
B12 | /SMEMR | --> | System Memory Read
B13 | /IOW | --> | I/O Write
B14 | /IOR | --> | I/O Read
B15 | /DACK3 | --> | DMA Acknowledge 3
B16 | DRQ3 | <-- | DMA Request 3
B17 | /DACK1 | --> | DMA Acknowledge 1
B18 | DRQ1 | <-- | DMA Request 1
B19 | /REFRESH | <-> | Refresh
B20 | CLOCK | --> | System Clock (67 ns, 8-8.33 MHz, 50% duty cycle)
B21 | IRQ7 | <-- | Interrupt Request 7
B22 | IRQ6 | <-- | Interrupt Request 6
B23 | IRQ5 | <-- | Interrupt Request 5
B24 | IRQ4 | <-- | Interrupt Request 4
B25 | IRQ3 | <-- | Interrupt Request 3
B26 | /DACK2 | --> | DMA Acknowledge 2
B27 | T/C | --> | Terminal count; pulses high when DMA term. count reached
B28 | ALE | --> | Address Latch Enable
B29 | +5V |  | +5 VDC
B30 | OSC | --> | High-speed Clock (70 ns, 14.31818 MHz, 50% duty cycle)
B31 | GND |  | Ground
C1 | SBHE | <-> | System bus high enable (data available on SD8-15)
C2 | LA23 | <-> | Address bit 23
C3 | LA22 | <-> | Address bit 22
C4 | LA21 | <-> | Address bit 21
C5 | LA20 | <-> | Address bit 20
C6 | LA18 | <-> | Address bit 19
C7 | LA17 | <-> | Address bit 18
C8 | LA16 | <-> | Address bit 17
C9 | /MEMR | <-> | Memory Read (Active on all memory read cycles)
C10 | /MEMW | <-> | Memory Write (Active on all memory write cycles)
C11 | SD08 | <-> | Data bit 8
C12 | SD09 | <-> | Data bit 9
C13 | SD10 | <-> | Data bit 10
C14 | SD11 | <-> | Data bit 11
C15 | SD12 | <-> | Data bit 12
C16 | SD13 | <-> | Data bit 13
C17 | SD14 | <-> | Data bit 14
C18 | SD15 | <-> | Data bit 15
D1 | /MEMCS16 | <-- | Memory 16-bit chip select (1 wait, 16-bit memory cycle)
D2 | /IOCS16 | <-- | I/O 16-bit chip select (1 wait, 16-bit I/O cycle)
D3 | IRQ10 | <-- | Interrupt Request 10
D4 | IRQ11 | <-- | Interrupt Request 11
D5 | IRQ12 | <-- | Interrupt Request 12
D6 | IRQ15 | <-- | Interrupt Request 15
D7 | IRQ14 | <-- | Interrupt Request 14
D8 | /DACK0 | --> | DMA Acknowledge 0
D9 | DRQ0 | <-- | DMA Request 0
D10 | /DACK5 | --> | DMA Acknowledge 5
D11 | DRQ5 | <-- | DMA Request 5
D12 | /DACK6 | --> | DMA Acknowledge 6
D13 | DRQ6 | <-- | DMA Request 6
D14 | /DACK7 | --> | DMA Acknowledge 7
D15 | DRQ7 | <-- | DMA Request 7
D16 | +5 V |  |
D17 | /MASTER | <-- | Used with DRQ to gain control of system
D18 | GND |  | Ground


EISA bus

EISA总线定义

EISA 是 Extended Industry Standard Architecture 的缩写，由 Compaq, AST, Zenith, Tandy 等公司开发。

接口卡的外观
+---------------------------------------------+
|            元件面                           |
|                                             |
|___________ ISA-16bit __       ISA-8bit    __|
            |||||||||||  |||||||||||||||||||  A1 正面/B1 反面
             | | | | |    | | | | | | | | |  EISA: E1 正面/F1 反面
                   C1/D1
                  G1/H1
A,C,E,G=元件面
A,B,F,H=线路面
引脚定义

Pin | Name | Description
E1 | CMD# | Command Phase
E2 | START# | Start Phase
E3 | EXRDY | EISA Ready
E4 | EX32# | EISA Slave Size 32
E5 | GND | Ground
E6 | KEY | Access Key
E7 | EX16# | EISA Slave Size 16
E8 | SLBURST# | Slave Burst
E9 | MSBURST# | Master Burst
E10 | W/R# | Write/Read
E11 | GND | Ground
E12 | RES | Reserved
E13 | RES | Reserved
E14 | RES | Reserved
E15 | GND | Ground
E16 | KEY | Access Key
E17 | BE1# | Byte Enable 1
E18 | LA31# | Latchable Addressline 31
E19 | GND | Ground
E20 | LA30# | Latchable Addressline 30
E21 | LA28# | Latchable Addressline 28
E22 | LA27# | Latchable Addressline 27
E23 | LA25# | Latchable Addressline 25
E24 | GND | Ground
E25 | KEY | Access Key
E26 | LA15 | Latchable Addressline 15
E27 | LA13 | Latchable Addressline 13
E28 | LA12 | Latchable Addressline 12
E29 | LA11 | Latchable Addressline 11
E30 | GND | Ground
E31 | LA9 | Latchable Addressline 9
F1 | GND | Ground
F2 | +5V | +5 VDC
F3 | +5V | +5 VDC
F4 | --- |
F5 | --- |
F6 | KEY | Access Key
F7 | --- |
F8 | --- |
F9 | +12V | +12 VDC
F10 | M/IO# | Memory/Input-Output
F11 | LOCK# | Lock bus
F12 | RES | Reserved
F13 | GND | Ground
F14 | RES | Reserved
F15 | BE3# | Byte Enable 3
F16 | KEY | Access Key
F17 | BE2# | Byte Enable 2
F18 | BE0# | Byte Enable 0
F19 | GND | Ground
F20 | +5V | +5 VDC
F21 | LA29# | Latchable Addressline 29
F22 | GND | Ground
F23 | LA26# | Latchable Addressline 26
F24 | LA24# | Latchable Addressline 24
F25 | KEY | Access Key
F26 | LA16 | Latchable Addressline 16
F27 | LA14 | Latchable Addressline 14
F28 | +5V | +5 VDC
F29 | +5V | +5 VDC
F30 | GND | Ground
F31 | LA10 | Latchable Addressline 10
G1 | LA7 | Latchable Addressline 7
G2 | GND | Ground
G3 | LA4 | Latchable Addressline 4
G4 | LA3 | Latchable Addressline 3
G5 | GND | Ground
G6 | KEY | Access Key
G7 | D17 | Data 17
G8 | D19 | Data 19
G9 | D20 | Data 20
G10 | D22 | Data 22
G11 | GND | Ground
G12 | D25 | Data 25
G13 | D26 | Data 26
G14 | D28 | Data 28
G15 | KEY | Access Key
G16 | GND | Ground
G17 | D30 | Data 30
G18 | D31 | Data 31
G19 | MREQx | Master Request
H1 | LA8 | Latchable Addressline 8
H2 | LA6 | Latchable Addressline 6
H3 | LA5 | Latchable Addressline 5
H4 | +5V | +5 VDC
H5 | LA2 | Latchable Addressline 2
H6 | KEY | Access Key
H7 | D16 | Data 16
H8 | D18 | Data 18
H9 | GND | Ground
H10 | D21 | Data 21
H11 | D23 | Data 23
H12 | D24 | Data 24
H13 | GND | Ground
H14 | D27 | Data 27
H15 | KEY | Access Key
H16 | D29 | Data 29
H17 | +5V | +5 VDC
H18 | +5V | +5 VDC


VESA bus

VESA总线定义

[ 返回目录 ]

VESA 是 Video Electronics Standards Association 的缩写，本页列出的是扩展部分的引脚定义，非扩展部分请见 ISA 总线的定义。

接口卡的外观

58 PIN EDGE CONNECTOR MALE

插槽的外观

58 PIN EDGE CONNECTOR FEMALE

引脚定义

Pin | Name | Description
A1 | D1 | Data 1
A2 | D3 | Data 3
A3 | GND | Ground
A4 | D5 | Data 5
A5 | D7 | Data 7
A6 | D9 | Data 9
A7 | D11 | Data 11
A8 | D13 | Data 13
A9 | D15 | Data 15
A10 | GND | Ground
A11 | D17 | Data 17
A12 | Vcc | +5 VDC
A13 | D19 | Data 19
A14 | D21 | Data 21
A15 | D23 | Data 23
A16 | D25 | Data 25
A17 | GND | Ground
A18 | D27 | Data 27
A19 | D29 | Data 2
A20 | D31 | Data 31
A21 | A30 | Address 30
A22 | A28 | Address 28
A23 | A26 | Address 26
A24 | GND | Ground
A25 | A24 | Address 24
A26 | A22 | Address 22
A27 | VCC | +5 VDC
A28 | A20 | Address 20
A29 | A18 | Address 18
A30 | A16 | Address 16
A31 | A14 | Address 14
A32 | A12 | Address 12
A33 | A10 | Address 10
A34 | A8 | Address 8
A35 | GND | Ground
A36 | A6 | Address 6
A37 | A4 | Address 4
A38 | WBACK# | Write Back
A39 | BE0# | Byte Enable 0
A40 | VCC | +5 VDC
A41 | BE1# | Byte Enable 1
A42 | BE2# | Byte Enable 2
A43 | GND | Ground
A44 | BE3# | Byte Enable 3
A45 | ADS# | Address Strobe
A48 | LRDY# | Local Ready
A49 | LDEV | Local Device
A50 | LREQ | Local Request
A51 | GND | Ground
A52 | LGNT | Local Grant
A53 | VCC | +5 VDC
A54 | ID2 | Identification 2
A55 | ID3 | Identification 3
A56 | ID4 | Identification 4
A57 | LKEN# |
A58 | LEADS# | Local Enable Address Strobe
B1 | D0 | Data 0
B2 | D2 | Data 2
B3 | D4 | Data 4
B4 | D6 | Data 6
B5 | D8 | Data 8
B6 | GND | Ground
B7 | D10 | Data 10
B8 | D12 | Data 12
B9 | VCC | +5 VDC
B10 | D14 | Data 14
B11 | D16 | Data 16
B12 | D18 | Data 18
B13 | D20 | Data 20
B14 | GND | Ground
B15 | D22 | Data 22
B16 | D24 | Data 24
B17 | D26 | Data 26
B18 | D28 | Data 28
B19 | D30 | Data 30
B20 | VCC | +5 VDC
B21 | A31 | Address 31
B22 | GND | Ground
B23 | A29 | Address 29
B24 | A27 | Address 27
B25 | A25 | Address 25
B26 | A23 | Address 23
B27 | A21 | Address 21
B28 | A19 | Address 19
B29 | GND | Ground
B30 | A17 | Address 17
B31 | A15 | Address 15
B32 | VCC | +5 VDC
B33 | A13 | Address 13
B34 | A11 | Address 11
B35 | A9 | Address 9
B36 | A7 | Address 7
B37 | A5 | Address 5
B38 | GND | Ground
B39 | A3 | Address 3
B40 | A2 | Address 2
B41 | n/c | Not connected
B42 | RESET# | Reset
B43 | DC# | Data/Command
B44 | M/IO# | Memory/IO
B45 | W/R# | Write/Read
B48 | RDYRTN# | Ready Return
B49 | GND | Ground
B50 | IRQ9 | Interrupt 9
B51 | BRDY# | Burst Ready
B52 | BLAST# | Burst Last
B53 | ID0 | Identification 0
B54 | ID1 | Identification 1
B55 | GND | Ground
B56 | LCLK | Local Clock
B57 | VCC | +5 VDC
B58 | LBS16# | Local Bus Size 16


PCI bus

PCI总线定义

[ 返回目录 ]

PCI 是 Peripheral Component Interconnect 的缩写

接口卡的外观
PCI 标准 32位/64位 接口卡
 ----------------------------------------------------------------
|    PCI         元件侧 （B面）                                  |
|                                                                |
|                                                                |
|                                                                |
|    ____     32 位引脚部分                    64 位引脚部分  ___|
|___|    |||||||--|||||||||||||||||--|||||||--||||||||||||||
         ^     ^  ^               ^  ^     ^  ^            ^
       b01   b11  b14           b49  b52 b62  b63          b94
PCI 5V 32/64位卡
|                                                optional        |
|    ____     32 位引脚部分                    64 位引脚部分  ___|
|___|    ||||||||||||||||||||||||||--|||||||--||||||||||||||
PCI 3.3V 32/64位卡
|                                                optional        |
|    ____     32 位引脚部分                    64 位引脚部分  ___|
|___|    |||||||--||||||||||||||||||||||||||--||||||||||||||
引脚定义

Pin | +5V | +3.3V | Universal | Description
A1 | TRST |  |  | Test Logic Reset
A2 | +12V |  |  | +12 VDC
A3 | TMS |  |  | Test Mde Select
A4 | TDI |  |  | Test Data Input
A5 | +5V |  |  | +5 VDC
A6 | INTA |  |  | Interrupt A
A7 | INTC |  |  | Interrupt C
A8 | +5V |  |  | +5 VDC
A9 | RESV01 |  |  | Reserved VDC
A10 | +5V | +3.3V | Signal Rail | +V I/O (+5 V or +3.3 V)
A11 | RESV03 |  |  | Reserved VDC
A12 | GND03 | (OPEN) | (OPEN) | Ground or Open (Key)
A13 | GND05 | (OPEN) | (OPEN) | Ground or Open (Key)
A14 | RESV05 |  |  | Reserved VDC
A15 | RESET |  |  | Reset
A16 | +5V | +3.3V | Signal Rail | +V I/O (+5 V or +3.3 V)
A17 | GNT |  |  | Grant PCI use
A18 | GND08 |  |  | Ground
A19 | RESV06 |  |  | Reserved VDC
A20 | AD30 |  |  | Address/Data 30
A21 | +3.3V01 |  |  | +3.3 VDC
A22 | AD28 |  |  | Address/Data 28
A23 | AD26 |  |  | Address/Data 26
A24 | GND10 |  |  | Ground
A25 | AD24 |  |  | Address/Data 24
A26 | IDSEL |  |  | Initialization Device Select
A27 | +3.3V03 |  |  | +3.3 VDC
A28 | AD22 |  |  | Address/Data 22
A29 | AD20 |  |  | Address/Data 20
A30 | GND12 |  |  | Ground
A31 | AD18 |  |  | Address/Data 18
A32 | AD16 |  |  | Address/Data 16
A33 | +3.3V05 |  |  | +3.3 VDC
A34 | FRAME |  |  | Address or Data phase
A35 | GND14 |  |  | Ground
A36 | TRDY |  |  | Target Ready
A37 | GND15 |  |  | Ground
A38 | STOP |  |  | Stop Transfer Cycle
A39 | +3.3V07 |  |  | +3.3 VDC
A40 | SDONE |  |  | Snoop Done
A41 | SBO |  |  | Snoop Backoff
A42 | GND17 |  |  | Ground
A43 | PAR |  |  | Parity
A44 | AD15 |  |  | Address/Data 15
A45 | +3.3V10 |  |  | +3.3 VDC
A46 | AD13 |  |  | Address/Data 13
A47 | AD11 |  |  | Address/Data 11
A48 | GND19 |  |  | Ground
A49 | AD9 |  |  | Address/Data 9
A52 | C/BE0 |  |  | Command, Byte Enable 0
A53 | +3.3V11 |  |  | +3.3 VDC
A54 | AD6 |  |  | Address/Data 6
A55 | AD4 |  |  | Address/Data 4
A56 | GND21 |  |  | Ground
A57 | AD2 |  |  | Address/Data 2
A58 | AD0 |  |  | Address/Data 0
A59 | +5V | +3.3V | Signal Rail | +V I/O (+5 V or +3.3 V)
A60 | REQ64 |  |  | Request 64 bit ???
A61 | VCC11 |  |  | +5 VDC
A62 | VCC13 |  |  | +5 VDC
A63 | GND |  |  | Ground
A64 | C/BE[7]# |  |  | Command, Byte Enable 7
A65 | C/BE[5]# |  |  | Command, Byte Enable 5
A66 | +5V | +3.3V | Signal Rail | +V I/O (+5 V or +3.3 V)
A67 | PAR64 |  |  | Parity 64 ???
A68 | AD62 |  |  | Address/Data 62
A69 | GND |  |  | Ground
A70 | AD60 |  |  | Address/Data 60
A71 | AD58 |  |  | Address/Data 58
A72 | GND |  |  | Ground
A73 | AD56 |  |  | Address/Data 56
A74 | AD54 |  |  | Address/Data 54
A75 | +5V | +3.3V | Signal Rail | +V I/O (+5 V or +3.3 V)
A76 | AD52 |  |  | Address/Data 52
A77 | AD50 |  |  | Address/Data 50
A78 | GND |  |  | Ground
A79 | AD48 |  |  | Address/Data 48
A80 | AD46 |  |  | Address/Data 46
A81 | GND |  |  | Ground
A82 | AD44 |  |  | Address/Data 44
A83 | AD42 |  |  | Address/Data 42
A84 | +5V | +3.3V | Signal Rail | +V I/O (+5 V or +3.3 V)
A85 | AD40 |  |  | Address/Data 40
A86 | AD38 |  |  | Address/Data 38
A87 | GND |  |  | Ground
A88 | AD36 |  |  | Address/Data 36
A89 | AD34 |  |  | Address/Data 34
A90 | GND |  |  | Ground
A91 | AD32 |  |  | Address/Data 32
A92 | RES |  |  | Reserved
A93 | GND |  |  | Ground
A94 | RES |  |  | Reserved
B1 | -12V |  |  | -12 VDC
B2 | TCK |  |  | Test Clock
B3 | GND |  |  | Ground
B4 | TDO |  |  | Test Data Output
B5 | +5V |  |  | +5 VDC
B6 | +5V |  |  | +5 VDC
B7 | INTB |  |  | Interrupt B
B8 | INTD |  |  | Interrupt D
B9 | PRSNT1 |  |  | Reserved
B10 | RES |  |  | +V I/O (+5 V or +3.3 V)
B11 | PRSNT2 |  |  | ??
B12 | GND | (OPEN) | (OPEN) | Ground or Open (Key)
B13 | GND | (OPEN) | (OPEN) | Ground or Open (Key)
B14 | RES |  |  | Reserved VDC
B15 | GND |  |  | Reset
B16 | CLK |  |  | Clock
B17 | GND |  |  | Ground
B18 | REQ |  |  | Request
B19 | +5V | +3.3V | Signal Rail | +V I/O (+5 V or +3.3 V)
B20 | AD31 |  |  | Address/Data 31
B21 | AD29 |  |  | Address/Data 29
B22 | GND |  |  | Ground
B23 | AD27 |  |  | Address/Data 27
B24 | AD25 |  |  | Address/Data 25
B25 | +3.3V |  |  | +3.3VDC
B26 | C/BE3 |  |  | Command, Byte Enable 3
B27 | AD23 |  |  | Address/Data 23
B28 | GND |  |  | Ground
B29 | AD21 |  |  | Address/Data 21
B30 | AD19 |  |  | Address/Data 19
B31 | +3.3V |  |  | +3.3 VDC
B32 | AD17 |  |  | Address/Data 17
B33 | C/BE2 |  |  | Command, Byte Enable 2
B34 | GND13 |  |  | Ground
B35 | IRDY |  |  | Initiator Ready
B36 | +3.3V06 |  |  | +3.3 VDC
B37 | DEVSEL |  |  | Device Select
B38 | GND16 |  |  | Ground
B39 | LOCK |  |  | Lock bus
B40 | PERR |  |  | Parity Error
B41 | +3.3V08 |  |  | +3.3 VDC
B42 | SERR |  |  | System Error
B43 | +3.3V09 |  |  | +3.3 VDC
B44 | C/BE1 |  |  | Command, Byte Enable 1
B45 | AD14 |  |  | Address/Data 14
B46 | GND18 |  |  | Ground
B47 | AD12 |  |  | Address/Data 12
B48 | AD10 |  |  | Address/Data 10
B49 | GND20 |  |  | Ground
B50 | (OPEN) | GND | (OPEN) | Ground or Open (Key)
B51 | (OPEN) | GND | (OPEN) | Ground or Open (Key)
B52 | AD8 |  |  | Address/Data 8
B53 | AD7 |  |  | Address/Data 7
B54 | +3.3V12 |  |  | +3.3 VDC
B55 | AD5 |  |  | Address/Data 5
B56 | AD3 |  |  | Address/Data 3
B57 | GND22 |  |  | Ground
B58 | AD1 |  |  | Address/Data 1
B59 | VCC08 |  |  | +5 VDC
B60 | ACK64 |  |  | Acknowledge 64 bit ???
B61 | VCC10 |  |  | +5 VDC
B62 | VCC12 |  |  | +5 VDC
B63 | RES |  |  | Reserved
B64 | GND |  |  | Ground
B65 | C/BE[6]# |  |  | Command, Byte Enable 6
B66 | C/BE[4]# |  |  | Command, Byte Enable 4
B67 | GND |  |  | Ground
B68 | AD63 |  |  | Address/Data 63
B69 | AD61 |  |  | Address/Data 61
B70 | +5V | +3.3V | Signal Rail | +V I/O (+5 V or +3.3 V)
B71 | AD59 |  |  | Address/Data 59
B72 | AD57 |  |  | Address/Data 57
B73 | GND |  |  | Ground
B74 | AD55 |  |  | Address/Data 55
B75 | AD53 |  |  | Address/Data 53
B76 | GND |  |  | Ground
B77 | AD51 |  |  | Address/Data 51
B78 | AD49 |  |  | Address/Data 49
B79 | +5V | +3.3V | Signal Rail | +V I/O (+5 V or +3.3 V)
B80 | AD47 |  |  | Address/Data 47
B81 | AD45 |  |  | Address/Data 45
B82 | GND |  |  | Ground
B83 | AD43 |  |  | Address/Data 43
B84 | AD41 |  |  | Address/Data 41
B85 | GND |  |  | Ground
B86 | AD39 |  |  | Address/Data 39
B87 | AD37 |  |  | Address/Data 37
B88 | +5V | +3.3V | Signal Rail | +V I/O (+5 V or +3.3 V)
B89 | AD35 |  |  | Address/Data 35
B90 | AD33 |  |  | Address/Data 33
B91 | GND |  |  | Ground
B92 | RES |  |  | Reserved
B93 | RES |  |  | Reserved
B94 | GND |  |  | Ground


PCMCIA card define

PCMCIA引脚定义

PCMCIA 是 Personal Computer Memory Card International AssociationIndustry Standard Architecture 的缩写，是便携式计算机外扩卡的接口定义。

引脚定义

Pin | Name | Dir | Description
1 | GND | --- | Ground
2 | D3 | <-> | Data 3
3 | D4 | <-> | Data 4
4 | D5 | <-> | Data 5
5 | D6 | <-> | Data 6
6 | D7 | <-> | Data 7
7 | /CE1 | --> | Card Enable 1
8 | A10 | --> | Address 10
9 | /OE | --> | Output Enable
10 | A11 | --> | Address 11
11 | A9 | --> | Address 9
12 | A8 | --> | Address 8
13 | A13 | --> | Address 13
14 | A14 | --> | Address 14
15 | /WE:/P | --> | Write Enable : Program
16 | /READY:/IREQ | <-- | Ready : Busy (IREQ)
17 | VCC | --> | +5V
18 | VPP1 | --> | Programming Voltage (EPROM)
19 | A16 | --> | Address 16
20 | A15 | --> | Address 15
21 | A12 | --> | Address 12
22 | A7 | --> | Address 7
23 | A6 | --> | Address 6
24 | A5 | --> | Address 5
25 | A4 | --> | Address 4
26 | A3 | --> | Address 3
27 | A2 | --> | Address 2
28 | A1 | --> | Address 1
29 | A0 | --> | Address 0
30 | D0 | <-> | Data 0
31 | D1 | <-> | Data 1
32 | D2 | <-> | Data 2
33 | /WP:/IOIS16 | <-- | Write Protect : IOIS16
34 | GND | --- | Ground
35 | GND | --- | Ground
36 | /CD1 | <-- | Card Detect 1
37 | D11 | <-> | Data 11
38 | D12 | <-> | Data 12
39 | D13 | <-> | Data 13
40 | D14 | <-> | Data 14
41 | D15 | <-> | Data 15
42 | /CE2 | --> | Card Enable 2
43 | /VS1 | --> | Refresh
44 | /IORD | ? | I/O Read
45 | /IOWR | ? | I/O Write
46 | A17 | --> | Address 17
47 | A18 | --> | Address 18
48 | A19 | --> | Address 19
49 | A20 | --> | Address 20
50 | A21 | --> | Address 21
51 | VCC | --> | +5V
52 | VPP2 | --> | Programmeing Voltage 2 (EPROM)
53 | A22 | --> | Address 22
54 | A23 | --> | Address 23
55 | A24 | --> | Address 24
56 | A25 | --> | Address 25
57 | /VS2 | ? | RFU
58 | RESET | ? | RESET
59 | /WAIT | ? | WAIT
60 | /INPACK | ? |
61 | /REG | --> | Register Select
62 | /BVD2:SPKR | <-- | Battery Voltage Detect 2 : SPKR
63 | /BVD1:STSCHG | <-- | Battery Voltage Detect 1 : STSCHG
64 | D8 | <-> | Data 8
65 | D9 | <-> | Data 9
66 | D10 | <-> | Data 10
67 | /CD2 | <-- | Card Detect 2
68 | GND | --- | Ground

(C) Copyright by LuoYunBin's Win32 ASM


RS-232 define

RS-232串行接口定义

计算机侧为25针公插:

25 PIN D-SUB MALE

设备侧为25针母插:

25 PIN D-SUB FEMALE

引脚定义

Pin | Name | ITU-T | Dir | Description
1 | GND | 101 | --- | Shield Ground
2 | TXD | 103 | --> | Transmit Data
3 | RXD | 104 | <-- | Receive Data
4 | RTS | 105 | --> | Request to Send
5 | CTS | 106 | <-- | Clear to Send
6 | DSR | 107 | <-- | Data Set Ready
7 | GND | 102 | --- | System Ground
8 | CD | 109 | <-- | Carrier Detect
9 | - |  | - | RESERVED
10 | - |  | - | RESERVED
11 | STF | 126 | --> | Select Transmit Channel
12 | S.CD | ? | <-- | Secondary Carrier Detect
13 | S.CTS | ? | <-- | Secondary Clear to Send
14 | S.TXD | ? | --> | Secondary Transmit Data
15 | TCK | 114 | <-- | Transmission Signal Element Timing
16 | S.RXD | ? | <-- | Secondary Receive Data
17 | RCK | 115 | <-- | Receiver Signal Element Timing
18 | LL | 141 | --> | Local Loop Control
19 | S.RTS | ? | --> | Secondary Request to Send
20 | DTR | 108 | --> | Data Terminal Ready
21 | RL | 140 | --> | Remote Loop Control
22 | RI | 125 | <-- | Ring Indicator
23 | DSR | 111 | --> | Data Signal Rate Selector
24 | XCK | 113 | --> | Transmit Signal Element Timing
25 | TI | 142 | <-- | Test Indicator


PC serial port

PC 串行口定义

PC/AT 机上的串行口是 9 针公插座，引脚定义为：

9 PIN D-SUB MALE

Pin | Name | Dir | Description
1 | CD | <-- | Carrier Detect
2 | RXD | <-- | Receive Data
3 | TXD | --> | Transmit Data
4 | DTR | --> | Data Terminal Ready
5 | GND | --- | System Ground
6 | DSR | <-- | Data Set Ready
7 | RTS | --> | Request to Send
8 | CTS | <-- | Clear to Send
9 | RI | <-- | Ring Indicator

PC/XT 机上的串行口是 25 针公插座，引脚定义为：

25 PIN D-SUB MALE

Pin | Name | Dir | Description
1 | SHIELD | - | Shield Ground
2 | TXD | --> | Transmit Data
3 | RXD | <-- | Receive Data
4 | RTS | --> | Request to Send
5 | CTS | <-- | Clear to Send
6 | DSR | <-- | Data Set Ready
7 | GND | - | System Ground
8 | CD | <-- | Carrier Detect
9 | n/c | - |
10 | n/c | - |
11 | n/c | - |
12 | n/c | - |
13 | n/c | - |
14 | n/c | - |
15 | n/c | - |
16 | n/c | - |
17 | n/c | - |
18 | n/c | - |
19 | n/c | - |
20 | DTR | --> | Data Terminal Ready
21 | n/c | - |
22 | RI | <-- | Ring Indicator
23 | n/c | - |
24 | n/c | - |
25 | n/c | - |


RS-422 serial port

RS-422串行口定义

计算机侧 RS-422 串行接口的插口是 25 针公插：

37 PIN D-SUB MALE

设备侧 RS-422 串行接口插头是 25 针母插：

37 PIN D-SUB FEMALE

引脚定义

Pin | Name | Dir | Description
1 | GND | --- | Shield Ground
2 | SRI | <-- | Signal Rate Indicator
3 | n/c | - | Spare
4 | SD | --> | Send Data
5 | ST | --> | Send Timing
6 | RD | <-- | Receive Data
7 | RTS | --> | Request To Send
8 | RR | <-- | Receiver Ready
9 | CTS | <-- | Clear To Send
10 | LL | --> | Local Loopback
11 | DM | <-- | Data Modem
12 | TR | --> | Terminal Ready
13 | RR | <-- | Receiver Ready
14 | RL | --> | Remote Loopback
15 | IC | <-- | Incoming Call
16 | SF/SR | --> | Select Frequency/Select Rate
17 | TT | --> | Terminal Timing
18 | TM | <-- | Test Mode
19 | GND | --- | Ground
20 | RC | --- | Receive Twister-Pair Common
21 | GND | --- | Spare Twister-Pair Return
22 | /SD | --- | Send Data TPR
23 | GND | --- | Send Timing TPR
24 | GND | --- | Receive Timing TPR
25 | /RS | --- | Request To Send TPR
26 | /RT | --- | Receive Timing TPR
27 | /CS | --- | Clear To Send TPR
28 | IS | <-- | Terminal In Service
29 | /DM | --- | Data Mode TPR
30 | /TR | --- | Terminal Ready TPR
31 | /RR | --- | Receiver TPR
32 | SS | --> | Select Standby
33 | SQ | <-- | Signal Quality
34 | NS | --> | New Signal
35 | /TT | --- | Terminal Timing TPR
36 | SB | <-- | Standby Indicator
37 | SC | --- | Send Twister Pair Common


Serial of Mac

苹果机串行口定义

苹果机在型号 512KE 及以前的机上用 9 针串行口

接口的外观

计算机侧为 9 针母插：

9 PIN D-SUB FEMALE

设备侧为 9 针公插：

9 PIN D-SUB MALE

引脚定义

Pin | Name | Dir | Description
1 | GND | --- | Ground
2 | +5V | --> | +5 VDC. Don't use this one, it may be converted into output handshake in later equipment.
3 | GND | --- | Ground
4 | Tx+ | --> | Transmit Data, positive going component
5 | Tx- | --> | Transmit Data, negative going component
6 | +12V | --> | +12 VDC
7 | DSR/HSK | <-- | Handshake input. Signal name depends on mode: Used for Flow Control or Clock In.
8 | Rx+ | <-- | Receive Data, positive going component
9 | Rx- | <-- | Receive Data, negative going component


RS-422 serial of Mac

苹果机RS-422串行口定义

计算机侧为 8 针母插：

8 PIN MINI-DIN FEMALE

引脚定义

Pin | Name | Dir | Description
1 | HSKo | --> | Output Handshake
2 | HSKi/CLK | <-- | Input Handshake or External Clock
3 | TXD- | --> | Transmit Data (-)
4 | GND | --- | Ground
5 | RXD- | <-- | Receive Data (-)
6 | TXD+ | --> | Transmit Data (+)
7 | GPi | <-- | General Purpose Input
8 | RXD+ | <-- | Receive Data (+)


PC parallel

PC 并行接口定义

PC 并行接口外观是 25 针母插座：

25 PIN D-SUB FEMALE

引脚定义

Pin | Name | Dir | Description
1 | /STROBE | --> | Strobe
2 | D0 | --> | Data Bit 0
3 | D1 | --> | Data Bit 1
4 | D2 | --> | Data Bit 2
5 | D3 | --> | Data Bit 3
6 | D4 | --> | Data Bit 4
7 | D5 | --> | Data Bit 5
8 | D6 | --> | Data Bit 6
9 | D7 | --> | Data Bit 7
10 | /ACK | <-- | Acknowledge
11 | BUSY | <-- | Busy
12 | PE | <-- | Paper End
13 | SEL | <-- | Select
14 | /AUTOFD | --> | Autofeed
15 | /ERROR | <-- | Error
16 | /INIT | --> | Initialize
17 | /SELIN | --> | Select In
18 | GND | --- | Signal Ground
19 | GND | --- | Signal Ground
20 | GND | --- | Signal Ground
21 | GND | --- | Signal Ground
22 | GND | --- | Signal Ground
23 | GND | --- | Signal Ground
24 | GND | --- | Signal Ground
25 | GND | --- | Signal Ground


ECP parallel

ECP 并行口定义

ECP 是 Extended Capabilities Port 的缩写，外观同并行口，是 25 针母插座：

25 PIN D-SUB FEMALE

引脚定义

Pin | Name | Dir | Description
1 | nStrobe | --> | Strobe
2 | data0 | <-> | Address, Data or RLE Data Bit 0
3 | data1 | <-> | Address, Data or RLE Data Bit 1
4 | data2 | <-> | Address, Data or RLE Data Bit 2
5 | data3 | <-> | Address, Data or RLE Data Bit 3
6 | data4 | <-> | Address, Data or RLE Data Bit 4
7 | data5 | <-> | Address, Data or RLE Data Bit 5
8 | data6 | <-> | Address, Data or RLE Data Bit 6
9 | data7 | <-> | Address, Data or RLE Data Bit 7
10 | /nAck | <-- | Acknowledge
11 | Busy | <-- | Busy
12 | PError | <-- | Paper End
13 | Select | <-- | Select
14 | /nAutoFd | --> | Autofeed
15 | /nFault | <-- | Error
16 | /nInit | --> | Initialize
17 | /nSelectIn | --> | Select In
18 | GND | --- | Signal Ground
19 | GND | --- | Signal Ground
20 | GND | --- | Signal Ground
21 | GND | --- | Signal Ground
22 | GND | --- | Signal Ground
23 | GND | --- | Signal Ground
24 | GND | --- | Signal Ground
25 | GND | --- | Signal Ground


USB bus define

USB 总线定义

USB 是 Universal Serial Bus 的缩写，由 Compaq, Digital, IBM, Intel, Microsoft, NEC, Northern Telecom 联合推出。外观上计算机一侧为 4 针公插，设备一侧为 4 针母插。

引脚定义

Pin | Name | Description
1 | VCC | +5 VDC
2 | D- | Data -
3 | D+ | Data +
4 | GND | Ground


PS/2 mouse

PS/2 鼠标接口定义

PS/2 鼠标接口为 6 针母插，外观为：
 6 PIN MINI-DIN FEMALE

引脚定义

Pin | Name | Dir | Description
1 | DATA | <-> | Key Data
2 | n/c | - | Not connected
3 | GND | --- | Gnd
4 | VCC | --> | Power , +5 VDC
5 | CLK | --> | Clock
6 | n/c | - | Not connected


PS/2 keyboard

PS/2 键盘接口定义

PS/2 键盘接口为 6 针母插，外观为：
 6 PIN MINI-DIN FEMALE

引脚定义

Pin | Name | Dir | Description
1 | DATA | <-> | Key Data
2 | n/c | - | Not connected
3 | GND | --- | Gnd
4 | VCC | --> | Power , +5 VDC
5 | CLK | --> | Clock
6 | n/c | - | Not connected


Keyboard

键盘接口定义

键盘接口为 5 针母插座：

5 PIN DIN 180° (DIN41524) FEMALE

引脚定义

PC/AT 键盘接口的引脚定义：

Pin | Name | Description | Technical
1 | CLOCK | Clock | CLK/CTS, Open-collector
2 | DATA | Data | RxD/TxD/RTS, Open-collector
3 | n/c | Not connected | Reset on some very old keyboards.
4 | GND | Ground |
5 | VCC | +5 VDC

PC/XT 键盘接口的引脚定义稍有不同：

Pin | Name | Description | Technical
1 | CLOCK | Clock | CLK/CTS, Open-collector
2 | DATA | Data | RxD/TxD/RTS, Open-collector
3 | Reset | Reset |
4 | GND | Ground |
5 | VCC | +5 VDC

(C) Copyright by LuoYunBin's Win32 ASM Page，http://


Mac keyboard

苹果机键盘接口

苹果机键盘接口为 4 针母插座（RJ11）：

引脚定义

Pin | Name | Dir | Description
1 | CGND | --- | Chassis ground
2 | KBD1 | ? | Keyboard clock
3 | KBD2 | ? | Keyboard data
4 | +5V | --> | +5 VDC


Mac mouse

苹果机鼠标接口定义

苹果机鼠标接口外观为 9 针母插座：

9 PIN D-SUB FEMALE

鼠标线的插头外观为 9 针公插头：

9 PIN D-SUB MALE

引脚定义

Pin | Name | Dir | Description
1 | CGND | --- | Chassis ground
2 | +5V | --> | +5 VDC
3 | CGND | --- | Chassis ground
4 | X2 | <-- | Horizontal movement line (connected to VIA PB4 line)
5 | X1 | <-- | Horizontal movement line (connected to SCC DCDA-line)
6 | n/c | - | Not connected
7 | SW- | <-- | Mouse button line (connected to VIA PB3)
8 | Y2 | <-- | Vertical movement line (connected to VIA PB5 line)
9 | Y1 | <-- | Vertical movement line (connected to SCC DCDB-line)


PC Gameport

PC 游戏杆接口

PC 游戏杆接口为 15 针母插座，外观为：

15 PIN D-SUB FEMALE

PC 游戏杆为 15 针公插头，外观为：

15 PIN D-SUB MALE

引脚定义

Pin | Name | Dir | Description
1 | +5V | --> | +5 VDC
2 | /B1 | <-- | Button 1
3 | X1 | <-- | Joystick 1 - X
4 | GND | --- | Ground
5 | GND | --- | Ground
6 | Y1 | <-- | Joystick 1 - Y
7 | /B2 | <-- | Button 2
8 | +5V | --> | +5 VDC
9 | +5V | --> | +5 VDC
10 | /B4 | <-- | Button 4
11 | X2 | <-- | Joystick 2 - X
12 | GND | --- | Ground
13 | Y2 | <-- | Joystick 2 - Y
14 | /B3 | <-- | Button 3
15 | +5V | --> | +5 VDC


PC Gameport and MIDI

游戏杆+MIDI 接口定义

MIDI 接口和 PC 游戏杆接口是共用的，外观为 15 针母插座：

15 PIN D-SUB FEMALE

插头为 15 针公插头，外观为：

15 PIN D-SUB MALE

引脚定义

Pin | Name | Dir | Description
1 | +5V | --> | +5 VDC
2 | /B1 | <-- | Button 1
3 | X1 | <-- | Joystick 1 - X
4 | GND | --- | Ground
5 | GND | --- | Ground
6 | Y1 | <-- | Joystick 1 - Y
7 | /B2 | <-- | Button 2
8 | +5V | --> | +5 VDC
9 | +5V | --> | +5 VDC
10 | /B4 | <-- | Button 4
11 | X2 | <-- | Joystick 2 - X
12 | MIDITXD | --> | MIDI Transmit
13 | Y2 | <-- | Joystick 2 - Y
14 | /B3 | <-- | Button 3
15 | MIDIRXD | <-- | MIDI Receive


MDA adapter

单色 MDA 显示接口

MDA 即 Hercules（大力神）单色显示卡，显示卡端的接口为 9 针母插座：

9 PIN D-SUB FEMALE

显示器连线端的接口为 9 针公插头：
9 PIN D-SUB MALE

引脚定义

Pin | Name | Description
1 | GND | Ground
2 | GND | Ground
3 | n/c |
4 | n/c |
5 | n/c |
6 | I | Intensity
7 | M | Mono Video
8 | H | Horizontal Sync
9 | V | Vertical Sync


CGA adapter

CGA 显示接口

CGA 是 Color Graphics Adapter 的缩写，信号类型为 TTL，最多能显示 16 色，显示卡端的接口为 9 针母插座：

9 PIN D-SUB FEMALE

显示器连线端的接口为 9 针公插头：
9 PIN D-SUB MALE

引脚定义

Pin | Name | Description
1 | GND | Ground
2 | GND | Ground
3 | R | Red
4 | G | Green
5 | B | Blue
6 | I | Intensity
7 | RES | Reserved
8 | HSYNC | Horizontal Sync
9 | VSYNC | Vertical Sync


EGA adapter

EGA 显示接口

EGA 是 Enhanced Graphics Adapter 的缩写，信号类型为 TTL，颜色数为 16/64 色，显示卡端的接口为 9 针母插座：

9 PIN D-SUB FEMALE

显示器连线端的接口为 9 针公插头：
9 PIN D-SUB MALE

引脚定义

Pin | Name | Description
1 | GND | Ground
2 | SR | Secondary Red
3 | PR | Primary Red
4 | PG | Primary Green
5 | PB | Primary Blue
6 | SG/I | Secondary Green / Intensity
7 | SB | Secondary Blue
8 | H | Horizontal Sync
9 | V | Vertical Sync


VGA 9 pin adapter

9针 VGA 显示接口

VGA 是 Video Graphics Adapter(Array) 的缩写，信号类型为模拟类型，显示卡端的接口为 9 针母插座：

9 PIN D-SUB FEMALE

显示器连线端的接口为 9 针公插头：
9 PIN D-SUB MALE

引脚定义

Pin | Name | Dir | Description
1 | RED | --> | Red Video
2 | GREEN | --> | Green Video
3 | BLUE | --> | Blue Video
4 | HSYNC | --> | Horizontal Sync
5 | VSYNC | --> | Vertical Sync
6 | RGND | --- | Red Ground
7 | GGND | --- | Green Ground
8 | BGND | --- | Blue Ground
9 | SGND | --- | Sync Ground


VGA 15 pin adapter

15针 VGA 显示接口

VGA 是 Video Graphics Adapter(Array) 的缩写，信号类型为模拟类型，显示卡端的接口为 15 针母插座：

15 PIN HIGHDENSITY D-SUB FEMALE

显示器连线端的接口为 15 针公插头：
15 PIN HIGHDENSITY D-SUB MALE

引脚定义

Pin | Name | Dir | Description
1 | RED | --> | Red Video (75 ohm, 0.7 V p-p)
2 | GREEN | --> | Green Video (75 ohm, 0.7 V p-p)
3 | BLUE | --> | Blue Video (75 ohm, 0.7 V p-p)
4 | ID2 | <-- | Monitor ID Bit 2
5 | GND | --- | Ground
6 | RGND | --- | Red Ground
7 | GGND | --- | Green Ground
8 | BGND | --- | Blue Ground
9 | KEY | - | Key (No pin)
10 | SGND | --- | Sync Ground
11 | ID0 | <-- | Monitor ID Bit 0
12 | ID1 or SDA | <-- | Monitor ID Bit 1
13 | HSYNC or CSYNC | --> | Horizontal Sync (or Composite Sync)
14 | VSYNC | --> | Vertical Sync
15 | ID3 or SCL | <-- | Monitor ID Bit 3


VGA Vesa DDc adapter

VGA Vesa DDC显示接口

VGA 是 Video Graphics Adapter(Array) 的缩写，VESA 是 Video Electronics Standards Association 的缩写，DDC 是 Display Data Channel 的缩写，信号类型为模拟类型，显示卡端的接口为 15 针母插座：

15 PIN HIGHDENSITY D-SUB FEMALE

显示器连线端的接口为 15 针公插头：
15 PIN HIGHDENSITY D-SUB MALE

引脚定义

Pin | Name | Dir | Description
1 | RED | --> | Red Video (75 ohm, 0.7 V p-p)
2 | GREEN | --> | Green Video (75 ohm, 0.7 V p-p)
3 | BLUE | --> | Blue Video (75 ohm, 0.7 V p-p)
4 | RES | - | Reserved
5 | GND | --- | Ground
6 | RGND | --- | Red Ground
7 | GGND | --- | Green Ground
8 | BGND | --- | Blue Ground
9 | +5V | --> | +5 VDC
10 | SGND | --- | Sync Ground
11 | ID0 | <-- | Monitor ID Bit 0 (optional)
12 | SDA | <-> | DDC Serial Data Line
13 | HSYNC or CSYNC | --> | Horizontal Sync (or Composite Sync)
14 | VSYNC | --> | Vertical Sync
15 | SCL | <-> | DDC Data Clock Line


Mac video adapter

苹果机显示接口

苹果机显示接口为 15 针母插座：

15 PIN D-SUB FEMALE

引脚定义

Pin | Name | Dir | Description
1 | RGND | --- | Red Ground
2 | R | --> | Red
3 | CSYNC | --> | Composite sync
4 | SENSE0 | <-- | Monitor Sense 0
5 | G | --> | Green
6 | GGND | --- | Green Ground
7 | SENSE1 | <-- | Monitor Sense 1
8 | n/c | - | No connection
9 | B | --> | Blue
10 | SENSE2 | <-- | Monitor sense 2
11 | SGND | --- | Sync Ground
12 | VSYNC | --> | Vertical Sync
13 | BGND | --- | Blue Ground
14 | HSYNCGND | --- | Horizontal Sync Ground
15 | HSYNC | --> | Horizontal Sync


8" floppy disk

8 "软驱接口定义

8 " 软驱现在找都找不到，不知此档案有没有用，驱动器的接口引脚定义为：

Pin | Name | Dir | Description
2 | /REDWC | <-- | Reduced Write Current
4 | n/c | - | Reserved
6 | n/c | - | Reserved
8 | n/c | - | Reserved
10 | /FD2S | <-- | Disk is two sided
12 | /DCG | <-- | Disk has been changed/door open
14 | /SIDE | --> | Side select
16 | /DLOCK | --> | Door lock
18 | /HLD | --> | Head load
20 | /INDEX | <-- | Index Pulse
22 | /READY | <-- | Ready
24 | n/c | - | Not connected
26 | /SEL1 | <-- | Select Drive 1
28 | /SEL2 | <-- | Select Drive 2
30 | /SEL3 | <-- | Select Drive 3
32 | /SEL4 | <-- | Select Drive 4
34 | /DIR | --> | Direction
36 | /STEP | --> | Step
38 | /WDAT | --> | Write data
40 | /WGAT | --> | Write gate
42 | /TR00 | <-- | Track 00 (Zero)
44 | /WPROT | <-- | Write protect
46 | /RDATA | <-- | Read data
48 | n/c | - | Not connected
50 | n/c | - | Not connected


Floppy disk

软驱接口

3.5" 软驱和 5" 软驱连线，外观为：

34 PIN IDC MALE

引脚定义

Pin | Name | Dir | Description
2 | /REDWC | --> | Density Select
4 | n/c |  | Reserved
6 | n/c |  | Reserved
8 | /INDEX | <-- | Index
10 | /MOTEA | --> | Motor Enable A
12 | /DRVSB | --> | Drive Sel B
14 | /DRVSA | --> | Drive Sel A
16 | /MOTEB | --> | Motor Enable B
18 | /DIR | --> | Direction
20 | /STEP | --> | Step
22 | /WDATE | --> | Write Data
24 | /WGATE | --> | Floppy Write Enable
26 | /TRK00 | <-- | Track 0
28 | /WPT | <-- | Write Protect
30 | /RDATA | <-- | Read Data
32 | /SIDE1 | --> | Head Select
34 | /DSKCHG | --> | Disk Change


IDE adaptor

IDE 接口

IDE 是 Integrated Drive Electronics 的缩写，由 Compaq 和 Western Digital 公司开发，新版的 IDE 命名为 ATA 即 AT bus Attachment，IDE 接口在设备和主板侧的外观为 40 脚插针：

40 PIN IDC MALE

IDE 电缆外观为：
40 PIN IDC FEMALE

引脚定义

Pin | Name | Dir | Description
1 | /RESET | --> | Reset
2 | GND | --- | Ground
3 | DD7 | <-> | Data 7
4 | DD8 | <-> | Data 8
5 | DD6 | <-> | Data 6
6 | DD9 | <-> | Data 9
7 | DD5 | <-> | Data 5
8 | DD10 | <-> | Data 10
9 | DD4 | <-> | Data 4
10 | DD11 | <-> | Data 11
11 | DD3 | <-> | Data 3
12 | DD12 | <-> | Data 12
13 | DD2 | <-> | Data 2
14 | DD13 | <-> | Data 13
15 | DD1 | <-> | Data 1
16 | DD14 | <-> | Data 14
17 | DD0 | <-> | Data 0
18 | DD15 | <-> | Data 15
19 | GND | --- | Ground
20 | KEY | - | Key
21 | n/c | - | Not connected
22 | GND | --- | Ground
23 | /IOW | --> | Write Strobe
24 | GND | --- | Ground
25 | /IOR | --> | Read Strobe
26 | GND | --- | Ground
27 | IO_CH_RDY | <-- |
28 | ALE | --> | Address Latch Enable
29 | n/c | - | Not connected
30 | GND | --- | Ground
31 | IRQR | <-- | Interrupt Request
32 | /IOCS16 | ? | IO ChipSelect 16
33 | DA1 | --> | Address 1
34 | n/c | - | Not connected
35 | DA0 | --> | Address 0
36 | DA2 | --> | Address 2
37 | /IDE_CS0 | --> | (1F0-1F7)
38 | /IDE_CS1 | --> | (3F6-3F7)
39 | /ACTIVE | --> | Led driver
40 | GND | --- | Ground


ATA adaptor

ATA 接口

ATA 是 AT bus Attachment 的缩写，由 Conner, Seagate 和 Western Digital 公司开发，是新版的 IDE 接口，ATA 接口在设备和主板侧的外观为 40 脚插针：

40 PIN IDC MALE

ATA 电缆外观为：
40 PIN IDC FEMALE

引脚定义

Pin | Name | Dir | Description
1 | /RESET | --> | Reset
2 | GND | --- | Ground
3 | DD7 | <-> | Data 7
4 | DD8 | <-> | Data 8
5 | DD6 | <-> | Data 6
6 | DD9 | <-> | Data 9
7 | DD5 | <-> | Data 5
8 | DD10 | <-> | Data 10
9 | DD4 | <-> | Data 4
10 | DD11 | <-> | Data 11
11 | DD3 | <-> | Data 3
12 | DD12 | <-> | Data 12
13 | DD2 | <-> | Data 2
14 | DD13 | <-> | Data 13
15 | DD1 | <-> | Data 1
16 | DD14 | <-> | Data 14
17 | DD0 | <-> | Data 0
18 | DD15 | <-> | Data 15
19 | GND | --- | Ground
20 | KEY | - | Key (Pin missing)
21 | DMARQ | ? | DMA Request
22 | GND | --- | Ground
23 | /DIOW | --> | Write Strobe
24 | GND | --- | Ground
25 | /DIOR | --> | Read Strobe
26 | GND | --- | Ground
27 | IORDY | <-- | I/O Ready
28 | SPSYNC:CSEL | ? | Spindle Sync or Cable Select
29 | /DMACK | ? | DMA Acknowledge
30 | GND | --- | Ground
31 | INTRQ | <-- | Interrupt Request
32 | /IOCS16 | ? | IO ChipSelect 16
33 | DA1 | --> | Address 1
34 | PDIAG | ? | Passed Diagnostics
35 | DA0 | --> | Address 0
36 | DA2 | --> | Address 2
37 | /IDE_CS0 | --> | (1F0-1F7)
38 | /IDE_CS1 | --> | (3F6-3F7)
39 | /ACTIVE | --> | Led driver
40 | GND | --- | Ground


ATA 44 adaptor

ATA 44 接口

ATA 是 AT bus Attachment 的缩写，由 Conner, Seagate 和 Western Digital 公司开发，是新版的 IDE 接口，ATA 44 接口是 ATA 接口再加上电源等线，主要用于便携式计算机用 2.5" 硬盘，在设备和主板侧的外观为 44 脚插针：

44 PIN IDC (0.75") MALE

ATA 44 电缆外观为：
44 PIN IDC (0.75") FEMALE

引脚定义

ATA 44 接口的 1-40 脚的定义同 ATA，41-44 的定义为：

Pin | Name | Dir | Description
41 | +5VL | --> | +5 VDC (Logic)
42 | +5VM | --> | +5 VDC (Motor)
43 | GND | --- | Ground
44 | /TYPE | --> | Type (0=ATA)


ESDI adaptor

ESDI 接口

ESDI 是 Enhanced Small Device Interface 的缩写，早在 1980 年由 Maxtor 公司开发，是老式硬盘的接口，它的电缆由两部分组成，ESDI 接口在接口卡侧的外观为 34 脚和 20 脚公插针：

34 PIN IDC MALE
20 PIN IDC MALE

在硬盘侧为 34 脚的控制信号和 20 脚数据信号：

34 PIN IDC FEMALE
20 PIN IDC FEMALE

引脚定义

34 脚的控制信号：

Pin | Name | Description
2 |  | Head Sel 3
4 |  | Head Sel 2
6 |  | Write Gate
8 |  | Config/Stat Data
10 |  | Transfer Acknowledge
12 |  | Attention
14 |  | Head Sel 0
16 |  | Sect/Add MK Found
18 |  | Head Sel 1
20 |  | Index
22 |  | Ready
24 |  | Transfer Request
26 |  | Drive Sel 1
28 |  | Drive Sel 2
30 |  | Drive Sel 3
32 |  | Read Gate
34 |  | Command Data

20 脚的数据信号：

Pin | Name | Description
1 |  | Drive Selected
2 |  | Sect/Add MK Found
3 |  | Seek Complete
4 |  | Address Mark Enable
5 |  | (reserved, for step mode)
6 | GND | Ground
7 |  | Write Clock+
8 |  | Write Clock-
9 |  | Cartridge Changed
10 |  | Read Ref Clock+
11 |  | Read Ref Clock-
12 | GND | Ground
13 |  | NRZ Write Data+
14 |  | NRZ Write Data-
15 | GND | Ground
16 | GND | Ground
17 |  | NRZ Read Data+
18 |  | NRZ Read Data-
19 | GND | Ground
20 | GND | Ground


Mitsumi CD-ROM

Mitsumi CD-ROM 接口

Mitsumi CD-ROM 在光驱和接口卡侧的外观为 40 脚插针：

40 PIN IDC MALE

连接电缆外观为：
40 PIN IDC FEMALE

引脚定义

Pin | Name | Description
1 | A0 | Address Bit 0
2 | GND | Ground
3 | A1 | Address Bit 1
4 | GND | Ground
5 | n/c | Not connected
6 | GND | Ground
7 | n/c | Not connected
8 | GND | Ground
9 | n/c | Not connected
10 | GND | Ground
11 | n/c | Not connected
12 | GND | Ground
13 | INT | Interrupt
14 | GND | Ground
15 | REQ | Data request For DMA
16 | GND | Ground
17 | ACK | Data Acknowledge For DMA
18 | GND | Ground
19 | RE | Read Enable
20 | GND | Ground
21 | WE | Write Enable
22 | GND | Ground
23 | EN | Bus Enable
24 | GND | Ground
25 | DB0 | Data Bit 0
26 | GND | Ground
27 | DB1 | Data Bit 1
28 | GND | Ground
29 | DB2 | Data Bit 2
30 | GND | Ground
31 | DB3 | Data Bit 3
32 | GND | Ground
33 | DB4 | Data Bit 4
34 | GND | Ground
35 | DB5 | Data Bit 5
36 | GND | Ground
37 | DB6 | Data Bit 6
38 | GND | Ground
39 | DB7 | Data Bit 7
40 | GND | Ground


Panasonic CD-ROM

Panasonic CD-ROM 接口

Panasonic CD-ROM 在光驱和接口卡侧的外观为 40 脚插针：

40 PIN IDC MALE

连接电缆外观为：
40 PIN IDC FEMALE

引脚定义

Pin | Name | Description
1 | GND | Ground
2 | RESET | CD-Reset
3 | GND | Ground
4 | GND | Ground
5 | GND | Ground
6 | MODE0 | Operation Mode Bit 0
7 | GND | Ground
8 | MODE1 | Operation Mode Bit 1
9 | GND | Ground
10 | WRITE | CD-Write
11 | GND | Ground
12 | READ | CD-Read
13 | GND | Ground
14 | ST0 | CD-Status Bit 0
15 | GND | Ground
16 | n/c | No Connection
17 | GND | Ground
18 | n/c | No Connection
19 | GND | Ground
20 | ST1 | CD-Status Bit 1
21 | GND | Ground
22 | EN | CD-Data Enable
23 | GND | Ground
24 | ST2 | CD-Status Bit 2
25 | GND | Ground
26 | S/DE | CD-Status/Data Enable
27 | GND | Ground
28 | ST3 | CD-Status Bit 3
29 | GND | ground
30 | GND | ground
31 | D7 | CD-Data 7
32 | D6 | CD-Data 6
33 | GND | ground
34 | D5 | CD-Data 5
35 | D4 | CD-Data 4
36 | D3 | CD-Data 3
37 | GND | ground
38 | D2 | CD-Data 2
39 | D1 | CD-Data 1
40 | D0 | CD-Data 0


Sony CD-ROM

Sony CD-ROM 接口

Sony CD-ROM 在光驱和接口卡侧的外观为 34 脚插针：

34 PIN IDC MALE

连接电缆外观为：

34 PIN IDC FEMALE

引脚定义

Pin | Name | Description
1 | RESET | Reset
2 | GND | Ground
3 | DB7 | Data Bit 7
4 | GND | Ground
5 | DB6 | Data Bit 6
6 | GND | Ground
7 | DB5 | Data Bit 5
8 | GND | Ground
9 | DB4 | Data Bit 4
10 | GND | Ground
11 | DB3 | Data Bit 3
12 | GND | Ground
13 | DB2 | Data Bit 2
14 | GND | Ground
15 | DB1 | Data Bit 1
16 | GND | Ground
17 | DB0 | Data Bit 0
18 | GND | Ground
19 | WE | Write Enable
20 | GND | Ground
21 | RE | Read Enable
22 | GND | Ground
23 | ACK | Data Acknowledge For DMA
24 | GND | Ground
25 | REQ | Data Request For DMA
26 | GND | Ground
27 | INT | Interrupt
28 | GND | Ground
29 | A1 | Address Bit 1
30 | GND | Ground
31 | A0 | Address Bit 0
32 | GND | Ground
33 | EN | Bus Enable
34 | GND | Ground


SCSI internal (single)

内置SCSI卡接口(单端信号方式)

SCSI 是 Small Computer System Interface 的缩写，在控制器和设备侧的接口外观为 50 针公插座：

50 PIN IDC MALE

连接电缆为 50 针母插头：

50 PIN IDC FEMALE

引脚定义

Pin | Name | Dir | Description
2 | DB0 | <-> | Data Bus 0
4 | DB1 | <-> | Data Bus 1
6 | DB2 | <-> | Data Bus 2
8 | DB3 | <-> | Data Bus 3
10 | DB4 | <-> | Data Bus 4
12 | DB5 | <-> | Data Bus 5
14 | DB6 | <-> | Data Bus 6
16 | DB7 | <-> | Data Bus 7
18 | PARITY | <-> | Data Parity (odd Parity)
20 | GND | --- | Ground
22 | GND | --- | Ground
24 | GND | --- | Ground
26 | TMPWR | <-> | Termination Power
28 | GND | --- | Ground
30 | GND | --- | Ground
32 | /ATN | <-- | Attention
34 | GND | --- | Ground
36 | /BSY | <-> | Busy
38 | /ACK | <-- | Acknowledge
40 | /RST | <-> | Reset
42 | /MSG | --> | Message
44 | /SEL | <-> | Select
46 | /C/D | --> | Control/Data
48 | /REQ | --> | Request
50 | /I/O | --> | Input/Output


SCSI internal (diff)

内置SCSI卡接口(差分信号方式)

SCSI 是 Small Computer System Interface 的缩写，在控制器和设备侧的接口外观为 50 针公插座：

50 PIN IDC MALE

连接电缆为 50 针母插头：

50 PIN IDC FEMALE

引脚定义

Pin | Name | Dir | Description
01 | GND | --- | Ground
02 | GND | --- | Ground
03 | +DB0 | <-> | +Data Bus 0
04 | -DB0 | <-> | -Data Bus 0
05 | +DB1 | <-> | +Data Bus 1
06 | -DB1 | <-> | -Data Bus 1
07 | +DB2 | <-> | +Data Bus 2
08 | -DB2 | <-> | -Data Bus 2
09 | +DB3 | <-> | +Data Bus 3
10 | -DB3 | <-> | -Data Bus 3
11 | +DB4 | <-> | +Data Bus 4
12 | -DB4 | <-> | -Data Bus 4
13 | +DB5 | <-> | +Data Bus 5
14 | -DB5 | <-> | -Data Bus 5
15 | +DB6 | <-> | +Data Bus 6
16 | -DB6 | <-> | -Data Bus 6
17 | +DB7 | <-> | +Data Bus 7
18 | -DB7 | <-> | -Data Bus Parity7
19 | +DBP | <-> | +Data Bus Parity (odd Parity)
20 | -DBP | <-> | -Data Bus Parity (odd Parity)
21 | DIFFSENS | ? | ???
22 | GND | --- | Ground
23 | res | - | Reserved
24 | res | - | Reserved
25 | TERMPWR | <-> | Termination Power
26 | TERMPWR | <-> | Termination Power
27 | res | - | Reserved
28 | res | - | Reserved
29 | +ATN | <-- | +Attention
30 | -ATN | <-- | -Attention
31 | GND | --- | Ground
32 | GND | --- | Ground
33 | +BSY | <-> | +Bus is busy
34 | -BSY | <-> | -Bus is busy
35 | +ACK | <-- | +Acknowledge
36 | -ACK | <-- | -Acknowledge
37 | +RST | <-> | +Reset
38 | -RST | <-> | -Reset
39 | +MSG | --> | +Message
40 | -MSG | --> | -Message
41 | +SEL | <-> | +Select
42 | -SEL | <-> | -Select
43 | +C/D | --> | +Control or Data
44 | -C/D | --> | -Control or Data
45 | +REQ | --> | +Request
46 | -REQ | --> | -Request
47 | +I/O | --> | +In/Out
48 | -I/O | --> | -In/Out
49 | GND | --- | Ground
50 | GND | --- | Ground


25 Pin external SCSI

25针外置SCSI接口

SCSI 是 Small Computer System Interface 的缩写，外置式 25 针接口为扫描仪等低速设备使用，接口外观为母插座：

25 PIN D-SUB FEMALE

连接电缆插头为 25 针公插头：
 25 PIN D-SUB MALE

引脚定义

Pin | Name | Dir | Description
1 | GND | --- | Ground
2 | DB1 | <-> | Data Bus 1
3 | DB3 | <-> | Data Bus 3
4 | DB5 | <-> | Data Bus 5
5 | DB7 | <-> | Data Bus 7
6 | GND | --- | Ground
7 | /SEL | <-> | Select
8 | GND | --- | Ground
9 | TMPWR | <-> | Termination Power
10 | /RST | <-> | Reset
11 | C/D | --> | Control/Data
12 | I/O | --> | Input/Output
13 | GND | --- | Ground
14 | DB0 | <-> | Data Bus 0
15 | DB2 | <-> | Data Bus 2
16 | DB4 | <-> | Data Bus 4
17 | DB6 | <-> | Data Bus 6
18 | PARITY | <-> | Data Parity
19 | GND | --- | Ground
20 | /ATN | <-- | Attention
21 | /MSG | --> | Message
22 | /ACK | <-- | Acknowledge
23 | BSY | <-> | Busy
24 | /REQ | --> | Request
25 | GND | --- | Ground


SCSI external (single)

外置SCSI接口(单端信号方式)

SCSI 是 Small Computer System Interface 的缩写，外置式 50 针接口的外观为母插座：

50 PIN CENTRONICS FEMALE

连接电缆插头为 50 针公插头：
 50 PIN CENTRONICS MALE

引脚定义

Pin | Name | Dir | Description
1-25 | GND | --- | Ground
26 | DB0 | <-> | Data Bus 0
27 | DB1 | <-> | Data Bus 1
28 | DB2 | <-> | Data Bus 2
29 | DB3 | <-> | Data Bus 3
30 | DB4 | <-> | Data Bus 4
31 | DB5 | <-> | Data Bus 5
32 | DB6 | <-> | Data Bus 6
33 | DB7 | <-> | Data Bus 7
34 | PARITY | <-> | Data Parity (odd Parity)
35 | GND | --- | Ground
36 | GND | --- | Ground
37 | GND | --- | Ground
38 | TMPWR | <-> | Termination Power
39 | GND | --- | Ground
40 | GND | --- | Ground
41 | /ATN | <-- | Attention
42 | n/c | - | Not connected
43 | /BSY | <-> | Busy
44 | /ACK | <-- | Acknowledge
45 | /RST | <-> | Reset
46 | /MSG | --> | Message
47 | /SEL | <-> | Select
48 | /C/D | --> | Control/Data
49 | /REQ | --> | Request
50 | /I/O | --> | Input/Output


SCSI external (diff)

外置SCSI接口(差分信号方式)

SCSI 是 Small Computer System Interface 的缩写，外置式 50 针接口的外观为母插座：

50 PIN CENTRONICS FEMALE

连接电缆插头为 50 针公插头：
 50 PIN CENTRONICS MALE

引脚定义

Pin | Name | Dir | Description
01 | GND | --- | Ground
02 | +DB0 | <-> | +Data Bus 0
03 | +DB1 | <-> | +Data Bus 1
04 | +DB2 | <-> | +Data Bus 2
05 | +DB3 | <-> | +Data Bus 3
06 | +DB4 | <-> | +Data Bus 4
07 | +DB5 | <-> | +Data Bus 5
08 | +DB6 | <-> | +Data Bus 6
09 | +DB7 | <-> | +Data Bus 7
10 | +DBP | <-> | +Data Bus Parity (odd Parity)
11 | DIFFSENS | ? | ???
12 | res | - | Reserved
13 | TERMPWR | <-> | Termination Power
14 | res | - | Reserved
15 | +ATN | <-- | +Attention
16 | GND | --- | Ground
17 | +BSY | <-> | +Bus is busy
18 | +ACK | <-- | +Acknowledge
19 | +RST | <-> | +Reset
20 | +MSG | --> | +Message
21 | +SEL | <-> | +Select
22 | +C/D | --> | +Control or Data
23 | +REQ | --> | +Request
24 | +I/O | --> | +In/Out
25 | GND | --- | Ground
26 | GND | --- | Ground
27 | -DB0 | <-> | -Data Bus 0
28 | -DB1 | <-> | -Data Bus 1
29 | -DB2 | <-> | -Data Bus 2
30 | -DB3 | <-> | -Data Bus 3
31 | -DB4 | <-> | -Data Bus 4
32 | -DB5 | <-> | -Data Bus 5
33 | -DB6 | <-> | -Data Bus 6
34 | -DB7 | <-> | -Data Bus Parity7
35 | -DBP | <-> | -Data Bus Parity (odd Parity)
36 | GND | --- | Ground
37 | res | - | Reserved
38 | TERMPWR | <-> | Termination Power
39 | res | - | Reserved
40 | -ATN | <-- | -Attention
41 | GND | --- | Ground
42 | -BSY | <-> | -Bus is busy
43 | -ACK | <-- | -Acknowledge
44 | -RST | <-> | -Reset
45 | -MSG | --> | -Message
46 | -SEL | <-> | -Select
47 | -C/D | --> | -Control or Data
48 | -REQ | --> | -Request
49 | -I/O | --> | -In/Out
50 | GND | --- | Ground


SCSI external double (single)

外置SCSI高密接口(单端信号方式)

SCSI 是 Small Computer System Interface 的缩写，外置式高密 50 针接口的外观为母插座：

50 PIN HI-DENSITY D-SUB FEMALE

连接电缆插头为 50 针公插头：
 50 PIN HI-DENSITY D-SUB MALE

引脚定义

Pin | Name | Dir | Description
1-25 | GND | --- | Ground
26 | DB0 | <-> | Data Bus 0
27 | DB1 | <-> | Data Bus 1
28 | DB2 | <-> | Data Bus 2
29 | DB3 | <-> | Data Bus 3
30 | DB4 | <-> | Data Bus 4
31 | DB5 | <-> | Data Bus 5
32 | DB6 | <-> | Data Bus 6
33 | DB7 | <-> | Data Bus 7
34 | PARITY | <-> | Data Parity (odd Parity)
35 | GND | --- | Ground
36 | GND | --- | Ground
37 | GND | --- | Ground
38 | TMPWR | <-> | Termination Power
39 | GND | --- | Ground
40 | GND | --- | Ground
41 | /ATN | <-- | Attention
42 | n/c | - | Not connected
43 | /BSY | <-> | Busy
44 | /ACK | <-- | Acknowledge
45 | /RST | <-> | Reset
46 | /MSG | --> | Message
47 | /SEL | <-> | Select
48 | /C/D | --> | Control/Data
49 | /REQ | --> | Request
50 | /I/O | --> | Input/Output


SCSI external double (diff)

外置SCSI高密接口(差分信号方式)

SCSI 是 Small Computer System Interface 的缩写，外置式高密 50 针接口的外观为母插座：

50 PIN CENTRONICS FEMALE

连接电缆插头为 50 针公插头：
 50 PIN CENTRONICS MALE

引脚定义

Pin | Name | Dir | Description
01 | GND | --- | Ground
02 | +DB0 | <-> | +Data Bus 0
03 | +DB1 | <-> | +Data Bus 1
04 | +DB2 | <-> | +Data Bus 2
05 | +DB3 | <-> | +Data Bus 3
06 | +DB4 | <-> | +Data Bus 4
07 | +DB5 | <-> | +Data Bus 5
08 | +DB6 | <-> | +Data Bus 6
09 | +DB7 | <-> | +Data Bus 7
10 | +DBP | <-> | +Data Bus Parity (odd Parity)
11 | DIFFSENS | ? | ???
12 | res | - | Reserved
13 | TERMPWR | <-> | Termination Power
14 | res | - | Reserved
15 | +ATN | <-- | +Attention
16 | GND | --- | Ground
17 | +BSY | <-> | +Bus is busy
18 | +ACK | <-- | +Acknowledge
19 | +RST | <-> | +Reset
20 | +MSG | --> | +Message
21 | +SEL | <-> | +Select
22 | +C/D | --> | +Control or Data
23 | +REQ | --> | +Request
24 | +I/O | --> | +In/Out
25 | GND | --- | Ground
26 | GND | --- | Ground
27 | -DB0 | <-> | -Data Bus 0
28 | -DB1 | <-> | -Data Bus 1
29 | -DB2 | <-> | -Data Bus 2
30 | -DB3 | <-> | -Data Bus 3
31 | -DB4 | <-> | -Data Bus 4
32 | -DB5 | <-> | -Data Bus 5
33 | -DB6 | <-> | -Data Bus 6
34 | -DB7 | <-> | -Data Bus Parity7
35 | -DBP | <-> | -Data Bus Parity (odd Parity)
36 | GND | --- | Ground
37 | res | - | Reserved
38 | TERMPWR | <-> | Termination Power
39 | res | - | Reserved
40 | -ATN | <-- | -Attention
41 | GND | --- | Ground
42 | -BSY | <-> | -Bus is busy
43 | -ACK | <-- | -Acknowledge
44 | -RST | <-> | -Reset
45 | -MSG | --> | -Message
46 | -SEL | <-> | -Select
47 | -C/D | --> | -Control or Data
48 | -REQ | --> | -Request
49 | -I/O | --> | -In/Out
50 | GND | --- | Ground


Simm 30pin memory

SIMM 30线内存条

SIMM 是 Single Inline Memory Module (单列直插) 的缩写，最高容量为 4MBx9，在无校验内存条中，QP 和 DP 引脚是空的，外观为：

30 PIN SIMM

引脚定义

Pin | Name | Description
1 | VCC | +5 VDC
2 | /CAS | Column Address Strobe
3 | DQ0 | Data 0
4 | A0 | Address 0
5 | A1 | Address 1
6 | DQ1 | Data 1
7 | A2 | Address 2
8 | A3 | Address 3
9 | GND | Ground
10 | DQ2 | Data 2
11 | A4 | Address 4
12 | A5 | Address 5
13 | DQ3 | Data 3
14 | A6 | Address 6
15 | A7 | Address 7
16 | DQ4 | Data 4
17 | A8 | Address 8
18 | A9 | Address 9
19 | A10 | Address 10
20 | DQ5 | Data 5
21 | /WE | Write Enable
22 | GND | Ground
23 | DQ6 | Data 6
24 | A11 | Address 11
25 | DQ7 | Data 7
26 | QP | Data Parity Out
27 | /RAS | Row Address Strobe
28 | /CASP | Something Parity ????
29 | DP | Data Parity In
30 | VCC | +5 VDC


Simm 72pin memory

SIMM 72线内存条

SIMM 是 Single Inline Memory Module (单列直插) 的缩写，72 线内存条的外观为：

72 PIN SIMM

引脚定义

Pin | 无校验 | 有校验 | Description
1 | VSS | VSS | Ground
2 | DQ0 | DQ0 | Data 0
3 | DQ16 | DQ16 | Data 16
4 | DQ1 | DQ1 | Data 1
5 | DQ17 | DQ17 | Data 17
6 | DQ2 | DQ2 | Data 2
7 | DQ18 | DQ18 | Data 18
8 | DQ3 | DQ3 | Data 3
9 | DQ19 | DQ19 | Data 19
10 | VCC | VCC | +5 VDC
11 | n/c | n/c | Not connected
12 | A0 | A0 | Address 0
13 | A1 | A1 | Address 1
14 | A2 | A2 | Address 2
15 | A3 | A3 | Address 3
16 | A4 | A4 | Address 4
17 | A5 | A5 | Address 5
18 | A6 | A6 | Address 6
19 | A10 | A10 | Address 10
20 | DQ4 | DQ4 | Data 4
21 | DQ20 | DQ20 | Data 20
22 | DQ5 | DQ5 | Data 5
23 | DQ21 | DQ21 | Data 21
24 | DQ6 | DQ6 | Data 6
25 | DQ22 | DQ22 | Data 22
26 | DQ7 | DQ7 | Data 7
27 | DQ23 | DQ23 | Data 23
28 | A7 | A7 | Address 7
29 | A11 | A11 | Address 11
30 | VCC | VCC | +5 VDC
31 | A8 | A8 | Address 8
32 | A9 | A9 | Address 9
33 | /RAS3 | /RAS3 | Row Address Strobe 3
34 | /RAS2 | /RAS2 | Row Address Strobe 2
35 | n/c | PQ3 | Parity bit 3 (for the 3rd byte, bits 16-23)
36 | n/c | PQ1 | Parity bit 1 (for the 1st byte, bits 0-7)
37 | n/c | PQ2 | Parity bit 2 (for the 2nd byte, bits 8-15)
38 | n/c | PQ4 | Parity bit 4 (for the 4th byte, bits 24-31)
39 | VSS | VSS | Ground
40 | /CAS0 | /CAS0 | Column Address Strobe 0
41 | /CAS2 | /CAS2 | Column Address Strobe 2
42 | /CAS3 | /CAS3 | Column Address Strobe 3
43 | /CAS1 | /CAS1 | Column Address Strobe 1
44 | /RAS0 | /RAS0 | Row Address Strobe 0
45 | /RAS1 | /RAS1 | Row Address Strobe 1
46 | n/c | n/c | Not connected
47 | /WE | /WE | Read/Write
48 | n/c | n/c | Not connected
49 | DQ8 | DQ8 | Data 8
50 | DQ24 | DQ24 | Data 24
51 | DQ9 | DQ9 | Data 9
52 | DQ25 | DQ25 | Data 25
53 | DQ10 | DQ10 | Data 10
54 | DQ26 | DQ26 | Data 26
55 | DQ11 | DQ11 | Data 11
56 | DQ27 | DQ27 | Data 27
57 | DQ12 | DQ12 | Data 12
58 | DQ28 | DQ28 | Data 28
59 | VCC | VCC | +5 VDC
60 | DQ29 | DQ29 | Data 29
61 | DQ13 | DQ13 | Data 13
62 | DQ30 | DQ30 | Data 30
63 | DQ14 | DQ14 | Data 14
64 | DQ31 | DQ31 | Data 31
65 | DQ16 | DQ16 | Data 16
66 | n/c | n/c | Not connected
67 | PD1 | PD1 | Presence Detect 1
68 | PD2 | PD2 | Presence Detect 2
69 | PD3 | PD3 | Presence Detect 3
70 | PD4 | PD4 | Presence Detect 4
71 | n/c | n/c | Not connected
72 | VSS | VSS | Ground

容量定义：

PD2 | PD1 | 容量
GND | GND | 4 or 64 MB
GND | NC | 2 or 32 MB
NC | GND | 1 or 16 MB
NC | NC | 8 MB

存取速度：

PD4 | PD3 | 速度
GND | GND | 50, 100 ns
GND | NC | 80 ns
NC | GND | 70 ns
NC | NC | 60 ns


Simm ECC 72pin memory

SIMM ECC 72线内存条

SIMM 是 Single Inline Memory Module (单列直插) 的缩写，ECC 是 Error Correcting Code (纠错码) 的缩写，外观为：

72 PIN SIMM

引脚定义

Pin | ECC | Optimized | Description
1 | VSS | VSS | Ground
2 | DQ0 | DQ0 | Data 0
3 | DQ1 | DQ1 | Data 1
4 | DQ2 | DQ2 | Data 2
5 | DQ3 | DQ3 | Data 3
6 | DQ4 | DQ4 | Data 4
7 | DQ5 | DQ5 | Data 5
8 | DQ6 | DQ6 | Data 6
9 | DQ7 | DQ7 | Data 7
10 | VCC | VCC | +5 VDC
11 | PD5 | PD5 | Presence Detect 5
12 | A0 | A0 | Address 0
13 | A1 | A1 | Address 1
14 | A2 | A2 | Address 2
15 | A3 | A3 | Address 3
16 | A4 | A4 | Address 4
17 | A5 | A5 | Address 5
18 | A6 | A6 | Address 6
19 | n/c | n/c | Not connected
20 | DQ8 | DQ8 | Data 8
21 | DQ9 | DQ9 | Data 9
22 | DQ10 | DQ10 | Data 10
23 | DQ11 | DQ11 | Data 11
24 | DQ12 | DQ12 | Data 12
25 | DQ13 | DQ13 | Data 13
26 | DQ14 | DQ14 | Data 14
27 | DQ15 | DQ15 | Data 15
28 | A7 | A7 | Address 7
29 | DQ16 | DQ16 | Data 16
30 | VCC | VCC | +5 VDC
31 | A8 | A8 | Address 8
32 | A9 | A9 | Address 9
33 | n/c | n/c | Not connected
34 | /RAS1 | /RAS1 | Row Address Strobe 1
35 | DQ17 | DQ17 | Data 17
36 | DQ18 | DQ18 | Data 18
37 | DQ19 | DQ19 | Data 19
38 | DQ20 | DQ20 | Data 20
39 | VSS | VSS | Ground
40 | /CAS0 | /CAS0 | Column Address Strobe 0
41 | A10 | A10 | Address 10
42 | A11 | A11 | Address 11
43 | /CAS1 | /CAS1 | Column Address Strobe 1
44 | /RAS0 | /RAS0 | Row Address Strobe 0
45 | /RAS1 | /RAS1 | Row Address Strobe 1
46 | DQ21 | DQ21 | Data 21
47 | /WE | /WE | Read/Write
48 | /ECC | /ECC |
49 | DQ22 | DQ22 | Data 22
50 | DQ23 | DQ23 | Data 23
51 | DQ24 | DQ24 | Data 24
52 | DQ25 | DQ25 | Data 25
53 | DQ26 | DQ26 | Data 26
54 | DQ27 | DQ27 | Data 27
55 | DQ28 | DQ28 | Data 28
56 | DQ29 | DQ29 | Data 29
57 | DQ30 | DQ30 | Data 30
58 | DQ31 | DQ31 | Data 31
59 | VCC | VCC | +5 VDC
60 | DQ32 | DQ32 | Data 32
61 | DQ33 | DQ33 | Data 33
62 | DQ34 | DQ34 | Data 34
63 | DQ35 | DQ35 | Data 35
64 | n/c | DQ36 | Data 36
65 | n/c | DQ37 | Data 37
66 | n/c | DQ38 | Data 38
67 | PD1 | PD1 | Presence Detect 1
68 | PD2 | PD2 | Presence Detect 2
69 | PD3 | PD3 | Presence Detect 3
70 | PD4 | PD4 | Presence Detect 4
71 | n/c | DQ39 | Data 39
72 | VSS | VSS | Ground


SO Dimm 72pin memory

SO DIMM 72线内存条

DIMM 是 Dual Inline Memory Module (双列直插)的缩写，SO 是 Small Outline 的缩写

引脚定义

Pin | Non-Parity | Parity | Description
1 | VSS | VSS | Ground
2 | DQ0 | DQ0 | Data 0
3 | DQ1 | DQ1 | Data 1
4 | DQ2 | DQ2 | Data 2
5 | DQ3 | DQ3 | Data 3
6 | DQ4 | DQ4 | Data 4
7 | DQ5 | DQ5 | Data 5
8 | DQ6 | DQ6 | Data 6
9 | DQ7 | DQ7 | Data 7
10 | VCC | VCC | +5 VDC
11 | PD1 | PD1 | Presence Detect 1
12 | A0 | A0 | Address 0
13 | A1 | A1 | Address 1
14 | A2 | A2 | Address 2
15 | A3 | A3 | Address 3
16 | A4 | A4 | Address 4
17 | A5 | A5 | Address 5
18 | A6 | A6 | Address 6
19 | A10 | A10 | Address 10
20 | n/c | PQ8 | Data 8 (Parity 1)
21 | DQ9 | DQ9 | Data 9
22 | DQ10 | DQ10 | Data 10
23 | DQ11 | DQ11 | Data 11
24 | DQ12 | DQ12 | Data 12
25 | DQ13 | DQ13 | Data 13
26 | DQ14 | DQ14 | Data 14
27 | DQ15 | DQ15 | Data 15
28 | A7 | A7 | Address 7
29 | A11 | A11 | Address 11
30 | VCC | VCC | +5 VDC
31 | A8 | A8 | Address 8
32 | A9 | A9 | Address 9
33 | /RAS3 | RAS3 | Row Address Strobe 3
34 | /RAS2 | RAS2 | Row Address Strobe 2
35 | DQ16 | DQ16 | Data 16
36 | n/c | PQ17 | Data 17 (Parity 2)
37 | DQ18 | DQ18 | Data 18
38 | DQ19 | DQ19 | Data 19
39 | VSS | VSS | Ground
40 | /CAS0 | CAS0 | Column Address Strobe 0
41 | /CAS2 | CAS2 | Column Address Strobe 2
42 | /CAS3 | CAS3 | Column Address Strobe 3
43 | /CAS1 | CAS1 | Column Address Strobe 1
44 | /RAS0 | RAS0 | Row Address Strobe 0
45 | /RAS1 | RAS1 | Row Address Strobe 1
46 | A12 | A12 | Address 12
47 | /WE | WE | Read/Write
48 | A13 | A13 | Address 13
49 | DQ20 | DQ20 | Data 20
50 | DQ21 | DQ21 | Data 21
51 | DQ22 | DQ22 | Data 22
52 | DQ23 | DQ23 | Data 23
53 | DQ24 | DQ24 | Data 24
54 | DQ25 | DQ25 | Data 25
55 | n/c | PQ26 | Data 26 (Parity 3)
56 | DQ27 | DQ27 | Data 27
57 | DQ28 | DQ28 | Data 28
58 | DQ29 | DQ29 | Data 29
59 | DQ31 | DQ31 | Data 31
60 | DQ30 | DQ30 | Data 30
61 | VCC | VCC | +5 VDC
62 | DQ32 | DQ32 | Data 32
63 | DQ33 | DQ33 | Data 33
64 | DQ34 | DQ34 | Data 34
65 | n/c | PQ35 | Data 35 (Parity 4)
66 | PD2 | PD2 | Presence Detect 2
67 | PD3 | PD3 | Presence Detect 3
68 | PD4 | PD4 | Presence Detect 4
69 | PD5 | PD5 | Presence Detect 1
70 | PD6 | PD6 | Presence Detect 6
71 | PD7 | PD7 | Presence Detect 7
72 | VSS | VSS | Ground


SO Dimm 144pin memory

SO DIMM 144线内存条

DIMM 是 Dual Inline Memory Module (双列直插)的缩写，SO 是 Small Outline 的缩写

引脚定义

Pin | Normal | ECC | Description
1 | VSS | VSS | Ground
2 | VSS | VSS | Ground
3 | DQ0 | DQ0 | Data 0
4 | DQ32 | DQ32 | Data 32
5 | DQ1 | DQ1 | Data 1
6 | DQ33 | DQ33 | Data 33
7 | DQ2 | DQ2 | Data 2
8 | DQ34 | DQ34 | Data 34
9 | DQ3 | DQ3 | Data 3
10 | DQ35 | DQ35 | Data 35
11 | VCC | VCC | +5 VDC
12 | VCC | VCC | +5 VDC
13 | DQ4 | DQ4 | Data 4
14 | DQ36 | DQ36 | Data 36
15 | DQ5 | DQ5 | Data 5
16 | DQ37 | DQ37 | Data 37
17 | DQ6 | DQ6 | Data 6
18 | DQ38 | DQ38 | Data 38
19 | DQ7 | DQ7 | Data 7
20 | DQ39 | DQ39 | Data 39
21 | VSS | VSS | Ground
22 | VSS | VSS | Ground
23 | /CAS0 | /CAS0 | Column Address Strobe 0
24 | /CAS4 | /CAS4 | Column Address Strobe 4
25 | /CAS1 | /CAS1 | Column Address Strobe 1
26 | /CAS5 | /CAS5 | Column Address Strobe 5
27 | VCC | VCC | +5 VDC
28 | VCC | VCC | +5 VDC
29 | A0 | A0 | Address 0
30 | A3 | A3 | Address 3
31 | A1 | A1 | Address 1
32 | A4 | A4 | Address 4
33 | A2 | A2 | Address 2
34 | A5 | A5 | Address 5
35 | VSS | VSS | Ground
36 | VSS | VSS | Ground
37 | DQ8 | DQ8 | Data 8
38 | DQ40 | DQ40 | Data 40
39 | DQ9 | DQ9 | Data 9
40 | DQ41 | DQ41 | Data 41
41 | DQ10 | DQ10 | Data 10
42 | DQ42 | DQ42 | Data 42
43 | DQ11 | DQ11 | Data 11
44 | DQ43 | DQ43 | Data 43
45 | VCC | VCC | +5 VDC
46 | VCC | VCC | +5 VDC
47 | DQ12 | DQ12 | Data 12
48 | DQ44 | DQ44 | Data 44
49 | DQ13 | DQ13 | Data 13
50 | DQ45 | DQ45 | Data 45
51 | DQ14 | DQ14 | Data 14
52 | DQ46 | DQ46 | Data 46
53 | DQ15 | DQ15 | Data 15
54 | DQ47 | DQ47 | Data 47
55 | VSS | VSS | Ground
56 | VSS | VSS | Ground
57 | n/c | CB0 |
58 | n/c | CB4 |
59 | n/c | CB1 |
60 | n/c | CB5 |
61 | DU | DU | Don't use
62 | DU | DU | Don't use
63 | VCC | VCC | +5 VDC
64 | VCC | VCC | +5 VDC
65 | DU | DU | Don't use
66 | DU | DU | Don't use
67 | /WE | /WE | Read/Write
68 | n/c | n/c | Not connected
69 | /RAS0 | /RAS0 | Row Address Strobe 0
70 | n/c | n/c | Not connected
71 | /RAS1 | /RAS1 | Row Address Strobe 1
72 | n/c | n/c | Not connected
73 | /OE | /OE |
74 | n/c | n/c | Not connected
75 | VSS | VSS | Ground
76 | VSS | VSS | Ground
77 | n/c | CB2 |
78 | n/c | CB6 |
79 | n/c | CB3 |
80 | n/c | CB7 |
81 | VCC | VCC | +5 VDC
82 | VCC | VCC | +5 VDC
83 | DQ16 | DQ16 | Data 16
84 | DQ48 | DQ48 | Data 48
85 | DQ17 | DQ17 | Data 17
86 | DQ49 | DQ49 | Data 49
87 | DQ18 | DQ18 | Data 18
88 | DQ50 | DQ50 | Data 50
89 | DQ19 | DQ19 | Data 19
90 | DQ51 | DQ51 | Data 51
91 | VSS | VSS | Ground
92 | VSS | VSS | Ground
93 | DQ20 | DQ20 | Data 20
94 | DQ52 | DQ52 | Data 52
95 | DQ21 | DQ21 | Data 21
96 | DQ53 | DQ53 | Data 53
97 | DQ22 | DQ22 | Data 22
98 | DQ54 | DQ54 | Data 54
99 | DQ23 | DQ23 | Data 23
100 | DQ55 | DQ55 | Data 55
101 | VCC | VCC | +5 VDC
102 | VCC | VCC | +5 VDC
103 | A6 | A6 | Address 6
104 | A7 | A7 | Address 7
105 | A8 | A8 | Address 8
106 | A11 | A11 | Address 11
107 | VSS | VSS | Ground
108 | VSS | VSS | Ground
109 | A9 | A9 | Address 9
110 | A12 | A12 | Address 12
111 | A10 | A10 | Address 10
112 | A13 | A13 | Address 13
113 | VCC | VCC | +5 VDC
114 | VCC | VCC | +5 VDC
115 | /CAS2 | /CAS2 | Column Address Strobe 2
116 | /CAS6 | /CAS6 | Column Address Strobe 6
117 | /CAS3 | /CAS3 | Column Address Strobe 3
118 | /CAS7 | /CAS7 | Column Address Strobe 7
119 | VSS | VSS | Ground
120 | /VSS | /VSS | Ground
121 | DQ24 | DQ24 | Data 24
122 | DQ56 | DQ56 | Data 56
123 | DQ25 | DQ25 | Data 25
124 | DQ57 | DQ57 | Data 57
125 | DQ26 | DQ26 | Data 26
126 | DQ58 | DQ58 | Data 58
127 | DQ27 | DQ27 | Data 27
128 | DQ59 | DQ59 | Data 59
129 | VCC | VCC | +5 VDC
130 | VCC | VCC | +5 VDC
131 | DQ28 | DQ28 | Data 28
132 | DQ60 | DQ60 | Data 60
133 | DQ29 | DQ29 | Data 29
134 | DQ61 | DQ61 | Data 61
135 | DQ30 | DQ30 | Data 30
136 | DQ62 | DQ62 | Data 62
137 | DQ31 | DQ31 | Data 31
138 | DQ63 | DQ63 | Data 63
139 | VSS | VSS | Ground
140 | VSS | VSS | Ground
141 | SDA | SDA |
142 | SCL | SCL |
143 | VCC | VCC | +5 VDC
144 | VCC | VCC | +5 VDC


Dimm Dram 168pin memory

DIMM DRAM 168线内存条

DIMM 是 Dual Inline Memory Module (双列直插)的缩写

引脚定义

正面，左方：

Pin | Non-Parity? | Parity? | 72 ECC? | 80 ECC? | Description
1 | VSS | VSS | VSS | VSS | Ground
2 | DQ0 | DQ0 | DQ0 | DQ0 | Data 0
3 | DQ1 | DQ1 | DQ1 | DQ1 | Data 1
4 | DQ2 | DQ2 | DQ2 | DQ2 | Data 2
5 | DQ3 | DQ3 | DQ3 | DQ3 | Data 3
6 | VCC | VCC | VCC | VCC | +5 VDC or +3.3 VDC
7 | DQ4 | DQ4 | DQ4 | DQ4 | Data 4
8 | DQ5 | DQ5 | DQ5 | DQ5 | Data 5
9 | DQ6 | DQ6 | DQ6 | DQ6 | Data 6
10 | DQ7 | DQ7 | DQ7 | DQ7 | Data 7
11 | DQ8 | DQ8 | DQ8 | DQ8 | Data 8
12 | VSS | VSS | VSS | VSS | Ground
13 | DQ9 | DQ9 | DQ9 | DQ9 | Data 9
14 | DQ10 | DQ10 | DQ10 | DQ10 | Data 10
15 | DQ11 | DQ11 | DQ11 | DQ11 | Data 11
16 | DQ12 | DQ12 | DQ12 | DQ12 | Data 12
17 | DQ13 | DQ13 | DQ13 | DQ13 | Data 13
18 | VCC | VCC | VCC | VCC | +5 VDC or +3.3 VDC
19 | DQ14 | DQ14 | DQ14 | DQ14 | Data 14
20 | DQ15 | DQ15 | DQ15 | DQ15 | Data 15
21 | n/c | CB0 | CB0 | CB0 | Parity/Check Bit Input/Output 0
22 | n/c | CB1 | CB1 | CB1 | Parity/Check Bit Input/Output 1
23 | VSS | VSS | VSS | VSS | Ground
24 | n/c | n/c | n/c | CB8 | Parity/Check Bit Input/Output 8
25 | n/c | n/c | n/c | CB9 | Parity/Check Bit Input/Output 9
26 | VCC | VCC | VCC | VCC | +5 VDC or +3.3 VDC
27 | /WE0 | /WE0 | /WE0 | /WE0 | Read/Write Input
28 | /CAS0 | /CAS0 | /CAS0 | /CAS0 | Column Address Strobe 0
29 | /CAS1 | /CAS1 | /CAS1 | /CAS1 | Column Address Strobe 1
30 | /RAS0 | /RAS0 | /RAS0 | /RAS0 | Row Address Strobe 0
31 | /OE0 | /OE0 | /OE0 | /OE0 | Output Enable
32 | VSS | VSS | VSS | VSS | Ground
33 | A0 | A0 | A0 | A0 | Address 0
34 | A2 | A2 | A2 | A2 | Address 2
35 | A4 | A4 | A4 | A4 | Address 4
36 | A6 | A6 | A6 | A6 | Address 6
37 | A8 | A8 | A8 | A8 | Address 8
38 | A10 | A10 | A10 | A10 | Address 10
39 | A12 | A12 | A12 | A12 | Address 12
40 | VCC | VCC | VCC | VCC | +5 VDC or +3.3 VDC
41 | VCC | VCC | VCC | VCC | +5 VDC or +3.3 VDC
42 | DU | DU | DU | DU | Don't Use

正面，右方：

Pin | Non-Parity? | Parity? | 72 ECC? | 80 ECC? | Description
43 | VSS | VSS | VSS | VSS | Ground
44 | /OE2 | /OE2 | /OE2 | /OE2 |
45 | /RAS2 | /RAS2 | /RAS2 | /RAS2 | Row Address Strobe 2
46 | /CAS2 | /CAS2 | /CAS2 | /CAS2 | Column Address Strobe 2
47 | /CAS3 | /CAS3 | /CAS3 | /CAS3 | Column Address Strobe 3
48 | /WE2 | /WE2 | /WE2 | /WE2 | Read/Write Input
49 | VCC | VCC | VCC | VCC | +5 VDC or +3.3 VDC
50 | n/c | n/c | n/c | CB10 | Parity/Check Bit Input/Output 10
51 | n/c | n/c | n/c | CB11 | Parity/Check Bit Input/Output 11
52 | n/c | CB2 | CB2 | CB2 | Parity/Check Bit Input/Output 2
53 | n/c | CB3 | CB3 | CB3 | Parity/Check Bit Input/Output 3
54 | VSS | VSS | VSS | VSS | Ground
55 | DQ16 | DQ16 | DQ16 | DQ16 | Data 16
56 | DQ17 | DQ17 | DQ17 | DQ17 | Data 17
57 | DQ18 | DQ18 | DQ18 | DQ18 | Data 18
58 | DQ19 | DQ19 | DQ19 | DQ19 | Data 19
59 | VCC | VCC | VCC | VCC | +5 VDC or +3.3 VDC
60 | DQ20 | DQ20 | DQ20 | DQ20 | Data 20
61 | n/c | n/c | n/c | n/c | Not connected
62 | DU | DU | DU | DU | Don't Use
63 | n/c | n/c | n/c | n/c | Not connected
64 | VSS | VSS | VSS | VSS | Ground
65 | DQ21 | DQ21 | DQ21 | DQ21 | Data 21
66 | DQ22 | DQ22 | DQ22 | DQ22 | Data 22
67 | DQ23 | DQ23 | DQ23 | DQ23 | Data 23
68 | VSS | VSS | VSS | VSS | Ground
69 | DQ24 | DQ24 | DQ24 | DQ24 | Data 24
70 | DQ25 | DQ25 | DQ25 | DQ25 | Data 25
71 | DQ26 | DQ26 | DQ26 | DQ26 | Data 26
72 | DQ27 | DQ27 | DQ27 | DQ27 | Data 27
73 | VCC | VCC | VCC | VCC | +5 VDC or +3.3 VDC
74 | DQ28 | DQ28 | DQ28 | DQ28 | Data 28
75 | DQ29 | DQ29 | DQ29 | DQ29 | Data 29
76 | DQ30 | DQ30 | DQ30 | DQ30 | Data 30
77 | DQ31 | DQ31 | DQ31 | DQ31 | Data 31
78 | VSS | VSS | VSS | VSS | Ground
79 | n/c | n/c | n/c | n/c | Not connected
80 | n/c | n/c | n/c | n/c | Not connected
81 | n/c | n/c | n/c | n/c | Not connected
82 | SDA | SDA | SDA | SDA | Serial Data
83 | SCL | SCL | SCL | SCL | Serial Clock
84 | VCC | VCC | VCC | VCC | +5 VDC or +3.3 VDC

反面，左方：

Pin | Non-Parity? | Parity? | 72 ECC? | 80 ECC? | Description
85 | VSS | VSS | VSS | VSS | Ground
86 | DQ32 | DQ32 | DQ32 | DQ32 | Data 32
87 | DQ33 | DQ33 | DQ33 | DQ33 | Data 33
88 | DQ34 | DQ34 | DQ34 | DQ34 | Data 34
89 | DQ35 | DQ35 | DQ35 | DQ35 | Data 35
90 | VCC | VCC | VCC | VCC | +5 VDC or +3.3 VDC
91 | DQ36 | DQ36 | DQ36 | DQ36 | Data 36
92 | DQ37 | DQ37 | DQ37 | DQ37 | Data 37
93 | DQ38 | DQ38 | DQ38 | DQ38 | Data 38
94 | DQ39 | DQ39 | DQ39 | DQ39 | Data 39
95 | DQ40 | DQ40 | DQ40 | DQ40 | Data 40
96 | VSS | VSS | VSS | VSS | Ground
97 | DQ41 | DQ41 | DQ41 | DQ41 | Data 41
98 | DQ42 | DQ42 | DQ42 | DQ42 | Data 42
99 | DQ43 | DQ43 | DQ43 | DQ43 | Data 43
100 | DQ44 | DQ44 | DQ44 | DQ44 | Data 44
101 | DQ45 | DQ45 | DQ45 | DQ45 | Data 45
102 | VCC | VCC | VCC | VCC | +5 VDC or +3.3 VDC
103 | DQ46 | DQ46 | DQ46 | DQ46 | Data 46
104 | DQ47 | DQ47 | DQ47 | DQ47 | Data 47
105 | n/c | CB4 | CB4 | CB4 | Parity/Check Bit Input/Output 4
106 | n/c | CB5 | CB5 | CB5 | Parity/Check Bit Input/Output 5
107 | VSS | VSS | VSS | VSS | Ground
108 | n/c | n/c | n/c | CB12 | Parity/Check Bit Input/Output 12
109 | n/c | n/c | n/c | CB13 | Parity/Check Bit Input/Output 13
110 | VCC | VCC | VCC | VCC | +5 VDC or +3.3 VDC
111 | DU | DU | DU | DU | Don't Use
112 | /CAS4 | /CAS4 | /CAS4 | /CAS4 | Column Address Strobe 4
113 | /CAS5 | /CAS5 | /CAS5 | /CAS5 | Column Address Strobe 5
114 | /RAS1 | /RAS1 | /RAS1 | /RAS1 | Row Address Strobe 1
115 | DU | DU | DU | DU | Don't Use
116 | VSS | VSS | VSS | VSS | Ground
117 | A1 | A1 | A1 | A1 | Address 1
118 | A3 | A3 | A3 | A3 | Address 3
119 | A5 | A5 | A5 | A5 | Address 5
120 | A7 | A7 | A7 | A7 | Address 7
121 | A9 | A9 | A9 | A9 | Address 9
122 | A11 | A11 | A11 | A11 | Address 11
123 | A13 | A13 | A13 | A13 | Address 13
124 | VCC | VCC | VCC | VCC | +5 VDC or +3.3 VDC
125 | DU | DU | DU | DU | Don't Use
126 | DU | DU | DU | DU | Don't Use

反面，右方：

Pin | Non-Parity? | Parity? | 72 ECC? | 80 ECC? | Description
127 | VSS | VSS | VSS | VSS | Ground
128 | DU | DU | DU | DU | Don't Use
129 | /RAS3 | /RAS3 | /RAS3 | /RAS3 | Column Address Strobe 3
130 | /CAS6 | /CAS6 | /CAS6 | /CAS6 | Column Address Strobe 6
131 | /CAS7 | /CAS7 | /CAS7 | /CAS7 | Column Address Strobe 7
132 | DU | DU | DU | DU | Don't Use
133 | VCC | VCC | VCC | VCC | +5 VDC or +3.3 VDC
134 | n/c | n/c | n/c | CB14 | Parity/Check Bit Input/Output 14
135 | n/c | n/c | n/c | CB15 | Parity/Check Bit Input/Output 15
136 | n/c | CB6 | CB6 | CB6 | Parity/Check Bit Input/Output 6
137 | n/c | CB7 | CB7 | CB7 | Parity/Check Bit Input/Output 7
138 | VSS | VSS | VSS | VSS | Ground
139 | DQ48 | DQ48 | DQ48 | DQ48 | Data 48
140 | DQ49 | DQ49 | DQ49 | DQ49 | Data 49
141 | DQ50 | DQ50 | DQ50 | DQ50 | Data 50
142 | DQ51 | DQ51 | DQ51 | DQ51 | Data 51
143 | VCC | VCC | VCC | VCC | +5 VDC or +3.3 VDC
144 | DQ52 | DQ52 | DQ52 | DQ52 | Data 52
145 | n/c | n/c | n/c | n/c | Not connected
146 | DU | DU | DU | DU | Don't Use
147 | n/c | n/c | n/c | n/c | Not connected
148 | VSS | VSS | VSS | VSS | Ground
149 | DQ53 | DQ53 | DQ53 | DQ53 | Data 53
150 | DQ54 | DQ54 | DQ54 | DQ54 | Data 54
151 | DQ55 | DQ55 | DQ55 | DQ55 | Data 55
152 | VSS | VSS | VSS | VSS | Ground
153 | DQ56 | DQ56 | DQ56 | DQ56 | Data 56
154 | DQ57 | DQ57 | DQ57 | DQ57 | Data 57
155 | DQ58 | DQ58 | DQ58 | DQ58 | Data 58
156 | DQ59 | DQ59 | DQ59 | DQ59 | Data 59
157 | VCC | VCC | VCC | VCC | +5 VDC or +3.3 VDC
158 | DQ60 | DQ60 | DQ60 | DQ60 | Data 60
159 | DQ61 | DQ61 | DQ61 | DQ61 | Data 61
160 | DQ62 | DQ62 | DQ62 | DQ62 | Data 62
161 | DQ63 | DQ63 | DQ63 | DQ63 | Data 63
162 | VSS | VSS | VSS | VSS | Ground
163 | CK3 | CK3 | CK3 | CK3 |
164 | n/c | n/c | n/c | n/c | Not connected
165 | SA0 | SA0 | SA0 | SA0 | Serial Address 0
166 | SA1 | SA1 | SA1 | SA1 | Serial Address 1
167 | SA2 | SA2 | SA2 | SA2 | Serial Address 2
168 | VCC | VCC | VCC | VCC | +5 VDC or +3.3 VDC


Dimm SDram 168pin memory

DIMM SDRAM 168线内存条

DIMM 是 Dual Inline Memory Module (双列直插)的缩写

引脚定义

正面，左方：

Pin | Non-Parity | 72 ECC? | 80 ECC? | Description
1 | VSS | VSS | VSS | Ground
2 | DQ0 | DQ0 | DQ0 | Data 0
3 | DQ1 | DQ1 | DQ1 | Data 1
4 | DQ2 | DQ2 | DQ2 | Data 2
5 | DQ3 | DQ3 | DQ3 | Data 3
6 | VDD | VDD | VDD | +5 VDC or +3.3 VDC
7 | DQ4 | DQ4 | DQ4 | Data 4
8 | DQ5 | DQ5 | DQ5 | Data 5
9 | DQ6 | DQ6 | DQ6 | Data 6
10 | DQ7 | DQ7 | DQ7 | Data 7
11 | DQ8 | DQ8 | DQ8 | Data 8
12 | VSS | VSS | VSS | Ground
13 | DQ9 | DQ9 | DQ9 | Data 9
14 | DQ10 | DQ10 | DQ10 | Data 10
15 | DQ11 | DQ11 | DQ11 | Data 11
16 | DQ12 | DQ12 | DQ12 | Data 12
17 | DQ13 | DQ13 | DQ13 | Data 13
18 | VDD | VDD | VDD | +5 VDC or +3.3 VDC
19 | DQ14 | DQ14 | DQ14 | Data 14
20 | DQ15 | DQ15 | DQ15 | Data 15
21 | n/c | CB0 | CB0 | Parity/Check Bit Input/Output 0
22 | n/c | CB1 | CB1 | Parity/Check Bit Input/Output 01
23 | VSS | VSS | VSS | Ground
24 | n/c | n/c | CB8 | Parity/Check Bit Input/Output 8
25 | n/c | n/c | CB9 | Parity/Check Bit Input/Output 9
26 | VDD | VDD | VDD | +5 VDC or +3.3 VDC
27 | /WE | /WE | /WE | Read/Write
28 | DQMB0 | DQMB0 | DQMB0 | Byte Mask signal 0
29 | DQMB1 | DQMB1 | DQMB1 | Byte Mask signal 1
30 | /S0 | /S0 | /S0 | Chip Select 0
31 | DU | DU | DU | Don't Use
32 | VSS | VSS | VSS | Ground
33 | A0 | A0 | A0 | Address 0
34 | A2 | A2 | A2 | Address 2
35 | A4 | A4 | A4 | Address 4
36 | A6 | A6 | A6 | Address 6
37 | A8 | A8 | A8 | Address 8
38 | A10/AP | A10/AP | A10/AP | Address 10
39 | BA1 | BA1 | BA1 | Bank Address 1
40 | VDD | VDD | VDD | +5 VDC or +3.3 VDC
41 | VDD | VDD | VDD | +5 VDC or +3.3 VDC
42 | CK0 | CK0 | CK0 | Clock signal 0

正面，右方：

Pin | Non-Parity | 72 ECC? | 80 ECC? | Description
43 | VSS | VSS | VSS | Ground
44 | DU | DU | DU | Don't Use
45 | /S2 | /S2 | /S2 | Chip Select 2
46 | DQMB2 | DQMB2 | DQMB2 | Byte Mask signal 2
47 | DQMB3 | DQMB3 | DQMB3 | Byte Mask signal 3
48 | DU | DU | DU | Don't Use
49 | VDD | VDD | VDD | +5 VDC or +3.3 VDC
50 | n/c | n/c | CB10 | Parity/Check Bit Input/Output 10
51 | n/c | n/c | CB11 | Parity/Check Bit Input/Output 11
52 | n/c | CB2 | CB2 | Parity/Check Bit Input/Output 2
53 | n/c | CB3 | CB3 | Parity/Check Bit Input/Output 3
54 | VSS | VSS | VSS | Ground
55 | DQ16 | DQ16 | DQ16 | Data 16
56 | DQ17 | DQ17 | DQ17 | Data 17
57 | DQ18 | DQ18 | DQ18 | Data 18
58 | DQ19 | DQ19 | DQ19 | Data 19
59 | VDD | VDD | VDD | +5 VDC or +3.3 VDC
60 | DQ20 | DQ20 | DQ20 | Data 20
61 | n/c | n/c | n/c | Not connected
62 | Vref,NC | Vref,NC | Vref,NC |
63 | CKE1 | CKE1 | CKE1 | Clock Enable Signal 1
64 | VSS | VSS | VSS | Ground
65 | DQ21 | DQ21 | DQ21 | Data 21
66 | DQ22 | DQ22 | DQ22 | Data 22
67 | DQ23 | DQ23 | DQ23 | Data 23
68 | VSS | VSS | VSS | Ground
69 | DQ24 | DQ24 | DQ24 | Data 24
70 | DQ25 | DQ25 | DQ25 | Data 25
71 | DQ26 | DQ26 | DQ26 | Data 26
72 | DQ27 | DQ27 | DQ27 | Data 27
73 | VDD | VDD | VDD | +5 VDC or +3.3 VDC
74 | DQ28 | DQ28 | DQ28 | Data 28
75 | DQ29 | DQ29 | DQ29 | Data 29
76 | DQ30 | DQ30 | DQ30 | Data 30
77 | DQ31 | DQ31 | DQ31 | Data 31
78 | VSS | VSS | VSS | Ground
79 | CK2 | CK2 | CK2 | Clock signal 2
80 | n/c | n/c | n/c | Not connected
81 | n/c | n/c | n/c | Not connected
82 | SDA | SDA | SDA | Serial Data
83 | SCL | SCL | SCL | Serial Clock
84 | VDD | VDD | VDD | +5 VDC or +3.3 VDC

反面，左方：

Pin | Non-Parity | 72 ECC? | 80 ECC? | Description
85 | VSS | VSS | VSS | Ground
86 | DQ32 | DQ32 | DQ32 | Data 32
87 | DQ33 | DQ33 | DQ33 | Data 33
88 | DQ34 | DQ34 | DQ34 | Data 34
89 | DQ35 | DQ35 | DQ35 | Data 35
90 | VDD | VDD | VDD | +5 VDC or +3.3 VDC
91 | DQ36 | DQ36 | DQ36 | Data 36
92 | DQ37 | DQ37 | DQ37 | Data 37
93 | DQ38 | DQ38 | DQ38 | Data 38
94 | DQ39 | DQ39 | DQ39 | Data 39
95 | DQ40 | DQ40 | DQ40 | Data 40
96 | VSS | VSS | VSS | Ground
97 | DQ41 | DQ41 | DQ41 | Data 41
98 | DQ42 | DQ42 | DQ42 | Data 42
99 | DQ43 | DQ43 | DQ43 | Data 43
100 | DQ44 | DQ44 | DQ44 | Data 44
101 | DQ45 | DQ45 | DQ45 | Data 45
102 | VDD | VDD | VDD | +5 VDC or +3.3 VDC
103 | DQ46 | DQ46 | DQ46 | Data 46
104 | DQ47 | DQ47 | DQ47 | Data 47
105 | n/c | CB4 | CB4 | Parity/Check Bit Input/Output 4
106 | n/c | CB5 | CB5 | Parity/Check Bit Input/Output 5
107 | VSS | VSS | VSS | Ground
108 | n/c | n/c | CB12 | Parity/Check Bit Input/Output 12
109 | n/c | n/c | CB13 | Parity/Check Bit Input/Output 13
110 | VDD | VDD | VDD | +5 VDC or +3.3 VDC
111 | /CAS | /CAS | /CAS | Column Address Strobe
112 | DQMB4 | DQMB4 | DQMB4 | Byte Mask signal 4
113 | DQMB5 | DQMB5 | DQMB5 | Byte Mask signal 5
114 | /S1 | /S1 | /S1 | Chip Select 1
115 | /RAS | /RAS | /RAS | Row Address Strobe
116 | VSS | VSS | VSS | Ground
117 | A1 | A1 | A1 | Address 1
118 | A3 | A3 | A3 | Address 3
119 | A5 | A5 | A5 | Address 5
120 | A7 | A7 | A7 | Address 7
121 | A9 | A9 | A9 | Address 9
122 | BA0 | BA0 | BA0 | Bank Address 0
123 | A11 | A11 | A11 | Address 11
124 | VDD | VDD | VDD | +5 VDC or +3.3 VDC
125 | CK1 | CK1 | CK1 | Clock signal 1
126 | A12 | A12 | A12 | Address 12

反面，右方：

Pin | Non-Parity | 72 ECC? | 80 ECC? | Description
127 | VSS | VSS | VSS | Ground
128 | CKE0 | CKE0 | CKE0 | Clock Enable Signal 0
129 | /S3 | /S3 | /S3 | Chip Select 3
130 | DQMB6 | DQMB6 | DQMB6 | Byte Mask signal 6
131 | DQMB7 | DQMB7 | DQMB7 | Byte Mask signal 7
132 | A13 | A13 | A13 | Address 13
133 | VDD | VDD | VDD | +5 VDC or +3.3 VDC
134 | n/c | n/c | CB14 | Parity/Check Bit Input/Output 14
135 | n/c | n/c | CB15 | Parity/Check Bit Input/Output 15
136 | n/c | CB6 | CB6 | Parity/Check Bit Input/Output 6
137 | n/c | CB7 | CB7 | Parity/Check Bit Input/Output 7
138 | VSS | VSS | VSS | Ground
139 | DQ48 | DQ48 | DQ48 | Data 48
140 | DQ49 | DQ49 | DQ49 | Data 49
141 | DQ50 | DQ50 | DQ50 | Data 50
142 | DQ51 | DQ51 | DQ51 | Data 51
143 | VDD | VDD | VDD | +5 VDC or +3.3 VDC
144 | DQ52 | DQ52 | DQ52 | Data 52
145 | n/c | n/c | n/c | Not connected
146 | Vref,NC | Vref,NC | Vref,NC |
147 | n/c | n/c | n/c | Not connected
148 | VSS | VSS | VSS | Ground
149 | DQ53 | DQ53 | DQ53 | Data 53
150 | DQ54 | DQ54 | DQ54 | Data 54
151 | DQ55 | DQ55 | DQ55 | Data 55
152 | VSS | VSS | VSS | Ground
153 | DQ56 | DQ56 | DQ56 | Data 56
154 | DQ57 | DQ57 | DQ57 | Data 57
155 | DQ58 | DQ58 | DQ58 | Data 58
156 | DQ59 | DQ59 | DQ59 | Data 59
157 | VDD | VDD | VDD | +5 VDC or +3.3 VDC
158 | DQ60 | DQ60 | DQ60 | Data 60
159 | DQ61 | DQ61 | DQ61 | Data 61
160 | DQ62 | DQ62 | DQ62 | Data 62
161 | DQ63 | DQ63 | DQ63 | Data 63
162 | VSS | VSS | VSS | Ground
163 | CK3 | CK3 | CK3 | Clock signal 3
164 | n/c | n/c | n/c | Not connected
165 | SA0 | SA0 | SA0 | Serial address 0
166 | SA1 | SA1 | SA1 | Serial address 1
167 | SA2 | SA2 | SA2 | Serial address 2
168 | VDD | VDD | VDD | +5 VDC or +3.3 VDC


以太网 10/100Base-T 接口

网卡上以及 Hub 上接口的外观为 8 芯母插座 (RJ45)：

RJ45 FEMALE CONNECTOR
网线上插头的外观为 8 芯公插头 (RJ45)：

RJ45 FEMALE CONNECTOR

引脚定义

Pin | Name | Description
1 | TX+ | Tranceive Data+ (发信号+)
2 | TX- | Tranceive Data- (发信号-)
3 | RX+ | Receive Data+ (收信号+)
4 | n/c | Not connected (空脚)
5 | n/c | Not connected (空脚)
6 | RX- | Receive Data- (收信号-)
7 | n/c | Not connected (空脚)
8 | n/c | Not connected (空脚)


以太网 100Base-T4 接口

网卡上以及 Hub 上接口的外观为 8 芯母插座 (RJ45)：

RJ45 FEMALE CONNECTOR
网线上插头的外观为 8 芯公插头(RJ45)：

RJ45 FEMALE CONNECTOR

引脚定义

Pin | Name | Description
1 | TX_D1+ | Tranceive Data+
2 | TX_D1- | Tranceive Data-
3 | RX_D2+ | Receive Data+
4 | BI_D3+ | Bi-directional Data+
5 | BI_D3- | Bi-directional Data-
6 | RX_D2- | Receive Data-
7 | BI_D4+ | Bi-directional Data+
8 | BI_D4- | Bi-directional Data-


PC 主机板电源接口

主机板上的电源引脚外观为：

MOLEX 15-48-0106 CONNECTOR

引脚定义

P8:

Pin | Name | Color | Description
1 | PG | Orange(橙) | Power Good, +5 VDC when all voltages has stabilized.(电源正常信号)
2 | +5V | Red(红) | +5 VDC (or n/c)
3 | +12V | Yellow(黄) | +12 VDC
4 | -12V | Blue(蓝) | -12 VDC
5 | GND | Black(黑) | Ground (地线)
6 | GND | Black(黑) | Ground (地线)

P9:

Pin | Name | Color | Description
1 | GND | Black(黑) | Ground (地线)
2 | GND | Black(黑) | Ground (地线)
3 | -5V | White or Yellow | -5 VDC
4 | +5V | Red(红) | +5 VDC
5 | +5V | Red(红) | +5 VDC
6 | +5V | Red(红) | +5 VDC


5.25" 电源接口

5.25 " 电源引脚接线的外观为：

UNKNOWN CONNECTOR
5.25 " 驱动器上电源引脚插座的外观为：
UNKNOWN CONNECTOR

引脚定义

Pin | Name | Color | Description
1 | +12V | Yellow(黄) | +12 VDC
2 | GND | Black(黑) | +12 V Ground (Same as +5 V Ground)
3 | GND | Black(黑) | +5 V Ground
4 | +5V | Red(红) | +5 VDC


软驱电源接口

引脚定义：

Pin | Name | Color | Description
1 | +5V | Red(红) | +5 VDC
2 | GND | Black(黑) | +5 V Ground (地线)
3 | GND | Black(黑) | +12 V Ground (地线)
4 | +12V | Yellow(黄) | +12 VDC


串行直连电缆 9针-9针(NullModem)

串行直连电缆用于两台计算机的直接相连，电缆两端的插头都是 9 针母插头：

9 PIN D-SUB FEMALE

引脚定义

 | D-Sub 1 | D-Sub 2 |
Receive Data | 2 | 3 | Transmit Data
Transmit Data | 3 | 2 | Receive Data
Data Terminal Ready | 4 | 6+1 | Data Set Ready + Carrier Detect
System Ground | 5 | 5 | System Ground
Data Set Ready + Carrier Detect | 6+1 | 4 | Data Terminal Ready
Request to Send | 7 | 8 | Clear to Send
Clear to Send | 8 | 7 | Request to Send


串行直连电缆 9针-25针(NullModem)

串行直连电缆用于两台计算机的直接相连，电缆两端的插头一头是 9 针母插头，另一头是 25 针母插头：

9 PIN D-SUB FEMALE 25 PIN D-SUB FEMALE

引脚定义

 | D-Sub 9 | D-Sub 25 |
Receive Data | 2 | 2 | Transmit Data
Transmit Data | 3 | 3 | Receive Data
Data Terminal Ready | 4 | 6+8 | Data Set Ready + Carrier Detect
System Ground | 5 | 7 | System Ground
Data Set Ready + Carrier Detect | 6+1 | 20 | Data Terminal Ready
Request to Send | 7 | 5 | Clear to Send
Clear to Send | 8 | 4 | Request to Send


串行直连电缆 25针-25针(NullModem)

串行直连电缆用于两台计算机的直接相连，电缆两端的插头都是 25 针母插头：

25 PIN D-SUB FEMALE

引脚定义

 | D-Sub 1 | D-Sub 2 |
Receive Data | 3 | 2 | Transmit Data
Transmit Data | 2 | 3 | Receive Data
Data Terminal Ready | 20 | 6+8 | Data Set Ready + Carrier Detect
System Ground | 7 | 7 | System Ground
Data Set Ready + Carrier Detect | 6+8 | 20 | Data Terminal Ready
Request to Send | 4 | 5 | Clear to Send
Clear to Send | 5 | 4 | Request to Send


9针串口到25针Modem连线

电缆两端的到计算机的插头是 9 针母插头：

9 PIN D-SUB FEMALE
到 Modem 的一头是 25 针公插头：
25 PIN D-SUB MALE

引脚定义

 | Female | Male | Dir
Shield |  | 1 | ---
Transmit Data | 3 | 2 | -->
Receive Data | 2 | 3 | <--
Request to Send | 7 | 4 | -->
Clear to Send | 8 | 5 | <--
Data Set Ready | 6 | 6 | <--
System Ground | 5 | 7 | ---
Carrier Detect | 1 | 8 | <--
Data Terminal Ready | 4 | 20 | -->
Ring Indicator | 9 | 22 | <--


25针串口到25针Modem连线

电缆两端的到计算机的插头是 25 针母插头：

25 PIN D-SUB FEMALE
到 Modem 的一头是 25 针公插头：
25 PIN D-SUB MALE

引脚定义

 | Female | Male | Dir
Shield Ground | 1 | 1 | ---
Transmit Data | 2 | 2 | -->
Receive Data | 3 | 3 | <--
Request to Send | 4 | 4 | -->
Clear to Send | 5 | 5 | <--
Data Set Ready | 6 | 6 | <--
System Ground | 7 | 7 | ---
Carrier Detect | 8 | 8 | <--
Data Terminal Ready | 20 | 20 | -->
Ring Indicator | 22 | 22 | <--


打印机电缆

电缆两端的到计算机的插头是 25 针公插头：

25 PIN D-SUB MALE
到打印机的插头是 36 针母插头：
36 PIN CENTRONICS MALE

引脚定义

 | 25-DSub | 36-Cen
Strobe | 1 | 1
Data Bit 0 | 2 | 2
Data Bit 1 | 3 | 3
Data Bit 2 | 4 | 4
Data Bit 3 | 5 | 5
Data Bit 4 | 6 | 6
Data Bit 5 | 7 | 7
Data Bit 6 | 8 | 8
Data Bit 7 | 9 | 9
Acknowledge | 10 | 10
Busy | 11 | 11
Paper Out | 12 | 12
Select | 13 | 13
Autofeed | 14 | 14
Error | 15 | 32
Reset | 16 | 31
Select | 17 | 36
Signal Ground | 18 | 33
Signal Ground | 19 | 19,20
Signal Ground | 20 | 21,22
Signal Ground | 21 | 23,24
Signal Ground | 22 | 25,26
Signal Ground | 23 | 27
Signal Ground | 24 | 28,29
Signal Ground | 25 | 30,16
Shield | Shield | Shield+17


9针串口到25针打印机电缆

9针串口到25针串行打印机电缆

电缆两端的到计算机的插头是 9 针母插头：

9 PIN D-SUB FEMALE
到打印机的插头是 25 针母插头：
25 PIN D-SUB FEMALE

引脚定义

 | D-Sub 1 | D-Sub 2 |
Receive Data | 3 | 3 | Transmit Data
Transmit Data | 2 | 2 | Receive Data
Clear To Send + Data Set Ready | 8 + 6 | 20 | Data Terminal Ready
Carrier Detect + Data Terminal Ready | 1 + 4 |  |
Ground | 5 | 7 | Ground


25针串口到25针打印机电缆

25针串口到25针串行打印机电缆

电缆两端的到计算机的插头是 25 针母插头：

25 PIN D-SUB FEMALE
到打印机的插头是 25 针母插头：
25 PIN D-SUB FEMALE

引脚定义

 | D-Sub 1 | D-Sub 2 |
Receive Data | 2 | 3 | Transmit Data
Transmit Data | 3 | 2 | Receive Data
Clear To Send + Data Set Ready | 5 + 6 | 20 | Data Terminal Ready
Carrier Detect + Data Terminal Ready | 8 + 20 |  |
Ground | 7 | 7 | Ground


InterLink/LapLink 并行连接电缆

并行连接电缆用于以下软件的两机互连：LapLink，MS-DOS 6.0 的 InterLink，Windows 95 的直接电缆连接，Norton Commander v4.0/v5.0 的直接电缆连接。
电缆两端的插头都是 25 针公插头：

25 PIN D-SUB MALE

引脚定义

Name | Pin | Pin | Name
Data Bit 0 | 2 | 15 | Error
Data Bit 1 | 3 | 13 | Select
Data Bit 2 | 4 | 12 | Paper Out
Data Bit 3 | 5 | 10 | Acknowledge
Data Bit 4 | 6 | 11 | Busy
Acknowledge | 10 | 5 | Data Bit 3
Busy | 11 | 6 | Data Bit 4
Paper Out | 12 | 4 | Data Bit 2
Select | 13 | 3 | Data Bit 1
Error | 15 | 2 | Data Bit 0
Reset | 16 | 16 | Reset
Select | 17 | 17 | Select
Signal Ground | 25 | 25 | Signal Ground


以太网10/100Base-T直连电缆

以太网网络电缆用于网卡和 Hub 之间的连接，电缆两端的插头都是 8 针 (RJ45) 公插头：

RJ45 FEMALE CONNECTOR

引脚定义

Name | Pin | Cable Color | Pin | Name
TX+ | 1 | White/Orange | 1 | TX+
TX- | 2 | Orange | 2 | TX-
RX+ | 3 | White/Green | 3 | RX+
 | 4 | Blue | 4 |
 | 5 | White/Blue | 5 |
RX- | 6 | Green | 6 | RX-
 | 7 | White/Brown | 7 |
 | 8 | Brown | 8 |


以太网10/100Base-T对绞电缆

以太网对绞电缆用于 Hub 之间扩充时的级连，或用于两台计算机不用 Hub 而直接相连，电缆两端的插头都是 8 针 (RJ45) 公插头：

RJ45 FEMALE CONNECTOR

引脚定义

Name | NIC1 | NIC2 | Name
TX+ | 1 | 3 | RX+
TX- | 2 | 6 | RX-
RX+ | 3 | 1 | TX+
RX- | 6 | 2 | TX-


以太网100Base-T4对绞电缆

以太网对绞电缆用于 Hub 之间扩充时的级连，或用于两台计算机不用 Hub 而直接相连，电缆两端的插头都是 8 针 (RJ45) 公插头：

RJ45 FEMALE CONNECTOR

引脚定义

Name | Pin | Pin | Name
TX_D1+ | 1 | 3 | RX_D2+
TX_D1- | 2 | 6 | RX_D2-
RX_D2+ | 3 | 1 | TX_D1+
RX_D2- | 6 | 2 | TX_D1-
BI_D3+ | 4 | 7 | BI_D4+
BI_D3- | 5 | 8 | BI_D4-
BI_D4+ | 7 | 4 | BI_D3+
BI_D4- | 8 | 5 | BI_D3-


9 针串行口到 25 针转换器

本转换口把一个 25 针的串行设备接到 9 针的串行口上，PC 一侧为 9 针母插头：

9 PIN D-SUB FEMALE
到串行设备一侧为 25 针公插头：
 25 PIN D-SUB MALE

引脚定义

 | 9-Pin | 25-Pin
Carrier Detect | 1 | 8
Receive Data | 2 | 3
Transmit Data | 3 | 2
Data Terminal Ready | 4 | 20
System Ground | 5 | 7
Data Set Ready | 6 | 6
Request to Send | 7 | 4
Clear to Send | 8 | 5
Ring Indicator | 9 | 22


PS/2 键盘口到普通键盘转接器

本转换口把一个普通键盘接到 PS/2 的键盘口上，PC 一侧为 6 针 PS/2 公插头：
 6 PIN MINI-DIN MALE (PS/2 STYLE)
到键盘一侧为 5 针母插头：
 5 PIN DIN 180° (DIN41524) FEMALE

引脚定义

 | 到键盘 | 到 PC PS/2 键盘口
Shield | Shield | Shield
Clock | 1 | 5
Data | 2 | 1
Ground | 4 | 3
+5 VDC | 5 | 4


普通键盘口到 PS/2 键盘转接器

本转换口把一个 PS/2 键盘接到普通键盘口上，到键盘一侧为 6 针 PS/2 母插头：
 6 PIN MINI-DIN MALE (PS/2 STYLE)
到 PC 一侧为 5 针公插头：
 5 PIN DIN 180° (DIN41524) FEMALE

引脚定义

 | 到 PS/2 键盘 | 到 PC
Shield | Shield | Shield
Data | 1 | 2
Ground | 3 | 4
+5 VDC | 4 | 5
Clock | 5 | 1


串行口到 PS/2 鼠标转接器

说明：本图是我根据一些资料和自己对一些名牌鼠表内带转换口的测量画出的，不过我用在普通PS/2鼠标上试验没有成功过，大家可以注意到PS/2鼠标关键的两脚 1 (Data) 和 5 (Clock) 没有连接，而空脚 2,6 却和 RxD, TxD 相连，所以我认为，PS/2鼠标实际上是无法接在RS-232口的，一些名牌鼠标可以只不过是它利用了1，5两个空脚另外做了两个和 RS-232 口兼容的 RxD, TxD 而已，而普通PS/2鼠标却没有做。
所以大家拿到资料后不要一下子就对自己的鼠标“咔嚓”一剪 ... ... 我可不管了。

本转换口把一个 PS/2 鼠标接到串行口上，到鼠标一侧为 6 针 PS/2 母插头：
到 PC 一侧为 9 针母插头：
 9 PIN D-SUB FEMALE

引脚定义

 | 到PS/2 鼠标 | 到串行口 |
Data | 1 |  |
RxD | 2 | 2 | RxD
Gnd | 3 | 5 | Gnd
Vcc | 4 | 7 | Rts
Clk | 5 | 4+6 |
TxD | 6 | 3 | TxD


PS/2 鼠标口到串行鼠标转接器

本转换口把一个串行鼠标接到 PS/2 鼠标口上，到鼠标一侧为 9 针公插头：
 9 PIN D-SUB MALE
到 PC PS/2 鼠标口一侧为 6 针 PS/2 公插头：
 6 PIN MINI-DIN MALE

引脚定义

 | 到 PS/2 口 | 到鼠标 |
+5V | 4 | 4+7+9 | DTR+RTS+RI
Data | 1 | 1 | CD
Gnd | 3 | 3+5 | TXD+GND
Clock | 5 | 6 | DSR


Mac 苹果机到 VGA 显示器转接器

Mac 苹果机视频到 VGA 显示器转接器

本转换口把一个 PC 的 VGA 显示器接到苹果机的显示输出口上，到苹果机一侧为 15 针公插头：
 15 PIN D-SUB MALE
到 PC VGA 显示器一侧为 15 针母插头：
 15 PIN HIGHDENSITY D-SUB FEMALE

引脚定义

Description | Mac | VGA | Dir
Red Ground | 1 | 6 | ---
Red | 2 | 1 | -->
Composite sync | 3 | 13 | -->
Monitor Sense 0 | 4 | 4 | <--
Green | 5 | 2 | -->
Green Ground | 6 | 7 | ---
Monitor Sense 1 | 7 | 11 | <--
No connection | 8 | n/c |
Blue | 9 | 3 | -->
Monitor sense 2 | 10 | 12 | <--
Sync Ground | 11 | 10 | ---
Vertical Sync | 12 | 14 | -->
Blue Ground | 13 | 8 | ---
Horizontal Sync Ground | 14 | n/c |
Horizontal Sync | 15 | n/c |


AGP bus

AGP总线定义

AGP 是 Accelerated Graphics Port，是 Intel 推出的一种 3D 图形标准接口，它能够提供四倍于 PCI 的效率，AGP2X 的传输速率达到 533MB。有关 AGP 的说明可以在 Intel 的网站上找到。

引脚定义

Pin | B面 | A面
1 | Spare | 12V
2 | 5.0V | Spare
3 | 5.0V | Reserved
4 | USB+ | USB-
5 | Ground | Ground
6 | INTB# | INTA#
7 | Clock | RST#
8 | REQ# | GNT#
9 | Vcc3.3V | Vcc3.3V
10 | ST0 | ST1
11 | ST2 | Reserved
12 | RBF# | PIPE#
13 | Ground | Ground
14 | Spare | Spare
15 | SBA0 | SBA1
16 | Vcc3.3V | Vcc3.3V
17 | SBA2 | SBA3
18 | SB_STB | Reserved
19 | Ground | Ground
20 | SBA4 | SBA5
21 | SBA6 | SBA7
22 | Key | Key
23 | Key | Key
24 | Key | Key
25 | Key | Key
26 | Address31 | Address30
27 | Address29 | Address28
28 | Vcc3.3V | Vcc3.3V
29 | Address27 | Address26
30 | Address25 | Address24
31 | Ground | Ground
32 | AD_STB1 | Reserved
33 | Address23 | C/BE3#
34 | Vddq3.3 | Vddq3.3
35 | Address21 | Address22
36 | Address19 | Address20
37 | Ground | Ground
38 | Address17 | Address18
39 | C/BE2# | Address16
40 | Vddq3.3 | Vddq3.3
41 | IRDY# | FRAME#
42 |  |
43 | Ground | Ground
44 |  |
45 | Vcc3.3V | Vcc3.3V
46 | DEVSEL# | TRDY#
47 | Vddq3.3 | STOP#
48 | Perr# | Spare
49 | Ground | Ground
50 | SERR# | PAR
51 | C/BE1# | Address15
52 | Vddq3.3 | Vddq3.3
53 | Address14 | Address13
54 | Address12 | Address11
55 | Ground | Ground
56 | Address10 | Address9
57 | Address8 | C/BE0#
58 | Vddq3.3 | Vddq3.3
59 | AD_STB0 | Reserved
60 | Address7 | Address6
61 | Ground | Ground
62 | Address5 | Address4
63 | Address3 | Address2
64 | Vddq3.3 | Vddq3.3
65 | Address1 | Address0
66 | SMB0 | SMB1


PC ATX主机板电源接口

ATX 电源引脚为 20 脚，其中第一脚为方型，其余为圆型，外形为：

atx-power.gif (9254 bytes)

引脚定义为：

Pin | Name | Description
1 | 3.3V | 提供 +3.3V 电源
2 | 3.3V | 提供 +3.3V 电源
3 | GND | 地线
4 | 5V | 提供 +5V 电源
5 | GND | 地线
6 | 5V | 提供 +5V 电源
7 | GND | 地线
8 | PW-OK | Power OK，指示电源正常工作
9 | 5VSB | 提供 +5V Stand by电源，供电源启动电路用
10 | 12V | 提供 +12V 电源
11 | 3.3V | 提供 +3.3V 电源
12 | -12V | 提供 -12V 电源
13 | GND | 地线
14 | PS-ON | 电源启动信号，低电平-电源开启，高电平-电源关闭
15 | GND | 地线
16 | GND | 地线
17 | GND | 地线
18 | -5V | 提供 -5V 电源
19 | 5V | 提供 +5V 电源
20 | 5V | 提供 +5V 电源