EGE绘图之四 Gif动图播放

专栏:EGE专栏

上一篇:EGE绘图之三 动画

下一篇:EGE绘图之五 按钮(上)

一、Gif绘制

1. 动图加载

  常常有播放动图的需要,但是EGE不能直接加载Gif文件形成动图。用getimage 读取 gif 图片只会读取第一帧,无法获取多帧。
   前面说过,在EGE中,常用的是用 getimage() 读取每帧,保存到 PIMAGE 数组中。
   所以想要制作动图,这就需要先把动图每一帧都拆分成图片,再加载到 PIMAGE 数组中。这种方法缺点是每一帧都要保存到PIMAGE 中,占用的内存较多。

比如, 一般屏幕分辨率为1920 * 1080, 那么一张全屏的图片,占用内存为 1920 * 1080 * 4B = 8294400B, 约为 7.9 MB. 所以加载时还要考虑内存占用。这种方式读取小图片是没有什么问题的,比如 300*300 * 4B =36000B , 约 0.34MB

1.1 使用 getimage() 加载每一帧的图像

  对于一个连续的动画,由多帧图像组成,可以将每帧的图像,按顺序在默认添加数字编号命名。比如有20帧的动画,每帧图像可以按顺序,命名为 “imageX.jpg”, X代表编号,比如从1到20
那么就可以用如下的方式读取

PIMAGE pimgs[20];		//保存加载的图像
char fileName[30];		//用于保存文件名

for (int i = 0; i < 20; i++) {
	pimgs[i] = newimage();
	//生成对应的文件名
	sprintf(fileName, "image%d.jpg", i + 1);		
	getimage(pimgs[i], fileName);
}
  • 其中要用到 sprintf() 来生成我们需要的文件名字符串。

这样我们便获取到了动画的每一帧图像,按顺序绘制即可形成动图。

1.2 借助GDI+中的Bitmap类加载GIF动图

  BitmapGDI+ 中的一个类,可以读取各种图像文件,包括 gif 文件,我们可以借助它来读取 gif 动图。通过使用 Bitmap读取动图后,就可以绘制到窗口上。


2. Gif类

  下面是我写的一个 Gif 类,可以用来加载播放动图,是借用 Bitmap 加载的gif 图像。

下面是两个文件的内容,需要在项目中加入下面两个文件进行编译。

2.1 Gif.h

下面新建一个头文件 Gif.h

#pragma once
#ifndef _EGEUTIL_GIF_H_
#define _EGEUTIL_GIF_H_

#include <time.h>
#include <gdiplus.h>

class Gif
{
private:
	int x, y;
	int width, height;
	int frameCount;					//帧数

	HDC hdc;						//设备句柄
	Gdiplus::Graphics* graphics;	//图形对象

	Gdiplus::Bitmap* gifImage;		//gif图像
	Gdiplus::PropertyItem* pItem;	//帧延时数据

	int curFrame;					//当前帧
	clock_t pauseTime;				//暂停时间

	clock_t	frameBaseTime;			//帧基准时间
	clock_t	curDelayTime;			//当前帧的已播放时间
	clock_t	frameDelayTime;			//当前帧的总延时时间

	bool playing;					//是否播放
	bool visible;					//是否可见

public:
	Gif(const WCHAR* gifFileName = NULL, HDC hdc = getHDC(NULL));
	Gif(const Gif& gif);

	virtual ~Gif();

	Gif& operator=(const Gif& gif);

	//加载图像
	void load(const WCHAR* gifFileName);

	//绑定设备
	void bind(HDC hdc);
	void bindWindow();

	//清空加载图像
	void clear();

	//位置
	void setPos(int x, int y);
	void setSize(int width, int height);

	int getX() const { return x; }
	int getY() const { return y; }

	//图像大小
	int getWidth() const { return width; }
	int getHeight() const { return height; }

	//原图大小
	int getOrginWidth() const;
	int getOrginHeight() const;

	//帧信息
	int getFrameCount() const { return frameCount; }
	int getCurFrame() const { return curFrame; }

	//延时时间获取,设置
	int getDelayTime(int frame) const;
	void setDelayTime(int frame, long time_ms);
	void setAllDelayTime(long time_ms);
	
	//更新时间,计算当前帧
	void updateTime();
	
	//绘制当前帧或指定帧
	void draw();
	void draw(int x, int y);
	void drawFrame(int frame);
	void drawFrame(int frame, int x, int y);

	//获取图像
	void getimage(PIMAGE pimg, int frame);

	//播放状态控制
	void play();
	void pause();
	void toggle();

	bool isPlaying()const { return playing; }

	void setVisible(bool enable) { visible = enable; }
	bool isVisible() const { return visible; }

	bool isAnimation() const { return frameCount > 1; }

	//重置播放状态
	void resetPlayState();

	void info() const;
	
private:
	void init();	//初始化
	void read();	//读取图像信息
	void copy(const Gif& gif);
};

#endif // !_EGEUTIL_GIF_H_

2.2 Gif.cpp

  新建一个Gif.cpp

#define SHOW_CONSOLE
#include <graphics.h>
#include <stdio.h>
#include "Gif.h"

//构造函数
Gif::Gif(const WCHAR* gifFileName, HDC hdc)
{
	init();
	if (gifFileName != NULL)
		load(gifFileName);
	bind(hdc);
}

