目錄:
項目背景:
文字信息時代,傳統的文字聊天方式已不能滿足大衆的需求,很多時候文字不能表達自己的想法,或者溝通技巧的欠缺,後就成爲了尬聊。"一言不合就鬥圖",能用一張圖說明的。
暴走表情廣泛的遍佈於網絡,網民們大多用作鬥圖。一般常見於QQ、微信。鬥圖活動起始於QQ,羣聊時大家發送搞趣圖片以相互娛樂。後來發展到百度貼吧等各種論壇上,時常有人發帖組織鬥圖活動。
鬥圖發展到現在不僅僅用這些表情圖,還會惡搞明星、電影人物。以圖加文字的自由組合,結合網絡語言,製作搞笑逗比的圖片。
可行性方面:
- 經濟可行性——低成本
- 操作可行性——簡單
- 技術可行性——藉助其他工具
需求分析:
製作Gif動圖,因此選擇兩種生成方式:1.圖片生成;2.視頻生成。
技術方面藉助其他的工具實現:選擇 ffmpeg 工具。
FFmpeg即是一塊音視頻編解碼工具,同時也是一組音視頻編解碼開發套件,爲開發者提供了豐富的音視頻 處理調用接口。FFmpeg中的"FF"指的是"Fast Forward",mpeg則是動態圖像專家組。 它提供了錄製、轉換 以及流化音視頻的完整解決方案。 它包含了非常先進的音頻/視頻編解碼庫 libavcodec, 爲了保證高可移植 性和編解碼質量, libavcodec 裏很多 codec 都是從頭開發的。
FFmpeg項目由以下幾部分組成:
- 1. ffmpeg 視頻文件轉換命令行工具,也支持經過實時電視卡抓取和編碼成視頻文件。
- 2. ffserver 基於 HTTP、RTSP 用於實時廣播的多媒體服務器.也支持時間平移。
- 3. ffplay 用 SDL 和 FFmpeg 庫開發的一個簡單的媒體播放器。
- 4. libavcodec 一個包含了所有 FFmpeg 音視頻編解碼器的庫.爲了保證優性能和高可複用性,大多數編解 碼器從頭開發的。
- 5. libavformat 一個包含了所有的普通音視格式的解析器和產生器的庫
學習ffmpeg 工具我們可以知道:ffmpeg 是使用命令行的形式,給予 cmd 一定的命令,實現相應的操作。
因此我們的實驗原理:在程序中通過 cmd 控制檯調用 ffmpeg.exe 工具,並給該工具發送對應的命令,完成所需操作,發命令時,cmd窗口隱藏在後臺。
總體設計:
本項目有兩種生成gif動態圖方式: 1. 使用圖片生成 2. 使用短視頻生成
實現原理:在程序中通過cmd控制檯調用ffmpeg.exe工具,並給該工具發送對應的命令,完成所需操作,發命令時,cmd窗口隱藏在後臺。
我們還需要有一定的操作界面:依靠 duilib 庫。
DuiLib庫是一款由杭州月牙兒網絡技術有限公司開發,輕量級的C++界面開發庫,遵循開源BSD協議,可以免費用於商業項目。Duilib界面庫的優勢在於:
- 1. 基於GDI在窗口上自繪,無其他依賴,未使用特殊或危險的系統調用,能夠很好的解決傳統MFC界面的一系列問題
- 2. 使用XML來描述界面風格,界面佈局,將界面和邏輯分離,同時易於實現各種超炫的界面效果如換色,換膚,透明等
- 3. 完全兼容ActiveX控件(如常見的IE控件和Flash),也可以和MFC等界面庫配合使用
- 4. 可廣泛用於互聯網客戶端、工具軟件客戶端、管理系統客戶端、多媒體客戶端(如KTV、觸摸屏)、車載電腦系統、gps系統和手機客戶端軟件等。
許多知名公司都採用Duilib作爲界面庫,比如:華爲網盤、PPS、金山快盤、酷我音樂、愛奇藝視頻、百度殺毒、百度衛士等一些列產品。
注意:Duilib僅僅是基於Win32的一套UI庫。因此要了解 Win32 程序相關知識。
Win32 的相關知識:
一個Win32應用程序可以分爲程序代碼和UI資源兩大部分,兩部分終是以rc整合成一個完整的exe可執行程序。所謂UI資源,指的是功能菜單、對話框外貌、程序圖標、光標形狀等東西。
代碼示例,展示Win32 界面:
#include <Windows.h>
#include <tchar.h>
//消息回調函數
LRESULT CALLBACK WinProc(HWND hWnd, UINT message,
WPARAM wParam, LPARAM lParam) {
switch (message) {
case WM_CLOSE:
if (IDOK == MessageBox(hWnd, _T("你確定退出?"),
_T("退出"), MB_OKCANCEL)) {
DestroyWindow(hWnd);
return 0;
}
case WM_DESTROY:
PostQuitMessage(0);
return 0;
default:
return DefWindowProc(hWnd, message, wParam, lParam);
}
}
int APIENTRY _tWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPTSTR lpCmdLine, int nCmdShow) {
//Step1:註冊一個窗口類
HWND hwnd; //窗口的句柄
WNDCLASSEX wc; //窗口類結構
wc.cbSize = sizeof(WNDCLASSEX);
wc.style = CS_VREDRAW | CS_HREDRAW;
wc.lpszMenuName = 0;
wc.lpszClassName = _T("Win32");
wc.lpfnWndProc = WinProc; //消息回調函數
wc.hInstance = hInstance;
wc.hIcon = NULL;
wc.hCursor = NULL;
wc.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1);
wc.cbWndExtra = 0;
wc.cbClsExtra = 0;
wc.hIconSm = NULL;
RegisterClassEx(&wc); //註冊窗口
//Step2:創建一個窗口
hwnd = CreateWindow(
_T("Win32"), //窗口的類名,也就是上面我們自定義的窗口類的名字
_T("我的第一個Win32程序"), //窗口的標題
WS_OVERLAPPEDWINDOW, //窗口style
500, //窗口位置x座標
300, //窗口位置y座標
800, //窗口寬度
600, //窗口高度
NULL, //父窗口句柄
NULL, //菜單句柄
hInstance, //實例句柄
NULL //創建數據
);
if (!hwnd) {
return FALSE;
}
ShowWindow(hwnd, SW_SHOW); //顯示窗口
UpdateWindow(hwnd); //刷新
//Step3:消息循環
MSG msg;
while (GetMessage(&msg, NULL, 0, 0)){
TranslateMessage(&msg);
DispatchMessage(&msg);
}
return 0;
}
注意:Win32程序的入口點是WinMain,WinMain的四個參數由操作系統負責傳遞,main是控制檯程序的入 口點。
從上文可以看出,一個簡單的Win32程序包括以下步驟:
- 1. 註冊窗口類:RegisterClass 窗口創建前,對窗口的屬性進行一些設置,主要是窗口的外貌以及行爲,比如:窗口的邊框、顏色、位 置、標題、顏色、大小等就是窗口的外貌,窗口接受到消息後如何響應,就是爲該窗口綁定窗口處理函數,窗口在創建前,必須使用RegisterClass函數告訴系統。
- 2. 創建窗口:CreateWindow 按照設置的窗口大小、窗口風格、窗口標題、窗口位置等將窗口創建成功。注意:CreateWindow
- 3. 顯示窗口和更新窗口將窗口在界面中展示出來。
- 4. 消息循環不斷從應用程序消息隊列中獲取消息,交給註冊窗口時設定的消息響應函數進行處理。
Win32的消息循環:
什麼是消息?
Windows程序的運行是依靠外部的事件來驅動。換句話說,程序不斷等待,等待任何可能的輸入,然後做出 判斷,再做適當的處理。上述的“輸入”是由操作系統捕獲到後,以消息的形式發送給應用程序。消息,其實 就是系統內設的一種數據結構:
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_USER來表示 用戶自定義消息的起始值,#define WM_USER 0x0400。
消息的分類
消息類型可以分爲兩大類:系統定義消息和用戶自定義消息。
系統定義消息分爲:窗口消息、命令消息、控件通知消息。
- 1. 窗口消息 與窗口的內部運作有關的消息,如創建窗口,繪製窗口,銷燬窗口等 可以是一般的窗口,也可以是 MainFrame,Dialog,控件等。 如:WM_CREATE, WM_PAINT, WM_MOUSEMOVE, WM_CTLCOLOR, WM_HSCROLL等。
- 2. 命令消息 當用戶從菜單選中一個命令項目、按下一個快捷鍵、點擊工具欄上的一個按鈕或者點擊控件都將發送 WM_COMMAND命令消息。通過消息結構中的wParam和lParam成員就能清楚得知道消息的來源。
- LOWORD(wParam):代表菜單ID、或控件ID,或快捷鍵ID;
- HIWORD(wParam):表示通知碼,當消息是 從菜單發出時,則這個值爲0,當消息是從快捷鍵發出時,這個值爲1,當消息是從控件發出時,這個值 爲通知碼,比如按鈕的通知碼:BN_CLICKED, BN_DBLCLK等。
消息隊列:
應用程序獲取到各種消息,由硬件產生的消息(如:鼠標移動或鍵盤按下),放在系統消息隊列中,windows 系統或其他windows程序傳送過來的消息,放在應用程序的消息隊列中,然後由應用程序不斷的將消息取走 進行響應。其實就是,程序中有一個獲取消息的循環代碼,會不斷的從操作系統中獲取消息。
消息是否被放進消息隊列,可將消息分爲:
- 隊列消息:一般,程序都是從消息隊列中獲取消息。消息會先保存在消息隊列中,消息循環會從此隊列中取出消息並分發到各窗口處理 ,如:WM_PAINT,WM_TIMER,WM_CREATE,WM_QUIT,以及鼠標,鍵盤消息等。其中,WM_PAINT,WM_TIMER只有在隊列中沒有其他消息的時候纔會被處理,WM_PAINT消息 還會被合併以提高效率。其他所有消息以先進先出(FIFO)的方式被處理。 系統中維護着一個全局的系統消息隊列,還會爲每一個UI線程維護一個UI線程消息隊列。當系統消息隊列中存在消息時,系統會根據消息所屬的UI線程,分發到應用程序對應的UI線程消息隊列中去。
- 非隊列消息:但是還有一部分消息會繞過消息隊列,直接發送到窗口過程進行處理 。如WM_ACTIVATE, WM_SETFOCUS, WM_SETCURSOR,WM_WINDOWPOSCHANGED。
詳細設計:
1.相關環境的配置。VS2013導入 duilib 庫。
參考網上專業博客教程:https://www.cnblogs.com/Alberl/p/3342030.html
2.學習duilib庫的使用。
學習專業博客:https://www.xuebuyuan.com/1656742.html
3.各個部分的代碼實現:
- 功能設置部分:
class CDuiFrameWnd : public WindowImplBase
{
public:
virtual LPCTSTR GetWindowClassName() const { return _T("DUIMainFrame"); }
virtual CDuiString GetSkinFile() { return _T("duilib.xml"); }
virtual CDuiString GetSkinFolder() { return _T(""); }
//MessageBox(NULL, _T("路徑"), _T("測試"), IDOK);
virtual void Notify(TNotifyUI& msg){
CDuiString sCtrlName = msg.pSender->GetName();
if (msg.sType == _T("click")) {
if (sCtrlName == _T("closebtn")) {
Close();
return;
}
else if (sCtrlName == _T("minbtn")) {
SendMessage(WM_SYSCOMMAND, SC_MINIMIZE, 0);
return;
}
else if (sCtrlName == _T("maxbtn")) {
SendMessage(WM_SYSCOMMAND, SC_MAXIMIZE, 0);
return;
}
else if (sCtrlName == _T("restorebtn")) {
SendMessage(WM_SYSCOMMAND, SC_RESTORE, 0);
return;
}
else if (sCtrlName == _T("Look")) {
LoadFile();
return;
}
else if (sCtrlName == _T("CreatGif")) {
CComboBoxUI* pCombox = (CComboBoxUI*)m_PaintManager.FindControl(_T("Combox_Select"));
int i = pCombox->GetCurSel();
if (i==0) { //選擇方式生成方式:視頻 or 圖片
GenerateGifWithPic();
}
else {
GenerateGifWithView();
}
return;
}
else if (sCtrlName == _T("cut")) {
Cut();
return;
}
return;
}
}
- 加載文件
//加載文件
void LoadFile() {
TCHAR szPeFileExt[1024] = TEXT("*.*"); //打開任意類型的文件
TCHAR szPathName[80*MAX_PATH]; //文件路徑大小
OPENFILENAME ofn = { sizeof(OPENFILENAME) };
//ofn.hwndOwner = hWnd;// 打開OR保存文件對話框的父窗口
ofn.lpstrFilter = szPeFileExt;
lstrcpy(szPathName, TEXT(""));
ofn.lpstrFile = szPathName;
ofn.nMaxFile = sizeof(szPathName);//存放用戶選擇文件的 路徑及文件名 緩衝區
ofn.lpstrTitle = TEXT("選擇文件");//選擇文件對話框標題
ofn.Flags = OFN_EXPLORER | OFN_FILEMUSTEXIST | OFN_ALLOWMULTISELECT;//如果需要選擇多個文件 則必須帶有 OFN_ALLOWMULTISELECT標誌
BOOL bOk = GetOpenFileName(&ofn);
if (bOk) {
CEditUI* pPathEdit = (CEditUI*)m_PaintManager.FindControl(_T("Edit"));
pPathEdit->SetText(szPathName);
}
}
- 給cmd發送命令
void GenerateGifWithPic() {
//CDuiString strCMD(_T(" ffmpeg -r 1 -i D:\項目\項目1\ProjectZhou\Debug\\ffmpeg\\ffmpeg\\Pictrue\\%d.jpg D:\項目\項目1\ProjectZhou\Debug\\ffmpeg\\ffmpeg\\out.gif"));
//構造命令
CDuiString strFFmpegPath = CPaintManagerUI::GetInstancePath() + _T("\\ffmpeg\\ffmpeg\\ffmpeg ");
CDuiString strPictruePath = CPaintManagerUI::GetInstancePath() + _T("\\ffmpeg\\ffmpeg\\Pictrue\\%d.jpg ");
CDuiString strOutPath = CPaintManagerUI::GetInstancePath() + _T("\\ffmpeg\\ffmpeg\\out.gif");
CDuiString strCMD = (_T("/c "));
strCMD += strFFmpegPath;
strCMD += _T("-r 1 -i ");
strCMD += strPictruePath + strOutPath;
SendCmd(strCMD);
MessageBox(m_hWnd, _T("圖片方式生成Gif成功!"), _T("GIFF"), IDOK);
}
- 視頻截取部分
void Cut() {
CDuiString strStartTime = ((CEditUI *)m_PaintManager.FindControl(_T("BeginTime")))->GetText();
if (!IsVaildTime(strStartTime)) {
MessageBox(m_hWnd, _T("輸入起始時間有誤!"), _T("GIFF"), IDOK);
return;
}
CDuiString strFinishTime = ((CEditUI *)m_PaintManager.FindControl(_T("EndTime")))->GetText();
if (!IsVaildTime(strFinishTime)) {
MessageBox(m_hWnd, _T("輸入結束時間有誤!"), _T("GIFF"), IDOK);
return;
}
//構造截取視頻的命令
CDuiString strFFmpegPath = CPaintManagerUI::GetInstancePath() + _T("\\ffmpeg\\ffmpeg\\ffmpeg ");
CDuiString strOutPath = CPaintManagerUI::GetInstancePath() + _T("\\ffmpeg\\ffmpeg\\output.mp4");
CDuiString strCMD = (_T("/c "));
strCMD += strFFmpegPath;
strCMD += _T(" -ss ");
strCMD += strStartTime;
strCMD += _T(" -to ");
strCMD += strFinishTime;
strCMD += _T(" -i ");
CDuiString strViewPath = ((CEditUI *)m_PaintManager.FindControl(_T("Edit")))->GetText();
strCMD += strViewPath;
strCMD += _T(" -vcodec copy -acodec copy ");
strCMD += strOutPath;
SendCmd(strCMD);
MessageBox(m_hWnd, _T("視頻截取成功!"), _T("GIFF"), IDOK);
}
- 發送cmd 命令是一樣的,封裝起來。
void SendCmd(const 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");
////構造命令
//CDuiString strPictruePath = CPaintManagerUI::GetInstancePath() + _T("\\ffmpeg\\ffmpeg\\Pictrue\\%d.jpg ");
//CDuiString strOutPath = CPaintManagerUI::GetInstancePath() + _T("\\ffmpeg\\ffmpeg\\out.gif");
//CDuiString strCMD = (_T("/c "));
//strCMD += strFFmpegPath;
//strCMD += _T("-r 1 -i ");
//strCMD += strPictruePath + strOutPath;
strSEInfo.lpParameters = strCMD;
strSEInfo.nShow = SW_HIDE; //隱藏cmd'窗口
//2. 發送cmd命令
ShellExecuteEx(&strSEInfo);
WaitForSingleObject(strSEInfo.hProcess, INFINITE);
//D:\項目\項目1\ProjectZhou\Debug\\ffmpeg\\ffmpeg ffmpeg -r 1 -i D:\項目\項目1\ProjectZhou\Debug\\ffmpeg\\ffmpeg\\Pictrue\\%d.jpg D:\項目\項目1\ProjectZhou\Debug\\ffmpeg\\ffmpeg\\out.gif
}
- 時間判斷:
bool IsVaildTime(const CDuiString& strTime) {
if (strTime.GetLength() != 8) {
return false;
}
for (int i = 0; i < 8;i++) {
if (':'==strTime[i]) {
continue;
}
else if (isdigit(strTime[i])) {
continue;
}
else {
return false;
}
}
return true;
}
- 視頻生成
//視頻生成
void GenerateGifWithView() {
//構造命令
CDuiString strFFmpegPath = CPaintManagerUI::GetInstancePath() + _T("\\ffmpeg\\ffmpeg\\ffmpeg ");
CDuiString strViewPath = CPaintManagerUI::GetInstancePath() + _T("\\ffmpeg\\ffmpeg\\output.mp4 ");
CDuiString strOutPath = CPaintManagerUI::GetInstancePath() + _T("\\ffmpeg\\ffmpeg\\outView.gif");
CDuiString strCMD = (_T("/c "));
strCMD += strFFmpegPath;
strCMD += _T("-r 50 -i ");
strCMD += strViewPath + strOutPath;
SendCmd(strCMD);
MessageBox(m_hWnd, _T("視頻方式生成Gif成功!"), _T("GIFF"), IDOK);
}
- ASS生成:
//生成ASS
void CreatASS() {
//構造命令
CDuiString strFFmpegPath = CPaintManagerUI::GetInstancePath() + _T("\\ffmpeg\\ffmpeg\\ffmpeg ");
CDuiString strViewPath = CPaintManagerUI::GetInstancePath() + _T("\\ffmpeg\\ffmpeg\\output.mp4 ");
CDuiString strOutPath = CPaintManagerUI::GetInstancePath() + _T("\\ffmpeg\\ffmpeg\\output.ass ");
CDuiString strCMD = (_T("/c "));
strCMD += strFFmpegPath;
strCMD += _T("-i ");
strCMD += strViewPath;
strCMD += _T("-an -vn -scodec copy ");
strCMD += strOutPath;
SendCmd(strCMD);
MessageBox(m_hWnd, _T("視頻方式生成Gif成功!"), _T("GIFF"), IDOK);
}
- 主函數部分:
int APIENTRY _tWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPTSTR lpCmdLine, int nCmdShow)
{
CPaintManagerUI::SetInstance(hInstance);
CDuiFrameWnd duiFrame;
duiFrame.Create(NULL, _T("DUIWnd"), UI_WNDSTYLE_FRAME, WS_EX_WINDOWEDGE);
duiFrame.CenterWindow();
duiFrame.ShowModal();
return 0;
}
- 依靠duilib中的工具進行界面設計:
Duilib的界面佈局器:使用Duilib自帶的界面佈局器打開XML文件,進行自己編輯。
注:這個界面只是自己單獨實現的,字幕功能還沒有實現,完善,是爲需求分析中所實現的功能接口,還並未實現,在不斷的完善。
測試:
測試方式:單元測試,且測試方法採用白盒測試。
文件加載測試:按鈕中實現選擇文件,確認是否能夠選擇。
cmd 命令發送測試:打開 cmd 命令行,在程序中查看輸出的命令,將輸出命令輸入命令行,驗證命令輸出組裝是否成功。
視頻剪切測試:和 cmd 命令測試類似,打開 cmd 命令行,在程序中查看輸出的命令,將輸出命令輸入命令行,驗證命令輸出組裝是否成功。注意時間的判斷,輸入錯誤時間驗證是否有效時間。
視頻生成測試:打開 cmd 命令行,在程序中查看輸出的命令,將輸出命令輸入命令行,驗證命令輸出組裝是否成功。
總體測試:完整操作,驗證是否和功能對應。
維護:
主要完善性維護。功能的完善,在字幕。
其中有適應性維護,測試中圖片的規格過於大,顯示上會有一點兒的延遲。
項目效果圖展示:
主界面:
圖片方式生成:
生成位置:工程中ffmpeg位置的默認位置—— 那個就是最終圖片生成的Gif圖。
視頻方式生成效果展示圖:
這裏字幕實現忽略,後續完善,直接生成Gif:
完整代碼:
https://github.com/Zzzhouxiaochen/Gif