简介:一套基于Qt框架调用摄像头的入门工程示例,面向初学Qt多媒体模块的开发者,演示如何通过QCamera选择设备、初始化预览、并在QLabel中实时显示捕获画面。工程共5个文件,包含两个cpp源代码(主要逻辑与入口)、一个h头文件、一个ui界面设计文件及一个pro工程配置,压缩包仅2KB,代码量精简,但基础模块完整,便于快速读懂核心流程。项目源码中实现了摄像头状态监听、从取景器抓取帧并缩放显示到QLabel,同时给出析构时停止设备的管理方式,可直接在Qt环境中打开pro文件运行验证。已有5606人学习积累,适合想要快速理解QCamera与QLabel配合用法、体验摄像头调用全流程的开发者参考,也可作为后续实现视频录制或图像处理功能的起点。 做 Qt 开发久了,几乎每个人都会碰到一个需求:调摄像头。我最早是在一个设备巡检项目里用 QCamera 做扫码和拍照,后来又在人脸识别门禁端上用过它。QCamera 是 Qt Multimedia 模块里专门负责摄像头采集的类,配合 QCameraInfo、QCameraImageCapture、QMediaRecorder 这些兄弟类,可以实现设备枚举、实时预览、拍照、录像、参数设置等一整套功能。这篇文章我会把 Qt 5 / Qt 6 两套写法都说清楚,再把踩过的坑整理出来,给正准备接摄像头的同学一份可以直接抄的作业。
1. 环境准备:先把多媒体模块跑起来
1.1 工程配置里的三个关键点
QCamera 不是 Qt 核心模块,使用前必须把 multimedia 模块加进工程。这里有个容易搞混的点:Qt 5 和 Qt 6 的模块划分不一样,配置代码也不同。
Qt 5 的 .pro 文件里这样写:
QT += core gui multimedia multimediawidgetsQt 5 时代摄像头画面显示依赖QCameraViewfinder,这个类在multimediawidgets模块里,所以如果你只加multimedia,编译时会出现"找不到 QCameraViewfinder"这种莫名其妙的报错,实际原因就是模块没加全。
到了 Qt 6,multimediawidgets 被合并进了 multimedia 模块,所以只需要:
QT += core gui multimedia如果你用的是 CMake,则是这样:
find_package(Qt6 REQUIRED COMPONENTS Core Gui Widgets Multimedia) target_link_libraries(myapp PRIVATE Qt6::Core Qt6::Gui Qt6::Widgets Qt6::Multimedia)这里我再强调一个跟工程配置相关、但是非常容易踩的运行时坑:编译过了不等于能跑起来。不少人的程序在开发机上好好的,拷贝到别的电脑上运行就报windows no qt platform plugin could be initialized,在调用摄像头的程序里尤其常见。原因是发布时没有把 Qt 的 platform 插件(platforms/qwindows.dll)和 multimedia 相关的插件目录(multimedia目录下的 ffmpeg、windowsmedia 等插件)一起带上。我的建议是打包时直接用windeployqt生成依赖目录,然后检查一下multimedia文件夹是否存在,缺哪个补哪个,不然换电脑就翻车。
1.2 摄像头权限:桌面端和移动端都得处理
很多新手第一反应是代码写错了,其实摄像头打不开,很大概率是操作系统权限没给。
- Windows 10/11:在"设置 -> 隐私和安全性 -> 相机"里,需要允许桌面应用访问相机。如果你是用调试器跑的,第一次弹的权限授权框没点"允许",后面调试就一直是拒绝状态,得手动去设置里改。
- Linux:当前用户必须在
video组里,否则无法访问/dev/video0。命令是sudo usermod -aG video $USER,加完组要注销重新登录才生效。 - macOS:需要在 Info.plist 里声明
NSCameraUsageDescription,否则程序一启动就会被系统强杀,崩溃日志都看不到。 - Android:要在 AndroidManifest.xml 里添加
CAMERA和RECORD_AUDIO权限,运行时还需要动态申请权限,这个不能漏。
我见过最典型的场景:QCamera 对象创建成功了,start()也调用了,但error()信号一直报错,state()永远停留在UnloadedState。查了半天下载记录,最后发现是 Windows 隐私设置里把摄像头访问关掉了。所以一旦遇到这种诡异现象,第一反应先查权限,第二反应再查代码。
2. 设备发现与选择:别把第一个摄像头写死
2.1 遍历摄像头列表
现实中很多项目都是"多个摄像头任选一个",比如课堂录播、产线检测。直接把设备名写死是最不靠谱的做法,因为 Windows 下同一个摄像头的设备名在不同机器上可能完全不一样,而且同一个 UVC 摄像头从不同 USB 口插入,枚举出来的描述也可能发生变化。
推荐用QCameraInfo(Qt 5)或者QMediaDevices(Qt 5.14+ 和 Qt 6 都有)遍历设备,然后填到下拉框里:
// Qt 5 写法 const QList<QCameraInfo> cameras = QCameraInfo::availableCameras(); for (const QCameraInfo &info : cameras) { ui->comboCamera->addItem(info.description(), info.deviceName()); }注意我用了addItem(text, userData)这种带数据的写法:界面上给用户看的是info.description()(通常是"USB2.0 Camera"这种友好名称),但真正传给 QCamera 构造函数的应该是info.deviceName()。很多新手直接把 description 传进去,结果摄像头初始化失败,还找不到原因。
Qt 6 推荐用QMediaDevices:
const QList<QVideoInputDevice> inputs = QMediaDevices::videoInputs(); for (const QVideoInputDevice &device : inputs) { ui->comboCamera->addItem(device.description(), device.id()); }device.id()在 Windows 下是类似\\?\usb#vid_xxxx&pid_xxxx的字符串,这个才是内部唯一标识。别去解析这个 ID 的格式,直接当不透明字符串用就行。
2.2 监听热插拔
USB 摄像头随时可能被拔掉、换口插,如果程序不感知,轻则画面卡死,重则崩溃。Qt 提供了现成的信号:
connect(QMediaDevices::instance(), &QMediaDevices::videoInputsChanged, this, &MainWindow::refreshCameraList); // 连接到槽函数 void MainWindow::refreshCameraList() { const QString previousDevice = ui->comboCamera->currentData().toString(); ui->comboCamera->clear(); const QList<QVideoInputDevice> inputs = QMediaDevices::videoInputs(); for (const QVideoInputDevice &device : inputs) { ui->comboCamera->addItem(device.description(), device.id()); } // 如果原来的设备还在,就继续选中它 const int index = ui->comboCamera->findData(previousDevice); if (index != -1) { ui->comboCamera->setCurrentIndex(index); } }这里有个细节要记住:设备拔掉后,videoInputsChanged信号触发时,旧的摄像机对象已经失效。在刷新列表之前,必须先停掉正在使用的 QCamera 对象并释放,再去枚举设备列表,不然可能崩溃。设备重插回来之后,摄像头所需的状态要全部重新初始化,不要复用之前已经 stop 过的 QCamera 对象,我建议直接 delete 后 new 一个新的,省心很多。
3. 实时预览:QCameraViewfinder 与 QMediaCaptureSession
3.1 Qt 5:QCameraViewfinder 一行代码挂画面
如果你是接手的老项目还在用 Qt 5,画面显示的代码非常简单。先在 .ui 文件里拖一个QCameraViewfinder控件,然后:
m_camera = new QCamera(deviceId); m_camera->setViewfinder(ui->viewfinder); m_camera->start();三步就出画面。QCameraViewfinder本质上是一个 QVideoWidget 的派生控件,可以直接嵌入到界面布局里,甚至支持叠加绘图。
但这套 API 有一个明显的局限:逻辑全都绑死在控件上。如果你想同时把画面给到两个窗口,或者想对帧数据做图像处理,就得另想办法,比如拿到 viewfinder 控件后通过 QPainter 去截图,比较别扭。
3.2 Qt 6:QMediaCaptureSession + QVideoWidget
Qt 6 做了比较大的 API 重构,推荐用法是把相机和输出对象分开,通过QMediaCaptureSession来连接:
// 头文件成员 QCamera *m_camera = nullptr; QMediaCaptureSession *m_captureSession = nullptr; // 初始化 m_captureSession = new QMediaCaptureSession(this); m_camera = new QCamera(QMediaDevices::defaultVideoInput(), this); m_captureSession->setCamera(m_camera); m_captureSession->setVideoOutput(ui->videoWidget); // QVideoWidget m_camera->start();QMediaCaptureSession在这里扮演的是一个"连接器"角色:左边接摄像头,右边接显示控件、拍照器、录像器。这样设计的好处是解耦——画面显示归显示,拍照归拍照,录像归录像,互不干扰。以后想加一个滤镜或者二次采集,只需要在 session 上多挂一个QVideoSink就行,代码结构清晰很多。
我建议新项目直接用 Qt 6 的这套写法,哪怕你暂时还在 Qt 5 上开发,也可以用QMediaCaptureSession(Qt 5.15 往后已经提供),提前适配,后面升级 amd64 或者用新版编译环境时少改一遍。
3.3 预览对象的管理顺序
这是个大坑,值得单独说。QCamera 对象和显示控件之间一定不要搞错析构顺序:先停止摄像头,再销毁采集会话,最后释放摄像头对象。反过来就会出现奇怪的崩溃或者下一个摄像头打开失败。
反面教材是这样的:
// 错误示例:直接 delete QCamera,没有 stop delete m_camera; // 可能崩溃,或者设备一直被占用正确做法:
m_camera->stop(); // 1. 停止采集 delete m_captureSession; // 2. 断开所有关联 delete m_camera; // 3. 释放相机对象这里面的原理是:QCamera 持有底层视频采集设备句柄,如果你不主动 stop,设备和驱动之间没有正常握手关闭,会导致两种后果:一种是应用直接崩溃,另一种是这个摄像头被"占住"了,别的程序(包括你下次启动的程序)打开它就一直黑屏。
4. 拍照与录像:基础功能的完整实现
4.1 拍照:QCameraImageCapture / QImageCapture
拍照有两种常用方式:一种是直接保存到文件,另一种是先拿到 QImage 做处理再保存。往往实际项目中两种都要,比如拍完要立刻在界面上回显,同时备份原图。
Qt 5 的QCameraImageCapture可以同时满足这两个需求。它有三个信号:imageCaptured在内存里拿到图像时触发,imageSaved在文件写入完成后触发,还有一个readyForCaptureChanged告诉你现在能不能拍照。
m_imageCapture = new QCameraImageCapture(m_camera); m_imageCapture->setCaptureDestination(QCameraImageCapture::CaptureToFile); connect(m_imageCapture, &QCameraImageCapture::imageCaptured, this, [=](int id, const QImage &preview) { // 这里的 preview 是未经压缩的原始帧,可以直接显示 ui->labelPreview->setPixmap(QPixmap::fromImage(preview)); }); connect(m_imageCapture, &QCameraImageCapture::imageSaved, this, [=](int id, const QString &fileName) { qDebug() << "saved:" << fileName; });拍照的触发代码要注意加锁和解锁:
m_camera->searchAndLock(); m_imageCapture->capture(savePath); m_camera->unlock();searchAndLock是干什么的呢?它会把摄像头暂时锁住,让对焦和曝光稳定后再执行拍照,避免"快门按下的一瞬间取到的画面是糊的"。如果拍摄场景光线稳定、摄像头是定焦的,不锁问题也不大,但在闸机、扫码这类对清晰度要求高的场景,加上这一句明显能降低废片率。
Qt 6 中对应的是QImageCapture:
m_imageCapture = new QImageCapture(m_captureSession); m_captureSession->setImageCapture(m_imageCapture); connect(m_imageCapture, &QImageCapture::imageCaptured, this, [=](int id, const QImage &preview) { ui->labelPreview->setPixmap(QPixmap::fromImage(preview)); }); m_imageCapture->captureToFile(savePath);接口名字略有不同,但整体思路一致。
4.2 录像:QMediaRecorder
录像是另一个高频需求,比如做一个简易监控或者课堂录播。Qt 5 的写法是直接给 QCamera 挂一个 QMediaRecorder:
m_recorder = new QMediaRecorder(m_camera); m_recorder->setOutputLocation(QUrl::fromLocalFile(QString("D:/rec/%1.mp4").arg(timestamp))); m_recorder->record();停止录像就调用m_recorder->stop()。
Qt 6 里,录音和录像统一走 session:
m_recorder = new QMediaRecorder(m_captureSession); m_captureSession->setRecorder(m_recorder); // 如果想录出声音,还需要配一个音频输入 m_audioInput = new QAudioInput(m_captureSession); m_captureSession->setAudioInput(m_audioInput); m_recorder->setMediaFormat(QMediaFormat::MPEG4); m_recorder->setQuality(QMediaRecorder::HighQuality); m_recorder->setOutputLocation(QUrl::fromLocalFile("D:/rec/output.mp4")); m_recorder->record();这里有个容易被忽略的坑:默认情况下 QMediaRecorder 只录视频,不录声音。桌面端至少有麦克风的情况下,你需要显式地把音频输入设置上去。如果摄像头自带麦克风(很多 USB 摄像头有 mic),要注意 QAudioInput 默认选中的是系统默认麦克风,不一定是摄像头自带的那个,如果发现录出来没声音,大概率是音频输入设备选错了。
再有就是录像文件的扩展名要和容器格式匹配。Qt 6 里设置了MPEG4容器,但保存路径却写一个.avi,录制会报错或者生成无法播放的文件。建议用.mp4和 MPEG4 容器配对,兼容性最好。
4.3 参数设置:分辨率、帧率、对焦
摄像头默认参数往往不适合你的场景。比如默认 640x480@30fps,你可能需要 1920x1080@25fps。通过QCameraViewfinderSettings设置:
QCameraViewfinderSettings settings; settings.setResolution(1920, 1080); settings.setMinimumFrameRate(15.0); settings.setMaximumFrameRate(30.0); if (m_camera->isViewfinderSettingsSupported(settings)) { m_camera->setViewfinderSettings(settings); } else { qWarning() << "当前摄像头不支持该分辨率/帧率组合"; }isViewfinderSettingsSupported这一步一定要做。有的廉价 USB 摄像头标称支持 1080p,但实际输出的只有 30fps 的 MJPEG,这时候如果直接设置不成文的分辨率,摄像头会回退到最低配置甚至黑屏。更好的做法是先读取m_camera->supportedViewfinderSettings(),看看这个设备到底支持哪些参数,再从中选一个和目标最接近的。这个逻辑跟买鞋一样:先看货架上有哪些尺码,再挑最合脚的,而不是硬塞。
对焦方面,QCameraFocus类提供了对焦模式设置:
QCameraFocus *focus = m_camera->focus(); focus->setFocusMode(QCameraFocus::ContinuousFocus);连续对焦适合扫码、读证件这类需要随时变焦的场景;固定焦点更适合闸机、人脸打卡这种人脸位置基本固定的场景,反而能避免对焦来回拉风箱导致画面忽清晰忽模糊。
5. 常见问题与排查技巧实录
我在不同版本、不同平台的项目里调摄像头,踩过的坑少说也有两位数,这里整理成一份速查表,按优先级排:
| 现象 | 可能原因 | 排查与解决办法 |
|---|---|---|
| 摄像头对象创建成功,但 start() 之后一直没画面 | 系统权限未开启、设备被其他程序独占 | 先查系统隐私权限;关掉可能占用摄像头的软件;Linux 查 video 组权限 |
启动即崩溃,报platform plugin错误 | 发布目录缺少 Qt 插件 | 用 windeployqt 重新部署,确认 platforms 和 multimedia 目录被拷贝 |
| 画面全黑,但程序不报错 | 分辨率设置不被支持、摄像头初始化立即取帧 | 删除自定义分辨率设置;等待 1~2 秒再操作 |
| 热插拔 USB 摄像头后程序崩溃 | 设备失效后未及时停止 QCamera | 监听videoInputsChanged,先 stop 再 delete |
| 拍照保存的图片是黑的或花屏 | 在摄像头未就绪时调用了 capture | 等state()变成ActiveState后再允许拍照 |
| 录像文件无法播放 | 容器格式与扩展名不匹配、缺少音频流 | 统一用 MPEG4 + .mp4;按需设置音频输入 |
| Linux 下能枚举到设备但打开失败 | 权限或设备节点被占用 | ls -l /dev/video*查看属主,确认在 video 组;检查是否有 ffmpeg 等进程占用 |
5.1 摄像头打不开,error() 报什么
QCamera 有一个error(QCamera::Error)信号,很多新手根本没连这个信号。建议一开始就把错误信号全部打出来:
connect(m_camera, &QCamera::errorOccurred, this, [=](QCamera::Error error) { qWarning() << "QCamera error:" << error << m_camera->errorString(); });Qt 6 里统一为errorOccurred信号,Qt 5 里老接口是error(QCamera::Error),新代码建议用errorOccurred以兼容较新版本。
errorString()返回的描述有时候比较抽象,但至少能帮你区分是"设备被占用"还是"权限不足"。如果显示的是Resource unavailable或者Device not found,重点排查设备是否被其他程序占用,比如浏览器、微信、Zoom 都可能霸占摄像头,尤其是调试的时候最容易撞车。
5.2 画面全黑
黑屏问题我遇到得最多,原因也五花八门。最常见的两个:
一是摄像头没有完全启动就尝试显示。QCamera 的start()是非阻塞的,调用后底层驱动还在初始化,马上设置 viewfinder 或者马上截图,大概率拿到一帧黑图。我的经验是不要在start()后立刻操作画面,而是通过stateChanged信号判断状态为ActiveState后再执行想做的事。或者简单粗暴一点,start()之后加一个QTimer::singleShot(1000, ...),等待一秒再操作,在实际项目里效果也很好。
二是和分辨率设置冲突。有的摄像头在 UVC 协议下支持的格式列表很奇怪,比如 1280x720@30fps 只支持 YUYV,不支持 MJPEG,如果你用默认设置,它可能给你一个不支持的组合,结果就是黑屏。处理方法是先获取supportedViewfinderSettings(),打印出所有支持的格式,看清楚再设置。调试信息不要直接在 release 里打太多,但开发期一定要留全。
5.3 热插拔后崩溃
这个问题在 Qt 5 里特别常见,Qt 6 修复了一部分,但也不能完全依赖。核心原因是:USB 摄像头拔掉后,底层设备文件消失,但 QCamera 对象还持有旧设备句柄,此时任何操作都会导致未定义行为。
我的处理方案是,在videoInputsChanged信号中不要只刷新下拉框,还要主动切换状态:
void MainWindow::handleVideoInputsChanged() { m_camera->stop(); delete m_imageCapture; delete m_recorder; delete m_camera; m_camera = nullptr; // 重新枚举设备列表 refreshCameraList(); // 重新选择当前设备并启动 startCurrentCamera(); }注意这里一定要彻底重建对象,只做 stop() 然后复用旧对象是不够的,因为底层驱动状态已经不干净了。重新 new 一个 QCamera 的成本极低,却能把这类崩溃问题从根源上避免。
5.4 运行时找不到平台插件
文章开头提过no Qt platform plugin could be initialized,这里展开说一下怎么排查。这个报错出现的原因通常是两个:
- 可执行文件目录下缺少
platforms/qwindows.dll(Windows 下); platforms目录存在,但插件版本和主程序版本不匹配,比如程序是 Qt 5.15.2 编译的,拷过去的插件却是 Qt 6.x 的。
调用摄像头的程序还会多发一种情况:缺少multimedia插件目录下的 dll。Qt 5 的 multimedia 插件包括mediaservice下的dsengine.dll、wmfengine.dll,Qt 6 则是 plugins 下的multimedia/ffmpegmediaplugin.dll、windowsmediaplugin.dll等。用windeployqt部署时一般是齐全的,但如果你手动裁剪过 dll,往往就漏在这。我的习惯是发布前在干净的虚拟机或普通电脑上跑一遍,遇到缺 dll 就补,比在开发机上自我感觉良好有用得多。
5.5 有些摄像头枚举后出现重复项
虚拟摄像头软件(比如 OBS Virtual Camera、安卓模拟器的虚拟摄像头)会出现在枚举列表里,这本身不是 bug,但会影响用户体验。处理办法是屏蔽名称中包含特定关键字的设备,或者读取设备描述后做一次过滤。这个逻辑放在refreshCameraList之前执行就行。
6. 进阶经验:稳定采集的几条细节
6.1 等待首帧的耐心
摄像头从 start() 到输出第一帧画面,中间经历枚举、驱动握手、格式协商、缓冲区初始化等过程,实际耗时通常需要几百毫秒,某些低端 USB 摄像头甚至要 2~3 秒。如果你在 start() 之后立刻轮询 QVideoSink 的帧信号,大概率什么都拿不到。
我的做法是维护一个m_ready标志位,在拿到第一帧后再置位,同时将按钮的 enabled 状态与它绑定。这样既能避免用户在初始化期间乱点拍照导致崩溃,又让首帧等待过程对用户透明,体验会比甩一个 "加载中" 要自然。
6.2 设备释放顺序
再啰嗦一遍释放顺序,因为我在这个上面浪费过整整半天:先停摄像头,再断 session,再删对象。如果你用的是栈对象或者智能指针,务必要保证析构顺序,比如在 MainWindow 的成员变量中,m_captureSession和m_camera的声明顺序也很重要,因为成员变量的析构顺序是声明顺序的逆序。我建议把m_camera声明在m_captureSession之前,这样析构时会先析构 m_captureSession,再析构 m_camera,正好符合期望。
// 头文件成员声明顺序 QCamera *m_camera = nullptr; // 先声明 QMediaCaptureSession *m_captureSession = nullptr; // 后声明如果反过来,程序退出时可能偶尔崩一次,而且只在特定机器上崩,排查起来相当痛苦。
6.3 从本地摄像头延伸到网络摄像头
很多读者做到这一步,接下来就会问:能不能直接调海康、大华的网络摄像头,或者把 OV5647(树莓派摄像头)、ESP32-S3 USB 摄像头也接入进来?
先说结论:QCamera 只适合本地摄像头设备,网络摄像头走的是 RTSP/ONVIF 协议,需要另找方案。但如果是树莓派摄像头模块这类设备,在 Linux 下通过 V4L2 驱动后,QCamera 是可以直接枚举到的,只是设备名可能是unicam或者platform前缀加一串数字,需要在枚举列表里手动映射一个友好名称给用户。ESP32-S3 这类 USB 摄像头同理,只要系统把它识别为标准 UVC 设备,QCamera 这套 API 就能用。
我个人的体会是,QCamera 的价值在于它把 USB 摄像头、内置摄像头这类本地设备的接入门槛降到了很低的位置,让开发者把精力集中在业务逻辑上。但它的坑也集中在"设备状态、插件部署、权限"这些基础层面,所以每次在新电脑上部署项目时,我都会按"先查权限 -> 再查插件 -> 最后查代码"的顺序排查,这个流程帮我省下了大量时间。希望这份经验对你也有用。
本文还有配套的精品资源,点击获取