//复制构造函数
Gif::Gif(const Gif& gif)
{
	copy(gif);
}

//析构函数
Gif::~Gif()
{
	delete gifImage;
	delete pItem;
	delete graphics;
}

//赋值操作符重载
Gif & Gif::operator=(const Gif & gif)
{
	if (this == &gif)		return *this;
	if (graphics != NULL)	delete graphics;
	if (pItem != NULL)		delete pItem;
	if (gifImage != NULL)	delete gifImage;

	copy(gif);

	return *this;
}

//初始化
void Gif::init()
{
	x = y = 0;
	width = height = 0;
	hdc = 0;
	gifImage = NULL;
	graphics = NULL;
	pItem = NULL;
	visible = true;

	resetPlayState();
}

//加载图像
void Gif::load(const WCHAR * gifFileName)
{
	if (gifImage != NULL)
		delete gifImage;
	gifImage = new Gdiplus::Bitmap(gifFileName);

	read();
}

//绑定绘制目标HDC
void Gif::bind(HDC hdc)
{
	this->hdc = hdc;
	if (graphics != NULL)
		delete graphics;
	graphics = Gdiplus::Graphics::FromHDC(hdc);
}

//绑定绘制目标到窗口
void Gif::bindWindow()
{
	if (hdc != getHDC())
		bind(getHDC());
}

//清除加载的图像
void Gif::clear()
{
	if (gifImage) {
		delete gifImage;
		gifImage = NULL;
	}

	if (pItem) {
		delete pItem;
		pItem = NULL;
	}
	frameCount = 0;
}

//获取图像原宽度
int Gif::getOrginWidth() const
{
	if (!gifImage)
		return 0;
	return gifImage->GetWidth();
}

//获取图像原宽度
int Gif::getOrginHeight() const
{
	if (!gifImage)
		return 0;
	return gifImage->GetHeight();
}

void Gif::setPos(int x, int y)
{
	this->x = x;
	this->y = y;
}

//设置图像大小
void Gif::setSize(int width, int height)
{
	this->width = width;
	this->height = height;
}

//在当前位置绘制当前帧
void Gif::draw()
{
	draw(x, y);
}

//在指定位置绘制当前帧
void Gif::draw(int x, int y)
{
	updateTime();
	drawFrame(curFrame, x, y);
}

//在当前位置绘制指定帧
void Gif::drawFrame(int frame)
{
	drawFrame(frame, x, y);
}

//在指定位置绘制指定帧
void Gif::drawFrame(int frame, int x, int y)
{
	if (!visible)
		return;
	int w = width, h = height;
	if (w == 0 && h == 0) {
		w = gifImage->GetWidth();
		h = gifImage->GetHeight();
	}
	if (frameCount != 0 && gifImage && 0 <= frame) {
		frame %= frameCount;
		gifImage->SelectActiveFrame(&Gdiplus::FrameDimensionTime, frame);
		graphics->DrawImage(gifImage, x, y, w, h);
	}
}

//获取Gif的指定帧,并保存到pimg中
void Gif::getimage(PIMAGE pimg, int frame)
{
	if (frame < 0 || frameCount <= frame)
		return;

	int width = gifImage->GetWidth(), height = gifImage->GetHeight();

	if (width != getwidth(pimg) || height != getheight(pimg))
		resize(pimg, width, height);

	//自定义图像缓存区(ARGB)
	Gdiplus::BitmapData bitmapData;
	bitmapData.Stride = width * 4;
	int buffSize = width * height * sizeof(color_t);
	bitmapData.Scan0 = getbuffer(pimg);

	gifImage->SelectActiveFrame(&Gdiplus::FrameDimensionTime, frame);
	Gdiplus::Rect rect(0, 0, width, height);
	//以32位像素ARGB格式读取, 自定义缓存区

	gifImage->LockBits(&rect,
		Gdiplus::ImageLockModeRead | Gdiplus::ImageLockModeUserInputBuf, PixelFormat32bppARGB, &bitmapData);
	gifImage->UnlockBits(&bitmapData);
}

//获取指定帧的延时时间
int Gif::getDelayTime(int frame) const
{
	if (frame < 0 || frameCount <= frame ||
		!pItem || pItem->length <= (unsigned int)frame)
		return 0;
	else
		return ((long*)pItem->value)[frame] * 10;
}

//设置指定帧的延时时间
void Gif::setDelayTime(int frame, long time_ms)
{
	if (frame < 0 || frameCount <= frame ||
		!pItem || pItem->length <= (unsigned int)frame)
		return;
	else
		((long*)pItem->value)[frame] = time_ms / 10;
}

//统一设置所有帧的延时时间
void Gif::setAllDelayTime(long time_ms)
{
	for (int i = 0; i < frameCount; i++)
		((long*)pItem->value)[i] = time_ms / 10;
}

//播放
void Gif::play()
{
	playing = true;
	clock_t sysTime = clock();
	if (frameBaseTime == 0) {
		pauseTime = frameBaseTime = sysTime;
		curFrame = 0;
		frameDelayTime = getDelayTime(curFrame);
	}
	else
		frameBaseTime += sysTime - pauseTime;
}

//暂停
void Gif::pause()
{
	if (playing) {
		playing = false;
		this->pauseTime = clock();
	}
}

