用Visual C++从位图文件生成任意形状的窗口
有许多的软件的界面十分地漂亮,不仅窗口的客户区绘制得十分精细,连窗口的外形也是“奇形怪状”的,比如 Office 2000助手、Media Player 7、MediaRing Talk等等,连Winamp在应用了某些皮肤之后也不再是标准的矩形窗口,下图也是一个不规则的窗口。
那么,我们在编程的时候如何实现这一效果呢?
在众多的Windows API函数中,有一个名叫SetWindowRgn的函数可以用来将窗口的形状调整成为任意形状,所有那些软件中“奇形怪状”的窗口都是这样得到的。SetWindowRgn有三个参数,原型如下(在winuser.h文件中还可见到WINUSERAPI和WINAPI的前辍)。
int SetWindowRgn(
HWND hWnd, // 要调整的窗口的句柄
HRGN hRgn, // 区域的句柄
BOOL bRedraw // 是否立即重画窗口
); //返回非0值如果成功,否则返回0
在MFC的CWnd类的成员函数SetWindowRgn只不过少了第一个参数。
那么,剩下的工作就是创建一个区域了。
创建一个区域并不困难,有许多现成的API函数可以调用。只不过简比较简单的方法只能创建出比较简单的区域。这样的函数有:
1. 创建椭圆的区域
HRGN CreateEllipticRgn(
int nLeftRect, // 边界矩形左上角的x坐标
int nTopRect, // 边界矩形左上角的y坐标
int nRightRect, // 边界矩形右下角的x坐标
int nBottomRect // 边界矩形右下角的y坐标
);
2.创建矩形区域
HRGN CreateRectRgn(
int nLeftRect, // 边界矩形左上角的x坐标
int nTopRect, // 边界矩形左上角的y坐标
int nRightRect, // 边界矩形右下角的x坐标
int nBottomRect // 边界矩形右下角的y坐标
);
3.创建圆角椭圆区域
HRGN CreateRoundRectRgn(
int nLeftRect, // 边界矩形左上角的x坐标
int nTopRect, // 边界矩形左上角的y坐标
int nRightRect, // 边界矩形右下角的x坐标
int nBottomRect // 边界矩形右下角的y坐标
int nWidthEllipse, // 圆角椭圆的高度
int nHeightEllipse //圆角椭圆的宽度
);
4.创建任意形状的区域
HRGN ExtCreateRegion(
CONST XFORM *lpXform, // 变形数据的指针
DWORD nCount, // 数据在大小,以字节计
CONST RGNDATA *lpRgnData // 数据的指针
);
如果创建失败,所有的函数都返回NULL。
前三个函数的用法简单明了,第四个函数则有些复杂了。ExtCreateRegion函数可以从一系列的矩形创建一个区域,这个区域是所有矩形的并集。这样的功能可以通过创建相同的矩形区域,再通过CombineRgn函数将它们合并来实现,但是ExtCreateRegion有着更好的性能。一般地,我们只指定后两个参数,而将第一个参数置为NULL值。RGNDATA结构的定义如下(wingdi.h)
typedef struct _RGNDATA {
RGNDATAHEADER rdh;
char Buffer[1];
} RGNDATA, *PRGNDATA, NEAR *NPRGNDATA, FAR *LPRGNDATA;
其中,RGNDATAHEADER结构的定义是:
typedef struct _RGNDATAHEADER { //rdh
DWORD dwSize; // 结构本身的大小
DWORD iType; // 类型,必须是RDH_RECTANGLES(值为1)
DWORD nCount; // 矩形的个数
DWORD nRgnSize; // 接受矩形数据的缓冲区大小
RECT rcBound; // 区域的边界矩形的大小
} RGNDATAHEADER, *PRGNDATAHEADER;
指定了这些基本数据之后,可以填入矩形的数据,最后以RGNDATA*类型的指针为参数调用ExtCreateRegion函数就可以创建区域了。
现在,问题的关键就只剩下如何指定矩形的数据了。最简单的指定方法是使用固定的数据,如下例所示:
RGNDATA rd;
rd.rdh.dwSize = sizeof(RGNDATAHEADER);
rd.rdh.iType = 1;
rd.rdh.nRgnSize = 0;
rd.rdh.nCount = 2;
SetRect( &rd.rdh.rcBound, 0, 0, 1000, 1000);
LPRECT lpRect = (LPRECT) &rd.Buffer;
::SetRect( &lpRect[0], 0, 0, 250, 20);
::SetRect( &lpRect[1], 20, 20, 300, 40);
HRGN hRgn = ::ExtCreateRegion(NULL, sizeof(RGNDATAHEADER) + (sizeof(RECT) * 2 ), &rd);
但是这样创建出的简单区域并没有实用价值,一个比较通用的想法,是将要创建的区域画在一幅位图中,再从位图中读取有关的信息从而创建区域,这样就可以创建任意形状的区域,也就可以创建任意形状的窗口了,而且修改形状非常地方便,只要修改位图即可。特殊形状的窗口一般都有背景图片,我们将这个背景图片设置一个透明色后(窗口形状中不包含的部分),就可一举两得了。透明色一般可以取洋红色,其RGB值为255, 0, 255
将一幅位图转化为一系列矩形的集合的最优化算法并不是很容易实现的。一般采用这样的一个可行方法:逐行扫描位图,将非透明色且连续的象素点合并成为一个矩形。,每个矩形就只有一个象素点高。从我们的角度来看,这种方法就是用一条条的横线将图像表示出来。这种算法简便易行,时间复杂度和空间复杂度上可以估算,在位图很复杂的情况下与最优算法的差距不大,但在位图比较简单(如:由一组矩形构成)的情况下比最优算法性能想差很多倍(在那种情况下可以用CreateRectRgn函数,再调用CombineRgn合并区域)。
关于如何从位图中读取图像信息不再详述。
用Visual C++生成一个MFC .exe工程,名为ShapeWnd,类型为对话框,其余取默认值。将IDD_SHAPEWND_DIALOG资源的风格修改为无标题栏、弹出式窗口、无边界。再生成两个文件,名为bmp2rgn.h和bmp2rgn.cpp,并将它们加入到工程,其详细内容可参见下文。修改ShapeWndDlg.cpp文件,用Class Wizard添加WM_CREATE消息的处理器,将其内容修改如下:
int CShapeWndDlg::OnCreate(LPCREATESTRUCT lpCreateStruct)
{
if (CDialog::OnCreate(lpCreateStruct) == -1)
return -1;
// adjust the window's region
static HRGN hRgn;
hRgn = CreateRgnFromBitmap (IDB_SHAPE, RGB(255, 255, 255)); // pure white
SetWindowRgn(hRgn, TRUE);
return 0;
}
再将OnPaint函数修改如下:
void CShapeWndDlg::OnPaint()
{
if (IsIconic())
{
// AppWizard generated code
CPaintDC dc(this); // device context for painting
SendMessage(WM_ICONERASEBKGND, (WPARAM) dc.GetSafeHdc(), 0);
// Center icon in client rectangle
int cxIcon = GetSystemMetrics(SM_CXICON);
int cyIcon = GetSystemMetrics(SM_CYICON);
CRect rect;
GetClientRect(&rect);
int x = (rect.Width() - cxIcon + 1) / 2;
int y = (rect.Height() - cyIcon + 1) / 2;
// Draw the icon
dc.DrawIcon(x, y, m_hIcon);
}
else
{
// customized code
// paint the bitmap on the dialog
CPaintDC dcDlg(this);
CDC dcMem;
CBitmap bitmap;
CRect rect;
GetWindowRect(rect);
bitmap.LoadBitmap(IDB_SHAPE);
dcMem.CreateCompatibleDC(&dcDlg);
CBitmap * pOldBitmap = dcMem.SelectObject(&bitmap);
dcDlg.BitBlt(0,0,rect.Width(),rect.Height(),&dcMem,0,0,SRCCOPY);
dcMem.SelectObject(pOldBitmap);
}
}
新建或导入一个位图,取其ID为IDB_SHAPE,它所含的图像就是最后窗口的形状。
最后,在ShapeWndDlg.cpp文件的首部加入一行:
#include "bmp2rgn.h"
编译工程,就可以看到类似文首的效果了。
Bmp2rgn.h文件的内容
// bmp2rgn.h : the header file
//
#ifndef _BMP2RGN_H__INCLUDED
#define _BMP2RGN_H__INCLUDED
#define ALLOC_UNIT 100 /* allocate memory of 100 rectangles one time */
HRGN CreateRgnFromBitmap(UINT uIDBitmap, // the bitmap's ID
COLORREF crTransparent = RGB( 255, 0, 255) );// transparent color, default to megenta
#endif //_BMP2RGN_H__INCLUDED
bmp2rgn.cpp文件的内容
// bmp2rgn.cpp : implementation file
//
#include "stdafx.h"
#include "bmp2rgn.h"
#ifdef _DEBUG
#define new DEBUG_NEW
#undef THIS_FILE
static char THIS_FILE[] = __FILE__;
#endif
HRGN CreateRgnFromBitmap(UINT uIDBitmap, // the bitmap's ID
COLORREF crTransparent // transparent color, default to megenta
)
{
CDC memDC;
memDC.CreateCompatibleDC(NULL);
CBitmap bitmap;
BITMAP bm;
bitmap.LoadBitmap(uIDBitmap);
bitmap.GetBitmap(&bm);
BITMAPINFOHEADER bmih = { sizeof(BITMAPINFOHEADER),
bm.bmWidth, bm.bmHeight, 1, 32, BI_RGB, 0, 0, 0, 0, 0};
VOID * pBits;
HBITMAP hDIB = ::CreateDIBSection(memDC.m_hDC, (BITMAPINFO *)&bmih, DIB_RGB_COLORS, &pBits, NULL, 0);
HBITMAP holdBmp = (HBITMAP)::SelectObject(memDC.m_hDC, hDIB);
CDC dc;
dc.CreateCompatibleDC(&memDC);
// Get how many bytes per row (rounded up to 32 bits)
BITMAP dib;
::GetObject(hDIB,sizeof(BITMAP),&dib);
while (dib.bmWidthBytes % 4)
{
dib.bmWidthBytes++;
}
// Copy the bitmap into the memory DC
HBITMAP holdBmp2 = (HBITMAP)::SelectObject(dc.m_hDC, bitmap.m_hObject);
memDC.BitBlt(0, 0, bm.bmWidth, bm.bmHeight, &dc, 0, 0, SRCCOPY);
// alloc some memory using the ALLOC_UNIT
DWORD maxRect = ALLOC_UNIT;
HANDLE hData = ::GlobalAlloc(GMEM_MOVEABLE, sizeof(RGNDATAHEADER) + (sizeof(RECT) * maxRect));
// fill in the fields, just follow the rules
RGNDATA *pRgnData = (RGNDATA *)::GlobalLock(hData);
pRgnData->rdh.dwSize = sizeof(RGNDATAHEADER);
pRgnData->rdh.iType = RDH_RECTANGLES; // RDH_RECTANGLES eqs 1
pRgnData->rdh.nCount = pRgnData->rdh.nRgnSize = 0;
::SetRect(&pRgnData->rdh.rcBound, 0, 0, MAXLONG, MAXLONG);
// Get the R,G,B value of the transparent color
BYTE r0 = GetRValue(crTransparent);
BYTE g0 = GetGValue(crTransparent);
BYTE b0 = GetBValue(crTransparent);
// scan each bitmap row from bottom to top
BYTE *pRow = (BYTE *)dib.bmBits + (dib.bmHeight - 1) * dib.bmWidthBytes;
for (int y = 0; y x0 )
{
// Add the rectangle of (x0, y)-(x, y+1)
if (pRgnData->rdh.nCount >= maxRect) // if need more rectangles
{
// reallocate memory
::GlobalUnlock(hData);
maxRect += ALLOC_UNIT;
hData = ::GlobalReAlloc(hData, sizeof(RGNDATAHEADER) + (sizeof(RECT) * maxRect), GMEM_MOVEABLE);
pRgnData = (RGNDATA *)::GlobalLock(hData);
}
RECT *lpRect = (RECT *)&pRgnData->Buffer;
::SetRect(&lpRect[pRgnData->rdh.nCount], x0, y, x, y+1);
pRgnData->rdh.nCount++;
}
} // for x
pRow -= dib.bmWidthBytes; // go one row upper
} // for y
// Create a region consisted of the rectangles
HRGN hRgn = ::ExtCreateRegion(NULL, sizeof(RGNDATAHEADER) + (sizeof(RECT) * maxRect), pRgnData);
::GlobalFree( (HGLOBAL)pRgnData);
// do some cleaning up work
::SelectObject(dc.m_hDC, holdBmp2);
::DeleteObject(::SelectObject(memDC.m_hDC, holdBmp));
dc.DeleteDC();
memDC.DeleteDC();
bitmap.DeleteObject();
return hRgn;
}
因为这样生成的窗口没有标题栏,所以没有方法拖动它。所以,要重载窗口的OnNcHitTest函数,判断point参数,根据需要返回HTCAPTION即可实现拖动(因为窗口的边界为None,所以不会产生双击之后最大化的结果),在实际应用中可以判断鼠标所在区域返回不同的自定义值以供程序的其它部分使用。
最简单的OnNcHitTest函数可以写成这样:
UINT CShapeWndDlg::OnNcHitTest(CPoint point)
{
return HTCAPTION;
}