项目背景
文字信息时代,传统的文字聊天方式已不能满足大众的需求,很多时候文字不能表达自己的想法,或者沟通技巧的欠缺,这时表情包就派上用场了
“一言不合就斗图”,可以说斗图也成为了交流中的一种乐趣
暴走表情广泛的遍布于网络,网民们大多用作斗图。常见于QQ、微信等聊天工具。斗图活动起始于QQ,群聊时大家发送搞趣图片以相互娱乐。后来发展到百度贴吧等各种论坛上
斗图发展到现在不仅仅用系统本身提供的表情图,还会恶搞明星、动漫人物。以图加文字的自由组合,结合网络语言,制作搞笑逗比的图片
可行性分析
经济可行性——低成本
操作可行性——简单
技术可行性——借助其他工具
需求分析与系统设计
制作Gif动图,因此选择两种生成方式:1.图片生成;2.视频生成
技术方面借助其他的工具实现:选择 ffmpeg 工具
用户在选择后输入文件所在目录,若是图片,直接生成动态图;若是视频,截取视频片段,提取视频裸流,直接生成Gif或提取视频字幕【视频中的字幕必须是外挂字幕,否则使用的工具无法提取】由用户编辑文字显示在视频上最后生成Gif
概要设计
根据对需求的分析设计功能图:
工具
ffmpeg
ffmpeg是一个特别强大的专门用于处理音视频的开源库。可以用它的API对音视频进行处理,也可以使用它提供的工具,如 ffmpeg, ffplay, ffprobe,来编辑音视频文件。
命令:
- 图片生成动态图:
ffmpeg -r 3 -i .\Picture %d.jpg output.gif -y
#-r后数字:一次显示的帧数
#-i:输入
#.\Picture %d.jpg:图片路径
#output.gif:生成Gif名字
#-y:若目录下有与生成Gif名相同文件就覆盖
- 视频生成Gif
#从原视频截取片段
ffmpeg -i input,mkv -vcodec.copy -acodec copy -ss 00:01:01 -to 00:01:11 cut.mkv -y
#-vcodec.copy -acodec copy:将音视频拷贝进来
#-ss xx:xx:xx -to xx:xx:xx:剪切的视频
#cut:剪切下得片段
#提取STR
ffmpeg -i cut.mkv input.srt -y
#input.srt中:识别出的字幕
#编辑字幕
#仅提取视频不要字幕、音频
ffmpeg -i cut.mkv -vcodec copy -an -sn cut2.mvk -y
#修改字幕后放入视频
ffmpeg -i cut2.mkv -vf subtitles = input.srt cut3.mkv -y
#生成Gif
ffmpeg -i cut3.mkv -vf scale = iw/2:ih/2 -f gif
需要用cmd控制台执行这些命令,后续需要函数实现向控制台发送命令。
Duilib
Duilib是一款强大轻量级的界面开发工具,可以将用户界面和处理逻辑彻底分离,极大地提高用户界面的开发效率。提供所见即所得的开发工具UIDesigner。duilib主打的界面制作方式是XML+UI引擎+win32框架,通过XML的方式来重写窗口,然后Duilib对XML进行解析,将窗口创建成功。
这里对Win32进行简单的理解:必备的Win32程序设计原理
在下载后先将duilib运行一遍。注意运行时将其设置为启动项,并且进行批生成
VS环境配置
环境配置好后可以开始使用。
使用时添加头文件将DuiLib库包含进来:
#include "UIlib.h"
using namespace DuiLib;
#include <fstream>
#pragma comment(lib, "DuiLib_ud.lib")
各目录
- Control:Duilib各个控件对应的UI类,比如:按钮(CButtonUI)、编辑框(CEditUI)、下拉框(CComboBoxUI)等
- Layout:Duilib的各种布局器所对应的UI类,比如:水平布局(CHorizontalLayoutUI)、垂直布局(CVerticalLayoutUI)等
- Core:Duilib库的核心操作:窗口相关(Win32流程封装–CWindowWnd)、窗口解析(CMarkup)等。
- Utils:Duilib封装的一些类型:CDUIString、CPoint、压缩等
编辑项目的界面:需要与项目.exe相同目录下创建一xml文件,用生成的DuiDesigner_d应用程序对.xml文件进行编辑
界面编辑器简单介绍了如何进行编辑
实现
界面
最终生成的界面:
编码实现
主函数
WinMain是Windows程序的入口点。当Windows的“外壳”(Shell,如资源管理器)侦察到使用者要执行一个Windows程序,用加载器把该程序加载,然后调用C startup code,再由C startup code调用WinMain,开始执行程序。WinMain函数有四个固定的参数,而且这四个参数都是由操作系统负责传递进来
int APIENTRY _tWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPTSTR lpCmdLine, int
nCmdShow)
{
//对应xml文件,因为要找到路径
CPaintManagerUI::SetInstance(hInstance);
// 设置资源的默认路径(和exe在同一目录)
CPaintManagerUI::SetResourcePath(CPaintManagerUI::GetInstancePath());
CDuiFramWnd framWnd;
// UI_WNDSTYLE_FRAME: duilib封装的宏,代表窗口可视,具有标题栏,最大化最小化,关闭功能等
framWnd.Create(NULL, _T("MakeGif"), UI_WNDSTYLE_FRAME, WS_EX_WINDOWEDGE);
framWnd.CenterWindow();
//显示窗口,激活消息循环
framWnd.ShowModal();
return 0;
}
窗口类
新建一个继承自WindowImplBase的类CDuilibDemoWnd
让用户实现的窗口类继承自Duilib封装的:WindowImplBase类即可,该类是一个duilib的基础框架类,封装了常用操作,使用方便。 它是以XML作为界面描述的,所以用它的时候,我们必须将界面描述写到XML里
class CDuiFramWnd : public WindowImplBase
函数
xml文件窗口
//需要返回xml所在文件夹
virtual CDuiString GetSkinFolder()
{
return _T("");//这里不用给xml文件的路径,在主函数中已经给了
}
//需要返回xml文件名
virtual CDuiString GetSkinFile()
{
return _T("gifMake.xml");//一定要与目录放的xml文件文件名称相同
}
//需要返回窗口的类名
virtual LPCTSTR GetWindowClassName(void)const
{
return _T("GIFMakeWnd");
}
消息处理函数
virtual void Notify(TNotifyUI& msg)
{
CDuiString strName = msg.pSender->GetName();//获取控件的名字
if (msg.sType == _T("click"))//鼠标点击
{
if (strName == _T("btn_close"))//关闭按钮
{
Close();
}
else if (strName == _T("btn_min"))//最小化按钮
{
SendMessage(WM_SYSCOMMAND, SC_MINIMIZE, 0);
}
else if (strName == _T("btn_load"))
{
LoadFile();
}
else if (strName == _T("btn_cut"))//截取视频
{
Cutview();
}
else if (strName == _T("btn_get_srt"))//提取字幕
{
GetSRTFile();
LoadSRT();
}
else if (strName == _T("btn_commit"))//提交
{
CEditUI* pEdit = (CEditUI*)m_PaintManager.FindControl(_T("edit_word"));
CDuiString strWord = pEdit->GetText();//获取文本
//将该文本写回到list中
CListUI* pList = (CListUI*)m_PaintManager.FindControl(_T("List_srt"));
CListTextElementUI* pListItem = (CListTextElementUI*)pList->GetItemAt(pList->GetCurSel());
pListItem->SetText(1, strWord);
}
else if (strName == _T("btn_write_srt"))//写入字幕
{
WriteSRT();
}
else if (strName == _T("btn_view"))//提取视频
{
GenerateView();
}
else if (strName == _T("btn_bron"))//烧录
{
BornSRTtoView();
}
else if (strName == _T("btn_generate"))//生成Gif
{
CComboBoxUI* pCombo = (CComboBoxUI*)m_PaintManager.FindControl(_T("combo_select"));
if (0 == pCombo-> GetCurSel())
{
GenerateGifWithPic();
}
else
{
GenerateGifWithView();
}
}
}
//默认是图片生成
else if (msg.sType == _T("windowinit"))
{
SetControlEnable(false);
}
else if (msg.sType == _T("itemselect"))
{
if (strName == _T("List_srt"))
{
CListUI* pList = (CListUI*)m_PaintManager.FindControl(_T("List_srt"));
//拿出表中某项的内容
CListTextElementUI* pListItem = (CListTextElementUI*)pList->GetItemAt(pList->GetCurSel());
//将选中行中的对应文本信息写入edit中
CEditUI* pEdit = (CEditUI*)m_PaintManager.FindControl(_T("edit_word"));
pEdit->SetText(pListItem->GetText(1));
}
if (strName == _T("combo_select"))
{
CComboBoxUI* pComboUI = (CComboBoxUI*)m_PaintManager.FindControl(_T("combo_select"));
if (0 == pComboUI->GetCurSel())
{
SetControlEnable(false);
}
else
{
SetControlEnable(true);
}
}
}
}
设置控件是否有效,如果是由图片生成Gif,那么许多按钮将不再需要
void SetControlEnable(bool IsValid)//将一些控件设置为有效或者无效
{
((CEditUI*)m_PaintManager.FindControl(_T("edit_start")))->SetEnabled(IsValid);//start
((CEditUI*)m_PaintManager.FindControl(_T("edit_end")))->SetEnabled(IsValid);//end
((CButtonUI*)m_PaintManager.FindControl(_T("btn_cut")))->SetEnabled(IsValid);//截取按钮
((CButtonUI*)m_PaintManager.FindControl(_T("btn_get_srt")))->SetEnabled(IsValid);//提取SRT按钮
((CButtonUI*)m_PaintManager.FindControl(_T("btn_write_srt")))->SetEnabled(IsValid);//写入SRT按钮
((CButtonUI*)m_PaintManager.FindControl(_T("btn_view")))->SetEnabled(IsValid);//提取视频按钮
((CButtonUI*)m_PaintManager.FindControl(_T("btn_bron")))->SetEnabled(IsValid);//烧录按钮
}
向控制台发送命令:
void SendCmd(const CDuiString& strCMD)
{
SHELLEXECUTEINFO strSEInfo;
memset(&strSEInfo, 0, sizeof(SHELLEXECUTEINFO));
strSEInfo.cbSize = sizeof(SHELLEXECUTEINFO);
strSEInfo.fMask = SEE_MASK_NOCLOSEPROCESS;
strSEInfo.lpFile = _T("C:\\WINDOWS\\system32\\cmd.exe");
//windows命令行cmd所在的路径
strSEInfo.lpParameters = strCMD;//打开程序的参数
strSEInfo.nShow = SW_SHOW;//隐藏
ShellExecuteEx(&strSEInfo);//在函数中,会新创建一个进程,来负责调用命令行窗口执行命令
//等待命令响应
WaitForSingleObject(strSEInfo.hProcess, INFINITE);
MessageBox(m_hWnd, _T("命令操作完成"), _T("MakeGif"), IDOK);
}
用图片生成Gif
void GenerateGifWithPic()
{
CDuiString strPath = CPaintManagerUI::GetInstancePath();//获取工程的目录【与exe文件在同一个目录】
strPath += _T("ffmpeg\\");
//构造命令
CDuiString strCMD;
strCMD += _T("/c ");//构造命令期间第一条必须是'/c',要加上/c参数
strCMD += strPath;
strCMD += _T("ffmpeg -r 3 -i ");
strCMD += strPath;
strCMD += _T(".\\Picture\\%d.jpg ");
strCMD += strPath;
strCMD += _T("output.gif -y");
//给cmd发命令
SendCmd(strCMD);
}
视频文件生成Gif
- 截取片段函数(加载视频文件路径):
void Cutview()
{
CDuiString strPath = CPaintManagerUI::GetInstancePath();//获取路径
strPath += _T("ffmpeg\\");
CDuiString strViewPath = ((CEditUI*)m_PaintManager.FindControl(_T("edit_path")))->GetText();//获取用户输入的路径
CDuiString strCMD;
strCMD += _T("/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 ");
strCMD += _T("-ss ");
//获取start和end
CDuiString strStartTime = ((CEditUI*)m_PaintManager.FindControl(_T("edit_start")))->GetText();
if (!IsValidTime(strStartTime))
{
MessageBox(NULL, _T("起始时间有误"), _T("MakeGif"), IDOK);
}
CDuiString strEndTime = ((CEditUI*)m_PaintManager.FindControl(_T("edit_end")))->GetText();
if (!IsValidTime(strEndTime))
{
MessageBox(NULL, _T("终止时间有误"), _T("MakeGif"), IDOK);
}
strCMD += strStartTime;
strCMD += _T(" -to ");
strCMD += strEndTime;
strCMD += _T(" ");
//输出文件的路径
strCMD += strPath;
strCMD += _T("cut.mkv -y");
SendCmd(strCMD);
}
void LoadFile()//加载视频文件的路径
{
OPENFILENAME ofn;
memset(&ofn, 0, sizeof(OPENFILENAME));
//设置参数
TCHAR strPath[MAX_PATH] = { 0 };//MAX_PATH--->260
ofn.lStructSize = sizeof(OPENFILENAME);
ofn.lpstrFile = strPath;
ofn.nMaxFile = sizeof(strPath);
ofn.lpstrFilter = _T("All(*.*)\0 *.*\0mkv(*.mkv)\0 *.mkv\0");//使用这个过滤串,我们这里是过滤出mkv文件
ofn.Flags = OFN_PATHMUSTEXIST | OFN_FILEMUSTEXIST;
if (GetOpenFileName(&ofn))
{
//将文件的路径设置到edit
((CEditUI*)m_PaintManager.FindControl(_T("edit_path")))->SetText(strPath);//将路径设置进编辑框中
}
}
- 提取字幕文件
void GetSRTFile()
{
CDuiString strPath = CPaintManagerUI::GetInstancePath();//获取路径
strPath += _T("ffmpeg\\");
//1.构造命令
CDuiString strCMD;
strCMD += _T("/c ");
strCMD += strPath;
strCMD += _T("ffmpeg -i ");
strCMD += strPath;
strCMD += _T("cut.mkv ");//视频文件的路径
strCMD += strPath;
strCMD += _T("input.srt -y");//输出的字幕文件
//向cmd发送命令
SendCmd(strCMD);
}
- 编辑字幕文件
void LoadSRT()
{
//将.srt,加载到界面中list控件
CDuiString strPath = CPaintManagerUI::GetInstancePath();//文件路径
strPath += _T("ffmpeg\\input.srt");
std::ifstream fIn(strPath.GetData());//获取字符串类型
char strSRTCon[512] = { 0 };
CListUI* pList = (CListUI*)m_PaintManager.FindControl(_T("List_srt"));
pList->RemoveAll();//第二次获取字幕时清理第一次获取的字幕
while (!fIn.eof())//文件指针是否在结尾处
{
//字幕序号
fIn.getline(strSRTCon, 512);
//给出list中的文本
CListTextElementUI* pListItem = new CListTextElementUI;
pList->Add(pListItem);
//读取时间轴
fIn.getline(strSRTCon, 512);
pListItem->SetText(0, UTF8ToUniCode(strSRTCon));
//从0开始,将文本设置进去,此时strSRTCon是LPCTSTR类型的
//win32项目是基于Unicode的,不是ASCII形式的,要进行转化
//读取字幕
fIn.getline(strSRTCon, 512);
pListItem->SetText(1, UTF8ToUniCode(strSRTCon));
//读取空行
fIn.getline(strSRTCon, 512);
}
fIn.close();
}
void WriteSRT()
{
CDuiString strPath = CPaintManagerUI::GetInstancePath();
strPath += _T("ffmpeg\\input.srt");
std::ofstream fOut(strPath.GetData());
// 获取文本内容
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);
// 将获取到的内容写进srt文件中
string strNewLine = Unicode2UTF8(_T("\n"));
// 行号
string itemNo = Unicode2UTF8(strNo);
fOut.write(itemNo.c_str(), itemNo.size());
fOut.write(strNewLine.c_str(), strNewLine.size());
// 时间轴
string itemTime = Unicode2UTF8(strTime);
fOut.write(itemTime.c_str(), itemTime.size());
fOut.write(strNewLine.c_str(), strNewLine.size());
// 文本
string itemWord = Unicode2UTF8(strWord);
fOut.write(itemWord.c_str(), itemWord.size());
fOut.write(strNewLine.c_str(), strNewLine.size());
// 字幕和字幕之间都要换行
fOut.write(strNewLine.c_str(), strNewLine.size());
}
fOut.close();
}
- 提取视频裸流
void GenerateView()
{
CDuiString strPath = CPaintManagerUI::GetInstancePath();//获取路径
strPath += _T("ffmpeg\\");
CDuiString strCMD;
strCMD += _T("/c ");//构造命令期间第一条必须是'/c'
strCMD += strPath;
strCMD += _T("ffmpeg -i ");//要加上\c参数
strCMD += strPath;
strCMD += _T("cut.mkv -vcodec copy -an -sn ");
strCMD += strPath;
strCMD += _T("cut2.mkv -y");
SendCmd(strCMD);
}
- 写入字幕
void BornSRTtoView()
{
CDuiString strCMD;
strCMD += _T("/c ");
strCMD += _T("cd ");
strCMD += CPaintManagerUI::GetInstancePath() + _T("ffmpeg");//获取路径
strCMD += _T(" & ");
//构造命令
strCMD += _T("ffmpeg -i cut2.mkv -vf subtitles=input.srt cut3.mkv -y");
SendCmd(strCMD);
}
- 生成Gif
void GenerateGifWithView()
{
CDuiString strPath = CPaintManagerUI::GetInstancePath();//获取路径
strPath += _T("ffmpeg\\");
CDuiString strCMD;
strCMD += _T("/c ");
strCMD += strPath;
strCMD += _T("ffmpeg -i ");
strCMD += strPath;
strCMD += _T("cut3.mkv -vf scale=iw/2:ih/2 -f gif ");
strCMD += strPath;
strCMD += _T("output!.gif -y");
SendCmd(strCMD);
}
UTF-8、Unicode转换
CDuiString UTF8ToUniCode(const char* str)
{
//获取转化之后的目标串的长度
int szLen = ::MultiByteToWideChar(CP_UTF8, 0, str, strlen(str), NULL, 0);
wchar_t* pContent = new wchar_t[szLen + 1];//为目标串申请空间
//进行转化
::MultiByteToWideChar(CP_UTF8, NULL, str, strlen(str), pContent, szLen);
pContent[szLen] = '\0';
CDuiString s(pContent);
delete[]pContent;
return s;
}
string Unicode2UTF8(CDuiString str)
{
int len = WideCharToMultiByte(CP_UTF8, 0, str.GetData(), -1, NULL, 0, NULL, NULL);
CHAR *szUtf8 = new CHAR[len + 1]{0};
::WideCharToMultiByte(CP_UTF8, 0, str.GetData(), -1, (LPSTR)szUtf8, len, NULL, NULL);
string s(szUtf8);
delete[] szUtf8;
return s;
}
判断时间是否有效
bool IsValidTime(CDuiString strTime)//判断给的时间是否有效
{
//时间格式:xx:xx:xx
if (strTime.GetLength() != 8)
{
return false;
}
for (int i = 0; i < strTime.GetLength(); ++i)
{
if (strTime[i] == ':')
continue;
if (!(strTime[i] >= '0' && strTime[i] <= '9'))
{
return false;
}
}
return true;
}
测试
由于本程序需要实现的功能简单明了,这里用测试V模型简单进行测试【略去验收测试】
单元测试
对各个模块接口进行测试
文件加载测试
按选择文件按钮,确认用户能够实现选择文件的功能
视频剪辑测试
因为视频剪辑使用ffmpeg工具,与cmd发送测试类似,此外要注意对输入的时间进行判断
cmd命令发送测试
打开cmd命令行,岔开输出的命令,验证命令输出组装是否成功
生成Gif测试
同样对cmd测试,验证命令输出组装是否成功
集成测试
根据概要设计文档对程序进行测试,将多个模块组合起来进行测试,观察运行结果是否符合预期结果
系统测试
首先进行冒烟测试,确认核心功能正常运行
功能【场景设计法】
- 基本事件流:
用户选择视频或图片后加载相应的文件,选择图片就直接生成Gif;选择视频需要由用户截取视频(截取时间段不超出加载视频时长),选择是否更换视频中字幕,最后生成Gif- 备选事件流:
1.用户选择与加载文件不符合:如用户选择视频后却加载图片文件【操作有错】
2.用户截取视频时间段超出加载的视频文件【操作有错】界面
由于本程序对界面要求是简洁明了,符合常规即可
界面中元素文字信息与功能信息一致
易用性
界面风格符合大多使用人的习惯,功能能够适应用户需求的不断变化
性能
各按钮响应时间在3秒以内
兼容性
在各种类型的电脑上是否能正常运行