深入GDI图像显示
摘 要:本文首先给出了一种结合了DIB和DDB两种位图优点的图像显示方法,其次对GDI函数的高级应用,如透明位图显示、图像旋转显示、图像镜像显示进行了研究。
关键词:GDI图像显示,特殊GDI函数的应用,透明位图显示,图像几何变换显示
图像信息是人类认识世界的重要知识来源,人类获得的70%以上的信息来自于眼睛摄取的丰富和真切的图像。图像与计算机相结合带给人们近乎神奇的图像艺术。对于程序开发者来说,实现高速的绘图是设计漂亮友好的用户界面的基础和关键所在。
在Win32图像程序设计中,图像显示的方法主要有:GDI,DirectDraw,OpenGL等技术,本文仅讨论利用GDI函数实现位图的显示。 Windows使用的位图有两种:设备无关位图DIB(Device IndependentBitmap)和设备相关位图DDB(Device Dependent Bitmap)。DDB位图一般用HBITMAP句柄或CBitmap对象保存,Windows提供的创建DDB位图的函数主要有 CreateCompatibleBitmap()、CreateBitmap()和CreateBitmapIndirect()。出于效率的关系,一 般创建彩色位图用CreateCompatibleBitmap()函数,因为它创建的位图格式与显示DC的格式一致,用SelectObject()选 入DC和显示的速度要快,另外两个函数多用来创建单色位图。也可以创建与显示DC不同格式的位图,但这样灰使得显示速度略为变慢。
由于DDB位图的设备相关性,因此它在显示速度方面有优势,但是同时也限制了DDB位图只能在相关DC上才能有效显示。而这时DIB位图的优势就体现出来 了,DIB位图具有良好的设备无关性,可以包含诸如调色板、分辨率等信息。可以使得应用程序独立于操作系统,因此得到了广泛的应用。在Win32中DIB 位图的显示技术有了较大的提高,与DDB位图相比,两者差别并不是很明显。但是可以对DDB位图操作的GDI函数远远多于DIB位图。另外获取和操作 DDB位图数据比较麻烦,而DIB位图可以采用直接分配内存来保存,因此在位图数据获取和操作上具有一定的优势。本文给出一种充分结合了两者优点的图像显 示技术,并且深入研究了诸如透明位图显示,图像旋转显示、镜像显示、梯度填充等GDI高级功能应用。
一、位图显示新方法
用于操作DIB图像的应用类有许多,笔者在"电脑编程与技巧"杂志99年第10期, 介绍了一个封装的通用图像基类(CImage),可完成DIB图像数据的管理和一些基本处理功能。其中大多的DIB类都采用直接分配内存的方式,然后用 DIB操作函数来实现图像的显示。这种方式在Win98中,DIB操作函数将DIB内存直接写入显示内存中,但是在NT中,操作系统首先将DIB拷贝到服 务器端创建一个DDB,然后再将DDB内存写入显存中,因此图像显示速度就会变慢。具体的原理有兴趣的读者可以参阅MSDN种的技术文献中关于GDI操作 的文章"Win32 动画原理"。
在Win98/NT中提供了一个新的函数CreateDIBSection(),可以在客户与服务器之间创建一个存储DIB位图的公共内存区给GDI。可 以在该内存上执行各种GDI操作(包括利用BitBlt()函数直接输出到显存);另外可以直接访问该内存。这样就可以提高DIB位图的显示速度。但是可 以看到极少数介绍这种方法的书籍上采用的方法是:先分配DIB内存读入位图,然后用获得位图信息再利用CreateDIBSection()函数分配内 存,将数据拷贝到其中,最后将先分配DIB内存删除。这种方法过程繁琐就不说了,如果遇到调入的位图有几十兆,那速度将慢的惊人。
本文采用的显示方法是:分配DIB内存就用CreateDIBSection()函数,然后用该函数返回的HBITMAP结构变量将位图连接到一个 CBitmap变量中。这样在图像显示的时候,建立一个与当前DC兼容的DC,然后将位图选入用BitBlt()或StretchBlt()函数来显示。 这种方法不仅显示图像的速度快,而且可以直接获得图像数据,对于图像处理应用程序,可以提高图像显示和处理的性能。该方法是基于通用图像基类 (CImage)来实现的,
关于该基类的介绍,可参见“一个通用图像基类”一文。具体的实现代码如下:
在类的定义中加入下面的变量
HBITMAP hBitmap; // 位图句柄
CBitmap m_Bitmap; // DDB位图变量
CDC *BMP_DC; // 兼容的DC
CBitmap *m_lpOldBmp; // 存放旧图像的指针
在类的构造函数中加入如下代码,建立兼容的DC
BMP_DC = new CDC;
BMP_DC->CreateCompatibleDC( NULL );
在分配内存的函数中加入下面的代码,位图信息头m_lpDibInfo要首先获得。
// 用CreateDIBSection()分配图像数据内存
hBitmap = CreateDIBSection( BMP_DC->m_hDC,// 兼容DC句柄
m_lpDibInfo,// 位图信息头
DIB_RGB_COLORS, // 色彩类型
(void **)&m_lpDibArray, // 数据内存指针
NULL, 0 );
// 如果内存分配成功,将它连接到一个CBitmap变量中
if( hBitmap != NULL )
{
m_Bitmap.Attach( hBitmap );
BMP_DC->SelectObject( m_Bitmap );
}
// 如果内存分配失败,用new来分配内存
else
{
// 分配图像数据内存
m_lpDibArray = new BYTE[ m_ImageSize ];
// 初始化图像数据内存
memset( m_lpDibArray, 0, m_ImageSize );
// 如果分配失败,报错
if( m_lpDibArray == NULL ) AfxThrowMemoryException();
}
其中CreateDIBSection()函数第三个参数指明色彩类型,一般有两种DIB_RGB_COLORS和DIB_PAL_COLORS。在 Win98操作系统下,对于压缩格式为BI_BITFIELDS类型的BMP位图(一般为16位或32位有色彩掩码位图)应用参数 DIB_PAL_COLORS,而对于压缩格式为BI_RGB类型的BMP位图用的参数应为DIB_RGB_COLORS。但是在NT或Win2000操 作系统下,该参数只能为DIB_RGB_COLORS,否者在为16位或32位有色彩掩码位图分配内存时会出错。由于笔者开发程序基于Win2000操作 系统,因此该参数就直接用DIB_RGB_COLORS。
获取图像的数据可用指针m_lpDibArray,显示图像可用BitBlt()或StretchBlt()函数来完成。
下面给出的显示函数,可以实现图像放大和大型位图的显示,在视类的OnDraw()函数中调用,参数仅为当前DC指针和客户区的大小,其余计算滚动位置和源图范围的工作均由该函数完成。
BOOL CImage::BestBlt(CDC *pMpDc, CRect ClientRect, BOOL IsFull)
{
// 将设备坐标转换位逻辑坐标
pMpDc->DPtoLP( &ClientRect );
// 设置Blt的方式
pMpDc->SetStretchBltMode( COLORONCOLOR );
// 计算对应客户区的图像区大小
int ClientW = ClientRect.Width();
int ClientH = ClientRect.Height();
CPoint BMPLUP, BMPRBP;
if( IsFull )
{
BMPLUP.x = BMPLUP.y = 0;
BMPRBP.x = m_ImageWidth;
BMPRBP.y = m_ImageHeight;
}
else
{
BMPLUP.x = (int) (ClientRect.left / m_fScale + 0.5);
BMPLUP.y = (int) (ClientRect.top / m_fScale + 0.5);
BMPRBP.x = (int) (ClientRect.right / m_fScale + 0.5);
BMPRBP.y = (int) (ClientRect.bottom / m_fScale + 0.5);
if( BMPRBP.x > m_ImageWidth )
{
BMPRBP.x = m_ImageWidth;
}
if( BMPRBP.y > m_ImageHeight )
{
BMPRBP.y = m_ImageHeight;
}
}
// 显示图像
return pMpDc->StretchBlt(ClientRect.left,
ClientRect.top,
ClientRect.Width(),
ClientRect.Height(),
BMP_DC,
BMPLUP.x,
BMPLUP.y,
BMPRBP.x - BMPLUP.x,
BMPRBP.y - BMPLUP.y,
SRCCOPY);
}
上面的位图显示方法不仅可以直接获得图像数据,而且图像又可以作为一个CBitmap变量,CBitmap变量作为GDI对象可以灵活地被各种GDI函数调用,因此次方法结合了DIB和DDB两种位图优点,实现了位图灵活、快速的显示。
二、透明位图的显示方法
在许多动画程序中,精灵(sprite)的出现给程序增色许多。精灵的显示就涉及到透明位图的显示问题,其实精灵实际上视一幅矩形位图,只是背景是固定的颜色,类似于电影拍摄中的蓝幕技术。下面就给出几种能实现透明位图的显示方法。
1.直接修改位图数据
许多程序是用编制的DIB图像类来实现图像的显示,如果直接改变图像内存的数据,如果源图像上点的颜色等于背景色,就不修改目标图像,否则用源图像点替代目标图像点。这样就可以实现精灵图像的显示。
这种方法有两个缺点:一是要求精灵图像较小,二是对于源图像和目标图像格式不同的情况处理起来比较困难,而且要求程序员对程序进行必要的优化以加快图像更新速度,因此这种方法一般不被采用。
2.用TransparentBlt()函数实现
对于Win98或NT5.0操作系统,提供了一个函数TransparentBlt(),可以实现透明位图的显示。该函数类似于StretchBlt()函数,支持缩放操作,但不支持镜像操作。具体用法如下:
TransparentBlt( pDC->m_hDC, // 目标设备环境句柄
10, // 目标矩形区域的左上角x坐标
70, // 目标矩形区域的左上角y坐标
w, // 目标矩形区域宽度
h, // 目标矩形区域高度
BackDC.m_hDC, // 源图像的设备环境句柄
0, // 源图像的左上角x坐标
0, // 源图像的左上角y坐标
w, // 源图像的宽度
h, // 源图像的高度
BackColor );// 透明区域的颜色值
注意要使用该函数必须要将MSIMG32.LIB库连接到工程中,可以通过在相应CPP文件中加入下面的代码来将该库连接进工程:
#pragma comment( lib, "MSIMG32.LIB")
3.用MaskBlt()函数实现
MaskBlt()函数通过一个单色模板位图和相应的光栅操作码来实现透明位图得显示。对于模板图像上颜色值为“1”的点,采用前景光栅码来操作,而对于 颜色值为“0”的点,采用背景光栅码来操作。如果前景光栅操作码光栅操作码为0x00AA0029(显示目标图像点,具体含义可参见MSDN),只要制作 好单色模板位图就可以用MaskBlt()函数来实现透明位图的显示。实现的代码如下:
// 设置位图兼容DC的背景颜色为透明色
BackDC.SetBkColor( BackColor );
// 生成模板位图
MaskDC.BitBlt( 0, 0, w, h, &BackDC, 0, 0, NOTSRCCOPY );
// 显示模板位图
pDC->BitBlt(70, 10, 48, 48, &MaskDC, 0, 0, SRCCOPY);
// 生成MaskBlt透明显示所需的光栅操作码
DWORD dwFore = SRCCOPY; // 前景光栅码
DWORD dwBack = 0x00AA0029; // 背景光栅码
DWORD dwRop4 = MAKEROP4( dwFore, dwBack );
// 显示透明位图
pDC->MaskBlt(10, 70, w, h, &BackDC, 0, 0, MaskBmp, 0, 0, dwRop4);
生成单色模板位图的方法比较简单,首先将兼容DC的背景色设为透明色,然后用BitBlt()函数将透明图像写入模板图像兼容DC中,就可以得到所需的单 色模板位图。注意选用不同的光栅操作码得到的结果是不同的。如果选用SRCCOPY如果源图像的颜色与目标的背景色相同,则相应单色图的输出为白色1,否 则为黑色0,这里要选用NOTSRCCOPY光栅操作码才能得到所需的模板。
4.用CImageList类实现
CImageList类是用来管理相同大小的图标和位图的类,可以实现图标和位图的透明显示,因此可以用它来实现位图的透明显示,方法如下:
CImageList m_ImageList; // 定义CImageList类的对象
// 将位图装入,设置透明色
m_ImageList.Create( IDB_BITMAP_TRANS, 48, 1, BackColor );
// 显示透明位图
m_ImageList.Draw( pDC, 0, CPoint(10, 70), ILD_TRANSPARENT );
5.用光栅操作码来实现
用于图像显示的三元光栅码有两百多个,三元光栅码指明了源图像、画刷和目标图像的颜色的组合操作方式,如果巧妙地利用光栅操作码可以组合出许多的显示透明位图的方法,这里给出一种。
// 生成模板位图
MaskDC.BitBlt( 0, 0, w, h, &BackDC, 0, 0, SRCCOPY );
// 显示模板位图
pDC->BitBlt(70, 10, 48, 48, &MaskDC, 0, 0, SRCCOPY);
// 将源图与目标图像进行"异或(xor)"运算
// d xor s = d|s
pDC->BitBlt(10, 70, 48, 48, &BackDC, 0, 0, SRCINVERT);
// 将模板与目标图像进行 "and" 运算
// 透明的部分保持不变,不透明的部分为 0
pDC->BitBlt(10, 70, 48, 48, &MaskDC, 0, 0, SRCAND);
// 将源图与目标图像进行"异或(xor)"运算
// 0 xor G = G, 因此不透明的地方被源图覆盖
// d|s xor s = d, 因此透明的地方恢复
pDC->BitBlt(10, 70, 48, 48, &BackDC, 0, 0, SRCINVERT);
6.用PlgBlt()函数实现
PlgBlt()函数可以将源DC中的图像传送到目标DC上的一个平行四边形区域中,而且可以根据传入的单色模板位图来实现透明位图的显示。该函数的第一 个参数是三个点结构的指针,第四个点的计算由函数内部实现,该点的位置如下图示意。只要计算好平行四边形区域,也可以实现位图的旋转显示,本文将在下面讨 论。
实现代码如下:
┏━━━━━━━━━┓
┃ Pic01.jpg 文件 ┃
┗━━━━━━━━━┛
图1 平行四边形构成示意图
// 生成模板位图
MaskDC.BitBlt(0, 0, w, h, &BackDC, 0, 0, NOTSRCCOPY);
// 显示模板位图
pDC->BitBlt(70, 10, 48, 48, &MaskDC, 0, 0, SRCCOPY);
// 计算显示的位置
CPoint Pt[3];
Pt[0].x = 10;
Pt[0].y = 70;
Pt[1].x = Pt[0].x + w;
Pt[1].y = Pt[0].y;
Pt[2].x = Pt[0].x;
Pt[2].y = Pt[0].y + h;
// 显示透明位图
pDC->PlgBlt(Pt, &BackDC, 0, 0, w, h, MaskBmp, 0, 0);
三、图像的几何变换显示
1. 用StretchBlt()函数实现图像镜像显示
StretchBlt()函数支持图像的镜像显示,如果将目标区域的高度或宽度取为负值,就可以实现图像的镜像显示。
// 显示正常图像
pDC->StretchBlt(100, 100, 48, 48, &BackDC, 0, 0, 48, 48, SRCCOPY);
// 显示水平对称图像
pDC->StretchBlt(100, 100, -48, 48, &BackDC, 0, 0, 48, 48, SRCCOPY);
// 显示垂直对称图像
pDC->StretchBlt(100, 100, 48, -48, &BackDC, 0, 0, 48, 48, SRCCOPY);
// 显示中心对称图像
pDC->StretchBlt(100, 100, -48, -48, &BackDC, 0, 0, 48, 48, SRCCOPY);
2.用SetWorldTransform()函数实现几何变换显示 SetWorldTransform()函数设置世界坐标与目标DC坐标之间的二维坐标变换,可以实现图像的旋转、镜像、缩放、平移、剪切以及上述各种变 换的组合变换。该函数的第二个参数为XFORM结构变量,具体定义读者可参见MSDN,它的变换方程如下:
x' = x * eM11 + y * eM21 + eDx,
y' = x * eM12 + y * eM22 + eDy,
可以用CombineTransform()函数将两个变换组合为一个变换。下面的例子实现了位图的旋转显示,要实现其它的变换,只需给XFORM结构变 量赋予不同的值就可以实现。另外注意一点:只有首先用SetGraphicsMode()函数将DC的属性设为GM_ADVANCED类 型,SetWorldTransform()函数才有效,并且注意文字显示坐标也要随之变化。
// 计算旋转的参数
double Angle = 40.0/ 180* 3.1415926;
float cosAngle = (float)cos( Angle );
float sinAngle = (float)sin( Angle );
XFORM xform;
xform.eM11 = cosAngle;
xform.eM12 =-sinAngle;
xform.eM21 = sinAngle;
xform.eM22 = cosAngle;
xform.eDx = 0;
xform.eDy = 0;
// 设置DC的属性,使得SetWorldTransform()执行有效
SetGraphicsMode( pDC->m_hDC, GM_ADVANCED );
// 设置坐标转换方式
SetWorldTransform( pDC->m_hDC, &xform );
// 显示位图
pDC->StretchBlt(10, 100, 90, 90, &BackDC, 0, 0, 48, 48, SRCCOPY);
3.用PlgBlt()函数实现图像旋转、剪切显示
在介绍图像透明已经介绍了PlgBlt()函数的用法,只要按所需计算好显示区域的A、B、C点的坐标就可以实现图像旋转、剪切显示。这里要注意如果要显示全部图像可以将模板位图像素值用下面的方法设为“1”。
MaskDC.BitBlt(0, 0, w, h, &BackDC, 0, 0, WHITENESS);
4.直接修改位图数据
在介绍图像透明已经介绍了这种方法,只要设计好算法,这种方法可以实现更多的变换。比如类似于Photoshop中的各种变换滤镜,限于篇幅,本文对这种方法就不进行讨论了。
四、图像的梯度填充效果实现
编过OpengGL程序的读者一定知道,在对场景内的物体进行光滑明暗处理时,如果多边形的各个顶点颜色不同,则多边形内部的点就会用Gouraud方法 来进行平滑,融合处理。以往用GDI函数来实现采用的方法一般为:将区域分为许多小份,用渐变颜色的画刷进行填充。其实可以用 GradientFill()函数实现三角形片、三角形扇和矩形区域的梯度填充,如果与SelectClipRgn()函数结合就可以实现特殊区域的梯度 填充显示。下面的例子实现了三角形片和矩形梯度填充。
// 定义三角形的边长
int w = 100;
// 定义三角形片的顶点结构变量
TRIVERTEX vert[4];
// 给顶点赋坐标和颜色值
vert[0].x = 10;
vert[0].y = 10;
vert[0].Red = 0x0000;
vert[0].Green = 0x0000;
vert[0].Blue = 0x0000;
vert[0].Alpha = 0x0000;
vert[1].x = vert[0].x + w;
vert[1].y = vert[0].y;
vert[1].Red = 0x0000;
vert[1].Green = 0x0000;
vert[1].Blue = 0xff00;
vert[1].Alpha = 0x0000;
vert[2].x = vert[0].x + w;
vert[2].y = vert[0].y + w;
vert[2].Red = 0x0000;
vert[2].Green = 0x0000;
vert[2].Blue = 0xff00;
vert[2].Alpha = 0x0000;
vert[3].x = vert[0].x;
vert[3].y = vert[0].y + w;
vert[3].Red = 0xff00;
vert[3].Green = 0xff00;
vert[3].Blue = 0xff00;
vert[3].Alpha = 0x0000;
// 指定三角形片的顺序
GRADIENT_TRIANGLE gTRi[2];
gTRi[0].Vertex1 = 0;
gTRi[0].Vertex2 = 1;
gTRi[0].Vertex3 = 2;
gTRi[1].Vertex1 = 0;
gTRi[1].Vertex2 = 2;
gTRi[1].Vertex3 = 3;
// 填充三角形片
GradientFill( hdc, vert, 4, &gTRi, 2, GRADIENT_FILL_TRIANGLE );
// 定义矩形区域的左上和右下顶点结构变量
TRIVERTEX Rectvert[2];
// 给顶点赋坐标和颜色值
Rectvert[0].x = vert[0].x + w + 10;
Rectvert[0].y = 10;
Rectvert[0].Red = 0x0000;
Rectvert[0].Green = 0x0000;
Rectvert[0].Blue = 0x0000;
Rectvert[0].Alpha = 0x0000;
Rectvert[1].x = Rectvert[0].x + w;
Rectvert[1].y = Rectvert[0].y + w;
Rectvert[1].Red = 0xFF00;
Rectvert[1].Green = 0x0000;
Rectvert[1].Blue = 0x0000;
Rectvert[1].Alpha = 0x0000;
// 指定矩形区域的左上和右下顶点顺序
GRADIENT_RECT gRect;
gRect.UpperLeft = 0;
gRect.LowerRight = 1;
// 水平方向梯度填充矩形
GradientFill( hdc, Rectvert, 2, &gRect, 1, GRADIENT_FILL_RECT_H );
Rectvert[0].x += 10 + w;
Rectvert[1].x += 10 + w;
// 垂直方向梯度填充矩形
GradientFill( hdc, Rectvert, 2, &gRect, 1, GRADIENT_FILL_RECT_V );
五、获取任何DC中图像
从DC中获取图像有许多的用处,比如将DDB图像转化位DIB图像保存,实现屏幕图像抓取等。
为了扩大CImage类的功能,添加了一个GetIMGFromDC()函数,可以将传入的DC中指定区域的图像保存到CImage类中。具体的代码如下:
BOOL CImage::GetIMGFromDC( CDC *pDC, // 传入的DC指针
int ox, // 区域的左上点x坐标
int oy, // 区域的左上点y坐标
int cw, // 区域的宽度
int ch) // 区域的高度
{
CBitmap bitmap; // 定义位图
CDC memDC; // 定义兼容DC
// 创建定义兼容DC
memDC.CreateCompatibleDC(pDC);
// 创建DDB兼容位图
bitmap.CreateCompatibleBitmap(pDC, m_ImageWidth, m_ImageHeight);
// 将兼容位图选入兼容DC
CBitmap* pOldBitmap = memDC.SelectObject( &bitmap );
// 设置Blt模式
memDC.SetStretchBltMode( COLORONCOLOR );
// 将当前DC的图像写入兼容DC中
memDC.StretchBlt( 0, 0, m_ImageWidth, m_ImageHeight, pDC, ox, oy, cw, ch, SRCCOPY );
// 获得色彩类型
if( m_lpDibInfo->bmiHeader.biCompression == BI_RGB )
m_uBltUsage = DIB_RGB_COLORS;
else if( m_lpDibInfo->bmiHeader.biCompression == BI_BITFIELDS )
m_uBltUsage = DIB_PAL_COLORS;
// 从位图中获得图像数据
GetDIBits ( memDC.m_hDC, HBITMAP( bitmap ),
0, m_ImageHeight, m_lpDibArray,
m_lpDibInfo, m_uBltUsage );
// 恢复兼容DC
memDC.SelectObject( pOldBitmap );
// 删除兼容DC
memDC.DeleteDC();
// 删除位图
bitmap.DeleteObject();
return true;
}
如果要将获取当前屏幕的图像,使用的方法为:
// 获得屏幕的尺寸
int ScreenX = ::GetSystemMetrics(SM_CXSCREEN);
int ScreenY = ::GetSystemMetrics(SM_CYSCREEN);
// 定义CImage对象
CImage lhw(ScreenX/ 2, ScreenY/ 2, 16, 0);
// 定义一个当前屏幕的DC
CClientDC dc( NULL );
// 抓取屏幕图像
lhw.GetIMGFromDC(&dc, 0, 0, ScreenX, ScreenY);
创建的CImage图像对象的尺寸为屏幕尺寸的一半,该函数内部实现图像的缩放。
以上就是笔者在对GDI函数在图像显示应用深入研究后的一点体会,希望对广大读者有所帮助。