位图是一个二维的位数组,它与图像的图素一一对应。当现实世界的图像被扫描成位图以后,图像被分割成网格,并以图素作为取样单位。在位图中的每个图素值指明了一个单位网格内图像的平均颜色。单色位图每个图素只需要一位,灰色或彩色位图中每个图素需要多个位。
位图代表了Windows程序内储存图像信息的两种方法之一。储存图像信息的另一种形式是metafile,我将在第十八章讨论。Metafile储存的就是对图像如何生成的描述,而不是将图像以数字化的图标代表。
以后我将更详细地讨论,Microsoft Windows 3.0定义了一种称为设备无关位图(DIB:device-independent bitmap)。我将在下一章讨论DIB。本章主要讨论GDI位图对象,这是一种在Windows中比DIB更早支持的位图形数据。如同本章大量的范例程序所说明的,这种比DIB位图更早被Windows支持的图形格式仍然有其利用价值。
位图和metafile在计算机图形处理世界中都占有一席之地。位图经常用来表示来自真实世界的复杂图像,例如数字化的照片或者视讯图像。Metafile更适合于描述由人或者机器产生的图像,比如建筑蓝图。位图和metafile都能存于内存或作为文件存于磁盘上,并且都能通过剪贴簿在Windows应用程序之间传输。
位图和metafile的区别在于位映像图像和向量图像之间的差别。位映像图像用离散的图素来处理输出设备;而向量图像用笛卡尔坐标系统来处理输出设备,其线条和填充对象能被个别拖移。现在大多数的图像输出设备是位映像设备,这包括视讯显示、点阵打印机、激光打印机和喷墨打印机。而笔式绘图机则是向量输出设备。
位图有两个主要的缺点。第一个问题是容易受设备依赖性的影响。最明显的就是对颜色的依赖性,在单色设备上显示彩色位图的效果总是不能令人满意的。另一个问题是位图经常暗示了特定的显示分辨率和图像纵横比。尽管位图能被拉伸和缩小,但是这样的处理通常包括复制或删除图素的某些行和列,这样会破坏图像的大小。而metafile在放大缩小后仍然能保持图形样貌不受破坏。
位图的第二个缺点是需要很大的储存空间。例如,描述完整的640×480图素,16色的视频图形数组(VGA:Video Graphics Array)屏幕的一幅位图需要大于150 KB的空间;一幅1024×768,并且每个图素为24位颜色的图像则需要大于2 MB的空间。Metafile需要通常比位图来得少的空间。位图的储存空间由图像的大小及其包含的颜色决定,而metafile的储存空间则由图像的复杂程度和它所包含的GDI指令数决定。
然而,位图优于metafile之处在于速度。将位图复制给视讯显示器通常比复制基本图形文件的速度要快。最近几年,压缩技术允许压缩位图的文件大小,以使它能有效地通过电话线传输并广泛地用于Internet的网页上。
位图的来源
位图可以手工建立,例如,使用Windows 98附带的「小画家」程序。一些人宁愿使用位映像绘图软件也不使用向量绘图软件。他们假定:图形最后一定会复杂到不能用线条跟填充区域来表达。
位图图像也能由计算机程序计算生成。尽管大多数计算生成的图像能按向量图形metafile储存,但是高清晰度的画面或碎形图样通常还是需要位图。
现在,位图通常用于描述真实世界的图像,并且有许多硬设备能让您把现实世界的图像输入到计算机。这类硬件通常使用 电荷耦合设备(CCD:charge-coupled device),这种设备接触到光就释放电荷。有时这些CCD单元能排列成一组,一个图素对应一个CCD;为节约开支,只用一行CCD扫描图像。
在这些计算机CCD设备中,扫描仪是最古老的。它用一行CCD沿着纸上图像(例如照片)的表面扫描。CCD根据光的强度产生电荷。模拟数字转换器(ADC:Analog-to-digital converters)把电荷转换为数字讯号,然后排列成位图。
便携式摄像机也利用CCD单元组来捕捉影像。通常,这些影像是记录到录像带上。不过,这些视讯输出也能直接进入 影像捕捉器(frame grabber),该设备能把模拟视讯信号转换为一组图素值。这些影像捕捉器与任何兼容的视讯信号来源都能同时使用,例如VCR、光盘、DVD播放机或有线电视译码器。
最近,数字照相机的价位对于家庭使用者来说开始变得负担得起了。它看起来很像普通照相机。但是数字照相机不使用底片,而用一组CCD来拦截图像,并且在ADC内部把数字图像直接储存在照相机内的内存中。通常,数字照相机与计算机的接口要通过串行端口。
位图呈矩形,并有空间尺寸,图像的高度和宽度都以图素为单位。例如,此网格可描述一个很小的位图:宽度为9图素,高度为6图素,或者更简单地计为9×6:
习惯上,位图的速记尺寸是先给出宽度。位图总数为9×6或者54图素。我将经常使用符号cx和cy来表示位图的宽度和高度。c表示计数,因此cx和cy是沿着x轴(水平)和y轴(垂直)的图素数。
我们能根据x和y坐标来描述位图上具体的图素。一般(并不都是这样),在网格内计算图素时,位图开始于图像的左上角。这样,在此位图右下角的图素坐标就是(8, 5)。因为从0开始计数,所以此值比图像的宽度和高度小1。
位图的空间尺寸通常也指定了分辨率,但这是一个有争议的词。我们说我们的视讯显示有640×480的分辨率,但是激光打印机的分辨率只有每英寸300点。我喜欢用后一种情况中分辨率的意思作为每单位图素的数量。位图在这种意义上的分辨率指的是位图在特定测量单位中的图素数。不管怎样,当我使用分辨率这个词语时,其定义的内容应该是明确的。
位图是矩形的,但是计算机内存空间是线性的。通常(但并不都是这样)位图按列储存在内存中,且从顶列图素开始到底列结束。(DIB是此规则的一个主要例外)。每一列,图素都从最左边的图素开始依次向右储存。这就好像储存几列文字中的各个字符。
颜色和位图
除空间尺寸以外,位图还有颜色尺寸。这里指的是每个图素所需要的位数,有时也称为位图的 颜色深度(color depth)、位数(bit-count)或 位/图素(bpp:bits per pixel)数。位图中的每个图素都有相同数量的颜色位。
每图素1位的位图称为二阶(bilevel)、 二色(bicolor)或者单色 (monochrome)位图。每图素可以是0或1,0表示黑色,1可以表示白色,但并不总是这样。对于其它颜色,一个图素就需要有多个位。可能的颜色值等于2位数值。用2位可以得到4种颜色,用4位可以得16种颜色,8位可得到256种颜色,16位可得到65,536种颜色,而24位可得到16,777,216种颜色。
如何将颜色位的组合与人们所熟悉的颜色相对应是目前处理位图时经常碰到(而且常常是灾难)的问题。
实际的设备
位图可按其颜色位数来分类;在Windows的发展过程中,不同的位图颜色格式取决于常用视讯显示卡的功能。实际上,我们可把视讯显示内存看作是一幅巨大的位图-我们从显示器上就可以看见。
Windows 1.0多数采用的显示卡是IBM的彩色图像适配器(CGA:Color Graphics Adapter)和单色图形卡(HGC:Hercules Graphics Card)。HGC是单色设备,而CGA也只能在Windows以单色图形模式使用。单色位图现在还很常用(例如,鼠标的光标一般为单色),而且单色位图除显示图像以外还有其它用途。
随着增强型图形显示卡(EGA:Enhanced Graphics Adapter)的出现,Windows使用者开始接触16色的图形。每个图素需要4个颜色位。(实际上,EGA比这里所讲的更复杂,它还包括一个64种颜色的调色盘,应用程序可以从中选择任意的16种颜色,但Windows只按较简单的方法使用EGA)。在EGA中使用的16种颜色是黑、白、两种灰色、高低亮度的红色、绿和蓝(三原色)、青色(蓝和绿组合的颜色)。现在认为这16种颜色是Windows的最低颜色标准。同样,其它16色位图也可以在Windows中显示。大多数的图示都是16色的位图。通常,简单的卡通图像也可以用这16种颜色制作。
在16色位图中的颜色编码有时称为IRGB(高亮红绿蓝:Intensity-Red-Green-Blue),并且实际上是源自IBM CGA文字模式下最初使用的十六种颜色。每个图素所用的4个IRGB颜色位都映像为表14-1所示的Windows十六进制RGB颜色。
表14-1 |
IRGB | RGB颜色 | 颜色名称 |
0000 | 00-00-00 | 黑 |
0001 | 00-00-80 | 暗蓝 |
0010 | 00-80-00 | 暗绿 |
0011 | 00-80-80 | 暗青 |
0100 | 80-00-00 | 暗红 |
0101 | 80-00-80 | 暗洋红 |
0110 | 80-80-00 | 暗黄 |
0111 | C0-C0-C0 | 亮灰 |
1000 | 80-80-80 | 暗灰 |
1001 | 00-00-FF | 蓝 |
1010 | 00-FF-00 | 绿 |
1011 | 00-FF-FF | 青 |
1100 | FF-00-00 | 红 |
1101 | FF-00-FF | 洋红 |
1110 | FF-FF-00 | 黄 |
1111 | FF-FF-FF | 白 |
EGA的内存组成了四个「颜色面」,也就是说,定义每个图素颜色的四位在内存中是不连续的。然而,这样组织显示内存便于使所有的亮度位都排列在一起、所有的红色位都排在一起,等等。这样听起来就好像一种设备依赖特性,即Windows程序写作者不需要了解所有细节,但这时应或多或少地知道一些。不过,这些颜色面会出现在一些API呼叫中,例如GetDeviceCaps和CreateBitmap。
Windows 98和Microsoft Windows NT需要VGA或分辨率更高的图形卡。这是目前公认的显示卡的最低标准。
1987年,IBM最早发表视讯图像数组(Video Graphics Array:VGA)以及PS/2系列的个人计算机。它提供了许多不同的显示模式,但最好的图像模式(Windows也使用其中之一)是水平显示640个图素,垂直显示480个图素,带有16种颜色。要显示256种颜色,最初的VGA必须切换到320×240的图形模式,这种图素数不适合Windows的正常工作。
一般人们已经忘记了最初VGA卡的颜色限制,因为其它硬件制造商很快就开发了「Super-VGA」(SVGA)显示卡,它包括更多的视讯内存,可显示256种颜色并有多于640×480的模式。这是现在的标准,而且也是一件好事,因为对于现实世界中的图像来说,16种颜色过于简单,有些不适合。
显示256种颜色的显示卡模式采用每图素8位。不过,这些8位值都不必与实际的颜色相符。事实上,显示卡提供了「调色盘对照表(palette lookup table)」,该表允许软件指定这8位的颜色值,以便与实际颜色相符合。在Windows中,应用程序不能直接存取调色盘对照表。实际上,Windows储存了256种颜色中的20种,而应用程序可以通过「Windows调色盘管理器」来自订其余的236种颜色。关于这些内容,我将在 第十六章详细介绍。调色盘管理器允许应用程序在256色显示器上显示实际位图。Windows所储存的20种颜色如表14-2所示。
表14-2 |
IRGB | RGB颜色 | 颜色名称 |
00000000 | 00-00-00 | 黑 |
00000001 | 80-00-00 | 暗红 |
00000010 | 00-80-00 | 暗绿 |
00000011 | 80-80-00 | 暗黄 |
00000100 | 00-00-80 | 暗蓝 |
00000101 | 80-00-80 | 暗洋红 |
00000110 | 00-80-80 | 暗青 |
00000111 | C0-C0-C0 | 亮灰 |
00001000 | C0-DC-C0 | 美元绿 |
00001001 | A6-CA-F0 | 天蓝 |
11110110 | FF-FB-F0 | 乳白 |
11110111 | A0-A0-A4 | 中性灰 |
11111000 | 80-80-80 | 暗灰 |
11111001 | FF-00-00 | 红 |
11111010 | 00-FF-00 | 绿 |
11111011 | FF-FF-00 | 黄 |
11111100 | 00-00-FF | 蓝 |
11111101 | FF-00-FF | 洋红 |
11111110 | 00-FF-FF | 青 |
11111111 | FF-FF-FF | 白 |
最近几年,True-Color显示卡很普遍,它们在每图素使用16位或24位。有时每图素虽然用了16位,其中有1位不用,而其它15位主要近似于红、绿和蓝。这样红、绿和蓝每种都有32色阶,组合起来就可以达到32,768种颜色。更普遍的是,6位用于绿色(人类对此颜色最敏感),这样就可得到65,536种颜色。对于非技术性的PC使用者来说,他们并不喜欢看到诸如32,768或65,536之类的数字,因此通常将这种视讯显示卡称为Hi-Color显示卡,它能提供数以千计的颜色。
到了每个图素24位时,我们总共有了16,777,216种颜色(或者True Color、数百万的颜色),每个图素使用3字节。这与今后的标准很相似,因为它大致代表了人类感官的极限而且也很方便。
在呼叫GetDeviceCaps时(参见第五章的DEVCAPS程序),您能利用BITSPIXEL和PLANES常数来获得显示卡的颜色单位,这些值显示如表14-3所示
表14-3 |
BITSPIXEL | PLANES | 颜色数 |
1 | 1 | 2 |
1 | 4 | 16 |
8 | 1 | 256 |
15或16 | 1 | 32,768或65 536 |
24或32 | 1 | 16 777 216 |
最近,您应该不会再碰到单色显示器了,但即便碰到了,您的应用程序也应该不会发生问题。
GDI支援的位图
Windows图形设备接口(GDI:Graphics Device Interface)从1.0版开始支持位图。不过,一直到Windows 3.0以前,Windows下唯一支持GDI对象的只有位图,以位图句柄来使用。这些GDI位图对象是单色的,或者与实际的图像输出设备(例如视讯显示器)有相同的颜色单位。例如,与16色VGA兼容的位图有四个颜色面。问题是这些颜色位图不能储存,也不能用于颜色单位不同的图像输出设备(如每图素占8位就可以产生256种颜色的设备)上。
从Windows 3.0开始,定义了一种新的位图格式,我们称之为设备无关位图(device-independent bitmap),或者DIB。DIB包括了自己的调色盘,其中显示了与RGB颜色相对应的图素位。DIB能显示在任何位映像输出设备上。这里唯一的问题是DIB的颜色通常一定会转换成设备实际表现出来的颜色。
与DIB同时,Windows 3.0还介绍了「Windows调色盘管理器」,它让程序能够从显示的256种颜色中自订颜色。就像我们在 第十六章所看到的那样,应用程序通常在显示DIB时使用「调色盘管理器」。
Microsoft在Windows 95(和Windows NT 4.0)中扩展了DIB的定义,并且在Windows 98(和Windows NT 5.0)中再次扩展。这些扩展增加了所谓的「图像颜色管理器(ICM:Image Color Management),并允许DIB更精确地指定图像所需要的颜色。我将在 第十五章简要讨论ICM。
不论DIB多么重要,在处理位图时,早期的GDI位图对象依然扮演了重要的角色。掌握位图使用方式的最好方法是按各种用法在演进发展的时间顺序来学习,先从GDI位图对象和位块传输的概念开始。
我前面提到过,您可以把整个视讯显示器看作是一幅大位图。您在屏幕上见到的图素由储存在视讯显示卡上内存中的位来描述。任何视讯显示的矩形区域也都是一个位图,其大小是它所包含的行列数。
让我们从将图像从视讯显示的一个区域复制到另一个区域,开始我们在位图世界的旅行吧!这个是强大的BitBlt函数的工作。
Bitblt(读作「bit blit」)代表「位块传输(bit-block transfer)」。BLT起源于一条汇编语言指令,该指令在DEC PDP-10上用来传输内存块。术语「bitblt」第一次用在图像上与Xerox Palo Alto Research Center(PARC)设计的SmallTalk系统有关。在SmallTalk中,所有的图形输出操作都使用bitblt。程序写作者有时将blt用作动词,例如:「Then I wrote some code to blt the happy face to the screen and play a wave file.」
BitBlt函数移动的是图素,或者(更明确地)是一个位映像图块。您将看到,术语「传输(transfer)」与BitBlt函数不尽相同。此函数实际上对图素执行了一次位操作,而且可以产生一些有趣的结果。
简单的BitBlt
程序14-1所示的BITBLT程序用BitBlt函数将程序系统的菜单图标(位于程序Windows的左上角)复制到它的显示区域。
程序14-1 BITBLT
BITBLT.C
/*----------------------------------------------------------------------
BITBLT.C -- BitBlt Demonstration
(c) Charles Petzold, 1998
-------------------------------------------------------------------*/
#include <windows.h>
LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM) ;
int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance,
PSTR szCmdLine, int iCmdShow)
{
static TCHAR szAppName [] = TEXT ("BitBlt") ;
HWND hwnd ;
MSG msg ;
WNDCLASS wndclass ;
wndclass.style = CS_HREDRAW | CS_VREDRAW ;
wndclass.lpfnWndProc = WndProc ;
wndclass.cbClsExtra = 0 ;
wndclass.cbWndExtra = 0 ;
wndclass.hInstance = hInstance ;
wndclass.hIcon = LoadIcon (NULL, IDI_INFORMATION) ;
wndclass.hCursor = LoadCursor (NULL, IDC_ARROW) ;
wndclass.hbrBackground = (HBRUSH) GetStockObject (WHITE_BRUSH) ;
wndclass.lpszMenuName = NULL ;
wndclass.lpszClassName = szAppName ;
if (!RegisterClass (&wndclass))
{
MessageBox (NULL, TEXT ("This program requires Windows NT!"),
szAppName, MB_ICONERROR) ;
return 0 ;
}
hwnd = CreateWindow (szAppName, TEXT ("BitBlt Demo"),
WS_OVERLAPPEDWINDOW,
CW_USEDEFAULT, CW_USEDEFAULT,
CW_USEDEFAULT, CW_USEDEFAULT,
NULL, NULL, hInstance, NULL) ;
ShowWindow (hwnd, iCmdShow) ;
UpdateWindow (hwnd) ;
while (GetMessage (&msg, NULL, 0, 0))
{
TranslateMessage (&msg) ;
DispatchMessage (&msg) ;
}
return msg.wParam ;
}
LRESULT CALLBACK WndProc (HWND hwnd, UINT message, WPARAM wParam,LPARAM lParam)
{
static int cxClient, cyClient, cxSource, cySource ;
HDC hdcClient, hdcWindow ;
int x, y ;
PAINTSTRUCT ps ;
switch (message)
{
case WM_CREATE:
cxSource = GetSystemMetrics (SM_CXSIZEFRAME) +
GetSystemMetrics (SM_CXSIZE) ;
cySource = GetSystemMetrics (SM_CYSIZEFRAME) +
GetSystemMetrics (SM_CYCAPTION) ;
return 0 ;
case WM_SIZE:
cxClient = LOWORD (lParam) ;
cyClient = HIWORD (lParam) ;
return 0 ;
case WM_PAINT:
hdcClient = BeginPaint (hwnd, &ps) ;
hdcWindow = GetWindowDC (hwnd) ;
for (y = 0 ; y < cyClient ; y += cySource)
for (x = 0 ; x < cxClient ; x += cxSource)
{
BitBlt (hdcClient, x, y, cxSource, cySource,
hdcWindow, 0, 0, SRCCOPY) ;
}
ReleaseDC (hwnd, hdcWindow) ;
EndPaint (hwnd, &ps) ;
return 0 ;
case WM_DESTROY:
PostQuitMessage (0) ;
return 0 ;
}
return DefWindowProc (hwnd, message, wParam, lParam) ;
}
但为什么只用了一个BitBlt呢?实际上,那个BITBLT用系统菜单图标的多个副本来填满显示区域(在此情况下是信息方块中普遍使用的IDI_INFORMATION图示),如图14-1所示。
图14-1 BITBLT的屏幕显示 |
BitBlt函数从称为「来源」的设备内容中将一个矩形区的图素传输到称为「目的(destination)」的另一个设备内容中相同大小的矩形区。此函数的语法如下:
BitBlt (hdcDst, xDst, yDst, cx, cy, hdcSrc, xSrc, ySrc, dwROP) ;
来源和目的设备内容可以相同。
在BITBLT程序中,目的设备内容是窗口的显示区域,设备内容句柄从BeginPaint函数获得。来源设备内容是应用程序的整个窗口,此设备内容句柄从GetWindowDC获得的。很明显地,这两个设备内容指的是同一个实际设备(视讯显示器)。不过,这两个设备内容的坐标原点不同。
xSrc和ySrc参数指明了来源图像左上角的坐标位置。在BITBLT中,这两个参数设为0,表示图像从来源设备内容(也就是整个窗口)的左上角开始,cx和cy参数是图像的宽度和高度。BITBLT根据从GetSytemMetrics函数获得的信息来计算这些值。
xDst和yDst参数表示了复制图像位置左上角的坐标位置。在BITBLT中,这两个参数设定为不同的值以便多次复制图像。对于第一次BitBlt呼叫,这两个参数设制为0,将图像复制到显示区域的左上角位置。
BitBlt的最后一个参数是位映像操作型态。我将简短地讨论一下这个值。
请注意,BitBlt是从实际视讯显示内存传输图素,而不是从系统菜单图标的其它图像传输。如果您移动BITBLT窗口以使部分系统菜单图标移出屏幕,然后调整BITBLT窗口的尺寸使其重画,这时您将发现BITBLT显示区域中显示的是菜单图示的一部分。BitBlt函数不再存取整个图像。
在BitBlt函数中,来源和目的设备内容可以相同。您可以重新编写BITBLT以使WM_PAINT处理执行以下内容:
BitBlt (hdcClient, 0, 0, cxSource, cySource,
hdcWindow, 0, 0, SRCCOPY) ;
for (y = 0 ; y < cyClient ; y += cySource)
for (x = 0 ; x < cxClient ; x += cxSource)
{
if (x > 0 || y > 0)
BitBlt (hdcClient, x, y, cxSource, cySource,
hdcClient, 0, 0, SRCCOPY) ;
}
这将与前面显示的BITBLT一样产生相同的效果,只是显示区域左上角比较模糊。
在BitBlt内的最大限制是两个设备内容必须是兼容的。这意味着或者其中之一必须是单色的,或者两者的每个图素都相同的位数。总而言之,您不能用此方法将屏幕上的某些图形复制到打印机。
拉伸位图
在BitBlt函数中,目的图像与来源图像的尺寸是相同的,因为函数只有两个参数来说明宽度和高度。如果您想在复制时拉伸或者压缩图像尺寸,可以使用StretchBlt函数。StretchBlt函数的语法如下:
StretchBlt (hdcDst, xDst, yDst, cxDst, cyDst,
hdcSrc, xSrc, ySrc, cxSrc, cySrc, dwROP) ;
此函数添加了两个参数。现在的函数就分别包含了目的和来源各自的宽度和高度。STRETCH程序展示了StretchBlt函数,如程序14-2所示。
程序14-2 STRETCH
STRETCH.C
/*--------------------------------------------------------------------------
STRETCH.C -- StretchBlt Demonstration
(c) Charles Petzold, 1998
----------------------------------------------------------------------------*/
#include <windows.h>
LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM) ;
int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance,
PSTR szCmdLine, int iCmdShow)
{
static TCHAR szAppName [] = TEXT ("Stretch") ;
HWND hwnd ;
MSG msg ;
WNDCLASS wndclass ;
wndclass.style = CS_HREDRAW | CS_VREDRAW ;
wndclass.lpfnWndProc = WndProc ;
wndclass.cbClsExtra = 0 ;
wndclass.cbWndExtra = 0 ;
wndclass.hInstance = hInstance ;
wndclass.hIcon = LoadIcon (NULL, IDI_INFORMATION) ;
wndclass.hCursor = LoadCursor (NULL, IDC_ARROW) ;
wndclass.hbrBackground = (HBRUSH) GetStockObject (WHITE_BRUSH) ;
wndclass.lpszMenuName = NULL ;
wndclass.lpszClassName = szAppName ;
if (!RegisterClass (&wndclass))
{
MessageBox ( NULL, TEXT ("This program requires Windows NT!"),
szAppName, MB_ICONERROR) ;
return 0 ;
}
hwnd = CreateWindow (szAppName, TEXT ("StretchBlt Demo"),
WS_OVERLAPPEDWINDOW,
CW_USEDEFAULT,
CW_USEDEFAULT,
CW_USEDEFAULT,
CW_USEDEFAULT,
NULL, NULL, hInstance, NULL) ;
ShowWindow (hwnd, iCmdShow) ;
UpdateWindow (hwnd) ;
while (GetMessage (&msg, NULL, 0, 0))
{
TranslateMessage (&msg) ;
DispatchMessage (&msg) ;
}
return msg.wParam ;
}
LRESULT CALLBACK WndProc (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
{
static int cxClient, cyClient, cxSource, cySource ;
HDC hdcClient, hdcWindow ;
PAINTSTRUCT ps ;
switch (message)
{
case WM_CREATE:
cxSource = GetSystemMetrics (SM_CXSIZEFRAME) +
GetSystemMetrics (SM_CXSIZE) ;
cySource = GetSystemMetrics (SM_CYSIZEFRAME) +
GetSystemMetrics (SM_CYCAPTION) ;
return 0 ;
case WM_SIZE:
cxClient = LOWORD (lParam) ;
cyClient = HIWORD (lParam) ;
return 0 ;
case WM_PAINT:
hdcClient = BeginPaint (hwnd, &ps) ;
hdcWindow = GetWindowDC (hwnd) ;
StretchBlt (hdcClient, 0, 0, cxClient, cyClient,
hdcWindow, 0, 0, cxSource, cySource, MERGECOPY) ;
ReleaseDC (hwnd, hdcWindow) ;
EndPaint (hwnd, &ps) ;
return 0 ;
case WM_DESTROY:
PostQuitMessage (0) ;
return 0 ;
}
return DefWindowProc (hwnd, message, wParam, lParam) ;
}
此程序只有呼叫了StretchBlt函数一次,但是利用此函数以系统菜单图标填充了整个显示区域,如图14-2所示。
图14-2 STRETCH的屏幕显示 |
BitBlt和StretchBlt函数中所有的坐标与大小都是依据逻辑单位的。但是当您在BitBlt函数中定义了两个不同的设备内容,而这两个设备内容虽然参考同一个实际设备,却各自有着不同的映像模式,这时将发生什么结果呢?如果出现这种情况,呼叫BitBlt产生的结果就显得不明确了:cx和cy参数都是逻辑单位,而它们同样应用于来源设备内容和目的设备内容中的矩形区。所有的坐标和尺寸必须在实际的位传输之前转换为设备坐标。因为cx和cy值同时用于来源和目的设备内容,所以此值必须转换为设备内容自己的单位。
当来源和目的设备内容相同,或者两个设备内容都使用MM_TEXT图像模式时,设备单位下的矩形尺寸在两个设备内容中会是相同的,然后才由Windows进行图素对图素的转换。不过,如果设备单位下的矩形尺寸在两个设备内容中不同时,则Windows就把此工作转交给更通用的StretchBlt函数。
StretchBlt也允许水平或垂直翻转图像。如果cxSrc和cxDst标记(转换成设备单位以后)不同,那么StretchBlt就建立一个镜像:左右翻转。在STRETCH程序中,通过将xDst参数改为cxClient并将cxDst参数改成-cxClient,您就可以做到这一点。如果cySrc和cyDst不同,则StretchBlt会上下翻转图像。要在STRETCH程序中测试这一点,可将yDst参数改为cyClient并将cyDst参数改成-cyClient。
StretchBlt模式
使用StretchBlt会碰到一些与位图大小缩放相关的一些根本问题。在扩展一个位图时,StretchBlt必须复制图素行或列。如果放大倍数不是原图的整数倍,那么此操作会造成产生的图像有些失真。
如果目的矩形比来源矩形小,那么StretchBlt在缩小图像时就必须把两行(或列)或者多行(或列)的图素合并到一行(或列)。完成此操作有四种方法,它根据设备内容伸展模式属性来选择其中一种方法。您可使用SetStretchBltMode函数来修改这个属性。
SetStretchBltMode (hdc, iMode) ;
iMode可取下列值:
- BLACKONWHITE或者STRETCH_ANDSCANS(内定)如果两个或多个图素得合并成一个图素,那么StretchBlt会对图素执行一个逻辑AND运算。这样的结果是只有全部的原始图素是白色时该图素才为白色,其实际意义是黑色图素控制了白色图素。这适用于白背景中主要是黑色的单色位图。
- WHITEONBLACK或STRETCH_ORSCANS 如果两个或多个图素得合并成一个图素,那么StretchBlt执行逻辑OR运算。这样的结果是只有全部的原始图素都是黑色时才是黑色,也就是说由白色图素决定颜色。这适用于黑色背景中主要是白色的单色位图。
- COLORONCOLOR或STRETCH_DELETESCANS StretchBlt简单地消除图素行或列,而没有任何逻辑组合。这是通常是处理彩色位图的最佳方法。
- HALFTONE或STRETCH_HALFTONE Windows根据组合起来的来源颜色来计算目的的平均颜色。这将与半调调色盘联合使用, 第十六章将展示这一程序。
Windows还包括用于取得目前伸展模式的GetStretchBltMode函数。
位映像操作
BITBLT和STRETCH程序简单地将来源位图复制给了目的位图,在过程中也可能进行了缩放。这是把SRCCOPY作为BitBlt和StretchBlt函数最后一个参数的结果。SRCCOPY只是您能在这些函数中使用的256个位映像操作中的一个。让我们先在STRETCH程序中做一个别的实验,然后再系统地研究位映像操作。
尽量用NOTSRCCOPY来代替SRCCOPY。与它们名称一样,位映像操作在复制位图时转换其颜色。在显示区域窗口,所有的颜色转换:黑色变成白色、白色变成黑色,蓝色变成黄色。现在试一下SRCINVERT,您将得到同样效果。如果试一下BLACKNESS,正如其名称一样,整个显示区域都将变成黑色,而WHITENESS则使其变成白色。
现在试一试用下列三条叙述来代替StretchBlt呼叫:
SelectObject (hdcClient, CreateHatchBrush (HS_DIAGCROSS, RGB (0, 0, 0)));
StretchBlt (hdcClient, 0, 0, cxClient, cyClient,
hdcWindow, 0, 0, cxSource, cySource, MERGECOPY) ;
DeleteObject (hdcClient, GetStockObject (WHITE_BRUSH)) ;
这次,您将在图像上看到一个菱形的画刷,这是什么?
我在前面说过,BitBlt和StretchBlt函数不是简单的位块传输。此函数实际在下面三种图像间执行位操作。
- Source 来源位图,拉伸或压缩(如果有必要)到目的矩形的尺寸。
- Destination 在BitBlt或StretchBlt呼叫之前的目的矩形。
- Pattern 在目的设备内容中选择的目前画刷,水平或垂直地复制到目的矩形范围内。
结果是复制到了目的矩形中。
位映像操作与我们在第五章遇到的绘图模式在概念上相似。绘图模式采用图像对象的控件方式,例如一条线就组合成一个目的。我们知道有16种绘图模式-也就是说,对象中的0和1画出时,唯一结果就是目的中0和1的组合。
使用BitBlt和StretchBlt的位映像操作包含了三个对象的组合,这将产生256种位映像操作。有256种方法来组合来源位图、目的位图和图案。有15种位映像操作已经命名-其中一些名称其实还不能够清楚清楚说明其意义-它们定义在WINGDI.H里头,其余的都有数值,列在/Platform SDK/Graphics and Multimedia Services/GDI/Raster Operation Codes/Ternary Raster Operations之中。
有名称的15种ROP代码见表14-4。
表14-4 |
图案(P):1 1 1 1 0 0 0 0 来源(s):1 1 0 0 1 1 0 0 目的(D):1 0 1 0 1 0 1 0 | 布尔操作 | ROP代码 | 名称 | |
结果: | 0 0 0 0 0 0 0 0 | 0 | 0x000042 | BLACKNESS |
0 0 0 1 0 0 0 1 | ~(S?#160;D) | 0x1100A6 | NOTSRCERASE | |
0 0 1 1 0 0 1 1 | ~S | 0x330008 | NOTSRCCOPY | |
0 1 0 0 0 1 0 0 | S & ~D | 0x440328 | SRCERASE | |
0 1 0 1 0 1 0 1 | ~D | 0x550009 | DSTINVERT | |
0 1 0 1 1 0 1 0 | P ^ D | 0x5A0049 | PATINVERT | |
0 1 1 0 0 1 1 0 | S ^ D | 0x660046 | SRCINVERT | |
1 0 0 0 1 0 0 0 | S & D | 0x8800C6 | SRCAND | |
1 0 1 1 1 0 1 1 | ~S?#160;D | 0xBB0226 | MERGEPAINT | |
1 1 0 0 0 0 0 0 | P & S | 0xC000CA | MERGECOPY | |
1 1 0 0 1 1 0 0 | S | 0xCC0020 | SRCCOPY | |
1 1 1 0 1 1 1 0 | S?#160;D | 0xEE0086 | SRCPAINT | |
1 1 1 1 0 0 0 0 | P | 0xF00021 | PATCOPY | |
1 1 1 1 1 0 1 1 | P?#160; ~S?#160; D | 0xFB0A09 | PATPAINT | |
1 1 1 1 1 1 1 1 | 1 | 0xFF0062 | WHITENESS |
此表格对于理解和使用位映像操作非常重要,因此我们应花点时间来研究。
在这个表格中,「ROP代码」行的值将传递给BitBlt或StretchBlt的最后一个参数;在「名称」行中的值在WINGDI.H定义。ROP代码的低字组协助设备驱动程序传输位映像操作。高字组是0到255之间的数值。此数值与第2列的图案的位相同,这是在图案、来源和显示在顶部的目的之间进行位操作的结果。「布尔运算」列按C语法显示图案、来源和目的的组合方式。
要开始了解此表,最简单的办法是假定您正处理一个单色系统(每图素1位)其中0代表黑色,1代表白色。BLACKNESS操作的结果是不管是来源、目的和图案是什么,全部为零,因此目的将显示黑色。类似地,WHITENESS总导致目的呈白色。
现在假定您使用位映像操作PATCOPY。这导致结果位与图案位相同,而忽略了来源和目的位图。换句话说,PATCOPY简单地将目前图案复制给了目的矩形。
PATPAINT位映像操作包含一个更复杂的操作。其结果相同于在图案、目的和反转的来源之间进行位或操作。当来源位图是黑色(0)时,其结果总是白色(1);当来源是白色(1)时,只要图案或目的为白色,则结果就是白色。换句话说,只有来源为白色而图案和目的都是黑色时,结果才是黑色。
彩色显示时每个图素都使用了多个位。BitBlt和StretchBlt函数对每个颜色位都分别提供了位操作。例如,如果目的是红色而来源为蓝色,SRCPAINT位映像操作把目的变成洋红色。注意,操作实际是按显示卡内储存的位执行的。这些位所对应的颜色取决于显示卡的调色盘的设定。Windows完成了此操作,以便位映像操作能达到您预计的结果。不过,如果您修改了调色盘(我将在第十六章讨论),位映像操作将产生无法预料的结果。
如要得到位映像操作较好的应用程序,请参见本章后面的「非矩形位图图像」一节。
图案Blt
除了BitBlt和StretchBlt以外,Windows还包括一个称为PatBlt (「pattern block transfer:图案块传输」)的函数。这是三个「blt」函数中最简单的。与BitBlt和StretchBlt不同,它只使用一个目的设备内容。PatBlt语法是:
PatBlt (hdc, x, y, cx, cy, dwROP) ;
x、y、cx和cy参数字于逻辑单位。逻辑点(x,y)指定了矩形的左上角。矩形宽为cx单位,高为cy单位。这是PatBlt修改的矩形区域。PatBlt在画刷与目的设备内容上执行的逻辑操作由dwROP参数决定,此参数是ROP代码的子集-也就是说,您可以只使用那些不包括来源目的设备内容的ROP代码。下表列出了PatBlt支持的16个位映像操作:
表14-5 |
图案(P):1 1 0 0 目的(D):1 0 1 0 | 布尔操作 | ROP代码 | 名称 | |
结果: | 0 0 0 0 | 0 | 0x000042 | BLACKNESS |
0 0 0 1 | ~(P | D) | 0x0500A9 | ||
0 0 1 0 | ~P & D | 0x0A0329 | ||
0 0 1 1 | ~P | 0x0F0001 | ||
0 1 0 0 | P & ~D | 0x500325 | ||
0 1 0 1 | ~D | 0x550009 | DSTINVERT | |
0 1 1 0 | P ^ D | 0x5A0049 | PATINVERT | |
0 1 1 1 | ~(P & D) | 0x5F00E9 | ||
1 0 0 0 | P & D | 0xA000C9 | ||
1 0 0 1 | ~(P ^ D) | 0xA50065 | ||
1 0 1 0 | D | 0xAA0029 | ||
1 0 1 1 | ~P | D | 0xAF0229 | ||
1 1 0 0 | P | 0xF00021 | PATCOPY | |
1 1 0 1 | P | ~D | 0xF50225 | ||
1 1 1 0 | P | D | 0xFA0089 | ||
1 1 1 1 | 1 | 0xFF0062 | WHITENESS |
下面列出了PatBlt一些更常见用途。如果想画一个黑色矩形,您可呼叫
PatBlt (hdc, x, y, cx, cy, BLACKNESS) ;
要画一个白色矩形,请用
PatBlt (hdc, x, y, cx, cy, WHITENESS) ;
函数
PatBlt (hdc, x, y, cx, cy, DSTINVERT) ;
用于改变矩形的颜色。如果目前设备内容中选择了WHITE_BRUSH,那么函数
PatBlt (hdc, x, y, cx, cy, PATINVERT) ;
也改变矩形。
您可以再次呼叫FillRect函数来用画笔充满一个矩形区域:
FillRect (hdc, &rect, hBrush) ;
FillRect函数相同于下列代码:
hBrush = SelectObject (hdc, hBrush) ;
PatBlt (hdc, rect.left, rect.top,
rect.right - rect.left,
rect.bottom - rect.top, PATCOPY) ;
SelectObject (hdc, hBrush) ;
实际上,此程序代码是Windows用于执行FillRect函数的动作。如果您呼叫
InvertRect (hdc, &rect) ;
Windows将其转换成函数:
PatBlt (hdc, rect.left, rect.top,
rect.right - rect.left,
rect.bottom - rect.top, DSTINVERT) ;
在介绍PatBlt函数的语法时,我说过点(x,y)指出了矩形的左上角,而且此矩形宽度为cx单位,高度为cy单位。此叙述并不完全正确。BitBlt、PatBlt和StretchBlt是最合适的GDI画图函数,它们根据从一个角测得的逻辑宽度和高度来指定逻辑直角坐标。矩形边框用到的其它所有GDI画图函数都要求根据左上角和右下角的坐标来指定坐标。对于MM_TEXT映像模式,上面讲述的PatBlt参数就是正确的。然而对于公制的映像模式来说,就不正确。如果您使用一的cx和cy值,那么点(x,y)将是矩形的左下角。如果希望点(x,y)是矩形的左上角,那么cy参数必须设为矩形的负高度。
如果想更精确,用PatBlt修改颜色的矩形将通过cx的绝对值获得逻辑宽度,通过cy的绝对值获得逻辑高度。这两个参数可以是负值。由逻辑点(x, y)和(x + cx, y + cy)给定的两个角定义了矩形。矩形的左上角通常属于PatBlt修改的区域。右上角则超出了矩形的范围。根据映像模式和cx、cy参数的符号,矩形左上角的点应为(x, y)、(x, y + cy)、(x + cx, y)或者(x + cx, y + cy)。
如果给MM_LOENGLISH设定了映像模式,并且您想在显示区域左上角的一小块正方形上使用PatBlt,您可以使用
PatBlt (hdc, 0, 0, 100, -100, dwROP) ;
或
PatBlt (hdc, 0, -100, 100, 100, dwROP) ;
或
PatBlt (hdc, 100, 0, -100, -100, dwROP) ;
或
PatBlt (hdc, 100, -100, -100, 100, dwROP) ;
给PatBlt设定正确参数最容易的方法是将x和y设为矩形左上角。如果映像模式定义y坐标随着向上卷动显示而增加,那么请使用负的cy参数。如果映像模式定义x坐标向左增加(很少有人用),则需要使用负的cx参数。
我在本章前面已提到过Windows从1.0开始就支持GDI位图对象。因为在Windows 3.0发表了设备无关位图,GDI位图对象有时也称为设备相关位图,或者DDB。我尽量不全部引用device-dependent bitmap的全文,因为它看上去与device-independent bitmap类似。缩写DDB会好一些,因为我们很容易把它与DIB区别开来。
对程序写作者来说,现存的两种不同型态的位图从Windows 3.0开始就更为混乱。许多有经验的Windows程序写作者都不能准确地理解DIB和DDB之间的关系。(恐怕本书的Windows 3.0版本不能澄清这个问题)。诚然,DIB和DDB在许多方面是相关的:DIB与DDB能相互转换(尽管转换程序中会丢失一些信息)。然而DIB和DDB是不可以相互替换的,并且不能简单地选择一种方法来表示同一个可视数据。
如果我们能假设说DIB一定会替代DDB,那以后就会很方便了。但现实并不是如此,DDB还在Windows中扮演着很重要角色,尤其是您在乎程序执行表现好坏时。
建立DDB
DDB是Windows图形设备接口的图形对象之一(其中还包括绘图笔、画刷、字体、metafile和调色盘)。这些图形对象储存在GDI模块内部,由应用程序软件以句柄数字的方式引用。您可以将DDB句柄储存在一个HBITMAP(「handle to a bitmap:位图句柄」)型态的变量中,例如:
HBITMAP hBitmap ;
然后通过呼叫DDB建立的一个函数来获得句柄,例如:CreateBitmap。这些函数配置并初始化GDI内存中的一些内存来储存关于位图的信息,以及实际位图位的信息。应用程序不能直接存取这段内存。位图与设备内容无关。当程序使用完位图以后,就要清除这段内存:
DeleteObject (hBitmap) ;
如果程序执行时您使用了DDB,那么程序终止时,您可以完成上面的操作。
CreateBitmap函数用法如下:
hBitmap = CreateBitmap (cx, cy, cPlanes, cBitsPixel, bits) ;
前两个参数是位图的宽度和高度(以图素为单位),第三个参数是颜色面的数目,第四个参数是每图素的位数,第五个参数是指向一个以特定颜色格式存放的位数组的指针,数组内存放有用来初始化该DDB的图像。如果您不想用一张现有的图像来初始化DDB,可以将最后一个参数设为NULL。以后您还是可以设定该DDB内图素的内容。
使用此函数时,Windows也允许建立您喜欢的特定型态GDI位图对象。例如,假设您希望位图宽7个图素、高9个图素、5个?色位面,并且每个图素占3位,您只需要执行下面的操作:
hBitmap = CreateBitmap (7, 9, 5, 3, NULL) ;
这时Windows会好好给您一个有效的位图句柄。
在此函数呼叫期间,Windows将储存您传递给函数的信息,并为图素位配置内存。粗略的计算是此位图需要7×9×5×3,即945位,这需要比118个字节还多几个位。
然而,Windows为位图配置好内存以后,每行图素都占用许多连贯的字节,这样
iWidthBytes = 2 * ((cx * cBitsPixel + 15) / 16) ;
或者C程序写作者更倾向于写成:
iWidthBytes = (cx * cBitsPixel + 15) & ~15) >> 3 ;
因此,为DDB配置的内存就是:
iBitmapBytes = cy * cPlanes * iWidthBytes ;
本例中,iWidthBytes占4字节,iBitmapBytes占180字节。
现在,知道一张位图有5个颜色位面,每图素占3个颜色位有什么意义吗?真是见鬼了,这甚至不能把它称作一个习题作业。虽然您让GDI内部配置了些内存,并且让这些内存有一定结构的内容,但是您这张位图完全作不出任何有用的事情来。
实际上,您将用两种型态的参数来呼叫CreateBitmap。
- cPlanes和cBitsPixel都等于1(表示单色位图);或者
- cPlanes和cBitsPixel都等于某个特定设备内容的值,您可以使用PLANES和BITSPIXEL索引来从GetDeviceCaps函数获得。
更现实的情况下,您只会在第一种情况下呼叫CreateBitmap。对于第二种情况,您可以用CreateCompatibleBitmap来简化问题:
hBitmap = CreateCompatibleBitmap (hdc, cx, cy) ;
此函数建立了一个与设备兼容的位图,此设备的设备内容句柄由第一个参数给出。CreateCompatibleBitmap用设备内容句柄来获得GetDeviceCaps信息,然后将此信息传递给CreateBitmap。除了与实际的设备内容有相同的内存组织之外,DDB与设备内容没有其它联系。
CreateDiscardableBitmap函数与CreateCompatibleBitmap的参数相同,并且功能上相同。在早期的Windows版本中,CreateDiscardableBitmap建立的位图可以在内存减少时由Windows将其从内存中清除,然后程序再重建位图数据。
第三个位图建立函数是CreateBitmapIndirect:
hBitmap CreateBitmapIndirect (&bitmap) ;
其中bitmap是BITMAP型态的结构。BITMAP结构定义如下:
typedef struct _tagBITMAP
{
LONG bmType ; // set to 0
LONG bmWidth ; // width in pixels
LONG bmHeight ; // height in pixels
LONG bmWidthBytes ; // width of row in bytes
WORD bmPlanes ; // number of color planes
WORD bmBitsPixel ; // number of bits per pixel
LPVOIDbmBits ; // pointer to pixel bits
}
BITMAP, * PBITMAP ;
在呼叫CreateBitmapIndirect函数时,您不需要设定bmWidthBytes字段。Windows将为您计算,您也可以将bmBits字段设定为NULL,或者设定为初始化位图时用的图素位地址。
GetObject函数内也使用BITMAP结构,首先定义一个BITMAP型态的结构。
BITMAP bitmap ;
并呼叫函数如下:
GetObject (hBitmap, sizeof (BITMAP), &bitmap) ;
Windows将用位图信息填充BITMAP结构的字段,不过,bmBits字段等于NULL。
您最后应呼叫DeleteObject来清除程序内建立的所有位图。
位图位
用CreateBitmap或CreateBitmapIndirect来建立设备相关GDI位图对象时,您可以给位图图素位指定一个指针。或者您也可以让位图维持未初始化的状态。在建立位图以后,Windows还提供两个函数来获得并设定图素位。
要设定图素位,请呼叫:
SetBitmapBits (hBitmap, cBytes, &bits) ;
GetBitmapBits函数有相同的语法:
GetBitmapBits (hBitmap, cBytes, &bits) ;
在这两个函数中,cBytes指明要复制的字节数,bits是最少cBytes大小的缓冲区。
DDB中的图素位从顶列开始排列。我在前面说过,每列的字节数都是偶数。除此之外,没什么好说明的了。如果位图是单色的,也就是说它有1个位面并且每个图素占1位,则每个图素不是1就是0。每列最左边的图素是本列第一个字节最高位的位。我们在本章的后面讲完如何显示单色DDB之后,将做一个单色的DDB。
对于非单色位图,应避免出现您需要知道图素位含义的状况。例如,假定在8位颜色的VGA上执行Windows,您可以呼叫CreateCompatibleBitmap。通过GetDeviceCaps,您能够确定您正处理一个有1个颜色位面和每图素8位的设备。一个字节储存一个图素。但是图素值0x37是什么意思呢?很明显是某种颜色,但到底是什么颜色呢?
图素实际上并不涉及任何固定的颜色,它只是一个值。DDB没有颜色表。问题的关键在于:当DDB显示在屏幕上时,图素的颜色是什么。它肯定是某种颜色,但具体是什么颜色呢?显示的图素将与在显示卡上的调色盘查看表里的0x37索引值代表的RGB颜色有关。这就是您现在碰到的设备依赖性。
不过,不要只因为我们不知道图素值的含义,就假定非单色DDB没用。我们将简要看一下它们的用途。 下一章,我们将看到SetBitmapBits和GetBitmapBits函数是如何被更有用的SetDIBits和GetDIBits函数所取代的。
因此,基本的规则是这样的:不要用CreateBitmap、CreateBitmapIndirect或SetBitmapBits来设定彩色DDB的位,您只能安全地使用这些函数来设定单色DDB的位。(如果您在呼叫GetBitmapBits期间,从其它相同格式的DDB中获得位,那么这些规则例外。)
在继续之前,让我再讨论一下SetBitmapDimensionEx和GetBitmapDimensionEx函数。这些函数让您设定(和获得)位图的测量尺寸(以0.1毫米为单位)。这些信息与位图分辨率一起储存在GDI中,但不用于任何操作。它只是您与DDB联系的一个测量尺寸标识。
内存设备内容
我们必须解决的下一个概念是内存设备内容。您需要用内存设备内容来处理GDI位图对象。
通常,设备内容指的是特殊的图形输出设备(例如视讯显示器或者打印机)及其设备驱动程序。内存设备内容只位于内存中,它不是真正的图形输出设备,但可以说与指定的真正设备「兼容」。
要建立一个内存设备内容,您必须首先有实际设备的设备内容句柄。如果是hdc,那么您可以像下面那样建立内存设备内容:
hdcMem = CreateCompatibleDC (hdc) ;
通常,函数的呼叫比这更简单。如果您将参数设为NULL,那么Windows将建立一个与视讯显示器相兼容的内存设备内容。应用程序建立的任何内存设备内容最终都通过呼叫DeleteDC来清除。
内存设备内容有一个与实际位映像设备相同的显示平面。不过,最初此显示平面非常小-单色、1图素宽、1图素高。显示平面就是单独1位。
当然,用1位的显示平面,您不能做更多的工作,因此下一步就是扩大显示平面。您可以通过将一个GDI位图对象选进内存设备内容来完成这项工作,例如:
SelectObject (hdcMem, hBitmap) ;
此函数与您将画笔、画刷、字体、区域和调色盘选进设备内容的函数相同。然而,内存设备内容是您可以选进位图的唯一一种设备内容型态。(如果需要,您也可以将其它GDI对象选进内存设备内容。)
只有选进内存设备内容的位图是单色的,或者与内存设备内容兼容设备有相同的色彩组织时,SelectObject才会起作用。这也是建立特殊的DDB(例如有5个位面,且每图素3位)没有用的原因。
现在情况是这样:SelectObject呼叫以后,DDB就是内存设备内容的显示平面。处理实际设备内容的每项操作,您几乎都可以用于内存设备内容。例如,如果用GDI画图函数在内存设备内容中画图,那么图像将画在位图上。这是非常有用的。还可以将内存设备内容作为来源,把视讯设备内容作为目的来呼叫BitBlt。这就是在显示器上绘制位图的方法。如果把视讯设备内容作为来源,把内存设备内容作为目的,那么呼叫BitBlt可将屏幕上的一些内容复制给位图。我们将看到这些都是可能的。
载入位图资源
除了各种各样的位图建立函数以外,获得GDI位图对象句柄的另一个方法就是呼叫LoadBitmap函数。使用此函数,您不必担心位图格式。在程序中,您只需简单地按资源来建立位图,这与建立图标或者鼠标光标的方法类似。LoadBitmap函数的语法与LoadIcon和LoadCursor相同:
hBitmap = LoadBitmap (hInstance, szBitmapName) ;
如果想加载系统位图,那么将第一个参数设为NULL。这些不同的位图是Windows视觉接口(例如关闭方块和勾选标记)的一小部分,它们的标识符以字母OBM开始。如果位图与整数标识符而不是与名称有联系,那么第二个参数就可以使用MAKEINTRESOURCE宏。由LoadBitmap加载的所有位图最终应用DeleteObject清除。
如果位图资源是单色的,那么从LoadBitmap传回的句柄将指向一个单色的位图对象。如果位图资源不是单色,那么从LoadBitmap传回的句柄将指向一个GDI位图对象,该对象与执行程序的视讯显示器有相同的色彩组织。因此,位图始终与视讯显示器兼容,并且总是选进与视讯显示器兼容的内存设备内容中。采用LoadBitmap呼叫后,就不用担心任何色彩转换的问题了。在下一章中,我们就知道LoadBitmap的具体运作方式了。
程序14-3所示的BRICKS1程序示范了加载一小张单色位图资源的方法。此位图本身不像砖块,但当它水平和垂直重复时,就与砖墙相似了。
程序14-3 BRICKS1
BRICKS1.C
/*--------------------------------------------------------------------------
BRICKS1.C -- LoadBitmap Demonstration
(c) Charles Petzold, 1998
----------------------------------------------------------------------------*/
#include <windows.h>
LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM) ;
int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance,
PSTR szCmdLine, int iCmdShow)
{
static TCHAR szAppName [] = TEXT ("Bricks1") ;
HWND hwnd ;
MSG msg ;
WNDCLASS wndclass ;
wndclass.style = CS_HREDRAW | CS_VREDRAW ;
wndclass.lpfnWndProc = WndProc ;
wndclass.cbClsExtra = 0 ;
wndclass.cbWndExtra = 0 ;
wndclass.hInstance = hInstance ;
wndclass.hIcon = LoadIcon (NULL, IDI_APPLICATION) ;
wndclass.hCursor = LoadCursor (NULL, IDC_ARROW) ;
wndclass.hbrBackground = (HBRUSH) GetStockObject (WHITE_BRUSH) ;
wndclass.lpszMenuName = NULL ;
wndclass.lpszClassName = szAppName ;
if (!RegisterClass (&wndclass))
{
MessageBox (NULL, TEXT ("This program requires Windows NT!"),
szAppName, MB_ICONERROR) ;
return 0 ;
}
hwnd = CreateWindow ( szAppName, TEXT ("LoadBitmap Demo"),
WS_OVERLAPPEDWINDOW,
CW_USEDEFAULT, CW_USEDEFAULT,
CW_USEDEFAULT, CW_USEDEFAULT,
NULL, NULL, hInstance, NULL) ;
ShowWindow (hwnd, iCmdShow) ;
UpdateWindow (hwnd) ;
while (GetMessage (&msg, NULL, 0, 0))
{
TranslateMessage (&msg) ;
DispatchMessage (&msg) ;
}
return msg.wParam ;
}
LRESULT CALLBACK WndProc ( HWND hwnd, UINT message, WPARAM wParam,LPARAM lParam)
{
static HBITMAP hBitmap ;
static int cxClient, cyClient, cxSource, cySource ;
BITMAP bitmap ;
HDC hdc, hdcMem ;
HINSTANCE hInstance ;
int x, y ;
PAINTSTRUCT ps ;
switch (message)
{
case WM_CREATE:
hInstance = ((LPCREATESTRUCT) lParam)->hInstance ;
hBitmap = LoadBitmap (hInstance, TEXT ("Bricks")) ;
GetObject (hBitmap, sizeof (BITMAP), &bitmap) ;
cxSource = bitmap.bmWidth ;
cySource = bitmap.bmHeight ;
return 0 ;
case WM_SIZE:
cxClient = LOWORD (lParam) ;
cyClient = HIWORD (lParam) ;
return 0 ;
case WM_PAINT:
hdc = BeginPaint (hwnd, &ps) ;
hdcMem = CreateCompatibleDC (hdc) ;
SelectObject (hdcMem, hBitmap) ;
for (y = 0 ; y < cyClient ; y += cySource)
for (x = 0 ; x < cxClient ; x += cxSource)
{
BitBlt (hdc, x, y, cxSource, cySource, hdcMem, 0, 0, SRCCOPY) ;
}
DeleteDC (hdcMem) ;
EndPaint (hwnd, &ps) ;
return 0 ;
case WM_DESTROY:
DeleteObject (hBitmap) ;
PostQuitMessage (0) ;
return 0 ;
}
return DefWindowProc (hwnd, message, wParam, lParam) ;
}
BRICKS1.RC (摘录)
//Microsoft Developer Studio generated resource script.
#include "resource.h"
#include "afxres.h"
/
// Bitmap
BRICKS BITMAP DISCARDABLE "Bricks.bmp"
BRICKS.BMP |
在Visual C++ Developer Studio中建立位图时,应指明位图的高度和宽度都是8个图素,是单色,名称是「Bricks」。BRICKS1程序在WM_CREATE消息处理期间加载了位图并用GetObject来确定位图的图素尺寸(以便当位图不是8图素见方时程序仍能继续工作)。以后,BRICKS1将在WM_DESTROY消息中删除此位图。
在WM_PAINT消息处理期间,BRICKS1建立了一个与显示器兼容的内存设备内容,并且选进了位图。然后是从内存设备内容到显示区域设备内容一系列的BitBlt函数呼叫,再删除内存设备内容。图14-3显示了程序的执行结果。
顺便说一下,Developer Studio建立的BRICKS.BMP文件是一个设备无关位图。您可能想在Developer Studio内建立一个彩色的BRICKS.BMP文件(您可自己选定颜色),并且保证一切工作正常。
我们看到DIB能转换成与视讯显示器兼容的GDI位图对象。我们将在下一章看到这是如何操作的。
图14-3 BRICKS1的屏幕显示 |
单色位图格式
如果您在处理小块单色图像,那么您不必把它们当成资源来建立。与彩色位图对象不同,单色位的格式相对简单一些,而且几乎能全部从您要建立的图像中分离出来。例如,假定您要建立下图所示的位图:
您能写下一系列的位(0代表黑色,1代表白色),这些位直接对应于网格。从左到右读这些位,您能给每8字节配置一个十六进制元的字节值。如果位图的宽度不是16的倍数,在字节的右边用零填充,以得到偶数个字节:
0 1 0 1 0 0 0 1 0 1 1 1 0 1 1 1 0 0 0 1 = 51 77 10 00
0 1 0 1 0 1 1 1 0 1 1 1 0 1 1 1 0 1 0 1 = 57 77 50 00
0 0 0 1 0 0 1 1 0 1 1 1 0 1 1 1 0 1 0 1 = 13 77 50 00
0 1 0 1 0 1 1 1 0 1 1 1 0 1 1 1 0 1 0 1 = 57 77 50 00
0 1 0 1 0 0 0 1 0 0 0 1 0 0 0 1 0 0 0 1 = 51 11 10 00
图素宽为20,扫描线高为5,字节宽为4。您可以用下面的叙述来设定此位图的BITMAP结构:
static BITMAP bitmap = { 0, 20, 5, 4, 1, 1 } ;
并且可以将位储存在BYTE数组中:
static BYTE bits [] = { 0x51, 0x77, 0x10, 0x00,
0x57, 0x77, 0x50, 0x00,
0x13, 0x77, 0x50, 0x00,
0x57, 0x77, 0x50, 0x00,
0x51, 0x11, 0x10, 0x00 } ;
用CreateBitmapIndirect来建立位图需要下面两条叙述:
bitmap.bmBits = (PSTR) bits ;
hBitmap = CreateBitmapIndirect (&bitmap) ;
另一种方法是:
hBitmap = CreateBitmapIndirect (&bitmap) ;
SetBitmapBits (hBitmap, sizeof bits, bits) ;
您也可以用一道叙述来建立位图:
hBitmap = CreateBitmap (20, 5, 1, 1, bits) ;
在程序14-4显示的BRICKS2程序利用此技术直接建立了砖块位图,而没有使用资源。
程序14-4 BRICKS2
BRICKS2.C
/*--------------------------------------------------------------------------
BRICKS2.C -- CreateBitmap Demonstration
(c) Charles Petzold, 1998
---------------------------------------------------------------------------*/
#include <windows.h>
LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM) ;
int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance,
PSTR szCmdLine, int iCmdShow)
{
static TCHAR szAppName [] = TEXT ("Bricks2") ;
HWND hwnd ;
MSG msg ;
WNDCLASS wndclass ;
wndclass.style = CS_HREDRAW | CS_VREDRAW ;
wndclass.lpfnWndProc = WndProc ;
wndclass.cbClsExtra = 0 ;
wndclass.cbWndExtra = 0 ;
wndclass.hInstance = hInstance ;
wndclass.hIcon = LoadIcon (NULL, IDI_APPLICATION) ;
wndclass.hCursor = LoadCursor (NULL, IDC_ARROW) ;
wndclass.hbrBackground = (HBRUSH) GetStockObject (WHITE_BRUSH) ;
wndclass.lpszMenuName = NULL ;
wndclass.lpszClassName = szAppName ;
if (!RegisterClass (&wndclass))
{
MessageBox ( NULL, TEXT ("This program requires Windows NT!"),
szAppName, MB_ICONERROR) ;
return 0 ;
}
hwnd = CreateWindow (szAppName, TEXT ("CreateBitmap Demo"),
WS_OVERLAPPEDWINDOW,
CW_USEDEFAULT, CW_USEDEFAULT,
CW_USEDEFAULT, CW_USEDEFAULT,
NULL, NULL, hInstance, NULL) ;
ShowWindow (hwnd, iCmdShow) ;
UpdateWindow (hwnd) ;
while (GetMessage (&msg, NULL, 0, 0))
{
TranslateMessage (&msg) ;
DispatchMessage (&msg) ;
}
return msg.wParam ;
}
LRESULT CALLBACK WndProc ( HWND hwnd, UINT message, WPARAM wParam,LPARAM lParam)
{
static BITMA Pbitmap = { 0, 8, 8, 2, 1, 1 } ;
static BYTE bits [8][2]={ 0xFF, 0, 0x0C, 0, 0x0C, 0, 0x0C, 0,
0xFF, 0, 0xC0, 0, 0xC0, 0, 0xC0, 0 } ;
static HBITMAP hBitmap ;
static int cxClient, cyClient, cxSource, cySource ;
HDC hdc, hdcMem ;
int x, y ;
PAINTSTRUCT ps ;
switch (message)
{
case WM_CREATE:
bitmap.bmBits = bits ;
hBitmap = CreateBitmapIndirect (&bitmap) ;
cxSource = bitmap.bmWidth ;
cySource = bitmap.bmHeight ;
return 0 ;
case WM_SIZE:
cxClient = LOWORD (lParam) ;
cyClient = HIWORD (lParam) ;
return 0 ;
case WM_PAINT:
hdc = BeginPaint (hwnd, &ps) ;
hdcMem = CreateCompatibleDC (hdc) ;
SelectObject (hdcMem, hBitmap) ;
for (y = 0 ; y < cyClient ; y += cySource)
for (x = 0 ; x < cxClient ; x += cxSource)
{
BitBlt (hdc, x, y, cxSource, cySource, hdcMem, 0, 0, SRCCOPY) ;
}
DeleteDC (hdcMem) ;
EndPaint (hwnd, &ps) ;
return 0 ;
case WM_DESTROY:
DeleteObject (hBitmap) ;
PostQuitMessage (0) ;
return 0 ;
}
return DefWindowProc (hwnd, message, wParam, lParam) ;
}
您可以尝试一下与彩色位图相似的对象。例如,如果您的视讯显示器执行在256色模式下,那么您可以根据表14-2来定义彩色砖的每个图素。不过,当程序执行在其它显示模式下时,此程序代码不起作用。以设备无关方式处理彩色位图需要使用 下章讨论的DIB。
位图中的画刷
BRICKS系列的最后一个项目是BRICKS3,如程序14-5所示。乍看此程序,您可能会有这种感觉:程序代码哪里去了呢?
程序14-5 BRICKS3
BRICKS3.C
/*-------------------------------------------------------------------------
BRICKS3.C -- CreatePatternBrush Demonstration
(c) Charles Petzold, 1998
---------------------------------------------------------------------------*/
#include <windows.h>
LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM) ;
int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance,
PSTR szCmdLine, int iCmdShow)
{
static TCHAR szAppName [] = TEXT ("Bricks3") ;
HBITMAP hBitmap ;
HBRUSH hBrush ;
HWND hwnd ;
MSG msg ;
WNDCLASS wndclass ;
hBitmap = LoadBitmap (hInstance, TEXT ("Bricks")) ;
hBrush = CreatePatternBrush (hBitmap) ;
DeleteObject (hBitmap) ;
wndclass.style = CS_HREDRAW | CS_VREDRAW ;
wndclass.lpfnWndProc = WndProc ;
wndclass.cbClsExtra = 0 ;
wndclass.cbWndExtra = 0 ;
wndclass.hInstance = hInstance ;
wndclass.hIcon = LoadIcon (NULL, IDI_APPLICATION) ;
wndclass.hCursor = LoadCursor (NULL, IDC_ARROW) ;
wndclass.hbrBackground = hBrush ;
wndclass.lpszMenuName = NULL ;
wndclass.lpszClassName = szAppName ;
if (!RegisterClass (&wndclass))
{
MessageBox ( NULL, TEXT ("This program requires Windows NT!"),
szAppName, MB_ICONERROR) ;
return 0 ;
}
hwnd = CreateWindow (szAppName, TEXT ("CreatePatternBrush Demo"),
WS_OVERLAPPEDWINDOW,
CW_USEDEFAULT, CW_USEDEFAULT,
CW_USEDEFAULT, CW_USEDEFAULT,
NULL, NULL, hInstance, NULL) ;
ShowWindow (hwnd, iCmdShow) ;
UpdateWindow (hwnd) ;
while (GetMessage (&msg, NULL, 0, 0))
{
TranslateMessage (&msg) ;
DispatchMessage (&msg) ;
}
DeleteObject (hBrush) ;
return msg.wParam ;
}
LRESULT CALLBACK WndProc ( HWND hwnd, UINT message, WPARAM wParam,LPARAM lParam)
{
switch (message)
{
case WM_DESTROY:
PostQuitMessage (0) ;
return 0 ;
}
return DefWindowProc (hwnd, message, wParam, lParam) ;
}
BRICKS3.RC (摘录)
//Microsoft Developer Studio generated resource script.
#include "resource.h"
#include "afxres.h"
/
// Bitmap
BRICKS BITMAP DISCARDABLE "Bricks.bmp"
此程序与BRICKS1使用同一个BRICKS.BMP文件,而且窗口看上去也相同。
正如您看到的一样,窗口消息处理程序没有更多的内容。BRICKS3实际上使用砖块图案作为窗口类别背景画刷,它在WNDCLASS结构的hbrBackground字段中定义。
您现在可能猜想GDI画刷是很小的位图,通常是8个图素见方。如果将LOGBRUSH结构的lbStyle字段设定为BS_PATTERN,然后呼叫CreatePatternBrush或CreateBrushIndirect,您就可以在位图外面来建立画刷了。此位图至少是宽高各8个图素。如果再大,Windows 98将只使用位图的左上角作为画刷。而Windows NT不受此限制,它会使用整个位图。
请记住,画刷和位图都是GDI对象,而且您应该在程序终止前删除您在程序中建立画刷和位图。如果您依据位图建立画刷,那么在用画刷画图时,Windows将复制位图位到画刷所绘制的区域内。呼叫CreatePatternBrush(或者CreateBrushIndirect)之后,您可以立即删除位图而不会影响到画笔。类似地,您也可以删除画刷而不会影响到您选进的原始位图。注意,BRICKS3在建立画刷后删除了位图,并在程序终止前删除了画刷。
绘制位图
在窗口中绘图时,我们已经将位图当成绘图来源使用过了。这要求先将位图选进内存设备内容,并呼叫BitBlt或者StretchBlt。您也可以用内存设备内容句柄作为所有实际呼叫的GDI函数中的第一参数。内存设备内容的动作与实际的设备内容相同,除非显示平面是位图。
程序14-6所示的HELLOBIT程序展示了此项技术。程序在一个小位图上显示了字符串「Hello, world!」,然后从位图到程序显示区域执行BitBlt或StretchBlt(依照选择的菜单选项而定)。
程序14-6 HELLOBIT
HELLOBIT.C
/*-----------------------------------------------------------------------
HELLOBIT.C -- Bitmap Demonstration
(c) Charles Petzold, 1998
-------------------------------------------------------------------------*/
#include <windows.h>
#include "resource.h"
LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM) ;
int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance,
PSTR szCmdLine, int iCmdShow)
{
static TCHAR szAppName [] = TEXT ("HelloBit") ;
HWND hwnd ;
MSG msg ;
WNDCLASS wndclass ;
wndclass.style = CS_HREDRAW | CS_VREDRAW ;
wndclass.lpfnWndProc = WndProc ;
wndclass.cbClsExtra = 0 ;
wndclass.cbWndExtra = 0 ;
wndclass.hInstance = hInstance ;
wndclass.hIcon = LoadIcon (NULL, IDI_APPLICATION) ;
wndclass.hCursor = LoadCursor (NULL, IDC_ARROW) ;
wndclass.hbrBackground = (HBRUSH) GetStockObject (WHITE_BRUSH) ;
wndclass.lpszMenuName = szAppName ;
wndclass.lpszClassName = szAppName ;
if (!RegisterClass (&wndclass))
{
MessageBox ( NULL, TEXT ("This program requires Windows NT!"),
szAppName, MB_ICONERROR) ;
return 0 ;
}
hwnd = CreateWindow (szAppName, TEXT ("HelloBit"),
WS_OVERLAPPEDWINDOW,
CW_USEDEFAULT, CW_USEDEFAULT,
CW_USEDEFAULT, CW_USEDEFAULT,
NULL, NULL, hInstance, NULL) ;
ShowWindow (hwnd, iCmdShow) ;
UpdateWindow (hwnd) ;
while (GetMessage (&msg, NULL, 0, 0))
{
TranslateMessage (&msg) ;
DispatchMessage (&msg) ;
}
return msg.wParam ;
}
LRESULT CALLBACK WndProc ( HWND hwnd, UINT message, WPARAM wParam,LPARAM lParam)
{
static HBITMAP hBitmap ;
static HDC hdcMem ;
static int cxBitmap, cyBitmap, cxClient, cyClient, iSize = IDM_BIG ;
static TCHAR * szText = TEXT (" Hello, world! ") ;
HDC hdc ;
HMENU hMenu ;
int x, y ;
PAINTSTRUCT ps ;
SIZE size ;
switch (message)
{
case WM_CREATE:
hdc = GetDC (hwnd) ;
hdcMem = CreateCompatibleDC (hdc) ;
GetTextExtentPoint32 (hdc, szText, lstrlen (szText), &size) ;
cxBitmap = size.cx ;
cyBitmap = size.cy ;
hBitmap = CreateCompatibleBitmap (hdc, cxBitmap, cyBitmap) ;
ReleaseDC (hwnd, hdc) ;
SelectObject (hdcMem, hBitmap) ;
TextOut (hdcMem, 0, 0, szText, lstrlen (szText)) ;
return 0 ;
case WM_SIZE:
cxClient = LOWORD (lParam) ;
cyClient = HIWORD (lParam) ;
return 0 ;
case WM_COMMAND:
hMenu = GetMenu (hwnd) ;
switch (LOWORD (wParam))
{
case IDM_BIG:
case IDM_SMALL:
CheckMenuItem (hMenu, iSize, MF_UNCHECKED) ;
iSize = LOWORD (wParam) ;
CheckMenuItem (hMenu, iSize, MF_CHECKED) ;
InvalidateRect (hwnd, NULL, TRUE) ;
break ;
}
return 0 ;
case WM_PAINT:
hdc = BeginPaint (hwnd, &ps) ;
switch (iSize)
{
case IDM_BIG:
StretchBlt (hdc, 0, 0, cxClient, cyClient,
hdcMem, 0, 0, cxBitmap, cyBitmap, SRCCOPY) ;
break ;
case IDM_SMALL:
for (y = 0 ; y < cyClient ; y += cyBitmap)
for (x = 0 ; x < cxClient ; x += cxBitmap)
{
BitBlt (hdc, x, y, cxBitmap, cyBitmap,
hdcMem, 0, 0, SRCCOPY) ;
}
break ;
}
EndPaint (hwnd, &ps) ;
return 0 ;
case WM_DESTROY:
DeleteDC (hdcMem) ;
DeleteObject (hBitmap) ;
PostQuitMessage (0) ;
return 0 ;
}
return DefWindowProc (hwnd, message, wParam, lParam) ;
}
HELLOBIT.RC (摘录)
//Microsoft Developer Studio generated resource script.
#include "resource.h"
#include "afxres.h"
/
// Menu
HELLOBIT MENU DISCARDABLE
BEGIN
POPUP "&Size"
BEGIN
MENUITEM "&Big", IDM_BIG, CHECKED
MENUITEM "&Small", IDM_SMALL
END
END
RESOURCE.H (摘录)
// Microsoft Developer Studio generated include file.
// Used by HelloBit.rc
#define IDM_BIG 40001
#define IDM_SMALL 40002
程序从呼叫GetTextExtentPoint32确定字符串的图素尺寸开始。这些尺寸将成为与视讯显示兼容的位图尺寸。当此位图被选进内存设备内容(也与视讯显示兼容)后,再呼叫TextOut将文字显示在位图上。内存设备内容在程序执行期间保留。在处理WM_DESTROY信息期间,HELLOBIT删除了位图和内存设备内容。
HELLOBIT中的一条菜单选项允许您显示位图尺寸,此尺寸或者是显示区域中水平和垂直方向平铺的实际尺寸,或者是缩放成显示区域大小的尺寸,如图14-4所示。正与您所见到的一样,这不是显示大尺寸字符的好方法!它只是小字体的放大版,并带有放大时产生的锯齿线。
图14-4 HELLOBIT的屏幕显示 |
您可能想知道一个程序,例如HELLOBIT,是否需要处理WM_DISPLAYCHANGE消息。只要使用者(或者其它应用程序)修改了视讯显示大小或者颜色深度,应用程序就接收到此讯息。其中颜色深度的改变会导致内存设备内容和视讯设备内容不兼容。但这并不会发生,因为当显示模式修改后,Windows自动修改了内存设备内容的颜色分辨率。选进内存设备内容的位图仍然保持原样,但不会造成任何问题。
阴影位图
在内存设备内容绘图(也就是位图)的技术是执行「阴影位图(shadow bitmap)」的关键。此位图包含窗口显示区域中显示的所有内容。这样,对WM_PAINT消息的处理就简化到简单的BitBlt。
阴影位图在绘画程序中最有用。程序14-7所示的SKETCH程序并不是一个最完美的绘画程序,但它是一个开始。
程序14-7 SKETCH
SKETCH.C
/*-------------------------------------------------------------------------
SKETCH.C -- Shadow Bitmap Demonstration
(c) Charles Petzold, 1998
---------------------------------------------------------------------------*/
#include <windows.h>
LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM) ;
int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance,
PSTR szCmdLine, int iCmdShow)
{
static TCHAR szAppName [] = TEXT ("Sketch") ;
HWND hwnd ;
MSG msg ;
WNDCLASS wndclass ;
wndclass.style = CS_HREDRAW | CS_VREDRAW ;
wndclass.lpfnWndProc = WndProc ;
wndclass.cbClsExtra = 0 ;
wndclass.cbWndExtra = 0 ;
wndclass.hInstance = hInstance ;
wndclass.hIcon = LoadIcon (NULL, IDI_APPLICATION) ;
wndclass.hCursor = LoadCursor (NULL, IDC_ARROW) ;
wndclass.hbrBackground = (HBRUSH) GetStockObject (WHITE_BRUSH) ;
wndclass.lpszMenuName = NULL ;
wndclass.lpszClassName = szAppName ;
if (!RegisterClass (&wndclass))
{
MessageBox ( NULL, TEXT ("This program requires Windows NT!"),
szAppName, MB_ICONERROR) ;
return 0 ;
}
hwnd = CreateWindow (szAppName, TEXT ("Sketch"),
WS_OVERLAPPEDWINDOW,
CW_USEDEFAULT, CW_USEDEFAULT,
CW_USEDEFAULT, CW_USEDEFAULT,
NULL, NULL, hInstance, NULL) ;
if (hwnd == NULL)
{
MessageBox ( NULL, TEXT ("Not enough memory to create bitmap!"),
szAppName, MB_ICONERROR) ;
return 0 ;
}
ShowWindow (hwnd, iCmdShow) ;
UpdateWindow (hwnd) ;
while (GetMessage (&msg, NULL, 0, 0))
{
TranslateMessage (&msg) ;
DispatchMessage (&msg) ;
}
return msg.wParam ;
}
void GetLargestDisplayMode (int * pcxBitmap, int * pcyBitmap)
{
DEVMODE devmode ;
int iModeNum = 0 ;
* pcxBitmap = * pcyBitmap = 0 ;
ZeroMemory (&devmode, sizeof (DEVMODE)) ;
devmode.dmSize = sizeof (DEVMODE) ;
while (EnumDisplaySettings (NULL, iModeNum++, &devmode))
{
* pcxBitmap = max (* pcxBitmap, (int) devmode.dmPelsWidth) ;
* pcyBitmap = max (* pcyBitmap, (int) devmode.dmPelsHeight) ;
}
}
LRESULT CALLBACK WndProc ( HWND hwnd, UINT message, WPARAM wParam,LPARAM lParam)
{
static BOOL fLeftButtonDown, fRightButtonDown ;
static HBITMAP hBitmap ;
static HDC hdcMem ;
static int cxBitmap, cyBitmap, cxClient, cyClient, xMouse, yMouse ;
HDC hdc ;
PAINTSTRUCT ps ;
switch (message)
{
case WM_CREATE:
GetLargestDisplayMode (&cxBitmap, &cyBitmap) ;
hdc = GetDC (hwnd) ;
hBitmap = CreateCompatibleBitmap (hdc, cxBitmap, cyBitmap) ;
hdcMem = CreateCompatibleDC (hdc) ;
ReleaseDC (hwnd, hdc) ;
if (!hBitmap) // no memory for bitmap
{
DeleteDC (hdcMem) ;
return -1 ;
}
SelectObject (hdcMem, hBitmap) ;
PatBlt (hdcMem, 0, 0, cxBitmap, cyBitmap, WHITENESS) ;
return 0 ;
case WM_SIZE:
cxClient = LOWORD (lParam) ;
cyClient = HIWORD (lParam) ;
return 0 ;
case WM_LBUTTONDOWN:
if (!fRightButtonDown)
SetCapture (hwnd) ;
xMouse = LOWORD (lParam) ;
yMouse = HIWORD (lParam) ;
fLeftButtonDown = TRUE ;
return 0 ;
case WM_LBUTTONUP:
if (fLeftButtonDown)
SetCapture (NULL) ;
fLeftButtonDown = FALSE ;
return 0 ;
case WM_RBUTTONDOWN:
if (!fLeftButtonDown)
SetCapture (hwnd) ;
xMouse = LOWORD (lParam) ;
yMouse = HIWORD (lParam) ;
fRightButtonDown = TRUE ;
return 0 ;
case WM_RBUTTONUP:
if (fRightButtonDown)
SetCapture (NULL) ;
fRightButtonDown = FALSE ;
return 0 ;
case WM_MOUSEMOVE:
if (!fLeftButtonDown && !fRightButtonDown)
return 0 ;
hdc = GetDC (hwnd) ;
SelectObject (hdc,
GetStockObject (fLeftButtonDown ? BLACK_PEN : WHITE_PEN)) ;
SelectObject (hdcMem,
GetStockObject (fLeftButtonDown ? BLACK_PEN : WHITE_PEN)) ;
MoveToEx (hdc, xMouse, yMouse, NULL) ;
MoveToEx (hdcMem, xMouse, yMouse, NULL) ;
xMouse = (short) LOWORD (lParam) ;
yMouse = (short) HIWORD (lParam) ;
LineTo (hdc, xMouse, yMouse) ;
LineTo (hdcMem, xMouse, yMouse) ;
ReleaseDC (hwnd, hdc) ;
return 0 ;
case WM_PAINT:
hdc = BeginPaint (hwnd, &ps) ;
BitBlt (hdc, 0, 0, cxClient, cyClient, hdcMem, 0, 0, SRCCOPY) ;
EndPaint (hwnd, &ps) ;
return 0 ;
case WM_DESTROY:
DeleteDC (hdcMem) ;
DeleteObject (hBitmap) ;
PostQuitMessage (0) ;
return 0 ;
}
return DefWindowProc (hwnd, message, wParam, lParam) ;
}
要想在SKETCH中画线,请按下鼠标左键并拖动鼠标。要擦掉画过的东西(更确切地说,是画白线),请按下鼠标右键并拖动鼠标。要清空整个窗口,请…结束程序,然后重新加载,一切从头再来。图14-5中显示的SKETCH程序图样表达了对频果公司的麦金塔计算机早期广告的敬意。
图14-5 SKETCH的屏幕显示 |
此阴影位图应多大?在本程序中,它应该大到能包含最大化窗口的整个显示区域。这一问题很容易根据GetSystemMetrics信息计算得出,但如果使用者修改了显示设定后再显示,进而扩大了最大化时窗口的尺寸,这时将发生什么呢?SKETCH程序在EnumDisplaySettings函数的帮助下解决了此问题。此函数使用DEVMODE结构来传回全部有效视讯显示模式的信息。第一次呼叫此函数时,应将EnumDisplaySettings的第二参数设为0,以后每次呼叫此值都增加。EnumDisplaySettings传回FALSE时完成。
与此同时,SKETCH将建立一个阴影位图,它比目前视讯显示模式的表面还多四倍,而且需要几兆字节的内存。由于如此,SKETCH将检查位图是否建立成功了,如果没有建立,就从WM_CREATE传回-1,以表示错误。
在WM_MOUSEMOVE消息处理期间,按下鼠标左键或者右键,并在内存设备内容和显示区域设备内容中画线时,SKETCH拦截鼠标。如果画线方式更复杂的话,您可能想在一个函数中实作,程序将呼叫此函数两次-一次画在视讯设备内容上,一次画在内存设备内容上。
下面是一个有趣的实验:使SKETCH窗口小于全画面尺寸。随着鼠标左键的按下,将鼠标拖出窗口的右下角。因为SKETCH拦截鼠标,所以它继续接收并处理WM_MOUSEMOVE消息。现在扩大窗口,您将看到阴影位图包含您在SKETCH窗口外所画的内容。
在菜单中使用位图
您也可以用位图在菜单上显示选项。如果您联想起菜单中文件夹、剪贴簿和资源回收筒的图片,那么不要再想那些图片了。您应该考虑一下,菜单上显示位图对画图程序用途有多大,想象一下在菜单中使用不同字体和字体大小、线宽、阴影图案以及颜色。
GRAFMENU是展示图形菜单选项的范例程序。此程序顶层菜单如图14-6所示。放大的字母来自于40×16图素的单色位图文件,该文件在Visual C++ Developer Studio建立。从菜单上选择「FONT」将弹出三个选择项-「Courier New」、「 Arial」和「Times New Roman」。它们是标准的Windows TrueType字体,并且每一个都按其相关的字体显示,如图14-7所示。这些位图在程序中用内存设备内容建立。
图14-6 GRAFMENU程序的顶层菜单 |
图14-7 GRAFMENU程序弹出的「FONT」菜单 |
最后,在拉下系统菜单时,您将获得一些「辅助」信息,用「HELP」表示了新使用者的在线求助项目(参见图14-8)。此64×64图素的单色位图是在Developer Studio中建立的。
图14-8 GRAFMENU程序系统菜单 |
GRAFMENU程序,包括四个Developer Studio中建立的位图,如程序14-8所示。
GRAFMENU.C
/*----------------------------------------------------------------------------
GRAFMENU.C -- Demonstrates Bitmap Menu Items
(c) Charles Petzold, 1998
-----------------------------------------------------------------------------*/
#include <windows.h>
#include "resource.h"
LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM) ;
void AddHelpToSys (HINSTANCE, HWND) ;
HMENU CreateMyMenu (HINSTANCE) ;
HBITMAP StretchBitmap (HBITMAP) ;
HBITMAP GetBitmapFont (int) ;
void DeleteAllBitmaps (HWND) ;
TCHAR szAppName[] = TEXT ("GrafMenu") ;
int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance,
PSTR szCmdLine, int iCmdShow)
{
HWND hwnd ;
MSG msg ;
WNDCLASS wndclass ;
wndclass.style = CS_HREDRAW | CS_VREDRAW ;
wndclass.lpfnWndProc = WndProc ;
wndclass.cbClsExtra = 0 ;
wndclass.cbWndExtra = 0 ;
wndclass.hInstance = hInstance ;
wndclass.hIcon = LoadIcon (NULL, IDI_APPLICATION) ;
wndclass.hCursor = LoadCursor (NULL, IDC_ARROW) ;
wndclass.hbrBackground = (HBRUSH) GetStockObject (WHITE_BRUSH) ;
wndclass.lpszMenuName = NULL ;
wndclass.lpszClassName = szAppName ;
if (!RegisterClass (&wndclass))
{
MessageBox ( NULL, TEXT ("This program requires Windows NT!"),
szAppName, MB_ICONERROR) ;
return 0 ;
}
hwnd = CreateWindow (szAppName,TEXT ("Bitmap Menu Demonstration"),
WS_OVERLAPPEDWINDOW,
CW_USEDEFAULT, CW_USEDEFAULT,
CW_USEDEFAULT, CW_USEDEFAULT,
NULL, NULL, hInstance, NULL) ;
ShowWindow (hwnd, iCmdShow) ;
UpdateWindow (hwnd) ;
while (GetMessage (&msg, NULL, 0, 0))
{
TranslateMessage (&msg) ;
DispatchMessage (&msg) ;
}
return msg.wParam ;
}
LRESULT CALLBACK WndProc (HWND hwnd, UINT iMsg, WPARAM wParam,LPARAM lParam)
{
HMENU hMenu ;
static int iCurrentFont = IDM_FONT_COUR ;
switch (iMsg)
{
case WM_CREATE:
AddHelpToSys (((LPCREATESTRUCT) lParam)->hInstance, hwnd) ;
hMenu = CreateMyMenu (((LPCREATESTRUCT) lParam)->hInstance) ;
SetMenu (hwnd, hMenu) ;
CheckMenuItem (hMenu, iCurrentFont, MF_CHECKED) ;
return 0 ;
case WM_SYSCOMMAND:
switch (LOWORD (wParam))
{
case IDM_HELP:
MessageBox (hwnd, TEXT ("Help not yet implemented!"),
szAppName, MB_OK | MB_ICONEXCLAMATION) ;
return 0 ;
}
break ;
case WM_COMMAND:
switch (LOWORD (wParam))
{
case IDM_FILE_NEW:
case IDM_FILE_OPEN:
case IDM_FILE_SAVE:
case IDM_FILE_SAVE_AS:
case IDM_EDIT_UNDO:
case IDM_EDIT_CUT:
case IDM_EDIT_COPY:
case IDM_EDIT_PASTE:
case IDM_EDIT_CLEAR:
MessageBeep (0) ;
return 0 ;
case IDM_FONT_COUR:
case IDM_FONT_ARIAL:
case IDM_FONT_TIMES:
hMenu = GetMenu (hwnd) ;
CheckMenuItem (hMenu, iCurrentFont, MF_UNCHECKED) ;
iCurrentFont = LOWORD (wParam) ;
CheckMenuItem (hMenu, iCurrentFont, MF_CHECKED) ;
return 0 ;
}
break ;
case WM_DESTROY:
DeleteAllBitmaps (hwnd) ;
PostQuitMessage (0) ;
return 0 ;
}
return DefWindowProc (hwnd, iMsg, wParam, lParam) ;
}
/*--------------------------------------------------------------------------
AddHelpToSys: Adds bitmap Help item to system menu
------------------------------------------------------------------------*/
void AddHelpToSys (HINSTANCE hInstance, HWND hwnd)
{
HBITMAP hBitmap ;
HMENU hMenu ;
hMenu = GetSystemMenu (hwnd, FALSE);
hBitmap = StretchBitmap (LoadBitmap (hInstance, TEXT ("BitmapHelp"))) ;
AppendMenu (hMenu, MF_SEPARATOR, 0, NULL) ;
AppendMenu (hMenu, MF_BITMAP, IDM_HELP, (PTSTR) (LONG) hBitmap) ;
}
/*------------------------------------------------------------------------
CreateMyMenu: Assembles menu from components
--------------------------------------------------------------------------*/
HMENU CreateMyMenu (HINSTANCE hInstance)
{
HBITMAP hBitmap ;
HMENU hMenu, hMenuPopup ;
int i ;
hMenu = CreateMenu () ;
hMenuPopup = LoadMenu (hInstance, TEXT ("MenuFile")) ;
hBitmap = StretchBitmap (LoadBitmap (hInstance, TEXT ("BitmapFile"))) ;
AppendMenu (hMenu, MF_BITMAP | MF_POPUP, (int) hMenuPopup,
(PTSTR) (LONG) hBitmap) ;
hMenuPopup = LoadMenu (hInstance, TEXT ("MenuEdit")) ;
hBitmap = StretchBitmap (LoadBitmap (hInstance, TEXT ("BitmapEdit"))) ;
AppendMenu (hMenu, MF_BITMAP | MF_POPUP, (int) hMenuPopup,
(PTSTR) (LONG) hBitmap) ;
hMenuPopup = CreateMenu () ;
for (i = 0 ; i < 3 ; i++)
{
hBitmap = GetBitmapFont (i) ;
AppendMenu (hMenuPopup, MF_BITMAP, IDM_FONT_COUR + i,
(PTSTR) (LONG) hBitmap) ;
}
hBitmap = StretchBitmap (LoadBitmap (hInstance, TEXT ("BitmapFont"))) ;
AppendMenu (hMenu, MF_BITMAP | MF_POPUP, (int) hMenuPopup,
(PTSTR) (LONG) hBitmap) ;
return hMenu ;
}
/*-------------------------------------------------------------------------
StretchBitmap: Scales bitmap to display resolution
---------------------------------------------------------------------------*/
HBITMAP StretchBitmap (HBITMAP hBitmap1)
{
BITMAP bm1, bm2 ;
HBITMAP hBitmap2 ;
HDC hdc, hdcMem1, hdcMem2 ;
int cxChar, cyChar ;
// Get the width and height of a system font character
cxChar = LOWORD (GetDialogBaseUnits ()) ;
cyChar = HIWORD (GetDialogBaseUnits ()) ;
// Create 2 memory DCs compatible with the display
hdc = CreateIC (TEXT ("DISPLAY"), NULL, NULL, NULL) ;
hdcMem1 = CreateCompatibleDC (hdc) ;
hdcMem2 = CreateCompatibleDC (hdc) ;
DeleteDC (hdc) ;
// Get the dimensions of the bitmap to be stretched
GetObject (hBitmap1, sizeof (BITMAP), (PTSTR) &bm1) ;
// Scale these dimensions based on the system font size
bm2 = bm1 ;
bm2.bmWidth = (cxChar * bm2.bmWidth) / 4 ;
bm2.bmHeight = (cyChar * bm2.bmHeight) / 8 ;
bm2.bmWidthBytes = ((bm2.bmWidth + 15) / 16) * 2 ;
// Create a new bitmap of larger size
hBitmap2 = CreateBitmapIndirect (&bm2) ;
// Select the bitmaps in the memory DCs and do a StretchBlt
SelectObject (hdcMem1, hBitmap1) ;
SelectObject (hdcMem2, hBitmap2) ;
StretchBlt (hdcMem2, 0, 0, bm2.bmWidth, bm2.bmHeight,
hdcMem1, 0, 0, bm1.bmWidth, bm1.bmHeight, SRCCOPY) ;
// Clean up
DeleteDC (hdcMem1) ;
DeleteDC (hdcMem2) ;
DeleteObject (hBitmap1) ;
return hBitmap2 ;
}
/*---------------------------------------------------------------------------
GetBitmapFont: Creates bitmaps with font names
-----------------------------------------------------------------------------*/
HBITMAP GetBitmapFont (int i)
{
static TCHAR * szFaceName[3]= { TEXT ("Courier New"), TEXT ("Arial"),
TEXT ("Times New Roman") } ;
HBITMAP hBitmap ;
HDC hdc, hdcMem ;
HFONT hFont ;
SIZE size ;
TEXTMETRIC tm ;
hdc = CreateIC (TEXT ("DISPLAY"), NULL, NULL, NULL) ;
GetTextMetrics (hdc, &tm) ;
hdcMem = CreateCompatibleDC (hdc) ;
hFont = CreateFont (2 * tm.tmHeight, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
szFaceName[i]) ;
hFont = (HFONT) SelectObject (hdcMem, hFont) ;
GetTextExtentPoint32 (hdcMem, szFaceName[i],
lstrlen (szFaceName[i]), &size);
hBitmap = CreateBitmap (size.cx, size.cy, 1, 1, NULL) ;
SelectObject (hdcMem, hBitmap) ;
TextOut (hdcMem, 0, 0, szFaceName[i], lstrlen (szFaceName[i])) ;
DeleteObject (SelectObject (hdcMem, hFont)) ;
DeleteDC (hdcMem) ;
DeleteDC (hdc) ;
return hBitmap ;
}
/*---------------------------------------------------------------------------
DeleteAllBitmaps: Deletes all the bitmaps in the menu
-------------------------------------------------------------------------*/
void DeleteAllBitmaps (HWND hwnd)
{
HMENU hMenu ;
int i ;
MENUITEMINFO mii = { sizeof (MENUITEMINFO), MIIM_SUBMENU | MIIM_TYPE } ;
// Delete Help bitmap on system menu
hMenu = GetSystemMenu (hwnd, FALSE);
GetMenuItemInfo (hMenu, IDM_HELP, FALSE, &mii) ;
DeleteObject ((HBITMAP) mii.dwTypeData) ;
// Delete top-level menu bitmaps
hMenu = GetMenu (hwnd) ;
for (i = 0 ; i < 3 ; i++)
{
GetMenuItemInfo (hMenu, i, TRUE, &mii) ;
DeleteObject ((HBITMAP) mii.dwTypeData) ;
}
// Delete bitmap items on Font menu
hMenu = mii.hSubMenu ;;
for (i = 0 ; i < 3 ; i++)
{
GetMenuItemInfo (hMenu, i, TRUE, &mii) ;
DeleteObject ((HBITMAP) mii.dwTypeData) ;
}
}
GRAFMENU.RC (摘录)
//Microsoft Developer Studio generated resource script.
#include "resource.h"
#include "afxres.h"
/
// Menu
MENUFILE MENU DISCARDABLE
BEGIN
MENUITEM "&New", IDM_FILE_NEW
MENUITEM "&Open...", IDM_FILE_OPEN
MENUITEM "&Save", IDM_FILE_SAVE
MENUITEM "Save &As...", IDM_FILE_SAVE_AS
END
MENUEDIT MENU DISCARDABLE
BEGIN
MENUITEM "&Undo", IDM_EDIT_UNDO
MENUITEM SEPARATOR
MENUITEM "Cu&t", IDM_EDIT_CUT
MENUITEM "&Copy", IDM_EDIT_COPY
MENUITEM "&Paste", IDM_EDIT_PASTE
MENUITEM "De&lete", IDM_EDIT_CLEAR
END
/
// Bitmap
BITMAPFONT BITMAP DISCARDABLE "Fontlabl.bmp"
BITMAPHELP BITMAP DISCARDABLE "Bighelp.bmp"
BITMAPEDIT BITMAP DISCARDABLE "Editlabl.bmp"
BITMAPFILE BITMAP DISCARDABLE "Filelabl.bmp"
RESOURCE.H (摘录)
// Microsoft Developer Studio generated include file.
// Used by GrafMenu.rc
#define IDM_FONT_COUR 101
#define IDM_FONT_ARIAL 102
#define IDM_FONT_TIMES 103
#define IDM_HELP 104
#define IDM_EDIT_UNDO 40005
#define IDM_EDIT_CUT 40006
#define IDM_EDIT_COPY 40007
#define IDM_EDIT_PASTE 40008
#define IDM_EDIT_CLEAR 40009
#define IDM_FILE_NEW 40010
#define IDM_FILE_OPEN 40011
#define IDM_FILE_SAVE 40012
#define IDM_FILE_SAVE_AS 40013
EDITLABL.BMP
|
FILELABL.BMP
|
FONTLABL.BMP
|
BIGHELP.BMP
|
要将位图插入菜单,可以利用AppendMenu或InsertMenu。位图有两个来源:可以在Visual C++ Developer Studio建立位图,包括资源脚本中的位图文件,并在程序使用LoadBitmap时将位图资源加载到内存,然后呼叫AppendMenu或InsertMenu将位图附加到菜单上。但是用这种方法会有一些问题:位图不适于所有显示模式的分辨率和纵横比;有时您需要缩放加载的位图以解决此问题。另一种方法是:在程序内部建立位图,并将它选进内存设备内容,画出来,然后再附加到菜单中。
GRAFMENU中的GetBitmapFont函数的参数为0、1或2,传回一个位图句柄。此位图包含字符串「Courier New」、「Arial」或「Times New Roman」,而且字体是各自对应的字体,大小是正常系统字体的两倍。让我们看看GetBitmapFont是怎么做的。(下面的程序代码与GRAFMENU.C文件中的有些不同。为了清楚起见,我用「Arial」字体相应的值代替了引用szFaceName数组。)
第一步是用TEXTMETRIC结构来确定目前系统字体的大小,并建立一个与目前屏幕兼容的内存设备内容:
hdc = CreateIC (TEXT ("DISPLAY"), NULL, NULL, NULL) ;
GetTextMetrics (hdc, &tm) ;
hdcMem = CreateCompatibleDC (hdc) ;
CreateFont函数建立了一种逻辑字体,该字体高是系统字体的两倍,而且逻辑名称为「Arial」:
hFont = CreateFont (2 * tm.tmHeight, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
TEXT ("Arial")) ;
从内存设备内容中选择该字体,然后储存内定字体句柄:
hFont = (HFONT) SelectObject (hdcMem, hFont) ;
现在,当我们向内存设备内容写一些文字时,Windows就会使用选进设备内容的TrueType Arial字体了。
但这个内存设备内容最初只有一个单图素单色设备平面。我们必须建立一个足够大的位图以容纳我们所要显示的文字。通过GetTextExtentPoint32函数,可以取得文字的大小,而用CreateBitmap可以根据这些尺寸来建立位图:
GetTextExtentPoint32 (hdcMem, TEXT ("Arial"), 5, &size) ;
hBitmap = CreateBitmap (size.cx, size.cy, 1, 1, NULL) ;
SelectObject (hdcMem, hBitmap) ;
现在这个设备内容是一个单色的显示平面,大小也是严格的文字尺寸。我们现在要做的就是书写文字:
TextOut (hdcMem, 0, 0, TEXT ("Arial"), 5) ;
除了清除,所有的工作都完成了。要清除,我们可以用SelectObject将系统字体(带有句柄hFont)重新选进设备内容,然后删除SelectObject传回的前一个字体句柄,也就是Arial字体句柄:
DeleteObject (SelectObject (hdcMem, hFont)) ;
现在可以删除两个设备内容:
DeleteDC (hdcMem) ;
DeleteDC (hdc) ;
这样,我们就获得了一个位图,该位图上有Arial字体的字符串「Arial」。
当我们需要缩放字体以适应不同显示分辨率或纵横比时,内存设备内容也能解决问题。在GRAFMENU程序中,我建立了四个位图,这些位图只适用于系统字体高8图素、宽4图素的显示。对于其它尺寸的系统字体,只能缩放位图。GRAFMENU中的StretchBitmap函数完成此功能。
第一步是获得显示的设备内容,然后取得系统字体的文字规格,接下来建立两个内存设备内容:
hdc = CreateIC (TEXT ("DISPLAY"), NULL, NULL, NULL) ;
GetTextMetrics (hdc, &tm) ;
hdcMem1 = CreateCompatibleDC (hdc) ;
hdcMem2 = CreateCompatibleDC (hdc) ;
DeleteDC (hdc) ;
传递给函数的位图句柄是hBitmap1。程序能用GetObject获得位图的大小:
GetObject (hBitmap1, sizeof (BITMAP), (PSTR) &bm1) ;
此操作将尺寸复制到BITMAP型态的结构bm1中。结构bm2等于结构bm1,然后根据系统字体大小来修改某些字段:
bm2 = bm1 ;
bm2.bmWidth = (tm.tmAveCharWidth * bm2.bmWidth) / 4 ;
bm2.bmHeight = (tm.tmHeight * bm2.bmHeight) / 8 ;
bm2.bmWidthBytes = ((bm2.bmWidth + 15) / 16) * 2 ;
下一个位图带有句柄hBitmap2,可以根据动态的尺寸建立:
hBitmap2 = CreateBitmapIndirect (&bm2) ;
然后将这两个位图选进两个内存设备内容中:
SelectObject (hdcMem1, hBitmap1) ;
SelectObject (hdcMem2, hBitmap2) ;
我们想把第一个位图复制给第二个位图,并在此程序中进行拉伸。这包括StretchBlt呼叫:
StretchBlt (hdcMem2, 0, 0, bm2.bmWidth, bm2.bmHeight,
hdcMem1, 0, 0, bm1.bmWidth, bm1.bmHeight, SRCCOPY) ;
现在第二幅图适当地缩放了,我们可将其用到菜单中。剩下的清除工作很简单:
DeleteDC (hdcMem1) ;
DeleteDC (hdcMem2) ;
DeleteObject (hBitmap1) ;
在建造菜单时,GRAFMENU中的CreateMyMenu函数呼叫了StretchBitmap和GetBitmapFont函数。GRAFMENU在资源文件中定义了两个菜单,在选择「File」和「Edit」选项时会弹出这两个菜单。函数开始先取得一个空菜单的句柄:
hMenu = CreateMenu () ;
从资源文件加载「File」的弹出式菜单(包括四个选项:「New」、「Open」、「Save」和「Save as」):
hMenuPopup = LoadMenu (hInstance, TEXT ("MenuFile")) ;
从资源文件还加载了包含「FILE」的位图,并用StretchBitmap进行了拉伸:
hBitmapFile = StretchBitmap (LoadBitmap (hInstance, TEXT ("BitmapFile"))) ;
位图句柄和弹出式菜单句柄都是AppendMenu呼叫的参数:
AppendMenu (hMenu, MF_BITMAP | MF_POPUP, hMenuPopup, (PTSTR) (LONG)
hBitmapFile) ;
「Edit」菜单类似程序如下:
hMenuPopup = LoadMenu (hInstance, TEXT ("MenuEdit")) ;
hBitmapEdit = StretchBitmap (LoadBitmap (hInstance, TEXT ("BitmapEdit"))) ;
AppendMenu (hMenu, MF_BITMAP | MF_POPUP, hMenuPopup, (PTSTR)(LONG) hBitmapEdit) ;
呼叫GetBitmapFont函数可以构造这三种不同字体的弹出式菜单:
hMenuPopup = CreateMenu () ;
for (i = 0 ; i < 3 ; i++)
{
hBitmapPopFont [i] = GetBitmapFont (i) ;
AppendMenu (hMenuPopup, MF_BITMAP, IDM_FONT_COUR + i,
(PTSTR) (LONG) hMenuPopupFont [i]) ;
}
然后将弹出式菜单添加到菜单中:
hBitmapFont = StretchBitmap (LoadBitmap (hInstance, "BitmapFont")) ;
AppendMenu (hMenu, MF_BITMAP | MF_POPUP, hMenuPopup, (PTSTR) (LONG) hBitmapFont) ;
WndProc通过呼叫SetMenu,完成了窗口菜单的建立工作。
GRAFMENU还改变了AddHelpToSys函数中的系统菜单。此函数首先获得一个系统菜单句柄:
hMenu = GetSystemMenu (hwnd, FALSE) ;
这将载入「HELP」位图,并将其拉伸到适当尺寸:
hBitmapHelp = StretchBitmap (LoadBitmap (hInstance, TEXT ("BitmapHelp"))) ;
这将给系统菜单添加一条分隔线和拉伸的位图:
AppendMenu (hMenu, MF_SEPARATOR, 0, NULL) ;
AppendMenu (hMenu, MF_BITMAP, IDM_HELP, (PTSTR)(LONG) hBitmapHelp) ;
GRAFMENU在退出之前呼叫一个函数来清除并删除所有位图。
下面是在菜单中使用位图的一些注意事项。
在顶层菜单中,Windows调整菜单列的高度以适应最高的位图。其它位图(或字符串)是根据菜单列的顶端对齐的。如果在顶层菜单中使用了位图,那么从使用常数SM_CYMENU的GetSystemMetrics得到的菜单列大小将不再有效。
执行GRAFMENU期间可以看到:在弹出式菜单中,您可使用带有位图菜单项的勾选标记,但勾选标记是正常尺寸。如果不满意,您可以建立一个自订的勾选标记,并使用SetMenuItemBitmaps。
在菜单中使用非文字(或者使用非系统字体的文字)的另一种方法是「拥有者绘制」菜单。
菜单的键盘接口是另一个问题。当菜单含有文字时,Windows会自动添加键盘接口。要选择一个菜单项,可以使用Alt与字符串中的一个字母的组合键。而一旦在菜单中放置了位图,就删除了键盘接口。即使位图表达了一定的含义,但Windows并不知道。
目前我们可以使用WM_MENUCHAR消息。当您按下Alt和与菜单项不相符的一个字符键的组合键时,Windows将向您的窗口消息处理程序发送一个WM_MENUCHAR消息。GRAFMENU需要截取WM_MENUCHAR消息并检查wParam的值(即按键的ASCII码)。如果这个值对应一个菜单项,那么向Windows传回双字组:其中高字组为2,低字组是与该键相关的菜单项索引值。然后由Windows处理余下的事。
位图都是矩形,但不需要都显示成矩形。例如,假定您有一个矩形位图图像,但您却想将它显示成椭圆形。
首先,这听起来很简单。您只需将图像加载Visual C++ Developer Studio或者Windows的「画图」程序(或者更昂贵的应用程序),然后用白色的画笔将图像四周画上白色。这时将获得一幅椭圆形的图像,而椭圆的外面就成了白色。只有当背景色为白色时此位图才能正确显示,如果在其它背景色上显示,您就会发现椭圆形的图像和背景之间有一个白色的矩形。这种效果不好。
有一种非常通用的技术可解决此类问题。这种技术包括「屏蔽(mask)」位图和一些位映像操作。屏蔽是一种单色位图,它与您要显示的矩形位图图像尺寸相同。每个屏蔽的图素都对应位图图像的一个图素。屏蔽图素是1(白色),对应着位图图素显示;是0(黑色),则显示背景色。(或者屏蔽位图与此相反,这根据您使用的位映像操作而有一些相对应的变化。)
让我们看看BITMASK程序是如何实作这一技术的。如程序14-9所示。
程序14-9 BITMASK
BITMASK.C
/*---------------------------------------------------------------------------
BITMASK.C -- Bitmap Masking Demonstration
(c) Charles Petzold, 1998
----------------------------------------------------------------------------*/
#include <windows.h>
LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM) ;
int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance,
PSTR szCmdLine, int iCmdShow)
{
static TCHAR szAppName [] = TEXT ("BitMask") ;
HWND hwnd ;
MSG msg ;
WNDCLASS wndclass ;
wndclass.style = CS_HREDRAW | CS_VREDRAW ;
wndclass.lpfnWndProc = WndProc ;
wndclass.cbClsExtra = 0 ;
wndclass.cbWndExtra = 0 ;
wndclass.hInstance = hInstance ;
wndclass.hIcon = LoadIcon (NULL, IDI_APPLICATION) ;
wndclass.hCursor = LoadCursor (NULL, IDC_ARROW) ;
wndclass.hbrBackground = (HBRUSH) GetStockObject (LTGRAY_BRUSH) ;
wndclass.lpszMenuName = NULL ;
wndclass.lpszClassName = szAppName ;
if (!RegisterClass (&wndclass))
{
MessageBox (NULL, TEXT ("This program requires Windows NT!"),
szAppName, MB_ICONERROR) ;
return 0 ;
}
hwnd = CreateWindow (szAppName, TEXT ("Bitmap Masking Demo"),
WS_OVERLAPPEDWINDOW,
CW_USEDEFAULT, CW_USEDEFAULT,
CW_USEDEFAULT, CW_USEDEFAULT,
NULL, NULL, hInstance, NULL) ;
ShowWindow (hwnd, iCmdShow) ;
UpdateWindow (hwnd) ;
while (GetMessage (&msg, NULL, 0, 0))
{
TranslateMessage (&msg) ;
DispatchMessage (&msg) ;
}
return msg.wParam ;
}
LRESULT CALLBACK WndProc (HWND hwnd, UINT message, WPARAM wParam,LPARAM lParam)
{
static HBITMAP hBitmapImag, hBitmapMask ;
static HINSTANCE hInstance ;
static int cxClient, cyClient, cxBitmap, cyBitmap ;
BITMAP bitmap ;
HDC hdc, hdcMemImag, hdcMemMask ;
int x, y ;
PAINTSTRUCT ps ;
switch (message)
{
case WM_CREATE:
hInstance = ((LPCREATESTRUCT) lParam)->hInstance ;
// Load the original image and get its size
hBitmapImag = LoadBitmap (hInstance, TEXT ("Matthew")) ;
GetObject (hBitmapImag, sizeof (BITMAP), &bitmap) ;
cxBitmap = bitmap.bmWidth ;
cyBitmap = bitmap.bmHeight ;
// Select the original image into a memory DC
hdcMemImag = CreateCompatibleDC (NULL) ;
SelectObject (hdcMemImag, hBitmapImag) ;
// Create the monochrome mask bitmap and memory DC
hBitmapMask = CreateBitmap (cxBitmap, cyBitmap, 1, 1, NULL) ;
hdcMemMask = CreateCompatibleDC (NULL) ;
SelectObject (hdcMemMask, hBitmapMask) ;
// Color the mask bitmap black with a white ellipse
SelectObject (hdcMemMask, GetStockObject (BLACK_BRUSH)) ;
Rectangle (hdcMemMask, 0, 0, cxBitmap, cyBitmap) ;
SelectObject (hdcMemMask, GetStockObject (WHITE_BRUSH)) ;
Ellipse (hdcMemMask, 0, 0, cxBitmap, cyBitmap) ;
// Mask the original image
BitBlt (hdcMemImag, 0, 0, cxBitmap, cyBitmap,
hdcMemMask, 0, 0, SRCAND) ;
DeleteDC (hdcMemImag) ;
DeleteDC (hdcMemMask) ;
return 0 ;
case WM_SIZE:
cxClient = LOWORD (lParam) ;
cyClient = HIWORD (lParam) ;
return 0 ;
case WM_PAINT:
hdc = BeginPaint (hwnd, &ps) ;
// Select bitmaps into memory DCs
hdcMemImag = CreateCompatibleDC (hdc) ;
SelectObject (hdcMemImag, hBitmapImag) ;
hdcMemMask = CreateCompatibleDC (hdc) ;
SelectObject (hdcMemMask, hBitmapMask) ;
// Center image
x = (cxClient - cxBitmap) / 2 ;
y = (cyClient - cyBitmap) / 2 ;
// Do the bitblts
BitBlt (hdc, x, y, cxBitmap, cyBitmap, hdcMemMask, 0, 0, 0x220326) ;
BitBlt (hdc, x, y, cxBitmap, cyBitmap, hdcMemImag, 0, 0, SRCPAINT) ;
DeleteDC (hdcMemImag) ;
DeleteDC (hdcMemMask) ;
EndPaint (hwnd, &ps) ;
return 0 ;
case WM_DESTROY:
DeleteObject (hBitmapImag) ;
DeleteObject (hBitmapMask) ;
PostQuitMessage (0) ;
return 0 ;
}
return DefWindowProc (hwnd, message, wParam, lParam) ;
}
BITMASK.RC
// Microsoft Developer Studio generated resource script.
#include "resource.h"
#include "afxres.h"
/
// Bitmap
MATTHEW BITMAP DISCARDABLE "matthew.bmp"
资源文件中的MATTHEW.BMP文件是我侄子的一幅黑白数字照片,宽200图素,高320图素,每图素8位。不过,另外制作个BITMASK只是因为此文件的内容是任何东西都可以。
注意,BITMASK将窗口背景设为亮灰色。这样就确保我们能正确地屏蔽位图,而不只是将其涂成白色。
下面让我们看一下WM_CREATE的处理程序:BITMASK用LoadBitmap函数获得hBitmapImag变量中原始图像的句柄。用GetObject函数可取得位图的宽度高度。然后将位图句柄选进句柄为hdcMemImag的内存设备内容中。
程序建立的下一个单色位图与原来的图大小相同,其句柄储存在hBitmapMask,并选进句柄为hdcMemMask的内存设备内容中。在内存设备内容中,使用GDI函数,屏蔽位图就涂成了黑色背景和一个白色的椭圆:
SelectObject (hdcMemMask, GetStockObject (BLACK_BRUSH)) ;
Rectangle (hdcMemMask, 0, 0, cxBitmap, cyBitmap) ;
SelectObject (hdcMemMask, GetStockObject (WHITE_BRUSH)) ;
Ellipse (hdcMemMask, 0, 0, cxBitmap, cyBitmap) ;
因为这是一个单色的位图,所以黑色区域的位是0,而白色区域的位是1。
然后BitBlt呼叫就按此屏蔽修改了原图像:
BitBlt (hdcMemImag, 0, 0, cxBitmap, cyBitmap,
hdcMemMask, 0, 0, SRCAND) ;
SRCAND位映像操作在来源位(屏蔽位图)和目的位(原图像)之间执行了位AND操作。只要屏蔽位图是白色,就显示目的;只要屏蔽是黑色,则目的就也是黑色。现在原图像中就形成了一个黑色包围的椭圆区域。
现在让我们看一下WM_PAINT处理程序。此程序同时改变了选进内存设备内容中的图像位图和屏蔽位图。两次BitBlt呼叫完成了这个魔术,第一次在窗口上执行屏蔽位图的BitBlt:
BitBlt (hdc, x, y, cxBitmap, cyBitmap, hdcMemMask, 0, 0, 0x220326) ;
这里使用了一个没有名称的位映像操作。逻辑运算子是D & ~S。回忆来源-即屏蔽位图-是黑色(位值0)包围的一个白色(位值1)椭圆。位映像操作首先将来源反色,也就是改成白色包围的黑色椭圆。然后位操作在这个已转换的来源和目的(即窗口上)之间执行位AND操作。当目的和位值1「AND」时保持不变;与位值0「AND」时,目的将变黑。因此,BitBlt操作将在窗口上画一个黑色的椭圆。
第二次的BitBlt呼叫则在窗口中绘制图像位图:
BitBlt (hdc, x, y, cxBitmap, cyBitmap, hdcMemImag, 0, 0, SRCPAINT) ;
位映像操作在来源和目的之间执行位「OR」操作。由于来源位图的外面是黑色,因此保持目的不变;而在椭圆区域内,目的是黑色,因此图像就原封不动地复制了过来。执行结果如图14-9所示。
注意事项:
有时您需要一个很复杂的屏蔽-例如,抹去原始图像的整个背景。您将需要在画图程序中手工建立然后将其储存到成文件。
图14-9 BITMASK的屏幕显示 |
如果正在为Windows NT编写类似的应用程序,那么您可以使用与MASKBIT程序类似的MaskBlt函数,而只需要更少的函数呼叫。Windows NT还包括另一个类似BitBlt的函数,Windows 98不支持该函数。此函数是PlgBlt(「平行四边形位块移动:parallelogram blt」)。这个函数可以对图像进行旋转或者倾斜位图图像。
最后,如果在您的机器上执行BITMASK程序,您就只会看见黑色、白色和两个灰色的阴影,这是因为您执行的显示模式是16色或256色。对于16色模式,显示效果无法改进,但在256色模式下可以改变调色盘以显示灰阶。您将在第十六章学会如何设定调色盘。
简单的动画
小张的位图显示起来非常快,因此可以将位图和Windows定时器联合使用,来完成一些基本的动画。
现在开始这个弹球程序。
BOUNCE程序,如程序14-10所示,产生了一个在窗口显示区域弹来弹去的小球。该程序利用定时器来控制小球的行进速度。小球本身是一幅位图,程序首先通过建立位图来建立小球,将其选进内存设备内容,然后呼叫一些简单的GDI函数。程序用BitBlt从一个内存设备内容将这个位图小球画到显示器上。
程序14-10 BOUNCE
BOUNCE.C
/*---------------------------------------------------------------------------
BOUNCE.C -- Bouncing Ball Program
(c) Charles Petzold, 1998
----------------------------------------------------------------------------*/
#include <windows.h>
#define ID_TIMER 1
LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM) ;
int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance,
PSTR szCmdLine, int iCmdShow)
{
static TCHAR szAppName[] = TEXT ("Bounce") ;
HWND hwnd ;
MSG msg ;
WNDCLASS wndclass ;
wndclass.style = CS_HREDRAW | CS_VREDRAW ;
wndclass.lpfnWndProc = WndProc ;
wndclass.cbClsExtra = 0 ;
wndclass.cbWndExtra = 0 ;
wndclass.hInstance = hInstance ;
wndclass.hIcon = LoadIcon (NULL, IDI_APPLICATION) ;
wndclass.hCursor = LoadCursor (NULL, IDC_ARROW) ;
wndclass.hbrBackground = (HBRUSH) GetStockObject (WHITE_BRUSH) ;
wndclass.lpszMenuName = NULL ;
wndclass.lpszClassName = szAppName ;
if (!RegisterClass (&wndclass))
{
MessageBox ( NULL, TEXT ("This program requires Windows NT!"),
szAppName, MB_ICONERROR) ;
return 0 ;
}
hwnd = CreateWindow ( szAppName, TEXT ("Bouncing Ball"),
WS_OVERLAPPEDWINDOW,
CW_USEDEFAULT, CW_USEDEFAULT,
CW_USEDEFAULT, CW_USEDEFAULT,
NULL, NULL, hInstance, NULL) ;
ShowWindow (hwnd, iCmdShow) ;
UpdateWindow (hwnd) ;
while (GetMessage (&msg, NULL, 0, 0))
{
TranslateMessage (&msg) ;
DispatchMessage (&msg) ;
}
return msg.wParam ;
}
LRESULT CALLBACK WndProc (HWND hwnd, UINT iMsg, WPARAM wParam, LPARAM lParam)
{
static HBITMAP hBitmap ;
static int cxClient, cyClient, xCenter, yCenter, cxTotal, cyTotal,
cxRadius, cyRadius, cxMove, cyMove, xPixel, yPixel ;
HBRUSH hBrush ;
HDC hdc, hdcMem ;
int iScale ;
switch (iMsg)
{
case WM_CREATE:
hdc = GetDC (hwnd) ;
xPixel = GetDeviceCaps (hdc, ASPECTX) ;
yPixel = GetDeviceCaps (hdc, ASPECTY) ;
ReleaseDC (hwnd, hdc) ;
SetTimer (hwnd, ID_TIMER, 50, NULL) ;
return 0 ;
case WM_SIZE:
xCenter = (cxClient = LOWORD (lParam)) / 2 ;
yCenter = (cyClient = HIWORD (lParam)) / 2 ;
iScale = min (cxClient * xPixel, cyClient * yPixel) / 16 ;
cxRadius = iScale / xPixel ;
cyRadius = iScale / yPixel ;
cxMove = max (1, cxRadius / 2) ;
cyMove = max (1, cyRadius / 2) ;
cxTotal = 2 * (cxRadius + cxMove) ;
cyTotal = 2 * (cyRadius + cyMove) ;
if (hBitmap)
DeleteObject (hBitmap) ;
hdc = GetDC (hwnd) ;
hdcMem = CreateCompatibleDC (hdc) ;
hBitmap = CreateCompatibleBitmap (hdc, cxTotal, cyTotal) ;
ReleaseDC (hwnd, hdc) ;
SelectObject (hdcMem, hBitmap) ;
Rectangle (hdcMem, -1, -1, cxTotal + 1, cyTotal + 1) ;
hBrush = CreateHatchBrush (HS_DIAGCROSS, 0L) ;
SelectObject (hdcMem, hBrush) ;
SetBkColor (hdcMem, RGB (255, 0, 255)) ;
Ellipse (hdcMem, cxMove, cyMove, cxTotal - cxMove, cyTotal - cyMove) ;
DeleteDC (hdcMem) ;
DeleteObject (hBrush) ;
return 0 ;
case WM_TIMER:
if (!hBitmap)
break ;
hdc = GetDC (hwnd) ;
hdcMem = CreateCompatibleDC (hdc) ;
SelectObject (hdcMem, hBitmap) ;
BitBlt (hdc, xCenter - cxTotal / 2,
yCenter - cyTotal / 2, cxTotal, cyTotal,
hdcMem, 0, 0, SRCCOPY) ;
ReleaseDC (hwnd, hdc) ;
DeleteDC (hdcMem) ;
xCenter += cxMove ;
yCenter += cyMove ;
if ((xCenter + cxRadius >= cxClient) || (xCenter - cxRadius <= 0))
cxMove = -cxMove ;
if ((yCenter + cyRadius >= cyClient) || (yCenter - cyRadius <= 0))
cyMove = -cyMove ;
return 0 ;
case WM_DESTROY:
if (hBitmap)
DeleteObject (hBitmap) ;
KillTimer (hwnd, ID_TIMER) ;
PostQuitMessage (0) ;
return 0 ;
}
return DefWindowProc (hwnd, iMsg, wParam, lParam) ;
}
BOUNCE每次收到一个WM_SIZE消息时都重画小球。这就需要与视讯显示器兼容的内存设备内容:
hdcMem = CreateCompatibleDC (hdc) ;
小球的直径设为窗口显示区域高度或宽度中较短者的十六分之一。不过,程序构造的位图却比小球大:从位图中心到位图四个边的距离是小球半径的1.5倍:
hBitmap = CreateCompatibleBitmap (hdc, cxTotal, cyTotal) ;
将位图选进内存设备内容后,整个位图背景设成白色:
Rectangle (hdcMem, -1, -1, xTotal + 1, yTotal + 1) ;
那些不固定的坐标使矩形边框在位图之外着色。一个对角线开口的画刷选进内存设备内容,并将小球画在位图的中央:
Ellipse (hdcMem, xMove, yMove, xTotal - xMove, yTotal - yMove) ;
当小球移动时,小球边界的空白会有效地删除前一时刻的小球图像。在另一个位置重画小球只需在BitBlt呼叫中使用SRCCOPY的ROP代码:
BitBlt (hdc, xCenter - cxTotal / 2, yCenter - cyTotal / 2, cxTotal, cyTotal,
hdcMem, 0, 0, SRCCOPY) ;
BOUNCE程序只是展示了在显示器上移动图像的最简单的方法。在一般情况下,这种方法并不能令人满意。如果您对动画感兴趣,那么除了在来源和目的之间执行或操作以外,您还应该研究其它的ROP代码(例如SRCINVERT)。其它动画技术包括Windows调色盘(以及AnimatePalette函数)和CreateDIBSection函数。对于更高级的动画您只好放弃GDI而使用DirectX接口了。
窗口外的位图
SCRAMBLE程序,如程序14-11所示,编写非常粗糙,我本来不应该展示这个程序,但它示范了一些有趣的技术,而且在交换两个显示矩形内容的BitBlt操作的程序中,用内存设备内容作为临时储存空间。
程序14-11 SCRAMBLE
SCRAMBLE.C
/*---------------------------------------------------------------------------
SCRAMBLE.C -- Scramble (and Unscramble) Screen
(c) Charles Petzold, 1998
-----------------------------------------------------------------------------*/
#include <windows.h>
#define NUM 300
LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM) ;
int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance,
PSTR szCmdLine, int iCmdShow)
{
static int iKeep [NUM][4] ;
HDC hdcScr, hdcMem ;
int cx, cy ;
HBITMAP hBitmap ;
HWND hwnd ;
int i, j, x1, y1, x2, y2 ;
if (LockWindowUpdate (hwnd = GetDesktopWindow ()))
{
hdcScr = GetDCEx (hwnd, NULL, DCX_CACHE | DCX_LOCKWINDOWUPDATE) ;
hdcMem = CreateCompatibleDC (hdcScr) ;
cx = GetSystemMetrics (SM_CXSCREEN) / 10 ;
cy = GetSystemMetrics (SM_CYSCREEN) / 10 ;
hBitmap = CreateCompatibleBitmap (hdcScr, cx, cy) ;
SelectObject (hdcMem, hBitmap) ;
srand ((int) GetCurrentTime ()) ;
for (i = 0 ; i < 2 ; i++)
for (j = 0 ; j < NUM ; j++)
{
if (i == 0)
{
iKeep [j] [0] = x1 = cx * (rand () % 10) ;
iKeep [j] [1] = y1 = cy * (rand () % 10) ;
iKeep [j] [2] = x2 = cx * (rand () % 10) ;
iKeep [j] [3] = y2 = cy * (rand () % 10) ;
}
else
{
x1 = iKeep [NUM - 1 - j] [0] ;
y1 = iKeep [NUM - 1 - j] [1] ;
x2 = iKeep [NUM - 1 - j] [2] ;
y2 = iKeep [NUM - 1 - j] [3] ;
}
BitBlt (hdcMem, 0, 0, cx, cy, hdcScr, x1, y1, SRCCOPY) ;
BitBlt (hdcScr, x1, y1, cx, cy, hdcScr, x2, y2, SRCCOPY) ;
BitBlt (hdcScr, x2, y2, cx, cy, hdcMem, 0, 0, SRCCOPY) ;
Sleep (10) ;
}
DeleteDC (hdcMem) ;
ReleaseDC (hwnd, hdcScr) ;
DeleteObject (hBitmap) ;
LockWindowUpdate (NULL) ;
}
return FALSE ;
}
SCRAMBLE没有窗口消息处理程序。在WinMain中,它首先呼叫带有桌面窗口句柄的LockWindowUpdate。此函数暂时防止其它程序更新屏幕。然后SCRAMBLE通过呼叫带有参数DCX_LOCKWINDOWUPDATE的GetDCEx来获得整个屏幕的设备内容。这样就只有SCRAMBLE可以更新屏幕了。
然后SCRAMBLE确定全屏幕的尺寸,并将长宽分别除以10。程序用这个尺寸(名称是cx和cy)来建立一个位图,并将该位图选进内存设备内容。
使用C语言的rand函数,SCRAMBLE计算出四个随机值(两个坐标点)作为cx和cy的倍数。程序透过三次呼叫BitBlt函数来交换两个矩形块中显示的内容。第一次将从第一个坐标点开始的矩形复制到内存设备内容。第二次BitBlt将从第二坐标点开始的矩形复制到第一点开始的位置。第三次将内存设备内容中的矩形复制到第二个坐标点开始的区域。
此程序将有效地交换显示器上两个矩形中的内容。SCRAMBLE执行300次交换,这时的屏幕显示肯定是一团糟。但不用担心,因为SCRAMBLE记得是怎么把显示弄得这样一团糟的,接着在退出前它会按相反的次序恢复原来的桌面显示(锁定屏幕前的画面)!
您也可以用内存设备内容将一个位图复制给另一个位图。例如,假定您要建立一个位图,该位图只包含另一个位图左上角的图形。如果原来的图像句柄为hBitmap,那么您可以将其尺寸复制到一个BITMAP型态的结构中:
GetObject (hBitmap, sizeof (BITMAP), &bm) ;
然后建立一个未初始化的新位图,该位图的尺寸是原来图的1/4:
hBitmap2 = CreateBitmap ( bm.bmWidth / 2, bm.bmHeight / 2,
bm.bmPlanes, bm.bmBitsPixel, NULL) ;
现在建立两个内存设备内容,并将原来位图和新位图选分别进这两个内存设备内容:
hdcMem1 = CreateCompatibleDC (hdc) ;
hdcMem2 = CreateCompatibleDC (hdc) ;
SelectObject (hdcMem1, hBitmap) ;
SelectObject (hdcMem2, hBitmap2) ;
最后,将第一个位图的左上角复制给第二个:
BitBlt ( hdcMem2, 0, 0, bm.bmWidth / 2, bm.bmHeight / 2,
hdcMem1, 0, 0, SRCCOPY) ;
剩下的只是清除工作:
DeleteDC (hdcMem1) ;
DeleteDC (hdcMem2) ;
DeleteObject (hBitmap) ;
BLOWUP.C程序,如图14-21所示,也用窗口更新锁定来在程序窗口之外显示一个捕捉的矩形。此程序允许使用者用鼠标圈选屏幕上的矩形区域,然后BLOWUP将该区域的内容复制到位图。在WM_PAINT消息处理期间,位图复制到程序的显示区域,必要时将拉伸或压缩。(参见程序14-12。)
程序14-12 BLOWUP
BLOWUP.C
/*--------------------------------------------------------------------------
BLOWUP.C -- Video Magnifier Program
(c) Charles Petzold, 1998
----------------------------------------------------------------------------*/
#include <windows.h>
#include <stdlib.h> // for abs definition
#include "resource.h"
LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM) ;
int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance,
PSTR szCmdLine, int iCmdShow)
{
static TCHAR szAppName [] = TEXT ("Blowup") ;
HACCEL hAccel ;
HWND hwnd ;
MSG msg ;
WNDCLASS wndclass ;
wndclass.style = CS_HREDRAW | CS_VREDRAW ;
wndclass.lpfnWndProc = WndProc ;
wndclass.cbClsExtra = 0 ;
wndclass.cbWndExtra = 0 ;
wndclass.hInstance = hInstance ;
wndclass.hIcon = LoadIcon (NULL, IDI_APPLICATION) ;
wndclass.hCursor = LoadCursor (NULL, IDC_ARROW) ;
wndclass.hbrBackground = (HBRUSH) GetStockObject (WHITE_BRUSH) ;
wndclass.lpszMenuName = szAppName ;
wndclass.lpszClassName = szAppName ;
if (!RegisterClass (&wndclass))
{
MessageBox ( NULL, TEXT ("This program requires Windows NT!"),
szAppName, MB_ICONERROR) ;
return 0 ;
}
hwnd = CreateWindow ( szAppName, TEXT ("Blow-Up Mouse Demo"),
WS_OVERLAPPEDWINDOW,
CW_USEDEFAULT, CW_USEDEFAULT,
CW_USEDEFAULT, CW_USEDEFAULT,
NULL, NULL, hInstance, NULL) ;
ShowWindow (hwnd, iCmdShow) ;
UpdateWindow (hwnd) ;
hAccel = LoadAccelerators (hInstance, szAppName) ;
while (GetMessage (&msg, NULL, 0, 0))
{
if (!TranslateAccelerator (hwnd, hAccel, &msg))
{
TranslateMessage (&msg) ;
DispatchMessage (&msg) ;
}
}
return msg.wParam ;
}
void InvertBlock (HWND hwndScr, HWND hwnd, POINT ptBeg, POINT ptEnd)
{
HDC hdc ;
hdc = GetDCEx (hwndScr, NULL, DCX_CACHE | DCX_LOCKWINDOWUPDATE) ;
ClientToScreen (hwnd, &ptBeg) ;
ClientToScreen (hwnd, &ptEnd) ;
PatBlt (hdc, ptBeg.x, ptBeg.y, ptEnd.x - ptBeg.x, ptEnd.y - ptBeg.y,
DSTINVERT) ;
ReleaseDC (hwndScr, hdc) ;
}
HBITMAP CopyBitmap (HBITMAP hBitmapSrc)
{
BITMAP bitmap ;
HBITMAP hBitmapDst ;
HDC hdcSrc, hdcDst ;
GetObject (hBitmapSrc, sizeof (BITMAP), &bitmap) ;
hBitmapDst = CreateBitmapIndirect (&bitmap) ;
hdcSrc = CreateCompatibleDC (NULL) ;
hdcDst = CreateCompatibleDC (NULL) ;
SelectObject (hdcSrc, hBitmapSrc) ;
SelectObject (hdcDst, hBitmapDst) ;
BitBlt (hdcDst, 0, 0, bitmap.bmWidth, bitmap.bmHeight,
hdcSrc, 0, 0, SRCCOPY) ;
DeleteDC (hdcSrc) ;
DeleteDC (hdcDst) ;
return hBitmapDst ;
}
LRESULT CALLBACK WndProc ( HWND hwnd, UINT message, WPARAM wParam,LPARAM lParam)
{
static BOOL bCapturing, bBlocking ;
static HBITMAP hBitmap ;
static HWND hwndScr ;
static POINT ptBeg, ptEnd ;
BITMAP bm ;
HBITMAP hBitmapClip ;
HDC hdc, hdcMem ;
int iEnable ;
PAINTSTRUCT ps ;
RECT rect ;
switch (message)
{
case WM_LBUTTONDOWN:
if (!bCapturing)
{
if (LockWindowUpdate (hwndScr = GetDesktopWindow ()))
{
bCapturing = TRUE ;
SetCapture (hwnd) ;
SetCursor (LoadCursor (NULL, IDC_CROSS)) ;
}
else
MessageBeep (0) ;
}
return 0 ;
case WM_RBUTTONDOWN:
if (bCapturing)
{
bBlocking = TRUE ;
ptBeg.x = LOWORD (lParam) ;
ptBeg.y = HIWORD (lParam) ;
ptEnd = ptBeg ;
InvertBlock (hwndScr, hwnd, ptBeg, ptEnd) ;
}
return 0 ;
case WM_MOUSEMOVE:
if (bBlocking)
{
InvertBlock (hwndScr, hwnd, ptBeg, ptEnd) ;
ptEnd.x = LOWORD (lParam) ;
ptEnd.y = HIWORD (lParam) ;
InvertBlock (hwndScr, hwnd, ptBeg, ptEnd) ;
}
return 0 ;
case WM_LBUTTONUP:
case WM_RBUTTONUP:
if (bBlocking)
{
InvertBlock (hwndScr, hwnd, ptBeg, ptEnd) ;
ptEnd.x = LOWORD (lParam) ;
ptEnd.y = HIWORD (lParam) ;
if (hBitmap)
{
DeleteObject (hBitmap) ;
hBitmap = NULL ;
}
hdc = GetDC (hwnd) ;
hdcMem = CreateCompatibleDC (hdc) ;
hBitmap= CreateCompatibleBitmap (hdc,
abs (ptEnd.x - ptBeg.x),
abs (ptEnd.y - ptBeg.y)) ;
SelectObject (hdcMem, hBitmap) ;
StretchBlt (hdcMem, 0, 0, abs (ptEnd.x - ptBeg.x),
abs (ptEnd.y - ptBeg.y),
hdc, ptBeg.x, ptBeg.y, ptEnd.x - ptBeg.x,
ptEnd.y - ptBeg.y, SRCCOPY) ;
DeleteDC (hdcMem) ;
ReleaseDC (hwnd, hdc) ;
InvalidateRect (hwnd, NULL, TRUE) ;
}
if (bBlocking || bCapturing)
{
bBlocking = bCapturing = FALSE ;
SetCursor (LoadCursor (NULL, IDC_ARROW)) ;
ReleaseCapture () ;
LockWindowUpdate (NULL) ;
}
return 0 ;
case WM_INITMENUPOPUP:
iEnable = IsClipboardFormatAvailable (CF_BITMAP) ?
MF_ENABLED : MF_GRAYED ;
EnableMenuItem ((HMENU) wParam, IDM_EDIT_PASTE, iEnable) ;
iEnable = hBitmap ? MF_ENABLED : MF_GRAYED ;
EnableMenuItem ((HMENU) wParam, IDM_EDIT_CUT, iEnable) ;
EnableMenuItem ((HMENU) wParam, IDM_EDIT_COPY, iEnable) ;
EnableMenuItem ((HMENU) wParam, IDM_EDIT_DELETE, iEnable) ;
return 0 ;
case WM_COMMAND:
switch (LOWORD (wParam))
{
case IDM_EDIT_CUT:
case IDM_EDIT_COPY:
if (hBitmap)
{
hBitmapClip = CopyBitmap (hBitmap) ;
OpenClipboard (hwnd) ;
EmptyClipboard () ;
SetClipboardData (CF_BITMAP, hBitmapClip) ;
}
if (LOWORD (wParam) == IDM_EDIT_COPY)
return 0 ;
//fall through for IDM_EDIT_CUT
case IDM_EDIT_DELETE:
if (hBitmap)
{
DeleteObject (hBitmap) ;
hBitmap = NULL ;
}
InvalidateRect (hwnd, NULL, TRUE) ;
return 0 ;
case IDM_EDIT_PASTE:
if (hBitmap)
{
DeleteObject (hBitmap) ;
hBitmap = NULL ;
}
OpenClipboard (hwnd) ;
hBitmapClip = GetClipboardData (CF_BITMAP) ;
if (hBitmapClip)
hBitmap = CopyBitmap (hBitmapClip) ;
CloseClipboard () ;
InvalidateRect (hwnd, NULL, TRUE) ;
return 0 ;
}
break ;
case WM_PAINT:
hdc = BeginPaint (hwnd, &ps) ;
if (hBitmap)
{
GetClientRect (hwnd, &rect) ;
hdcMem = CreateCompatibleDC (hdc) ;
SelectObject (hdcMem, hBitmap) ;
GetObject (hBitmap, sizeof (BITMAP), (PSTR) &bm) ;
SetStretchBltMode (hdc, COLORONCOLOR) ;
StretchBlt (hdc, 0, 0, rect.right, rect.bottom,
hdcMem, 0, 0, bm.bmWidth, bm.bmHeight, SRCCOPY) ;
DeleteDC (hdcMem) ;
}
EndPaint (hwnd, &ps) ;
return 0 ;
case WM_DESTROY:
if (hBitmap)
DeleteObject (hBitmap) ;
PostQuitMessage (0) ;
return 0 ;
}
return DefWindowProc (hwnd, message, wParam, lParam) ;
}
BLOWUP.RC (摘录)
//Microsoft Developer Studio generated resource script.
#include "resource.h"
#include "afxres.h"
/
// Menu
BLOWUP MENU DISCARDABLE
BEGIN
POPUP "&Edit"
BEGIN
MENUITEM "Cu&t\tCtrl+X", IDM_EDIT_CUT
MENUITEM "&Copy\tCtrl+C", IDM_EDIT_COPY
MENUITEM "&Paste\tCtrl+V", IDM_EDIT_PASTE
MENUITEM "De&lete\tDelete", IDM_EDIT_DELETE
END
END
/
// Accelerator
BLOWUP ACCELERATORS DISCARDABLE
BEGIN
"C", IDM_EDIT_COPY, VIRTKEY, CONTROL, NOINVERT
"V", IDM_EDIT_PASTE, VIRTKEY, CONTROL, NOINVERT
VK_DELETE, IDM_EDIT_DELETE, VIRTKEY, NOINVERT
"X", IDM_EDIT_CUT, VIRTKEY, CONTROL, NOINVERT
END
RESOURCE.H (摘录)
// Microsoft Developer Studio generated include file.
// Used by Blowup.rc
#define IDM_EDIT_CUT 40001
#define IDM_EDIT_COPY 40002
#define IDM_EDIT_PASTE 40003
#define IDM_EDIT_DELETE 40004
图14-10 BLOWUP显示的一个范例 |
由于鼠标拦截的限制,所以开始使用BLOWUP时会有些困难,需要逐渐适应。下面是使用本程序的方法:
- 在BLOWUP显示区域按下鼠标左键不放,鼠标指针会变成「+」字型。
- 继续按住左键,将鼠标移到屏幕上的任何其它位置。鼠标光标的位置就是您要圈选的矩形区域的左上角。
- 继续按住左键,按下鼠标右键,然后拖动鼠标到您要圈选的矩形区域的右下角。释放鼠标左键和右键。(释放鼠标左、右键次序无关紧要。)
鼠标光标恢复成箭头状,这时您圈选的矩形区域已复制到了BLOWUP的显示区域,并作了适当的压缩或拉伸变化。
如果您从右上角到左下角选取的话,BLOWUP将显示矩形区域的镜像。如果从左下到右上角选取,BLOWUP将显示颠倒的图像。如果从右上角至左上角选取,程序将综合两种效果。
BLOWUP还包含将位图复制到剪贴簿,以及将剪贴簿中的位图复制到程序的处理功能。BLOWUP处理WM_INITMENUPOPUP消息来启用或禁用「Edit」菜单中的不同选项,并通过WM_COMMAND消息来处理这些菜单项。您应该对这些程序代码的结构比较熟悉,因为它们与第十二章中的复制和粘贴文字项目的处理方式在本质上是一样的。
不过,对于位图,剪贴簿对象不是整体句柄而是位图句柄。当您使用CF_BITMAP时, GetClipboardData函数传回一个HBITMAP对象,而且SetClipboardData函数接收一个HBITMAP对象。如果您想将位图传送给剪贴簿又想保留副本以供程序本身使用,那么您必须复制位图。同样,如果您从剪贴簿上粘贴了一幅位图,也应该做一个副本。BLOWUP中的CopyBitmap函数是通过取得现存位图的BITMAP结构,并在CreateBitmapIndirect函数中用这个结构建立一个新位图来完成此项操作的。(变量名的后缀Src和Dst分别代表「来源」和「目的」。) 两个位图都被选进内存设备内容,而且通过呼叫BitBlt来复制位图内容。(另一种复制位的方法,可以先按位图大小配置一块内存,然后为来源位图呼叫GetBitmapBits,为目的位图呼叫SetBitmapBits。)
我发现BLOWUP对于检查Windows及其应用程序中大量分散的小位图和图片非常有用。