win32 帧动画

举报
福州司马懿 发表于 2022/11/19 19:34:55 2022/11/19
【摘要】 什么是帧动画逐帧动画是一种常见的动画形式(Frame By Frame),其原理是在“连续的关键帧”中分解动画动作,也就是在时间轴的每帧上逐帧绘制不同的内容,使其连续播放而成动画。 主要函数 BitBlt对指定的源设备环境区域中的像素进行位块(bit_block)转换,以传送到目标设备环境。即将源句柄上指定区域的图像,绘制到目标句柄上函数原型如下WINGDIAPIBOOLWINAPIBit...

什么是帧动画

逐帧动画是一种常见的动画形式(Frame By Frame),其原理是在“连续的关键帧”中分解动画动作,也就是在时间轴的每帧上逐帧绘制不同的内容,使其连续播放而成动画。

主要函数

BitBlt

对指定的源设备环境区域中的像素进行位块(bit_block)转换,以传送到目标设备环境。即将源句柄上指定区域的图像,绘制到目标句柄上

函数原型如下

WINGDIAPI
BOOL
WINAPI
BitBlt(
	_In_ HDC hdc, _In_ int x, _In_ int y, _In_ int cx, _In_ int cy,
	_In_opt_ HDC hdcSrc, _In_ int x1, _In_ int y1,
	_In_ DWORD rop);

rop是传输过程要执行的光栅运算

#define SRCCOPY             (DWORD)0x00CC0020 /* dest = source                   */
#define SRCPAINT            (DWORD)0x00EE0086 /* dest = source OR dest           */
#define SRCAND              (DWORD)0x008800C6 /* dest = source AND dest          */
#define SRCINVERT           (DWORD)0x00660046 /* dest = source XOR dest          */
#define SRCERASE            (DWORD)0x00440328 /* dest = source AND (NOT dest )   */
#define NOTSRCCOPY          (DWORD)0x00330008 /* dest = (NOT source)             */
#define NOTSRCERASE         (DWORD)0x001100A6 /* dest = (NOT src) AND (NOT dest) */
#define MERGECOPY           (DWORD)0x00C000CA /* dest = (source AND pattern)     */
#define MERGEPAINT          (DWORD)0x00BB0226 /* dest = (NOT source) OR dest     */
#define PATCOPY             (DWORD)0x00F00021 /* dest = pattern                  */
#define PATPAINT            (DWORD)0x00FB0A09 /* dest = DPSnoo                   */
#define PATINVERT           (DWORD)0x005A0049 /* dest = pattern XOR dest         */
#define DSTINVERT           (DWORD)0x00550009 /* dest = (NOT dest)               */
#define BLACKNESS           (DWORD)0x00000042 /* dest = BLACK                    */
#define WHITENESS           (DWORD)0x00FF0062 /* dest = WHITE                    */

SetTimer

创建或设置一个定时器,该函数创建的定时器与Timer控件(定时器控件)效果相同

函数原型如下

WINUSERAPI
UINT_PTR
WINAPI
SetTimer(
    _In_opt_ HWND hWnd,
    _In_ UINT_PTR nIDEvent,
    _In_ UINT uElapse,
    _In_opt_ TIMERPROC lpTimerFunc);

有3种用法

  1. 第一个参数设置为捕获该定时消息的窗口句柄, 第二个参数是定时器的id,第三个是以毫秒为单位的定时长度,最后一个参数设置为NULL,可以使窗口的回调函数进行处理WM_TIMER消息。
  2. 第一种方法唯一的区别就是最后一个参数不是NULL,而是一个自己定义的回调函数,这样,WM_TIMER消息将被自己定义回调函数获取,进行处理。
  3. 将第一个参数设置为NULL ,第二个参数设置为0,第三个和第四个参数的设置与第二种方法一致,这样创建一个定时器将返回一个定时器ID。这种方式适合多次定时容易混淆定时器ID的程序,因为其返回值管理定时器ID,而不要自己去管理。

