最近在研究如何在MFC内实现雪花飘落的动画,在各路高手的帮忙指引下,本人成功创建出了这种效果,现在就和大家分享一下。 通过这个实例,大家可以学习到有关透空图的技巧,双缓冲贴图防闪烁的技巧以及叠层窗体的应用等等,大家可以点击这个链接来看看我的原帖:
http://topic.csdn.net/u/20090724/12/49f59334-1a4f-4022-b37f-69b786d4ac63.html
此工程在Windows XP,VC6下测试通过。
下载地址:在VC/MFC内实现雪花动画(请点此链接)http://download.csdn.net/source/1545169
要实现雪花飘落,首先雪花的素材要有,搜索了一下,找到一些不错的雪花形状的BMP位图,但由于是非规则的形状,所以需要透空处理,才可以贴至背景并且与其融合在一起,实现想要的效果。
1.如何制作透空图
下面,我来介绍一种最简单实用的透空图的技巧。
如图所示,
大家可以看见位图分左右部分,各有15朵雪花,
左半边是原图,而右半边则是雪花的屏蔽图,他们和任意的图片通过BitBlt贴图可以实现相互融合!
以第一朵雪花为例子,见带红色框图的雪花。
我给2个框图的雪花划分4个区域
原图框:
A-黑色(RGB(0,0,0))
B-任意颜色或混合色
屏蔽图:
C-白色(RGB(255,255,255))
D-黑色(RGB(0,0,0))
这里可以认为雪花是蓝色的,对其进行透空使其在某个背景图片内呈现异型,有如下步骤:
1)将雪花有半边的屏蔽图和背景图片作AND运算
0000……C 黑
& 1010……背景图片任意色
-> 0000……黑色(结果)
1111……D 白
& 1010……背景图片任意色
-> 1010……背景图片任意色,不改变(结果)
请看,雪花在背景图片上变成黑炭了!
2)将雪花图的左半边图片和前一个有黑炭的背景图片作OR运算
0001……B雪花图片蓝色
| 0000……背景与雪花形状对应的黑色区域
-> 0001……还是雪花图片的蓝色(结果)
0000……A 黑
| 1010……背景图片任意色
-> 1010……背景图片任意色,不改变
很简单吧〉?大家利用这招,可以把任意形状任意的小冬冬贴到背景上去,把某副BMP位图去除不要的区域,留下可用的异形区域,最后贴到对话框背景处和背景融为一体。 实现一体化!
当然,制造这种带屏蔽图的图片是要费些力的,所以需要你懂得一些PhotoShop的常用技巧。在工程实例内的snow2.bmp文件,有15种不同 的雪花图案,只要在选用BitBlt进行贴图的时候,设定恰当的坐标来选定对应的元雪花图和对应的屏蔽图,即可实现任意雪花图案漂落。
2. 如何防止闪烁
我们的关键还是要考虑如何防闪烁,如何实现飘落的动画,下边,结合我的帖子问题谈谈如何防闪烁,而且整个动画界面的背景看起来够美观。
大家都知道,双缓冲可以防止窗体在刷新的时候闪烁,可是,如何处理才是双缓冲?普通刷新的时候,闪烁的原因在哪里?本人在工程内,用上了双缓冲的方法,这个在帖子里边也以代码的形式告诉了大家。
简单地说,导致闪烁的原因,是因为在处理贴图的时候,贴图的每一步都直接通过窗口DC显示到了屏幕上边,当刷新用上Invaliate的时候,背景的白刷就出现了,图片-白刷-图片……如此循环,导致闪烁出现。
而后,因为刷新的时候,控件区域的背景需要刷新,但是贴图的双缓冲又顾及不到,所以显示动画的时候,会导致微微地闪烁……
可以给窗体添加如下风格防止控件区域背景的刷新。
this->ModifyStyle(0, WS_SYSMENU|WS_CLIPCHILDREN);
闪烁没了,控件背景全被透空了,很是难看,如图:
实现动画的图内,左上角有1个CPicture控件,其下方有一个CStatic和一个CEdit控件,图片主角枪口的地方,有个小人形状的图标和一个放大镜的图标,那是大名鼎鼎的CButtonST类的按钮实现的贴图,但是都成了这个样子,像补丁一样真难看……
各位到了这一步怎么处理呢?
我想有些人可能会想到自己去写控件类,用BitBlt实现区域截取背景位图数据;开始我也是这么想的,但代码量估计不少,不划算。
把动画的代码写入OnEraseBkgnd吧,这是我最原来的做法,虽然没了闪烁,但是控件区域也没了。
后来,我尝试将动画显示到OnPaint内,而通过OnEraseBkgnd将背景放入窗体内,可是,这样一来,造成的闪烁比原来还厉害……真晕了
所以,以上的方法都不通!!!
后来我认真思考了一下,发现默认的控件背景刷新原理就是通过截屏的操作实现的,不如把对话框内子窗体的背景都透明化,然后想办法在其后显示一个和主界面相同位置相同大小相同内容的位图,不就成了?
于是,我尝试使用了窗体联动的方式去实现。
按我的想法,我首先重载了对话框的OnCtrlColor,按如下代码进行操作:
HBRUSH CTSnowDlg::OnCtlColor(CDC* pDC, CWnd* pWnd, UINT nCtlColor)
{
if(::GetForegroundWindow() != m_hWnd )
{
pDC->SetBkMode(TRANSPARENT); //设置窗体透明
return (HBRUSH)GetStockObject(NULL_BRUSH); //必须返回一个NULL画刷
}
if ( nCtlColor == CTLCOLOR_STATIC ) //处理所有的静态文本控件
{
pDC->SetTextColor(RGB(255,255,255));
pDC->SetBkMode(TRANSPARENT);
return (HBRUSH)GetStockObject(NULL_BRUSH);
}
if ( nCtlColor == CTLCOLOR_EDIT ) //处理所有的编辑框控件
{
if(GetFocus() == NULL);
else
{
HBRUSH hbr = CDialog::OnCtlColor(pDC, pWnd, nCtlColor);
if(pWnd->m_hWnd == GetFocus()->m_hWnd) return hbr;
}
}
HBRUSH hbr = CDialog::OnCtlColor(pDC, pWnd, nCtlColor);
return hbr;
}
其次,添加一个新的对话框资源,其类名为CBkDlg,重载其内的OnWindowPosChanged 和 OnEraseBkgnd 2个函数。
目的是为了使得这个对话框时刻处于主界面之下,供控件区域进行刷新截屏,代码如下:
BOOL CBkDlg::OnEraseBkgnd(CDC* pDC)
{
CRect rectbk; // 客户区的大小
CDC memDC; // 内存设备描述表
CBitmap cBitmap; // 位图对象
CBitmap* pOldMemBmp=NULL;
BITMAP stBitmap;
// 得到窗口区域
GetClientRect(&rectbk);
// 1.构造内存设备环境,并将位图装入
memDC.CreateCompatibleDC(pDC);
// 2.加载背景位图,一般把cBitmap声明为对话框成员变量,在构造函数内只进行一次LoadBitmap提高效率
cBitmap.LoadBitmap(IDB_BITMAP2);
pOldMemBmp = memDC.SelectObject(&cBitmap);
// Get the cBitmap's size.
cBitmap.GetBitmap(&stBitmap);
int stBMPwidth = stBitmap.bmWidth;
int stBMPHeigth = stBitmap.bmHeight;
// 将背景位图复制到窗口客户区
pDC->SetStretchBltMode( HALFTONE );
pDC->StretchBlt(0, 0, rectbk.Width(), rectbk.Height(), &memDC, 0, 0,
stBMPwidth, stBMPHeigth, SRCCOPY);
// 绘图完成后的清理
memDC.SelectObject(pOldMemBmp);
return true;
}
void CBkDlg::OnWindowPosChanged(WINDOWPOS FAR* lpwndpos)
{
CDialog::OnWindowPosChanged(lpwndpos);
// 保证主界面永远处于CBkDlg的前一层
if ( ((CMyApp *)AfxGetApp())->GetMainWnd()->GetSafeHwnd() )
( (CMyApp *)AfxGetApp())->GetMainWnd()->SetWindowPos(&wndTop,
0,0, 0,0,
SWP_NOMOVE|SWP_NOSIZE|SWP_NOACTIVATE);
}
void CBkDlg::PostNcDestroy()
{
// 别忘了,窗体是new出来的,在此销毁!
delete this;
CDialog::PostNcDestroy();
}
但是,如果主界面拖动了那么怎么办呢?对策就是,重载主界面的OnMove,当这个背景窗口创建出来后,随主界面移动而移动,这样,就不会出现交错的情况发生,看起来就像只有一个窗口一样。
这个背景窗体对话框,可以是在另外一个线程内开启,也可以在主界面的OnInitDialog内创建,但注意,这个窗体最好是无模式对话框,而且Create函数内的第二参数不能是NULL 或者 this。为什么呢?这个读者动动脑筋思考一下就知道了,或者自己去修改工程代码看看会有什么情况发生。
做好以上事情后,再运行一下程序看看,效果如图:
如何,很不错吧?
当然,把雪花动画画在某个无控件窗体内,在其上盖上一个被透明的只有控件可见的窗体,也能实现这个效果,但必须要先用SetWindowLong将前景带控件的窗体设置成层级窗体,显示成功后再用UpdateLayeredWindow函数进行透明处理。
3.如何实现雪花分散漂落
好了,基本效果处理都差不多讲完了,现在来说说如何实现雪花飘落的效果吧。
首先,定义一个结构体:
typedef struct tagSNOW { // 定义粒子系统(雪花粒子)
int x; // pt( Top, Left)
int y; // 两个坐标
BOOL exist; // 可见.
CBitmap* pBitmap; // 可开辟多个雪花位图对象的空间
// note : 完整的粒子应该还包括诸如:角度、
// 大小( 决定重量和速度 )、形状等等,
// 当然,为求简化,都被省略掉了。
} SNOW, *pSNOW;
声明5个和雪花贴图相关的CBitmap成员对象:
public:
// 定义背景,屏蔽图,雪花图,目标图和 缓冲图 的CBitmap对象
CBitmap bg, mask, snow, dest, temp; // 5个CBitmap
SNOW m_flakes[NUMOFSNOW]; // 雪花粒,NUMOFSNOW为雪花颗粒个数
如果有谁做过粒子碰撞之类的算法,我想实现这种效果是轻而易举的事情。
然后,处理OnTimer内的消息,关键在于每次贴图的时候,随机从窗口顶部(x,0)的位置产生N朵雪花,雪花的位置由m_flakes的的x,y坐标进行记录。
每次响应定时器事件触发的时候记录下雪花贴图和背景融合的位图数据,每次定时器到,就出现NUMOFSNOW朵雪花,雪花直接位置的错开由结构体内的x,y变量进行实现,如下代码:
// 飘.啊.飘
if( ( rand() & 1 ) == 0 )
flakes[i].x += 3;
else
flakes[i].x -= 3;
flakes[i].y += 10;
if( flakes[i].y > cr.bottom ){ // 到底了, 再来
flakes[i].x = rand() % cr.right;
flakes[i].y = 0;
}
下一次再产生的时候,便将上一次的位图和新的雪花贴在一起,便成了雪花飘落的动画。详细可见工程的代码,本人在这里仅仅提个思路。
大家下载例子代码后,可以看到主对话框内有2个函数:
void Method1(); //第一种透空方法
void Method2(); //第二种透空方法
第一种即为本人使用的透空图技巧,第二种透空的方法由一位CSDN上名为lambochan的朋友提供,同时他也是防闪烁的方法提供者,在此本人先对他表示感谢。大家可以参照使用,详细代码有注释。
有位名为softist的朋友,也推荐了一种防闪烁的方法:
每次都用大幅的图片,就像放电影的原理一样,不断地贴,这种方法,代码简单量少,只要动画的位图足够多,再加上个定时器配合即可。但缺点在于,对不同的背景,需要自己去PS,而且,要形成比较像样的雪花动画,我估计要40张位图,这样一来,位图如果作为资源加入工程,那么编译得到的exe文件就会很大,即使放在磁盘内,初始化load入一个CBitmap数组内,也要占据不少的内存。
所以,应根据场合而用,在此,也感谢这位朋友提供的方法。
最后,运行起来,漂落的效果如图所示:
界面下边有2个未修饰的按钮,用于显示不同的雪花类型,有兴趣的朋友可以据此修改原工程的代码,做到雪花的变
动,甚至可以产生雪花飘落时旋转的效果哦!
当然,此例子还有些待完善的地方,如启动程序后,在任务栏留下的标题,如图:
空白的标题是没必要显示的,可以通过改变窗体风格实现。当你最小化动画窗体后,或者改变窗体大小,就会发现控件背景不协调的地方再次出现了,但都没关系,处理一下WM_SIZE就可以。至于CButtonST修饰的按钮,因为其内部绘图机制的决定,所以空间窗口背景的矩形区域会遮挡雪花,但没关系,也是有办法处理的,每次定时器事件触发后,都进行一次按钮初始化即可。
好了,此次雪花飘落的动画制作就讲到这,希望对大家有一定参考价值和启发作用。
如果哪位朋友有疑问的,或是有什么新建议的,欢迎给本人留言!