项目之动态图片的制作

1.动态图片制作的背景

随着微信的发展,越来越多的人们喜欢斗图并且使用动态图片,能用一张图说明的,就不用文字来逼逼,我的做出来一个可执行程序,可以通过这个可执行程序来制作动图。
首先呢,制作动图分为图片制作、视频制作这两部分。
那么在制作动图之前,我们先来认识一下工具吧。

2.工具介绍

2.1 win32应用程序

2.1.1 介绍
一个Win32应用程序可以分为程序代码和UI资源两大部分,两部分终是以rc整合成一个完整的exe可执行程序。所谓UI资源,指的是功能菜单、对话框外貌、程序图标、光标形状等东西,程序员在一个.rc的资源描述文档中描述它们。rc编译器读取rc文档的描述后将所有的rc资源集中制作成一个.res的文档,再与程序代码结合。
注意:Win32程序的入口点是WinMain,WinMain的四个参数由操作系统负责传递,main是控制台程序的入口点。
2.1.2 win32一般步骤
1.设计创建类-----完善串口类的结构体----窗口类的名字(唯一)+提供窗口消息的响应函数(回调函数)
2. 注册窗口类-----窗口类的名字、提供窗口过程处理函数
3. 创建窗口CreateWindow()
4. 显示窗口ShowWindow()
5. 更新窗口UpdateWindow()
6. 进入消息循环while(Getmassage)相当于是一个死循环
7.消息响应,在用户自定义的窗口过程处理函数中,用户对自己需要处理的消息进行拦截响应,对不关心的消息采用系统默认的消息响应函数DefWindowProc()处理
2.1.3 win32的消息机制
** 消息介绍**
消息:系统内设的一种数据结构:

typedef struct MSG {     HWND hwnd;
//hwnd 是窗口的句柄,这个参数将决定由哪个窗口过程函数对消息进行处理     
 UINT message;      //message是一个消息常量,用来表示消息的类型      
 WPARAM wParam;     //32 位的附加信息,具体表示什么内容,要视消息的类型而定      
 LPARAM lParam;     //32 位的附加信息,具体表示什么内容,要视消息的类型而定      
 DWORD time;       //time 是消息发送的时间      
 POINT pt;         //消息发送时鼠标所在的位置 
 } 

从上面的代码中可以看出,,消息类型其实就是一个UINT类型的变量。系统定义消息值的范围是:0x00000x03ff,用户自定义消息值的范围是:0x0400-0x07ff。
消息类型
消息类型分为:系统定义消息和用户自定义消息。
而系统定义消息分为:窗口消息、命令消息、控件通知消息。
窗口消息:与窗口的内部运作有关的消息
命令消息:当用户从菜单选中一个命令项目、按下一个快捷键、点击工具栏上的一个按钮或者点击控件都将发送 WM_COMMAND命令消息,通过消息结构中的wParam和lParam成员就能清楚得知道消息的来源。
控件通知消息:为了给父窗口发送更多的信息,微软定义了一个新的 WM_NOTIFY消息来扩展WM_COMMAND消息。M_NOTIFY消息仍然使用MSG消息结构,只是此时 wParam为控件ID,lParam为一个NMHDR指针,不同的控件可以按照规则对NMHDR进行扩充,因此 WM_NOTIFY消息传送的信息量可以相当的大。
消息队列
消息队列是用来存放消息的一个队列,系统中维护着一个全局的系统消息队列,还会为每一个UI线程维护一个UI线程消息队列:
系统消息队列:由操作系统维护的消息队列,存放系统产生的消息,如鼠标单击移动、键盘按下消息等 等。 用户消息队列:属于每一个应用程序(线程)的消息队列,用应用程序维护。
当系统消息队列中存在消息时,系统会根据消息所属的UI线程(hWnd:窗口句柄),分发到应用程序对应的UI 线程消息队列中去。
2.1.4 win32的缺陷
在windows上进行图形界面开发,使用Win32开发效率极低,而传统的MFC界面库又存在以下缺陷:
1.不美观
2.界面细节处理不好
3.开发效率低下
4.生成程序体积大
5.MFC界面美化库使用HOOK技术,可能会导致系统不稳定或者引发其他错误

2.2 Duilib库

