《WINDOWS 环境下32位汇编语言程序设计》第5章 使用资源(2)

5.4 对话框

5.4.1 对话框简介

顾名思义,对话框完成的是“对话”功能,应用程序一般建立一个主窗口用做工作界面,大部分的工作会在主窗口的客户区完成,但程序往往需要和用户交互,如输入参数和输入文本等,这些界面不必要全部放在主窗口中。习惯的做法是通过选择菜单项弹出一个窗口,然后在这个窗口中完成对话,这个窗口就是“对话框”,对话框中的按钮、文本框和图标等称为“子窗口控件”。

为了提示用户,习惯于在会引出对话框的菜单项后面加上省略号。如“文件”菜单中的“另存为...”表示会引出一个选择文件名的对话框,所以“另存为”3个字后面加了个省略号。对话框最典型的例子就是写字板“查找”菜单弹出的窗口,以及IE浏览器中选择“Internet选项”菜单项弹出的设置窗口。

1.对话框的类型

对话框分两类:modal对话框和modeless对话框,翻译成中文就是“模态的”和“非模态的”(也有的地方翻译成“模式的”和“非模式的”,Visual FoxPro中文版就是这样),它们之间的区别在于是否允许用户在不同窗口间进行切换:当显示非模态对话框时,用户可以随意在这个对话框和其他窗口之间切换;而显示一个模态对话框时,用户在关闭对话框之前不允许切换到同一程序的其他窗口中,但可以切换到其他程序的窗口中;如果显示的是操作系统所属的模态对话框(即“系统模态的”),则切换到其他任何程序的窗口都是不允许的。

Windows在资源文件中定义对话框,然后在程序中利用这个模板创建对话框,模态对话框和非模态对话框的资源定义是相同的,只是创建时调用的函数不同而已。

2.对话框的工作原理

很明显,对话框和普通窗口之间有很多相似之处,实际上对话框就是基于窗口的,对话框的窗口风格使用的就是普通窗口的风格定义,对话框也有一个类似于窗口过程的对话框过程,但对话框和普通窗口在实现上又有很多不同之处,模态对话框和非模态对话框的实现也是不同的,图5.4对比了它们之间的不同之处。普通的窗口在建立之前需要用RegisterClass注册一个窗口类,然后用CreateWindow建立窗口,建立窗口所需的参数如窗口风格、大小位置和窗口过程地址等由窗口类,以及CreateWindow中的参数共同提供。

建立对话框的时候并不使用CreateWindow函数,取而代之,建立模态对话框的函数是DialogBoxParam,建立非模态对话框的函数是CreateDialogParam,调用这两个函数创建对话框窗口之前不需要注册对话框的窗口类。

                                   图5.4 对话框和普通窗口工作方式的区别

Windows在这两个函数的内部调用CreateWindowEx来建立对话框,使用的风格、大小和位置等参数取自资源中定义的对话框模板,使用的窗口类则是Windows内部定义的类。如果读者用一些工具去查看,会发现类名是“#32770”之类的字符串,在这个名字奇特的窗口类中,窗口过程被定义到了Windows内部的“对话框管理器”代码中,Windows在这里处理对话框的大部分消息,如维护客户区的刷新,键盘接口(按Tab键在不同子窗口之间切换、按回车键调用默认按钮等),对话框管理器在初始化对话框时会根据对话框模板中定义的子窗口控件建立对话框中所有的子窗口。

用户程序中的对话框过程是由对话框管理器调用的,在处理消息前,对话框管理器会先调用用户指定的对话框过程,再根据对话框过程的返回值决定是否处理它们。

Windows对模态对话框和非模态对话框的处理有些不同。在创建并显示模态对话框后,Windows会为它在内部建立一个消息循环,在这个消息循环中把消息发送给对话框管理器,对话框管理器在处理消息的过程中会调用用户定义的对话框过程,当对话框关闭的时候,Windows退出内建的消息循环,并从DialogBoxParam函数返回。而对于非模态对话框,CreateDialogParam函数在创建对话框后直接返回,对话框窗口的消息是通过用户程序中的消息循环派送的。

由于模态对话框的特征,使得用它来做小程序的主窗口非常方便,因为用一句DialogBoxParam函数就可以搞定了,既不用注册窗口类,也不用写消息循环,这对看到创建窗口的几十句代码就烦的读者来说可真是个福音,笔者也很喜欢用模态对话框做程序的主窗口。这种方法的缺点就是无法使用依赖消息循环来完成的功能,很明显,加速键就不能用了。

在接下来的内容中,以一个最简单的例子来讲解如何实现模态对话框,所有的源程序可以在所附光盘的Chapter05\Dialog目录中找到,包括资源脚本文件Dialog.rc,汇编源文件Dialog.asm以及makefile文件,Dialog.exe运行的结果如图5.5所示。

                      图5.5 例子对话框的运行结果

5.4.2 对话框的资源定义

1.对话框资源定义的语法

在资源脚本中定义对话框的语法是:

对话框ID DIALOG [DISCARDABLE] x坐标, y坐标, 宽度, 高度 
[可选属性] 
BEGIN 
子窗口控件 
... 
END 

对话框中的子窗口控件语句定义在BEGIN/END(当然也可以用花括号)之中,在这之前,可以定义对话框的一些可选属性,每种属性单独用一行定义,常用的可选属性如表5.3所示。

                                                    表5.3 对话框的可选属性

在本节的例子中,资源脚本文件Dialog.rc是这样定义的:

    //>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
    #include               <resource.h>
    //>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
    #define ICO_MAIN         0x1000  //图标
    #define DLG_MAIN          1
    //>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
    ICO_MAIN          ICON     "Main.ico"
    //>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
    DLG_MAIN DIALOG 50, 50, 113, 64
    STYLE DS_MODALFRAME | WS_POPUP | WS_VISIBLE | WS_CAPTION | WS_SYSMENU
    CAPTION "对话框模板"
    FONT 9, "宋体"
    {
    ICON ICO_MAIN, -1, 10, 11, 18, 21
    CTEXT "简单的对话框例子\n用Win32ASM编写", -1, 36, 14, 70, 19
    DEFPUSHBUTTON "退出(&X)", IDOK, 58, 46, 50, 14
    CONTROL "", -1, "Static", SS_ETCHEDHORZ | WS_CHILD | WS_VISIBLE, 6, 39, 10
    }

脚本文件中除了定义图标以外,另外还定义了一个ID为1的对话框,对话框中有4个子窗口控件,分别是图标、文本、按钮和一条横线,按钮的ID为IDOK,其他的子窗口控件由于是静态控件,不会向对话框过程发送命令,所以ID就设置为−1,这些控件的具体用法将在后面的内容中详细介绍。

定义中还指定了一些可选属性,STYLE语句定义了对话框窗口的风格,CAPTION语句把标题定义为“对话框模板”,FONT语句指定了对话框使用的字体是大小为9的宋体。

对话框的位置为(50,50),大小为宽113单位、高64单位,读者可能已经注意到:这个对话框的大小好像比宽113像素、高64像素的窗口要大。事实上的确如此,这也正是大小是“单位”而不是“像素”的原因。对话框的位置、大小以及所有子窗口控件的度量单位是根据系统字体的大小来决定的,横向(x坐标和宽度)每单位为系统字符平均宽度的1/4,纵向(y坐标和高度)每单位为字符平均高度的1/8,由于系统字体的字符高度大致为宽度的两倍,所以虽然这种计算方法有些费解,但横向和纵向的数值在视觉上还是成比例的,但和以“像素”为单位在数值上肯定是不同的。如果读者一定要知道这个值换算成像素后是多少,那么可以用GetDialogBaseUnits函数来获取系统字体的高度和宽度再进行计算。

当一些英文版的软件在中文Windows上运行的时候,对话框中有些文本往往被砍掉了尾巴,原因就是这些程序是在英文Windows上调试的,文本框的尺寸是以英文Windows系统字符的大小来度量的,到了其他语言的Windows上后,系统字符的大小可能改变,对话框的大小也随着改变,结果就是原来刚好的宽度可能会变得不够,这也算是对话框尺寸度量方法的缺点吧!

使用文本编辑器直接书写对话框脚本定义不是很直观,所以在创建对话框资源时最好使用可视化的资源编辑器,如VC++或ResourceWorkshop等。

在子窗口控件的ID定义中有两个特殊的ID值——IDOK和IDCANCEL,在Resource.h中它们的值定义为1和2,IDOK是默认的“确定”ID,IDCANCEL是默认的“取消”ID。如果一个按钮的ID是IDOK,当焦点没有停留在其他按钮上的时候,在任何地方按下回车键就相当于按下了这个按钮,而按下Esc键的时候,就相当于按下了ID为IDCANCEL的按钮。

2.Tab停留位和组

对话框中可以定义多个子窗口控件,有的子窗口控件可以拥有输入焦点(如按钮、文本框与组合框等),有些则不能(如图标与文本等),当对话框中有多个允许拥有输入焦点的子窗口控件时(有WS_TABSTOP风格),用户可以用Tab键将输入焦点切换到下一个有WS_TABSTOP风格的子窗口控件上,也可以用Shift+Tab键切换到上一个,Tab键切换的顺序就叫做Tab停留位。

Tab停留位并不是系统根据子窗口控件的坐标位置自动排列的,而是按照子窗口控件在资源脚本文件中的定义顺序来排列的,所以读者在定义的时候最好根据子窗口控件的位置适当排列语句的先后,以免按动Tab键切换的时候焦点上下左右无规则地跳来跳去。如果使用可视化的资源编辑器,那么菜单中一般会有“Tab停留位”菜单项,在编辑完成后也要进到这个菜单项中设置一下,资源编辑器会根据设置调整rc文件中定义语句的先后顺序。

对话框中往往有一些排列在一起的同类子窗口控件,如几个单选钮,几个单选钮之间的选中标记是互斥的,在对话框的其他地方可能又有一组互斥的单选钮用来代表其他功能,在对话框中规定所有的单选钮都是互斥的显然不现实,解决的方法就是将不同的子窗口控件 “分组”,这就是“组”的含义。使用中可以选择一些子窗口控件定义WS_GROUP属性,两个有WS_GROUP属性的子窗口控件之间的所有子窗口控件同属同一组。

5.4.3 使用对话框

使用对话框的代码分为创建部分和对话框过程两个部分。先看Dialog.asm的源代码,再分析具体的使用过程,源代码如下:

;Dialog.asm               对话框资源使用的模板代码
;使用 nmake 或下列命令进行编译和链接:
;ml /c /coff Dialog.asm
;rc Dialog.rc
;Link /subsystem:windows Dialog.obj Dialog.res

.386
.model flat,stdcall 
option casemap:none 
;include 文件定义
include 	C:/masm32/include/windows.inc 
include 	C:/masm32/include/user32.inc 
includelib	C:/masm32/lib/user32.lib 
include 	C:/masm32/include/kernel32.inc 
includelib	C:/masm32/lib/kernel32.lib 

;Equ 等值定义
ICO_MAIN	equ 1000h   ;图标
DLG_MAIN 	equ 1 

;数据段
.data?
hInstance dword ?

