用Visual C++从位图文件生成任意形状的窗口 [转 - dingdang的维客笔记 ...

来源:百度文库 编辑:神马文学网 时间:2024/04/28 12:54:02
有许多的软件的界面十分地漂亮,不仅窗口的客户区绘制得十分精细,连窗口的外形也是“奇形怪状”的,比如   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;  
  }