admin 管理员组

文章数量: 1184232

主要思路 :目标是录制屏幕上的操作保存为一个本地视频文件,我们选取通用的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设成




本文标签: 句柄 文件 编程