专栏:EGE专栏
上一篇:EGE绘图之三 动画
下一篇:EGE绘图之五 按钮(上)
常常有播放动图的需要,但是EGE不能直接加载Gif文件形成动图。用getimage 读取 gif 图片只会读取第一帧,无法获取多帧。
前面说过,在EGE中,常用的是用 getimage() 读取每帧,保存到 PIMAGE 数组中。
所以想要制作动图,这就需要先把动图每一帧都拆分成图片,再加载到 PIMAGE 数组中。这种方法缺点是每一帧都要保存到PIMAGE 中,占用的内存较多。
比如, 一般屏幕分辨率为1920 * 1080, 那么一张全屏的图片,占用内存为 1920 * 1080 * 4B = 8294400B, 约为 7.9 MB. 所以加载时还要考虑内存占用。这种方式读取小图片是没有什么问题的,比如 300*300 * 4B =36000B , 约 0.34MB。
对于一个连续的动画,由多帧图像组成,可以将每帧的图像,按顺序在默认添加数字编号命名。比如有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);
}
这样我们便获取到了动画的每一帧图像,按顺序绘制即可形成动图。
Bitmap 是 GDI+ 中的一个类,可以读取各种图像文件,包括 gif 文件,我们可以借助它来读取 gif 动图。通过使用 Bitmap读取动图后,就可以绘制到窗口上。
下面是我写的一个 Gif 类,可以用来加载播放动图,是借用 Bitmap 加载的gif 图像。
下面是两个文件的内容,需要在项目中加入下面两个文件进行编译。
下面新建一个头文件 Gif.h
#pragma once
#ifndef _EGEUTIL_GIF_H_
#define _EGEUTIL_GIF_H_
#include
#include
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_
新建一个Gif.cpp
#define SHOW_CONSOLE
#include
#include
#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;
}
}
下面是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
#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() 来单独为某一帧设置延时时间。
有两种方式:
使用如下的构造函数
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];
加载图像使用的是 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];
默认是输出到窗口,这一步可以跳过
如果想要绘制到 PIMAGE 上的话,可以传入 PIMAGE的HDC (这个是ege_head.h 头文件中的)
PIMAGE pimg = newimage(100, 100);
gif.bind(pimg->getdc());
如果设置了绘制到图像上,想要设置回绘制到窗口,可以调用 bindWindow() .
gif.bindWindow();
绘制位置默认是 (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;
默认为暂停状态, 从调用 play() 开始计时
gif.play();
调用 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);
}
void Gif::setVisible();
bool Gif::isVisible() const;
//帧信息
int Gif::getFrameCount() const;
int Gif::getCurFrame() const;
int Gif::getDelayTime(int frame) const;
时间单位是ms, 因为数据存储中都是单位是10ms,所以time_ms需要是10的倍数,个位会被截断
void Gif::setDelayTime(int frame, long time_ms);
void Gif::setAllDelayTime(long time_ms); //设置全部帧延时
//播放状态控制
gif.play();
gif.pause();
gif.toggle();
gif.isPlaying();
重置后将切换为暂停状态,调用play() 后将重新播放
gif.resetPlayState();
gif.clear();
gif.info();
gif.getimage()
PIMAGE pimg = newimage();
int frame = 0; //第1帧
gif.getimage(pimg, frame);
前面已经说过 Bitmap 类加载 Gif 图像并绘制的方法。但如果我们想要得到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就可以直接用了
在 (四) EGE基础教程 中篇 后面的 EGE窗口刷新 相关内容中有提到,EGE中的有延时时间的 delay_ms(time) 和 delay_fps() 会强制刷新窗口,而 delay_ms(0) 和 getch() 则是根据标志位是否为 true 来决定是否刷新窗口。
而 Gif 类的绘制,没有通过EGE绘图函数,所以标志位是不会变为 true 的。所以像下面的程序,按一次显示一帧,是不会看到有变化的。
下面是小老鼠图片,一共100只小老鼠,可以自行保存起来(鼠标右键,另存为)(鼠年快乐)。
因为只用 getch() 刷新窗口,中间也没有用到EGE的绘制函数,所以除了第一次setbkcolor()使标志位为 true 外, 其它时候不会刷新。需要进行强制刷新。
#include
#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;
}
#include
#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
#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;
}
这部分属于实现原理讲解,可以不看。==
获取时间使用的是
clock_t sysTime = clock();
主要由下面四个变量记录(类型为 clock_t):
- pauseTime
暂停时间,用于播放暂停,记录暂停时的时间- frameBaseTime
帧基准时间,记录当前帧是从何时开始播放- curDelayTime
当前帧的已延时时间, 为从 frameBaseTime 开始所经过的时间- frameDelayTime,
当前帧的延时时间, 是从Gif图像中获取的帧延时时间数据,
初始四个时间值都为0。
下面是 Gif 类中的 play() 函数
void Gif::play()
{
clock_t sysTime = clock();
if (frameBaseTime == 0) {
pauseTime = frameBaseTime = sysTime;
frameDelayTime = getDelayTime(curFrame);
}
else if (!playing){
playing = true;
frameBaseTime += sysTime - pauseTime;
}
}
暂停时就把播放状态切换为暂停,记录下暂停的时间即可
void Gif::pause()
{
if (playing) {
playing = false;
this->pauseTime = clock();
}
}
方便进行播放状态切换。
void Gif::toggle()
{
playing ? pause() : play();
}
curFrame 为当前帧,初始值为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 的活动帧,再绘制即可
Gif 类时使用的 Bitmap 类来加载Gif图像, 下面就讲解 Bitmap 的使用。
Bitmap 是在 Gdiplus 命名空间中,需要包含
Bitmap 的构造函数中有带 const WCHAR* 参数的,传入需要加载的 gif 文件名即可, 不过需要注意是 WCHAR 型的,字符串前要加个 L 。
Gdiplus::Bitmap gifImage(L"gifFileName.gif");
加载图像后,我们需要知道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;
对,因为指针 value 是 void* 型的,我们需要把它转成 long* 型的指针。里面的延时时间单位是10 毫秒,而我们用的是单位是毫秒,所以要乘以10。
//尺寸
int gifWidth = gifImage.GetWidth();
int gifHeight = gifImage.GetHeight();
GDI+的绘图需要有设备句柄,而设备句柄的创建需要依赖窗口句柄。
EGE的窗口句柄我们可以通过ege的 getHWnd() 获得:
先创建设备句柄:
//获取设备句柄
HWND egeHWnd = getHWnd();
//获取设备句柄
HDC hdc = GetDC(egeHWnd);
使用完后可以使用 RealseDC() 来释放
然后用设备句柄创建 graphics 对象
//创建Graphics对象
Gdiplus::Graphics graphics(hdc);
此时绘图的准备就已经好了
因为动图是有很多帧的,想要画某一帧时,需要将其设置为活动帧。
使用的是SelectActiveFrame() 函数, 有个const GUID* 参数,传入全局变量 Gdiplus::FrameDimensionTime 的地址就好
gifImage.SelectActiveFrame(&Gdiplus::FrameDimensionTime, i);
graphics.DrawImage(&gifImage, x, y, gifImage.GetWidth(), gifImage.GetHeight());
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;
}
整个完整的程序如下:
将 “gifFileName.gif” 换成自己的Gif文件名。
#include
#include
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;
}
上面的例程可以将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
#include
#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;
}
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绘图之五 按钮(上)