GDI+ Bitmap和GDI HBITMAP互转

之所以将GDI+ Bitmap和GDI HBITMAP互转单独挑出来写下,是因为实际应用中经常我们需要GDI和GDI+换用,特别是需要兼顾效率和渲染效果的场合,更是通常使用GDI+做平滑/抗锯齿,用GDI做常规绘图。

熟悉GDI+的人已经知道,GDI+本身已经提供了HBITMAP和Bitmap转换的函数:

HBITMAP->Bitmap 构造函数或FromHBITMAP

Bitmap->HBITMAP GetHBITMAP

对于无透明度的图像来说,直接调用没有问题,但是对应带透明度的图像,使用时就有问题了,所以本文重点讨论的是带透明度的HBITMAP和Bitmap的转换


1.Bitmap->HBITMAP

msdn函数原型如下:

[cpp] view plain copy
  1. Status GetHBITMAP(  
  2.   [in, ref]  const Color &colorBackground,  
  3.   [out]      HBITMAP *hbmReturn  
  4. );  
重要的是这里的colorBackground参数,这个参数微软没有给出详细说明,很容易误用。注意两点:

1.Color中的Alpha参数无用,这里纯粹是因为Color包含Alpha才不得不指明这个参数

2.这个参数主要是针对带Alpha通道的图,可以将透明背景替换成指定的单色背景,对于不透明图无法指明其背景色


对于如下带透明背景的图像

GDI+ Bitmap和GDI HBITMAP互转

编写如下代码,注意这里输入Color使用的是默认值(0):

[cpp] view plain copy
  1. void DrawImages3(HDC &hdc)  
  2. {  
  3.     RECT rect = {0,0,500,500};  
  4.     FillRect(hdc, &rect, (HBRUSH)::GetStockObject(GRAY_BRUSH));  
  5.   
  6.     Bitmap bmp1(L".\\pop_bk.png");  
  7.     //CGdiplusConvertHelper::DumpBitmap(bmp1);  
  8.   
  9.     HBITMAP hBitmap= NULL;  
  10.     Color clr(0,0,0,0);   
  11.     if (Gdiplus::Ok == bmp1.GetHBITMAP(clr, &hBitmap))//转换完成的是预乘Alpha的图像  
  12.     {  
  13.         MyTrace(L"Bkclr %d-%d-%d-%d PixelFormat:%d %d %d %d",   
  14.             clr.GetRed(), clr.GetGreen(), clr.GetBlue(), clr.GetAlpha(),   
  15.             bmp1.GetPixelFormat(), PixelFormat24bppRGB, PixelFormat32bppARGB, PixelFormat32bppPARGB);  
  16.         //CGdiplusConvertHelper::DumpHBITMAP(hdc, hBitmap);  
  17.   
  18.         HDC hMemDC = ::CreateCompatibleDC(hdc);  
  19.         HBITMAP hOld = (HBITMAP)SelectObject(hMemDC, hBitmap);  
  20.   
  21.         BLENDFUNCTION blendfunc = {AC_SRC_OVER, 0, 255, AC_SRC_ALPHA};  
  22.         AlphaBlend(hdc,   
  23.             0, 0, bmp1.GetWidth(), bmp1.GetHeight(),  
  24.             hMemDC,  
  25.             0, 0, bmp1.GetWidth(), bmp1.GetHeight(),  
  26.             blendfunc);  
  27.   
  28.         SelectObject(hMemDC, hOld);  
  29.         DeleteDC(hMemDC);  
  30.         DeleteObject(hBitmap);  
  31.     }  
  32. }  
这里为了突出透明,把背景画成灰色,转换完成的HBITMAP,使用AlphaBlend绘制,效果如下,背景透明,没有问题。

GDI+ Bitmap和GDI HBITMAP互转
 

和注释标注的一样,注意这里转换完成的就是Alpha通道预乘过的图,AlphaBlend要求必须是预乘过的图,所以刚好没有问题。为了验证确实生成的图确实是预乘过的图,可以打开代码中的两行注释,debugview分别输出Bitmap和HBITMAP中的像素值,选择其中的部分转换前后对应的像素如下,