;代码段
.code 
_ProcDlgMain proc uses ebx edi esi, hWnd, wMsg, wParam, lParam 
	mov eax, wMsg 
	.if eax == WM_CLOSE 
		invoke EndDialog, hWnd, NULL 
	.elseif eax == WM_INITDIALOG 
		invoke LoadIcon, hInstance, ICO_MAIN 
		invoke SendMessage, hWnd, WM_SETICON, ICON_BIG, eax 
	.elseif eax == WM_COMMAND 
		mov eax, wParam 
		.if ax == IDOK 
			invoke EndDialog, hWnd, NULL 
		.endif 
	.else
		mov eax, FALSE 
		ret 
	.endif
	mov eax, TRUE
	ret 
_ProcDlgMain endp 

main proc 
	invoke GetModuleHandle, NULL 
	mov hInstance, eax 
	invoke DialogBoxParam, hInstance, DLG_MAIN, NULL, offset _ProcDlgMain, NULL 
	invoke ExitProcess, NULL 
main endp 
end main 

编译运行:

读者可以发现,相对于普通窗口的使用,对话框的使用显得特别简单,最明显的区别在于主程序中的一大堆代码不见了,换成了一个DialogBoxParam语句。

1.创建模态对话框

创建模态对话框的函数是DialogBoxParam,它的使用方法是:

invoke   DialogBoxParam,hInstance,lpTemplateName,hWndParent,\
                    lpDialogFunc,dwInitParam

函数的各参数说明如下:

● hInstance和lpTemplateName—函数从hInstance参数指定的模块中装入lpTemplateName参数指定的对话框资源,然后显示对话框窗口。例子程序中的lpTemplateName参数用的就是我们定义的DLG_MAIN。

● hWndParent——对话框的父窗口,对话框关闭之前将无法切换到父窗口所属的其他窗口中,例子中用对话框做主窗口,所以父窗口句柄是NULL,在其他程序中使用时,这个参数设置为主窗口的句柄。

● lpDialogFunc——指定了对话框过程的地址,例子程序中是_ProcDlgMain。

● dwInitParam——当做WM_INITDIALOG消息的lParam传给对话框过程,读者可以用它来做自定义的用途。

要结束模态对话框,必须在对话框过程的WM_CLOSE消息中使用EndDialog函数:

invoke   EndDialog,hDlg,dwResult

不能使用通常的DestroyWindow函数,参数中的hDlg就是对话框窗口的句柄,dwResult参数是退出时的返回值,这个值最后由DialogBoxParam函数返回到主程序中。

2.创建非模态对话框

创建非模态对话框的函数是CreateDialogParam,它的参数定义和DialogBoxParam一模一样:

invoke   CreateDialogParam,hInstance,lpTemplateName,hWndParent,\
                    lpDialogFunc,dwInitParam
mov      hDlg,eax

CreateDialogParam和DialogBoxParam在使用中有几个不同点:

● CreateDialogParam在创建对话框后,会根据对话框模板的风格是否定义了WS_VISIBLE来决定是否显示对话框窗口。如果定义了则显示,没有的话,则程序需要在以后自行调用ShowWindow来显示它;而DialogBoxParam函数不管是否定义了WS_VISIBLE风格都会显示对话框。

● CreateDialogParam在建立对话框窗口后直接返回,返回值是对话框窗口的句柄;而DialogBoxParam要在对话框关闭后才返回,返回值是EndDialog中的dwResult参数。

● 由于在CreateDialogParam返回后,应用程序在自己的消息循环中获取对话框消息,所以如果要用非模态对话框做程序的主窗口,消息循环的代码还是要写的;而DialogBoxParam是使用Windows为它内建的消息循环。

● 关闭非模态对话框仍然使用DestroyWindow函数,注意在这里不要用EndDialog函数。

3.对话框过程

Windows在“对话框管理器” ——也就是为对话框内建的窗口过程中处理对话框消息,在处理前会首先调用用户定义的对话框过程,程序可以在这里选择是否自行处理某些消息。读者在理解时可以把“对话框管理器”看成是对话框的DefWindowProc,凡是自己不想处理的消息都由它来处理。

与窗口过程一样,对话框过程是一个“回调”子程序,它由程序定义,Windows来调用,模态对话框和非模态对话框的对话框过程是一样的。

对话框过程和窗口过程的输入参数是一样的,也是:

DialogProc  proc      hwndDlg,uMsg,wParam,lParam

在程序里面一般编写对话框过程的分支结构如下:

    _ProcDlgMain     proc     uses ebx edi esi hWnd,wMsg,wParam,lParam
                mov      eax,wMsg
                .if      eax == WM_CLOSE
                        ;模态对话框用EndDialog关闭
                        ;非模态对话框用DestroyWindow关闭
                .elseif eax == WM_INITDIALOG
                        ;初始化代码
                .elseif eax == WM_COMMAND
                        ;子窗口控件发送的消息
                        ;wParam的低16位为子窗口控件ID
                .elseif eax == WM_XXXX
                        ;处理其他需要处理的消息
                .else
                        mov      eax,FALSE
                        ret
                .endif
                mov      eax,TRUE
                ret
    _ProcDlgMain     endp

注意对话框过程和普通的窗口过程在使用上有以下区别:

● 窗口过程对应于不同的消息有各种不同含义的返回值,而对话框过程返回BOOL类型的值,返回TRUE表示已经处理了某条消息,返回FALSE表示没有处理。“对话框管理器”代码会根据返回值决定是否继续处理某一条消息(唯一的例外是WM_INITDIALOG消息)。

● 对于不处理的消息,不需要...调用DefWindowProc来处理,这事情由“对话框管理器”来做。

“对话框管理器”不会把WM_CREATE消息转发给对话框过程,取而代之,它会以WM_INITDIALOG消息来调用对话框过程,程序可以在这里进行一些初始化的操作,WM_INITDIALOG消息的返回值有点特殊,如果程序想自行设置输入焦点,那么可以用SetFocus函数把输入焦点设置到需要的子窗口控件上,然后返回FALSE;如果返回TRUE的话,那么Windows会自动将输入焦点设置到第一个有WS_TABSTOP的子窗口控件上。

对话框过程在WM_COMMAND消息中处理子窗口控件发送的命令,当用户在对话框中按下了按钮,输入文字或选择复选框等操作时,子窗口控件会向对话框过程发送WM_COMMAND消息,wParam是子窗口控件的ID,如例子程序中处理“退出”按钮的消息,在里面用EndDialog函数关闭对话框。

对话框窗口的标题栏上默认没有定义图标,如果要像普通窗口一样显示一个图标,那么可以像例子程序中那样,在WM_INITDIALOG中用WM_SETICON消息来设置。

5.4.4 在对话框中使用子窗口控件

子窗口控件是一些Windows预定义类,它们实际上就是一个个以对话框为父窗口的子窗口。对于程序员来说,在对话框中使用它们的时候并不需要手工去逐一创建,只需要在对话框中定义就可以了,“对话框管理器”会在初始化对话框的时候,根据定义语句自动建立所有的子窗口。

1.子窗口控件的定义

子窗口控件定义的一般语法是:

CONTROL文本,ID,类,风格, x, y,宽度,高度[,扩展风格]

“文本”指控件的初始化值,“ID”是子窗口向对话框过程发送WM_COMMAND中用的ID值,“类”可以是按钮(Button)、静态(Static)、编辑(Edit)、滚动条(ScrollBar)、列表框(ListBox)和组合框(ComboBox),这些类都是Windows系统中已经预定义的,“对话框管理器”在初始化的时候把每一条控件定义语句转换成下面的CreateWindow命令:

invoke  CreateWindow,类名,文本,风格,\
                    x,y,宽度,高度,\
                    对话框窗口句柄,ID,hInstance,NULL

正因为如此,所有可以用CreateWindow建立的子窗口都可以在资源中定义,只要知道要使用的类和风格就可以了。所以除了上面这些基本的类之外,对话框中还可以使用一些通用控件,如“日期”(SysDateTimePick32)、“月历”(SysMonthCal32)、“热键”(msctls_hotkey32)和“列表”(SysListView32)等,括号内是它们的类名,只要把定义语句的“类”写成对应的名称就可以了。

基于同一个预定义类的控件根据风格属性的不同,外表可能完全不同,如单选钮、复选框和分组框使用的类都是Button类,文本、图标框、位图框和线条等都是Static类。使用CONTROL语句定义的时候可能不是很直观,所以Rc.exe资源编译器允许使用另一种语法来书写控件定义:

控件名称 [文本,] ID,x,y,宽度,高度[,风格][,扩展风格]

这里使用“控件名称”而不是“类”是因为这个名称只是Rc.exe使用的缩写,并不是真正的Windows类的名称,“控件名称”由Rc.exe解释成“类”名,同时为它使用了几种默认的风格,定义语句中风格属性实际上是附加在默认风格上的,表5.4列出了每种控件使用的类和默认属性,除了表中列出的默认属性外,每种控件还被默认定义了WS_CHILD和WS_VISIBLE属性。

                                            表5.4 资源脚本中使用的控件名称

看下面的例子:

GROUPBOX "选项", -1, 55, 5, 120, 100
PUSHBUTTON "退出", IDCANCEL, 255, 115, 50, 14

这两条语句和下面的语句编译后产生的二进制资源文件是一模一样的:

CONTROL "选项", -1, "Button", BS_GROUPBOX | WS_TABSTOP, 55, 5, 120, 100
CONTROL "退出", IDCANCEL, "Button", BS_PUSHBUTTON | WS_TABSTOP, 255, 115, 50, 14

第一种语句的用法比第二种语句不但要直观许多,而且不必书写默认的窗口风格。

当用到的控件没有缩写语法时,那就必须用CONTROL定义了,下面的两句分别定义了一条横线和一个图片框,它们并没有缩写的用法:

CONTROL "", -1, "Static", SS_ETCHEDHORZ | WS_CHILD | WS_VISIBLE, 60, 65, 110, 1
CONTROL BMP_ID, -1, "Static", SS_BITMAP | WS_CHILD | WS_VISIBLE, 5, 5, 40, 95

下面以一个例子来演示各种子窗口控件的用法,读者可以在所附光盘的Chapter05\Control目录中找到全部的源代码,其中的Control.rc文件如下:

//>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
#include		<resource.h>
//>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
#define	ICO_MAIN		0x1000	//图标
#define	DLG_MAIN		1
#define IDB_1			1
#define IDB_2			2
#define IDC_ONTOP		101
#define IDC_SHOWBMP		102
#define IDC_ALOW		103
#define IDC_MODALFRAME	104
#define IDC_THICKFRAME	105
#define IDC_TITLETEXT	106
#define IDC_CUSTOMTEXT	107
#define IDC_BMP			108
#define IDC_SCROLL		109
#define	IDC_VALUE		110
//>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
ICO_MAIN	ICON		"Main.ico"
IDB_1		BITMAP		"Picture1.bmp"
IDB_2		BITMAP		"Picture2.bmp"
//>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
DLG_MAIN DIALOG 193, 180, 310, 134
STYLE DS_MODALFRAME | WS_POPUP | WS_VISIBLE | WS_CAPTION | WS_SYSMENU | WS_THICKFRAME
CAPTION "对话框子窗口控制示例"
FONT 9, "宋体"
{
 GROUPBOX "选项", -1, 55, 5, 120, 100
 AUTOCHECKBOX "总在最前面", IDC_ONTOP, 65, 20, 100, 12
 AUTOCHECKBOX "显示图片", IDC_SHOWBMP, 65, 35, 100, 12
 AUTOCHECKBOX "允许更换图片", IDC_ALOW, 65, 50, 100, 12
 CONTROL "", -1, "Static", SS_ETCHEDHORZ | WS_CHILD | WS_VISIBLE, 60, 65, 110, 1
 AUTORADIOBUTTON "模态边框(&Modal Frame)", IDC_MODALFRAME, 65, 70, 100, 12, WS_TABSTOP
 AUTORADIOBUTTON "可变边框(&Thick Frame)", IDC_THICKFRAME, 65, 85, 100, 12, WS_TABSTOP
 GROUPBOX "标题栏文字", -1, 180, 5, 125, 100, BS_GROUPBOX
 COMBOBOX IDC_TITLETEXT, 190, 20, 105, 70, CBS_DROPDOWNLIST | WS_TABSTOP
 LTEXT "自定义文字:", -1, 190, 40, 105, 10
 EDITTEXT IDC_CUSTOMTEXT, 190, 55, 105, 12
 LTEXT "请在此选择显示在标题栏上面的文字,或者选择“自定义”后自行输入", -1, 191, 73, 105, 26, WS_BORDER
 CONTROL "", -1, "Static", SS_ETCHEDHORZ | WS_CHILD | WS_VISIBLE, 5, 110, 300, 1
 DEFPUSHBUTTON "更换图片(&C)", IDOK, 200, 115, 50, 14
 PUSHBUTTON "退出(&X)", IDCANCEL, 255, 115, 50, 14
 CONTROL IDB_1, IDC_BMP, "Static", SS_BITMAP | WS_CHILD | WS_VISIBLE, 5, 5, 40, 95
 SCROLLBAR IDC_SCROLL, 6, 118, 125, 10
 LTEXT "0", IDC_VALUE, 138, 119, 50, 8
}
//>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>

编译后的Control.exe运行后的界面如图5.6所示。

                                          图5.6 子窗口控件使用的例子

图5.6中的子窗口控件分别是GROUPBOX①,AUTOCHECKBOX②,SS_ETCHEDHORZ风格的Static类③,AUTORADIOBUTTON④,COMBOBOX⑤,EDITTEXT⑥,WS_BORDER风格LTEXT⑦,DEFPUSHBUTTON⑧,SS_BITMAP风格的Static类⑨和滚动条⑩。

程序有这些功能:按下“更换图片”按钮⑧可以切换图片框⑨的图片;在组合框⑤中可以选择更换标题栏的文字,选“自定义”的时候可以激活文本编辑框⑥并输入自定义文字;默认状态下对话框是可以调整大小的,如果将单选钮④切换到“模态边框”,那么大小就无法调整。Control.asm源代码如下:

;Control.asm            对话框资源中子窗口控件的使用方法
;使用 nmake 或下列命令进行编译和链接:
;ml /c /coff Control.asm
;rc Control.rc
;Link /subsystem:windows Control.obj Control.res

.386
.model flat, stdcall 
option casemap:none 
;include 文件定义 
include		C:/masm32/include/windows.inc 
include 	C:/masm32/include/user32.inc 
includelib 	C:/masm32/lib/user32.lib 
include		C:/masm32/include/kernel32.inc 
includelib 	C:/masm32/lib/kernel32.lib 
include		C:/masm32/include/gdi32.inc 
includelib 	C:/masm32/lib/gdi32.lib 

;Equ 等值定义
ICO_MAIN	equ 1000h
DLG_MAIN 	equ 1 
IDB_1		equ 1 
IDB_2 		equ 2 
IDC_ONTOP 	equ 101 
IDC_SHOWBMP equ 102 
IDC_ALOW	equ 103
IDC_MODALFRAME equ 104
IDC_THICKFRAME equ 105 
IDC_TITLETEXT  equ 106 
IDC_CUSTOMTEXT equ 107
IDC_BMP 	   equ 108
IDC_SCROLL 	   equ 109
IDC_VALUE 	   equ 110

;数据段
.data?
hInstance 	dword ?
hBmp1		dword ?
hBmp2		dword ?
dwPos 		dword ?
.const 
szText1 	byte 'Hello, World!', 0
szText2 	byte '嘿,你看到标题栏变了吗?', 0
szText3		byte '自定义', 0

;代码段
.code 
_ProcDlgMain proc uses ebx edi esi, hWnd, wMsg, wParam, lParam 
	local @szBuffer[128]:byte 
	mov eax, wMsg 
	.if eax == WM_CLOSE 
		invoke EndDialog, hWnd, NULL 
		invoke DeleteObject, hBmp1 
		invoke DeleteObject, hBmp2
	.elseif eax == WM_INITDIALOG 
		;设置标题栏图标
		invoke LoadIcon,hInstance, ICO_MAIN 
		invoke SendMessage, hWnd, WM_SETICON, ICON_BIG, eax 
		;初始化组合框
		invoke SendDlgItemMessage, hWnd, IDC_TITLETEXT, CB_ADDSTRING, 0, addr szText1
		invoke SendDlgItemMessage, hWnd, IDC_TITLETEXT, CB_ADDSTRING, 0, addr szText2
		invoke SendDlgItemMessage, hWnd, IDC_TITLETEXT, CB_ADDSTRING, 0, addr szText3
		invoke SendDlgItemMessage, hWnd, IDC_TITLETEXT, CB_SETCURSEL, 0, 0
		invoke GetDlgItem, hWnd, IDC_CUSTOMTEXT 
		invoke EnableWindow, eax, FALSE 
		
		invoke LoadBitmap, hInstance, IDB_1 
		mov hBmp1,eax 
		invoke LoadBitmap, hInstance, IDB_2 
		mov hBmp2, eax 
		;初始化单选钮和复选框
		invoke CheckDlgButton, hWnd, IDC_SHOWBMP, BST_CHECKED 
		invoke CheckDlgButton, hWnd, IDC_ALOW, BST_CHECKED 
		invoke CheckDlgButton, hWnd, IDC_THICKFRAME, BST_CHECKED 
		;初始化滚动条
		invoke SendDlgItemMessage, hWnd, IDC_SCROLL, SBM_SETRANGE, 0, 100
	.elseif eax == WM_COMMAND 
		mov eax, wParam 
		.if ax == IDCANCEL 
			invoke EndDialog, hWnd, NULL 
			invoke DeleteObject, hBmp1 
			invoke DeleteObject, hBmp2 
		;更换图片
		.elseif ax == IDOK 
			mov eax, hBmp1 
			xchg eax, hBmp2 
			mov hBmp1, eax 
			invoke SendDlgItemMessage, hWnd, IDC_BMP, STM_SETIMAGE, IMAGE_BITMAP, eax 
		;设置是否“总在最前面”
		.elseif ax == IDC_ONTOP 
			invoke IsDlgButtonChecked, hWnd, IDC_ONTOP 
			.if eax == BST_CHECKED 
				invoke SetWindowPos, hWnd, HWND_TOPMOST, 0, 0, 0, 0,\
				SWP_NOMOVE or SWP_NOSIZE 
			.else 
				invoke SetWindowPos, hWnd, HWND_NOTOPMOST, 0, 0, 0, 0, \
				SWP_NOMOVE or SWP_NOSIZE
			.endif 
		;演示隐藏和显示图片控件
		.elseif ax == IDC_SHOWBMP 
			invoke GetDlgItem, hWnd, IDC_BMP 
			mov ebx, eax 
			invoke IsWindowVisible, ebx 
			.if eax 
				invoke ShowWindow, ebx, SW_HIDE 
			.else 
				invoke ShowWindow, ebx, SW_SHOW 
			.endif 
		;演示允许和灰化“更换图片”按钮
		.elseif ax == IDC_ALOW 
			invoke IsDlgButtonChecked, hWnd, IDC_ALOW 
			.if eax == BST_CHECKED 
				mov ebx, TRUE 
			.else 
				xor ebx, ebx 
			.endif 
			invoke GetDlgItem, hWnd, IDOK 
			invoke EnableWindow, eax, ebx 
		.elseif ax == IDC_MODALFRAME 
			invoke GetWindowLong, hWnd, GWL_STYLE 
			and eax, not WS_THICKFRAME 
			invoke SetWindowLong, hWnd, GWL_STYLE, eax 
		.elseif ax == IDC_THICKFRAME
			invoke GetWindowLong, hWnd, GWL_STYLE
			or eax, WS_THICKFRAME
			invoke SetWindowLong, hWnd, GWL_STYLE, eax 
		;演示处理下拉式组合框
		.elseif ax == IDC_TITLETEXT 
			shr eax, 16 
			.if ax == CBN_SELENDOK 
				invoke SendDlgItemMessage, hWnd, IDC_TITLETEXT, CB_GETCURSEL, 0, 0
				.if eax == 2 
					invoke GetDlgItem, hWnd, IDC_CUSTOMTEXT 
					invoke EnableWindow, eax, TRUE 
				.else 
					mov ebx, eax 
					invoke SendDlgItemMessage, hWnd, IDC_TITLETEXT, CB_GETLBTEXT, ebx, addr @szBuffer 
					invoke SetWindowText, hWnd, addr @szBuffer 
					invoke GetDlgItem, hWnd, IDC_CUSTOMTEXT 
					invoke EnableWindow, eax, FALSE 
				.endif 
			.endif 
		;在文本框中输入文字
		.elseif ax == IDC_CUSTOMTEXT 
			invoke GetDlgItemText, hWnd, IDC_CUSTOMTEXT, addr @szBuffer, sizeof @szBuffer 
			invoke SetWindowText, hWnd, addr @szBuffer 
		.endif 
	;处理滚动条消息
	.elseif eax == WM_HSCROLL 
		mov eax, wParam 
		.if ax == SB_LINELEFT 
			dec dwPos 
		.elseif ax == SB_LINERIGHT 
			inc dwPos 
		.elseif ax == SB_PAGELEFT
			sub dwPos, 10
		.elseif ax == SB_PAGERIGHT
			add dwPos, 10
		.elseif ax == SB_THUMBPOSITION || ax == SB_THUMBTRACK 
			mov eax, wParam 
			shr eax, 16
			mov dwPos, eax 
		.else 
			mov eax, TRUE 
			ret
		.endif 
		cmp dwPos, 0
		jge @F 
		mov dwPos, 0
		@@:
		cmp dwPos, 100
		jle @F 
		mov dwPos, 100 
		@@:
		invoke SetDlgItemInt, hWnd, IDC_VALUE, dwPos, FALSE 
		invoke SendDlgItemMessage, hWnd, IDC_SCROLL, SBM_SETPOS, dwPos, TRUE 
	.else 
		mov eax, FALSE 
		ret 
	.endif 
	mov eax, TRUE 
	ret 
_ProcDlgMain endp  

main proc 
	invoke GetModuleHandle, NULL 
	mov hInstance, eax 
	invoke DialogBoxParam, hInstance, DLG_MAIN, NULL, offset _ProcDlgMain, NULL 
	invoke ExitProcess, NULL 
main endp 
end main 

编译运行:

2.子窗口控件的通用使用方法

由于子窗口控件实际上就是窗口,大部分窗口函数对它们都是适用的,如可以用EnableWindow在灰化和允许状态之间切换,可以用ShowWindow在显示和隐藏之间切换,可以用GetWindowText和SetWindowText来改变上面的文字,也可以用MoveWindow来改变大小和移动位置等。在Control.asm中用“显示图片”复选框切换图片框的隐藏和显示,用的就是ShowWindow函数,处理“允许更换图片”复选框时切换“更换图片”按钮的状态,用的是EnableWindow函数。

