如何在MFC内实现雪花动画(修正补充篇)

本文深入解析了MFC环境下雪花动画的两种实现方法,并着重介绍了一种简化背景显示的方案。通过对比分析,阐述了第一种方案的优点:背景图片与动画结合,失真少;直接创建动画背景,边缘与雪花融合。文章还详细解释了初始化流程和动画原理,包括雪花贴图的透空处理方法,以及如何在控件中应用此动画效果。最后,提供了代码注释,帮助读者理解和实现类似动画。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

       时隔一年了,在MFC内实现雪花飘动效果的设计的更新也停滞了N久,博文《如何在MFC内实现雪花动画》重点讲解了第二种雪花贴图的方式,原理简单易懂广受好评。最近,有读

者反映,第一种实现雪花贴图的方法因为涉及成员数量多,贴图次数多且过程复杂,让人

眼花缭乱。我也只能说,贴图在程序中作为一种高级的操作,原理有时候是只可意会而不

可言传的,必须在掌握基础知识的前提下,在实践中积累经验并最终掌握方法。

      下面,我结合之前CSDN的友人Conry提到过:“没必要用第二个对话框作为背景填充显示。单纯利用dest内存在的背景数据作为支持即可。”将第二种显示动画背景的方案提出,

并根据个人理解,讲解第一种方案内实现动画的原理。


      较第二种方案,第一种方案有2个好处:
1、背景图片拉伸后可以和动画联合在一起,失真少;
2、其创建了直接的动画背景,并且控件边缘和雪花动画融合得天衣无缝。

      第一种方案创造雪花动画,主要和2个函数有关:Init()和Method1()。其中Init()负责进

行拉伸背景的创建和各个贴图成员的初始化,Method1()负责雪花透空处理和动画效果的

产生,最后,通过OnTimer启动动画,并结合OnPaint() 来刷新显示。

      下边讲解一下Init()的原理,
      原先在博文《如何在MFC内实现雪花动画》中,源程序背景显示是通过创建另一个和主动

画窗体CTSnowDlg大小相同,并置于其后的窗体CBkDlg作为虚拟背景载入的。因为有了方

案1,所以得以使用其内的成员CBitmap dest完美地显示背景和动画,只需要做如下改进