//播放暂停切换
void Gif::toggle()
{
	playing ? pause() : play();
}

//重置播放状态
void Gif::resetPlayState()
{
	curFrame = 0;
	curDelayTime = frameBaseTime = frameDelayTime = 0;
	pauseTime = 0;
	playing = false;
}

//控制台显示Gif信息
void Gif::info() const
{
	printf("绘制区域大小: %d x %d\n", getWidth(), getHeight());
	printf("原图像大小 : %d x %d\n", getOrginWidth(), getOrginHeight());
	int frameCnt = getFrameCount();
	printf("帧数: %d\n", getFrameCount());
	printf("帧的延时时间:\n");
	for (int i = 0; i < frameCnt; i++)
		printf("第%3d 帧:%4d ms\n", i, getDelayTime(i));
}

//读取图像
void Gif::read()
{
	/*读取图像信息*/
	UINT count = gifImage->GetFrameDimensionsCount();
	GUID* pDimensionIDs = (GUID*)new GUID[count];
	gifImage->GetFrameDimensionsList(pDimensionIDs, count);
	//帧数
	frameCount = gifImage->GetFrameCount(&pDimensionIDs[0]);
	delete[] pDimensionIDs;

	if (pItem != NULL)
		delete pItem;

	//获取每帧的延时数据
	int size = gifImage->GetPropertyItemSize(PropertyTagFrameDelay);
	pItem = (Gdiplus::PropertyItem*)malloc(size);
	gifImage->GetPropertyItem(PropertyTagFrameDelay, size, pItem);
}

//Gif复制
void Gif::copy(const Gif& gif)
{
	hdc = gif.hdc;
	x = gif.x;
	y = gif.y;
	width = gif.width;
	height = gif.height;
	curFrame = gif.curFrame;
	pauseTime = gif.pauseTime;

	frameBaseTime = gif.frameBaseTime;
	curDelayTime = gif.curDelayTime;
	frameDelayTime = gif.frameDelayTime;

	frameCount = gif.frameCount;
	graphics = new Gdiplus::Graphics(hdc);
	gifImage = gif.gifImage->Clone(0, 0, gif.getWidth(), gif.getHeight(), gif.gifImage->GetPixelFormat());

	int size = gif.gifImage->GetPropertyItemSize(PropertyTagFrameDelay);
	pItem = (Gdiplus::PropertyItem*)malloc(size);
	memcpy(pItem, gif.pItem, size);
}

//Gif时间更新,计算当前帧
void Gif::updateTime()
{
	//图像为空,或者不是动图,或者没有调用过play()播放()
	if (frameCount <= 1 || frameBaseTime == 0
		|| (pItem && pItem->length == 0))
		return;

	//根据播放或暂停计算帧播放时间
	curDelayTime = playing ? (clock() - frameBaseTime) : (pauseTime - frameBaseTime);

	int cnt = 0, totalTime = 0;

	//间隔时间太长可能会跳过多帧
	while (curDelayTime >= frameDelayTime) {
		curDelayTime -= frameDelayTime;
		frameBaseTime += frameDelayTime;

		//切换到下一帧
		if (++curFrame >= frameCount)
			curFrame = 0;
		frameDelayTime = getDelayTime(curFrame);

		totalTime += frameDelayTime;

		//多帧图像,但总延时时间为0的处理
		if (++cnt == frameCount && totalTime == 0)
			break;
	}
}

2.3 Gif 的使用

2.3.1 使用示例:

  下面是Gif 类的一个使用示例,先简单看一下 Gif 如何使用

最简单的使用:

//创建并加载, 传入gif文件路径
Gif gif(L"C:\\Users\\19078\\Desktop\\1.gif");
//播放
gif.play();
//绘制出当前帧
for (; is_run(); delay_fps(60)) {
	gif.draw();
}

  上面是最基础的,调用默认的设置(在 (0, 0) 出绘制原图大小),gif.draw() 根据播放开始的时间自动绘制当前帧, 而不是按顺序绘制所有帧,所以如果帧率小的话,会有一些帧被跳过。

下面是完整的示例
  固定的缩放绘制,300x300大小,按任意键控制播放暂停切换。(根据gif的实际路径修改第)

#define SHOW_CONSOLE
#include <graphics.h>

#include "Gif.h"

int main()
{
	initgraph(600, 400, INIT_RENDERMANUAL);
	setbkcolor(WHITE);
	setcolor(BLACK);
	setbkmode(TRANSPARENT);
	setfont(20, 0, "楷体");

	//创建Gif对象
	Gif gif(L"这里填gif带路径文件名,如绝对路径:E:/gif图片.gif, 相对路径: ./gif图片.gif");
	gif.setPos(20, 20);
	gif.setSize(300, 300);

	//控制台输出Gif图像信息
	gif.info();

	//开始播放
	gif.play();

	key_msg keyMsg = { 0 };
	for (; is_run(); delay_fps(30)) {
		//清屏,这个示例里不清屏也行
		cleardevice();

		//绘制
		gif.draw();

		//这里是交互控制,按任意键切换播放暂停
		xyprintf(340, 40, "按任意键切换播放暂停");

		while (kbmsg()) {
			keyMsg = getkey();
			if (keyMsg.msg == key_msg_down) {
				gif.toggle();
			}
		}
	}
	
	closegraph();

	return 0;
}

  gif.info() 能够输出Gif图像信息和每帧的延时时间,如果Gif 图不动,可以输出试试,看看每帧的延时时间是不是为0, 如果为0,可以自己用 setAllDelayTime() 来为所有帧设置固定的延时时间,或者用setDelayTime() 来单独为某一帧设置延时时间。