除了可以用对子窗口控件使用窗口的通用函数外,还可以使用针对它们的专用函数。下面介绍一些常用的函数。

在资源脚本文件中定义的是控件的ID,当这些子窗口控件被创建以后同样会有一个窗口句柄,但既然它们不是由我们自己创建的,那么怎么知道它们的窗口句柄呢?有一个函数可以从ID中获取子窗口句柄:

invoke   GetDlgItem,hDlg,dwIDDlgItem
mov      hDlgItem,eax

函数的输入参数是对话框句柄和ID值,返回值是子窗口句柄;反过来,有两种方法可以从子窗口句柄获取ID:

(1) invoke  GetDlgCtrlID,hWndCtrl    ;输入子窗口句柄,返回值是控件ID
(2) invoke  GetWindowLong,hWndCtrl,GWL_ID

当需要向控件发送消息的时候,当然可以先用GetDlgItem获取子窗口句柄再用SendMessage函数,但有一个函数更为简便:

invoke   SendDlgItemMessage,hDlg,dwIDDlgItem,Msg,wParam,lParam

这个函数可以直接向控件发送消息,只需要在参数中指定对话框句柄和子窗口ID。(注意:并没有PostDlgItemMessage这样的函数!)

如果要想知道在一个控件上按下了Tab键或Shift+Tab键会跳到哪一个控件上去,也就是说下一个或上一个Tab停留位在哪里,可以使用GetNextDlgTabItem函数:

invoke   GetNextDlgTabItem,hDlg,hCtl,bPrevious
.if eax
      mov hWinNext,eax
.endif

其中的bPrevious参数指定了搜索的方向;与之相似,使用GetNextDlgGroupItem函数可以返回下一个分组的位置:

invoke   GetNextDlgGroupItem,hDlg,hCtl,bPrevious
.if      eax
      mov hWinNext,eax
.endif

3.使用单选钮和复选框

单选钮是互斥的选择钮,同一组的多个单选钮只能有一个被选中,单选钮的外形是一个圆形的标记加上文本,圆形中有黑点表示被选中。复选框不是互斥的,多个复选框的状态不会互相影响,复选框的外形是一个方框加上文本,方框中可以用有无打钩来表示是否被选中。

单选钮和复选框控件都是基于Button类的,只不过它们的窗口风格分别是BS_RADIOBUTTON和BS_CHECKBOX。既然它们是特殊的“按钮”,所以和它们有关的函数都带有“Button”一词,查看一个单选钮或复选框是否被选中可以用下面的函数来检测:

invoke   IsDlgButtonChecked,hDlg,nIDButton

函数的返回值可能是BST_CHECKED(选中状态),BST_INDETERMINATE(三态复选框的灰化状态)或BST_UNCHECKED(未选中状态)。也可以用向子窗口控件发送BM_GETCHECK消息的方法来检测,返回值和上面的函数是一样的。

如果想设置单选钮或复选框的状态,可以使用下面的语句:

invoke   CheckDlgButton,hDlg,nIDButton,uCheck

参数uCheck用BST_CHECKED,BST_INDETERMINATE或BST_UNCHECKED来表示需要设置的状态,含义同上。向控件发送BM_SETCHECK消息也可以取得同样的效果,这时消息的wParam中放置需要设置的状态。

因为复选框是不互斥的,所以可以随意设置状态。而对于BS_RADIOBUTTON风格的单选钮来说,并不是把某个按钮设置为选中状态以后,同组的其他按钮就会自动变成非选中状态,所以用CheckDlgButton函数选中了一个单选钮以后,如果不是手动把同组的其他按钮全部改为非选中状态(逐个地调用CheckDlgButton),就会看到同时有两个单选钮是选中的。但由于把同组的所有单选钮逐个地设置显得有点麻烦,所以针对单选钮有一个专用函数:

invoke   CheckRadioButton,hDlg,\
            nIDFirstButton,nIDLastButton,nIDCheckButton

这个函数把ID在nIDFirstButton和nIDLastButton之间的单选钮全部设置为非选中状态,只有nIDCheckButton是选中状态,当然在使用中要注意将这一批ID定义为连续的数值。

如果还嫌CheckRadioButton有点麻烦,还有一种最简单的办法——使用自动单选钮,同组的AUTORADIOBUTTON会随着用户选中一个而自动清除其他单选钮的状态,所以在程序中只需要在初始化的时候预设一次,其他时间就可以不必关心设置问题了,以后唯一用到的就是调用IsDlgButtonChecked检查状态了。

4.使用静态控件

静态控件是基于Static类的子窗口控件,之所以叫“静态”控件,是因为它是“安静”的——它们不向对话框发送WM_COMMAND消息,所以静态控件的ID一般是没有用处的,定义时常常将它们定为-1,如果需要在程序中改变静态控件的属性,那么也可以为静态控件指定一个唯一的ID以便操作。

资源脚本文件中可以使用缩写的基于Static类的有LTEXT,CTEXT,RTEXT(这是3种对齐方向不同的文本框)和ICON(图标框),除了这些常用的类型之外,Static类还可以用CONTROL语句通过指定不同的窗口风格派生出不同用途的控件来。

下面说明静态控件的一些用法。

对于文本框,文本长度超过边界的时候默认是自动换行的,但如果同时指定SS_SIMPLE风格的话,就不会自动换行。读者可以在程序中用SetWindowText或发送WM_SETTEXT消息来动态改变显示的文本,同样,也可以用GetWindowText或发送WM_GETTEXT消息来获取其中的文本。

静态控件可以用来构筑简单的线条和图形,如果指定SS_BLACKFRAME,SS_GRAYFRAME或SS_WHITEFRAME风格,那么静态控件显示为填充的矩形,填充颜色分别是黑色、灰色或白色;而指定SS_BLACKRECT,SS_GRAYRECT或SS_WHITERECT风格的话,则显示为非填充的矩形框,边线颜色是黑色、灰色或白色。

静态控件也可以用来做立体感的线条或边框,指定SS_ETCHEDHORZ风格显示为横线,指定SS_ETCHEDVERT风格显示为竖线,指定SS_ETCHEDFRAME风格则显示为立体的矩形框,视觉上的效果类似于没有文字的GROUPBOX。

静态控件还有一个用途是做图形显示,当图形是图标的时候,用ICON语句就可以定义了,其默认的风格是SS_ICON,如果想使用位图,那么可以指定SS_BITMAP风格,例子程序中的图片框就是这样定义的:

CONTROL IDB_1, IDC_BMP, "Static", SS_BITMAP | WS_CHILD | WS_VISIBLE, 5, 5, 40, 95

在这里,“文字”部分指定位图资源的ID,前面已经把Picture1.bmp的资源ID定义为IDB_1,IDC_BMP是图片框自己的ID,如果不需要在程序中改变图片的话,那么这里可以定义为-1。

在程序中可以通过向控件发送STM_SETIMAGE消息来设置新的图片,消息的wParam指定图片的格式,取值可以是IMAGE_BITMAP,IMAGE_CURSOR和IMAGE_ICON,分别对应新图片的格式,lParam是图片的句柄,如果是位图,lParam就是用LoadBitmap装入的位图句柄,同样,图片类型是光标和图标的时候,这里就是用LoadCursor和LoadIcon装入的句柄。

在例子程序中,用来改变图片框图片的语句是:

invoke SendDlgItemMessage,hWnd,IDC_BMP,STM_SETIMAGE,IMAGE_BITMAP,hBmp

hBmp是用LoadBitmap装入的位图句柄,IDC_BMP是图片框的ID,wParam参数用IMAGE_BITMAP表示要设置的图片类型是位图。

5.使用文本编辑控件

文本编辑控件是基于Edit类的控件,可以用缩写EDITTEXT定义,读者可以在文本编辑控件中输入并编辑文本。每当用户在文本编辑控件中输入一个字符的时候,控件就会向对话框过程发送一个WM_COMMAND消息,所以在例子程序中,当在自定义文字的编辑框中每输入一个字,标题栏文字就会马上改变。

要获取编辑框中的文本有多种方法,可以用GetWindowText,也可以用发送WM_GETTEXT消息的办法,要设置文本,同样可以用SetWindowText或发送WM_SETTEXT,但最简便的办法还是使用下面的函数:

invoke  GetDlgItemText,hDlg,nIDDlgItem,lpString,nMaxCount   	  ;取文本
invoke  SetDlgItemText,hDlg,nIDDlgItem,lpString                   ;设置文本

lpString是放置字符的缓冲区地址,用GetDlgItemText函数来获取文本的时候,要用nMaxCount参数指定缓冲区的最大长度,以免获取的文本长度超过缓冲区长度引起溢出,设置的时候若使用SetDlgItemText函数时就不需要这个参数。

在实际使用中,经常要在文本编辑控件中输入输出数值型参数,将文本转换为数值比较麻烦,把数值转换为文本也要经过一个wsprintf调用,为了简化操作,Windows提供了两个函数来处理这个问题:

invoke  SetDlgItemInt,hDlg,nIDDlgItem,uValue,bSigned     		;设置控件中的数值
invoke  GetDlgItemInt,hDlg,nIDDlgItem,lpTranslated,bSigned 	    ;取控件中的数值

SetDlgItemInt函数将uValue参数先转换成字符串格式,然后设置到文本编辑控件中,bSigned参数指定了uValue的格式,如果是TRUE的话,表示uValue是符号数;是FALSE的话,表示uValue是无符号数。

GetDlgItemInt函数则将对话框中的文本转换成数值型返回,同样,用bSigned指定转换的方式,TRUE表示按照符号数格式转换,这时函数会检测文本的第一个字符是不是负号;FALSE则按照无符号数转换。参数lpTranslated是指向一个dword型变量的指针,GetDlgItemInt会在这个变量中返回BOOL类型值表示函数是否调用成功,成功则返回TRUE,有这样一个参数的原因是函数的返回值用来返回转换后的数值了,以至于没有地方可以表示函数是否执行成功。当然,lpTranslated参数也可以输入NULL,这样,当函数返回0的时候就无法知道是文本框是“0”还是文本不符合格式造成转换失败。

SetDlgItemInt和GetDlgItemInt函数不仅适用于文本编辑控件,所有对其上面的文本可以修改的控件都可以使用它们。

使用文本编辑控件的时候,文本的长度也是个需要注意的问题。如果控件的宽度定义得过窄,当字符填充到最右边的时候,编辑框就不允许继续输入了,为了继续输入并让文本自动卷动,可以指定WS_HSCROLL风格;反之,定义WS_HSCROLL风格后输入文本的长度不受限制又不好,那么可以用向控件发送EM_LIMITTEXT消息的方法来设定最大长度。下面的例子将IDC_EDIT的输入最大长度定为10个字符:

invoke SendDlgItemMessage,hDlg,IDC_EDIT,EM_LIMITTEXT,10,NULL

另外,有时候可能需要把编辑框设置为只读的(和灰化不同,灰化的编辑框中文本无法进行任何操作,包括卷动操作,而只读的仅仅是不能修改),要把初始状态定义为只读的,只需在定义语句中加上ES_READONLY风格,在程序中需要动态改变只读状态可以发送EM_SETREADONLY消息,下面的第一句把编辑框设为只读,第二句把编辑框改回到可写状态:

invoke SendDlgItemMessage,hDlg,IDC_EDIT,EM_SETREADONLY,TRUE,NULL     	;只读
invoke SendDlgItemMessage,hDlg,IDC_EDIT,EM_SETREADONLY,FALSE,NULL 		;可写

文本编辑框在默认状态下是单行的,也可以通过加上ES_MULTILINE风格变成多行的,这时可以同时加上WS_VSCROLL风格显示一个垂直方向的滚动条。

6.使用滚动条

滚动条有水平和垂直两种,默认的SCROLLBAR语句定义的是水平的滚动条,它的默认风格是SBS_HORZ,例子程序中用下面的语句定义了一个水平滚动条:

SCROLLBAR IDC_SCROLL, 6, 118, 125, 10

如果要定义垂直的滚动条,那么要指明SBS_VERT风格:

SCROLLBAR IDC_SCROLL, x, y, 宽度, 高度,SBS_VERT

和其他子窗口控件发送WM_COMMAND消息不同,水平滚动条向对话框窗口发送WM_HSCROLL消息,而垂直滚动条则发送WM_VSCROLL消息,所以针对两种方式的滚动条要分别处理不同的消息。

WM_xSCROLL消息的参数如下所示:

wParam的低16位 = nScrollCode     	;动作码
wParam的高16位 = nPos             	;滚动条当前位置
lParam  = hwndScrollBar             ;滚动条控件的窗口句柄

其中nScrollCode代表了滚动条的当前动作,定义值及其含义如下:

● SB_BOTTOM——滚动条移到了最下/右边。

● SB_ENDSCROLL——用户停止了滚动动作。

● SB_THUMBPOSITION——滚动条被拖动到某处。

● SB_THUMBTRACK——滚动条在拖动中。

● SB_TOP——滚动条移到了最上/左边。

● SB_LINELEFT——滚动条左移了一格(对于水平滚动条)。

● SB_LINERIGHT——滚动条右移了一格(对于水平滚动条)。

● SB_PAGELEFT——滚动条左移了一页(对于水平滚动条)。

● SB_LINEDOWN——滚动条下移了一格(对于垂直滚动条)。

● SB_LINEUP——滚动条上移了一格(对于垂直滚动条)。

● SB_PAGEDOWN——滚动条下移了一页(对于垂直滚动条)。

● SB_PAGEUP——滚动条上移了一页(对于垂直滚动条)。

nPos的值只有当动作码是SB_THUMBPOSITION或SB_THUMBTRACK时才有效,其他的时候为0,图5.7示出了鼠标点击滚动条各处时对应的nScrollCode。

第一眼看到SB_xxx动作码的时候,读者可能会以为水平滚动条和垂直滚动条的动作码是不相同的——水平滚动条是SB_xxxLEFT、SB_xxxRIGHT,而垂直滚动条是SB_xxxUP、SB_xxxDOWN,但在Windows.inc中查看一下就可以发现,SB_xxxLEFT和SB_xxxUP在数值上是相同的,SB_xxxRIGHT和SB_xxxDOWN也是如此,所以不同定义方法只是为了直观起见而已。

以水平滚动条为例,处理滚动条消息的代码一般是如下结构:

    .elseif eax == WM_HSCROLL        ;窗口的消息处理分支,eax为wMsg
          mov      eax, lParam
          .if      eax == hWnd滚动条1
                            mov      eax,wParam
                            .if      ax ==   SB_LINELEFT
                          dec       位置变量
                            .elseif ax ==   SB_LINERIGHT
                          inc       位置变量
                            .elseif ax ==   SB_PAGELEFT
                          sub       位置变量,页长
                            .elseif ax ==   SB_PAGERIGHT
                          add       位置变量,页长
                            .elseif ax ==   SB_THUMBPOSITION || ax == SB_THUMBTRACK
                          mov       eax,wParam
                          shr       eax,16
                          mov       位置变量,eax
                            .endif
          .elseif eax == hWnd滚动条2
                            ;处理滚动条2的代码,同上面的结构
                            ...
          .endif

                                        图5.7 鼠标点击滚动条各处时产生的nScrollCode

由于在例子程序Control.asm中只定义了一个滚动条,所有的消息肯定都是它发出的,所以去掉了判断lParam是哪个滚动条的步骤直接处理wParam中的动作码。

在用户按动滚动条后,滚动条不会自己移动位置,它只是将用户的动作以WM_xSCROLL消息的形式反馈给程序,真正要移动它还是要靠程序来设置,所以代码中要根据不同的动作首先计算新的位置,并判断新的位置是否越界,例子程序中的这些代码判断新的位置是否超出0~100的范围,如果是,则校正到0~100之间:

    cmp      dwPos,0
    jge      @F
    mov      dwPos,0
    @@:
    cmp      dwPos,100
    jle      @F
    mov      dwPos,100

在介绍MASM语句的时候提到过,因为.if dwPos > 0语句只可以用来比较无符号数,所以在这里使用cmp指令自己构建测试分支而不是使用 .if伪指令。

当计算好新位置的时候要将位置设置回去,用户才会看到滚动条移动了,方法是向滚动条发送SBM_SETPOS消息:

invoke   SendDlgItemMessage,hWnd,IDC_SCROLL,SBM_SETPOS,dwPos,TRUE

最后一个参数为TRUE表示设置后重新绘画滚动条。

在初始化的时候,要给滚动条发送SBM_SETRANGE消息来设定滚动范围:

invoke  SendDlgItemMessage,hWnd,IDC_SCROLL,SBM_SETRANGE,最小值,最大值

如果需要获取滚动条的信息,可以尝试发送下面两个消息:SBM_GETPOS可以获取滚动条的当前位置,也就是上一次用SBM_SETPOS设置的值;SBM_GETRANGE可以获取滚动的范围,也就是用SBM_SETRANGE设置的值。

7.使用组合框

顾名思义,组合框是一个“组合”起来的东西,它由一个可供选择的列表和一个可供输入的edit类组合而成。组合框让用户既可以自己输入文本,也可以选择列表中的某一项当做输入。用不同的风格定义可以产生3种类型的组合框,如图5.8所示。左边的是CBS_SIMPLE风格的组合框,它的上面可以输入文本,下面的列表可供选择预设文本;中间的是CBS_DROPDOWN风格的组合框,上面同样可以输入文本,但下面的列表是下拉式的,平时处于收起状态,点击编辑框右边的三角形才会拉下来;右边的是CBS_DROPDOWNLIST风格的组合框,它仅是一个下拉的选择框,上面的框中不允许输入文字。

                                              图5.8 组合框的3种风格

组合框中还有几种常用的、可以附加的风格:

● CBS_AUTOHSCROLL——输入过长的文本时输入框自动卷动。

● CBS_LOWERCASE——自动将所有的文本转换成小写。

● CBS_SORT——自动将插入的文本项排序。

● CBS_UPPERCASE——自动将所有的文本转换成大写。

组合框中列表框部分的文字添加、项目的选择等操作都是通过发送消息来完成的,主要的消息如表5.5所示。

当用户在组合框中进行选择操作时,Windows向对话框过程发送WM_COMMAND消息,消息中wParam参数的低16位是组合框ID,高16位是通知码,用来表示用户的操作,通知码的定义如表5.6所示。

                                                        表5.5 组合框的消息

                                               表5.6 用户操作组合框后的通知码

如果想在用户选择了一个项目后做相应的动作,最好的办法就是处理CBN_SELENDOK通知码,因为这才意味着用户真正完成了一个选择动作,例子程序中就是这样处理的:

.elseif ax ==   IDC_TITLETEXT   ;在WM_COMMAND消息中
                    shr      eax,16
                    .if      ax ==    CBN_SELENDOK
                            invoke  SendDlgItemMessage,hWnd,IDC_TITLETEXT,\
                                    CB_GETCURSEL,0,0
                            ;根据返回的eax值做相应动作…
                    .endif

以上的操作都是针对下拉列表部分的,另外也有很多消息是针对组合框中的编辑控件的,对组合框的窗口句柄发送WM_GETTEXT和WM_SETTEXT,操作的对象就是组合框的编辑控件;如果要限制编辑控件中文本的最大输入长度,可以发送CB_LIMITTEXT消息,这时候wParam参数指定最大数量;当用户在编辑框中编辑文本的时候,Windows在用户输入之后、字符显示之前会发送CBN_EDITUPDATE通知码;当字符在编辑框中显示以后,又会发送CBN_EDITCHANGE通知码。所以在处理WM_COMMAND消息时通过处理这两个通知码可以检测到用户的输入操作。

组合框是子窗口控件中比较复杂的一种,这里介绍的是常用的消息和通知码,另外还有少量不常用的内容,读者可以自行查看相关的资料。

8.使用列表框列

表框提供一个可供用户选择的列表,用户可以一次选择一个项目,也可以同时选中多个项目,本节用一个单独的例子程序来说明列表框的用法,所有的源程序可以在所附光盘的Chapter05\Listbox目录中找到,包括Listbox.rc文件和Listbox.asm文件,其中Listbox.rc文件定义如下:

#include		<resource.h>
//>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
#define	ICO_MAIN		0x1000	//图标
#define	DLG_MAIN		1
#define IDC_LISTBOX1	101
#define IDC_LISTBOX2	102
#define	IDC_SEL1		103
#define IDC_RESET		104
//>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
ICO_MAIN	ICON		"Main.ico"
//>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
DLG_MAIN DIALOG 163, 160, 190, 108
STYLE DS_MODALFRAME | WS_POPUP | WS_VISIBLE | WS_CAPTION | WS_SYSMENU
CAPTION "列表框控件示例"
FONT 9, "宋体"
{
 LISTBOX IDC_LISTBOX1, 6, 5, 55, 86, LBS_STANDARD
 LISTBOX IDC_LISTBOX2, 68, 5, 115, 86, LBS_STANDARD | LBS_MULTIPLESEL
 LTEXT "", IDC_SEL1, 6, 93, 55, 8
 PUSHBUTTON "复位(&R)", IDC_RESET, 89, 90, 45, 14
 DEFPUSHBUTTON "查看(&S)", IDOK, 139, 90, 45, 14, WS_DISABLED
}

程序运行的效果如图5.9所示。程序中总共定义了两个列表框。左边列表框为IDC_LISTBOX1,这是一个单选的列表框,选择一个项目的时候下面的文本中会显示出选择的项目,双击某个项目的时候会弹出图中标有②的消息框。右边的列表框是IDC_LISTBOX2,是一个多选的列表框,选择完毕后可以用“查看”按钮弹出图中标有①的消息框,消息框中显示了所有选中的项目。读者也可以按下“复位”按钮清除列表框的选择。

                                          图5.9 列表框例子的运行结果

定义列表框时可以使用的风格如表5.7所示。

                                                  表5.7 列表框可以使用的风格

一般单选列表框只需定义LBS_STANDARD就可以了。

汇编源代码Listbox.asm如下所示:

;Listbox.asm          在对话框中使用列表框控件的例子
;使用 nmake 或下列命令进行编译和链接:
;ml /c /coff Listbox.asm
;rc Listbox.rc
;Link /subsystem:windows Listbox.obj Listbox.res
.386
.model flat, stdcall 
option casemap:none 

