除了7.2小节中的绘图函数,块传送函数也是重要的图形操作函数。块传送指把源位置中的数据块按照指定的方式传送到目的位置中。把内存中的位图复制到窗口客户区以及在不同的DC中复制图形数据都要用到块传送操作,块传送完成的工作就相当于图形之间的拷贝工作。块传送函数有PatBlt,BitBlt,MaskBlt,PlgBlt,TransparentBlt和StretchBlt等。
7.4.1 块传送方式
与7.2.4小节中介绍的绘图函数的ROP操作类似,块传送函数也是可以用ROP码来定义的传送方式,但块传送函数的ROP码定义不同于7.2.4小节中的ROP码,因为这里涉及的操作对象更多。
块传送的ROP码是一个32位的整数,对应的操作涉及3种原始数据:源像素、目标像素和画刷,块传送操作的结果是目标像素的数据被3种原始数据的计算结果替换掉。
并不是每一种ROP码都要用到全部3种原始数据,有的甚至连1种也用不到,例如全黑或者全白的ROP码。块传送函数使用的ROP码总共有256种,它们是3种原始数据进行不同位操作(取反、与、或和异或)的组合,但有些ROP码对应的操作结果实在是太难想像了,比如ROP码0e20746对应的操作是((目标像素 xor 画刷) and 源像素) xor 目标像素),凭这个算式的确比较难以想像最后得到的位图是什么样子的!在实际使用中很多算法组合也并不是那么有用,所以Windows只对15种最常用的ROP码定义了预定义的助记代码,
如表7.6所示,对于这些ROP码,在程序中可以直接使用助记码,对于表中没有列出的ROP码,可以直接使用16进制数值。
表7.6 块传送函数中使用的ROP码
ROP码 | 16进制数值 | 新像素点算法 |
BLACKNESS | 00000042h | 全部为0 |
DSTINVERT | 00550009h | not目标像素 |
MERGECOPY | 00c000cah | 画刷 and源像素 |
MERGEPAINT | 00bb0226h | (not源像素)or目标像素 |
NOTSRCCOPY | 00330008h | not源像素 |
NOTSRCERASE | 001100a6h | not(源像素 or目标像素) |
PATCOPY | 00f00021h | 画刷 |
PATINVERT | 005a0049h | 画刷 xor目标像素 |
PATPAINT | 00fb0a09h | 画刷 or (not源像素)or目标像素 |
SRCAND | 008800c6h | 源像素 and目标像素 |
SRCCOPY | 00cc0020h | 源像素 |
SRCERASE | 00440328h | 源像素 and(not目标像素) |
SRCINVERT | 00660046h | 源像素 xor目标像素 |
SRCPAINT | 00ee0086h | 源像素 or目标像素 |
WHITENESS | 00ff0062h | 全部为1 |
7.4.2 块传送函数
1. PatBlt函数
PatBlt函数完成的是“图案块传送”的功能,即“PatternBlockTransfer”。使用的方法是:
invoke PatBlt,hDC,xDest,yDest,dwWidth,dwHeight,dwROP
这个函数将当前画刷的图案拷贝到hDC中以(xDest,yDest)为左上角坐标,dwWidth为宽度,dwHeigth为高度的区域中,传送的方式由dwROP中的ROP码指定。PatBlt函数的功能和矩形填充函数FillRect与InvertRect等是很像的,但它包含了它们的全部功能,如ROP码指定DSTINVERT,那么PatBlt的功能就相当于InvertRect函数;ROP码指定为PATCOPY的时候,PatBlt的功能相当于FillRect函数。
在BmpClock.asm的_CreateBackGround子程序中,当建立背景图片的时候,就是用PATCOPY方式的PatBlt函数用资源中的背景图片填充整个时钟背景的。
在所有的ROP码中,可以用在PatBlt函数中的只有一部分,所有算法中包含源像素的ROP码在PatBlt函数中都不能使用,因为PatBlt函数只涉及当前画刷和目标像素,并没有一个“源像素”,所以对于这个函数来说,表7.6中的ROP码中只有BLACKNESS,WHITENESS,DSTINVERT,PATINVERT和PATCOPY是可用的。
2. BitBlt函数
PatBlt函数能完成的工作BitBlt函数都能完成,BitBlt是“数据块传送”的意思,即“Bit Block Transfer”。BitBlt函数的用法是:
invoke BitBlt,hDcDest,xDest,yDest,dwWidth,dwHeigt,/
hDcSrc,xSrc,ySrc,dwROP
这个函数将源hDcSrc中以(xSrc,ySrc)为左上角的一个矩形区域传送到目标hDcDest中以(xDest,yDest)为左上角的地方去,矩形的宽度为dwWidth,高度为dwHeight,当然目标DC中的最后结果是由dwROP中的ROP码定义的源、目标和画刷三者数据的组合。
灵活使用ROP码可以实现很多的功能,比如在一个背景图片上叠加一个非矩形的位图,游戏程序中人物在背景上面的移动就是这样的一个例子。BmpClock程序中也实现了类似的功能——读者可以注意到,程序可以自由更换背景和边框,但是边框是中空的,它相当于以一个不规则的图形叠加在背景上面,图7.10示范了实现的方法。
分析一下BmpClock.asm中的_CreateBackGround子程序就可以发现,程序用到了资源中的Back1.bmp,Mask1.bmp和Circle1.bmp这3个图片(在图7.10中以A,B,C来表示),子程序将3个图片装入内存后,创建了3个DC来存取它们,对应的DC句柄分别放在@hBmpBack,@hBmpMask和@hBmpCircle中。
好了!现在的任务是将图片C中需要的部分(非黑色部分)透明叠放在以图片A形成的背景上,得到时钟背景图片D。继续做准备工作:为图片D建立一个未初始化位图和内存DC,DC句柄存放在hDcBack中。
图7.10 在背景上叠加不规则图形的方法
如图7.10中的步骤1所示,首先,程序用CreatePatternBrush建立以图片A为图案的画刷,用PatBlt函数以这个画刷填充整个图片:
invoke CreatePatternBrush,@hBmpBack
push eax
invoke SelectObject,hDcBack,eax
invoke PatBlt,hDcBack,0,0,CLOCK_SIZE,CLOCK_SIZE,PATCOPY
pop eax
invoke DeleteObject,eax
现在如果直接将图片C拷贝上去,虽然需要的部分是拷贝上去了,但是图片C中的黑色部分也会覆盖全部的背景像素,为了让黑色部分的背景像素保持不变,应该使用or操作,因为黑色的颜色值为0,任何数据和0进行or操作将保持不变,查看ROP码可以发现,SRCPAINT使用的是or操作,所以可以使用SRCPAINT操作码进行BitBlt操作。
但还有个问题:图片C中的非黑色部分如果也用or操作绘画到背景上,那么经过和背景像素的or操作后就不是原来的颜色值了,为了让这部分不变,解决的办法是预先将背景中对应的部分先绘制成黑色,这样对应图片C中的黑色部分将保持背景颜色,而非黑色部分将使用图片C中的像素。遮掩图片B就是这样用的,它是一个黑白两色的图片,黑色部分是图片C中要在背景上“镂空”的部分,在步骤2中,程序使用下列语句将图片B用SRCAND操作码绘制到背景上:
invoke BitBlt,hDcBack,0,0,CLOCK_SIZE,CLOCK_SIZE,@hDcMask,0,0,SRCAND
查表7.6可以发现,SRCAND进行源像素和目标像素的and操作,任何数和1进行and将保持不变,和0进行and将变成0,这样背景中对应图片B中的白色部分将保持不变,对应图片B中的黑色部分将被“镂空”。
接下来就是最后的步骤3了:
invoke BitBlt,hDcBack,0,0,CLOCK_SIZE,CLOCK_SIZE,@hDcCircle,0,0,SRCPAINT
程序用SRCPAINT操作码将图片C和已经镂空的背景进行or操作,得到的结果就是将由遮掩图片B指定的图片C中的不规则区域画到了背景上面。
为了简化起见,BmpClock程序中的遮掩图片是预先设计好的,实际使用中也可以通过扫描源位图中的像素位,找出背景颜色并动态生成一个遮掩位图,虽然这样可能对速度有一些影响,但灵活性要高得多。
在游戏程序中,将一个不规则的图形如人或物体等图形叠加到背景上面使用的就是这样的技术。
3. MaskBlt函数
MaskBlt函数允许在一个图片中对不同的部分以不同的ROP码进行操作,它的语法是:
invoke MaskBlt,hDcDest,xDest,yDest,dwWidth,dwHeigt,/
hDcSrc,xSrc,ySrc,hMaskBmp,xMask,yMask,dwROP
它和BitBlt的不同之处是多了一个遮掩图片句柄hMaskBmp(注意:是位图句柄而不是DC句柄)以及hMaskBmp的开始坐标。
MaskBlt函数同样是将hDcSrc以(xSrc,ySrc)为左上角的矩形区域以指定ROP码操作方式传送到hDcDest中以(xDest,yDest)为左上角的矩形区域中,矩形的宽度和高度由dwWidth和dwHeight指定,函数的特殊之处是可以指定两个ROP码,传送时使用哪个ROP码要参考遮掩图片,参考的位置从遮掩图片的(xMask,yMask)坐标开始。
hMaskBmp指定了一幅黑白位图,如果位图中对应位置的像素为黑(即为0),那么使用背景ROP码,如果对应位置的像素为白(即为1),那么使用前景ROP码。注意:遮掩位图一定要是黑白两色的,如果使用其他颜色深度的位图,那么函数调用将会失败。
读者一定有个问题:参数中只有一个dwROP参数,如何指定两个ROP码?这正是这个函数比较费解的地方,实际上dwROP参数是两个ROP码的组合,组合的算法是:
((背景ROP码 shl 8)& 0ff000000h)or前景ROP码
灵活使用这个函数可以带来很多的方便,比如在_CreateBackGround子程序中,以“;$$$$$”为注释之间的8句代码完全可以改成下面的4句:
invoke CreatePatternBrush,@hBmpBack
invoke SelectObject,hDcBack,eax
invoke DeleteObject,eax
invoke MaskBlt,hDcBack,0,0,CLOCK_SIZE,CLOCK_SIZE,/
@hDcCircle,0,0,@hBmpMask,0,0,/
((SRCCOPY shl 8) and 0ff000000h) or PATCOPY
相比之下少了一个PatBlt和一个BitBlt调用。因为程序的要求是遮掩图片中黑色的部分使用边框图片,而白色部分使用背景,所以将背景ROP设置为SRCCOPY就可以将需要的边框部分复制过去,而前景ROP使用PATCOPY就免除了使用PatBlt填充背景的步骤。当然,使用前需要把Mask1.bmp和Mask2.bmp重新保存成黑白色,因为现在这两个位图文件是256色格式的。
如果hMaskBmp不指定(参数写为NULL),那么MasmBlt函数就相当于BitBlt函数,只不过ROP是使用dwROP中指定的前景ROP码。
4. PlgBlt函数
PlgBlt实现平行四边形旋转传送操作(Parallelogram Block Transfer),它复制一幅位图,同时将其转换成一个平行四边形,所以利用它可对位图进行旋转处理。PlgBlt函数的使用语法是:
invoke PlgBlt,hdcDest,lpPoint,/
hdcSrc,xSrc,ySrc,dwWidth,dwHeight,/
hBmpMask,xMask,yMask
这个函数和ROP码无关,但它同样指定了一个单色的遮掩位图,遮掩位图中为1的像素对应的位置会被传送,为0的像素不被传送。(xSrc,ySrc)指定了源DC中需要传送的矩形的左上角,dwWidth和dwHeight指定宽度和高度,这个矩形将被旋转后传送到目的DC中去,旋转后的平行四边形位置由lpPoint指定的POINT结构阵列指出。
lpPoint是一个指针,指向含有3个POINT结构的内存中(这种使用POINT结构数组的方法在PolyLine中已经使用过),其中第一个点对应于一个平行四边形的左上角位置,第二个点代表右上角位置,第三个点代表右下角的位置,不需要第四个点是因为它可以从上面3个点的坐标推导出来。
5. StretchBlt函数
StretchBlt函数实现像素的缩放功能,它的语法是:
invoke StretchBlt,hDcDest,xDest,yDest,dwWidthDest,dwHeightDest,/
hDcSrc,xSrc,ySrc,dwWidthSrc,dwHeightSrc,dwROP
这个函数将源hDcSrc中以(xSrc,ySrc)为左上角,dwWidthSrc和dwHeightSrc为宽度和高度的矩形以dwROP指定的方式传送到目标hDcDest中去,目标位置是(xDest,yDest),新的矩形区域大小为dwWidthDest和dwHeight,如果源DC中的矩形大小和目标DC中的不一样,函数会将像素数据自动缩放。
但是StretchBlt对像素的缩放办法仅仅是删除多余的像素(从大到小)或者重复像素(从小到大),并不像一些图形处理软件一样进行插值计算,所以缩放的效果并不好,笔者建议如果能够不用这个函数就不要去用它,除非对图形的质量并没有要求。
6. TransparentBlt函数
还有一个函数可以方便地实现不规则区域的像素拷贝操作,那就是TransparentBlt函数,它的用法如下:
invoke TransparentBlt,hDcDest,xDest,yDest,dwWidthDest,dwHeightDest,/
hDcSrc,xSrc,ySrc,dwWidthSrc,dwHeightSrc,crTransparent
可以看出,它和StretchBlt函数的参数很像,也有dwWidthSrc和dwHeightSrc参数,难道这个函数也可以缩放吗?答案是肯定的,它也可以进行像素缩放。TransparentBlt函数的最后一个参数指定了一个透明色,源DC指定的矩形区域中和这个颜色相同的像素不会被拷贝,所以,BmpClock程序中的下列两个语句:
invoke BitBlt,hDcBack,0,0,CLOCK_SIZE,CLOCK_SIZE,@hDcMask,0,0,SRCAND
invoke BitBlt,hDcBack,0,0,CLOCK_SIZE,CLOCK_SIZE,/
@hDcCircle,0,0,SRCPAINT
完全可以用下面这一句来代替:
invoke TransparentBlt,hDcBack,0,0,CLOCK_SIZE,CLOCK_SIZE,/
@hDcCircle,0,0,CLOCK_SIZE,CLOCK_SIZE,0
这样甚至连遮掩图片Back1.bmp和Back2.bmp都可以省掉了。
当然,这个函数的传送方式使用拷贝方式,如果需要用到ROP码,那么只有使用其他函数了。由此可见,各种块传送函数都有它们的优缺点,在实际应用中,最好的办法就是根据实际情况灵活使用。
TransparentBlt函数并不包含在Gdi32.dll中,而是在Msimg32.dll中,所以使用时注意在源程序头部加上下面的包含语句:
include Msimg32.inc
includelib Msimg32.lib