当前位置: 首页>編程日記>正文

EGE绘图之四 Gif动图播放

EGE绘图之四 Gif动图播放

专栏:EGE专栏

上一篇:EGE绘图之三 动画

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

目录

  • 一、Gif绘制
    • 1. 动图加载
      • 1.1 使用 getimage() 加载每一帧的图像
      • 1.2 借助GDI+中的Bitmap类加载GIF动图
    • 2. Gif类
      • 2.1 Gif.h
      • 2.2 Gif.cpp
      • 2.3 Gif 的使用
        • 2.3.1 使用示例:
        • 2.3.2 初始化
        • 2.3.3 加载图像
        • 2.3.4 绑定设备
        • 2.3.5 设置绘制位置,大小
        • 2.3.6 切换至播放状态
        • 2.3.7 绘制图像
      • 2.4 其他函数
        • 2.4.1 可见性
        • 2.4.2 帧数, 当前帧,当前帧的延时时间
        • 2.4.3 设置每帧,或者设置全部帧的延时时间
        • 2.4.4 播放状态控制(播放、暂停、切换)
        • 2.4.5 查询当前是否播放
        • 2.4.6 重置播放状态
        • 2.4.7 清空图像数据
        • 2.4.8 控制台输出Gif图像信息
          • 2.4.9 生成某一帧的PIMAGE
      • 2.5 由Gif类生成某一帧的PIMAGE
    • 3. Gif的刷新显示
  • 二、实现原理分析
    • 1. Gif类的分析
      • 1.1 Gif中的时间系统
        • 1.1.1 当前运行时间获取
        • 1.1.2 时间记录
        • 1.1.3 播放时的时间记录
        • 1.1.4 暂停时的时间记录
      • 1.2 播放状态切换
      • 1.3 当前帧的计算
    • 2. Bitmap类的使用
      • 2.1 加载图像
      • 2.2 读取图像大小,帧数,帧延时等数据
      • 2.3 将Bitmap绘制在EGE窗口
      • 2.4 Gif 绘图示例
        • 2.4.1 例程分析
      • 2.5 将Bitmap中某一帧图像输出

一、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;elsereturn ((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);}elseframeBaseTime += 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绘图之五 按钮(上)


https://www.fengoutiyan.com/post/15872.html

相关文章:

  • gif动画怎么做
  • gif动态图
  • gif图片怎么制作
  • geogebra制作动态图
  • gif动图制作教程
  • uiimageview gif
  • 鏡像模式如何設置在哪,圖片鏡像操作
  • 什么軟件可以把圖片鏡像翻轉,C#圖片處理 解決左右鏡像相反(旋轉圖片)
  • 手機照片鏡像翻轉,C#圖像鏡像
  • 視頻鏡像翻轉軟件,python圖片鏡像翻轉_python中鏡像實現方法
  • 什么軟件可以把圖片鏡像翻轉,利用PS實現圖片的鏡像處理
  • 照片鏡像翻轉app,java實現圖片鏡像翻轉
  • 什么軟件可以把圖片鏡像翻轉,python圖片鏡像翻轉_python圖像處理之鏡像實現方法
  • matlab下載,matlab如何鏡像處理圖片,matlab實現圖像鏡像
  • 圖片鏡像翻轉,MATLAB:鏡像圖片
  • 鏡像翻轉圖片的軟件,圖像處理:實現圖片鏡像(基于python)
  • canvas可畫,JavaScript - canvas - 鏡像圖片
  • 圖片鏡像翻轉,UGUI優化:使用鏡像圖片
  • Codeforces,CodeForces 1253C
  • MySQL下載安裝,Mysql ERROR: 1253 解決方法
  • 勝利大逃亡英雄逃亡方案,HDU - 1253 勝利大逃亡 BFS
  • 大一c語言期末考試試題及答案匯總,電大計算機C語言1253,1253《C語言程序設計》電大期末精彩試題及其問題詳解
  • lu求解線性方程組,P1253 [yLOI2018] 扶蘇的問題 (線段樹)
  • c語言程序設計基礎題庫,1253號C語言程序設計試題,2016年1月試卷號1253C語言程序設計A.pdf
  • 信奧賽一本通官網,【信奧賽一本通】1253:抓住那頭牛(詳細代碼)
  • c語言程序設計1253,1253c語言程序設計a(2010年1月)
  • 勝利大逃亡英雄逃亡方案,BFS——1253 勝利大逃亡
  • 直流電壓測量模塊,IM1253B交直流電能計量模塊(艾銳達光電)
  • c語言程序設計第三版課后答案,【渝粵題庫】國家開放大學2021春1253C語言程序設計答案
  • 18轉換為二進制,1253. 將數字轉換為16進制
  • light-emitting diode,LightOJ-1253 Misere Nim
  • masterroyale魔改版,1253 Dungeon Master
  • codeformer官網中文版,codeforces.1253 B
  • c語言程序設計考研真題及答案,2020C語言程序設計1253,1253計算機科學與技術專業C語言程序設計A科目2020年09月國家開 放大學(中央廣播電視大學)
  • c語言程序設計基礎題庫,1253本科2016c語言程序設計試題,1253電大《C語言程序設計A》試題和答案200901
  • 肇事逃逸車輛無法聯系到車主怎么辦,1253尋找肇事司機