;include 文件定义 
include		C:/masm32/include/windows.inc 
include 	C:/masm32/include/user32.inc 
includelib 	C:/masm32/lib/user32.lib 
include 	C:/masm32/include/kernel32.inc 
includelib 	C:/masm32/lib/kernel32.lib 

;Equ 等值定义
ICO_MAIN 		equ 1000h 
DLG_MAIN 		equ 1 
IDC_LISTBOX1	equ 101
IDC_LISTBOX2	equ 102 
IDC_SEL1		equ 103 
IDC_RESET 		equ 104 

;数据段
.data?
hInstance 	dword ?
.const 
szText1		byte '项目1', 0
szText2 	byte '项目2', 0
szText3 	byte '项目3', 0
szPath 		byte '*.*', 0
szMessage	byte '选择结果:%s', 0
szTitle 	byte '您的选择', 0
szSelect 	byte '您选择了以下的项目:', 0
szReturn 	byte 0dh, 0ah, 0

;代码段
.code 
_ProcDlgMain proc uses ebx edi esi, hWnd, wMsg, wParam, lParam 
	local @szBuffer1[128]:byte 
	local @szBuffer2[128]:byte 
	local @szTextBuff[2048]:byte 
	local @dwCount 
	
	mov eax, wMsg 
	.if eax == WM_CLOSE 
		invoke EndDialog, hWnd, NULL 
	.elseif eax == WM_INITDIALOG 
		;设置标题栏图标
		invoke LoadIcon, hInstance, ICO_MAIN 
		invoke SendMessage, hWnd, WM_SETICON, ICON_BIG, eax 
		;初始化列表框
		invoke SendDlgItemMessage, hWnd, IDC_LISTBOX1, LB_ADDSTRING, 0, addr szText1 
		invoke SendDlgItemMessage, hWnd, IDC_LISTBOX1, LB_ADDSTRING, 0, addr szText2
		invoke SendDlgItemMessage, hWnd, IDC_LISTBOX1, LB_ADDSTRING, 0, addr szText3
		invoke SendDlgItemMessage, hWnd, IDC_LISTBOX2, LB_DIR, \
				DDL_ARCHIVE or DDL_DRIVES or DDL_DIRECTORY, addr szPath 
	.elseif eax == WM_COMMAND 
		mov eax, wParam 
		.if ax == IDOK 
			;取多选的列表框项目
			invoke SendDlgItemMessage, hWnd, IDC_LISTBOX2, LB_GETSELCOUNT, 0, 0
			mov @dwCount, eax 
			invoke SendDlgItemMessage, hWnd, IDC_LISTBOX2, LB_GETSELITEMS, 128/4, addr @szBuffer1 
			invoke lstrcpy, addr @szTextBuff, addr szSelect 
			lea esi, @szBuffer1 
			.while @dwCount
				lodsd 	;将由ESI寄存器指向的内存地址处4字节数据,加载到 EAX 寄存器中。EAX = DS:[ESI]
				lea ecx, @szBuffer2 
				invoke SendDlgItemMessage, hWnd, IDC_LISTBOX2, LB_GETTEXT, eax, ecx 
				invoke lstrcat, addr @szTextBuff, addr szReturn 
				invoke lstrcat, addr @szTextBuff, addr @szBuffer2
				dec @dwCount 
			.endw 
			invoke MessageBox, hWnd, addr @szTextBuff, addr szTitle, MB_OK 
		.elseif ax == IDC_RESET 
			invoke SendDlgItemMessage, hWnd, IDC_LISTBOX2, LB_SETSEL, FALSE, -1 
		.elseif ax == IDC_LISTBOX1 
			shr eax, 16 
			.if ax == LBN_SELCHANGE 
				;将鼠标点击结果显示在文本框中
				invoke SendMessage, lParam, LB_GETCURSEL, 0, 0
				lea ecx, @szBuffer1 
				invoke SendMessage, lParam, LB_GETTEXT, eax, ecx 
				invoke SetDlgItemText, hWnd, IDC_SEL1, addr @szBuffer1 
			.elseif ax == LBN_DBLCLK 
				;双击项目则弹出对话框
				invoke SendMessage, lParam, LB_GETCURSEL, 0, 0
				lea ecx, @szBuffer1 
				invoke SendMessage, lParam, LB_GETTEXT, eax, ecx 
				invoke MessageBox, hWnd, addr @szBuffer2, addr szTitle, MB_OK 
			.endif 
		.elseif ax == IDC_LISTBOX2 
			shr eax, 16 
			.if ax == LBN_SELCHANGE 
				invoke SendMessage, lParam, LB_GETSELCOUNT, 0, 0
				mov ebx, eax 
				invoke GetDlgItem, hWnd, IDOK 
				invoke EnableWindow, eax, ebx 
			.endif 
		.endif 
	.else 
		mov eax, FALSE 
		ret 
	.endif 
	mov eax, TRUE 
	ret 
_ProcDlgMain endp 

main proc 
	invoke GetModuleHandle, NULL 
	mov hInstance, eax 
	invoke DialogBoxParam, hInstance, DLG_MAIN, NULL, offset _ProcDlgMain, NULL 
	invoke ExitProcess, NULL 
main endp 
end main 

编译运行:

下面结合源程序来说明列表框的使用。当列表框有LBS_NOTIFY风格的时候,用户有所动作时列表框会向父窗口发送WM_COMMAND消息,同时在wParam的高16位中指定通知码,列表框的通知码种类很少,基本上就是以下几种:

● LBN_DBLCLK——用户双击了一个项目。

● LBN_ERRSPACE——插入项目时无法申请到足够的内存。

● LBN_KILLFOCUS——输入焦点被切换到其他控件中,列表框丢失了焦点。

● LBN_SELCANCEL——用户撤销了一个选择。

● LBN_SELCHANGE——选定状态改变。

● LBN_SETFOCUS——列表框得到输入焦点。

我们最关心的是LBN_DBLCLK和LBN_SELCHANGE通知码,在单选列表框中,如果程序用双击来选择项目,那么就要处理LBN_DBLCLK通知,例子程序中当用户双击IDC_LISTBOX1时弹出一个消息框,读者可以查看其使用方法。在多选列表框中,由于用户可能选择了多个项目,所以一般不用双击的方法选定;如果收到LBN_SELCHANGE通知的话,可以得知用户有一个选择动作,在这里可以进行相应的操作。

列表框通知父窗口是通过发送WM_COMMAND消息,而程序控制列表框的时候是通过向列表框发送消息来完成的,常用的列表框消息如表5.8所示。

                                                            表5.8 列表框消息

在这些消息中LB_DIR是个比较有趣的消息,它可以将指定目录中的文件名自动列出来并加入列表框中,如例子中用*.*将当前目录的全部文件名加到列表框中。LB_DIR消息中wParam参数可以指定的属性可以是以下值的组合:

● DDL_ARCHIVE加入归档属性的文件。

● DDL_DIRECTORY加入目录。

● DDL_DRIVES加入驱动器名。

● DDL_HIDDEN包含隐含文件。

● DDL_READONLY包含只读文件。

● DDL_READWRITE包含可读写的文件。

● DDL_SYSTEM包含系统文件。

在列表框中初始化时加入项目可以使用LB_ADDSTRING和LB_INSERTSTRING消息,删除项目可以用LB_DELETESTRING消息,删除全部项目用LB_RESETCONTENT消息。

对于单选列表框,要获取选中项目可以发送LB_GETCURSEL消息,要得到这个项目的字符串需要再用索引值通过LB_GETTEXT消息获取,读者可以查看例子中处理LBN_DBLCLK通知码的部分代码。

对于多选列表框,需要用LB_GETSELITEMS消息获取全部选中项目,这个消息返回的是一个列表,所有选中项目的索引按顺序排列返回到缓冲区中,所以在例子中处理“查看”按钮消息(IDOK)的时候,程序先发送LB_GETSELCOUNT消息得到选中的项目数,以便在下面用一个循环获取所有的项目,得到项目数后,再用LB_GETSELITEMS将选中项目的索引取到@szBuffer中,接下来进入一个循环,循环的次数就是LB_GETSELCOUNT得到的数值,在循环中,程序从@szBuffer中将索引值逐个取出并用LB_GETTEXT消息获取每一项的字符串,最后用一个MessageBox显示出来。

5.5 字符串资源

程序中用到的字符串常常定义在.const段中,但Windows也提供了另外一种使用字符串常量的方法,那就是在资源中定义。虽然在资源中定义字符串使用起来比直接在.const段中定义要复杂一点,但它带来的好处是便于开发不同语言的版本,比如,要推出其他语种的版本只需要修改资源中的字符串表就可以了,即使语言转换的工作是由第三者通过修改可执行文件来做的(如编程爱好者常常做的汉化工作),修改资源也远比修改代码来得快捷和安全。

另外,有些API使用到的字符串必须定义在资源里面,如显示菜单帮助的MenuHelp函数等。

在资源脚本中定义字符串的语法是:

STRINGTABLE [DISCARDABLE]
BEGIN
        字符串ID1 "字符串1"
        字符串ID2 "字符串2"
        . . .
END

全部字符串组成一个字符串表,和其他资源定义不同,由于整个资源文件中只能定义一个字符串表,所以字符串表没有资源ID,但是表中的不同字符串分别有一个字符串ID。

在程序中使用字符串资源也很简单,用LoadString把字符串装入到缓冲区中去就可以用了:

invoke  LoadString,hInstance,字符串ID,addr缓冲区,sizeof缓冲区

为了防止溢出,最后一个参数指定缓冲区的长度。

如果要在单个可执行文件中实现多语种,那么可以在字符串表中定义不同语言的字符串。同一语种的字符串按规律排列,如下列中文的以1000开头,英文的以2000开头:

    stringtable
    {
                    1001         "文件未找到!"
                    1002         "无法打开文件!"
                    ...
                    2001          "File not found!"
                    2002          "Can not open file!"
                    ...
    }

在程序中使用的时候,先确定一种语言并预先设置在dwLanguage变量中,使用中文时将dwLanguage设置为1000,使用英文时设置为2000,再写一个读取不同版本字符串的子程序_GetString,这样调用_GetString子程序后就不用考虑版本问题了:

    _GetString  proc     _dwID,_lpBuffer,_dwSize
                pushad
                mov      eax,_dwID
                add      eax,dwLanguage
                invoke  LoadString,hInstance,eax,_lpBuffer,_dwSize
                popad
                ret
    _GetString  endp

5.6 版本信息资源

有时应用程序需要确保自己运行时使用某一特定版本的DLL,以便确保可以使用某些函数。检测版本是通过API函数查询定义于资源中的版本信息来完成的,如果资源中没有定义版本,那么就无法知道一个文件的版本究竟是多少。

版本信息是以VERSIONINFO类型的资源保存在应用程序中的,里面可以定义的信息包括文件的版本号、创建单位和语种等。版本信息的定义是可选的,一个程序可以不定义版本信息资源,如果定义了的话,也不一定要定义全部信息项目。

如果一个文件定义有版本信息资源,那么在文件的属性页(在文件图标上按鼠标右键,在弹出的菜单上选择)上就会有一个“版本”页面,如图5.10所示。

                                                     图5.10 文件属性中的版本信息

5.6.1 版本信息资源的定义

在所附光盘的Chapter05\VersionInfo目录中有一个例子,在该目录的Version.rc文件中定义了一个版本信息,读者可以看编译后的VersionInfo.exe中的“版本”属性页,对比一下资源定义中的内容究竟出现在属性页的哪些地方,定义的代码如下:

//>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
#include		<resource.h>
#include		<Version.h>
//>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
#define	ICO_MAIN		0x1000	//图标
ICO_MAIN	ICON		"Main.ico"
//>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
1 VERSIONINFO
FILEVERSION 1,2,3,4
PRODUCTVERSION 2,3,4,5
FILEOS VOS_WINDOWS32
FILETYPE VFT_APP
FILESUBTYPE VFT2_UNKNOWN
BEGIN
    BLOCK "VarFileInfo"
    BEGIN
        VALUE "Translation", 0x804, 0x4b0
    END
    BLOCK "StringFileInfo"
    BEGIN
        BLOCK "080404b0"
        BEGIN
            VALUE "Comments", "Here is Comments\0"
            VALUE "CompanyName", "Here is CompanyName\0"
            VALUE "FileDescription", "Here is FileDescription\0"
            VALUE "FileVersion", "1, 0, 0, 1\0"
            VALUE "InternalName", "Here is InternalName\0"
            VALUE "LegalCopyright", "Here is LegalCopyright\0"
            VALUE "LegalTrademarks", "Here is LegalTrademarks\0"
            VALUE "OriginalFilename", "Here is OriginalFilename\0"
            VALUE "PrivateBuild", "Here is PrivateBuild\0"
            VALUE "ProductName", "Here is ProductName\0"
            VALUE "ProductVersion", "1, 0, 0, 1\0"
            VALUE "SpecialBuild", "Here is SpecialBuild\0"
        END
    END
END
//>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>

现在来看这些定义语句的含义。首先,版本信息定义的语句格式是:

    版本信息资源ID  VERSIONINFO
    固定属性
    BEGIN
        块声明定义
        …
    END

版本信息资源ID的取值必须为1,如果不为1则属性页上的“版本”信息是无法显示出来的。(笔者也不明白为什么必须为1还要定义这个ID,像stringtable一样没有ID不就完事了?)

可以定义的固定属性有:

● FILEVERSION——定义文件版本号,可以定义4个16位版本号xx.xx.xx.xx。

● PRODUCTVERSION——定义产品版本号,可以定义4个16位版本号xx.xx.xx.xx。

● FILEFLAGSMASK——指定FILEFLAGS属性中哪些位有效。

● FILEFLAGS—文件标志,是一些标志位的组合:VS_FF_PATCHED,VS_FF_DEBUG,VS_FF_PRIVATEBUILD,VS_FF_INFOINFERRED,VS_FF_PRERELEASE和VS_FF_SPECI- ALBUILD。

● FILEOS—定义适用的操作系统,可以定义为VOS_UNKNOWN,VOS_DOS,VOS_NT,VOS_WINDOWS16,VOS_WINDOWS32,VOS_DOS_WINDOWS16,VOS_DOS_WINDOWS32或VOS_NT_WINDOWS32。

● FILETYPE—定义文件类型,可以是VFT_UNKNOWN,VFT_APP,VFT_DLL,VFT_DRV,VFT_FONT,VFT_VXD或VFT_STATIC_LIB。

● FILESUBTYPE——定义文件的子类型。当文件类型是VFT_DRV(驱动程序)的时候,这里可以是VFT2_UNKNOWN,VFT2_DRV_COMM,VFT2_DRV_PRINTER,VFT2_DRV_KEYBOARD,VFT2_DRV_LANGUAGE,VFT2_DRV_DISPLAY,VFT2_DRV_MOUSE,VFT2_DRV_NETWORK,VFT2_DRV_SYSTEM,VFT2_DRV_INSTALLABLE或VFT2_DRV_SOUND;当文件类型是VFT_FONT(字体)的时候,这里可以是VFT2_UNKNOWN,VFT2_FONT_RASTER,VFT2_FONT_VECTOR或VFT2_FONT_TRUETYPE。

在固定属性定义完成以后,需要定义一些块声明,块声明有两种:变量型的信息块和字符串类型的信息块,变量类型的信息块定义如下:

    BLOCK "VarFileInfo"
    BEGIN
        VALUE "Translation",语言ID,字符集ID
        ……
    END

语言ID的常用值有0x0404(繁体中文)、0x0409(美国英语)和0x0804(简体中文),字符集ID的常用值有0(7位ASCII)、950(台湾GB5)和1200(Unicode)。一般使用0x804, 0x4b0来定义,也就是简体中文和Unicode(0x4b0=1200)。其他还有很多取值,读者可以查看Rc.exe的帮助文件。

变量类型信息块用来表示VERSIONINFO资源中定义有哪些语言和字符集的字符串类型信息块。如上例中有一句VALUE "Translation",0x804,0x4b0表示对应有一个名为“080404b0”的字符串类型的信息块。

字符串信息块的定义语句为:

BLOCK "StringFileInfo"
BEGIN
        BLOCK "语言集"
        BEGIN
              VALUE "字符串名称", "字符串"
              ……
        END
END

语言集就是变量类型中定义的,其名称一定要是将语言ID和字符集ID组合成一个8位的十六进制的格式,以上例文件来说明,当变量类型的信息块种定义0x804,0x4b0时,语言集名称就是“080404b0”,在语言集块的定义中,还可以定义多条字符串型的版本信息,这些版本信息的字符串名有12种,如表5.9所示。

                                                   表5.9 版本信息字符串类型

定义版本信息字符串的时候要注意,所有的字符串必须是以NULL结尾的串,所以要在字符串尾加上\0,如例子程序所示:

VALUE "Comments", "Here is Comments\0"
VALUE "CompanyName", "Here is CompanyName\0"
...

【学习笔记】

;VersionInfo.asm              资源中版本信息使用的例子
; 使用 nmake 或下列命令进行编译和链接:
; ml /c /coff VersionInfo.asm
; rc VersionInfo.rc
; Link /subsystem:windows VersionInfo.obj VersionInfo.res
.386
.model flat,stdcall 
option casemap:none 
;include 文件定义
include 	C:/masm32/include/windows.inc 
include 	C:/masm32/include/user32.inc 
includelib	C:/masm32/lib/user32.lib 
include		C:/masm32/include/kernel32.inc 
includelib 	C:/masm32/lib/kernel32.lib 
.const 
szTitle 	byte '提示...', 0
szMessage	byte '请用鼠标右键单击本文件,再看看“属性”菜单项中的“版本”页!', 0

;代码段
.code 
main proc 
	invoke MessageBox, NULL, addr szMessage, addr szTitle, MB_OK or MB_ICONEXCLAMATION 
	invoke ExitProcess, 0
main endp 
end main 

编译运行:

5.6.2 在程序中检测版本信息

Win32 API中有3个版本信息函数:GetFileVersionSize,GetFileVersionInfo和VerQueryValue,它们驻留在VERSION.DLL文件中,如果在源程序中使用它们,注意要加上include Version.inc以及includelib Version.lib语句。

用这3个函数获取版本信息的方法是:

首先调用GetFileVersionInfoSize函数检测文件中有没有版本信息资源:

invoke   GetFileVersionInfoSize,addr szFile,NULL

其中szFile是要检测的PE文件名字符串,该函数的返回值是版本信息资源的长度,如果返回0,则表示文件不是PE文件或没有定义版本信息资源。

如果检测到文件中有版本信息资源,那么可以将版本信息资源读取到一个缓冲区中,缓冲区的长度必须足够容纳上一步返回的资源长度,方法是:

invoke   GetFileVersionInfo,addr szFile,NULL,\
                    sizeof dbVerInfo,addr dbVerInfo

其中dbVerInfo为一个足够大的缓冲区。该函数会把整个版本信息资源拷贝到这个缓冲区中。

复制到缓冲区中的信息有它自己的格式,必须用VerQueryValue去“解码”,解码固定属性的方法是:

invoke   VerQueryValue,addr dbVerInfo,addr szRoot,addr lpBuffer,addr dwLen

第一个参数指向前一步返回的版本信息资源数据,第二个参数指向一个字符串:“\”,第三个和第四个参数指向dw类型的变量lpBuffer和dwLen,返回到lpBuffer中的是指向一个VS_FIXEDFILEINFO结构的指针,这个结构中有定义的固定属性内容。

如果要获取字符串类型信息块中的版本信息,那就比较复杂一点了,必须首先知道语言集的名称,所以先要获取版本信息资源中变量类型信息块的内容,方法是:

invoke   VerQueryValue,addr dbVerInfo,addr szVarInfo,\
                    addr lpBuffer,addr dwLen
mov      eax,lpBuffer
mov      eax,[eax]
ror      eax,16

szVarInfo是一个字符串:“\VarFileInfo\Translation”,这时函数在lpBuffer中返回语言集变量的指针,所以要先mov eax,lpBuffer,再用eax做指针用mov eax,[eax]得到语言集变量。语言集变量的高16位是字符集ID,低16位是语言ID,可以使用ror eax,16位来调换高低位,以我们的例子为例,现在eax中的值就是080404b0h了!

接下来就可以获取字符串版本信息了,先将语言集的值通过wsprintf函数转换成“080404b0”的形式,然后拼装成“\StringFileInfo\080404b0\字符串名称”形式的字符串,中间的“字符串名称”可以是表5.9中的12种名称之一,最后调用下面的语句(假定拼装好的字符串地址为szString):

invoke   VerQueryValue,addr dbVerInfo,addr szString,addr lpBuffer,addr dwLen

执行后lpBuffer中会得到一个指针,指向版本信息字符串定义的内容,这就是我们最后需要的结果!重复这个步骤可以得到所有12种字符串版本信息。

读者可以在所附光盘的Chapter05\ShowVersionInfo目录中找到一个ShowInfo程序,它可以获取PE文件中的版本信息资源并显示出来,详细的代码请参考该目录中的文件,主文件ShowInfo.asm是界面程序,版本信息资源的代码在GetVersionInfo.inc文件中,由于篇幅有限,源程序在这里就不列出来了。

【学习笔记】

;GetVersionInfo.inc   
;用来获取 PE 文件资源中版本信息的子程序
;通过 include 语句包含到其它源程序中使用
;-------------------------------------------------------------------
.const

szRoot		db	'\',0
szVarInfo	db	'\VarFileInfo\Translation',0
szStrInfoFmt	db	'\StringFileInfo\%08x\',0
szArrow		db	' --> ',0
szReturn	db	0dh,0ah,0
szFixFileInfo	db	'固定版本信息属性:',0dh,0ah
		db	'FileVersion: %d.%d.%d.%d',0dh,0ah
		db	'ProductVersion: %d.%d.%d.%d',0dh,0ah
		db	'FileOS: %08x',0dh,0ah
		db	'FileType: %08x',0dh,0ah,0dh,0ah
		db	'字符串版本属性:',0dh,0ah,0
