第7章 位图
位图也称点阵图像或光栅图像,通过使用一格一格的像素点来描述图像。位图的文件类型有很多,例如.bmp、.jpg、.png、.gif等。位图由一个个像素点组成。当放大位图时,像素点随之放大,但是每个像素点所表示的颜色是单一的,所以在位图放大以后就会出现马赛克状。位图的优点是色彩比较丰富,可以逼真地展现自然界的各类实物,颜色信息越多,图像色彩越丰富,但是占用的存储空间就越大。通常会拿位图和矢量图作比较,矢量图是根据几何特性绘制图形,矢量图并不保存图像的具体颜色信息,而是对图像的描述,矢量图可以任意放大而不影响清晰度。矢量图色彩不丰富,无法表现逼真的实物,不过矢量图的文件通常很小。矢量图通常用来制作图标、Logo、图纸、动画等简单直接的图像。做矢量图的软件有Illustrator、CorelDRAW、AutoCAD等。矢量图的文件类型有.ai、.cdr、.dwg、 .wmf、.emf等,.wmf和.emf是Windows图元文件格式。
位图分为两种,设备相关位图(Device Dependent Bitmap,DDB)和设备无关位图(Device Independent Bitmap,DIB)。DDB也称为GDI位图或设备兼容位图,是16位Windows中唯一可用的位图格式。然而,随着显示技术的改进和各种类型显示设备的增加,一些问题浮出水面,这些问题只能通过设备无关位图DIB来解决。DDB显示的图像随着计算机显示设备设置的不同而不同,因此DDB一般不存储为文件,而是作为程序运行时的内部位图格式在内存中使用。DDB是由GDI和设备驱动程序管理的GDI对象,与DIB相比,DDB具有更好的GDI性能,在一些场合仍然很有用。
DIB不依赖于设备,因为DIB格式包含了完整的图像信息,可以在不同的设备上显示。Windows程序中直接支持的DIB文件格式是.bmp。.bmp位图通常没有压缩,.bmp文件比较大,例如一个1024 768分辨率的.bmp文件的大小为1024 768 3字节/像素,再加上54字节的位图文件头,一共需要2 359 350字节(2.25 MB)的存储空间,因此在网络上使用更多的是.jpg、.png、.gif等经过压缩的位图。没有压缩的好处是执行速度快,不需要解码,复制到显卡中就可以直接显示在屏幕上。
与位图相关的参数有宽度、高度、颜色深度等。宽度和高度以像素为单位。颜色深度是指位图中要用多少个二进制位来表示每个像素点的颜色,常用的颜色深度有1位(单色)、2位(4色,CGA)、4位(16色,VGA)、8位(256色)、16位(增强色)、24位和32位等。每像素只有1位的位图称为单色位图,每个像素值不是0就是1,0代表黑色,1代表白色。要想表示更多颜色,就需要每个像素有更多的位,2位可以有4种颜色,4位有16种颜色,8位有256种颜色,16位有65 536种颜色,24位(即一个像素需要3字节来描述)则有16 777 216种颜色。24位色称为真彩色,它可以达到人眼分辨的极限,颜色数是1 677万多种,即2的24次方。但32位色并不是2的32次方颜色数,32位色也是1 677万多种,不过它增加了256阶颜色的灰度。有的显卡能达到36位色,它是27位颜色数再加512阶颜色灰度。
7.1 DDB
Windows有4种类型的设备环境(DC),分别是显示DC、打印DC、内存DC(也称内存兼容DC)和信息DC,每种类型的DC都有特定的用途。前面我们都是在显示DC上进行绘图。
DDB通常作为程序运行时的内部位图格式在内存中使用。DDB也称为GDI位图或设备兼容位图,是与内存DC关联的图形对象之一,GDI可以直接写入DDB。通常,DC对应于特定的图形输出设备(例如显示器),但内存DC只存在于内存,它不是一个真实的图形输出设备,它和特定的真实设备“兼容”。有了这个内存DC句柄,就可以和在真实DC上一样执行GDI绘图操作。
要创建一个内存DC,必须有一个对应于真实设备的DC句柄。CreateCompatibleDC函数创建与指定设备兼容的内存DC:
HDC CreateCompatibleDC(_In_ HDC hdc); // 设备环境句柄,通常指定为对应于真实设备的现有DC的句柄
hdc参数通常指定为对应于真实设备的现有DC的句柄。如果设置为NULL,则该函数将创建与应用程序当前显示器兼容的内存DC。如果函数执行成功,则返回一个内存DC句柄;如果函数执行失败,则返回值为NULL。例如下面的代码:
hdc = GetDC(hwnd);
hdcMem = CreateCompatibleDC(hdc);
// …………
ReleaseDC(hwnd, hdc);
但是,CreateCompatibleDC函数创建的内存DC只有1像素宽,1像素高,而且是单色,因此在内存DC中进行绘制操作以前,应该在内存DC中选入合适宽度和高度的DDB。要将DDB选入内存DC,就要先调用CreateCompatibleBitmap函数创建与指定DC关联的设备兼容的位图:
HBITMAP CreateCompatibleBitmap(
_In_ HDC hdc, // 设备环境句柄,通常指定为对应于真实设备的现有DC的句柄
_In_ int nWidth, // 位图宽度,以像素为单位
_In_ int nHeight); // 位图高度,以像素为单位
函数执行成功,返回值是DDB的句柄。例如下面的代码:
hdc = GetDC(hwnd);
hdcMem = CreateCompatibleDC(hdc);
hBitmap = CreateCompatibleBitmap(hdc, nWidth, nHeight);
SelectObject(hdcMem, hBitmap);
// 绘图操作
DeleteObject(hBitmap);
DeleteDC(hdcMem);
ReleaseDC(hwnd, hdc);
调用GDI绘图函数在内存DC上进行绘图,就会画在设备兼容位图上。一些绘图程序就是使用这种方式,创建一个DDB作为画布。当不再需要内存DC时,需要调用DeleteDC函数删除内存DC;当不再需要设备兼容位图时,需要调用DeleteObject函数将其删除,这和删除逻辑画笔、逻辑画刷、逻辑字体等一样。
选入内存DC中的位图也可以通过调用LoadBitmap或LoadImage函数加载DIB获得。此时不需要像CreateCompatibleBitmap函数那样指定宽度和高度,DIB有自己的宽度值和高度值,这两个函数返回位图句柄和设备兼容。添加位图资源的方法非常简单,只需要选择Bitmap资源类型单击导入即可。资源脚本文件中对位图资源的定义格式如下所示:
IDB_BITMAP BITMAP "someimg.bmp"
先看一个示例,再解释几个函数。DDB程序从程序资源或文件中加载一副位图,将返回的位图句柄选入内存DC,在位图上输出一些文字,然后把这幅位图显示到程序的客户区中。程序运行效果如原书的图7.1所示。
DDB.cpp源文件的部分内容如下所示:
LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
PAINTSTRUCT ps;
HDC hdc, hdcMem;
HBITMAP hBitmap;
BITMAP bmp;
switch (uMsg)
{
case WM_PAINT:
hdc = BeginPaint(hwnd, &ps);
hdcMem = CreateCompatibleDC(hdc);
//hBitmap = LoadBitmap(g_hInstance, MAKEINTRESOURCE(IDB_GIRL));
hBitmap = (HBITMAP)LoadImage(NULL, TEXT("Girl.bmp"), IMAGE_BITMAP, 0, 0,
LR_LOADFROMFILE);
SelectObject(hdcMem, hBitmap);
// 绘图操作
SetBkMode(hdcMem, TRANSPARENT);
TextOut(hdcMem, 10, 10, TEXT("窈窕淑女 君子好逑"), _tcslen(TEXT("窈窕淑女 君子好逑")));
// 把内存位图复制到窗口客户区中
GetObject(hBitmap, sizeof(bmp), &bmp);
BitBlt(hdc, 0, 0, bmp.bmWidth, bmp.bmHeight, hdcMem, 0, 0, SRCCOPY);
EndPaint(hwnd, &ps);
DeleteObject(hBitmap);
DeleteDC(hdcMem);
return 0;
case WM_DESTROY:
PostQuitMessage(0);
return 0;
}
return DefWindowProc(hwnd, uMsg, wParam, lParam);
}
完整代码参见Chapter7\\DDB项目。
LoadBitmap函数用于从指定的模块中加载指定的位图资源:
HBITMAP LoadBitmap(
_In_ HINSTANCE hInstance, // 模块句柄
_In_ LPCTSTR lpBitmapName); // 要加载的位图资源的名称,可以使用MAKEINTRESOURCE宏
如果函数执行成功,则返回指定位图的句柄;如果函数执行失败,则返回值为NULL。
也可以把hInstance参数设置为NULL,加载系统预定义的位图,此时lpBitmapName参数可以通过MAKEINTRESOURCE宏使用表7.1所示的值之一。
表7.1
| OBM_BTNCORNERS | |
| OBM_BTSIZE | |
| OBM_CHECK | |
| OBM_CHECKBOXES | |
| OBM_CLOSE | |
| OBM_COMBO | |
| OBM_DNARROW | |
| OBM_DNARROWD | |
| OBM_DNARROWI | |
| OBM_LFARROW | |
| OBM_LFARROWD | |
| OBM_LFARROWI | |
| OBM_MNARROW | |
| OBM_REDUCE | |
| OBM_REDUCED | |
| OBM_RESTORE | |
| OBM_RESTORED | |
| OBM_RGARROW | |
| OBM_RGARROWD | |
| OBM_RGARROWI | |
| OBM_SIZE | |
| OBM_UPARROW | |
| OBM_UPARROWD | |
| OBM_UPARROWI | |
| OBM_ZOOM | |
| OBM_ZOOMD |
如果需要使用OBM_开头的常量,在Windows.h头文件之前必须定义常量OEMRESOURCE。
LoadImage函数可以加载图标、光标、位图等,该函数返回相应的图像句柄:
HANDLE WINAPI LoadImage(
_In_opt_ HINSTANCE hinst, // 模块句柄,如果需要加载系统的预定义或加载文件,则设置为NULL
_In_ LPCTSTR lpszName, // 要加载的图像的名称
_In_ UINT uType, // 要加载的图像类型,IMAGE_ICON、IMAGE_CURSOR或IMAGE_BITMAP
_In_ int cxDesired,// 图像的宽度,以像素为单位,设置为0表示使用资源的实际宽度
_In_ int cyDesired,// 图像的高度,以像素为单位,设置为0表示使用资源的实际高度
_In_ UINT fuLoad); // 加载选项
参数fuLoad指定加载选项,常用的值如表7.2所示。
表7.2
| LR_DEFAULTCOLOR | 默认值 |
| LR_DEFAULTSIZE | 如果cxDesired或cyDesired值设置为0,则使用GetSystemMetrics(SM_CXICON或SM_CXCURSOR)和GetSystemMetrics(SM_CYICON或SM_CYCURSOR)获取的宽度和高度值;如果未指定该标志且cxDesired和cyDesired设置为0,则使用图像文件的实际尺寸 |
| LR_LOADFROMFILE | 从文件加载图标、光标或位图 |
| LR_MONOCHROME | 以黑白方式加载图像 |
| LR_SHARED | 如果对同一资源多次调用本函数,则直接使用先前返回的图像句柄,而不会再去调用本函数。加载系统图标或光标时,必须使用LR_SHARED |
| LR_CREATEDIBSECTION | 当uType参数指定了IMAGE_BITMAP时,该函数返回DIB节位图而不是设备兼容位图 |
如果没有指定LR_SHARED标志,当不再使用加载的图标、光标或位图时,则需要调用DestroyIcon删除图标,调用DestroyCursor删除光标,调用DeleteObject删除位图。
如果函数执行成功,则返回新加载图像的句柄;如果函数执行失败,则返回值为NULL。
GetObject函数用于获取指定图形对象(画笔、画刷、字体和位图等)的信息:
int GetObject(
_In_ HGDIOBJ hgdiobj, // 图形对象句柄
_In_ int cbBuffer, // 缓冲区的大小
_Out_ LPVOID lpvObject); // 存放信息的缓冲区
如果函数执行成功,并且lpvObject参数是一个有效的缓冲区指针,则返回值为存储在缓冲区中的字节数,如果lpvObject参数设置为NULL,函数返回所需的缓冲区大小;如果函数执行失败,则返回值为0。
位图的基本信息可以用一个BITMAP结构来描述,该结构在wingdi.h头文件中定义如下:
typedef struct tagBITMAP
{
LONG bmType; // 位图类型,必须为0
LONG bmWidth; // 位图的宽度,以像素为单位
LONG bmHeight; // 位图的高度,以像素为单位
LONG bmWidthBytes;// 位图中每一个像素行中的字节数,必须是2的倍数,WORD对齐
WORD bmPlanes; // 颜色平面的数量,可通过GetDeviceCaps(hdc, PLANES)获取,通常是1
WORD bmBitsPixel; // 每个像素使用的位数,可通过GetDeviceCaps(hdc, BITSPIXEL)获取,通常是32
LPVOID bmBits; // 指向位图像素位数据的指针
} BITMAP, *PBITMAP, NEAR *NPBITMAP, FAR *LPBITMAP;
图形设备有一个颜色平面数的概念。不同的图形设备保存像素数据的方法不同,有的用一个颜色平面,有的用多个颜色平面。每个像素点的颜色位数 = 颜色深度(每个像素使用的位数) 颜色平面数,颜色深度和颜色平面数表示颜色格式。DDB的颜色格式与显示器设备的颜色格式相匹配,和显示DC有相同的颜色内存组织,这就是设备兼容的概念。这样的内存组织如果放在其他显示设备上,显示出来的颜色可能不同,这就是设备相关。再详细解释就比较复杂了,对于位图,本书只介绍编程中常用的位图操作,更深入的描述是数字图像处理的范围。现在的计算机通过调用GetDeviceCaps(hdc, PLANES)函数获取到的颜色平面数是1,通过调用GetDeviceCaps(hdc, BITSPIXEL)函数获取到的每个像素点的颜色位数是32。
7.2 位块传送
本节介绍位块传送函数BitBlt、PatBlt、MaskBlt、PlgBlt、StretchBlt和TransparentBlt的用法。虽然函数比较多,但是用法基本一致,了解BitBlt函数的用法以后,其他的就很简单了。
BitBlt(Bit Block Transfer,位块传送)函数把源DC中的指定矩形区域复制到目标DC中的指定位置,该函数是最常使用的:
BOOL BitBlt(
_In_ HDC hdcDest, // 目标设备环境句柄
_In_ int nXDest, // 目标矩形左上角的X坐标
_In_ int nYDest, // 目标矩形左上角的Y坐标
_In_ int nWidth, // 源矩形和目标矩形的宽度
_In_ int nHeight, // 源矩形和目标矩形的高度
_In_ HDC hdcSrc, // 源设备环境句柄
_In_ int nXSrc, // 源矩形左上角的X坐标
_In_ int nYSrc, // 源矩形左上角的Y坐标
_In_ DWORD dwRop); // 光栅操作码,通常指定为SRCCOPY
参数dwRop指定光栅操作码ROP,前面介绍过绘图模式(二元光栅操作ROP2),ROP2定义画笔、画刷的颜色与目标显示区域颜色的混合方式。这里的光栅操作是三元光栅操作,块传送的ROP码是一个32位的整数,对应的操作涉及3个对象:源像素、目标像素和画刷。块传送函数使用的ROP码有256种,它们是对3个对象进行不同位运算(取反、与、或、异或)的结果。有些ROP码对应的操作结果实在太难想像,例如ROP码0x00E20746对应的操作是((目标像素 ^ 画刷) & 源像素) ^ 目标像素,很难想象最后得到的位图是什么样子。在实际使用中很多算法组合并不常用,所以Windows只对15种常用的ROP码定义了常量。并不是每一种ROP码都要用到全部3个对象,有的甚至连1个也用不到,例如全黑BLACKNESS或全白WHITENNESS的ROP码。15种常用的ROP码如表7.3所示。
例如,设置画刷(B)为11110000,设置源像素(S)为11001100,设置目标像素(D)为10101010,依据位运算规则得到的运算结果如表7.3所示。
表7.3
| 0 | 00000000 | 0x000042 | BLACKNESS |
| 1 | 11111111 | 0xFF0062 | WHITENNESS |
| S | 11001100 | 0xCC0020 | SRCCOPY |
| ~S | 00110011 | 0x330008 | NOTSRCCOPY |
| ~D | 01010101 | 0x550009 | DSTINVERT |
| B | 11110000 | 0xF00021 | PATCOPY |
| B ^ D | 01011010 | 0x5A0049 | PATINVERT |
| S ^ D | 01100110 | 0x660046 | SRCINVERT |
| S | D | 11101110 | 0xEE0086 |
| B & S | 11000000 | 0xC000CA | MERGECOPY |
| S & D | 10001000 | 0x8800C6 | SRCAND |
| S & ~D | 01000100 | 0x440328 | SRCERASE |
| ~(S | D) | 00010001 | 0x1100A6 |
| ~S | D | 10111011 | 0xBB0226 |
| B | ~S | D | 11111011 |
前3种不需要解释。NOTSRCCOPY是把源位图的每个位取反得到的颜色,DSTINVERT是把目标区域的每个位取反得到的颜色,PATCOPY则直接使用画刷的颜色。其他的(例如PATINVERT、SRCINVERT、SRCPAINT、MERGECOPY、SRCAND等)有时候也可能用到。
接下来实现一个使用SRCPAINT光栅操作码的示例。我想把DDB程序显示的人物裁剪一下,只显示中间的椭圆部分,SRCPAINT程序执行效果如原书的图7.2所示。
SRCPAINT.cpp源文件的部分内容如下所示:
LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
PAINTSTRUCT ps;
HDC hdc;
static HBITMAP hBitmap, hBitmapMask;
static BITMAP bmp;
static HDC hdcMem, hdcMemMask;
RECT rect;
switch (uMsg)
{
case WM_CREATE:
hdc = GetDC(hwnd);
// 源位图,加载人物,hdcMem
hdcMem = CreateCompatibleDC(hdc);
hBitmap = (HBITMAP)LoadImage(NULL, TEXT("Girl.bmp"), IMAGE_BITMAP, 0, 0,
LR_LOADFROMFILE);
SelectObject(hdcMem, hBitmap);
GetObject(hBitmap, sizeof(bmp), &bmp);
// 掩码位图,白色背景黑色椭圆,hdcMemMask
hdcMemMask = CreateCompatibleDC(hdc);
hBitmapMask = CreateCompatibleBitmap(hdc, bmp.bmWidth, bmp.bmHeight);
SelectObject(hdcMemMask, hBitmapMask);
SelectObject(hdcMemMask, GetStockObject(NULL_PEN));
Rectangle(hdcMemMask, 0, 0, bmp.bmWidth + 1, bmp.bmHeight + 1);
SelectObject(hdcMemMask, GetStockObject(BLACK_BRUSH));
Ellipse(hdcMemMask, 0, 0, bmp.bmWidth + 1, bmp.bmHeight + 1);
SelectObject(hdcMemMask, GetStockObject(BLACK_PEN));
SelectObject(hdcMemMask, GetStockObject(WHITE_BRUSH));
ReleaseDC(hwnd, hdc);
// 掩码位图复制到内存位图,并执行光栅操作
SetRect(&rect, 0, 0, bmp.bmWidth, bmp.bmHeight);
SetBkMode(hdcMem, TRANSPARENT);
SetTextColor(hdcMem, RGB(255, 255, 255));
DrawText(hdcMem, TEXT("窈窕淑女 君子好逑"), _tcslen(TEXT("窈窕淑女 君子好逑")),
&rect, DT_CENTER | DT_VCENTER | DT_SINGLELINE);
BitBlt(hdcMem, 0, 0, bmp.bmWidth, bmp.bmHeight, hdcMemMask, 0, 0, SRCPAINT);
// 设置窗口大小
AdjustWindowRectEx(&rect, GetWindowLongPtr(hwnd, GWL_STYLE),
GetMenu(hwnd) != NULL, GetWindowLongPtr(hwnd, GWL_EXSTYLE));
SetWindowPos(hwnd, NULL, 0, 0, rect.right – rect.left, rect.bottom – rect.top,
SWP_NOZORDER | SWP_NOMOVE);
return 0;
case WM_PAINT:
hdc = BeginPaint(hwnd, &ps);
// 将执行光栅操作的内存位图复制到窗口客户区中
BitBlt(hdc, 0, 0, bmp.bmWidth, bmp.bmHeight, hdcMem, 0, 0, SRCCOPY);
EndPaint(hwnd, &ps);
return 0;
case WM_DESTROY:
DeleteObject(hBitmap);
DeleteObject(hBitmapMask);
DeleteDC(hdcMem);
DeleteDC(hdcMemMask);
PostQuitMessage(0);
return 0;
}
return DefWindowProc(hwnd, uMsg, wParam, lParam);
}
完整代码参见Chapter7\\SRCPAINT。hdcMem和hdcMemMask都定义为静态变量,调用BeginPaint / EndPaint以及GetDC等函数获取的hdc的使用时间不能超出本条消息,但是使用CreateCompatibleDC函数创建的hdc就没有这个限制,可以在任何时候创建并且一直使用到不再需要为止。
SRCPAINT即S | D,黑色矩形的颜色位全为0,0和任何数按位或,其结果不变,就是说黑色矩形范围内的人物图片不变;黑色矩形以外的部分是白色的,颜色位全为1,1和任何数按位或,其结果为1,因此调用BitBlt以后黑色矩形以外的部分显示为白色。
实际应用中,可能会遇到更复杂的情况。大家可以使用Photoshop制作掩码位图,然后使用合适的光栅操作码进行位块传送操作。
PatBlt函数使用当前画刷填充矩形,该函数的光栅操作只涉及画刷颜色和目标区域颜色,可用的光栅操作码有PATCOPY、PATINVERT、DSTINVERT、BLACKNESS和WHITENNESS:
BOOL PatBlt(
_In_ HDC hdc, // 目标设备环境句柄
_In_ int nXLeft, // 要填充的矩形左上角的X坐标
_In_ int nYLeft, // 要填充的矩形左上角的Y坐标
_In_ int nWidth, // 矩形的宽度
_In_ int nHeight, // 矩形的高度
_In_ DWORD dwRop); // 光栅操作码
PatBlt函数的功能和矩形填充函数FillRect与InvertRect等类似,但PatBlt包含了它们的全部功能,例如ROP码被指定为DSTINVERT,那么PatBlt的功能就相当于InvertRect函数;ROP码被指定为PATCOPY,PatBlt的功能就相当于FillRect函数。当然,PatBlt函数能完成的工作BitBlt函数也都能完成(不指定源DC句柄及相关参数即可)。例如,下面的BitBlt和PatBlt两个函数调用实现的功能完全相同:
SelectObject(hdc, CreateHatchBrush(HS_BDIAGONAL, RGB(255, 0, 0)));
BitBlt(hdc, 0, 0, 100, 100, NULL, 0, 0, PATCOPY);
PatBlt(hdc, 0, 0, 100, 100, PATCOPY);
和BitBlt函数相比,MaskBlt函数多了掩码位图句柄和掩码位图的开始坐标参数:
BOOL MaskBlt(
_In_ HDC hdcDest, // 目标设备环境句柄
_In_ int nXDest, // 目标矩形左上角的X坐标
_In_ int nYDest, // 目标矩形左上角的Y坐标
_In_ int nWidth, // 源矩形和目标矩形的宽度
_In_ int nHeight, // 源矩形和目标矩形的高度
_In_ HDC hdcSrc, // 源设备环境句柄
_In_ int nXSrc, // 源矩形左上角的X坐标
_In_ int nYSrc, // 源矩形左上角的Y坐标
_In_ HBITMAP hbmMask, // 掩码位图的句柄,必须是单色位图
_In_ int xMask, // 掩码位图左上角的X坐标
_In_ int yMask, // 掩码位图左上角的Y坐标
_In_ DWORD dwRop); // 两个光栅操作码,使用MAKEROP4宏
和BitBlt函数相比,MaskBlt多了hbmMask、xMask和yMask这3个参数。另外,dwRop可以使用MAKEROP4宏指定两个ROP码。
-
参数dwRop是一个DWORD类型。背景光栅操作码存储在dwRop的高位字的高位字节中,前景光栅操作码存储在dwRop的高位字的低位字节中,dwRop的低位字被忽略,应为0。宏MAKEROP4可以创建前景和背景光栅操作代码的组合。
-
参数hbmMask指定一幅黑白位图,如果位图中对应位置的像素位为黑(为0),那么使用背景ROP码来对源和目标进行位运算;如果对应位置的像素位为白(为1),那么使用前景ROP码来对源和目标进行位运算。如果没有提供掩码位图,则MaskBlt函数的功能和BitBlt函数完全相同,使用前景光栅操作代码。
注意:掩码位图要求必须是黑白两色的,如果使用其他颜色深度的位图,那么函数调用将会失败。可以使用Photoshop自己制作掩码位图,也可以调用CreateBitmap或CreateBitmapIndirect函数创建指定宽度、高度和颜色格式的DDB,这两个函数适合于创建单色位图。如果感觉MaskBlt函数不易理解,则完全可以通过多次调用BitBlt函数来达到同样的效果。
和BitBlt函数一样,PlgBlt函数也是把源DC中的指定矩形区域复制到目标DC中的指定位置。BitBlt函数的目标位置由nXDest、nYDest、nWidth和nHeight这4个参数指定,而PlgBlt函数的目标位置由lpPoint参数指向的3个POINT数组指定,先看函数原型:
BOOL PlgBlt(
_In_ HDC hdcDest, // 目标设备环境句柄
_In_ const POINT *lpPoint,// POINT结构数组
_In_ HDC hdcSrc, // 源设备环境句柄
_In_ int nXSrc, // 源矩形左上角的X坐标
_In_ int nYSrc, // 源矩形左上角的Y坐标
_In_ int nWidth, // 源矩形的宽度
_In_ int nHeight, // 源矩形的高度
_In_ HBITMAP hbmMask, // 掩码位图的句柄,必须是单色位图,用于屏蔽源矩形的颜色
_In_ int xMask, // 掩码位图左上角的X坐标
_In_ int yMask); // 掩码位图左上角的Y坐标
-
参数lpPoint指向含有3个POINT结构的数组(这种使用POINT结构数组的方法在PolyLine函数中已经使用过),其中第1个点指定矩形的左上角,第2个点指定右上角,第3个点指定左下角,不需要第4个点是因为系统可以通过上面3个点的坐标推导出来。可以自己指定矩形的点,说明我们可以把矩形设计成平行四边形,这样一来图像可以被斜角拉伸,还可以旋转、缩放等。
-
参数hbmMask指定掩码位图的句柄,必须是单色位图,用于屏蔽源矩形的颜色。如果掩码位图对应的像素位的值为1,则把源矩形的对应像素位复制到目标矩形;如果为0,则不复制。
StretchBlt函数也和BitBlt函数一样,把源DC中的指定矩形区域复制到目标DC中的指定位置,但是StretchBlt函数可以分别指定目标矩形和源矩形的宽度和高度,即该函数可以实现缩放:
BOOL StretchBlt(
_In_ HDC hdcDest, // 目标设备环境句柄
_In_ int nXOriginDest, // 目标矩形左上角的X坐标
_In_ int nYOriginDest, // 目标矩形左上角的Y坐标
_In_ int nWidthDest, // 目标矩形的宽度
_In_ int nHeightDest, // 目标矩形的高度
_In_ HDC hdcSrc, // 源设备环境句柄
_In_ int nXOriginSrc, // 源矩形左上角的X坐标
_In_ int nYOriginSrc, // 源矩形左上角的Y坐标
_In_ int nWidthSrc, // 源矩形的宽度
_In_ int nHeightSrc, // 源矩形的高度
_In_ DWORD dwRop); // 光栅操作码
这个函数将源hdcSrc中以(nXOriginSrc, nYOriginSrc)为左上角,宽度和高度分别为nWidthSrc和nHeightSrc的矩形以dwRop指定的光栅操作方式传送到目标hdcDest中,目标矩形左上角为(nXOriginDest, nYOriginDest),目标矩形区域的宽度和高度分别为nWidthDest和nHeightDest,宽度和高度可以指定为负值以达到镜像的目的。如果源DC中的矩形大小和目标DC中的矩形大小不一样,则函数会将像素数据自动拉伸缩放,但是StretchBlt函数对像素的缩放方式仅仅是删除多余的像素(从大到小)或者重复像素(从小到大),并不像一些图形处理软件一样可以进行插值计算,所以拉伸缩放的效果并不好,只适用于对图形质量要求不高的场合。
与DC相关的5种图形模式,我们已经学过背景模式、绘图模式(二元三元光栅操作)、映射模式、多边形填充模式,还有一个拉伸模式。SetStretchBltMode函数可以设置位图的拉伸模式:
int SetStretchBltMode(
_In_ HDC hdc, // 设备环境句柄
_In_ int iStretchMode); // 拉伸模式
参数iStretchMode指定拉伸模式,可以是表7.4所示的值之一。
表7.4
| COLORONCOLOR或STRETCH_DELETESCANS | StretchBlt函数只是简单地去掉像素行或列,而不做任何逻辑操作。这对彩色位图来说是最佳的方法 |
| BLACKONWHITE或STRETCH_ANDSCANS | 这是默认情况,如果两个或多个像素必须被结合成一个像素,StretchBlt将对像素进行逻辑与操作。只有当所有的像素都是白色时,才是白色,即黑色像素比白色像素占优势。这对以白色为底,图像主要是黑色的单色位图来说效果比较好 |
| WHITEONBLACK或STRETCH_ORSCANS | 如果两个或多个像素必须被结合成一个像素,StretchBlt将对像素进行逻辑或操作。只有当所有的像素都是黑色时,才是黑色,即白色像素比黑色像素占优势。这对以黑色为底,图像主要是白色的单色位图来说效果比较好 |
| HALFTONE或STRETCH_HALFTONE | Windows根据要结合的源的颜色,计算平均目标颜色,设置为该模式以后必须调用SetBrushOrgEx函数来设置画刷原点 |
介绍最后一个位块传送函数TransparentBlt,意即透明传送。TransparentBlt函数和StretchBlt函数的唯一区别是把光栅操作参数替换为了透明颜色参数:
BOOL TransparentBlt(
_In_ HDC hdcDest, // 目标设备环境句柄
_In_ int xoriginDest, // 目标矩形左上角的X坐标
_In_ int yoriginDest, // 目标矩形左上角的Y坐标
_In_ int wDest, // 目标矩形的宽度
_In_ int hDest, // 目标矩形的高度
_In_ HDC hdcSrc, // 源设备环境句柄
_In_ int xoriginSrc, // 源矩形左上角的X坐标
_In_ int yoriginSrc, // 源矩形左上角的Y坐标
_In_ int wSrc, // 源矩形的宽度
_In_ int hSrc, // 源矩形的高度
_In_ UINT crTransparent);// 指定一个RGB颜色值,源位图中的这个颜色视为透明
crTransparent参数指定一个透明色,源hdcSrc指定的矩形区域中和这个颜色相同的像素不会被复制。如果需要用到ROP码,那么只能使用其他函数了。各种位块传送函数都有它们的优缺点,在实际应用中,可以根据实际情况灵活使用。
7.3 DIB
DIB在Windows程序中用得非常广泛,例如图标、光标、最小化/最大化按钮等都是用的位图,不过在格式上存在微小的区别。Windows程序中直接支持的DIB文件格式是.bmp,本节介绍.bmp文件格式。
.bmp文件的开始是一个BITMAPFILEHEADER结构,称为文件头,该结构共14字节,在wingdi.h头文件中定义如下:
typedef struct tagBITMAPFILEHEADER {
WORD bfType; // 文件类型,或者说是文件签名,必须是BM,也就是0x4D42
DWORD bfSize; // 整个位图文件的大小,以字节为单位
WORD bfReserved1;// 保留字段,必须为0
WORD bfReserved2;// 保留字段,必须为0
DWORD bfOffBits; // 位图像素数据的偏移量,通常是0x36,即从第54字节往后就是像素数据
} BITMAPFILEHEADER, FAR *LPBITMAPFILEHEADER, *PBITMAPFILEHEADER;
BITMAPFILEHEADER结构的后面通常是一个BITMAPINFOHEADER结构,称为信息头。该结构共40字节,在wingdi.h头文件中定义如下:
typedef struct tagBITMAPINFOHEADER {
DWORD biSize; // 该结构的大小,0x28,即40
LONG biWidth; // 位图的宽度,以像素为单位
LONG biHeight; // 位图的高度,以像素为单位
WORD biPlanes; // 目标设备的颜色平面数,设置为1
WORD biBitCount; // 每像素位数,彩色位图通常是0x18或0x20,即24或32
DWORD biCompression; // 位图的压缩类型,通常是BI_RGB(0)表示未压缩
DWORD biSizeImage; // 像素数据的大小,不包括文件头。对于BI_RGB位图,可以设置为0
LONG biXPelsPerMeter; // 目标设备的水平分辨率,单位是像素/米,可以设置为0
LONG biYPelsPerMeter; // 目标设备的垂直分辨率,单位是像素/米,可以设置为0
DWORD biClrUsed; // 颜色表中实际使用的颜色索引数,彩色位图没有颜色表,所以是0
DWORD biClrImportant; // 显示位图所需的颜色索引数,0
} BITMAPINFOHEADER, FAR *LPBITMAPINFOHEADER, *PBITMAPINFOHEADER;
可以看到,文件头和信息头结构包含了与位图相关的一些参数。在这两个数据结构的后面,是位图的像素数据。对于24位位图,每个像素用3字节来表示,这3字节分别表示像素的蓝色、绿色和红色值。整个.bmp文件由这3个部分组成。
7.4 DDB与DIB
GetDIBits函数可以把指定设备兼容位图DDB的像素数据转换为设备无关位图DIB像素数据:
int GetDIBits(
_In_ HDC hdc, // 设备环境句柄
_In_ HBITMAP hbmp, // DDB兼容位图句柄
_In_ UINT uStartScan,// 起始扫描行,获取整张位图就设置为0
_In_ UINT cScanLines,// 扫描行数,获取整张位图就设置为位图的高度
_Out_ LPVOID lpvBits, // 在这个参数中返回指定格式的DIB像素数据
_Inout_ LPBITMAPINFO lpbi, // BITMAPINFO结构的指针,指定DIB的格式,可使用
// BITMAPINFOHEADER结构
_In_ UINT uUsage); // 通常设置为DIB_RGB_COLORS(0)
参数lpvBits返回指定格式的DIB像素数据,这些数据可以保存为位图文件,不过还需要位图文件头和信息头数据,lpvBits指向的缓冲区不包含这两个结构的数据;这些数据可以通过调用SetDIBitsToDevice或StretchDIBits函数显示到设备上。
接下来实现一个屏幕截图程序,ScreenShots程序的用法是,用户单击开始截图菜单,或按Ctrl + N组合键开始截图,出现一个十字线,用户可以通过鼠标来移动十字线的位置,确定位置以后单击鼠标左键,这时截图区域的左上角就确定好了;然后用户可以移动鼠标确定右下角的位置,确定以后单击鼠标左键;程序客户区中会显示用户的截图,用户可以选择保存图片或者继续截图。截图的方法有很多种,某些截图工具就可以实时显示坐标和尺寸,这些信息通过一个子窗口来显示。如果桌面上有一个程序窗口,某些截图程序还可以自动识别,要实现这样一个截图程序还是挺复杂的。本例因为限于目前所学以及篇幅关系,所以比较简单。具体代码参见Chapter7\\ScreenShots项目。
如果需要在窗口过程中使用hInstance实例句柄,以前都是使用全局变量,则在WinMain函数中赋值。调用CreateWindow或CreateWindowEx函数后,窗口过程会收到WM_CREATE消息,实际上WM_CREATE消息的lParam参数提供了丰富的信息,lParam参数是一个指向CREATESTRUCT结构的指针,该结构在WinUser.h头文件中定义如下:
typedef struct tagCREATESTRUCT {
LPVOID lpCreateParams;// CreateWindow/Ex函数的lpParam参数
HINSTANCE hInstance; // 实例句柄
HMENU hMenu; // 主菜单句柄
HWND hwndParent; // 父窗口句柄
int cy; // 窗口的高度,以像素为单位
int cx; // 窗口的宽度,以像素为单位
int y; // 窗口左上角的Y坐标,以像素为单位
int x; // 窗口左上角的X坐标,以像素为单位
LONG style; // 窗口样式
LPCTSTR lpszName; // 窗口标题
LPCTSTR lpszClass; // 窗口类名
DWORD dwExStyle; // 窗口扩展样式
} CREATESTRUCT, *LPCREATESTRUCT;
另外,本程序获取桌面DC用的是CreateDC函数,CreateDC函数使用指定的设备名称获取DC句柄,例如CreateDC(TEXT(“DISPLAY”), NULL, NULL, NULL);获取的是屏幕DC句柄。和CreateCompatibleDC函数一样,当不再需要该函数时可以调用DeleteDC函数删除DC,不能用ReleaseDC,ReleaseDC函数和GetDC / GetWindowDC配对使用。
hdcDesk、hdcMem、hdcMemResult都被定义为静态变量,和CreateCompatibleDC函数一样,可以在任何时候调用CreateDC创建hdc,并且可以一直使用它到不再需要为止。
case ID_START:用户开始截图以后,最小化本程序,最小化需要一定的时间,因此程序调用Sleep(500);暂停程序0.5s,然后截取整个屏幕内容到hdcMem;去掉程序的标题栏、边框等,然后最大化显示,客户区中显示的是截取的整个屏幕的内容,然后用户可以在这个截取的屏幕内容中选择截取哪一部分。
WM_MOUSEMOVE和WM_LBUTTONDOWN消息的处理逻辑比较简单,DrawCrossLine函数中用的是R2_XORPEN绘图模式,DrawRect函数中用的是R2_NOTXORPEN绘图模式。GDI绘图过程中经常会出现闪烁、抖动现象,主要原因是对显示DC的操作太过频繁,解决方法是先在内存DC中完成所有绘图操作,再把这个内存DC一次性绘制到显示DC,这样就不会出现闪烁、抖动,这就是通常所说的“双缓存技术”。另外,保存图片的SaveBmp函数用到了创建文件、写文件的函数,这些函数后面会介绍。截图以后,位图没有复制到剪贴板,等学习了剪贴板以后读者就会知道,该功能只需要几行代码。