2.2.1 介绍
Duilib库一款强大轻量级的界面开发工具,可以将用户界面和处理逻辑 彻底分离,极大地提高用户界面的开发效率。提供所见即所得的开发工具UIDesigner。Duilib界面库可广泛用于互联网客户端、工具软件客户端、管理系统客户端、多媒体客户端(如KTV、触摸 屏)、车载电脑系统、gps系统和手机客户端软件等,并且Duilib仅仅是基于Win32的一套UI库,并不是使用了Duilib后它就不是Win32程序。
2.2.2 Duilib界面库框架
Duilib的库目录中主要包含了4个目录:
Control:Duilib各个控件对应的UI类,比如:按钮(CButtonUI)、编辑框(CEditUI)、下拉框 (CComboBoxUI)等。
Layout:Duilib的各种布局器所对应的UI类,比如:水平布局(CHorizontalLayoutUI)、垂直布局 (CVerticalLayoutUI)等。
Core:Duilib库的核心操作:窗口相关(Win32流程封装–CWindowWnd)、窗口解析(CMarkup)等。
Utils:Duilib封装的一些类型:CDUIString、CPoint、压缩等。
2.2.3 Duilib的环境搭建
在项目中使用Duilib界面库之前,必须要对Duilib库进行编译,编译完成后,只需将生产的静态库和Duilib源 文件目录包含到工程中,将生成的dll包含到工程目录下即可,具体操作如何下
1.讲Duilib源文件和生成的lib文件夹拷贝到工程目录下
项目之动态图片的制作
2.对环境进行配置
项目之动态图片的制作
3.将编译后duilib-master文件夹下bin目录中的dll文件拷贝到项目的exe文件中
项目之动态图片的制作
4.增加一个新的头文件

#include  "UIlib.h"
using namespace DuiLib;//包含1duilib的头文件已经命名空间
#pragma comment(lib,"DuiLib_ud.lib")//引入库目录

2.2.4一个简单窗口的创建

/INotifyUI:duilib自己定义的类-->抽象类
class CDuiFramWnd : public CWindowWnd, public INotifyUI
{
public:
	// CWindowWnd类的纯虚函数,在该函数中必须返回用户所定义窗口的类名称,注册窗口时需要用到
	//返回窗口类的名字
	virtual LPCTSTR GetWindowClassName() const
	{
		return _T("DuiFramWnd");//_T宏替换将格式转换为duilib格式
	}
	// uMsg:获取到的消息ID--->区分捕获到的是什么类型的消息
	virtual LRESULT HandleMessage(UINT uMsg, WPARAM wParam, LPARAM lParam)//子类如果需要处理系统消息的时候,需要重写
	{
		if (WM_CREATE == uMsg)
		{
			m_PaintManager.Init(m_hWnd);

			CDialogBuilder builder;    
			// duilib.xml需要放到exe目录下    
			CControlUI* pRoot = builder.Create(_T("duilib.xml"), (UINT)0, NULL, &m_PaintManager); 
			m_PaintManager.AttachDialog(pRoot);   
			m_PaintManager.AddNotifier(this);     
				return 0;
		}
		//去掉标题栏
		else if (uMsg == WM_NCACTIVATE)        
		{ 
			if(!::IsIconic(m_hWnd))           
			{
				return (wParam == 0) ? TRUE : FALSE; 
			}
		}
		else if (uMsg == WM_NCCALCSIZE)       
		{
			return 0; 
		}
		else if (uMsg == WM_NCPAINT)
		{
			return 0; 
		}

		//拦截绘画相关的消息
		LRESULT lRes = 0;
		if (m_PaintManager.MessageHandler(uMsg, wParam, lParam, lRes))
		{
			return lRes;
		}
		//其他消息
		//__super::指调用基类的
		return __super::HandleMessage(uMsg, wParam, lParam);
	}
	virtual void Notify(TNotifyUI& msg)  //如果需要拦截duilib自己维护的消息时,只需要在子类中重写Notify
	{
		//响应按钮点击消息
		if (msg.sType == _T("click"))       
		{
			MessageBox(m_hWnd, _T("Hello World"), _T("DuiFramWnd"), IDOK);//弹窗测试
		}
	}

private:
	CPaintManagerUI m_PaintManager;
};

int APIENTRY _tWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPTSTR lpCmdLine, int
	nCmdShow)
{
	CPaintManagerUI::SetInstance(hInstance); 
	// 设置资源的默认路径(此处设置为和exe在同一目录)
	CPaintManagerUI::SetResourcePath(CPaintManagerUI::GetInstancePath()); 
	CDuiFramWnd framWnd;
	// Cashier即在窗口右上角显式的名字
	// UI_WNDSTYLE_FRAME: duilib封装的宏,代表窗口可视,具有标题栏,最大化最小化,关闭功能等
	// WS_EX_WINDOWEDGE: Win32的窗口风格,带有边框
	framWnd.Create(NULL, _T("Cashier"), UI_WNDSTYLE_FRAME, WS_EX_WINDOWEDGE);
	//显示窗口,激活消息循环
	framWnd.ShowModal();
	return 0;
}