GDI+ Bitmap和GDI HBITMAP互转

比如87-141-202-204 -> 70-113-162-204 

70=87*204/255

113=141*204/255

162=202*204/255

所以转换的图是Alpha预乘过的图


这里输出Bitmap每行像素很简单,调用GetPixel即可,遍历HBITMAP的代码稍后再讲。


下面我们把输入的Color=(0,255,0,0),即把透明背景换成红色 显示,结果如下

GDI+ Bitmap和GDI HBITMAP互转

这个红色怎么看起来这么怪呢,感觉比真正的红色要浅一些,什么原因呢?接着我们再用上面的方法,看下转换前后的对应像素值:

GDI+ Bitmap和GDI HBITMAP互转

原因还是我们刚才说的,转换时Alpha不会被调整,所以转换完成后的图像的原先透明背景处虽然RGB=(255,0,0),但是Alpha=0表示还是透明的,使用AlphaBlend显示时就会和背景混合造成颜色变浅

解决这个问题很简单,既然我们指明了背景色,就是想把透明背景换掉,这时候使用AlphaBlend显示时,第四个参数不应该是AC_SRC_ALPHA而是0,这样表明要显示的图不使用透明度,这样就解决问题了。调整参数后,结果如下,完美。

GDI+ Bitmap和GDI HBITMAP互转


2.HBITMAP->Bitmap

如下,使用上文中的方法生成带透明度的HBITMAP,再使用Bitmap构造函数转换HBITMAP(FromHBITMAP效果一样)为Bitmap,最后显示出来

[cpp] view plain copy
  1. void DrawImages4(HDC &hdc)  
  2. {  
  3.     RECT rect = {0,0,500,500};  
  4.     FillRect(hdc, &rect, (HBRUSH)::GetStockObject(GRAY_BRUSH));  
  5.   
  6.     Bitmap bmp1(L".\\pop_bk.png");  
  7.   
  8.     HBITMAP hBitmap= NULL;  
  9.     Color clr;   
  10.     if (Gdiplus::Ok == bmp1.GetHBITMAP(clr, &hBitmap))//转换完成的是预乘Alpha的图像  
  11.     {  
  12.         CGdiplusConvertHelper::DumpHBITMAP(hdc, hBitmap);  
  13.   
  14.         //透明HBITMAP转Bitmap  
  15.         Bitmap bmp2(hBitmap, NULL);  
  16.         CGdiplusConvertHelper::DumpBitmap(bmp2);  
  17.   
  18.         //绘制Bitmap  
  19.         Graphics g(hdc);  
  20.         g.DrawImage(&bmp2, 0, 0);  
  21.   
  22.         DeleteObject(hBitmap);  
  23.     }  
  24. }  

显示效果如下:

GDI+ Bitmap和GDI HBITMAP互转

透明背景全部变成黑色了,这是为什么,还是使用前文方法,查看对应像素值如下

GDI+ Bitmap和GDI HBITMAP互转

可以看到,之前显示透明的像素,RGB值没有变,但是透明度变成255=完全不透明,而RGB(0,0,0)刚好是黑色,其他的Bitmap的透明度也都为255,这说明转换过程中透明度丢失了,因此对于带透明度的HBITMAP,这种方法是行不通的。这里多说一句,为什么微软这里实现不把透明度带上呢,结合GDI Bitblt等函数也都不支持Alpha操作,我猜可能是GDI+设计时候HBITMAP支持Alpha还比较弱,所以干脆不读取了,可能还是有别的考虑,知道内幕的请留言,学习下。


下面接着说,怎么才能够完成带透明度的HBITMAP转Bitmap的操作呢,我们知道两者的格式不一样,但是他们的数据都是一样的,所以考虑自己创建一个带透明度的Bitmap,从HBITMAP中读取数据,填充到Bitmap对应位置即可,具体操作如下:


1.读取HBITMAP信息

[cpp] view plain copy
  1. //获取DDB或DIB信息,DDB和hdc相关,DIB和实际数据相关    
  2. static BOOL GetDIBitsInfo(const HDC &hdc, const HBITMAP &hBitmap, BITMAPINFOHEADER &bmih)  
  3. {  
  4.     bmih.biSize = sizeof(BITMAPINFOHEADER); //暂时未知颜色表大小,先只填充BITMAPINFOHEADER,BI_BITFIELDS需要多余数据也不填充  
  5.     bmih.biBitCount = 0;                    //不填充颜色表,此时查询图像属性    
  6.   
  7.     if (0 == GetDIBits(hdc,     
  8.         hBitmap,     
  9.         0, 0,     
  10.         NULL, (BITMAPINFO *)&bmih,     
  11.         DIB_RGB_COLORS))    
  12.     {    
  13.         return FALSE;    
  14.     }    
  15.   
  16.     return TRUE;  
  17. }  

2.读取HBITMAP数据

[cpp] view plain copy
  1. //按照指定格式导出数据,不必和上述获取的保持一致,GetDIBits会自动转换  
  2. static PBYTE GetDIBitsData(const HDC &hdc, const HBITMAP &hBitmap, BITMAPINFOHEADER &bi)  
  3. {  
  4.     bi.biSize = sizeof(BITMAPINFOHEADER);      
  5.     //bi.biWidth = bi.biWidth;     
  6.     bi.biHeight = abs(bi.biHeight);//数据自底向上  
  7.     bi.biPlanes = 1;         
  8.     bi.biBitCount = (bi.biBitCount==32) ? 32 : 24;//不带透明度的统一输出为24bit图  
  9.     bi.biCompression = BI_RGB;      
  10.     bi.biSizeImage = 0;    
  11.     bi.biXPelsPerMeter = 0;      
  12.     bi.biYPelsPerMeter = 0;      
  13.     bi.biClrUsed = 0;      
  14.     bi.biClrImportant = 0;  
  15.   
  16.     //分配数据存储区     
  17.     int nLineSize = (((bi.biBitCount*bi.biPlanes*bi.biWidth + 31) & ~31) >> 3);//DIB每行为4字节倍数,向上取整,不足的每行后面填充0  
  18.     DWORD dwBitsSize = bi.biHeight * nLineSize;  
  19.   
  20.     BOOL bRet = FALSE;  
  21.     PBYTE pBits = NULL;  
  22.     do   
  23.     {  
  24.         pBits = (PBYTE)malloc(dwBitsSize);    
  25.         if (NULL == pBits)    
  26.         {      
  27.             break;  
  28.         }    
  29.   
  30.         //读入位图信息和位图数据  
  31.         if (0 == GetDIBits( hdc,     
  32.             hBitmap,     
  33.             0, bi.biHeight,     
  34.             pBits, (BITMAPINFO *)&bi,     
  35.             DIB_RGB_COLORS))    
  36.         {     
  37.             break;  
  38.         }  
  39.   
  40.         bRet = TRUE;  
  41.     } while (false);  
  42.   
  43.     if (!bRet)  
  44.     {  
  45.         FreeDIBitsData(pBits);  
  46.     }  
  47.   
  48.     return bRet ? pBits : NULL;  
  49. }  


3.根据HBITMAP创建对应Bitmap,并将HBITMAP数据拷贝到Bitmap对应数据区

