如何创建位图(VC版)
这篇文章介绍了一种创建位图的方法以及如何在位图中绘图并保存成为位图文件。位图本质上是视频图像的内存表示,它做为windows的一种资源可以用于很多场合,被几乎所有的绘图软件所支持。象图标、墙纸、图形、图像都可以以位图的形式来存储。在这里我们讨论的位图是属于DIB(device-independent-bitmap)文件格式,我们称之为与设备环境(在这里可以称为显示设备)无关的位图。位图文件可以分为三部分来组成(有些人把它分为四部分):
一、 文件头(BITMAPFILEHEADER):
我们看文件头的数据结构:
typedef struct tagBITMAPFILEHEADER {
WORD bfType;// 用来指定文件类型,其值必须为BM即:0x424d。
DWORD bfSize;//以字节为单位指定位图文件的大小。
WORD bfReserved1;//保留,但其值必须为0。
WORD bfReserved2; //保留,但其值必须为0。
DWORD bfOffBits;///以字节为单位的从文件头到位图数据的偏移量
} BITMAPFILEHEADER;
二、 位图信息(BITMAPINFO):
我们看位图信息的数据结构:
typedef struct tagBITMAPINFO {
BITMAPINFOHEADER bmiHeader;/*信息头结构,包含位图的尺寸和颜色格式。对于不同系统版本,我们有不同的信息头结构版本:BITMAPINFOHEADER用于NT3.51及老的版本, BITMAPV4HEADER用于NT4.0和WIN95版本,BITMAPV5HEADER用于NT5.0和WIN98版本。*/
RGBQUAD bmiColors[1];///调色板结构,用来存放颜色的。常以数组(也常常称///为调色表)形式出现。
} BITMAPINFO
我们看信息头结构BITMAPINFOHEADER:
typedef struct tagBITMAPINFOHEADER{
DWORD biSize; ///此结构所需要的字节数。
LONG biWidth; ///以像素为单位的位图的宽度。
LONG biHeight; ///以像素为单位的位图的高度。见详细说明一。
WORD biPlanes; ///指定目标设备的平面数,目前这个值必须设置为1。
WORD biBitCount ///指定每个像素点所需要的位的个数。见详细说明二。
DWORD biCompression; ///指定压缩的类型。见详细说明三。
DWORD biSizeImage; ///以字节位单位的图像大小。见详细说明四。
LONG biXPelsPerMeter;///以像素/米为单位来说明水平分辨率。
LONG biYPelsPerMeter; ///以像素/米为单位来说明垂直分辨率。
DWORD biClrUsed; ///规定了位图中使用调色表索引的数目。一般此值为0,///表示可使用所有的颜色索引。
DWORD biClrImportant; ///指定调色表的一个索引,这个索引所代表的颜色///将做为显示位图需要的很重要的颜色。如果0代表所///有索引。
} BITMAPINFOHEADER;
详细说明一:
如果这个高度值是正数(>0),这个位图的原点坐标位于左下角(意味着位图的方向是倒的),如果是负数,这个位图的原点坐标位于左上角(意味着位图的方向是正的)。
详细说明二:
这个值指定了在位图中一个像素点用多少个位(bit)来表示。位数必须是下面值的一种:
1、 0。意味着jpeg格式。只能够在win98,NT5.0或更高的版本上使用。
2、 1。每个像素点需要一个位。两种颜色,缺省的是黑色和白色。在这种情况下,位图数据中的每一个位表示一个像素。如果这个位是0,那么这个像素就用调色板表的第一个索引所对应的颜色值来表示;如果这个位是1,那么这个像素就用调色表中的第二个索引所对应的颜色值来表示。
3、 4。每个像素点需要四个位。最多有十六种颜色。每一个像素可以用在调色表中从0到15的索引所对应的颜色值来表示。举例来讲,在位图数据种的第一个字节如果是0x1F,那么这两个像素的第一个像素所包含的颜色在调色表中由索引值为1的索引所代表的颜色组成;而第二个像素所包含的颜色在调色表中由索引值为15的索引所代表的颜色组成。
4、8。每个像素点需要八个位。最多有256种颜色。每一个像素可以用在调色表中从0到255的索引所对应的颜色值来表示。因为8个位即为一个字节,所以每个像素点由单个字节组成。举例而言,在位图数据中如果有某一个数据为OxFF,那么这个像素点所包含的颜色在调色表中由索引值为255的索引所代表的颜色组成。
8、16。用两个字节(16bit)来表示一个像素点。最多有65536(2^16)种颜色。如果biCompression的值是BI_RGB,那么BITMAPINFO 的bmiColors成员须为NULL值。红、绿、蓝三基色都用5个位(bit)来表示,这三基色的位的顺序为:0(不用)00000(红)00000(绿)00000(蓝),即由小到大分别为蓝、绿、红。
9、24。用24个位来表示一个像素点。 最多有16777216(2^24)种颜色。BITMAPINFO 的bmiColors成员必须为NULL值。在位图数据中,每三个字节一组用来表示一个像素点。
10、32。用32个位表示一个像素点。最多有4294967296(2^32)种颜色。如果biCompression的值是BI_RGB,那么BITMAPINFO 的bmiColors成员须为NULL值。
详细说明三:
压缩仅仅对于倒向(左下角坐标的位图)的位图有效。可以有下面的选择:
1、 BI_RGB:不压缩的格式。
2、 BI_RLE8:8位每像素点的RLE格式。
3、 BI_RLE4:4位每像素点的RLE格式。
4、 BI_BITFIELDS:位图不压缩,对于16位和32位位图有效。
5、 BI_JPEG:对于win98、NT5.0或较高的版本有效。指示这个图像是一个JPEG的文件格式。
详细说明四:
如果是BI_RGB格式,其值可以设为零。如果biCompressionshi是JBI_JPEG,则:其值指的是jpeg图像缓冲的大小。
我们看调色板结构RGBQUAD:
typedef struct tagRGBQUAD {
BYTE rgbBlue; ///指定蓝色的深度
BYTE rgbGreen; ///指定绿色的深度
BYTE rgbRed; ///指定红色的深度
BYTE rgbReserved; ///保留,暂时不用
} RGBQUAD;
第三部分(DATA CONTENT):
这部分存放位图的图像数据。
仅仅创建一个位图,大部分情况不是我们编程的目的,在位图中绘出我们所需要的东西,才是我们真正需要的。WINDOWS提供了专门的绘图对象如画刷(填充颜色)、画笔(画点、线)及其它的GDI对象如:字体、矩形等等。有了这些对象我们就可以在位图中画出自己想要的图,并保存为位图文件。生成一个位图文件,大体上可以分为以下几个步骤:
一、 根据自己的需要初始化位图信息结构BITMAPINFO,同时建立设备环境(显示设备环境)和与此设备环境逻辑兼容的内存设备环境。内存设备环境对我们很重要,在本文中几乎所有的绘图操作都是针对它而言的。还好windows提供了CreateCompatibleDC这个函数,用它可以建立内存设备环境对象。
二、 建立自己的位图对象或位图句柄(可以利用CreateDIBSection函数),并将其选进(或者说关联到)内存设备环境上,以后我们就可以通过内存设备环境来对位图操作了。
三、 建立自己需要的绘图工具,如画刷、画笔并选择它们的颜色。以及其它所需要的GDI对象如字体、矩形等等。
四、 通过内存设备环境选择自己将要用的绘图工具,要选择绘图工具(对象)SelectObject这个函数(或者说成员)是我们最佳的选择。
五、 利用绘图工具开始绘图,如画线,画点、画矩形、画圆等等,以及填充某些区域。在这个过程中我们可以根据需要更换自己不同颜色的画笔和画刷。这好像一个画家当他画太阳时用红色的画笔,然后用红色的画刷去填充,画蓝天时就用蓝色的画笔,然后用蓝色的画刷去填充。
六、 当所有的绘图操作完成以后,开始对位图进行保存,因此我们要建立和初始化位图文件头结构BITMAPFILEHEADER,为建立一个位图文件做准备。当然需要保存的文件格式必须严格按照位图(bmp)的文件格式,即:文件头、位图信息和图像数据。对于图像数据我们可以通过位图数据的入口指针(由函数CreateDIBSection的第四个参数*ppvBits
确定)来获得。
下面给出一个完整的实例函数(仅供参考),在VC5.0上编译通过,其目的用来绘出某一股票的指数K线图(在内存设备环境上),并将其保存为位图文件(可以在本人个人信息中看到生成的图片)。
void CImage::MakeIndex(TIndex *pIndex,TDate &pDate,const TCHAR *pchTitle,int iDays,int iRectWidth)
{
//* Create a file about bmp and Create BITMAPINFOHEADER First.
LPBITMAPINFO lpbmih = new BITMAPINFO;
lpbmih->bmiHeader.biSize = sizeof(BITMAPINFOHEADER);
lpbmih->bmiHeader.biWidth = m_iWidth;
lpbmih->bmiHeader.biHeight = m_iHeight;
lpbmih->bmiHeader.biPlanes = 1;
lpbmih->bmiHeader.biBitCount = g_iPixel;
lpbmih->bmiHeader.biCompression = BI_RGB;
lpbmih->bmiHeader.biSizeImage = 0;
lpbmih->bmiHeader.biXPelsPerMeter = 0;
lpbmih->bmiHeader.biYPelsPerMeter = 0;
lpbmih->bmiHeader.biClrUsed = 0;
lpbmih->bmiHeader.biClrImportant = 0;
//* Create Data for bmp
HDC hdc,hdcMem;
HBITMAP hBitMap = NULL;
CBitmap *pBitMap = NULL;
CDC *pMemDC = NULL;
BYTE *pBits;
CBrush brWhite,brBlack;
hdc = CreateIC(TEXT("DISPLAY"),NULL,NULL,NULL);
hdcMem = CreateCompatibleDC(hdc);
hBitMap = CreateDIBSection(hdcMem,lpbmih,DIB_PAL_COLORS,(void **)&pBits,NULL,0);
pBitMap = new CBitmap;
pBitMap->Attach(hBitMap);
pMemDC = new CDC;
pMemDC->Attach(hdcMem);
pMemDC->SelectObject(pBitMap);
brWhite.CreateSolidBrush(RGB(255,255,255));
brBlack.CreateSolidBrush(RGB(0,0,0));
//* Clear First and draw coordinate with gray.
pMemDC->FillRect(CRect(0,0,m_iWidth,m_iHeight),&brWhite);
int iGray = RGB(192,192,192),iDrakGray = RGB(128,128,128),iDateHeight = 11; //* Color Gray and eg.2002-3-8's and 2-11M's and MinIndex's and MaxIndex's Height.
int iSpaceLine = 12; //* The space with two lines of bottom.
int iTitleHeight; //* eg.上海A股's Height.
TEXTMETRIC ptm;
CRect rect;
CPen newPenDot(PS_DASH,1,iDrakGray);
CPen penDrakGray(PS_SOLID,1,iDrakGray);
CPen penGray(PS_SOLID,1,iGray);
GetTextMetrics(hdcMem,&ptm);
iTitleHeight = ptm.tmHeight;
pMemDC->SelectObject(&penDrakGray);
//* Draw K line for Index.
int iOffsetClose = -1;
for(int i=0; i<iDays; i++)
{
if(pIndex[i].fOpen <= pIndex[i].fClose) //* 阳线
{
//* Up-shadow line.
pMemDC->MoveTo(pIndex[i].iDay,pIndex[i].fClose); //* iRectWidth equate 2
pMemDC->LineTo(pIndex[i].iDay,pIndex[i].fHigh);
//* Entity of white
pMemDC->Rectangle(pIndex[i].iDay - iRectWidth,pIndex[i].fOpen,pIndex[i].iDay + iRectWidth,pIndex[i].fClose);
//* Bottom-shadow line.
pMemDC->MoveTo(pIndex[i].iDay,pIndex[i].fOpen);
pMemDC->LineTo(pIndex[i].iDay,pIndex[i].fLow);
//* Draw Volume.
pMemDC->SelectObject(&penDrakGray);
pMemDC->Rectangle(pIndex[i].iDay - iRectWidth,m_iHeight - iDateHeight,pIndex[i].iDay + iRectWidth,pIndex[i].iVolume);
pMemDC->SelectStockObject(BLACK_PEN);
}
else //* 阴线
{
//* Up-shadow line.
pMemDC->MoveTo(pIndex[i].iDay,pIndex[i].fOpen);
pMemDC->LineTo(pIndex[i].iDay,pIndex[i].fHigh);
//* Entity of black.
rect.SetRect(pIndex[i].iDay - iRectWidth,pIndex[i].fClose,pIndex[i].iDay + iRectWidth,pIndex[i].fOpen);
pMemDC->FillRect(rect,&brBlack);
//* Draw Vloume.
rect.SetRect(pIndex[i].iDay - iRectWidth,m_iHeight - iDateHeight,pIndex[i].iDay + iRectWidth,pIndex[i].iVolume);
pMemDC->FillRect(rect,&brBlack);
}
}
//* Save to File.And Create BITMAPFILEHEADER.
BITMAPFILEHEADER bmfh;
ZeroMemory(&bmfh,sizeof(BITMAPFILEHEADER));
*((char *)&bmfh.bfType) = 'B';
*(((char *)&bmfh.bfType) + 1) = 'M';
bmfh.bfOffBits = sizeof(BITMAPFILEHEADER) + sizeof(BITMAPINFOHEADER);
bmfh.bfSize = bmfh.bfOffBits + (m_iWidth * m_iHeight) * g_iPixel / 8;
TCHAR szBMPFileName[32];
int iBMPBytes = m_iWidth * m_iHeight * g_iPixel / 8;
strcpy(szBMPFileName,m_szFileName);
CFile file;
if(file.Open(szBMPFileName,CFile::modeWrite | CFile::modeCreate))
{
file.Write(&bmfh,sizeof(BITMAPFILEHEADER));
file.Write(&(lpbmih->bmiHeader),sizeof(BITMAPINFOHEADER));
file.Write(pBits,iBMPBytes);
file.Close();
}
pMemDC->DeleteDC();
delete pMemDC;
delete pBitMap;
delete lpbmih;
}
————————————————————————
有编程经验的程序员都知道:要使应用程序的界面美观不可避免的要使用大量位图。现在流行的可视化编程工具对位图的使用提供了很好的支持,被称为三大可视化开发工具的VB、VC、Delphi通过封装位图对象对位图使用提供了很好的支持:VB提供了两个功能很强的对象:PictureBox及Image,通过使用它们,装载、显示位图变得非常容易。Delphi中也提供了一个位图对象:TImage,它的功能与用法与VB中的Image类似。在VC中通过使用设备相关类CDC与GDI对象类CBitmap来完成位图的操作。
然而在VC中使用CBitmap类必须将BMP位图装入资源中,然后通过类 CBitmap的成员函数使用它,在通过CDC类的成员函数操作它。这样做有两点缺陷:将位图装入资源导致可执行文件增大,不利于软件发行;只能使用资源中有限的位图,无法选取其它位图。而且BMP位图文件是以DIB(设备无关位图)方式保存,BMP位图装入资源后被转换为DDB(设备相关位图),类CBitmap就是对一系列DDB操作的API函数进行了封装,使用起来有一定的局限性,不如DIB可以独立于平台特性。
要弥补使用资源位图的两点不足,就必须直接使用BMP位图文件。VC的示例中提供了一种方法读取并显示BMP位图文件,但使用起来相当的麻烦。首先使用API函数GlobalAlloc分配内存并创建HDIB位图句柄,所有操作只能直接读写内存,然后通过StrechDIBits及SetDIBsToDevice函数来显示于屏幕上,操作起来费时费力。
因此笔者通过研究类CBitmap的封装与DIB结构,使用Win32中提供的新函数,建立了一个专用于操作BMP文件的类,而且完全仿照类CBitmap的实现:从类CGdiObject派生,新类的所有接口与类CBitmap 的部分接口完全相同。这样对于习惯使用CBitmap类接口用法的程序员来说两者的接口在使用上没有什么分别。
首先我们先简单介绍一下DIB的结构。DIB位图既可以存在于内存,也可以以文件形式保存在磁盘上(BMP文件)。所有DIB都包含两部分信息:位图信息(BITMAPINFO),包括位图信息头和颜色表;位图数据。对于内存中DIB的只要有上述两部分就行,而对于DIB文件则还要加上位图文件头。
其次,Win32中提供了一个新函数CreateDIBSection,通过它可以创建一个存储DIB位的内存区域,既可以执行相应的GDI操作,又可以直接通过指向DIB位区域的指针方位DIB位区域。这是一个非常有用的函数,通过它我们可以用DIB替代DDB。
在了解了相应的知识后,我们可以自己由类CGdiObject派生一个操作BMP文件的类:CBitmapFile。
在自己编写类时有两点值得注意:
在BitmapFile.h文件中定义类CBitmapFile,首先必须声明类CBitmapFile是从类CGdiObject中公有派生。然后在类中首先使用宏DECLARE_DYNAMIC(CBitmapFile)表明新类的最高父类是类CObject,是符合MFC的类库规范。紧接着宏DECLARE_DYNAMIC的是声明静态函数FromHandle,这两个声明必须放在类定义的最前面。
在BitmapFile.cpp文件中类的成员函数的实现前加上IMPLEMENT_DYNAMIC(CBitmapFile,CGdiObject);表明类CBitmapFile直接派生于类CGdiObject。
在类CBitmapFile的声明中有三个函数与类Cbitmap中的定义稍有不同:
在类CbitmapFile中LoadBitmap函数的参数是LPCTSTR型,保存的是BMP文件的文件名。
在类CbitmapFile中CreateBitmap函数的参数中少了参数nPlanes,在函数内部默认为1。
在类CbitmapFile中CreateBitmapIndirect函数的参数中多了参数lpBits,它指向指定位图DIB位的内存区域。
在成员函数中最重要的是函数CreateBitmapIndirect和函数LoadBitmap:
在函数CreateBitmapIndirect中使用函数CreateDIBSection创建了一个以兼容DC为基础的HBITMAP句柄,并用继承自类CGdiObject 的函数Attach把它与类CGdiObject的句柄m_hObject关联起来。然后将指定位图的DIB位图数据拷贝到由函数CreateDIBSection创建的DIB位的内存区域。
在函数LoadBitmap中首先从指定文件名的文件中读取以结构BITMAPFILEHEADER为大小的数据块,然后由文件头标志判断文件是否为BMP位图文件,然后由BITMAPFILEHEADER中bfSize保存的文件大小与文件的真实大小比较文件是否有损坏,再由BITMAPFILEHEADER中bfOffBits与BITMAPFILEHEADER结构大小相减计算出位图信息头和颜色表一共的大小,动态申请一块空间保存位图信息头和颜色表信息,再由BITMAPFILEHEADER中bfSize与bfOffBits相减计算出DIB位图数据的大小,动态申请一块空间保存DIB位图数据,最后调用成员函数CreateBitmapIndirect来创建DIB位图。
在应用程序的OnPaint()事件中绘制DIB位图的方法与使用类CBitmap时绘制位图的方法完全相同,但有一点要注意的是由于CDC类没有提供返回新类CBitmapFile指针类型的将DIB位图选入内存的SelectObject函数,所以在使用SelectObject时要将返回类型强制转换为CbitmapFile *类型。
至此,关于新类CBitmapFile编写中的一些要点和使用时一些要注意的问题就介绍这么多了。