注意:如果HWNDNULL,第二个参数nIDEvent无效(即此时不能指定ID)

返回值:
类型:UINT_PTR

  • 如果函数成功,hWnd参数为0,则返回新建立的时钟编号,可以把这个时钟编号传递给KillTimer来销毁时钟。
  • 如果函数成功,hWnd参数为非0,则返回一个非零的整数,可以把这个非零的整数传递给KillTimer来销毁时钟.
  • 如果函数失败,返回值是零。若想获得更多的错误信息,调用GetLastError函数。

KillTimer

在窗口销毁的时候进行计时器的销毁

WINUSERAPI
BOOL
WINAPI
KillTimer(
    _In_opt_ HWND hWnd,
    _In_ UINT_PTR uIDEvent);

uIDEvent 是SetTimer时设定的事件ID

InvalidateRect

该函数向指定的窗体更新区域添加一个矩形,然后窗体跟新区域的这一部分将被重新绘制

函数原型如下

WINUSERAPI
BOOL
WINAPI
InvalidateRect(
    _In_opt_ HWND hWnd,
    _In_opt_ CONST RECT *lpRect,
    _In_ BOOL bErase);

bErase指明是否要发送WM_ERASEBKGND消息从而擦除原来的背景

InvalidateRect发送WM_PAINT的形式是一种POST形式(即发送到程序消息队列),而不是像SendMessage一样直接让操作系统带着消息,调用WndProc。

当然如果想像SendMessage一样的。可以在后面接着使用UpdateWindow直接绕过程序消息队列直接发送消息到WndProc函数,来重绘窗口

代码实战

打开控制台(查看日志)

首先,定义并打开控制台,方便待会调试和查看日志

#define CONSOLE_TITLE "测试输出"

void ShowConsole()
{
    AllocConsole();
    FILE* stream;
    freopen_s(&stream, "CON", "r", stdin);//重定向输入流
    freopen_s(&stream, "CON", "w", stdout);//重定向输入流
    SetConsoleTitleA(CONSOLE_TITLE);//设置窗口名
}

定义常量和变量

我们假设,这个npc只在窗口水平方向行走

#define ID_NPC_TIMER        123  //NPC帧动画ID
#define NPC_HORIZONAL_COUNT 4    //水平方向有多少帧
#define NPC_VERTICAL_COUNT  2    //垂直方向有多少帧
#define NPC_SLEEP           80   //每次切换帧后,休息多久(单位毫秒)
#define NPC_BEGIN_X         0.2  //起始点相对窗口x轴的比例
#define NPC_END_X           0.5  //终止点相对窗口x轴的比例
#define X_STEP              10   //每一帧后,朝x轴正方向移动多少距离

HDC hdc, npcDC;
HBITMAP npcBitmap;
BITMAP npcBmpInfo;
int i = 0, j = 0, x = 0;

初始化图片

用到的图片 npc.bmp
图片.png

Init函数在窗口句柄创建之后调用

void Init(HINSTANCE hInstance, HWND hWnd)
{
    hdc = GetDC(hWnd);
    npcBitmap = (HBITMAP)LoadImage(hInstance, _T("Npc.bmp"), IMAGE_BITMAP, 0, 0, LR_LOADFROMFILE);
    npcDC = CreateCompatibleDC(hdc);//创建兼容DC
    GetObject(npcBitmap, sizeof(npcBmpInfo), &npcBmpInfo);
    SelectObject(npcDC, npcBitmap);
}

定位关键帧并进行绘制

首先获取窗口大小,然后定位npc在x轴的位置,随后绘制该帧,npc前进一步,定位到下一帧,最后通过定时器定时触发下一次绘制

流程图如下:
图片.png