在这里插入图片描述

2.3.2 初始化

有两种方式:
使用如下的构造函数

Gif::Gif(const WCHAR* gifFileName = NULL, HDC hdc = graph_setting.dc);

  gifFileName 默认为NULL, hdc 默认是EGE的内部窗口帧缓存的设备句柄,即默认绘制到窗口上,而不是图像中。

WCHAR类型的话,字符串用L" " 表示, 前面有个L

(1) 设置绘制对象的同时加载gif图像

Gif gif(L"gif图片.gif");

(2)只设置绘制对象,不加载gif图像

Gif gif;

这种方式只是设置绘制的对象,并没有加载Gif图像,可以创建数组,之后再使用 load() 函数加载

Gif gif[20];
2.3.3 加载图像

  加载图像使用的是 load() 函数, 如果上面已经加载了,就不用再加载了

void Gif::load(const WCHAR* gifFileName);

  调用后可以从Gif图像文件中加载Gif图像(实际上不是gif图像也行,不过如果是jpg, png的话,只有一张图片,也动不了)
示例:

Gif gif[10];
for (int i = 0; i < 10; i++) {
	gif[i].load(L"gif图像.gif");
}

即使已经加载有图像,也可以直接使用 load() 更换加载另一张图像

Gif gif;
gif.load(L"gif图像.gif");
gif.load(L"另一张gif图像.gif");//这时变成另一张图像

如果想要创建多个相同的Gif对象的话
可以只加载一个, 然后用赋值运算符,这样就复制出了多个。

Gif gif[20];
gif[0].load(L"gif图像");
for (int i = 1; i < 20; i++)
	gif[i] = gif[0];
2.3.4 绑定设备

  默认是输出到窗口,这一步可以跳过
  如果想要绘制到 PIMAGE 上的话,可以传入 PIMAGE的HDC (这个是ege_head.h 头文件中的)

PIMAGE pimg = newimage(100, 100);
gif.bind(pimg->getdc());

如果设置了绘制到图像上,想要设置回绘制到窗口,可以调用 bindWindow() .

gif.bindWindow();
2.3.5 设置绘制位置,大小

  绘制位置默认是 (0, 0);
  如果设置图片宽高都为0的话,则按原图大小绘制, 否则按设置的大小绘制 (默认为按原图大小绘制)

void Gif::setPos(int x, int y);
void Gif::setSize(int width, int height);

如果尺寸已经缩放了,要设置成按原图大小绘制

gif.setSize(0, 0);

相关属性获取:
获取绘制位置

int Gif::getX() const;
int Gif::getY() const;

获取设置的绘制图像大小(都为0表示按原图大小绘制)

int Gif::getWidth() const;
int Gif::getHeight() const;

获取原图大小

int Gif::getOrginWidth() const;
int Gif::getOrginHeight() const;
2.3.6 切换至播放状态

默认为暂停状态, 从调用 play() 开始计时

	gif.play();
2.3.7 绘制图像

调用 draw() 即可在设置的区域绘制出当前帧

gif.draw();

绘制相关的函数:
  带有 x, y 参数的是指定绘制的位置
  带有 frame 参数的是指定绘制的帧(不小于0则有效,对帧数取模,这意味着如果绘制的帧序号一直增加的话,是循环绘制的)

void Gif::draw();
void Gif::draw(int x, int y);
void Gif::drawFrame(int frame);
void Gif::drawFrame(int frame, int x, int y);

上面说 frame 是对帧数取模,所以下面的程序是一直循环绘制,而不是只显示一遍,而不会出现 frame超出范围的情况

for (int i = 0; is_run(); delay_fps(3), i++) {
	gif.drawFrame(i);
}

2.4 其他函数

2.4.1 可见性
  • 设置为不可见时,是绘制不出图像的
	void Gif::setVisible();
	bool Gif::isVisible() const;
2.4.2 帧数, 当前帧,当前帧的延时时间
	//帧信息
	int Gif::getFrameCount() const;
	int Gif::getCurFrame() const;
	int Gif::getDelayTime(int frame) const;
2.4.3 设置每帧,或者设置全部帧的延时时间

时间单位是ms, 因为数据存储中都是单位是10ms,所以time_ms需要是10的倍数,个位会被截断

	void Gif::setDelayTime(int frame, long time_ms);
	void Gif::setAllDelayTime(long time_ms);	//设置全部帧延时
2.4.4 播放状态控制(播放、暂停、切换)
	//播放状态控制
	gif.play();
	gif.pause();
	gif.toggle();
2.4.5 查询当前是否播放
	gif.isPlaying();
2.4.6 重置播放状态

重置后将切换为暂停状态,调用play() 后将重新播放

	gif.resetPlayState();
2.4.7 清空图像数据
	gif.clear();
2.4.8 控制台输出Gif图像信息
	gif.info();
2.4.9 生成某一帧的PIMAGE

gif.getimage()

PIMAGE pimg = newimage();
int frame = 0;	//第1帧
gif.getimage(pimg, frame);

