win32 帧动画
什么是帧动画
逐帧动画是一种常见的动画形式(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种用法
- 第一个参数设置为捕获该定时消息的窗口句柄, 第二个参数是定时器的id,第三个是以毫秒为单位的定时长度,最后一个参数设置为NULL,可以使窗口的回调函数进行处理WM_TIMER消息。
- 第一种方法唯一的区别就是最后一个参数不是NULL,而是一个自己定义的回调函数,这样,WM_TIMER消息将被自己定义回调函数获取,进行处理。
- 将第一个参数设置为NULL ,第二个参数设置为0,第三个和第四个参数的设置与第二种方法一致,这样创建一个定时器将返回一个定时器ID。这种方式适合多次定时容易混淆定时器ID的程序,因为其返回值管理定时器ID,而不要自己去管理。
注意:如果HWND
为NULL
,第二个参数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
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前进一步,定位到下一帧,最后通过定时器定时触发下一次绘制
流程图如下:
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
清除走到最右边的那帧,那么就会一直显示出来
调用InvalidateRect
后则只会显示一个人
命令行输出如下
- 点赞
- 收藏
- 关注作者
评论(0)