2.3 ffmpeg

2.3.1 介绍
ffmpeg即是一块音视频编解码工具,同时也是一组音视频编解码开发套件,为开发者提供了丰富的音视频 处理调用接口。FFmpeg中的"FF"指的是"Fast Forward",mpeg则是动态图像专家组。 它提供了录制、转换 以及流化音视频的完整解决方案。 它包含了非常先进的音频/视频编解码库 libavcodec, 为了保证高可移植 性和编解码质量, libavcodec 里很多 codec 都是从头开发的。

3.实现原理

3.1 用图片生成
项目之动态图片的制作
3.2 用视频生成
项目之动态图片的制作
3.3 界面
界面实现是利用UIdesigner画出来的
项目之动态图片的制作

4.功能实现

4.1 界面实现
界面的实现主要是应用于duilib这套库进行实现的,duilib主打的界面制作方式是XML + UI引擎 + win32框架,通过XML的方式来重写窗口,然后Duilib对XML进行解析,将窗口创建成功。
针对于Duilib 有一个界面设计的可视化工具:Designer。使用Duilib自带的界面布局器打开上述XML文件:可通过该界面布局器,快速方便的对界面进行布局,完成后保存成XML即可,Duilib程序就可以解析。
项目之动态图片的制作
注意:Duilib的界面布局器存在bug,不经意间可能会崩溃,需要对修改的内容及时保存。
那么界面的实现就是通过上述工具进行绘制出来的。
4.2 使用cmd给ffmpeg发送命令

void SendMessage(CDuiString& strCMD)
	{
		// 1. 初始化结构体    
		SHELLEXECUTEINFO strSEInfo; 
		memset(&strSEInfo, 0, sizeof(SHELLEXECUTEINFO));   
		strSEInfo.cbSize = sizeof(SHELLEXECUTEINFO);   
		strSEInfo.fMask = SEE_MASK_NOCLOSEPROCESS;  //掩码  
		strSEInfo.lpFile = _T("C:\\WINDOWS\\system32\\cmd.exe"); //命令工具的位置
		strSEInfo.lpParameters = strCMD;  // 具体命令   
		strSEInfo.nShow = SW_HIDE;  // 控制台窗口隐藏

		// 2. 给cmd发送命令  
		ShellExecuteEx(&strSEInfo); //调用命令行窗口,执行用户命令,在该函数中,会新创建一个进程,来负责调用命令行窗口执行命令
		//等地命令响应完成
		//WaitForSingleObject(strSEInfo.hProcess,INFINITE);
		MessageBox(m_hWnd, _T("命令响应完成"), _T("GIF"), IDOK);

	}

4.3 对界面的按钮进行响应
1、加载按钮
步骤:
定义OPENFILENAME结构体变量ofn,并对其进行初始化
将ofn的地址作为参数调用GetOpenFileName函数,弹出打开文件对话框
在弹出的对话框中找到文件位置 从ofn的lpstrFile参数中提取文件位置
对获取的路径进行检测后,将其显式在编辑框中。
代码实现:

void LoadFile()
	{
		OPENFILENAME ofn;
		memset(&ofn, 0, sizeof(OPENFILENAME));

		TCHAR strPath[MAX_PATH] = { 0 };
		ofn.lStructSize = sizeof(OPENFILENAME);
		ofn.lpstrFile = strPath;
		ofn.nMaxFile = sizeof(strPath);
		ofn.lpstrFilter = _T("All(*.*)\0*.*\0mkv(*.mkv)\0 *.mkv\0");
		ofn.Flags = OFN_PATHMUSTEXIST | OFN_FILEMUSTEXIST;

		if (GetOpenFileName(&ofn))
		{
			//将文件的路径设置到edit
			 ((CEditUI*)m_PaintManager.FindControl(_T("edit_path")))->SetText(strPath);


		}
	}

2、截取按钮
截取按钮通过cmd给ffmpeg发送命令来进行截取所需视频的。
命令解析:
ffmpeg -i input.mkv -vcodec copy -acodec copy -ss 00:40:07 -to 00:40:28 11.mkv -y
-i 输入
11.mkv 需要截取视频的路径
-vcode copy -acode copy 表示所要使用的视频和音频的编码格式,copy表示原样拷贝
-ss 00:40:07 代表起始时间 -
to 00:40:28 代表结束时间 即总共截取了21秒 -
y: 代表如果目录中有该文件,将源文件覆盖掉
代码实现:

void CutView()
	{
		CDuiString strPath = CPaintManagerUI::GetInstancePath();//获取路径
		strPath += _T("ffmpeg\\");
		CDuiString strViewPath = ((CEditUI*)m_PaintManager.FindControl(_T("edit_path")))->GetText();
		//1.构造命令
		//ffmpeg -i input.mkv -vcodec copy -acodec copy -ss 00:36:55 -to 00:37:07 11.mkv -y
		CDuiString strCMD;
		strCMD += _T("/c ");//\c参数来让命令执行
		strCMD += strPath;
		strCMD += _T("ffmpeg -i ");

		//视频的路径,优先在界面中加载
		if (!strViewPath.IsEmpty())
		{
			strCMD += strViewPath;
		}
		else
		{
			strCMD += strPath;//视频所在的路径
			strCMD += _T("input.mkv");
		}
		
		strCMD += _T(" -vcodec copy -acodec copy -ss ");
		//获取起始时间和结尾时间
		CDuiString strStartTime = ((CEditUI*)m_PaintManager.FindControl(_T("edit_start")))->GetText();
		if (!IsValidTime(strStartTime))
		{
			MessageBox(NULL, _T("起始时间有误"), _T("GIF"), IDOK);
			return;
		}
		CDuiString strEndTime = ((CEditUI*)m_PaintManager.FindControl(_T("edit_end")))->GetText();
		if (!IsValidTime(strEndTime))
		{
			MessageBox(NULL, _T("终止时间有误"), _T("GIF"), IDOK);
			return;
		}
		strCMD += strStartTime;
		strCMD += _T(" -to ");
		strCMD += strEndTime;
		strCMD += _T(" ");

		//输出文件的路径
		strCMD += strPath;
		strCMD += _T("11.mkv -y");

		//2.给cmd发送命令
		SendMessage(strCMD);

	}

3、提取SRT按钮
提取SRT就是字幕的提取,字幕又分为很多种类:
(1)硬字幕:是将字幕覆盖叠加在视频画面上。因为这种字幕与视频画面溶于一体,所以具有最佳的兼容性,只要能够播放视频,就能显示字幕,对于现阶段的手机、MP4播放器而言,只支持这类型的字幕。 缺点是字幕占据视频画面,破坏了视频内容,而且不可取消、不可编辑更改。
(2)外挂字幕:将字幕单独做成一个文件,字幕文件有多种格式。这类字幕的优点是不破坏视频画面,可随时根据 需要更换字幕语言,并且可随时编辑字幕内容。缺点是播放较为复杂,需要相应的字幕播放工具支持。
(3)软字幕:通过某种方式将外挂字幕与视频打包在一起,下载、复制时只需要复制一个文件即可。如DVD中 的VOB文件,高清视频封装格式MKV、TS、AVI等。这类型文件一般可以同时封装多种字幕文件, 播放时通过播放器选择所需字幕,非常方便。在需要的时候,还可以将字幕分离出来进行编辑修改或替换。
我这里操作的是外挂字幕,提取字幕命令:ffmpeg -i 11.mkv input.srt -y
代码实现:

void GetSRTFile()
	{
		CDuiString strCMD;
		strCMD += _T("/c cd ");
		strCMD += CPaintManagerUI::GetInstancePath() + _T("ffmpeg");
		strCMD += _T(" & ");
		// ffmpeg -i 11.mkv input.srt -y
		strCMD += _T("ffmpeg -i 11.mkv input.srt -y");

		SendMessage(strCMD);
	}

4、写入STR按钮
这个按钮是在编辑框中对字幕进行修改之后,我们要将修改好的字幕重新写入SRT文件中,在这里存在字幕格式转换问题。需要利用WideCharToMultiByte函数来进行转换。
代码实现:

void WriteSRT()
	{
		CDuiString strPath = CPaintManagerUI::GetInstancePath();//获取路径
		strPath += _T("ffmpeg\\input.srt");

		std::ofstream fOut(strPath.GetData());
		//1从list中获取文本
		CListUI* pList = (CListUI*)m_PaintManager.FindControl(_T("list_srt"));
		int szCount  = pList->GetCount();
		for (int i = 0; i < szCount; i++)
		{
			CListTextElementUI* pListItem = (CListTextElementUI*)pList->GetItemAt(i);

			//序号
			CDuiString strNo;
			strNo.Format(_T("%d"), i + 1);
			//时间轴
			CDuiString strTime = pListItem->GetText(0);
			//文本
			CDuiString strWord = pListItem->GetText(1);
			//2.将获取中的内容写回到srt中
			string strNewLine = Unicode2ANST(_T("\n"));
			//写行号
			//Unicode2ANST:将unicode转化为ANSI,要不然就会出现乱码状态
			string itemNo = Unicode2ANST(strNo);
			fOut.write(itemNo.c_str(),itemNo.size());
			fOut.write(strNewLine.c_str(), strNewLine.size());
			//转换时间轴
			string itemTime = Unicode2ANST(strTime);
			fOut.write(itemTime.c_str(), itemTime.size());
			fOut.write(strNewLine.c_str(), strNewLine.size());
			//写文本
			string itemWord = Unicode2ANST(strWord);
			fOut.write(itemWord.c_str(), itemWord.size());
			fOut.write(strNewLine.c_str(), strNewLine.size());
			//每条字幕之间都有换行
			fOut.write(strNewLine.c_str(), strNewLine.size());
		}
		fOut.close();
	}

5、提取视频按钮
命令:
ffmpeg -i 11.mkv -vcodec copy -an -sn 22.mkv -y
-an: 表示取消音频
-sn: 表示取消字幕
代码实现:

	void GenerateView()
	{
		//ffmpeg -i 11.mkv -vcodec copy -an -sn 22.mkv -y
		CDuiString strCMD;
		strCMD += _T("/c cd ");
		strCMD += CPaintManagerUI::GetInstancePath() + _T("ffmpeg");
		strCMD += _T(" & ");
		strCMD += _T("ffmpeg -i 11.mkv -vcodec copy -an -sn 22.mkv -y");
		//2.给cmd发送命令
		SendMessage(strCMD);
	}

6、烧录按钮
命令: ffmpeg -i 22.mkv -vf subtitles=input.srt 33.mkv -y
代码实现:

void BornSRT2View()
	{
		//ffmpeg -i 22.mkv -vf subtitles=input.srt 33.mkv -y
		CDuiString strCMD;
		strCMD += _T("/c cd ");
		strCMD += CPaintManagerUI::GetInstancePath() + _T("ffmpeg");
		strCMD += _T(" & ");
		
		strCMD += _T("ffmpeg -i 22.mkv -vf subtitles=input.srt 33.mkv -y");
		//2.给cmd发送命令
		SendMessage(strCMD);
	}

7.生成gif按钮
命令:
使用图片生成gif:
ffmpeg -r 3 -i .\Pictrue%d.jpg output.gif -y -r 控制帧数
代码实现:

void GenerateGifWithPic()
	{
		// fmpeg -r 3 -i .\Pictrue\%d.jpg output.gif -y
		CDuiString strCMD;
		strCMD += _T("/c cd ");
		strCMD += CPaintManagerUI::GetInstancePath() + _T("ffmpeg");
		strCMD += _T(" & ");
		
		strCMD += _T("fmpeg -r 3 -i .\Pictrue\%d.jpg output.gif -y");
		//2.给cmd发送命令
		SendMessage(strCMD);
	}

使用视频生成gif:
ffmpeg -i 33.mkv -vf scale=iw/2:ih/2 -f gif output.gif -y
代码实现:

//生成动态图
	void GernerateGifWithView()
	{
		// ffmpeg -i 33.mkv -vf scale=iw/2:ih/2 -f gif output.gif -y
		CDuiString strCMD;
		strCMD += _T("/c cd ");
		strCMD += CPaintManagerUI::GetInstancePath() + _T("ffmpeg");
		strCMD += _T(" & ");
		strCMD += _T("ffmpeg -i 33.mkv -vf scale=iw/2:ih/2 -f gif output.gif -y");
		//2.给cmd发送命令
		SendMessage(strCMD);
	}

5.结果展示

5.1 使用图片生成gif:项目之动态图片的制作
使用视频生成gif:(这里的视频是具有外挂字幕的视频)
这个由于内存过大无法上传,但是道理在上面已经解释过。

6.遇到的问题

1、在程序中用命令没有给控制台发送过去?
解决:发现\c没有加
2、Designer库容易崩溃,有一次写到一半崩溃了
没有办法解决,只能边画边保存
3、提取之后SRT文件的时候发现文字乱码?
解决:将UFT-8转化为Unicode格式,利用MultiByteToWideChar函数
4.在写项目的时候一定要细心,在文件路径的书写过程中,少写了一个空格而导致编译失败。

上一篇:正式发布DuiLib脚本系统 结合angelscript


下一篇:nim_duilib(10)之slider、progress and circleprogress