搞Windows程序的人尽皆知分层窗口能够实现很多不错的效果,之前看过一些异形窗口的实现,所以就手痒也想自己搞一个玩一玩。自己动手实现过程才发现还是有不少问题的。
基本思路是:
1.将窗口扩展属性设置为分层属性WS_EX_LAYERED。
2.选一张透明的png图片,并将其加载进来。
3.创建与窗口兼容的内存设备上下文,以及兼容位图,将兼容位图选入兼容设备上下文。
4.将png图片绘制到内存设备上下文中。
5.设置BLENDFUNCTION结构,调用UpdateLayeredWindow。
第一步设置窗口的分层属性比较简单:
windowStyle = GetWindowLong(hWnd, GWL_EXSTYLE); windowStyle = windowStyle | WS_EX_LAYERED; SetWindowLong(hWnd, GWL_EXSTYLE, windowStyle);
第二步是将png图片加载到程序中,ATL的CImage和GDI+的Image这两个类比较常用。使用CImage直接通过Load方法加载绝对路径图片或者内存中的图片,我这里就是使用CImage类实现。代码:
// CImage类方式加载图片 CImage img; img.Load(TEXT( "绝对路径 png图片 " )); // 将图片与类关联起来
然后是使用Image类方式:
// Image类方式加载图片 Image* pImage = Image::FromFile(_T( " 绝对路径png图片 " ));
第三步比较繁琐点。加载了图片后就需要创建一个位图句柄HBITMAP,创建位图句柄有两种方式:CreateCompatibleBitmap和CreateDIBSection这两个函数。先介绍下这两个函数。
HBITMAP CreateCompatibleBitmap( HDC hdc, // handle to DC int nWidth, // width of bitmap, in pixels int nHeight // height of bitmap, in pixels);
HBITMAP CreateDIBSection( HDC hdc, // handle to DC CONST BITMAPINFO *pbmi, // bitmap data UINT iUsage, // data type indicator VOID **ppvBits, // bit values HANDLE hSection, // handle to file mapping object DWORD dwOffset // offset to bitmap bit values );
首先看看CreateCompatibleBitmap函数的代码:
// CreateCompatibleBitmap函数创建 hdc = GetDC(hWnd); // hWnd为需要分层窗口的句柄 hdcMem = CreateCompatibleDC(hdc); // 创建与hdc相兼容的内存句柄 hBitmap = CreateCompatibleBitmap(hdc, sz.cx, sz.cy); // 创建与hdc相兼容的位图句柄 SelectObject(hdcMem,(HGDIOBJ)hBitmap); // 将位图选入内存句柄作为画板
然后是使用CreateDIBSection函数的代码:
// CreateDIBSection函数创建 hdc = GetDC(hWnd); hdcMem = CreateCompatibleDC(hdc); BITMAPINFO bitmapinfo; bitmapinfo.bmiHeader.biSize = sizeof (BITMAPINFOHEADER); bitmapinfo.bmiHeader.biBitCount = 32 ; bitmapinfo.bmiHeader.biHeight = sz.cy; bitmapinfo.bmiHeader.biWidth = sz.cx; bitmapinfo.bmiHeader.biPlanes = 1 ; bitmapinfo.bmiHeader.biCompression = BI_RGB; bitmapinfo.bmiHeader.biXPelsPerMeter = 0 ; bitmapinfo.bmiHeader.biYPelsPerMeter = 0 ; bitmapinfo.bmiHeader.biClrUsed = 0 ; bitmapinfo.bmiHeader.biClrImportant = 0 ; bitmapinfo.bmiHeader.biSizeImage = bitmapinfo.bmiHeader.biWidth * bitmapinfo.bmiHeader.biHeight * bitmapinfo.bmiHeader.biBitCount / 8 ; hBitmap = ::CreateDIBSection(hdcMem,&bitmapinfo, 0 ,NULL, 0 , 0 ); SelectObject(hdcMem,(HGDIOBJ)hBitmap);
第四步,将png图片绘制到内存设备上下文中。这一步根据前面加载图片的两种方式对应不同的绘制函数。
CImage类方式代码:
// CImage类方式代码 // 将img关联的png图片绘制到内存句柄中 img.Draw(hdcMem, 0 , 0 , sz.cx, sz.cy, 0 , 0 , sz.cx, sz.cy); // hdcMem为内存兼容句柄,之后四个参数表明了在内存兼容句柄中绘制的位置,最后四个参数表示了img关联的png图片的位置
然后是Image类方式处理代码:
// Image类方式代码 Graphics g(hMemDC); g.DrawImage( pImage, 0 , 0 );
第五步:设置BLENDFUNCTION结构,调用UpdateLayeredWindow。
设置BLENDFUNCTION结构代码如下:
BLENDFUNCTION bf; bf.AlphaFormat = AC_SRC_ALPHA; // 源位图具有Alpha通道 bf.BlendFlags = 0 ; // 必须为0 bf.BlendOp = AC_SRC_OVER; // bf.SourceConstantAlpha = 255 ; // 设置透明度
然后就是调用UpdateLayeredWindow函数了
UpdateLayeredWindow(hWnd, hdc, NULL, &sz, hdcMem, &pt, NULL, &bf, ULW_ALPHA);
看看MSDN对UpdateLayeredWindow函数的介绍:
BOOL UpdateLayeredWindow( HWND hwnd, // 需要分层的窗口句柄 HDC hdcDst, // 需要分层窗口设备句柄 POINT *pptDst, // 窗口位置不发生变化可以设置为NULL SIZE *psize, // 窗口大小不发生变化可以设置为NULL HDC hdcSrc, // 绘制源的设备句柄 POINT *pptSrc, // COLORREF crKey, // 指定一个透明色,使用ULW_COLORKEY标志时有效,也就是说crKey为白色时候,那么位图上所有白色的地方均为透明,其他地方不透明 BLENDFUNCTION *pblend, // 之前介绍过了 DWORD dwFlags // ULW_ALPHA使用Alpha通道,ULW_COLORKEY使用crKey作为透明色,ULW_OPAQUE不透明 );
这样异形窗口就算完成了。
整理以下代码如下:
LONG windowStyle = GetWindowLong(hWnd, GWL_EXSTYLE); windowStyle = windowStyle | WS_EX_LAYERED; SetWindowLong(hWnd, GWL_EXSTYLE, windowStyle); CImage img; img.Load(TEXT( " png图片 " )); SIZE sz; // 图片大小 sz.cx = img.GetWidth(); sz.cy = img.GetHeight(); SetWindowPos(hWnd, NULL, 0 , 0 , sz.cx, sz.cy, SWP_NOREDRAW); // 将窗口大小设置为图片大小使之相互合适 HDC hdc = GetDC(hWnd); // 获取窗口设备句柄 HDC hdcMem = CreateCompatibleDC(hdc); // 创建一个与hdc相兼容的内存设备句柄 HBITMAP hBitmap = CreateCompatibleBitmap(hdc, sz.cx, sz.cy); SelectObject(hdcMem,(HGDIOBJ)hBitmap); img.Draw(hdcMem, 0 , 0 , sz.cx, sz.cy, 0 , 0 , sz.cx, sz.cy); POINT pt; pt.x = 0 ; pt.y = 0 ; BLENDFUNCTION bf; bf.AlphaFormat = AC_SRC_ALPHA; bf.BlendFlags = 0 ; bf.BlendOp = AC_SRC_OVER; bf.SourceConstantAlpha = 255 ; UpdateLayeredWindow(hWnd, hdc, NULL, &sz, hdcMem, &pt, NULL, & bf, ULW_ALPHA); ReleaseDC(hWnd, hdc);
说说遇到的问题,最开始就碰到了一个问题发现png图片没有办法产生异形的效果,然后才发现原来我使用的png图片不是透明png图片。
当png透明图片问题解决后,发现窗口出现后,本来那个应该是透明的地方却是白色不透明的了。这个问题网上也是有介绍的,我在 这里 得到了答案。下面我把原因重新贴一下:
PNG图片的透明背景总是一片白色,后来才发现这其实是微软GDI+的设计问题,PNG图片是ARGB,使用GDI+载入图片的时候,GDI+会默认已经进行了预剩运算(PARGB),即每象素的实际值是已经和ALPHA值按比例相乘的结果,实际上它根本就没有做预乘,在使用透明图片的象素ALPHA通道的时候,CImage内部正是调用的AlphaBlend,没有预乘的图当作预乘的图片处理的结果就是这相当于一张和纯白背景进行了预剩,所以图象总是出现白色背景。
下面给出解决这个问题的代码,代码来源 这里 ,处理还是比较简单的:
if (pImage->GetBPP() == 32 ) // 确认该图像包含Alpha通道 { for (inti= 0 ; i<pImage->GetWidth();i++ ) { for ( int j= 0 ; j<pImage->GetHeight(); j++ ) { byte *pByte = ( byte *)pImage-> GetPixelAddress(i, j); pByte[ 0 ]= pByte[ 0 ] * pByte[ 3 ]/ 255 ; pByte[ 1 ]= pByte[ 1 ] * pByte[ 3 ]/ 255 ; pByte[ 2 ]= pByte[ 2 ] * pByte[ 3 ]/ 255 ; } } }
这样异形窗口也算是完成了,但是这样生成的exe文件需要依赖外部的png图片,所以我应该把那张png图片放到资源文件中,然后直接生成的exe就包含了那张png图片。这就涉及到了如何从资源中加载图片的问题。