;********************************************************************
szStr1		db	'Comments',0
szStr2		db	'CompanyName',0
szStr3		db	'FileDescription',0
szStr4		db	'FileVersion',0
szStr5		db	'InternalName',0
szStr6		db	'LegalCopyright',0
szStr7		db	'LegalTrademarks',0
szStr8		db	'OriginalFilename',0
szStr9		db	'PrivateBuild',0
szStr10		db	'ProductName',0
szStr11		db	'ProductVersion',0
szStr12		db	'SpecialBuild',0
;********************************************************************
lpStr1		dd	szStr1
lpStr2		dd	szStr2
lpStr3		dd	szStr3
lpStr4		dd	szStr4
lpStr5		dd	szStr5
lpStr6		dd	szStr6
lpStr7		dd	szStr7
lpStr8		dd	szStr8
lpStr9		dd	szStr9
lpStr10		dd	szStr10
lpStr11		dd	szStr11
lpStr12		dd	szStr12

		.code
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
; 获取文件版本信息
; 入口参数:文件名,返回信息缓冲区
; 返回参数:TRUE = 成功,FALSE = 失败
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
_GetVersionInfo	proc	_lpFileName,_lpBuffer
		local	@dbVerInfo[4096]:byte
		local	@szStringInfo[128]:byte
		local	@szString[128]:byte
		local	@lpBuffer,@dwLen
		local	@dwVer1,@dwVer2,@dwVer3,@dwVer4
		local	@dwVer5,@dwVer6,@dwVer7,@dwVer8

		pushad
		invoke	GetFileVersionInfoSize,_lpFileName,NULL
		.if	!eax
			popad
			xor	eax,eax
			ret
		.endif
		invoke	GetFileVersionInfo,_lpFileName,NULL,sizeof @dbVerInfo,addr @dbVerInfo
;********************************************************************
; 获取固定属性
;********************************************************************
		invoke	VerQueryValue,addr @dbVerInfo,addr szRoot,addr @lpBuffer,addr @dwLen
		mov	esi,@lpBuffer
		assume	esi:ptr VS_FIXEDFILEINFO
;********************************************************************
; 计算版本号
;********************************************************************
		mov	eax,[esi].dwFileVersionMS
		movzx	ecx,ax
		mov	@dwVer2,ecx
		shr	eax,16
		mov	@dwVer1,eax

		mov	eax,[esi].dwFileVersionLS
		movzx	ecx,ax
		mov	@dwVer4,ecx
		shr	eax,16
		mov	@dwVer3,eax

		mov	eax,[esi].dwProductVersionMS
		movzx	ecx,ax
		mov	@dwVer6,ecx
		shr	eax,16
		mov	@dwVer5,eax

		mov	eax,[esi].dwProductVersionLS
		movzx	ecx,ax
		mov	@dwVer8,ecx
		shr	eax,16
		mov	@dwVer7,eax
		invoke	wsprintf,_lpBuffer,addr szFixFileInfo,\
			@dwVer1,@dwVer2,@dwVer3,@dwVer4,\
			@dwVer5,@dwVer6,@dwVer7,@dwVer8,\
			[esi].dwFileOS,[esi].dwFileType

		assume	esi:nothing
;********************************************************************
; 获取语言集
;********************************************************************
		invoke	VerQueryValue,addr @dbVerInfo,addr szVarInfo,addr @lpBuffer,addr @dwLen
		mov	eax,@lpBuffer
		mov	eax,[eax]
		ror	eax,16
		invoke	wsprintf,addr @szStringInfo,addr szStrInfoFmt,eax
;********************************************************************
; 获取字符串版本信息
;********************************************************************
		mov	ebx,offset lpStr1
		.while	ebx <=	offset lpStr12
			invoke	lstrcpy,addr @szString,addr @szStringInfo
			mov	eax,[ebx]
			invoke	lstrcat,addr @szString,eax
			mov	eax,[ebx]
			invoke	lstrcat,_lpBuffer,eax
			invoke	lstrcat,_lpBuffer,addr szArrow		;加上 -->
			invoke	VerQueryValue,addr @dbVerInfo,addr @szString,addr @lpBuffer,addr @dwLen
			.if	eax
				invoke	lstrcat,_lpBuffer,@lpBuffer	;加上获得的版本信息
			.endif
			invoke	lstrcat,_lpBuffer,addr szReturn		;加上回车
			add	ebx,4
		.endw
		popad
		mov	eax,TRUE
		ret

_GetVersionInfo	endp

汇编文件:

;ShowInfo.asm               显示 PE 文件资源中版本信息的例子
;使用 nmake 或下列命令进行编译和链接:
;ml /c /coff ShowInfo.asm
;rc ShowInfo.rc
;Link /subsystem:windows ShowInfo.obj ShowInfo.res
.386
.model flat, stdcall 
option casemap:none 
;include 文件定义
include 	C:/masm32/include/windows.inc 
include 	C:/masm32/include/user32.inc 
includelib 	C:/masm32/lib/user32.lib 
include 	C:/masm32/include/kernel32.inc 
includelib	C:/masm32/lib/kernel32.lib 
include		C:/masm32/include/Version.inc 
includelib 	C:/masm32/lib/Version.lib 
include 	C:/masm32/include/comctl32.inc 
includelib 	C:/masm32/lib/comctl32.lib 
include 	C:/masm32/include/comdlg32.inc 
includelib 	C:/masm32/lib/comdlg32.lib 

include GetVersionInfo.inc 

;equ 等值定义
ICO_MAIN	equ 1000h
DLG_MAIN 	equ 1 
IDC_INFO 	equ 101
IDC_FILE	equ 102 

;数据段
.data?
hInstance	dword ?
szBuffer 	byte 4096 dup(?)
.const 
szPeFileExt byte 'PE文件',0,'*.exe;*.dll;*.scr;*.drv', 0, 0
szError 	byte '文件中没有包含版本信息!', 0

;代码段
.code 
_ProcDlgMain proc uses ebx edi esi, hWnd, wMsg, wParam, lParam 
	local @szBuffer[MAX_PATH]:byte 
	local @stOpenFileName:OPENFILENAME 
	mov eax, wMsg 
	.if eax == WM_CLOSE 
		invoke EndDialog, hWnd, NULL 
	.elseif eax == WM_INITDIALOG 
		;设置标题栏图标
		invoke LoadIcon, hInstance, ICO_MAIN 
		invoke SendMessage, hWnd, WM_SETICON, ICON_BIG, eax 
	.elseif eax == WM_COMMAND 
		mov eax,wParam 
		.if ax == IDOK 
			;打开一个选择文件的对话框
			invoke RtlZeroMemory, addr @stOpenFileName, sizeof OPENFILENAME 
			invoke RtlZeroMemory, addr @szBuffer, sizeof @szBuffer 
			mov @stOpenFileName.lStructSize, sizeof @stOpenFileName 
			mov @stOpenFileName.Flags, OFN_FILEMUSTEXIST or OFN_PATHMUSTEXIST 
			push hWnd 
			pop @stOpenFileName.hwndOwner 
			mov @stOpenFileName.lpstrFilter, offset szPeFileExt 
			lea eax, @szBuffer 
			mov @stOpenFileName.lpstrFile, eax 
			mov @stOpenFileName.nMaxFile, MAX_PATH 
			invoke GetOpenFileName, addr @stOpenFileName 
			.if eax 
				;获取版本信息并显示出来
				invoke _GetVersionInfo, addr @szBuffer, addr szBuffer 
				.if eax 
					invoke SetDlgItemText, hWnd, IDC_FILE, addr @szBuffer 
					invoke SetDlgItemText, hWnd, IDC_INFO, addr szBuffer 
				.else 
					invoke MessageBox, hWnd, addr szError, NULL, MB_OK or MB_ICONHAND 
				.endif 
			.endif 
		.endif 
	.else 
		mov eax, FALSE 
		ret 
	.endif 
	mov eax, TRUE 
	ret 
_ProcDlgMain endp 	

main proc 
	invoke InitCommonControls 
	invoke GetModuleHandle, NULL 
	mov hInstance, eax 
	invoke DialogBoxParam, hInstance, DLG_MAIN, NULL, offset _ProcDlgMain, NULL 
	invoke ExitProcess, 0
main endp 
end main 

编译运行:

5.7 二进制资源和自定义资源

5.7.1 使用二进制资源

在第2章中曾经提到DOS的exe文件可以带一个覆盖部分,覆盖部分实际上就是在真正的可执行部分后面附加的数据,然后由程序在运行中打开自身文件并使用这些数据。Win32的可执行文件中除了上面介绍的这些标准类型的资源外,也可以在程序中附带其他数据,当然方法完全不同——Win32资源中允许用户自己定义二进制的资源或者自定义格式的资源,资源的内容可以是任何数据,也可以将一个磁盘文件按二进制格式包括进去。

二进制资源的定义格式是:

资源ID   RCDATA [DISCARDABLE]
            BEGIN
                    数据定义
                    …
END

也可以用一个磁盘文件当做资源的内容:

资源ID  RCDATA [DISCARDABLE] 文件名

在程序中要使用资源的内容时,可以通过以下步骤将资源装入内存使用:

(1)用FindResource(hInstance,lpName,lpType)查找资源。lpName的值为资源ID,lpType的值为RT_RCDATA,如果找到资源。那么函数返回一个资源信息句柄。

(2)用LoadResource(hInstance, hResInfo)装入资源。hResInfo是上一步中得到的资源信息句柄,装入成功的话函数会返回一个资源句柄。

(3)用LockResource(hResData)将资源锁定到内存中。hResData是上一步得到的资源句柄,函数返回资源装入的内存地址,程序就可以使用内存中的数据了。

(4)如果想知道装入资源的大小是多少,可以使用FindResource返回的hResInfo来调用SizeofResource(hInstance,hResInfo)从而得到资源大小。

下面是一个装入资源ID为ID_MYRES的RCDATA类型资源的例子:

invoke  FindResource,hInstance,ID_MYRES,RT_RCDATA   ;寻找资源
            .if       eax
          mov      hResInfo,eax
          invoke  SizeofResource,hInstance,eax         ;获取资源尺寸
          mov      dwResSize,eax
          invoke  LoadResource,hInstance,hResInfo      ;装入资源
          .if      eax
                            invoke  LockResource,eax               ;锁定资源
                            .if      eax
                          mov      lpRes,eax
                          ;处理lpRes指向的资源内容
                            .endif
          .endif
.endif

5.7.2 使用自定义资源

自定义资源的定义格式比二进制资源更灵活,它和二进制资源的区别在于可以指定资源类别为自定义的名称:

资源ID       类型ID [DISCARDABLE]
            BEGIN
        数据定义
        …
END

或用一个磁盘文件当做资源的内容:

资源ID  类型ID [DISCARDABLE] 文件名

类型ID可以是大于255的数值(255及以下的数值由Windows使用)或字符串,如可以定义如下:

1000     WAVE     "Hello.wav"      ;定义类型为“WAVE”,资源ID为1000的资源
1000     TEXT     "Readme.txt"    ;定义类型为“TEXT”,资源ID为1000的资源
1000     1000     "Test.bin"       ;定义类型ID为1000,资源ID为1000的资源

在程序中使用自定义资源的方法和使用二进制资源类似,唯一的区别是使用FindResource得到hResInfo的参数有些区别,得到hResInfo以后的步骤是一模一样的。针对上面3句定义,查找资源的方法可以是:

szResType1       db       "WAVE",0
szResType2       db       "TEXT",0
...
invoke  FindResource,hInstance,1000,addr szResType1     ;针对上面第一句
invoke  FindResource,hInstance,1000,addr szResType2     ;针对上面第二句
invoke  FindResource,hInstance,1000,1000                ;针对上面第三句

在使用完二进制或自定义资源以后,不必使用任何函数去释放它们,Windows在程序退出的时候会自动将它们释放。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值