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.在写项目的时候一定要细心,在文件路径的书写过程中,少写了一个空格而导致编译失败。