
简介这是一份面向C与Windows桌面开发初学者的MFC摄像头调用示例工程基于Visual Studio 2010环境借助DirectShow框架实现笔记本摄像头的枚举、预览与视频流启动适合正在学习MFC界面编程与多媒体采集的开发者参考实践。压缩包共78个文件约35.71MB包含sln解决方案与vcxproj工程文件、cpp与h源码、rc资源脚本与ico图标、exe可执行程序、avi示例视频、bmp位图以及obj、pdb、tlog等编译中间产物完整保留了Debug与Release两套构建配置便于直接打开运行或对照调试。目前已有1031人学习下载。读者可从中获得一套可直接编译运行的摄像头调用工程模板理解过滤器图构建、设备枚举与视频预览的代码组织方式并在此基础上扩展截图与视频保存功能同时借助目录结构熟悉VS项目的资源与中间文件分布为后续多媒体应用开发打下基础。1. 打开摄像头MFC 里调摄像头这件事卡住的人比想象中多很多人第一次在 MFC 工程里点下「打开摄像头」按钮弹出来的不是画面而是一串 HRESULT 错误码或者干脆窗口一片黑。标题里的 MFC、C、调用摄像头三个词拆开都不难合在一起却让不少人翻车MFC 是微软那套基于消息循环的 C 桌面框架摄像头采集走的是 DirectShow 或 Media Foundation 这套 COM 组件两边的线程模型、消息泵、资源释放节奏完全不是一回事。这篇笔记面向的是手上已经有 Visual Studio、写过 MFC 对话框程序、现在要把 USB 摄像头画面塞进 Picture Control 的 C 开发者。我会把从工程配置、设备枚举、采集回调到窗口绘制的完整链路讲清楚参数怎么设、哪里会崩、怎么排查都落到具体代码上照着改就能跑起来。2. 选 DirectShow 还是 Media FoundationMFC 调摄像头的两条路2.1 两套采集框架在 MFC 场景下的真实差别在 Windows 上让 C 程序拿到摄像头帧主流就两条路DirectShow 和 Media Foundation。DirectShow 是 DirectX 时代留下来的老框架接口稳定、资料多、网上能搜到的 MFC 示例绝大多数基于它Media Foundation 是 Vista 之后微软主推的替代品对高清、硬件编码支持更好但 API 更啰嗦异步模型对新手不友好。我一般给 MFC 项目的建议是如果只是把摄像头画面显示到对话框上、做点简单截图或录像DirectShow 足够代码量小出问题也好查。如果要做 1080p 以上、多路采集、和硬件编码器对接再考虑 Media Foundation。下面这张表是我实际选型时会对照的几个维度。维度DirectShowMedia Foundation上手难度低ISampleGrabber 直接拿帧中高需要处理异步回调MFC 集成成熟消息循环兼容好需要自己管工作队列高分辨率支持一般老设备驱动更稳好原生支持硬件加速资料丰富度多老代码可参考少官方文档为主适合场景单路预览、截图、简单录像多路、高清、编码管线标题里没指定框架我按最常见的做法走 DirectShow因为绝大多数「MFC 调用摄像头」的需求就是单路预览。选它的另一个理由是 ISampleGrabber 这个接口能让我们在回调里直接拿到 RGB 数据省掉自己解析媒体类型的麻烦。2.2 工程配置把 DirectShow 的头文件和库挂上新建或打开一个 MFC 对话框工程后第一件事是把 DirectShow 的开发文件接进来。Windows SDK 里自带 dshow.h 和 strmiids.lib但 ISampleGrabber 属于旧版 DirectShow SDK 的 qedit 组件需要额外处理。在 stdafx.h 或 pch.h 里加头文件引用// pch.h 或 stdafx.h #include dshow.h #include comdef.h // 用于 _com_error 打印 HRESULT 描述 #include qedit.h // ISampleGrabber 定义需确保 SDK 路径包含它如果编译报找不到 qedit.h说明当前 SDK 没带这个老文件。常见做法是从旧版 DirectShow SDK 里把 qedit.h 拷到工程目录或者直接用接口的 GUID 手动声明。链接库在项目属性里加# 项目属性 - 链接器 - 输入 - 附加依赖项 strmiids.lib quartz.lib提示qedit.h 和新版 Windows SDK 里的某些定义会冲突如果报重定义错误把 qedit.h 里重复的接口声明注释掉只保留 ISampleGrabber 和 ISampleGrabberCB 相关部分。配置这一步的坑在于 32 位和 64 位。DirectShow 的 SampleGrabber 在 64 位下能用但部分老摄像头驱动只有 32 位版本工程平台要和驱动匹配。我一般先用 x64 试设备枚举不到再切 Win32。2.3 枚举设备并绑定第一个摄像头配置好之后第一步是枚举系统里的视频采集设备。DirectShow 用 ICreateDevEnum 配合 CLSID_VideoInputDeviceCategory 来做这件事。#include dshow.h // 枚举视频设备返回设备友好名称列表 HRESULT EnumVideoDevices(std::vectorCString names) { ICreateDevEnum* pDevEnum nullptr; IEnumMoniker* pEnum nullptr; HRESULT hr CoCreateInstance(CLSID_SystemDeviceEnum, nullptr, CLSCTX_INPROC_SERVER, IID_PCreateDevEnum, (void**)pDevEnum); if (FAILED(hr)) return hr; hr pDevEnum-CreateClassEnumerator(CLSID_VideoInputDeviceCategory, pEnum, 0); if (hr ! S_OK) { pDevEnum-Release(); return hr; } // S_FALSE 表示没有设备 IMoniker* pMoniker nullptr; while (pEnum-Next(1, pMoniker, nullptr) S_OK) { IPropertyBag* pBag nullptr; hr pMoniker-BindToStorage(0, 0, IID_IPropertyBag, (void**)pBag); if (SUCCEEDED(hr)) { VARIANT var; VariantInit(var); // 读取 FriendlyName 属性 hr pBag-Read(LFriendlyName, var, nullptr); if (SUCCEEDED(hr)) { names.push_back(var.bstrVal); VariantClear(var); } pBag-Release(); } pMoniker-Release(); } pEnum-Release(); pDevEnum-Release(); return S_OK; }这段代码的逻辑是先拿到系统设备枚举器再按「视频输入设备」这个类别创建枚举器逐个取出 moniker从属性包里读 FriendlyName。参数上CreateClassEnumerator 的第三个参数传 0 表示只枚举这一类返回 S_FALSE 不是错误只是说明当前没有摄像头。实际项目里我会把 names 显示到一个 ComboBox 里让用户选而不是硬编码第一个设备因为笔记本自带的摄像头和 USB 外接摄像头经常同时存在。3. 把摄像头画面画到 Picture Control采集链路怎么搭3.1 构建 Filter Graph 并插入 SampleGrabber拿到设备 moniker 之后要构建一条 Filter Graph源滤波器摄像头→ SampleGrabber → 空渲染器Null Renderer。SampleGrabber 负责在数据流过时回调给我们Null Renderer 负责让整条链路有个终点不然 Graph 跑不起来。IGraphBuilder* pGraph nullptr; ICaptureGraphBuilder2* pBuilder nullptr; IBaseFilter* pCap nullptr; // 摄像头源 IBaseFilter* pGrabberF nullptr; // SampleGrabber ISampleGrabber* pGrabber nullptr; IBaseFilter* pNullRenderer nullptr; CoCreateInstance(CLSID_FilterGraph, nullptr, CLSCTX_INPROC_SERVER, IID_IGraphBuilder, (void**)pGraph); CoCreateInstance(CLSID_CaptureGraphBuilder2, nullptr, CLSCTX_INPROC_SERVER, IID_ICaptureGraphBuilder2, (void**)pBuilder); pBuilder-SetFiltergraph(pGraph); // 用 moniker 绑定到摄像头源滤波器 pMoniker-BindToObject(0, 0, IID_IBaseFilter, (void**)pCap); pGraph-AddFilter(pCap, LCapture Source); // 创建 SampleGrabber 并加入 Graph CoCreateInstance(CLSID_SampleGrabber, nullptr, CLSCTX_INPROC_SERVER, IID_IBaseFilter, (void**)pGrabberF); pGraph-AddFilter(pGrabberF, LSample Grabber); pGrabberF-QueryInterface(IID_ISampleGrabber, (void**)pGrabber); // 设置期望的媒体类型24 位 RGB AM_MEDIA_TYPE mt {}; mt.majortype MEDIATYPE_Video; mt.subtype MEDIASUBTYPE_RGB24; pGrabber-SetMediaType(mt); // 创建 Null Renderer 并加入 CoCreateInstance(CLSID_NullRenderer, nullptr, CLSCTX_INPROC_SERVER, IID_IBaseFilter, (void**)pNullRenderer); pGraph-AddFilter(pNullRenderer, LNull Renderer); // 连接源 - Grabber - NullRenderer pBuilder-RenderStream(PIN_CATEGORY_CAPTURE, MEDIATYPE_Video, pCap, pGrabberF, pNullRenderer);参数上最关键的是 SetMediaType 里的 MEDIASUBTYPE_RGB24。摄像头原生输出通常是 YUY2 或 MJPGSampleGrabber 会自动做色彩空间转换转成 RGB24 后每帧数据就是 width×height×3 字节方便直接画到内存 DC。代价是转换有 CPU 开销1080p 下比较明显720p 一般够用。SetBufferSamples 和 SetOneShot 这两个参数后面回调时会用到。3.2 回调函数里拿帧并转成位图SampleGrabber 支持两种回调ISampleGrabberCB 的 SampleCB 和 BufferCB。BufferCB 直接给一帧连续内存处理起来简单我一般用它。class CSampleGrabberCB : public ISampleGrabberCB { public: STDMETHODIMP BufferCB(double SampleTime, BYTE* pBuffer, long BufferLen) { if (m_pOwner pBuffer BufferLen 0) { // 把帧数据拷到主线程待处理缓冲避免在回调线程直接操作 UI m_pOwner-OnFrameReceived(pBuffer, BufferLen); } return S_OK; } // QueryInterface / AddRef / Release 按 COM 规范实现 // ... };回调运行在 DirectShow 的工作线程上绝对不能在这里直接调 MFC 控件的 SetBitmap 或 Invalidate否则轻则界面卡死重则崩溃。我的做法是把帧拷进一个双缓冲用 PostMessage 通知主线程去取。BufferLen 应该等于 width×height×3如果对不上说明 SetMediaType 没生效需要检查摄像头是否支持 RGB24 输出。3.3 在 OnPaint 里把帧画到 Picture Control主线程收到消息后把 RGB 数据包装成 GDI 位图画到 Picture Control 对应的矩形里。void CCameraDlg::OnFrameReady() { if (m_frameBuf.empty()) return; CDC* pDC m_picCamera.GetDC(); // Picture Control 的 DC CRect rc; m_picCamera.GetClientRect(rc); BITMAPINFO bmi {}; bmi.bmiHeader.biSize sizeof(BITMAPINFOHEADER); bmi.bmiHeader.biWidth m_frameWidth; bmi.bmiHeader.biHeight -m_frameHeight; // 负值表示自上而下 bmi.bmiHeader.biPlanes 1; bmi.bmiHeader.biBitCount 24; bmi.bmiHeader.biCompression BI_RGB; // 用 StretchDIBits 直接缩放绘制省去中间位图对象 StretchDIBits(pDC-GetSafeHdc(), 0, 0, rc.Width(), rc.Height(), 0, 0, m_frameWidth, m_frameHeight, m_frameBuf.data(), bmi, DIB_RGB_COLORS, SRCCOPY); m_picCamera.ReleaseDC(pDC); }biHeight 用负值是为了让图像自上而下排列否则画面会上下颠倒这是新手最常见的翻车点之一。StretchDIBits 直接做缩放比先 CreateDIBSection 再 BitBlt 少一次拷贝720p 下帧率能稳在 30。如果 Picture Control 的宽高比和摄像头不一致画面会拉伸变形需要自己算 letterbox 的偏移量。4. 避坑与排查MFC 调摄像头最常见的 5 个翻车现场4.1 现象点按钮没反应或者弹「无法创建 Graph」原因通常是 COM 没初始化。MFC 工程默认在 InitInstance 里调了 AfxOleInit但如果你新建的是纯 Win32 风格的对话框可能没调。DirectShow 所有接口都依赖 COM 单元。解决在 InitInstance 开头加CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED)退出时CoUninitialize()。注意 MFC 的 AfxOleInit 内部已经做了这件事重复调用会返回 RPC_E_CHANGED_MODE检查一下有没有混用。4.2 现象画面全黑但 Graph 状态是 Running原因多半是 SampleGrabber 的媒体类型没设对或者摄像头不支持 RGB24 输出RenderStream 静默失败。解决在 SetMediaType 之后调pGrabber-GetConnectedMediaType(mt)把实际协商的类型打出来看。如果 subtype 不是 RGB24改成摄像头原生支持的格式自己在回调里做转换。另一个可能是 SetBufferSamples(TRUE) 没调导致 BufferCB 拿不到数据。4.3 现象运行几分钟后崩溃或者切换设备时卡死原因是 COM 对象没按顺序释放。Filter Graph 里的滤波器有引用计数先 Release Graph 再 Release 各个 Filter 的顺序错了会出问题。解决停止时先调pMediaControl-Stop()然后依次 Release pGrabber、pGrabberF、pNullRenderer、pCap、pBuilder、pGraph每个指针 Release 后置 nullptr。回调对象要在 Stop 之后再释放否则工作线程可能还在访问它。4.4 现象界面卡顿拖动窗口时画面冻结原因是把帧处理放在了 UI 线程或者回调里做了耗时操作。BufferCB 每秒被调 30 次每次拷贝 720p 数据就是 2.7MB如果在回调里做缩放或格式转换工作线程会被拖慢进而影响整个 Graph。解决回调里只做内存拷贝转换和绘制放到主线程或单独的消费者线程。用双缓冲加临界区保护避免读写冲突。如果帧率要求高考虑用 Media Foundation 的异步模型替代。4.5 现象换一台电脑就枚举不到摄像头原因是权限或驱动。Win10 之后摄像头访问受隐私设置控制桌面应用默认有权限但如果被组策略限制就会枚举失败。另外部分 USB 摄像头需要先被系统识别为「成像设备」才能被 DirectShow 看到。解决先在「相机」应用里确认摄像头能用再跑程序。如果系统能用而程序不行检查工程平台位数和驱动是否匹配。枚举返回空列表时用SetupDiGetClassDevs交叉验证一下设备是否真的存在。5. 进阶把预览帧接到截图、录像和实时处理上跑通预览之后真正有价值的是拿这些帧做后续处理。我一般会在 OnFrameReady 里留一个钩子把 RGB 缓冲同时喂给几个消费者。截图最简单把当前帧的 BITMAPINFO 和缓冲交给 GDI 的 Bitmap 保存成 PNG// 保存当前帧为 PNG需链接 gdiplus.lib Gdiplus::Bitmap bmp(m_frameWidth, m_frameHeight, m_frameWidth * 3, PixelFormat24bppRGB, m_frameBuf.data()); CLSID pngClsid; CLSIDFromString(L{557CF406-1A04-11D3-9A73-0000F81EF32E}, pngClsid); bmp.Save(Lsnapshot.png, pngClsid, nullptr);录像的话DirectShow 本身可以用 AVI Mux 加 File Writer 滤波器在 Graph 里再挂一路输出不用自己编码。实时处理比如人脸检测、运动检测把帧转成 OpenCV 的 Mat 就行注意 OpenCV 的 BGR 顺序和 RGB24 差一个通道交换忘了这步颜色会偏蓝。验证方案是否真的可用我的习惯是连续跑 30 分钟期间反复切换设备、最小化恢复窗口、拔插 USB看有没有内存增长和句柄泄漏。任务管理器里 GDI 对象数和句柄数如果一直涨说明有资源没释放。这套东西不复杂但细节多第一次写建议先把最小链路跑通再往上加功能。希望帮到你。本文还有配套的精品资源点击获取