[cpp] view plain copy
  1. static Bitmap* HBITMAPToBitmap(HDC hdc, HBITMAP hBitmap)  
  2.     {  
  3.         if (!hdc || !hBitmap)  
  4.         {  
  5.             return NULL;  
  6.         }  
  7.   
  8.         BITMAPINFOHEADER bmih={0};  
  9.         if (!GetDIBitsInfo(hdc, hBitmap, bmih))  
  10.         {  
  11.             return NULL;  
  12.         }  
  13.   
  14.         PBYTE pBits = NULL;  
  15.         Bitmap* pBitmap = NULL;  
  16.   
  17.         BOOL bRet = FALSE;  
  18.         do   
  19.         {  
  20.             if (NULL == (pBits=GetDIBitsData(hdc, hBitmap, bmih)))  
  21.             {  
  22.                 break;  
  23.             }  
  24.   
  25.             //创建Bitmap信息  
  26.             PixelFormat pf  = bmih.biBitCount==32 ? PixelFormat32bppPARGB : PixelFormat24bppRGB;  
  27.             LONG cyHeight   = abs(bmih.biHeight);  
  28.             UINT nLineSize  = (((bmih.biBitCount*bmih.biPlanes*bmih.biWidth + 31) & ~31) >> 3);  
  29.   
  30.             pBitmap = new Bitmap(bmih.biWidth, cyHeight, pf);  
  31.             if (!pBitmap)  
  32.             {  
  33.                 break;  
  34.             }  
  35.   
  36.             //填充GDI+ Bitmap数据  
  37.             BitmapData bitmapData;  
  38.             Rect rect(0, 0, bmih.biWidth, cyHeight);  
  39.             if ( Gdiplus::Ok != pBitmap->LockBits(&rect, ImageLockModeRead, pf, &bitmapData) )  
  40.             {  
  41.                 break;  
  42.             }  
  43.   
  44.             BYTE *pDestBits = (BYTE*)bitmapData.Scan0;  
  45.             for ( int y = 0; y < cyHeight; y++ )// 从下到上复制数据,因为前面设置高度时是正数。  
  46.             {  
  47.                 memcpy_s(   
  48.                     (pDestBits + y * nLineSize),   
  49.                     nLineSize,   
  50.                     (pBits + (cyHeight - y - 1) * nLineSize),   
  51.                     nLineSize);  
  52.             }  
  53.   
  54.             if ( Gdiplus::Ok != pBitmap->UnlockBits(&bitmapData))  
  55.             {  
  56.                 break;  
  57.             }  
  58.           
  59.             bRet = TRUE;  
  60.         } while (false);  
  61.           
  62.         FreeDIBitsData(pBits);  
  63.         if (!bRet)  
  64.         {  
  65.             delete pBitmap;  
  66.             pBitmap = NULL;  
  67.         }  
  68.   
  69.         return pBitmap;  
  70.     }  


所以,HBITMAP转换和显示代码,如下:

[cpp] view plain copy
  1. void DrawImages5(HDC &hdc)  
  2. {  
  3.     RECT rect = {0,0,500,500};  
  4.     FillRect(hdc, &rect, (HBRUSH)::GetStockObject(GRAY_BRUSH));  
  5.   
  6.     Bitmap bmp1(L".\\pop_bk.png");  
  7.   
  8.     HBITMAP hBitmap= NULL;  
  9.     Color clr;   
  10.     if (Gdiplus::Ok == bmp1.GetHBITMAP(clr, &hBitmap))//转换完成的是预乘Alpha的图像  
  11.     {  
  12.         //转换和显示  
  13.         Bitmap *pbmp2 = CGdiplusConvertHelper::HBITMAPToBitmap(hdc, hBitmap);  
  14.         if (pbmp2)  
  15.         {  
  16.             Graphics g(hdc);  
  17.             g.DrawImage(pbmp2, 0, 0);  
  18.   
  19.             CGdiplusConvertHelper::DumpBitmap(*pbmp2);  
  20.   
  21.             delete pbmp2;  
  22.         }  
  23.   
  24.         DeleteObject(hBitmap);  
  25.     }  
  26. }  
显示如下,完美。到此转换代码讲解完,这时候回归之前的一个问题,如何输出HBITMAP每行像素,其实就是和这里的处理方式一样,不同的只是把拷贝过程换成了输出过程。

GDI+ Bitmap和GDI HBITMAP互转


本文查看每行像素和转换代码已经封装成一个帮助类CGdiplusConvertHelper,下载链接

原创,转载请注明来自http://blog.****.net/wenzhou1219