2.5 由Gif类生成某一帧的PIMAGE

  前面已经说过 Bitmap 类加载 Gif 图像并绘制的方法。但如果我们想要得到Gif中某一帧的图像,并保存在 PIMAGE 中一般有两种方法,一种是使用绘制的办法:

  1. 先创建和原图大小的图像 。获取原图尺寸要使用 getOrginWidth(), getOrginHeight(), 因为 getWidth(), getHeight() 在没设置宽高没设置的情况下返回的是0,而不是原图大小。
    或者先调用 gif.setSize() 设置想要的大小。
  2. 先使用 gif.bind() 绑定 PIMAGE的设备句柄。
  3. 然后将调用 gif.drawFrame( i, x, y ), 将第 i 帧绘制到图像(x, y)位置上。
  4. 如果 gif 还要继续绘制到窗口,那么调用 gif.bindWindow(), 设置绘制到窗口,不然 gif 将会绘制到PIMAGE, 影响继续使用。
//创建原图大小的图像
PIMAGE pimg = newimage(gif.getOrginWidth(), gif.getOrginHeight());
//或者创建缩放的图像
//int width = 200, height = 200;
//gif.setSize(width, height);
//PIMAGE pimg = newimage(width, height);


//绑定到图像上,接下来的绘制将绘制到图像
gif.bind(pimg->getdc());

//绘制第frame帧,到图像
int frame = 0;
gif.drawFrame(frame, 0, 0);

//绑定回窗口
gif.bindWindow();

第二种是调用 Gif 类中的 getimage() 函数
这个方法获取的是原图大小的图像, 不需要提前设置PIMAGE的尺寸

PIMAGE pimg = newimage();
int frame = 0;		//第1帧
gif.getimage(pimg, frame);

然后 pimg就可以直接用了

3. Gif的刷新显示

  在 (四) EGE基础教程 中篇 后面的 EGE窗口刷新 相关内容中有提到,EGE中的有延时时间delay_ms(time)delay_fps() 会强制刷新窗口,而 delay_ms(0)getch() 则是根据标志位是否为 true 来决定是否刷新窗口。
  而 Gif 类的绘制,没有通过EGE绘图函数,所以标志位是不会变为 true 的。所以像下面的程序,按一次显示一帧,是不会看到有变化的。

下面是小老鼠图片,一共100只小老鼠,可以自行保存起来(鼠标右键,另存为)(鼠年快乐)。
在这里插入图片描述

  因为只用 getch() 刷新窗口,中间也没有用到EGE的绘制函数,所以除了第一次setbkcolor()使标志位为 true 外, 其它时候不会刷新。需要进行强制刷新。

#include <graphics.h>
#include "Gif.h"

int main()
{
	initgraph(240* 3, 240 * 3, 0);
	setbkcolor(WHITE);
	Gif gif(L"C:\\Users\\19078\\Desktop\\小老鼠.gif");
	gif.setSize(240, 240);
	
	for (int i = 0; i < 100; i++) {
		gif.drawFrame(i, i % 3 * 240, i % 9 / 3 * 240);
		
		getch();
	}

	closegraph();

	return 0;
}

在这里插入图片描述
下面是添加了 delay_ms(1)的程序

#include <graphics.h>
#include "Gif.h"

int main()
{
	initgraph(240* 3, 240 * 3, 0);
	setbkcolor(WHITE);
	Gif gif(L"C:\\Users\\19078\\Desktop\\小老鼠.gif");
	gif.setSize(240, 240);
	
	for (int i = 0; i < 100; i++) {
		gif.drawFrame(i, i % 3 * 240, i % 9 / 3 * 240);
		
		delay_ms(1);		//强制刷新窗口
		getch();
	}

	closegraph();

	return 0;
}

在这里插入图片描述

当然,delay_fps() 也是会强制刷新窗口的,来看看自动播放的小老鼠, 在9个格里顺序绘制。

#include <graphics.h>
#include "Gif.h"

int main()
{
	initgraph(240* 3, 240 * 3, 0);
	setbkcolor(WHITE);
	delay_ms(0);		//刷新一下背景色
	Gif gif(L"C:\\Users\\19078\\Desktop\\小老鼠.gif");
	gif.setSize(240, 240);
	
	for (int i = 0; is_run(); delay_fps(3), i++) {
		gif.drawFrame(i, i % 3 * 240, i % 9 / 3 * 240);
	}

	closegraph();

	return 0;
}

在这里插入图片描述

二、实现原理分析

  这部分属于实现原理讲解,可以不看。==

1. Gif类的分析

1.1 Gif中的时间系统

1.1.1 当前运行时间获取

获取时间使用的是 <time.h> 头文件中的 clock() 函数, 返回程序运行的时间,单位毫秒。

clock_t sysTime = clock();
1.1.2 时间记录

主要由下面四个变量记录(类型为 clock_t):

  • pauseTime
    暂停时间,用于播放暂停,记录暂停时的时间
  • frameBaseTime
    帧基准时间,记录当前帧是从何时开始播放
  • curDelayTime
    当前帧的已延时时间, 为从 frameBaseTime 开始所经过的时间
  • frameDelayTime,
    当前帧的延时时间, 是从Gif图像中获取的帧延时时间数据,

初始四个时间值都为0。

1.1.3 播放时的时间记录

