Timers and Animation

theForger's Win32 API教程第二版(简体中文)

主页

基础
  1. 开始学习
  2. 一个简单的窗口
  3. 处理消息
  4. 理解消息循环
  5. 使用资源
  6. 菜单和图标
  7. 对话框
  8. 非模态对话框
  9. 标准控件
  10. 对话框常见问题
创建一个简单应用
  1. 在运行时创建控件
  2. 文件与常用对话框
  3. 工具栏与状态栏
  4. 多文档界面
图形设备接口
  1. 位图,设备上下文
  2. 透明位图
  3. 定时器与动画
  4. 文本,字体与顏色
工具与文档
  1. 参考
  2. 免费的Visual C++(最新更新)
附表
  1. 常见错误的解決方法
  2. API vs. MFC
  3. 关于资源文件的说明

定时器与动画

[images/anim_one.gif]

范例:anim_one

设置

  在我们开始动画之前,我们要设置一个可以存储在移动之间的球的位置的结构.这个结构将用来存储球现在的位置与大小,和其增量,就是我们要求其在每帧之间移动的距离.

  一旦我们声明了我们的结构体类型,我们同时也要声明一个此结构体的全局实例.我们现在只有一个球,如果我们想要使一堆的球来动画的话,就要声明一个数组或是其它的什么容器(比如C++中的链表)来方便地保存它们.

const int BALL_MOVE_DELTA = 2;

typedef struct _BALLINFO 
{
    int width;
    int height;
    int x;
    int y;

    int dx;
    int dy;
}BALLINFO;

BALLINFO g_ballInfo;

  这里我们也定义了一个常量BALL_MOVE_DELTA,表示我们要将这个球在每次更新时移动多远的距离.我们之所以要把增量保存在一个BALLINFO结构体中,是因为想让移动球至上下左右四个方向的操作为独立的过程,BALL_MOVE_DELTA只是为了叫起来方便,如果需要,我们可以在后面的程序中进行更改.

  现在我们要在装入位图后对此结构体进行初始化:

    BITMAP bm;
    GetObject(g_hbmBall, sizeof(bm), &bm);

    ZeroMemory(&g_ballInfo, sizeof(g_ballInfo));
    g_ballInfo.width = bm.bmWidth;
    g_ballInfo.height = bm.bmHeight;

    g_ballInfo.dx = BALL_MOVE_DELTA;
    g_ballInfo.dy = BALL_MOVE_DELTA;

  球于左上角开始运动,根据BALLINFO中的dx和dy向右和下的方向移动.

设置定时器

  最简单的向窗口程序添加一个定时器的方法是用SetTimer(),但不是最好的方法,而且不推荐在真实或是纯粹的遊戏中使用,然而在我们的样例中的这样简单的动画中还是沒问题的.如果要更好的请在MSDN查閱一下timeSetEvent(),那个更精确些.

const int ID_TIMER = 1;
    ret = SetTimer(hwnd, ID_TIMER, 50, NULL);
    if(ret == 0)
        MessageBox(hwnd, "Could not SetTimer()!", "Error", MB_OK | MB_ICONEXCLAMATION);

  这里我们声明了一个定时器的标识以在后面引用它(终止它)并在我们的主窗口的WM_CREATE消息处理代码中来设置它.每次时间到了,它就向我们的窗口发送一个WM_TIMER消息,并把定时器标识放于wParam中.因为我们只有一个定时器所以不需要标识,在有多个定时器的时候你就需要它们来作区分.

  我们把定时器的间隔设为50毫秒,大致上每秒钟可以数20帧.之所以说大致上,因为我说过,SetTimer()有点不精确,但是这里不是很关键的代码,这里那里的一点毫秒的误差并不是什么致命的问题.

WM_TIMER实现动画

  现在我们在得到WM_TIMER消息的时候我们要计算出球的新位置并在更新的位置再绘制一次.

    case WM_TIMER:
    {
        RECT rcClient;
        HDC hdc = GetDC(hwnd);

        GetClientRect(hwnd, &rcClient);

        UpdateBall(&rcClient);
        DrawBall(hdc, &rcClient);

        ReleaseDC(hwnd, hdc);
    }
    break;

  我把更新和绘图的代码分別放于两个单独的函数中.这样做的好处就是可以让我们在每个WM_TIMER或WM_PAINT消息中来绘制球而不会有重复的代码,注意我们在两个地方获取HDC的方法是不同的,所以最好是把这些代码留在消息处理中并把结果传到DrawBall()函数中去.

