主要思路
:目标是录制屏幕上的操作保存为一个本地视频文件,我们选取通用的avi格式。
VFW
提供了生成avi的相关接口,而且Windows自带了VFW,无需额外工具。无论怎样做,我们都要不断获取视频的每一帧,然后添加到视频文件中,视频文件本来就是由一帧帧构成。所以我们要想办法截屏,然后添加到视频中。当然截屏最好是截在内存中,而且添加完就释放,这样可以保证不生成额外临时文件、可以边录边生成视频、占用内存稳定。
1. 创建一个
WIN32窗口程序
,菜单中添加
开始
、
结束项
。
在窗口处理函数的WM_COMMAND消息处理代码中,判断控件ID。如果点击的是开始菜单,就创建“录屏线程”,并设置变量“
endFlag
”,endFlag变量用于标识录屏是否结束。如果点击的是结束菜单,就设置录屏结束标识“endFlag”,然后线程会退出。【endFlag的作用参考录屏线程处理函数】
case
IDM_START:
// 启动视频处理线程
endFlag
= false;
DWORD threadId;
hThread = CreateThread(NULL,
// 默认安全属性
0,
//
默认栈大小
ThreadProc
,
//
线程处理函数
NULL,
//
线程参数
0,
//
创建标识
&threadId);
//
线程ID
break
;
case
IDM_END:
// 退出视频处理线程
endFlag
= true;
break
;
2. 为自己的录屏线程指定线程处理函数:
DWORD WINAPI
ThreadProc
(LPVOID lpParam)
{
// 初始化录屏过程
InitAvi
();
while(true)
{
if(
endFlag
)
// 判断录屏结束标识,如果为true,则完成录屏结束操作并退出录屏循环,线程结束。
{
CloseAvi
();
break;
}
// 将屏幕数据画如内存上下文
BitBlt(hDCMem,0,0,xScrn,yScrn,hDCSource,0,0,SRCCOPY|CAPTUREBLT);
// 因为获取的屏幕上不包含鼠标信息,可以手动添加到内存上下文
GetCursorPos(&ptCursor);
DrawIconEx(hDCMem, ptCursor.x, ptCursor.y, hCursor, 0, 0, 0, NULL, DI_NORMAL | DI_COMPAT | DI_DEFAULTSIZE);
// 获取屏幕位图的DIB数据(生成avi需要)
int ret=GetDIBits(hDCSource,hBmp,0,yScrn,pBits,(BITMAPINFO*)&bmpInfoHeader,DIB_RGB_COLORS);
if(!ret || !pBits)
MessageBox(NULL,L"ThreadProc:获取设备无关位图内容失败",L"error",MB_OK);
// 调用函数添加视频帧到已创建的avi文件。
HRESULT hr=myAviMaker->
AddAviFrame
(myAviMaker->m_havi,hbmDIB);
if(hr!=S_OK)
{
MessageBox(NULL,L"ThreadProc:添加帧失败",L"error",MB_OK);
}
Sleep(70);
}
return 0;
}
可以看出,在线程处理函数中首先执行了InitAvi()初始化了一下,然后就一直添加视频帧,结束时执行CloseAvi()。所以我们现在要知道InitAvi()、CloseAvi()都干了些什么。我们可以猜想:InitAvi()中应该起码创建了一个avi,然后设置了一些相关操作,让开发者可以直接在录屏线程里添加帧;而CloseAvi()中应该是合法关闭了创建的avi视频文件。因为不做一些收尾工作,avi文件是不完整的,肯定无法正常播放。
HRESULT
InitAvi
()
{
// 获取当前时间作为视频文件名称
const DWORD dwStringLength=32;
char timeString[dwStringLength]={0};
WCHAR timeStringW[dwStringLength]={0};
time_t t = time(0);
strftime(timeString, sizeof(timeString), "%Y-%m-%d %H.%M.%S.avi",localtime(&t) );
for(int i=0;i<sizeof(timeStringW)/sizeof(WCHAR);i++)
{
timeStringW[i]=timeString[i];
}
// 计算完整视频路径(有点乱,因为实际代码中用户可以设置视频保存路径)
int pathLength=0;
for(int i=0;i<sizeof(pszVideoPath)/2;i++)
{
if(pszVideoPath[i]==0)
{
pathLength=i;
break;
}
}
WCHAR pszFileName[MAX_PATH+sizeof(timeStringW)+1]={0};
memcpy(pszFileName,pszVideoPath,pathLength*2);
memcpy(pszFileName+pathLength+1,timeStringW,sizeof(timeStringW));
pszFileName[pathLength]='\\';
myAviMaker=new CAviMaker();
myAviMaker->m_havi = myAviMaker->
CreateAvi
(pszFileName,105,NULL);
if(NULL==myAviMaker->m_havi)
{
MessageBox(NULL,L"InitAvi:初始化avi失败",L"error",MB_OK);
return AVIERR_ERROR;
}
// 获取屏幕上下文、并创建一个内存上下文
hDCSource = GetWindowDC(NULL);
hDCMem
= CreateCompatibleDC(hDCSource);
// 获取屏幕分辨率
xScrn = GetDeviceCaps(hDCSource, HORZRES);
yScrn = GetDeviceCaps(hDCSource, VERTRES);
// 创建位图供内存上下文使用
hBmp = CreateCompatibleBitmap(hDCSource, xScrn, yScrn);
if(!hDCSource || !hBmp || !hDCMem)
{
MessageBox(NULL,L"InitAvi:创建位图失败",L"error",MB_OK);
return AVIERR_ERROR;
}
HBITMAP oldBmp = (HBITMAP)SelectObject(hDCMem,hBmp);
if(!oldBmp)
{
MessageBox(NULL,L"InitAvi:设置内存设备位图失败",L"error",MB_OK);
return AVIERR_ERROR;
}
memset(&bmpInfoHeader,0,sizeof(BITMAPINFOHEADER));
bmpInfoHeader.biSize=sizeof(BITMAPINFOHEADER);
bmpInfoHeader.biWidth=xScrn;
bmpInfoHeader.biHeight=yScrn;
bmpInfoHeader.biPlanes=1;
bmpInfoHeader.biBitCount=24;
bmpInfoHeader.biCompression=BI_RGB;
hCursor = LoadCursor(NULL,IDC_ARROW);
hbmDIB=CreateDIBSection(hDCSource,(BITMAPINFO*)&bmpInfoHeader,DIB_RGB_COLORS,(void**)
&pBits,NULL,0);
if(!hbmDIB)
{
MessageBox(NULL,L"InitAvi:创建设备无关位图块失败",L"error",MB_OK);
return AVIERR_ERROR;
}
return S_OK;
}
HRESULT
CloseAvi
()
{
DeleteObject(hbmDIB);
DeleteObject(hCursor);
DeleteObject(hBmp);
ReleaseDC(NULL,hDCMem);
ReleaseDC(NULL,hDCSource);
myAviMaker->
CloseAvi
(myAviMaker->m_havi);
delete(myAviMaker);
myAviMaker=NULL;
CloseHandle(hThread);
hThread=NULL;
return S_OK;
}
可见,上面的代码就基本完成了录屏功能了。但是依然有几个函数的实现代码没有出现,就是myAviMaker实例的
CreateAvi、CloseAvi
成员函数:
// *************** 创建avi文件 ****************
// fileName
avi文件完整路径
// framePeriod
相邻帧时间间隔
// The waveformat
如果不添加音频,wfx为NULL
HAVI CAviMaker::
CreateAvi
(const WCHAR* fileName, int framePeriod, const WAVEFORMATEX *wfx)
{
IAVIFile
*pfile;
AVIFileInit
();
HRESULT hr =
AVIFileOpen
(&pfile, fileName, OF_WRITE|OF_CREATE, NULL);
if (hr!=AVIERR_OK)
//创建avi失败退出
{
MessageBox(NULL,L"CAviMaker::CreateAvi():视频创建失败",L"error",MB_OK);
AVIFileExit
();
return NULL;
}
// 初始化TAviUtil结构体
HAVIStruct
*aviStruct = new HAVIStruct;
aviStruct->pfile = pfile;
if (wfx==NULL)
ZeroMemory(&aviStruct->wfx,sizeof(WAVEFORMATEX));
else
CopyMemory(&aviStruct->wfx,wfx,sizeof(WAVEFORMATEX));
aviStruct->period = framePeriod;
aviStruct->aStream =
aviStruct->pStream =
aviStruct->pStreamCompressed = 0;
aviStruct->nextFrame =
aviStruct->nextSamp = 0;
aviStruct->iserr=false;
return (HAVI)aviStruct;
// 返回avi句柄
}
// *************** 关闭avi文件 ****************
// havi 要关闭的avi句柄
HRESULT CAviMaker::
CloseAvi
(HAVI havi)
{
//***** 参数为空,返回错误
if (havi==NULL)
return AVIERR_BADHANDLE;
//***** 定义HAVIStruct指针
HAVIStruct
*aviStruct = (HAVIStruct*)havi;
//删除接口引用并关闭avi
if (aviStruct->aStream!=0)
AVIStreamRelease
(aviStruct->aStream);
aviStruct->aStream=0;
if (aviStruct->pStreamCompressed!=0)
AVIStreamRelease
(aviStruct->pStreamCompressed);
aviStruct->pStreamCompressed=0;
if (aviStruct->pStream!=0)
AVIStreamRelease
(aviStruct->pStream);
aviStruct->pStream=0;
if (aviStruct->pfile!=0)
AVIFileRelease
(aviStruct->pfile);
aviStruct->pfile=0;
AVIFileExit
();
delete aviStruct;
// 释放avi句柄
return S_OK;
}
对了,还有个
AddAviFrame
函数:
// *************** 添加视频帧
****************
// havi avi句柄
// hbm 位图句柄,必须指向 DIBSection.
HRESULT CAviMaker::
AddAviFrame
(HAVI havi, HBITMAP hbm)
{
// 参数为null,返回
if (NULL==havi)
{
MessageBox(NULL,L"CAviMaker::AddAviFrame:传入视频为空",L"error",MB_OK);
return AVIERR_BADHANDLE;
}
if (NULL==hbm)
{
MessageBox(NULL,L"CAviMaker::AddAviFrame:传入位图为空",L"error",MB_OK);
return AVIERR_BADPARAM;
}
// 创建DIB位图区
DIBSECTION
dibs;
int sbm = GetObject(hbm,sizeof(DIBSECTION),&dibs);
if (sizeof(DIBSECTION) != sbm)
{
MessageBox(NULL,L"CAviMaker::AddAviFrame:设备无关位图块错误",L"error",MB_OK);
return AVIERR_BADPARAM;
}
HAVIStruct
*aviStruct = (HAVIStruct*)havi;
if (aviStruct->iserr)
{
MessageBox(NULL,L"CAviMaker::AddAviFrame:视频处于错误状态",L"error",MB_OK);
return
AVIERR_ERROR;
}
// 如果不存在视频流则创建
if (aviStruct->pStream==0)
{
// 设置视频流信息
AVISTREAMINFO
strhdr;
ZeroMemory(&strhdr,sizeof(strhdr));
strhdr.fccType
= streamtypeVIDEO;
//
流类型
strhdr.fccHandler
= 0;
//
编码器
strhdr.dwScale
= aviStruct->period;
//
帧距
strhdr.dwRate
= 1000;
//
样本帧数
strhdr.dwSuggestedBufferSize
=
dibs.dsBmih.biSizeImage;
//
缓存大小
SetRect(&strhdr.rcFrame,
0, 0, dibs.dsBmih.biWidth, dibs.dsBmih.biHeight);
// 视频尺寸
//
创建视频流
HRESULT
hr=
AVIFileCreateStream
(aviStruct->pfile,
&aviStruct->pStream,
&strhdr);
if
(hr!=AVIERR_OK)
{
MessageBox(NULL,L"CAviMaker::AddAviFrame:创建视频流出错",L"error",MB_OK);
aviStruct->iserr=true;
return
hr;
}
}
// 如果不存在压缩视频流则创建
if (aviStruct->pStreamCompressed==0)
{
// 设置压缩选项
AVICOMPRESSOPTIONS
opts;
ZeroMemory(&opts,sizeof(opts));
//opts.fccHandler=mmioFOURCC('D','I','B','
');
opts.fccHandler=mmioFOURCC('x','v','i','d');
// 创建压缩视频流
HRESULT
hr =
AVIMakeCompressedStream
(&aviStruct->pStreamCompressed,
aviStruct->pStream,
&opts,
NULL);
if (hr != AVIERR_OK)
{
MessageBox(NULL,L"CAviMaker::AddAviFrame:创建压缩视频流出错",L"error",MB_OK);
aviStruct->iserr=true;
return hr;
}
// 设置压缩视频流格式
hr =
AVIStreamSetFormat
(aviStruct->pStreamCompressed,
0,
&dibs.dsBmih,
dibs.dsBmih.biSize
+ dibs.dsBmih.biClrUsed * sizeof(RGBQUAD));
if (hr!=AVIERR_OK)
{
MessageBox(NULL,L"CAviMaker::AddAviFrame:设置压缩视频流格式出错",L"error",MB_OK);
aviStruct->iserr=true;
return hr;
}
}
//添加帧到视频流
HRESULT hr =
AVIStreamWrite
(aviStruct->pStreamCompressed,
aviStruct->nextFrame,
1,
dibs.dsBm.bmBits,
dibs.dsBmih.biSizeImage,
AVIIF_KEYFRAME,
NULL,
NULL);
if (hr!=AVIERR_OK)
{
MessageBox(NULL,L"CAviMaker::AddAviFrame:写入视频流出错",L"error",MB_OK);
aviStruct->iserr=true;
return
hr;
}
aviStruct->nextFrame++;
return S_OK;
}
可见这三个成员函数中调用的avi函数都是VFW提供的API,只有一个不是系统提供的结构体出现:
HAVIStruct
:
// HAVI句柄实际指向的结构
typedef struct
{
IAVIFile *pfile;
//
avi接口
WAVEFORMATEX wfx;
//
音频流创建时使用
int period;
//
视频流创建时使用
IAVIStream *aStream;
// 音频流
IAVIStream *pStream, *pStreamCompressed;
// 原始视频流、压缩视频流
unsigned long nextFrame, nextSamp;
// 下一帧、下一个样本
bool iserr;
//
是否出现错误,如是,退出
}
HAVIStruct
;
注意:文中涉及的变量多为全局变量,所以没有列出其定义
我们现在要考虑的是:开始或结束菜单点击后应该置灰,不然可要乱掉了,所以我们在点击这两个菜单项后要调用下面的代码:
HRESULT
SetMenuState
(HWND hWnd,BOOL isStartEnabled,BOOL isEndEnabled)
{
HMENU hMenu;
hMenu=GetMenu(hWnd);
UINT uState;
if(isStartEnabled)
uState = MF_ENABLED | MF_BYCOMMAND;
else
uState = MF_DISABLED | MF_GRAYED | MF_BYCOMMAND;
EnableMenuItem(hMenu,IDM_START,uState);
EnableMenuItem(hMenu,IDM_OPTION,uState);
if(isEndEnabled)
uState = MF_ENABLED | MF_BYCOMMAND;
else
uState = MF_DISABLED | MF_GRAYED | MF_BYCOMMAND;
EnableMenuItem(hMenu,IDM_END,uState);
return S_OK;
}
现在发现问题了,我们要录屏,总不能运行软件弹出个窗口,然后点击菜单这样操作吧,岂不是会录到我们的录屏软件,这太山寨了。所以要跟随主流,在通知栏中建立一个快捷图标,然后可以弹出右键菜单操作。而主窗口呢,创建时就直接隐藏。这样我们必须在程序运行时创建通知栏图标,在程序退出时,删除通知栏图标。所以在这两个位置,我们要调用下面的函数:
// 设置通知图标
// hWnd 窗口句柄
// isAdd 是否是添加图标
HRESULT
SetNotifyIcon
(HWND hWnd,BOOL isAdd)
{
hIcon = LoadIcon(hInst, MAKEINTRESOURCE(IDI_SCREENCAPTUREUNIT));
nid.hIcon=hIcon;
nid.cbSize=sizeof(NOTIFYICONDATA);
nid.hWnd=hWnd;
nid.uFlags=NIF_MESSAGE|NIF_ICON|NIF_TIP;
nid.uID=IDR_TRAYMENU;
WCHAR tipString[128]={'S','c','r','e','e','n','C','a','p','t','u','r','e'};
memcpy(nid.szTip,tipString,128);
nid.uCallbackMessage=WM_MY_TRAY_NOTIFICATION;
BOOL ret;
if(isAdd)
ret=Shell_NotifyIcon(NIM_ADD,&nid);
//
添加
else
ret=Shell_NotifyIcon(NIM_DELETE,&nid);
// 删除
if(!ret)
{
MessageBox(NULL,L"SetNotifyIcon:设置通知图标失败",L"error",MB_OK);
return AVIERR_ERROR;
}
return S_OK;
}
当然添加了通知托盘图标,总是要处理上面的事件的。可以在主窗口消息处理函数中添加对
WM_MY_TRAY_NOTIFICATION
的处理:
case WM_MY_TRAY_NOTIFICATION:
OnTrayNotification
(nid.uID,lParam);
break
;
在
OnTrayNotification
中主要是对通知栏图标的事件处理,比如我右击后,弹出的菜单上面的菜单项的图标应该是置灰的还是正常的:
// 通知托盘处理函数
LRESULT
OnTrayNotification
(WPARAM wID, LPARAM lEvent)
{
if (wID!=nid.uID || (lEvent!=WM_RBUTTONUP && lEvent!=WM_LBUTTONDBLCLK))
return 0;
HMENU hMenu;
hMenu=LoadMenuW(hInst,MAKEINTRESOURCE(nid.uID));
HMENU hSubMenu = GetSubMenu(hMenu,0);
if (!hSubMenu||!hMenu)
return 0;
LPPOINT mouse = new POINT();
GetCursorPos(mouse);
SetForegroundWindow
(nid.hWnd);
//****** 调用这个函数很重要,不然右键菜单弹出有问题(可以试下)
HBITMAP bit=LoadBitmap(hInst, MAKEINTRESOURCE(IDB_BITMAP_START));
SetMenuItemBitmaps(hSubMenu,0,MF_BYPOSITION,bit,NULL);
bit=LoadBitmap(hInst, MAKEINTRESOURCE(IDB_BITMAP_END));
SetMenuItemBitmaps(hSubMenu,1,MF_BYPOSITION,bit,NULL);
bit=LoadBitmap(hInst, MAKEINTRESOURCE(IDB_BITMAP_OPTION));
SetMenuItemBitmaps(hSubMenu,2,MF_BYPOSITION,bit,NULL);
bit=LoadBitmap(hInst, MAKEINTRESOURCE(IDB_BITMAP_QUIT));
SetMenuItemBitmaps(hSubMenu,3,MF_BYPOSITION,bit,NULL);
if(endFlag)
{
EnableMenuItem(hMenu,IDM_START,MF_ENABLED | MF_BYCOMMAND);
EnableMenuItem(hMenu,IDM_END,MF_DISABLED | MF_GRAYED | MF_BYCOMMAND);
EnableMenuItem(hMenu,IDM_OPTION,MF_ENABLED | MF_BYCOMMAND);
}
else
{
EnableMenuItem(hMenu,IDM_START,MF_DISABLED | MF_GRAYED | MF_BYCOMMAND);
EnableMenuItem(hMenu,IDM_END,MF_ENABLED | MF_BYCOMMAND);
EnableMenuItem(hMenu,IDM_OPTION,MF_DISABLED | MF_GRAYED | MF_BYCOMMAND);
}
BOOL ret=TrackPopupMenu(hSubMenu, 0, mouse->x, mouse->y, 0,nid.hWnd, NULL);
return ret;
}
至于通知栏图标的右键菜单中的菜单项,可以把ID设成
发表评论