下面是 Gif 类中的 play() 函数

  • playing 为播放状态变量, true表示播放, false 表示暂停
  • frameBaseTime 等于 0,说明首次播放,这时便进行时间的初始设置,即暂停时间 = 帧基准时间 = 当前时间, 并且获取当前帧的延时时间(frameDelayTime)
  • 如果已经播放过,并且当前为暂停状态,那么切换为播放状态,帧基准时间要加上从刚才暂停时所经过的时间。
    因为暂停时播放时间是不计算的,所以要基准时间要往后移,这样就能从刚才暂停的地方继续计时。
void Gif::play()
{
	clock_t sysTime = clock();
	if (frameBaseTime == 0) {
		pauseTime = frameBaseTime = sysTime;
		frameDelayTime = getDelayTime(curFrame);
	}
	else if (!playing){
		playing = true;
		frameBaseTime += sysTime - pauseTime;
	}	
}
1.1.4 暂停时的时间记录

暂停时就把播放状态切换为暂停,记录下暂停的时间即可

void Gif::pause()
{
	if (playing) {
		playing = false;
		this->pauseTime = clock();
	}
}

1.2 播放状态切换

方便进行播放状态切换。

void Gif::toggle()
{
	playing ? pause() : play();
}

1.3 当前帧的计算

curFrame 为当前帧,初始值为0,即第一帧

  • 图像为空,不是动图,或者 frameBaseTime == 0 (没有调用过play()播放),那就不必计算,一直是第一帧
  • 计算当前帧已经延时的时间
    播放时计算公式:curDelayTime = 当前时间 - frameBaseTime
    暂停时计算公式:curDelayTime = pauseTime - frameBaseTime (暂停的时候,当前帧的延时时间就不会变了,)
  • 如果 curDelayTime >= frameDelayTime, 那么说明已经过了当前帧的播放时间, 就切换到下一帧,并且 frameBaseTime 要移动到下一帧的开始时间,curDelayTime要减去 frameDelayTime 。因为这段时间里很有可能已经过了多帧,所以要循环一直判断。
  • 最后,还要防止虽然是多帧图片,但是每帧的延时时间为0,这样就跳不出循环,所以还要统计一下所有的时间, 如果统计完所有帧延时时间为0,那么就跳出循环。
void Gif::updateTime()
{
	//图像为空,或者不是动图,或者没有调用过play()播放()
	if (frameCount <= 1 || frameBaseTime == 0
		|| (pItem && pItem->length == 0))
		return;

	//根据播放或暂停计算帧播放时间
	curDelayTime = playing ? (clock() - frameBaseTime) : (pauseTime - frameBaseTime);

	int cnt = 0, totalTime = 0;

	//间隔时间太长可能会跳过多帧
	while (curDelayTime >= frameDelayTime) {
		curDelayTime -= frameDelayTime;
		frameBaseTime += frameDelayTime;

		//切换到下一帧
		if (++curFrame >= frameCount)
			curFrame = 0;
		frameDelayTime = getDelayTime(curFrame);

		totalTime += frameDelayTime;

		//多帧图像,但总延时时间为0的处理
		if (++cnt == frameCount && totalTime == 0)
			break;
	}
}

在绘制时,先调用一下 updateTime(), 更新时间,计算出当前帧,然后将当前帧设置为 Bitmap 的活动帧,再绘制即可

2. Bitmap类的使用

Gif 类时使用的 Bitmap 类来加载Gif图像, 下面就讲解 Bitmap 的使用。

Bitmap 是在 Gdiplus 命名空间中,需要包含 <gdiplus.h> 头文件

2.1 加载图像

Bitmap 的构造函数中有带 const WCHAR* 参数的,传入需要加载的 gif 文件名即可, 不过需要注意是 WCHAR 型的,字符串前要加个 L

Gdiplus::Bitmap gifImage(L"gifFileName.gif");

2.2 读取图像大小,帧数,帧延时等数据

加载图像后,我们需要知道GIF图像的一些数据,比如有多少帧,每一帧的延时是多少,图像的大小等。
这些信息的获取可以通过以下代码得到:(这部分看不懂的不用纠结是什么意思,只要获取到数据就好)

  • 获取帧数
/*读取图像信息*/
UINT count = gifImage.GetFrameDimensionsCount();
GUID* pDimensionIDs = (GUID*)new GUID[count];
gifImage.GetFrameDimensionsList(pDimensionIDs, count);
WCHAR strGuid[39];
StringFromGUID2(pDimensionIDs[0], strGuid, 39);

//帧数
int frameCnt = gifImage.GetFrameCount(&pDimensionIDs[0]);
delete[] pDimensionIDs;
  • 获取每一帧的延时时间
	//读取帧延时信息
	int size = gifImage.GetPropertyItemSize(PropertyTagFrameDelay);
	Gdiplus::PropertyItem* pItem = (Gdiplus::PropertyItem*)malloc(size);
	gifImage.GetPropertyItem(PropertyTagFrameDelay, size, pItem);

此时每一帧的延时时间就都保存在 pItem
获取延时时间数组的长度()

int length = pItem->length;

Gdiplus::PropertyItem 类指针成员 value 所指向的数组,里面就是延时时间数据。获取第 i 帧的延时时间如下(最好先利用 pItem->length 判断一下 i 是否在有效范围内, 因为如果你加载的是静态图片,那么是没有延时数据的,访问的地址是无效的)

