简介:这是一份面向C++与Windows桌面开发初学者的MFC摄像头调用实战工程,基于Visual Studio 2010环境,借助DirectShow框架实现笔记本摄像头的枚举、预览、截图与视频保存。资源包共78个文件,约35.71MB,包含sln解决方案与vcxproj工程文件、cpp与h源码、rc资源脚本与ico图标、exe可执行程序、avi示例视频、bmp位图,以及obj、pdb、tlog等编译中间产物,完整保留了Debug与Release两套构建配置,便于直接编译运行或对照调试。已有1031人学习下载。读者可从中获得一套可直接运行的摄像头采集示例,理解CoInitialize初始化COM、IFilterGraph构建过滤器图、IMoniker枚举设备、IAMStreamConfig设置视频格式等关键流程,并参考IAMSampleGrabber抓帧与ICaptureGraphBuilder2录制视频的扩展思路,适合作为MFC多媒体入门的练手项目与排错参考。
1. 打开摄像头这件事,MFC 里为什么总有人卡在第一步
很多人第一次在 MFC 工程里写「打开摄像头」,代码敲完编译通过,运行却弹出一个黑框或者直接返回失败,于是开始怀疑是驱动问题、系统问题,甚至怀疑摄像头坏了。实际情况往往更朴素:MFC 本身不提供摄像头采集能力,它只是一套 Windows 桌面 UI 框架,真正干活的是 DirectShow、Media Foundation 或者 OpenCV 这类底层库。标题里的「MFC_C++调用摄像头_MFC」拆开看就是三件事——用 C++ 写、跑在 MFC 对话框或文档程序里、把摄像头画面取出来并显示到控件上。适合谁看?手上有 Visual Studio、写过一点 C++、需要在工控上位机、医疗设备界面、教学演示软件里嵌入实时预览的开发者。这篇笔记按「先选路线、再跑最小可复现、最后处理踩坑」的顺序走,中间给可直接抄的代码和参数说明,目标是让你在半小时内看到画面,而不是在论坛里翻三天。
2. 三条调用路线怎么选:DirectShow、Media Foundation 与 OpenCV
2.1 先搞清楚 MFC 在这件事里的角色
MFC 的CWnd、CDialogEx、CStatic这些类负责的是窗口、消息循环和控件绘制。摄像头采集是另一条线:设备枚举、格式协商、帧回调、色彩空间转换。把这两条线接起来,常见做法是让采集库把每一帧交给你,你再用 GDI 的StretchDIBits或者SetDIBitsToDevice画到某个CStatic或自绘控件上。理解这个分工之后,选型就变成选采集库,而不是选 MFC 的哪个类。
DirectShow 是 Windows 上最老牌的多媒体框架,接口是 COM,ICaptureGraphBuilder2、IBaseFilter、ISampleGrabber这一套。优点是资料多、兼容性好,从 Windows XP 到 Windows 11 都能跑;缺点是代码啰嗦,ISampleGrabber在较新的 SDK 里被标记为过时,需要自己处理回调线程和 UI 线程的同步。Media Foundation 是微软推的替代方案,IMFSourceReader配合MFCreateDeviceSource读帧,性能更好、延迟更低,但要求 Windows Vista 以上,且部分老摄像头驱动对 MF 支持不完整。OpenCV 的VideoCapture是第三条路,跨平台、API 极简,cap >> frame两行就能出图,代价是引入一个几十 MB 的依赖,且默认后端在 Windows 上仍然走 DirectShow 或 MSMF,出问题时排查链路更长。
我一般这样建议:如果项目已经用了 OpenCV 做图像处理,直接用VideoCapture,省事;如果是纯 Windows 工控项目、不想引入大依赖,用 DirectShow,可控性最强;如果目标系统确定是 Win10/Win11 且追求低延迟,Media Foundation 值得投入。下面三条路线都会给最小可跑代码,你可以按自己的工程约束挑一条。
2.2 用 OpenCV 在 MFC 对话框里跑通最小预览
这是上手最快的一条路。假设你已经有一个基于CDialogEx的对话框工程,资源里放了一个 ID 为IDC_STATIC_VIDEO的静态控件。核心思路是开一个工作线程抓帧,主线程用定时器或自定义消息刷新控件,避免在采集回调里直接操作 UI。
// CameraThread.h #pragma once #include <opencv2/opencv.hpp> #include <atomic> class CCameraThread { public: CCameraThread() : m_bStop(false) {} void Start(int nIndex) { m_bStop = false; m_cap.open(nIndex, cv::CAP_DSHOW); // Windows 下优先 DirectShow 后端 if (!m_cap.isOpened()) return; m_cap.set(cv::CAP_PROP_FRAME_WIDTH, 640); m_cap.set(cv::CAP_PROP_FRAME_HEIGHT, 480); m_cap.set(cv::CAP_PROP_FPS, 30); m_thread = std::thread(&CCameraThread::Loop, this); } void Stop() { m_bStop = true; if (m_thread.joinable()) m_thread.join(); if (m_cap.isOpened()) m_cap.release(); } bool GetFrame(cv::Mat& out) { std::lock_guard<std::mutex> lk(m_mtx); if (m_frame.empty()) return false; m_frame.copyTo(out); return true; } private: void Loop() { cv::Mat tmp; while (!m_bStop) { m_cap >> tmp; if (tmp.empty()) continue; std::lock_guard<std::mutex> lk(m_mtx); tmp.copyTo(m_frame); } } cv::VideoCapture m_cap; std::thread m_thread; std::mutex m_mtx; cv::Mat m_frame; std::atomic<bool> m_bStop; };这段代码里几个参数值得说清楚。cv::CAP_DSHOW强制走 DirectShow 后端,在 Windows 上比默认的 MSMF 兼容性更稳,尤其是那些只提供 32 位驱动的老摄像头。CAP_PROP_FRAME_WIDTH/HEIGHT设成 640×480 是保守值,很多 USB 摄像头在 1920×1080 下会掉帧或协商失败,先用低分辨率跑通再往上调。CAP_PROP_FPS设 30 是请求值,实际帧率取决于设备,不要假设它一定生效。线程里用std::mutex保护m_frame,因为cv::Mat的拷贝不是原子的,主线程读、工作线程写必须加锁。
主线程这边,在对话框的OnInitDialog里启动线程,然后设一个 33 毫秒的定时器:
// 在 CDialogEx 派生类中 void CMyDlg::OnBnClickedBtnOpen() { m_cam.Start(0); // 0 表示第一个摄像头 SetTimer(1, 33, nullptr); // 约 30fps 刷新 } void CMyDlg::OnTimer(UINT_PTR nIDEvent) { if (nIDEvent == 1) { cv::Mat frame; if (m_cam.GetFrame(frame)) { // 转成 BGRA 供 GDI 绘制 cv::Mat bgra; cv::cvtColor(frame, bgra, cv::COLOR_BGR2BGRA); DrawToStatic(bgra); } } CDialogEx::OnTimer(nIDEvent); }DrawToStatic里用StretchDIBits把bgra.data画到静态控件的 DC 上,注意BITMAPINFO的biHeight要设成负值,否则图像上下颠倒。这是 OpenCV 路线最常见的翻车点,很多人第一次看到画面是倒的,以为摄像头装反了。
2.3 DirectShow 路线:不引第三方库的纯 Windows 方案
如果工程不允许引入 OpenCV,DirectShow 是标准选择。核心是建一条Capture Graph:源滤波器(摄像头)→ SampleGrabber(取帧)→ Null Renderer(不显示)。SampleGrabber 的回调在独立线程触发,你需要在回调里把缓冲区数据拷出来,再 PostMessage 给主窗口。
// 初始化 DirectShow 采集链 HRESULT InitCapture(HWND hWnd, int nIndex) { CoInitializeEx(nullptr, COINIT_MULTITHREADED); CComPtr<ICaptureGraphBuilder2> pBuilder; CComPtr<IGraphBuilder> pGraph; CoCreateInstance(CLSID_CaptureGraphBuilder2, nullptr, CLSCTX_INPROC, IID_PPV_ARGS(&pBuilder)); CoCreateInstance(CLSID_FilterGraph, nullptr, CLSCTX_INPROC, IID_PPV_ARGS(&pGraph)); pBuilder->SetFiltergraph(pGraph); // 枚举视频设备 CComPtr<ICreateDevEnum> pDevEnum; CoCreateInstance(CLSID_SystemDeviceEnum, nullptr, CLSCTX_INPROC, IID_PPV_ARGS(&pDevEnum)); CComPtr<IEnumMoniker> pEnum; pDevEnum->CreateClassEnumerator(CLSID_VideoInputDeviceCategory, &pEnum, 0); // ... 按 nIndex 跳过前 nIndex 个 moniker,取目标设备 // pMoniker->BindToObject 得到 IBaseFilter* pCap CComPtr<IBaseFilter> pGrabber; CoCreateInstance(CLSID_SampleGrabber, nullptr, CLSCTX_INPROC, IID_PPV_ARGS(&pGrabber)); CComQIPtr<ISampleGrabber> pSampleGrabber(pGrabber); AM_MEDIA_TYPE mt = {}; mt.majortype = MEDIATYPE_Video; mt.subtype = MEDIASUBTYPE_RGB24; // 让采集端直接输出 RGB24 pSampleGrabber->SetMediaType(&mt); pSampleGrabber->SetBufferSamples(FALSE); // 不缓存,回调里自己拷 pSampleGrabber->SetCallback(&g_callback, 1); pGraph->AddFilter(pCap, L"Capture"); pGraph->AddFilter(pGrabber, L"Grabber"); CComPtr<IBaseFilter> pNull; CoCreateInstance(CLSID_NullRenderer, nullptr, CLSCTX_INPROC, IID_PPV_ARGS(&pNull)); pGraph->AddFilter(pNull, L"Null"); pBuilder->RenderStream(&PIN_CATEGORY_CAPTURE, &MEDIATYPE_Video, pCap, pGrabber, pNull); CComQIPtr<IMediaControl> pControl(pGraph); pControl->Run(); return S_OK; }SetMediaType里指定MEDIASUBTYPE_RGB24是关键,这样 SampleGrabber 回调拿到的就是 24 位 RGB,省去 YUY2 转 RGB 的步骤。SetBufferSamples(FALSE)表示不复制到内部缓冲,回调里pBuffer指针只在回调期间有效,必须立刻memcpy出来。回调函数签名是SampleCB(double, IMediaSample*),在里面取GetPointer()和GetActualDataLength(),然后PostMessage给主窗口,消息的lParam可以传一个堆上分配的缓冲区指针,主窗口处理完负责delete[]。这里最容易踩的坑是跨线程访问 MFC 对象——绝不能在回调里直接调SetWindowText或Invalidate,MFC 的窗口对象有线程亲和性,跨线程操作会随机崩溃。
2.4 Media Foundation 路线:低延迟场景的取舍
Media Foundation 用IMFSourceReader读帧,代码比 DirectShow 短,但异步模型需要理解。典型流程是MFCreateDeviceSource拿设备源,MFCreateSourceReaderFromMediaSource创建 reader,SetCurrentMediaType设成 RGB32,然后循环ReadSample。它默认是同步阻塞读,放在工作线程里即可。相比 DirectShow,MF 在 Win10 上启动更快、CPU 占用略低,但部分虚拟摄像头和采集卡只暴露 DirectShow 接口,MF 枚举不到。选型时先确认你的设备在「相机」应用里能被识别,再决定是否走 MF。
3. 把画面画到 MFC 控件上:GDI 绘制的参数与坑
3.1 StretchDIBits 的关键参数逐个说
不管前面用哪条路线,最后都要落到 GDI 绘制。StretchDIBits的原型里,lpBitsInfo指向BITMAPINFO,其中bmiHeader.biHeight的符号决定图像方向:正值表示自下而上(Windows 默认 DIB 方向),负值表示自上而下。摄像头数据通常是自上而下的,所以biHeight要设成-height,否则画面上下颠倒。biBitCount设 24 或 32,对应 RGB24 和 BGRA。biCompression必须是BI_RGB,不要设成BI_JPEG。iUsage设DIB_RGB_COLORS。
void DrawToStatic(const cv::Mat& bgra, CWnd* pStatic) { if (bgra.empty() || !pStatic) return; CDC* pDC = pStatic->GetDC(); CRect rc; pStatic->GetClientRect(&rc); BITMAPINFO bmi = {}; bmi.bmiHeader.biSize = sizeof(BITMAPINFOHEADER); bmi.bmiHeader.biWidth = bgra.cols; bmi.bmiHeader.biHeight = -bgra.rows; // 负值:自上而下 bmi.bmiHeader.biPlanes = 1; bmi.bmiHeader.biBitCount = 32; bmi.bmiHeader.biCompression = BI_RGB; // 保持宽高比,居中显示 int dstW = rc.Width(), dstH = rc.Height(); double srcRatio = (double)bgra.cols / bgra.rows; double dstRatio = (double)dstW / dstH; int drawW, drawH; if (srcRatio > dstRatio) { drawW = dstW; drawH = (int)(dstW / srcRatio); } else { drawH = dstH; drawW = (int)(dstH * srcRatio); } int x = (dstW - drawW) / 2, y = (dstH - drawH) / 2; ::StretchDIBits(pDC->GetSafeHdc(), x, y, drawW, drawH, 0, 0, bgra.cols, bgra.rows, bgra.data, &bmi, DIB_RGB_COLORS, SRCCOPY); pStatic->ReleaseDC(pDC); }这段里保持宽高比的计算是实用细节:直接把 640×480 拉伸到任意大小的控件会变形,人脸会被拉宽。先算源和目标的比例,取较小的缩放方向,再居中偏移。StretchDIBits的SRCCOPY是光栅操作码,表示直接拷贝,不要用SRCAND之类。如果画面闪烁,给静态控件加WS_CLIPCHILDREN样式,或者在OnEraseBkgnd里直接返回TRUE阻止背景擦除。
3.2 双缓冲与刷新频率的平衡
直接在控件 DC 上画,帧率高时会闪。常见做法是建一个内存 DC 和兼容位图,先画到内存 DC,再BitBlt到控件 DC。但更省事的办法是给静态控件设SS_OWNERDRAW或者用CStatic派生类重写DrawItem,在DrawItem里做双缓冲。刷新频率不要盲目追高,30fps 对预览足够,定时器设 33ms;设成 10ms 只会让 CPU 空转,因为摄像头本身可能只出 15fps。判断实际帧率可以在采集线程里累加计数,每秒输出一次到状态栏——这正好呼应很多人搜的「mfc状态栏怎么显示」,用CMainFrame的SetPaneText把帧率和分辨率显示出来,调试时非常直观。
4. 避坑与排查:摄像头调不通时先看这五条
4.1 现象:cap.open(0)返回 false,设备管理器里摄像头正常
原因通常是后端选错或权限被占。Windows 10 以后摄像头有隐私开关,设置里「相机访问权限」如果关了,任何程序都打不开。另一个常见原因是CAP_DSHOW和CAP_MSMF混用,某些设备只认其中一个。解决:先确认系统隐私设置允许桌面应用访问相机;然后在代码里显式指定后端,OpenCV 用cv::CAP_DSHOW,DirectShow 确认CLSID_VideoInputDeviceCategory枚举到了设备;如果设备被其他程序(如钉钉、腾讯会议)占用,先关掉它们。
4.2 现象:画面上下颠倒或颜色发蓝
上下颠倒是BITMAPINFO.biHeight用了正值,改成负值即可。颜色发蓝是 RGB 和 BGR 通道顺序搞反了,OpenCV 的cv::Mat默认是 BGR,而 GDI 期望 BGRA 或 RGB,用cv::cvtColor(frame, out, cv::COLOR_BGR2BGRA)转成 BGRA 后biBitCount设 32,通道顺序就对了。如果用的是 DirectShow 且SetMediaType设了MEDIASUBTYPE_RGB24,GDI 端biBitCount设 24,不要设 32。
4.3 现象:运行几分钟后程序无响应或崩溃
典型原因是跨线程操作 UI。SampleGrabber 回调、OpenCV 工作线程都不在主 UI 线程,直接调SetWindowText、Invalidate、GetDC会触发 MFC 的线程断言或随机崩溃。解决:所有 UI 更新走PostMessage自定义消息,主窗口的消息处理函数里再操作控件。另一个原因是cv::Mat的浅拷贝——m_frame = tmp只拷贝头,数据共享,工作线程下一帧写入时主线程可能正在读,必须用copyTo做深拷贝,并用互斥量保护。
4.4 现象:预览有画面但延迟越来越大
这是缓冲区堆积。OpenCV 的VideoCapture内部有缓冲,如果抓帧速度大于处理速度,旧帧会排队。解决:在采集线程里连续grab()丢弃旧帧,只retrieve()最新一帧;或者把CAP_PROP_BUFFERSIZE设成 1(部分后端支持)。DirectShow 路线里SetBufferSamples(FALSE)已经避免了内部缓存,但如果你在回调里做了耗时操作(比如存盘),同样会堵住采集线程,应该把耗时逻辑丢到另一个队列线程。
4.5 现象:换一台电脑就找不到摄像头或分辨率不对
设备索引0不保证是同一个摄像头,多摄像头机器上顺序可能变。解决:不要硬编码索引,枚举设备名称让用户选,或者按名称匹配。分辨率方面,CAP_PROP_FRAME_WIDTH设了 1920 但实际出 640,说明设备不支持该组合,需要枚举设备支持的格式再设。DirectShow 里通过IAMStreamConfig的GetStreamCaps枚举所有支持的宽高和帧率,选最接近的。这个枚举过程代码不短,但它是工控项目里必须做的,否则现场换摄像头就翻车。
5. 进阶:把预览做成可用的采集模块
5.1 用状态栏和帧计数验证采集是否真的在跑
调试阶段最有用的一招是把实时帧率、分辨率、丢帧数显示在状态栏。在CMainFrame里创建三个窗格,采集线程每秒通过PostMessage上报一次统计。判断采集是否健康的标准不是「有画面」,而是「帧率稳定且丢帧数为零」。如果帧率在 30 和 15 之间跳,说明设备在两种格式间协商,需要固定格式。
// 采集线程里每秒统计一次 auto now = std::chrono::steady_clock::now(); m_nFrameCount++; if (now - m_lastStat >= std::chrono::seconds(1)) { int fps = m_nFrameCount; m_nFrameCount = 0; m_lastStat = now; // 自定义消息 WM_APP_CAM_STAT,wParam 传 fps ::PostMessage(m_hMainWnd, WM_APP_CAM_STAT, fps, 0); }主窗口收到消息后调m_wndStatusBar.SetPaneText(1, strFps)。这样你不用盯着画面猜,数字会告诉你真相。
5.2 从预览到抓拍:一帧数据的完整生命周期
预览跑通后,下一步通常是抓拍或录像。抓拍的关键是拿到一帧完整、未被后续帧覆盖的数据。在 OpenCV 路线里,GetFrame返回的cv::Mat已经是深拷贝,直接cv::imwrite即可。DirectShow 路线里,回调中memcpy出来的缓冲区就是完整帧,但要注意GetActualDataLength()返回的字节数可能小于biSizeImage,写文件时按实际长度写。录像则建议用cv::VideoWriter,编码器选MJPG或XVID,前者文件大但兼容好,后者体积小但需要对应解码器。帧率参数必须和采集帧率一致,否则回放速度不对。
5.3 我踩过的最深的一个坑
早期做的一个项目里,预览在开发机上一切正常,到现场跑了半天后画面卡死。查了很久才发现是采集线程里调用了AfxMessageBox弹错误提示——那个调用阻塞了采集线程,而主线程在等采集线程的锁,死锁。从那以后我给自己定了个习惯:采集线程里只做三件事——取数据、拷贝、发消息,任何可能阻塞或弹窗的操作一律禁止。这个习惯后来救了我很多次。希望帮到你。
本文还有配套的精品资源,点击获取