即可:

 1、新建一个与背景相对应的画刷成员 CBrush m_bkBrush; 并重载CTSnowDlg::OnCtlColor HBRUSH CTSnowDlg::OnCtlColor(CDC* pDC, CWnd* pWnd, UINT nCtlColor) { if ( nCtlColor == CTLCOLOR_STATIC ) { pDC->SetTextColor(RGB(255,255,255)); pDC->SetBkMode(TRANSPARENT); } if ( nCtlColor == CTLCOLOR_EDIT ) { return CDialog::OnCtlColor(pDC, pWnd, nCtlColor); } return (HBRUSH)(m_bkBrush);// 返回和背景相关的画刷 } 2、重载CTSnowDlg::OnInitDialog BOOL CTSnowDlg::OnInitDialog() { CDialog::OnInitDialog(); …… // 注释掉以下代码 // 背景方式1: // 建立一个无模式对话框 用于作为虚拟背景的对话框,与前对话框融合 // Create 内第二参数必须为CWnd::GetDesktopWindow(),否则无法达到想要的 分层效果 //m_pDlg = new CBkDlg; //m_pDlg->Create(IDD_DIALOG_BK, CWnd::GetDesktopWindow()); //m_pDlg->MoveWindow( 0,0, 800,600 ); //m_pDlg->ShowWindow(SW_SHOW); …… // 背景方式2:必须放在此处进行初始化! m_bkBrush.CreatePatternBrush(&dest); return TRUE; // return TRUE unless you set the focus to a control } 3、重载CTSnowDlg::OnPaint void CTSnowDlg::OnPaint() { if (IsIconic()) { ……按默认 ……不改动 } else { CPaintDC dc(this); if(m_bTimer2) dc.BitBlt(0,0, m_cr.Width(), m_cr.Height(), m_mdc2, 0,0, SRCCOPY); else if(m_bTimer1){ CDC memDC; memDC.CreateCompatibleDC( &dc ); CBitmap *pOld = ( CBitmap* )memDC.SelectObject( &dest ); dc.BitBlt( 0, 0, m_cr.Width(), m_cr.Height(), &memDC, 0, 0, SRCCOPY ); memDC.SelectObject( pOld ); } else{// Show the background without the cartoon. CDC memDC; memDC.CreateCompatibleDC( &dc ); CBitmap *pOld = ( CBitmap* )memDC.SelectObject( &dest ); dc.BitBlt( 0, 0, m_cr.Width(), m_cr.Height(), &memDC, 0, 0, SRCCOPY ); memDC.SelectObject( pOld ); } } }

经过以上代码后,再运行程序,可以看到,在程序初始化后弹出的窗体界面,就直接显示

了雪花背景图,和原先利用双层对话框组合实现雪花动画的背景效果一致,见下图1:

初始化流程图如下(未完成):

详细代码注释如下:

BOOL CTSnowDlg::Init() { // 载入雪花贴片 & 背景 if( !snow.LoadBitmap(IDB_SNOW) || !bg.LoadBitmap(IDB_BG) ){ MessageBox( _T("LoadBitmap()错误!")); return FALSE; } // 取贴片长、宽 BITMAP bm; snow.GetObject( sizeof(BITMAP), &bm ); m_snowSize.cx = bm.bmWidth; m_snowSize.cy = bm.bmHeight; // 制造雪花蒙板mask贴片进行透空屏蔽 if( !mask.CreateBitmap( bm.bmWidth, bm.bmHeight, 1, 1, NULL) ){ MessageBox( _T("CreateBitmap()错误!")); return FALSE; } CDC *pDC = GetDC(); //必须与ReleaseDC搭配使用 CDC maskDC, snowDC; // 创建2个暂存DC maskDC.CreateCompatibleDC( pDC ); // 将maskDC转化成与pDC兼容的DC snowDC.CreateCompatibleDC( pDC ); // 将snowDC转化成与pDC兼容的DC // maskDC内含雪花蒙板mask贴片 CBitmap *pOldMask = ( CBitmap* )maskDC.SelectObject( &mask ); // snowDC内含snow贴片 CBitmap *pOldSnow = ( CBitmap* )snowDC.SelectObject( &snow ); snowDC.SetBkColor( RGB( 255,0,0 ) ); // 设置snowDC的背景色,用于透空 处理 // maskDC内含 mask+snow maskDC.BitBlt( 0,0, bm.bmWidth,bm.bmHeight, &snowDC, 0,0, SRCCOPY ); // 获取背景图片大小 bg.GetObject( sizeof( BITMAP ), &bm ); // 给dest 创建与窗口一致大小的位图数据空间 if( !dest.CreateCompatibleBitmap( pDC, m_cr.Width(), m_cr.Height() ) ){ ReleaseDC( pDC ); return FALSE; } // 给temp 创建与窗口一致大小的位图数据空间 if( !temp.CreateCompatibleBitmap( pDC, m_cr.Width(), m_cr.Height() ) ){ ReleaseDC( pDC ); return FALSE; } // 经过以上2行,dest与temp内位图数据空间大小与窗体大小一致 // 但必须注意,maskDC内的bg(含背景位图数据)大小也许和窗体大小不一致,需要进 行拉伸处理 snowDC.SelectObject( &temp ); // snowDC内含 snow+temp maskDC.SelectObject( &bg ); // maskDC内含 mask+snow+bg snowDC.SetStretchBltMode( HALFTONE ); // 设置拉伸贴图模式,使得拉伸图 像不失真 // 把maskDC的内容拉伸处理并载入snowDC,snowDC内含 snow+mask+temp+bg snowDC.StretchBlt( 0,0, m_cr.Width(),m_cr.Height(), &maskDC, 0,0, bm.bmWidth,bm.bmHeight, SRCCOPY ); // ************************** 如果换成下边2行中任意一行,会怎么样呢? ***********************************// //snowDC.StretchBlt( 0,0, m_cr.Width(),m_cr.Height(), &maskDC, 0,0, m_cr.Width(),m_cr.Height(), SRCCOPY ); //snowDC.StretchBlt( 0,0, bm.bmWidth,bm.bmHeight, &maskDC, 0,0, bm.bmWidth,bm.bmHeight, SRCCOPY ); // ****************************************************************************** ***********************// // maskDC内含 mask+snow+temp+bg+dest maskDC.SelectObject( &dest ); maskDC.BitBlt( 0,0, m_cr.Width(),m_cr.Height(), &snowDC, 0,0, SRCCOPY ); // 选回旧对象指针,用于释放资源 maskDC.SelectObject( pOldMask ); snowDC.SelectObject( pOldSnow ); ReleaseDC( pDC ); //必须与GetDC搭配使用 // 所有都完成,设置标志为TRUE. // 这个时候,雪花未出现,因为初始化的时候SNOW的exist标志位为0 m_bOK = TRUE; return TRUE; }

      代码注释内,有我重点提示的说明语句//********//,读者不妨试试看,就可以了解这些语句的作用了。

      好,明白了初始化的必要步骤后,我们接下来讲解动画原理。Method1()内,不仅实现了

雪花动画,而且还实现了将雪花图片进行透空的处理。


透空流程图如下(未完成):
详细代码注释如下:

void CTSnowDlg::Method1() { // 粒子系统,制造雪花粒子 if( m_count < NUMOFSNOW ){ // NUMOFSNOW = 150 //srand(time(0)); // 随机种子 m_flakes[m_count].x = rand() % m_cr.right;// 随机x坐标下雪 m_flakes[m_count].y = 0; // 顶坐标 m_flakes[m_count].exist = TRUE; m_count++; } // 用两级的MemDC制造下雪图 CDC memDC1, memDC2; CBitmap *pOld1, *pOld2; CDC *pDC = GetDC(); memDC1.CreateCompatibleDC( pDC ); memDC2.CreateCompatibleDC( pDC ); pOld1 = ( CBitmap* )memDC1.SelectObject( &dest );// 含背景+雪花+蒙版 pOld2 = ( CBitmap* )memDC2.SelectObject( &temp );// 一片黑透明 // memDC2 作为第一级缓冲 // memDC1 作为第二级缓冲 memDC1.BitBlt( 0, 0, m_cr.Width(), m_cr.Height(), &memDC2, 0, 0, SRCCOPY ); // 以下透空雪花图片背景进行贴图处理 memDC2.SelectObject( &snow ); // 选入雪花图片 for( int i = 0; i < NUMOFSNOW; ++i ){ if( m_flakes[i].exist ){ // 雪花*背景 做 异或 运算 memDC1.BitBlt( m_flakes[i].x, m_flakes[i].y, m_snowSize.cx, m_snowSize.cy, &memDC2, 0, 0, SRCINVERT ); memDC2.SelectObject( &mask );// 选入蒙版图片 // 雪花*背景*蒙版 做 与 运算 memDC1.BitBlt( m_flakes[i].x, m_flakes[i].y, m_snowSize.cx, m_snowSize.cy, &memDC2, 0, 0, SRCAND ); memDC2.SelectObject( &snow );// 再次选入雪花 // 做 异或 运算还原其余部分 memDC1.BitBlt( m_flakes[i].x, m_flakes[i].y, m_snowSize.cx, m_snowSize.cy, &memDC2, 0, 0, SRCINVERT ); /**/ // 飘.啊.飘 //srand(time(0)); // 随机种子 if( ( rand() & 1 ) == 0 ) // 设定雪花左右摆动的幅度 ,不宜过大 m_flakes[i].x += rand() % 5; else m_flakes[i].x -= rand() % 5; m_flakes[i].y += 5; // 设定雪花往下漂移的幅度 if( m_flakes[i].y > m_cr.bottom ){ // 到底了, 从头再来 m_flakes[i].x = rand() % m_cr.right; m_flakes[i].y = 0; } } } // 释放资源 memDC1.SelectObject( pOld1 ); memDC2.SelectObject( pOld2 ); ReleaseDC( pDC ); }

      以上流程和方法,同样可以扩展延伸到控件方面和动画雪花相互融合的场合,读者在运行

本程序的时候,一定发现了方案1和方案2中雪花在左上角的皇冠中飘落时候的差别,见下

图2:

      对,这就是Method1()的魅力之二。同理子类化控件,构成类TransparentImage,void TransparentImage::OnPaint()内的代码结构,即其透空原理和void

CTSnowDlg::Method1()一致。
      所不同的是,void CTSnowDlg::Method1()内memDC1作用等同于void

TransparentImage::OnPaint()中的destDC;memDC2的兼并了负责载入Mask图 和 image图的功能,需要多次SelectObject,而void TransparentImage::OnPaint()中,则是区分了蒙版图和image图,分别用不同的DC变量载入,经过一系列复杂的内存贴图处理,最后通过BitBlt绘制到真正的现实设备CPaintDC dc上显示出来。这样一来,我们只是看到了最后的效果,变化的过程看不到,闪烁自然就没了。

      另外一个值得注意的地方在于,为了使控件、雪花、主对话框背景能紧密结合,必须在其

内贴图缓冲的时候通过以下代码取得主对话框的目标位图的指针pBmp,并且,为了使得透

空贴图位置更准确,必须获取控件坐标位置并转换成相对于主对话框的(为什么?读者可

以自己先思考),否则,当控件位置改变的时候,其透空会失败:

CTSnowDlg *pDlg = ( CTSnowDlg* )GetParent();// 取得主对话框的指针pDlg CDC tempDC; tempDC.CreateCompatibleDC( &dc ); CBitmap *pBmp = &pDlg->dest; // 取得主对话框的目标位图 的指针pBmp CBitmap *pOldTemp = ( CBitmap * )tempDC.SelectObject( pBmp ); CRect temp; //******************* 注意,下边2句不等效于:GetClientRect(&temp)! *******************// GetWindowRect( &temp ); pDlg->ScreenToClient( &temp ); // 转换成相对于对话框的坐 标 //**************************************************************************** ********//


      至此,MFC内实现雪花动画的原理讲解完成,如有任何疑问的地方,欢迎各位在我的博文

内提出。如哪位有更巧妙的方法或建议,也可以以邮件的方式和本人一起讨论,以便不断

改进和创新。

主要是模拟初中的物理实验的有源代码,可供初学者使用! 采用 MFC 编制 MVC 模式之球体演示程序 作者:haykey 下载源代码   在传统面向过程的程序设计中,往往采用 Input-Processing-Output 模式,这“归功”于 DOS 操作系统的单任务。当 Windows 图形界面 OS 出现后,MVC(Model-View-Controller)模型更适合 Windows 图形界面程序的设计,它将数据处理和数据显示分离开,使维护,扩展系统更加灵活 。其中,View:负责 显示数据,它从Model处获得数据然后显示。当然,一个Model会有用户可从不同角度来观察的多个View。Model:存储数据以及对数据进行各种运算和处理 。Controller:负责接受用户输入,并且把用户输入转换成对 Model 的操作。因此Controller 可能会修改 Model 的数据,当数据修改后,更新 View。其结构示意图如下:   一直采用MFC编程的朋友可能不太熟悉它,这是因为MFC的文档视图结构就是基于MVC的高层结构,这蒙蔽了我们的双眼。虽然MS替我们做了,我们还是有必要接触它,以在SDK or 其他地方有的放矢。我做了一个球体演示的例子,其界面如下:   左侧两个表面积和体积Edit让使用者从文本的角度精确地观察,我们称其为TextView。右侧为从CStatic派生的CGraphicView,使得人们可直观地观察Sphere.对话窗口CMVCSphereDlg是控制器,来获取用户的键盘输入(输入半径后回车)和在Static上的鼠标点击与拖动(可动态调整球体半径并实时反馈球体变化)而CSphere类是模型,存储了球体半径和计算表面积,计算体积等处理半径数据的操作.   现在让我们详细看看代码,来感受下Model,View,Controller之间如何关联,如何协同工作的。 class CSphere { public: ... .... //更新Graphic-VIEW BOOL UpdateGraphicView(HWND hWnd,const CRect &rect,BOOL bErase); //更新Text-VIEW void UpdateTextView(); //外界Controller的接口:设置球体半径 void SetRadius(float r); private: //球体半径 float m_fRadius; //计算球体表面积 float CalculateArea(float radius); //计算球体体积 float CSphere::CalculateVolumn(float radius); };   这里面 UpdateTextView,UpdateTextView 就是当用户输入新半径或拖动鼠标 Controller 捕获后通知 Model,Model 通知两个View更新显示 。具体代码如下: BOOL CSphere::UpdateGraphicView(HWND hWnd,const CRect &rect,BOOL bErase) { //data format examination if(!::IsWindow(hWnd)||::IsRectEmpty(&rect)) { AfxMessageBox("View is not created by now or rect is empty"); return false; } //get the window pointer from window handle CWnd *pView = CWnd::FromHandle(hWnd); if(pView == NULL) return false; //set graphic view''s radius in order to painting ((CGraphicView*)pView)->SetRadius(m_fRadius); bPaintSphere = true;//set paint tag true //repaint if(!::InvalidateRect(hWnd,&rect,bErase)&& !::UpdateWindow(hWnd)) { AfxMessageBox("UpdateView failed"); return true; } pView = NULL; return false; } void CSphere::UpdateTextView() { CMVCSphereDlg *parent = (CMVCSphereDlg *)AfxGetMainWnd(); CWnd *wnd1 = parent->GetDlgItem(IDC_SURFACE); CWnd *wnd2 = parent->GetDlgItem(IDC_VOLUMN); CString str; str.Format("%.2f平方米",CalculateArea(m_fRadius)); wnd1->SetWindowText(str); str.Empty(); str.Format("%.2f立方米",CalculateVolumn(m_fRadius)); wnd2->SetWindowText(str); } CGraphicView中绘图关键代码如下: void CGraphicView::OnPaint() { ... ..... if(!bPaintSphere) dc.DrawText("球体演示",rect,DT_VCENTER|DT_CENTER|DT_SINGLELINE); else { int r=(int)m_radius;//半径取整 CPoint MiddlePoint = rect.CenterPoint();//以矩形框的中心为球心 int x=MiddlePoint.x; int y=MiddlePoint.y; oldpen = (CPen*)dc.SelectObject(&solid_pen); oldbru = (CBrush*)dc.SelectObject(&brush); dc.Ellipse(x-r,y-r,x+r,y+r); //先画一个圆形 dc.SelectObject(&dash_pen); dc.Arc(x-r/2,y-r,x+r/2,y+r,x,y-r,x,y+r); //再画4个半圆弧 dc.Arc(x-r/2,y-r,x+r/2,y+r,x,y+r,x,y-r); dc.Arc(x-r,y-r/2,x+r,y+r/2,x-r,y,x+r,y); dc.Arc(x-r,y-r/2,x+r,y+r/2,x+r,y,x-r,y); ... ... } } 关于控制器CMVCSphereDlg响应用户输入半径回车核心代码如下: BOOL CMVCSphereDlg::PreTranslateMessage(MSG* pMsg) { UpdateData(); //violation examination if(m_r100) { AfxMessageBox("半径输入范围(0---100)"); return false; } if(pMsg->message == WM_KEYDOWN) if(pMsg->wParam == VK_RETURN)//回车 { CRect rect; m_GraphicView.GetClientRect(rect); m_Sphere.SetRadius(m_r);//把用户输入转换成对Model的操作 m_Sphere.UpdateTextView();//更新View m_Sphere.UpdateGraphicView(m_GraphicView.GetSafeHwnd(),rect,true);//更新View return true; } ... ... } 响应鼠标拖动核心代码如下: void CMVCSphereDlg::OnMouseMove(UINT nFlags, CPoint point) { CRect rect; m_GraphicView.GetClientRect(rect); CPoint middlepoint = rect.CenterPoint(); //if click on the graphic view if(rect.PtInRect(point)&&bIsDragging) { double dbDistance2 = (point.x-middlepoint.x)*(point.x-middlepoint.x)+(point.y-middlepoint.y)*(point.y-middlepoint.y); double dbDistance = sqrt(dbDistance2); if(dbDistance>100.) dbDistance = 100.; m_r = (float)dbDistance; //update radius edit UpdateData(false); m_Sphere.SetRadius(m_r); m_Sphere.UpdateTextView(); m_Sphere.UpdateGraphicView(m_GraphicView.GetSafeHwnd(),rect,true); } ... ... } 该程序功能简单,只是示例性说明采用 MFC 如何实现MVC模型,就当抛砖引玉了。具体实现参考源代码例子。
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值