因为自己很喜欢那些界面做得很漂亮的软件或者使用各种美化界面的软件,如avedesk,samurize等等。其中美化界面的一个重要的方面就是给窗口添加上阴影。虽然OS X已经原生的支持窗口阴影,但是windows要到Longhorn才开始支持原生的窗口阴影。现在如果想实现窗口阴影,一般都会借助第三方的软件,例如windowFX或者YzShadow。其中YzShadow是一个免费软件,我自己也在使用。但是这个软件有个弱点,就是无法为Layered Window添加阴影。而我自己编写的一个简易便条程序Stickies(功能类似OneNote,但是功能简单,小巧。)就是运用了Layered Window来作为软件的界面,于是便自己尝试添加窗口阴影。以下便是添加阴影的方法,写下来与大家讨论一下。
我的程序是在Visual Studio.NET 2003下编写的MFC应用程序。我为了实现窗口阴影创建了一个Shadow的类。首先我们看看各类之间的关系:
1. Class ShadowCastingWindow 该类是一个应用程序的窗口,它会在桌面上投射下阴影。这个类是从CWnd继承而来。 ShadowCastingWindow成员变量: m_Alpha 保存该窗口的透明度值。
ShadowCastingWindow成员函数: BOOL ShadowCastingWindow::CreateWindow( CString wndName, CWnd * pParentWnd ) { BOOL tmp = CWnd::CreateEx( WS_EX_TOOLWINDOW|WS_EX_LAYERED , … , WS_POPUP|WS_VISIBLE , … ); m_Shadow.CreateShadow( this, m_Alpha ); }
该函数用于创建应用程序的窗口并创建阴影。请留意CreateEx中窗口属性WS_EX_...和WS_...的取值,这使得该应用程序的窗口是一个没有标题栏的Layered Windows。是否有标题栏对于下文Shadow类中求遮挡窗口的大小会有所不同,这必须通过一个判断逻辑或者根据程序的应用不同编写好代码。对于Layered Windows会有两种刷新模式,一种就是传统的消息机制,就是操作系统自动地在适当的时候发送WM_PAINT的消息给应用程序窗口,应用程序窗口则相应该消息,对窗口进行刷新;另一种方式则是在Windows2000以后才支持的UpdateLayeredWindow的机制,在这种机制下,应用程序不再处理WM_PAINT消息,所有的刷新均由用户在内存中的一个绘图上下文中绘制好图像之后再通过UpdateLayeredWindow绘制到屏幕上,只要经过一次绘制,窗口的图像便会保存在一块预订好的内存区域内,如果窗口的图像没有改变那么操作系统便会自动地处理刷新。
void ShadowCastingWindow::OnSizing(UINT fwSide, LPRECT pRect) { CWnd::OnSizing(fwSide, pRect); m_Shadow.OnShadowCastingWndNewSize(pRect->right - pRect->left, pRect->bottom - pRect->top); }
void ShadowCastingWindow::OnSize(UINT nType, int cx, int cy) { CWnd::OnSize(nType, cx, cy); m_Shadow.OnShadowCastingWndNewSize(cx,cy); }
void ShadowCastingWindow::OnMoving(UINT fwSide, LPRECT pRect) { CWnd::OnMoving(fwSide, pRect); m_Shadow.OnShadowCastingWndNewPos(pRect->left, pRect->top ); }
void ShadowCastingWindow::OnMove(int x, int y) { CWnd::OnMove(x, y); m_Shadow.OnShadowCastingWndNewPos(x, y ); }
这四个事件函数都是处理应用程序窗口大小或者位置变化的。只需要在其中调用Shadow类中相应的处理函数即可,Shadow便会自动地更改大小或者移动位置。可能有人会问为什么需要显式地调整Shadow的位置和大小?因为从下文可以看到Shadow其实也是一个Layered Window,没有父窗口,所以操作系统不可以自动地保持两者的相对位置。
void ShadowCastingWindow::SetOpacity( int alpha ) { m_Alpha = alpha; m_Shadow.SetAlpha( alpha ); SetLayeredWindowAttributes( 0, (BYTE)m_Alpha, LWA_ALPHA ); Invalidate(); }
处理应用程序窗口透明度变化的函数,其中调用了阴影Shadow对于透明度变化的处理函数。并刷新应用程序窗口。
2. Class Shadow 该类继承与CWnd类。 Shadow成员变量: CWnd * m_pShadowCastingWindow; 指向父窗口—需要投射阴影的窗口的指针。 int m_Alpha; 当前阴影的透明度。 int m_DeltaTop; int m_DeltaLeft; int m_DeltaRight; int m_DeltaButtom; 用于表示阴影的尺寸。计算方法如下:
Shadow成员函数: BOOL Shadow::CreateShadow(CWnd * pShadowCastingWnd, int alpha ) { //根据投射阴影的窗口的尺寸和各参数计算出阴影的尺寸。 CRect rect; pShadowCastingWnd->GetWindowRect(&rect); rect.top += m_DeltaTop; rect.left -= m_DeltaLeft; rect.right += m_DeltaRight; rect.bottom += m_DeltaButtom; m_Alpha = alpha; BOOL tmp = CWnd::CreateEx( WS_EX_TOOLWINDOW|WS_EX_LAYERED ,… , WS_POPUP|WS_VISIBLE , rect , …);
m_IsCreated = true; CustomizedPaint(); return tmp; }
创建阴影,由于阴影必须是没有标题栏的,而且因为要绘制半透明的像素所以必须使用Layered Window。
void Shadow::CustomizedPaint(void) { if ( !m_IsCreated ) return;
BLENDFUNCTION blendPixelFunction= {AC_SRC_OVER, 0, m_Alpha, AC_SRC_ALPHA}; POINT ptWindowScreenPosition = {rect.left, rect.top}; POINT ptSrc = {0, 0}; SIZE szWindow = {rect.Width(), rect.Height()};
CDC * dcScreen = GetDesktopWindow()->GetDC(); CDC dcMemory; dcMemory.CreateCompatibleDC( dcScreen );
//----------------------------------- //把要绘制的内容绘制在dcMemory里。对于Shadow需要把投射阴影窗口所覆盖的区 //域剪裁掉 //-----------------------------------
UpdateLayeredWindow( dcScreen, &ptWindowScreenPosition, &szWindow, &dcMemory, &ptSrc, 0, &blendPixelFunction, ULW_ALPHA);
GetDesktopWindow()->ReleaseDC(dcScreen); dcMemory.DeleteDC(); } 根据不同程序需要加上适当的绘制流程。比如可以通过画一个长方形来表示阴影,这个效果自然就比较差;也可以利用一些在Photoshop中处理好的阴影图片把它做适当的大小调整作为窗口的阴影这样更容易做出阴影边缘柔化的效果。这个CustomizedPaint只需要在窗口的内容被改变的时候才需要重新调用,其他时候系统会自动管理已经绘制的图像,用它来刷新窗口,而不需要重新绘制。
BOOL Shadow::PreCreateWindow(CREATESTRUCT& cs) { cs.style &= ~WS_BORDER; cs.lpszClass = AfxRegisterWndClass(CS_HREDRAW|CS_VREDRAW|CS_DBLCLKS, ::LoadCursor( NULL, IDC_CURSOR ), NULL, NULL); return CWnd::PreCreateWindow(cs); } 通过修改默认的cs.lpszClass使得窗口不再自动重画背景。
void Shadow::OnShadowCastingWndNewSize( int x, int y ) { if ( !m_IsCreated ) return; SetWindowPos( m_pShadowCastingWindow,0,0,x+m_DeltaLeft+m_DeltaRight,y-m_DeltaTop+m_DeltaButtom, SWP_NOMOVE ); CustomizedPaint(); }
提供该投射阴影的窗口的接口函数,当投射阴影的窗口大小改变的时候便调用这个函数把新的窗口位置传给Shadow,Shadow便会改变自己的大小,并重绘窗口。
void Shadow::OnShadowCastingWndNewPos( int x, int y ) { if ( !m_IsCreated ) return; SetWindowPos( m_pShadowCastingWindow, x-m_DeltaLeft, y+m_DeltaTop, 0, 0, SWP_NOSIZE ); }
提供该投射阴影的窗口的接口函数,当投射阴影的窗口位置改变的时候便调用这个函数把新的窗口位置传给Shadow,Shadow便会改变自己的位置。注意了,这里并不需要重绘窗口,因为窗口的内容并没有改变。
void Shadow::SetAlpha( int alpha ) { m_Alpha = alpha; CustomizedPaint(); }
提供该投射阴影的窗口的接口函数,当投射阴影的窗口的透明度改变的时候便调用这个函数把新透明度传给Shadow,Shadow便会改变自己的透明度,并重绘窗口。
下面给出一个我自己写的程序的效果图:
3. 结论:
上面就简单地介绍了一个绘制窗口阴影的方法。这种方法基本上可以适用于各种类型的窗口,其中需要注意一下几点:
1. 在于Shadow::CreateShadow中如何正确取得投射阴影窗口m_pShadowCastingWindow的大小然后计算出阴影窗口的大小。
2. Shadow::CustomizedPaint中如何更高效的绘制阴影,例如剪裁掉投射阴影窗口遮挡住的窗口内容,避免绘制时出现闪烁。同时如何正确使用好UpdateLayeredWindow这个系统调用会是实现绘制阴影的关键。当然在当前的设计下,我们可以在CustomizePaint中绘制任何的东西,而不一定是阴影。? 大家可以在这里发挥想象力,让窗口更加绚丽多彩。
其实这个程序只要让他通过钩子函数与特定的Win32API挂钩,完全可以写出一个可以给系统中所有窗口加上阴影效果的小软件。大家不妨试试。如果做出来了,记得给我一份。
注:文章中的代码都是示意性的,都是通过我自己写的程序删减后得到,未必能通过测试。旨在说明一些关键步骤需要注意的地方。如果问题欢迎email讨论。
|