📄 007.txt
字号:
pop @stWndClass.hInstance
mov @stWndClass.cbSize,sizeof WNDCLASSEX
mov @stWndClass.style,CS_HREDRAW or CS_VREDRAW
mov @stWndClass.lpfnWndProc,offset _ProcWinMain
mov @stWndClass.hbrBackground,COLOR_WINDOW + 1
mov @stWndClass.lpszClassName,offset szClass1
invoke RegisterClassEx,addr @stWndClass
invoke CreateWindowEx,WS_EX_CLIENTEDGE,offset szClass1,\
offset szCaption1,WS_OVERLAPPEDWINDOW,\
450,100,300,300,\
NULL,NULL,hInstance,NULL
mov hWin1,eax
invoke ShowWindow,hWin1,SW_SHOWNORMAL
invoke UpdateWindow,hWin1
;********************************************************************
mov @stWndClass.lpszClassName,offset szClass2
invoke RegisterClassEx,addr @stWndClass
invoke CreateWindowEx,WS_EX_CLIENTEDGE,offset szClass2,\
offset szCaption2,WS_OVERLAPPEDWINDOW,\
100,100,300,300,\
NULL,NULL,hInstance,NULL
mov hWin2,eax
invoke ShowWindow,hWin2,SW_SHOWNORMAL
invoke UpdateWindow,hWin2
;********************************************************************
; 设置定时器
;********************************************************************
invoke SetTimer,NULL,NULL,100,addr _ProcTimer
mov @hTimer,eax
;********************************************************************
; 消息循环
;********************************************************************
.while TRUE
invoke GetMessage,addr @stMsg,NULL,0,0
.break .if eax == 0
invoke TranslateMessage,addr @stMsg
invoke DispatchMessage,addr @stMsg
.endw
;********************************************************************
; 清除定时器
;********************************************************************
invoke KillTimer,NULL,@hTimer
ret
_WinMain endp
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
start:
call _WinMain
invoke ExitProcess,NULL
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
end start
这个程序的代码用到的大部分知识都是前面各章已经讲到的,在_WinMain中,用一个同样的窗口类建立了两个窗口,两个窗口属于同一个窗口类,所以它们的窗口过程都是_ProcWinMain,为了关闭任何一个窗口都可以结束程序,WM_CLOSE消息中用DestroyWindow函数摧毁了两个窗口。程序设置了一个周期为100 ms的定时器,Windows会每隔100 ms调用_ProcTimer子程序。在_ProcTimer中,将其中一个窗口的客户区拷贝到另一个窗口的客户区中,方法是通过GetDC获取窗口的DC句柄,并用BitBlt函数完成拷贝工作(这些函数的具体用法在下面的内容中会讲到),所以在右边的窗口显示了一句“Win32 Assembly, Simple and powerful!”,左边的窗口中也会出现这句话。
这个程序能演示出什么效果来呢?图7.2就是程序运行的结果,屏幕上的两个并排的正方形窗口就是DcCopy程序建立的窗口,程序每100 ms将右边窗口的客户区拷贝到左边的窗口客户区中,通过左边窗口的客户区就可以了解右边客户区DC对应的究竟是什么内容。
图7.2 DcCopy程序的运行结果
现在用其他程序将右边窗口客户区的一部分覆盖掉,通过左边窗口的变化可以惊奇地发现:右边窗口客户区的内容并不是程序自己输出到客户区的那句文本,而是以客户区为矩形区域的屏幕上我们真正看到的东西,它竟然包括其他窗口覆盖在上面的东西。这就意味着,扫雷游戏和纸牌游戏通过自己客户区对应的设备环境画图形,图形数据竟然画到了DcCopy窗口客户区对应的设备环境中。
这个例子验证了“设备环境”只是“环境”而不是“设备”,它并不存储发给它的图形数据,图形数据透过它写到了它所描述的“设备”上,每个窗口客户区的“设备环境”对应的设备都是屏幕,但它们在位置上可能重叠,所以向一个窗口的客户区写数据相当于同时写了下层窗口的客户区。
为了让当前激活的窗口在视觉上保持在最上面,下层窗口向自己客户区写的内容首先要经过Windows的“过滤”,只有没有被其他窗口覆盖掉的部分才真正被写到了屏幕上。
读者应该时刻提醒自己——“设备环境”只是一个环境,是设备属性的一组定义,程序输出的图形数据透过“设备环境”被定向到了具体的设备上,“设备环境”本身并不存储这些数据(在这里也可以看出Device Context中Context一词的含义:设备环境的上面是应用程序,下面是具体设备,而它是用来“联系上下关系”用的)。
读者可能认为:屏幕上的窗口就像放在桌面上的一张张纸,虽然一张纸可能暂时被另一张遮住,但纸上写的东西还是存在的,移开另一张纸就可以再次露出来。但实际情况是:桌面更像一个用粉笔写的公告黑板,一个窗口相当于划了一块空间写告示,写另一个告示的时候要把老告示的内容擦去一部分以便写新的内容,擦去的东西也就不存在了,如果要恢复老告示,那么必须把擦去的部分重新写上去。
2. 获取设备环境句柄
要想对任何设备绘图,首先必须获取设备的“设备环境句柄”(hDC),几乎所有的GDI函数的操作目标都是hDC,在程序中得到一个hDC有几种方法。
最常用的方法是在WM_PAINT消息中用BeginPaint函数得到hDC,WM_PAINT消息的代码结构一般是:
.if eax == WM_PAINT ;eax为uMsg
invoke BeginPaint,hWnd,addr stPS
;刷新客户区的代码
invoke EndPaint,hWnd,addr stPS
xor eax,eax
ret
BeginPaint函数的返回值就是需要刷新区域的hDC。要注意的是:BeginPaint返回的hDC对应的尺寸仅是无效区域,无法用它绘画到这个区域以外的地方去。由于窗口过程每次接收WM_PAINT消息时的无效区域可能都是不同的,所以这个hDC的值仅在WM_PAINT消息中有效,程序不应该保存它并把它用在WM_PAINT消息以外的代码中。基于同样的道理,BeginPaint和EndPaint函数只能用在WM_PAINT消息中,因为只有这时候才存在无效区域。
程序中常常有这种需求,就是在非WM_PAINT消息中主动绘画客户区,由于BeginPaint和EndPaint函数必须在WM_PAINT消息中使用,所以这时必须用另外的方法获取hDC,可以使用以下的方法:
invoke GetDC,hWnd ;获取hDC
;返回值是hDC
;绘图代码
invoke ReleaseDC,hWnd ;释放hDC
GetDC函数返回的hDC对应窗口的整个客户区,当使用完毕的时候,hDC必须用ReleaseDC函数释放。对于用GetDC获取的hDC,Windows建议使用的范围限于单条消息内,当程序在处理某条消息的时候需要绘画客户区时,可以用GetDC获取hDC,但在消息返回前,必须用ReleaseDC将它释放掉,如果在下一条消息中需要继续用到hDC,那么必须重新用GetDC函数获取。
上面两种方法获取的hDC都是窗口的hDC,如果要操作的是其他的东西,如打印机、位图等,就不能使用BeginPaint或GetDC函数了。当绘图的对象是一个设备的时候,可以用Create DC函数来建立一个DC:
invoke CreateDC,lpszDriver,lpszDevice,lpszOutput,lpInitData
lpszDriver指向设备名称,如显示设备的设备名是DISPLAY,打印机的设备名一般为WINSPOOL,下面这几句代码建立的DC对应整个屏幕:
szDriver db "DISPLAY",0
...
invoke CreateDC,addr szDriver,NULL,NULL,NULL
mov hDC,eax
当绘图对象是位图的时候,同样需要一个和位图句柄相联系的DC,这时可以用函数CreateCompatibleDC来创建一个显示表面仅存在于内存中的DC:
invoke CreateCompatibleDC,hDc
参数中的hDC是用来参考的DC句柄,如果指定的参数是NULL,那么建立的DC将和当前屏幕的设置兼容,为了用CreateCompatibleDC建立的DC绘画一个位图,还需要用SelectObject函数将hDC和位图句柄联系起来。
用CreateDC和CreateCompatibleDC函数建立的hDC在使用结束以后,必须用DeleteDC函数删除,注意这里不能用ReleaseDC,这个函数是和GetDC配合用的。
用BeginPaint/EndPaint以及GetDC获取的hDC的使用时间不能超出本条消息,与此相比,用CreateDC以及CreateCompatibleDC建立的hDC就没有这个限制,可以在任何时刻建立它并且一直使用到不再需要为止。
7.1.3 色彩和坐标
1. Windows中的色彩
可以表示的颜色总数由颜色深度决定,也就是存储每个像素所用的位数,各种显示设备可以显示的颜色总数可能大不相同,如果设备支持的颜色深度太浅,就会影响到图像的质量,会让人看起来觉得很粗糙和不自然。
一种颜色可以分解成红、绿、蓝三原色,所以可以用红、绿、蓝3个分量的组合来表示各种颜色。
当设备支持的颜色深度少于等于8位时(如8位(256色)、4位(16色)、2位(4色)或1位(2色)),总体位数太少,不足以用来表达3个颜色分量,这时系统建立一个色彩表,像素数据用来做索引在色彩表中获取颜色值,所以低于8位的颜色称为索引色。
只有当颜色深度大于8位的时候,像素数据中才直接包含红、绿、蓝3个分量。当颜色深度为16位的时候,红、绿、蓝各用5位表示,剩下的1位用做属性位,实际可以表示的颜色数目为215=32 768种,16位深度的彩色又称为16位色、高彩色或增强色。当颜色深度为24位的时候,3个分量各用8位表示,实际可以表示的颜色数目为224=16 777 216种,24位深度的彩色又称为24位色、16M色或真彩色。对于人的双眼来说,超过16位的颜色就已经很难分辨了。
在Win32的编程中,统一使用32位的整数来表示一个深度为24位的颜色,在这32位中只使用低24位,每一种原色分量占用8位,其中0~7位为红色,8~15位为绿色,16~31位为蓝色。在程序中用到一种颜色常数的时候,可以如下使用:
mov eax,红色+绿色*100h+蓝色*10000h ;将颜色放入eax中
当显示设备无法表示24位色的时候,Windows会自动用设备可以显示的最接近的颜色来代替它,当显示设备的颜色深度比较低的时候,可以通过函数GetNearest Color来得知一种颜色(dwColor)会被系统替换成哪种颜色:
invoke GetNearestColor,hDC,dwColor ;返回真正使用的颜色值
但是当显示设备颜色深度太低的时候,经过Windows自动转换的图像可能会让人觉得很不自然,所以在有些时候,程序员可能希望预先得知设备的颜色深度,然后根据具体情况显示不同的图形。
显示设备的颜色深度可以用以下函数获取:
invoke GetDeviceCaps,hDC,PLANES
mov ebx,dwPlanes
invoke GetDeviceCaps,hDC,BITSPIXEL
mul ebx
mov dwColorDepth,eax
第一个函数调用返回DC的色彩平面数,第二个函数调用返回每个像素的色彩位数,颜色深度最后可以通过dwPlanes乘以dwBitsPixel得到。
2. Windows中的坐标系
要用GDI函数绘图,就必须首先了解这些函数使用的坐标系,在默认的状态下,Windows坐标系以左上角做坐标原点,以右方当做X坐标的正方向,以下方当做Y坐标的正方向。坐标的数值用一个有符号的16位数来表示,范围从―32 768~32 767,坐标的单位为像素,如图7.3所示。这种坐标系定义方法的好处是:窗口中每一点的坐标不会因为窗口的大小改变而改变,试想一下,如果以数学中通常的表示方法,以左下角做坐标原点,那么当窗口高度被用户调整的时候,客户区中每一点的Y坐标都会变化,在具体使用中就会有诸多不
但是Windows也提供了其他的一些坐标映射方法供程序员使用,可以用SetMap Mode函数来为一个DC设置新的坐标映射方法:
invoke SetMapMode,hDC,iMapMode
可以设置的参数包括坐标原点、坐标的逻辑单位和坐标的正方向等,参数中的iMapMode为新的映射方式,其可以选择的取值如表7.1所示,Windows默认使用的映射方法为MM_TEXT。
表7.1 Windows中可用的坐标映射方式
映 射 方 法
原 点
逻 辑 单 位
X 正 方 向
Y 正 方 向
MM_TEXT(默认方式)
左上
像素
右
下
MM_HIENGLISH
左上
0.001英寸
右
上
MM_LOENGLISH
左上
0.01英寸
右
上
MM_HIMETRIC
左上
0.01毫米
右
上
MM_LOMETRIC
左上
0.1毫米
右
上
MM_TWIPS
左上
1/1440英寸
右
上
MM_ISOTROPIC
可变
可变(x=y)
可变
可变
MM_ANISOTROPIC
可变
可变(x!=y)
可变
可变
可以看到,除了默认的MM_TEXT方式外,下面5种映射方式:MM_HIENGLISH,MM_LOENGLISH,MM_HIMETRIC,MM_LOMETRIC和MM_TWIPS采用的都是原点位于左上角、X正方向向上的映射方式,另外,它们的坐标逻辑单位是不同的。
最后的两种映射方式MM_ISOTROPIC和MM_ANISOTROPIC提供了更灵活的选择,设置为这两种映射方式后,程序可以继续调用SetViewportOrgEx,SetViewportExtEx和SetWindowExtEx函数来自由设置坐标系的原点、逻辑单位和坐标的正方向等所有参数。在其他映射方式下的时候,不能使用这3个设置函数,这时任何对它们的调用都会被忽略。
7.2 绘 制 图 形
有了前面的这些基础,这一节将用一个时钟的例子来演示如何进行简单的绘图,例子的源代码可以在所附光盘的Chapter07\Clock目录中找到,程序运行的结果如图7.4所示。
图7.4 时钟程序的运行结果
资源脚本文件Clock.rc中简单定义了一个用做图标的ico文件:
//>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
#include
//>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
#define ICO_MAIN Ox1000
ICO_MAIN ICON "Main.ico"
源文件Clock.asm如下:
.386
.model flat, stdcall
option casemap :none
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
; Include 文件定义
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
⌨️ 快捷键说明
复制代码
Ctrl + C
搜索代码
Ctrl + F
全屏模式
F11
切换主题
Ctrl + Shift + D
显示快捷键
?
增大字号
Ctrl + =
减小字号
Ctrl + -