void PlayNpc(HWND hWnd) {
    //获取窗口大小(由于窗口可能会改变,要么每次获取要么在WM_SIZE中获取)
    RECT rect;
    GetClientRect(hWnd, &rect);
    int width = rect.right - rect.left;

    //当x大于终点位置时,要擦除终点的那帧,同时将x移动到起点,并进行绘制
    if (x >= width * NPC_END_X)
    {
        //找到上一帧的绘制位置
        int lastX = x - X_STEP;
        //擦除上一帧
        RECT tmp;
        tmp.left = lastX;
        tmp.right = lastX + npcBmpInfo.bmWidth;
        tmp.top = 0;
        tmp.bottom = npcBmpInfo.bmHeight;
        InvalidateRect(hWnd, &tmp, TRUE);

        x = 0;
    }

    //初始化的时候,要将x的位置纠正到起点
    if (x == 0)
    {
        x = width * NPC_BEGIN_X;
    }

    //绘制当前帧
    BitBlt(hdc, x, 0, npcBmpInfo.bmWidth / NPC_HORIZONAL_COUNT, npcBmpInfo.bmHeight / NPC_VERTICAL_COUNT,
        npcDC, npcBmpInfo.bmWidth / NPC_HORIZONAL_COUNT * i, 0,
        SRCCOPY);

    //NPC朝x轴正方形移动一定步长
    x += X_STEP;

    //水平方向,移动到下一帧
    i++;
    if (i == NPC_HORIZONAL_COUNT)
    {
        //如果水平方向已经移动到最尾帧,则水平帧重置为0,垂直帧下移一帧
        i = 0;
        j = ++j % NPC_VERTICAL_COUNT;
    }

    //定时器触发WM_TIMER事件,ID存在wParam里
    UINT_PTR ret = SetTimer(hWnd, ID_NPC_TIMER, NPC_SLEEP, NULL);
}

处理消息

修改消息接收函数 WndProc 在收到 WM_TIMER 事件时,进行重绘。并在窗口被销毁前,关闭定时器

//  函数: WndProc(HWND, UINT, WPARAM, LPARAM)
//
//  目标: 处理主窗口的消息。
//
//  WM_COMMAND  - 处理应用程序菜单
//  WM_PAINT    - 绘制主窗口
//  WM_DESTROY  - 发送退出消息并返回
//
//
LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
    switch (message)
    {
    case WM_TIMER:
        printf("WM_TIMER wParam=%I64d, lParam=%I64d\n", wParam, lParam);
        if(wParam == ID_NPC_TIMER) PlayNpc(hWnd);
        break;
    case WM_DESTROY:
        KillTimer(hWnd, ID_NPC_TIMER);
        PostQuitMessage(0);
        break;
(略)
    }
}

测试代码

ShowWindow之前,打开控制台查看日志,并初始化图片
ShowWindow之后,开始执行动画

//
//   函数: InitInstance(HINSTANCE, int)
//
//   目标: 保存实例句柄并创建主窗口
//
//   注释:
//
//        在此函数中,我们在全局变量中保存实例句柄并
//        创建和显示主程序窗口。
//
BOOL InitInstance(HINSTANCE hInstance, int nCmdShow)
{
   hInst = hInstance; // 将实例句柄存储在全局变量中

   HWND hWnd = CreateWindowW(szWindowClass, szTitle, WS_OVERLAPPEDWINDOW,
      CW_USEDEFAULT, 0, 500, 300, nullptr, nullptr, hInstance, nullptr);

   if (!hWnd)
   {
      return FALSE;
   }
	ShowConsole();
   Init(hInstance, hWnd);

   ShowWindow(hWnd, nCmdShow);
   UpdateWindow(hWnd);

   PlayNpc(hWnd);
   return TRUE;
}

运行结果

如果不调用InvalidateRect清除走到最右边的那帧,那么就会一直显示出来

图片.png

调用InvalidateRect后则只会显示一个人

图片.png

命令行输出如下

图片.png

【版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息, 否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。