void UpdateBall(RECT* prc)
{
    g_ballInfo.x += g_ballInfo.dx;
    g_ballInfo.y += g_ballInfo.dy;

    if(g_ballInfo.x < 0)
    {
        g_ballInfo.x = 0;
        g_ballInfo.dx = BALL_MOVE_DELTA;
    }
    else if(g_ballInfo.x + g_ballInfo.width > prc->right)
    {
        g_ballInfo.x = prc->right - g_ballInfo.width;
        g_ballInfo.dx = -BALL_MOVE_DELTA;
    }

    if(g_ballInfo.y < 0)
    {
        g_ballInfo.y = 0;
        g_ballInfo.dy = BALL_MOVE_DELTA;
    }
    else if(g_ballInfo.y + g_ballInfo.height > prc->bottom)
    {
        g_ballInfo.y = prc->bottom - g_ballInfo.height;
        g_ballInfo.dy = -BALL_MOVE_DELTA;
    }
}

  这都是些基本的数学运算,我们把增量值加到x坐标来移动这个球.如果球到了客戶区域外,就把它拉回来范围內并把增量值改为相反方向的值以使球在边缘”弹”回来了.

void DrawBall(HDC hdc, RECT* prc)
{
    HDC hdcBuffer = CreateCompatibleDC(hdc);
    HBITMAP hbmBuffer = CreateCompatibleBitmap(hdc, prc->right, prc->bottom);
    HBITMAP hbmOldBuffer = SelectObject(hdcBuffer, hbmBuffer);

    HDC hdcMem = CreateCompatibleDC(hdc);
    HBITMAP hbmOld = SelectObject(hdcMem, g_hbmMask);

    FillRect(hdcBuffer, prc, GetStockObject(WHITE_BRUSH));

    BitBlt(hdcBuffer, g_ballInfo.x, g_ballInfo.y, g_ballInfo.width, g_ballInfo.height, hdcMem, 0, 0, SRCAND);

    SelectObject(hdcMem, g_hbmBall);
    BitBlt(hdcBuffer, g_ballInfo.x, g_ballInfo.y, g_ballInfo.width, g_ballInfo.height, hdcMem, 0, 0, SRCPAINT);

    BitBlt(hdc, 0, 0, prc->right, prc->bottom, hdcBuffer, 0, 0, SRCCOPY);

    SelectObject(hdcMem, hbmOld);
    DeleteDC(hdcMem);

    SelectObject(hdcBuffer, hbmOldBuffer);
    DeleteDC(hdcBuffer);
    DeleteObject(hbmBuffer);
}

  这里的绘图操作跟先前几个例子中的几乎是一样的,就是从BALLINFO结构体中取了球的位置与大小.但是还有一个重要的差異...

双缓冲

  当对你的窗口的HDC直接做绘图操作的时候,非常有可能的是屏幕会在你完成操作前就更新了...比如在你绘完掩图,还沒有在顶上绘彩图,用戶就会在你有机会绘彩色之前看到一个黑色背景的闪烁.你的计算机愈是慢,你的绘图操作愈多,闪烁就愈明显,很有可能看起来就像一个大的乱污点.

  这是相当郁闷的,但我们可以通过先在內存中完成所有的绘图操作,再把完成的杰作用一个BitBlt()拷入屏幕,这样屏幕就是直接从旧的图像直接更新至完全新的图像,而不再留下可见的单个操作.

  为了这样做,我们在內存中创建一个暂时的HBITMAP,跟我们要绘向屏幕的那个区域一模一样大小.我们也需要一个HDC以能向这个位图使用BitBlt().

    HDC hdcBuffer = CreateCompatibleDC(hdc);
    HBITMAP hbmBuffer = CreateCompatibleBitmap(hdc, prc->right, prc->bottom);
    HBITMAP hbmOldBuffer = SelectObject(hdcBuffer, hbmBuffer);

现在我们就有一个向內存绘图的地方了,所有的绘图操作用hdcBuffer而不是hdc(窗口的),这样在我们完成之前所有的结果都存于內存中的位图.我们现在就可以一步到位地把整个拷向窗口.

    BitBlt(hdc, 0, 0, prc->right, prc->bottom, hdcBuffer, 0, 0, SRCCOPY);

就这样,清除HDC和HBITMAP的操作还是如往常一样.

更快的双缓冲

  这个例子中我在每个帧都创建和销毀了用于双缓冲的位图,这样做的目的是我想能夠拉抻窗口的大小,所以总是创建一个新缓冲比在每次窗口的位置改变的时候都要跟蹤并且要重新決定缓冲区的大小.如果创建一个全局的缓冲位图,要么不允许窗口拉抻,要么在窗口拉抻的时候只拉抻这个位图,而不是每次创建一个新的,就会有效率得多.如果你要为一个遊戏或別的什么程序作优化的话,你就自己试一下.

终止定时器

  当我们的窗口被销毀的时候,将我们用的所有的资源进行释放是个好主意,这里就要包括我们设置的定时器了.要停止它,我们简单地把我们创建时用的标识传入KillTimer()就行了.

    KillTimer(hwnd, ID_TIMER);

Copyright © 1998-2008, Brook Miles (forgey). All rights reserved.