long delayTime = ((long*)pItem->value)[i] * 10;

对,因为指针 valuevoid* 型的,我们需要把它转成 long* 型的指针。里面的延时时间单位是10 毫秒,而我们用的是单位是毫秒,所以要乘以10

  • 获取图像大小
    这个比较简单,直接获取即可
//尺寸
	int gifWidth  = gifImage.GetWidth();
	int gifHeight = gifImage.GetHeight();

2.3 将Bitmap绘制在EGE窗口

GDI+的绘图需要有设备句柄,而设备句柄的创建需要依赖窗口句柄。
EGE的窗口句柄我们可以通过ege的 getHWnd() 获得:
先创建设备句柄:

//获取设备句柄
HWND egeHWnd = getHWnd();
//获取设备句柄
HDC hdc = GetDC(egeHWnd);

使用完后可以使用 RealseDC() 来释放

然后用设备句柄创建 graphics 对象

//创建Graphics对象
Gdiplus::Graphics graphics(hdc);

此时绘图的准备就已经好了
因为动图是有很多帧的,想要画某一帧时,需要将其设置为活动帧
使用的是SelectActiveFrame() 函数, 有个const GUID* 参数,传入全局变量 Gdiplus::FrameDimensionTime 的地址就好

  • 设置 第i帧 为活动帧
	gifImage.SelectActiveFrame(&Gdiplus::FrameDimensionTime, i);
  • 将图像绘制于EGE窗口 (x, y) 位置
    后面两个参数是绘制的大小,会 缩放, 这里就按原图大小绘制
graphics.DrawImage(&gifImage, x, y, gifImage.GetWidth(), gifImage.GetHeight());
  • 延时相应的时间
    前面说过,第i帧 的延时时间是 ((long*)pItem->value)[i] * 10 .只要获取后调用EGE中的 delay() 来延时即可(不能用delay_ms(),原因后面说)
int frame = 0;
while (is_run()) {
	//设置活动帧
	gifImage.SelectActiveFrame(&Gdiplus::FrameDimensionTime, frame);

	//绘制到窗口上
	graphics.DrawImage(&gifImage, 0, 0, gifImage.GetWidth(), gifImage.GetHeight());

	//延时
	delay(((long*)pItem->value)[frame] * 10);
	
	if (++frame >= frameCnt)
		frame = 0;
}

2.4 Gif 绘图示例

整个完整的程序如下:

  将 “gifFileName.gif” 换成自己的Gif文件名。

#include <graphics.h>
#include <gdiplus.h>

int main()
{
	initgraph(600, 600, INIT_RENDERMANUAL);
	setbkcolor(WHITE);
	
	Gdiplus::Bitmap gifImage(L"gifFileName.gif");

	/*读取图像信息*/
	UINT count = gifImage.GetFrameDimensionsCount();
	GUID* pDimensionIDs = (GUID*)new GUID[count];
	gifImage.GetFrameDimensionsList(pDimensionIDs, count);
	WCHAR strGuid[39];
	StringFromGUID2(pDimensionIDs[0], strGuid, 39);

	//帧数
	int frameCnt = gifImage.GetFrameCount(&pDimensionIDs[0]);
	delete[] pDimensionIDs;

	//获取每帧的延时数据
	int size = gifImage.GetPropertyItemSize(PropertyTagFrameDelay);
	Gdiplus::PropertyItem* pItem = (Gdiplus::PropertyItem*)malloc(size);
	gifImage.GetPropertyItem(PropertyTagFrameDelay, size, pItem);

	HWND egeHWnd = getHWnd();
	//获取设备句柄
	HDC hdc = GetDC(egeHWnd);
	//创建图形对象
	Gdiplus::Graphics graphics(hdc);

	//刷新一下窗口,把背景色先显示出来
	delay_ms(0);

	int frame = 0;
	while (is_run()) {
		//设置活动帧
		gifImage.SelectActiveFrame(&Gdiplus::FrameDimensionTime, frame);

		//绘制到窗口上
		graphics.DrawImage(&gifImage, 0, 0, gifImage.GetWidth(), gifImage.GetHeight());

		//延时
		delay(((long*)pItem->value)[frame] * 10);
		
		//切换下一帧
		if (++frame >= frameCnt)
			frame = 0;
	}

	//释放设备
	ReleaseDC(egeHWnd, hdc);

	closegraph();

	return 0;
}
2.4.1 例程分析

  上面的例程可以将GIF动图显示到窗口上,但是有些不足:如果刷新窗口,将会出现闪烁,甚至看不到图像
  试试把delay() 换成 delay_ms(), 或者统一延时,换成固定的每秒60帧,即使用 delay_fps(60), 这时就能看到刷新时的图像无法显示。

  这是因为:由前面讲过的EGE窗口刷新,我们可以知道,EGE的刷新窗口是把EGE 内部帧缓存 先输出到windows窗口帧缓存上,在要求windows窗口刷新, 这时才看到图像。而 GDI+ 是绘制到窗口帧缓存上的,一刷新窗口,GDI+绘制的内容就会被EGE帧缓存中的数据覆盖,相当于没画,所以会看到闪烁。

  这意味着这样绘图是没有什么用的,因为与EGE内部帧缓存上的内容不一致。

