
简介这是一份面向C与Windows桌面开发初学者的MFC摄像头调用实战工程基于Visual Studio 2010环境借助DirectShow框架实现笔记本摄像头的枚举、预览与视频流控制适合正在学习MFC界面编程与多媒体采集的开发者参考。压缩包共78个文件约35.71MB包含sln解决方案与vcxproj工程文件、cpp与h源码、rc资源脚本与ico图标、exe可执行程序、avi示例视频以及obj、pdb、tlog等编译中间产物完整保留了Debug与Release两套构建配置便于直接运行或二次修改。工程内已封装CvvImage图像处理类并演示了通过IAMSampleGrabber抓帧、ICaptureGraphBuilder2录制视频的扩展思路可作为截图与录像功能的改造起点。目前已有1031人学习下载适合希望打通MFC与DirectShow多媒体开发链路、积累Windows平台摄像头项目经验的读者。1. 打开摄像头-MFC一份能直接跑起来的 C 摄像头调用工程很多人第一次在 Windows 上写摄像头程序都会卡在同一个地方设备枚举出来了预览窗口却死活不出画面或者一关程序就报一堆 COM 释放错误。这份打开摄像头-MFC.zip就是冲着这个场景来的——它是一套基于 MFC 和 DirectShow 的完整工程用 C 调用笔记本摄像头带对话框界面、按钮触发、视频预览还附了一个MyVideo.avi作为输出样例。解压后能看到VideoMFC.sln、VideoMFC.vcxproj、VideoMFCDlg.cpp、CvvImage.cpp这些核心文件以及 Debug/Release 两套编译产物说明作者是在 Visual Studio 2010 环境下实际编译运行过的。它适合两类人一类是刚接触 MFC、想找一个能跑通的摄像头 Demo 当起点的 C 新手另一类是老手想拿一份干净的 DirectShow 过滤器图代码做二次开发比如加截图、录像、格式切换。下面我按「工程结构 → 编译运行 → 核心代码 → 避坑 → 进阶」的顺序把这份资源拆开讲透。2. 工程结构与编译环境先搞清楚每个文件干什么2.1 目录里到底有什么解压打开摄像头-MFC.zip之后根目录下是解决方案和工程文件Debug、Release、ipch 三个文件夹是编译中间产物和缓存。真正需要关注的源码文件并不多我列一个清单方便你对照文件作用VideoMFC.slnVS 解决方案入口双击打开VideoMFC.vcxproj工程配置含字符集、MFC 使用方式VideoMFCDlg.cpp / .h主对话框逻辑按钮响应和摄像头调用都在这VideoMFC.cpp / .h应用程序类负责启动对话框CvvImage.cpp / .hOpenCV 风格的图像显示辅助类用于把帧画到控件上stdafx.cpp / .h预编译头targetver.h目标系统版本宏MyVideo.avi示例视频输出验证录像功能res文件夹图标、位图等资源CvvImage这个类值得单独说一句。它原本是 OpenCV 早期版本里用来在 MFC 控件上绘制IplImage的辅助类这里被抽出来单独使用说明工程在预览渲染上走的是「抓帧 → 转成图像 → 画到 Picture Control」的路线而不是直接把视频窗口嵌进对话框。这个选择后面讲避坑时会再提。2.2 编译环境怎么配工程是 VS2010 建的.vcxproj里带vc100工具集标记。如果你用 VS2010直接打开就能编。用更高版本VS2015/2017/2019/2022打开时VS 会提示「重定向项目」点确认即可但要注意两点一是字符集工程用的是 Unicode别改成多字节二是 MFC 的使用方式必须在「项目属性 → 配置属性 → 常规 → 使用 MFC」里选「在共享 DLL 中使用 MFC」或「在静态库中使用 MFC」否则会报一堆afxwin.h找不到的错。编译前确认系统里装了 Windows SDKDirectShow 的头文件dshow.h、strmif.h都在 SDK 里不需要额外装 DirectX SDK。链接库方面工程默认已经带了strmiids.lib、quartz.lib这类如果报CLSID_FilterGraph未解析检查一下链接器输入里有没有strmiids.lib。# 如果你习惯命令行编译可以用 msbuild 指定工具集 msbuild VideoMFC.sln /p:ConfigurationRelease /p:PlatformWin32 /p:PlatformToolsetv100这段命令的意思是用 Release 配置、Win32 平台、v100 工具集编译。PlatformToolset是关键参数如果你机器上没装 v100 工具集改成v143VS2022或对应版本即可。编译成功后Debug 或 Release 目录下会生成VideoMFC.exe双击就能跑。提示如果编译时报fatal error C1083: 无法打开包括文件: dshow.h说明 Windows SDK 没装全去 VS 安装器里勾上「Windows SDK」组件。3. 核心代码拆解DirectShow 过滤器图怎么搭3.1 按钮响应与 COM 初始化工程的主界面是一个对话框上面有个按钮ID 是IDC_START_CAMERA具体 ID 以资源视图为准。点击按钮触发OnBnClickedStartCamera这个函数是整个摄像头调用的入口。DirectShow 基于 COM所以第一步永远是CoInitialize最后一步是CoUninitialize中间所有接口指针都要Release。void CVideoMFCDlg::OnBnClickedStartCamera() { // 初始化 COM 库DirectShow 依赖 COM HRESULT hr CoInitialize(NULL); if (FAILED(hr)) { AfxMessageBox(_T(COM 初始化失败)); return; } // 创建系统设备枚举器用来找摄像头 ICreateDevEnum* pDevEnum NULL; hr CoCreateInstance(CLSID_SystemDeviceEnum, NULL, CLSCTX_INPROC_SERVER, IID_ICreateDevEnum, (void**)pDevEnum); if (FAILED(hr)) { CoUninitialize(); return; } // 枚举视频捕获设备类别 IEnumMoniker* pEnum NULL; hr pDevEnum-CreateClassEnumerator(CLSID_VideoInputDeviceCategory, pEnum, 0); if (hr ! S_OK) { AfxMessageBox(_T(没有找到摄像头设备)); pDevEnum-Release(); CoUninitialize(); return; } // 取第一个摄像头 IMoniker* pMoniker NULL; if (pEnum-Next(1, pMoniker, NULL) ! S_OK) { AfxMessageBox(_T(摄像头枚举为空)); pEnum-Release(); pDevEnum-Release(); CoUninitialize(); return; } // 绑定 Moniker 到 IBaseFilter IBaseFilter* pCapFilter NULL; hr pMoniker-BindToObject(NULL, NULL, IID_IBaseFilter, (void**)pCapFilter); // ... 后续建图 }这段代码的逻辑是CoCreateInstance拿到设备枚举器 →CreateClassEnumerator指定视频输入设备类别 →Next取第一个设备 →BindToObject把设备标识绑定成过滤器。参数上CLSID_VideoInputDeviceCategory是固定值代表摄像头类别CLSCTX_INPROC_SERVER表示进程内组件。注意CreateClassEnumerator返回S_FALSE时表示类别为空不是错误但这里按「没找到设备」处理符合实际使用场景。3.2 建图、预览与格式设置拿到IBaseFilter之后要创建过滤器图管理器IGraphBuilder把捕获过滤器加进去再通过ICaptureGraphBuilder2或直接RenderStream把预览引脚接到渲染器上。工程里用的是RenderStream的简化写法配合IAMStreamConfig设置分辨率。// 创建过滤器图 IGraphBuilder* pGraph NULL; CoCreateInstance(CLSID_FilterGraph, NULL, CLSCTX_INPROC_SERVER, IID_IGraphBuilder, (void**)pGraph); // 把摄像头过滤器加入图 pGraph-AddFilter(pCapFilter, LCapture Filter); // 查询流配置接口设置视频格式 IAMStreamConfig* pConfig NULL; hr pCapFilter-QueryInterface(IID_IAMStreamConfig, (void**)pConfig); if (SUCCEEDED(hr)) { int count 0, size 0; pConfig-GetNumberOfCapabilities(count, size); // 遍历能力选一个 640x480 的格式 for (int i 0; i count; i) { VIDEO_STREAM_CONFIG_CAPS caps; AM_MEDIA_TYPE* pmt NULL; if (SUCCEEDED(pConfig-GetStreamCaps(i, pmt, (BYTE*)caps))) { if (pmt-formattype FORMAT_VideoInfo) { VIDEOINFOHEADER* vih (VIDEOINFOHEADER*)pmt-pbFormat; if (vih-bmiHeader.biWidth 640 vih-bmiHeader.biHeight 480) { pConfig-SetFormat(pmt); break; } } // 释放媒体类型 } } } // 渲染预览引脚自动接上视频渲染器 pGraph-RenderStream(NULL, PIN_CATEGORY_PREVIEW, pCapFilter, NULL, NULL); // 启动图 IMediaControl* pControl NULL; pGraph-QueryInterface(IID_IMediaControl, (void**)pControl); pControl-Run();GetNumberOfCapabilities返回设备支持的格式数量GetStreamCaps逐个取出能力描述VIDEOINFOHEADER里带分辨率。选 640x480 是个稳妥的默认值兼容性最好。RenderStream的第二个参数用PIN_CATEGORY_PREVIEW表示走预览引脚如果设备没有独立预览引脚DirectShow 会自动用捕获引脚加 Smart Tee 分流这也是为什么有些摄像头不设格式也能出画面。注意RenderStream之后一定要查IMediaControl并Run否则图建好了但视频流不跑界面一片黑这是新手最常见的「玄学黑屏」来源。3.3 截图与录像的扩展点工程里附了MyVideo.avi说明录像功能是通的。实现思路通常是加一个ISampleGrabber过滤器插在捕获过滤器和渲染器之间在BufferCB回调里拿每一帧的原始数据再交给CvvImage画到 Picture Control 上或者用AVIStream写文件。CvvImage.cpp里的DrawToHDC方法就是干这个的把帧数据转成 DIB再StretchBlt到控件 DC。// SampleGrabber 回调里抓帧的典型写法 STDMETHODIMP CSampleGrabberCB::BufferCB(double SampleTime, BYTE* pBuffer, long BufferLen) { // pBuffer 是当前帧的位图数据BufferLen 是字节数 // 这里可以保存为 BMP或写入 AVI 流 if (m_bSaveFrame) { // 用 CvvImage 或直接写文件 m_pDlg-OnFrameReceived(pBuffer, BufferLen); } return S_OK; }参数上SampleTime是帧时间戳pBuffer指向帧数据格式取决于前面SetFormat设的AM_MEDIA_TYPE。如果设的是 RGB24每像素 3 字节如果是 MJPEG需要先解码。工程里CvvImage默认按 RGB 处理所以格式设置那一步最好显式选 RGB 或 YUV 转 RGB否则画出来颜色是花的。4. 避坑与排查摄像头程序最容易翻车的五个点4.1 预览黑屏但设备枚举成功现象程序跑起来按钮点了没报错但 Picture Control 里全黑。原因通常是RenderStream用的引脚类别不对或者图没Run。有些摄像头只有捕获引脚没有预览引脚用PIN_CATEGORY_PREVIEW会失败。解决办法是改成PIN_CATEGORY_CAPTURE或者用ICaptureGraphBuilder2::RenderStream让它自动选。另外检查IMediaControl::Run的返回值S_FALSE表示图在过渡状态等几百毫秒再看。4.2 关闭程序时报 COM 释放异常现象点关闭按钮弹「内存不能为 read」或直接崩。原因是接口Release顺序错了或者重复释放。DirectShow 的规则是「谁创建谁释放后创建先释放」。CoInitialize和CoUninitialize必须配对且CoUninitialize要在所有接口释放之后调用。工程里如果用了CComPtr智能指针会省心很多手写Release容易漏。4.3 高版本 VS 打开后编译报 afxwin.h 找不到现象VS2019/2022 打开.vcxproj编译提示Cannot open include file: afxwin.h。原因是 MFC 组件没装或者工程属性里「使用 MFC」被重置了。解决VS 安装器里勾「C MFC 最新生成工具」然后在项目属性里把「使用 MFC」设为「在共享 DLL 中使用 MFC」。如果还不行检查stdafx.h里#include afxwin.h是否被条件编译挡掉了。4.4 摄像头被其他程序占用现象枚举到设备BindToObject也成功但Run之后没画面。原因很可能是摄像头被 QQ、钉钉、浏览器或另一个调试实例占着。Windows 的摄像头驱动通常独占访问一个进程打开后另一个就打不开。解决关掉所有可能用摄像头的程序任务管理器里结束残留的VideoMFC.exe进程再重试。调试时尤其注意VS 停止调试不一定释放设备要等进程完全退出。4.5 分辨率设置不生效现象代码里明明设了 640x480出来还是 1280x720。原因是SetFormat必须在RenderStream之前调用而且传入的AM_MEDIA_TYPE必须是GetStreamCaps返回的原始指针不能自己拼一个。有些驱动对格式匹配很严格宽高、步长、颜色空间差一个字节都不认。稳妥做法是遍历能力列表找到完全匹配的那一项再SetFormat设完检查返回值。5. 进阶用法把这份工程改成你自己的采集工具5.1 多摄像头切换与设备名显示工程默认取第一个摄像头实际项目里往往要列出所有设备让用户选。做法是在枚举IMoniker时通过IPropertyBag读FriendlyName填进 ComboBox。切换时先Stop图、RemoveFilter旧设备、释放旧接口再按新 Moniker 重建。注意每次切换都要重新RenderStream不能复用旧图。// 读取摄像头友好名称 IPropertyBag* pBag NULL; pMoniker-BindToStorage(0, 0, IID_IPropertyBag, (void**)pBag); VARIANT varName; VariantInit(varName); pBag-Read(LFriendlyName, varName, 0); CString strName varName.bstrVal; // 显示到下拉框 VariantClear(varName); pBag-Release();FriendlyName是设备在系统里显示的名字BindToStorage拿到属性包Read读出 VARIANT。这段代码放在枚举循环里就能把「Integrated Camera」「USB Video Device」这些名字列出来。5.2 用 SampleGrabber 做实时帧处理如果要在预览的同时做图像处理比如人脸检测、运动检测ISampleGrabber是最直接的切入点。把它加在捕获过滤器和渲染器之间设SetBufferSamples(TRUE)和SetOneShot(FALSE)然后在回调里处理。注意回调线程不是 UI 线程直接操作控件会崩要用PostMessage把数据传回主线程。参数含义建议值SetMediaType指定抓帧格式RGB24 或 YUY2SetBufferSamples是否缓冲样本TRUESetOneShot是否只抓一帧FALSE连续BufferCB回调每帧触发处理耗时控制在 30ms 内5.3 录像保存为 AVI 的注意点工程里的MyVideo.avi说明录像链路是通的。自己实现时常见做法是用ICaptureGraphBuilder2::SetOutputFileName配合AVI Mux和File Writer把捕获引脚直接写成文件。这样不用手动处理帧数据DirectShow 帮你封装。缺点是文件体积大且不能同时做复杂处理。如果既要预览又要录像用 Smart Tee 过滤器分流一路去渲染器一路去 AVI Mux。// 用 CaptureGraphBuilder 设置 AVI 输出 ICaptureGraphBuilder2* pBuild NULL; CoCreateInstance(CLSID_CaptureGraphBuilder2, NULL, CLSCTX_INPROC_SERVER, IID_ICaptureGraphBuilder2, (void**)pBuild); pBuild-SetFiltergraph(pGraph); pBuild-SetOutputFileName(MEDIASUBTYPE_Avi, Loutput.avi, pMux, NULL); pBuild-RenderStream(PIN_CATEGORY_CAPTURE, MEDIATYPE_Video, pCapFilter, NULL, pMux);SetOutputFileName的第二个参数是输出路径MEDIASUBTYPE_Avi指定 AVI 容器。RenderStream把捕获引脚接到 Mux 上Mux 再自动接 File Writer。这套写法比手动抓帧写文件省事但灵活性差一些适合「录下来就行」的场景。5.4 我自己的习惯这份工程我前后拆过三次第一次是刚学 MFC 时照着抄第二次是给一个检测项目做摄像头输入第三次是帮人排查黑屏问题。踩过的坑里最冤的一次是CoInitialize在对话框构造函数里调了CoUninitialize在析构里调结果按钮点第二次就崩——因为 COM 库被提前反初始化了。从那以后我每次写 DirectShow 相关代码都强制把CoInitialize放在按钮响应开头、CoUninitialize放在所有Release之后并且用CComPtr管接口不再手写Release。另外调试摄像头程序时我习惯先跑一个最小枚举 Demo确认设备能被系统识别再往上叠建图逻辑这样出问题能快速定位是设备层还是图层。希望这份拆解能帮你少走点弯路把这份 MFC 摄像头工程真正用起来。本文还有配套的精品资源点击获取