1st 用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__;
#endifHRGN 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 < bm.bmHeight; y++)
{
for (int x = 0; x < bm.bmWidth; x++)
{
// Search for a continuous range of "non transparent pixels"
int x0 = x;
LONG *p = (LONG *)pRow + x;
while (x < bm.bmWidth)
{
BYTE r = GetRValue(*p);
BYTE g = GetGValue(*p);
BYTE b = GetBValue(*p);
if ( r == r0 && g == g0 && b == b0 ) // transparent pixel
{
break; // cause x == x0, then will not create a region
}
p++, x++;
} if (x > 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;
}

解决方案 »

  1.   

    2nd 用Visual C++制作文件浏览对话框在编制Windows应用程序时,常常要用到这样的一种功能:显示一个模式对话框,要求用户指定一个文件夹,如图所示。这个功能看起来很简单,其实不然,因为首先,Windows是一具多任务的操作系统,在这个对话框显示期间,用户或其它的进程可能创建或删除了某个文件夹,或者改变了文件夹的图标,或者改变了系统文件夹的映射(如我的文档的位置),这些都会影响这个对话框中的内容,而且,这个对话框的风格最好与当前版本的Windows保持一致。从编程角度来讲,它应当有着最好的性能并占用最少量的内存。一个功能良好的应用程序应当能够处理所有上述的情况以及其它可以预见的情况。这样看来,编制这个对话框就成了一个非常繁重的任务。
    那么,有没有一个简便易行的方法来实现所有的这些功能呢?我们可以求助于一个外壳API函数:SHBrowseForFolder。这个函数以当前系统默认的风格显示一个对话框,并且可以有效地控制它的行为,所有的难于处理的问题都留给了Explorer去处理,我们只需简单地取出字符串即可。SHBrowseForFolder的说明如下:
    WINSHELLAPI LPITEMIDLIST WINAPI SHBrowseForFolder(
        LPBROWSEINFO lpbi
    );
    SHBrowseForFolder函数返回一个ITEMIDLIST结构的指针,这个结构包含了用户选择文件夹的信息,需要注意的是,SHBrowseForFolder函数要求调用程序负责删除这个指针。如果用户选择了“取消”按钮,则返回NULL。SHBrowseForFolder函数的参数是一个BROWSEINFO结构变量,它的定义如下:
    typedef struct _browseinfo { 
        HWND hwndOwner; // 父窗口的句柄
        LPCITEMIDLIST pidlRoot; // 一个ITEMIDLIST结构变量,指定根目录
        LPSTR pszDisplayName; // 
        LPCSTR lpszTitle; // 位于对话框顶端的一行文字
        UINT ulFlags; // 标志变量,按位有效
        BFFCALLBACK lpfn; // 回调函数
        LPARAM lParam; // 传给回调函数的参数,一个32位值
        int iImage; // 被选择的文件夹的图片序号,与shell32.dll中的图标号同
    } BROWSEINFO, *PBROWSEINFO, *LPBROWSEINFO;
    ulFlags参数在浏览文件夹时一般取值为BIF_BROWSEFORCOMPUTER,这样,对话框将只允许指定在实际文件系统中存在的文件夹,否则“确定”按钮将会被禁用。
    如果SHBrowseForFolder函数返回的ITEMIDLIST结构指针不为NULL,就可以使用SHGetPathFromIDList函数取得存储于ITEMIDLIST结构指针中的路径信息。SHGetPathFromIDList函数的原型如下:
    WINSHELLAPI BOOL WINAPI SHGetPathFromIDList(
        LPCITEMIDLIST pidl,
        LPSTR pszPath
    );
    第一个参数就是存储了路径信息的ITEMIDLIST结构指针,第二个参数是一个字符缓冲区,用于接收字符串。它应当有_MAX_PATH所指定的长度,_MAX_PATH在Windows系统中被定义为260个字符,其大小可以是260或520个字节,这取决于是否使用了Unicode。
    现在,让我们来制作一个工程。打开App Wizard,创建一个MFC EXE对话框工程,名为SelFolder,然后在Workspace窗口的FileView中将SelFolder.rc,SelFolder.h,SelFolderDlg.h,SelFolderDlg.cpp,Resource.h及以Res文件夹都删除,只保留SelFolder.cpp、Stdafx.h和Stdafx.cpp三个文件,现将SelFolder.cpp文件修改如下:#include "stdafx.h"struct CSelFolderApp : public CWinApp
    {
    virtual BOOL InitInstance();
    } theApp;BOOL CSelFolderApp::InitInstance()
    {
    // 要求Windows 95/NT4 或更新版本
    ASSERT(LOBYTE(LOWORD(GetVersion())) >= 4); TCHAR szPath[_MAX_PATH];
    BROWSEINFO bi;
    // 指定父窗口,在对话框显示期间,父窗口将被禁用
    bi.hwndOwner = NULL;
    // 如果指定NULL,就以“桌面”为根
    bi.pidlRoot = NULL;
    // 这一行将显示在对话框的顶端
    bi.lpszTitle =  _T("请选择一个文件夹");
    bi.pszDisplayName = szPath;
    // 只返回文件系统中存在的文件夹
    bi.ulFlags = BIF_RETURNONLYFSDIRS;
    bi.lpfn = NULL; // 回调函数的指针
    bi.lParam = NULL; // 传向回调函数的参数 // 现在,调用函数来显示对话框
    // 它总与Windows的外壳程序Explorer保持相同的外观
    LPITEMIDLIST pItemIDList = SHBrowseForFolder( &bi ); if ( pItemIDList ) // 点按了“确定”按钮
    {
    TCHAR szPath[ _MAX_PATH ];
    if ( SHGetPathFromIDList(pItemIDList, szPath) )
    {
    // 成功地取得了文件夹信息
    CString strMessage;
    strMessage.Format("选定的文件夹是\'%s\'", szPath);
    AfxMessageBox( strMessage );
    }
    // 防止内存泄漏,要使用IMalloc接口
    IMalloc* pMalloc;
    if ( SHGetMalloc( &pMalloc ) != NOERROR )
    {
    // 未返回有效的IMalloc接口指针
    TRACE(_T("无法取得外壳程序的IMalloc接口\n"));
    }
    pMalloc->Free( pItemIDList );
    if ( pMalloc )
    pMalloc->Release();
    }

    // 已完成任务,所以返回FALSE来终止进程
    return FALSE;
    } // InitInstance
      

  2.   

    谢谢,kenny_yuan(程序员),很好的文章。 
    不知有否将这些文章打包?有的话请给我发一份。
    [email protected]
      

  3.   

     如果可以的话,我也想要[email protected]
      

  4.   

    to padda_w:
    才来几天,
    不太懂这里的规矩,
    能否详细说明?