解决办法
  将 Gif 动图绘制在 EGE 的内部帧缓存上,而不是绘制在窗口帧缓存,这样窗口刷新时就能看到动图,而不会频繁闪烁。由前面我们知道,使用 GDI+ 中的 Graphics 绘图, 需要传入一个HDC参数,只要我们获得EGE内部帧缓存的HDC,即可以用GDI+将图像绘制到EGE内部帧缓存中。

  由前面的 ege_head.h , 我们可以得到EGE绘制相关的全局对象grap_setting, 通过它,我们可以获取EGE内存窗口帧缓存的HDC。

  获取EGE窗口帧缓存的设备句柄

HDC egedc = graph_setting.dc;

  同样的,因为 ege_head.h 中有 IMAGE 类,而PIMAGE 就是 IMAGE*, IMAGE类中有 getdc() 成员函数可以获取到图像的设备句柄

PIMAGE pimg = newimage(100, 100);
HDC imgdc = pimg->getdc();

  有了设备句柄有,就可以使用 Graphics 对象绘图到EGE帧缓存或图像中。

  下面是修改后的示例:(先将ege_head.h 放到 ege.h同一个目录下,并修正其中的错误)

#include <graphics.h>
#include <gdiplus.h>
#include "ege_head.h"

int main()
{
	initgraph(600, 600, INIT_RENDERMANUAL);
	setbkcolor(WHITE);
	Gdiplus::Bitmap gifImage(L"Gif1.gif");

	/*读取图像信息*/
	UINT count = gifImage.GetFrameDimensionsCount();
	GUID* pDimensionIDs = (GUID*)new GUID[count];
	gifImage.GetFrameDimensionsList(pDimensionIDs, count);
	//帧数
	int frameCnt = gifImage.GetFrameCount(&pDimensionIDs[0]);
	delete[] pDimensionIDs;

	//获取每帧的延时数据
	int size = gifImage.GetPropertyItemSize(PropertyTagFrameDelay);
	Gdiplus::PropertyItem* pItem = (Gdiplus::PropertyItem*)malloc(size);
	gifImage.GetPropertyItem(PropertyTagFrameDelay, size, pItem);

	//获取设备句柄
	HDC hdc = graph_setting.dc;
	//创建图形对象
	Gdiplus::Graphics graphics(hdc);
	
	int frame = 0;
	while (is_run()) {
		//设置活动帧
		gifImage.SelectActiveFrame(&Gdiplus::FrameDimensionTime, frame);

		//绘制到窗口上
		graphics.DrawImage(&gifImage, 0, 0, gifImage.GetWidth(), gifImage.GetHeight());

		//延时
		delay_ms(((long*)pItem->value)[frame] * 10);
		
		if (++frame >= frameCnt)
			frame = 0;
	}

	closegraph();

	return 0;
}


2.5 将Bitmap中某一帧图像输出

  Gif 类中的 getimage() 函数就是将Bitmap中某一帧图像输出的过程。

void Gif::getimage(PIMAGE pimg, int frame)
{
	if (frame < 0 || frameCount <= frame)
		return;
	
	int width = gifImage->GetWidth(), height = gifImage->GetHeight();

	if (width != getwidth(pimg) || height != getheight(pimg))
		resize(pimg, width, height);

	//自定义图像缓存区(ARGB)
	Gdiplus::BitmapData bitmapData;
	bitmapData.Stride = width * 4;
	int buffSize = width * height * sizeof(color_t);
	bitmapData.Scan0 = getbuffer(pimg);

	gifImage->SelectActiveFrame(&Gdiplus::FrameDimensionTime, frame);
	Gdiplus::Rect rect(0, 0, width, height);
	//以32位像素ARGB格式读取, 自定义缓存区
	//
	gifImage->LockBits(&rect, 
		Gdiplus::ImageLockModeRead | Gdiplus::ImageLockModeUserInputBuf, PixelFormat32bppARGB, &bitmapData);
	gifImage->UnlockBits(&bitmapData);
}

  getimage() 中,前面是检测pimg的尺寸合不合适,不合适则改变尺寸。然后就到了 Bitmap输出图像的部分
  Gdiplus::BitmapData 是使用来说明数据输出的目标的。
  bitmapData.Stride 表示一行中有多少个字节,不能被4整除的要凑够。因为EGE一个像素的颜色是用 color_t表示,即4个字节,所以直接为 width * 4
   bitmapData.Scan0 是输出目标的首地址,我们这里直接获取 PIMAGE 的图像缓存首地址。

  输出图像数据时, 先调用SelectActiveFrame 设置活动帧,然后 调用 LockBits 将图像数据锁定输出目标位置中,最后调用 UnlockBits() 解锁。

  LockBits 中有个 区域参数, 还有个图像锁定模式,取Gdiplus::ImageLockModeRead | Gdiplus::ImageLockModeUserInputBuf (将图像数据读取到自定义的图像输出缓存区中), 并设置像素颜色格式为 32位ARGB 格式,和PIMAGE 中的颜色格式一致,最后传入我们的 BitmapData的地址。




专栏:EGE专栏

上一篇:EGE绘图之三 动画

下一篇:EGE绘图之五 按钮(上)

  • 16
    点赞
  • 32
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 10
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 10
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

依稀_yixy

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值