1、1第 10 章 GDI 编程 3-动画动画是利用人的视觉滞留缺陷 (25ms400ms)和心理认可来动态生成系列相关画面以产生运动视觉的技术。位图动画是将预先制作好的一系列表示连续画面的位图,按一定的时间间隔一幅接一幅地连续显示,从而产生动画效果。因为绘制动画所需的图形,以及拍摄和处理图片,需要美术、摄影、数字图像处理、动画设计等知识,我们这里不讲。本书只介绍如何显示已有的位图(序列)以产生动画效果,以及如何动态绘制不同的简单图形以产生二维图形动画等。用 GDI 编程实现动画,一般需要用到计时器(Timer)操作,通常在计时器响应函数 OnTimer 中(而不要使用OnDraw)绘图来实现动画
2、。10.1 固定位图动画本节介绍利用一系列的位图资源,在同一个屏幕位置,接连显示位图序列,以达到动画的效果的具体方法。为此,可在交互绘图程序中添加一个如图 10-1 所示的位图动画对话框,并添加对应的对话框类 CDukeDlg。也可以创建一个基于对话框的独立的 MFC 应用程序。图 10-1 位图动画对话框资源当然还需添加相应的“位图动画”菜单项(ID_DUKE)和(为视图类添加)对应的菜单响应函数,并在该函数中创建对话框类的对象,打开对话框来运行动画:#include “DukeDlg.h“void CDrawView:OnDuke() CDukeDlg dlg;dlg.DoModal();
3、210.1.1 准备位图、加入位图资源系列公爵(Duke)BMP 文件 T1.BMP T10.BMP(见图 10-2) ,来自 Java 吉祥物的 GIF 动画文件,可存放在项目的 res 子目录的 Duke 子目录中(该位图资源已经打包成 Duke.rar 文件后,放到了系里的网站和我的个人网页上) 。T1.BMP T2.BMP T3.BMP T4.BMP T5.BMPT6.BMP T7.BMP T8.BMP T9.BMP T10.BMP图 10-2 Duke 位图文件用 VC 的资源编辑器依次加入位图文件:在左边的项目工作区中选“资源视图”页,展开项目的资源列表,在“Bitmap”表项(若
4、无此项,则可直接在项目资源项)上单击鼠标右键,在弹出的浮动菜单中选 “添加资源”菜单项,在打开的“添加资源”对话框中,选中左边“资源类型”栏中的“Bitmap ”表项,单击右边的“导入”按钮,在弹出的“导入”文件对话框中,定位 Duke 目录,选中所有 Ti.BMP 后按“打开”钮,则会自动加入 ID为 IDB_BITMAPi 的位图资源。为了以后循环编程的方便,必须保证是从 T1.BMP 到 T10.BMP 顺序依次加入。为了确认,可打开头文件Resource.h 查看,若其中的常量 IDB_BITMAPi 的定义数值不连续,可作一些手工修改使其连续,例如:#define IDB_BITMA
5、P1 131#define IDB_BITMAP2 132#define IDB_BITMAP3 133#define IDB_BITMAP4 134#define IDB_BITMAP5 135#define IDB_BITMAP6 136#define IDB_BITMAP7 137#define IDB_BITMAP8 138#define IDB_BITMAP9 139#define IDB_BITMAP10 140310.1.2 添加控件、创建对话框类为对话框资源添加图片控件:打开对话框资源,在控件工具箱中选图片控件(Picture Control)工具,在对话框的适当位置添加图片控
6、件,设置其“ID”属性值为“IDC_ANI” ,修改“Type”属性为(在其下拉式列表框中选中) “Bitmap”,再在“Image”属性的下拉式列表框中选中“IDB_BITMAP1”位图资源,则该位图绘显示在图片控件中。为了控制动画的播放,需要添加一个即可表示开始动画又可表示停止动画的按钮(初始标题为“开始动画” ) ,可设置其 ID 为 “IDC_ANI_START_STOP”。为了让用户选择动画的速度,可以添加静态文本提示框“每秒帧数:”和文本编辑框(IDC_N ) ,在后面的10.1.6 小节中还会添加滑块控件(Slider Control,IDC_SLIDER_N) 。创建该对话框资
7、源所对应的对话框类 CDukeDlg。10.1.3 添加类变量、装入与删除位图在对话框类的定义(头文件)中添加若干类变量:CBitmap *m_pBmp10; / 位图指针数组BITMAP m_bs; / 位图结构变量bool m_bStarted; / 判别动画是否启动(初始化为 false)int m_nCurFrame, / 当前帧号(初值为 0)m_nFramesPerSecond; / 每秒帧数(初值为 10)为 CDukeDlg 类添加(重写型)消息响应函数 OnInitDialog,在该函数中(也可以在构造函数中)创建位图对象并装入位图资源,然后获取位图结构(其中的位图宽和高用于
8、 BitBlt 函数) ,并初始化其他类变量,最后设置编辑控件的初值(粗体部分为新加的):BOOL CDukeDlg:OnInitDialog()CDialog:OnInitDialog();/ TODO: 在此添加额外的初始化for (int i = 0; i LoadBitmap(IDB_BITMAP1 + i);m_pBmp0-GetBitmap( /获取位图结构4m_bStarted = false; / 设置已开始动画为假m_nCurFrame = 0; / 设置初始的当前帧为 0m_nFramesPerSecond = 10; / 设置初始动画速度为每秒 10 帧SetDlgIte
9、mInt(IDC_N, m_nFramesPerSecond); / 设置编辑框初值return TRUE; / return TRUE unless you set the focus to a control/ 异常: OCX 属性页应返回 FALSE其中,语句 m_pBmpi-LoadBitmap(IDB_BITMAP1 + i);中的 IDB_BITMAP1 + i 用到了 Duke 位图资源 ID 的连续性。为了避免内存泄漏,还需要为对话框类添加析构函数,并在该函数中删除位图对象:CDukeDlg:CDukeDlg()for (int i = 0; i 100) / 最大帧速值为 1
10、00m_nFramesPerSecond = 100;/ 用调整后帧速值重新设置编辑框的内容SetDlgItemInt(IDC_N, m_nFramesPerSecond);/ 利用帧速值来设置计时器SetTimer(1, (UINT)(1000.0/m_nFramesPerSecond + 0.5), NULL);/ 设置按钮文本为“停止动画”SetDlgItemText(IDC_ANI_START_STOP, L“停止动画“);其中: m_bStarted 为布尔型类变量,用于判断动画是否已经开始播放。在构造函数中初始化为假,在用户按了开始/停止动画按钮后取反。 m_nCurFrame 为
11、整数型类变量,用于记录当前所要显示的位图序号,初始化为 0。 m_nFramesPerSecond 也为整数型类变量,用于记录当前的每秒帧数(帧速值,范围可设为 1100) 。 SetDlgItemText 函数,用于动态修改按钮上的文本。 SetTimer 用于设置计时器,它是 CWnd 的成员函数(所以也可在其派生的视图类和对话框类中使用) ,其函数原型为:UINT SetTimer( UINT nIDEvent, UINT nElapse, void (CALLBACK EXPORT* lpfnTimer)(HWND, UINT, UINT, DWORD) ); nIDEvent 为此计
12、时器的编号。因为一个应用程序可以设置多个计时器,为了在响应时区分它们,必须各有一个编号。简单程序的计时器一般只有一个,所以取 nIDEvent = 1 即可。 nElapse 为间隔时间,单位为毫秒(1/1000 秒) 。可用表达式(UINT) (1000.0 / m_nFramesPerSecond + 0.5),从每秒帧数计算出间隔时间的毫秒数。 lpfnTimer 为应用程序提供的处理 WM_TIMER 消息的回调函数,一般取为 NULL,这时WM_TIMER 消息由 CWnd 派生类的对应消息响应函数来处理。 KillTimer 用于删除计时器,它也是 CWnd 的成员函数,其函数原型
13、为:BOOL KillTimer( int nIDEvent );在设置了计时器后,系统会按指定的时间间隔发送 WM_TIMER 消息给应用程序窗口,程序员可在计时器消息响应函数 OnTimer 中作需要的处理,在本程序中是显示当前位图。610.1.5 绘制动画(响应计时器消息)为 CDukeDlg 类的 WM_TIMER 消息添加响应函数:void CDukeDlg:OnTimer(UINT nIDEvent) / TODO: 在此添加消息处理程序代码和 /或调用默认值CDC *pDC = GetDlgItem(IDC_ANI)-GetDC(); / 获取图片框 DCCDC dc; / 定义
14、内存 DCdc.CreateCompatibleDC(pDC); / 创建兼容 DCdc.SelectObject(m_pBmpm_nCurFrame); / 选入当前帧位图pDC-BitBlt(0, 0, m_bs.bmWidth, m_bs.bmHeight, / 显示当前帧位图m_nCurFrame+; / 当前帧加一(设置下一次要显示的位图序号)m_nCurFrame %= 10; / 当前帧余 10(避免超出位图数组,实现循环)CDialog:OnTimer(nIDEvent);结果如图 10-3 所示。图 10-3 Duke 动画10.1.6 滑块控件的使用为了使用户能够利用鼠标快
15、速修改每秒帧数的值,我们在对话框中添加了一个滑块控件(Slider Control) (可将其 ID 值设为 “IDC_SLIDER_N”) 。对应的 MFC 类为 CSliderCtrl,它是直接从 CWnd 派生的类:CObjectCCmdTargetCWnd CSliderCtrl。可以在对话框类中定义一个 CSliderCtrl 的类指针变量:CSliderCtrl *m_pSlider;然后在对话框类的初始化函数中获得滑块控件对象的指针,并设置其取值范围为 1100,再设置其滑块的当前位置(注意:粗体代码必需位于 SetDlgItemInt 之前):BOOL CDukeDlg:OnI
16、nitDialog() 7CDialog:OnInitDialog();/ TODO: 在此添加额外的初始化m_pSlider = (CSliderCtrl *)GetDlgItem(IDC_SLIDER_N);m_pSlider-SetRange(1, 100);m_pSlider-SetPos(m_nFramesPerSecond);SetDlgItemInt(IDC_N, m_nFramesPerSecond);为了能在用户移动滑块时,动态修改编辑控件中的值,需要为对话框类添加水平滚动消息(WM_HSCROLL)的响应函数 OnHScroll:void CDukeDlg:OnHScrol
17、l(UINT nSBCode, UINT nPos, CScrollBar* pScrollBar) / TODO: 在此添加消息处理程序代码和 /或调用默认值SetDlgItemInt(IDC_N, m_pSlider-GetPos();CDialog:OnHScroll(nSBCode, nPos, pScrollBar);同样,为了在用户修改编辑控件中的值时,动态改变滑块的位置,还需要为对话框类添加编辑控件的EN_CHANGE 消息响应函数:void CDukeDlg:OnEnChangeN() / TODO: 在此添加控件通知处理程序代码m_pSlider-SetPos(GetDlgI
18、temInt(IDC_N);注意,如果滑块初始化的代码位于 SetDlgItemInt 之后,则会造成此语句中的 m_pSlider 指针无效。10.1.7 CImageList类上面的位图动画,也可以使用 MFC 的图像列表类 CImageList 来实现,代码会更简单一些。CImageList 类是CObject 的直接派生类:CObject CImageList81成员函数CImageList 类的常用成员函数有: 构造函数:CImageList( ); 创建函数:BOOL Create( int cx, int cy, UINT nFlags, int nInitial, int nG
19、row ); 添加函数:int Add( CBitmap* pbmImage, COLORREF crMask ); 绘制函数:BOOL Draw( CDC* pDC, int nImage, POINT pt, UINT nStyle );2例子可在动画对话框类中添加如下代码:/ 类变量(头文件)bool m_bStarted;int m_nCurFrame, m_nFramesPerSecond;CSliderCtrl *m_pSlider;CImageList imgList;/ 初始化( OnInitDialog 函数)m_bStarted = false;m_nCurFrame =
20、0;m_nFramesPerSecond = 10; / 设置初始动画速度为每秒 10 帧m_pSlider = (CSliderCtrl *)GetDlgItem(IDC_SLIDER_N);m_pSlider-SetRange(1, 100);m_pSlider-SetPos(m_nFramesPerSecond);SetDlgItemInt(IDC_N, m_nFramesPerSecond); BITMAP bs;CBitmap bmp;bmp.LoadBitmap(IDB_BITMAP1);bmp.GetBitmap(imgList.Create(bs.bmWidth, bs.bmH
21、eight, ILC_COLOR8, 10, 0);imgList.Add(for (int i = 1; i GetDC();imgList.Draw(pDC, m_nCurFrame, CPoint(0,0), ILD_NORMAL);m_nCurFrame+; m_nCurFrame %= 10;/ OnBnClickedAniStartStop、OnHScroll 和 OnEnChangeN 函数同前10.1.8 过程框图下面分别给出使用位图数组和 CImageList 类来实现固定位图动画的主要过程框图。1使用位图数组图 10-4 是使用位图数组实现固定位图动画的主要过程框图。获取位
22、图结构GetBitmap装入位图资源LoadBitmap创建位图对象CBitmap获取 DCGetDC显示当前位图BitBlt获取控件窗口对象GetDlgItem响应计时器消息OnTimer添加位图资源Ti.BMP定义位图结构BITMAP设置计时器SetTimer定义位图对象指针数组CBitmap * 更新当前位图序号定义内存 DCCDC创建兼容 DCCreateCompatibleDC选入位图对象SelectObject图 10-4 使用位图数组实现固定位图动画的主要步骤2使用图像列表图 10-5 是使用 CImageList 类实现固定位图动画的主要过程框图。10获取位图结构GetBitm
23、ap装入位图资源LoadBitmap定义位图对象CBitmap创建图像列表Create加入位图对象Add获取 DCGetDC绘制当前位图Draw获取控件窗口对象GetDlgItem响应计时器消息OnTimer添加位图资源Ti.BMP定义位图结构BITMAP定义图像列表对象CImageList设置计时器SetTimer更新当前位图序号图 10-5 使用 CImageList 类实现固定位图动画的主要步骤10.2 图形动画在前面(参见 8.5.3 小节)利用鼠标进行交互绘图时,我们就已经实现了简单的图形动画动态画直线、矩形或椭圆等,用户可通过鼠标对绘图位置坐标和图形大小进行交互式选择。具体做法是,
24、用灰色点线笔在同一个位置异或画两次一样的图形第一次画图,第二次擦除。快速不断地在不同的地方画擦,就达到了动画的效果。即图形动画的原理就是边擦边画。如果要画的不再是简单的线状图形,而是复杂的面状图,则异或画图方法就不再有效。因为异或会大大改变窗口中原有图形的色彩,这是用户所不能容忍的。可用的解决方法是,擦除(或保存)要绘图的区域,然后再绘制新图形(并恢复原区域的图形) 。具体的实现方法有两种直接绘图和缓冲绘图。10.2.1 直接绘图利用直接绘图方法,来产生动画的原理很简单,但是会存在讨厌的闪烁现象。1原理通过不断地擦除(要绘图的区域)和绘制(新图形)动态图形而产生动画效果。可以使用 CDC 类的
25、画填充矩形的函数(使用白色刷):void FillRect(LPCRECT lpRect, CBrush* pBrush);来擦除指定矩形区域。例如:pDC-FillRect(然后,再在该矩形区域内绘制新图形。例如:pDC-SelectObject( / 选入画边框的笔pDC-SelectObject( / 选入画填充色的刷11pDC-Ellipse( / 绘制填充椭圆2例子下面是一个在白色背景上动态画伸缩填充椭圆的例子,需要创建一个传统单文档 MFC 应用程序。主要代码片段如下:1)在视图类中定义若干类变量:bool shrink; / 用于判断伸缩int r, w, h, / 当前椭圆的短
26、轴半径和宽高R, W, H, / 最大椭圆的短轴半径和宽高xc, yc; / 椭圆的中心坐标CPen pen; / 绘制椭圆边框的笔(与刷同色)CBrush brush, whiteBrush; / 绘制椭圆内部的刷和删除原椭圆的白刷2)在视图类的构造函数中,设置初值、构造笔和刷:shrink = true; / 初始为缩小COLORREF greenCol = RGB(0, 150, 0), /定义绿色whiteCol = RGB(255, 255, 255); /定义白色pen.CreatePen(0, 0, greenCol ); / 实心单像素宽的绿色笔brush.CreateSoli
27、dBrush(greenCol ); / 实心绿色刷whiteBrush.CreateSolidBrush(whiteCol); / 实心白色刷3)在某个菜单项的事件处理函数中计算并设置初值、启动计时器:CRect rect; GetClientRect( / 获取当前客户区矩形W = rect.Width(); H = rect.Height();r = R = min(W, H) / 2; / 初始为最大椭圆w = W / 2; h = H / 2;xc = W / 2; yc = H /2;SetTimer(1, 10, NULL); / 可设置不同的时间间隔,或者让用户来设置4)在计时
28、器的消息响应函数 OnTimer 中,擦除并绘制椭圆,调整半径:CDC *pDC = GetDC();/ 擦除if (shrink) / 对膨胀不需要擦除CRect rect(xc - w, yc - h, xc + w, yc + h);12pDC-FillRect(rect, / 调整半径if (shrink) w-; h-; r-; / 缩小 1 像素if (r = 0) shrink = false; / 转换成放大 else w+; h+; r+; / 放大 1 像素if (r = R) shrink = true; / 转换成缩小/ 绘制填充椭圆pDC-SelectObject(p
29、DC-SelectObject(pDC-Ellipse(xc - w, yc - h, xc + w, yc + h);ReleaseDC(pDC);运行结果如图 10-6 所示。图 10-6 伸缩填充椭圆运行该程序后会发现,存在明显的闪烁现象,这主要是由收缩时的擦除操作所造成的。解决办法是,采用内存 DC 进行缓冲绘图。10.2.2 缓冲绘图在前面资源位图动画的绘制过程中,我们已经采用了缓冲(buffering )方法来显示位图:CDC *pDC = GetDC();CDC dc;dc.CreateCompatibleDC(pDC);dc.SelectObject(m_pBmpm_nCurF
30、rame);pDC-BitBlt(0, 0, bs.bmWidth, bs.bmHeight, 13其中,起主要作用的是内存 DC 和 CDC 类的位块传送函数 BitBlt,而且在该位图动画中并没有出现闪烁现象。1原理下面我们将这种方法加以扩展,不仅使其可用于已有位图的绘制,还可以用于普通图形的动态绘制。这需要先创建一个与当前视图 DC 兼容的空位图对象,并将其作为画布选入内存 DC 中,然后对该内存 DC 进行各种图形绘制,最后再用同样的 BitBlt 函数将绘图结果传送到屏幕上。具体步骤为: 获取当前视图 DC采用视图类 CView 基类 CWnd 的成员函数:CDC* GetDC( )
31、; 创建与当前视图 DC 兼容的位图对象分两步进行,先用 CBitmap 类的缺省构造函数:CBitmap( ); 构造空位图对象,再利用成员函数:BOOL CreateCompatibleBitmap(CDC* pDC, int nWidth, int nHeight);创建指定宽高(一般为当前客户区大小)的与当前 DC 兼容的位图对象。 创建与当前视图 DC 兼容的内存 DC也分两步进行,也是先用 CDC 类的缺省构造函数:CDC( ); 构造空 DC 对象,再利用成员函数:BOOL CreateCompatibleDC(CDC* pDC);创建与当前 DC 兼容的 DC 对象。 选位图对
32、象入内存 DC利用 CDC 类的成员函数:CBitmap* SelectObject(CBitmap* pBitmap);将所创建的空白位图对象选入内存 DC 中。 内存 DC 绘图用该内存 DC,调用各种 CDC 的绘图成员函数,在内存 DC 中的位图上进行绘图。包括绘制白色客户区矩形,进行白色背景设置(缺省为黑色背景) 。 绘制屏幕利用 CDC 类的位块传送函数:BOOL BitBlt( int x, int y, int nWidth, int nHeight, CDC* pSrcDC,int xSrc, int ySrc, DWORD dwRop );将内存 DC 中,已经被绘制好图形
33、的位图对象,传送到屏幕上。其中,与前面显示位图资源的方法最大的不同是,需要先创建一个与当前视图 DC 兼容的空位图对象,并将其作为画布选入内存 DC 中,然后才能对该内存 DC 进行各种图形绘制。注意,在没有选入位图对象的内存 DC 中直接绘图是无效的。2例子1)简单的例子:14CDC memDC; / 内存 DCmemDC.CreateCompatibleDC(pDC); / 创建与当前视图 DC 兼容的 DCCRect rect; / 矩形对象,用于表示客户区GetClientRect( / 获取客户区矩形CBitmap bmp; / 位图对象,作为画布,用于创建可传送的兼容 DC/ 创建
34、大小与客户区一致并且与视图 DC 兼容的位图对象bmp.CreateCompatibleBitmap(pDC, rect.Width(), rect.Height();/ 将位图选入内存 DC,使其可以进行各种绘图操作,并能够传递到屏幕CBitmap *pOldBmp = memDC.SelectObject(/ 用白色填充背景(缺省为黑色)CBrush brush(RGB(255, 255, 255);memDC.FillRect(/ 绘制各种图形memDC.Rectangle(10, 10, 200, 150);/ / 将内存 DC 中的图形传送到屏幕(在视图客户区绘图)pDC-BitBl
35、t(0, 0, w, h, 2)伸缩填充椭圆例现在仍然以伸缩填充椭圆为例,说明缓冲绘图产生动画的方法。程序的结构同 1)中的例,下面主要介绍不同之处(可以在原程序的基础上进行修改) 。(1)在视图类中增加若干类变量定义:CDC memDC; / 空内存 DC 对象CBitmap bmp, *pOldBmp; / 空位图对象和老位图对象指针bool created; / 是否创建了内存 DC 和位图对象(2)在视图类的构造函数中,设置初值:created = false; / 初始为未创建(3)在某个菜单项的事件处理函数(也可以在 OnSize 信息响应函数)中,对内存 DC 和位图对象进行初始
36、化:CDC *pDC = GetDC(); if (!created) memDC.CreateCompatibleDC(pDC);/ created 为真时,需先删除原位图对象, 才能创建新位图对象15if (created) memDC.SelectObject(pOldBmp);bmp.DeleteObject();bmp.CreateCompatibleBitmap(pDC, W, H); / W 和 H 可能已经变化pOldBmp = memDC.SelectObject(memDC.FillRect(created = true; shrink = true;xc = W / 2;
37、 yc = H /2;SetTimer(1, 10, NULL); / 可以设置不同的时间间隔(4)在计时器的消息响应函数 OnTimer 中,擦除并绘制椭圆,调整半径:void CAniView:OnTimer(UINT_PTR nIDEvent)/ TODO: 在此添加消息处理程序代码和 /或调用默认值CDC *pDC = GetDC();/ 擦除原椭圆(用白色填充客户区)if (shrink) / 对膨胀不需要擦除/ 擦除原椭圆(用白色填充客户区)CRect rect;GetClientRect( / 获取当前客户区矩形CBrush brush(RGB(255, 255, 255);me
38、mDC.FillRect(/ 调整半径/ 绘制新椭圆memDC.SelectObject(memDC.SelectObject(memDC.Ellipse(xc - w, yc - h, xc + w, yc + h);/ 将内存 DC 中的图形传送到屏幕(在视图客户区绘图)pDC-BitBlt(0, 0, W, H, ReleaseDC(pDC);16CView:OnTimer(nIDEvent);(5)可以添加一个停止动画的菜单项(ID_KT)及其事件处理函数:void CAniView:OnKt()/ TODO: 在此添加命令处理程序代码KillTimer(1); / 删除计时器运行结果
39、类似图 10-6 的,但是已经没有闪烁了。3过程框图图 10-7 是缓冲绘制图形动画的主要过程框图(粗体为主要步骤):获取客户区矩形GetClientRect创建兼容空位图CreateCompatibleBitmap定义位图对象CBitmap获取当前 DCGetDC显示当前图形BitBlt创建兼容内存 DCCreateCompatibleDC定义矩形结构RECT 或 CRect定义内存 DC 对象CDC修改图形参数响应计时器消息OnTimer设置计时器SetTimer选入位图对象SelectObject绘制白色背景FillRect绘制当前图形Ellipse 等擦除原图FillRect释放 DC
40、ReleaseDC图 10-7 缓冲绘制图形动画的主要步骤10.3 移动位图动画前面的固定(资源)位图动画,是在屏幕的同一位置,连续显示多帧不同的图像,产生动画效果。现在我们要讨论的是,同一图像在屏幕的不同位置显示,产生移动动画的效果。这里被移动的图像,可以是资源位图或用户装入的图像、也可以是从屏幕的指定矩形区域中获取的截图、还可以是程序在内存 DC 中绘制图形所生成的图像。下面介绍在背景图上移动图像块以产生动画效果的具体方法。我们先讨论原理,再画出过程图,最后给出一个移动足球的具体例子。1710.3.1 原理与图形动画一样,移动位图动画原理也是边擦边画。不过,为了不破坏原有的背景,即不能采用
41、异或画图,也不能用白色覆盖来擦除。为了实现图像块在背景图上移动,非常重要的一点是,必须预先保存将会被移动图像所覆盖的背景图上的对应矩形区域,并在图像移走后再恢复该区域的图形,参见图 10-8。这一功能可由含图像的内存 DC 和位块传送函数来完成。另外,为了避免会产生严重的闪烁现象,必须采用缓冲绘图方法。保存背景图块(供擦除时用)显示移动图块(画图)恢复原背景图块(擦除)更新图块位置(循环形成动画)图 10-8 移动位图动画的基本原理还有一点需要考虑的是,被移动的图形一般不是矩形,直接绘制会产生难看的背景遮挡,例如,在 9.3.3 的随机绘制足球和单击定位图像块例子中(参见图 9-23 和图 9
42、-24) ,就存在难看的足球四角上的白色遮挡现象。为了将矩形块中的背景部分去掉,可以将矩形中的背景区,用一种图形中没有的颜色来统一着色,并将该色指定为透明色,再利用 CDC 类或 CImage 类的透明位块传送成员函数 TransparentBlt 来进行绘制操作。函数原型为:CDC 类:BOOL TransparentBlt(int xDest, int yDest, int nDestWidth, int nDestHeight, CDC* pSrcDC, int xSrc, int ySrc, int nSrcWidth, int nSrcHeight, UINT clrTranspar
43、ent);CImage 类:BOOL TransparentBlt(HDC hDestDC, int xDest, int yDest, int nDestWidth, int nDestHeight, UINT crTransparent = CLR_INVALID) const throw( );透明位块传送函数在传送的同时,可以进行缩放。其最后一个输入参数最关键clrTransparent 为指定的透明颜色(UINT 类型与 COLORREF 所对应的 DWORD 类型是等价的,都是无符号的 4 字节整数) 。例如(参见图 10-9):COLORREF transpCol = RGB(2
44、55, 0, 0); / 设置透明色memDC.TransparentBlt(x0, y0, w, h, 白色背景 红色背景图 10-9 足球图片10.3.2 过程框图图 9-10 是利用 CBitmap 类实现移动位图主要过程的逻辑框图。18绘制传送恢复背景传送绘制图块传送绘制 底图传送 保存背景块传送 绘制背景选入 画布与当前 DC 兼容的客户区内存 DC 对象CDC memDC与当前 DC 兼容、大小同客户区的位图对象装入了图片文件的图像对象选入 画布窗口客户区与当前 DC 兼容的背景块内存 DC 对象CDC memDC与当前 DC 兼容、大小同移动图块的位图对象选入 图块与当前 DC
45、兼容的图块内存 DC 对象CDC memDC装入了位图资源的位图对象装足球图块对应客户区的内存DC图 10-10 移动位图的主要步骤10.3.3 例子下面是一个单击和拖动鼠标来在一背景图上移动足球的例子:1创建项目、添加资源创建一个传统的单文档 MFC 应用程序,加入红色背景的足球资源( IDB_FOOTBALL_RED) 。2添加类变量在视图类中定义若干类变量:/ #include “atlimage.h“int W, H, w, h, x0, y0; / W 和 H 为客户区的宽高、w 和 h 为图像块的/ 宽高、 x0 和 y0 为图像块上次位置的坐标COLORREF transpCol
46、; / 透明色CDC memDC, dc, dc0; / memDC:客户区、dc:图像块、dc0 :背景块CBitmap memBmp, bmp, bmp0, *pOldBmp; / memBmp 用于客户区、/ bmp 用于图像块、bmp0 用于背景块、pOldBmp 用于选出位图CImage imgbk; / 用于底图3初始化在初始化函数 OnInitialUpdate 中装入图像、计算参数、创建图像和内存 DC 对象:/ 装入图像 -transpCol = RGB(255, 0, 0); / 设置透明色/ 装入 res 目录中的底图(在 IDE 中运行时,当前目录为项目所在目录)img
47、bk.Load(L“restulips.tif“); 19/ 如果直接在 Release 或 Debug 目录中运行,则需要修改文件路径为:if(imgbk.IsNull() imgbk.Load(L“restulips.tif“);/ 如果图片文件位于可执行程序所在目录,则可修改文件路径为:if(imgbk.IsNull() imgbk.Load(L“tulips.tif“);bmp.LoadBitmap(IDB_FOOTBALL_RED); / 装入图像块资源(红色背景足球)/ 创建图像块对应的内存 DC,并选入图像块-CDC *pDC = GetDC(); / 获取视图 DCdc.Cre
48、ateCompatibleDC(pDC);pOldBmp = dc.SelectObject(/ 获取图像块的宽高 -BITMAP bm;bmp.GetBitmap(w = bm.bmWidth;h = bm.bmHeight;/ 创建背景块位图和内存 DC,选入图像-bmp0.CreateCompatibleBitmap(pDC, w, h);dc0.CreateCompatibleDC(pDC);pOldBmp = dc0.SelectObject(/ 获取客户区大小 -CRect rect;GetClientRect(W = rect.Width(); H = rect.Height();/ 创建与客户区对应的图像和内存 DC 对象,选入图像,绘制白色背景 -/ 创建与视图 DC 兼容并与客