前文
上一节已经部署好安卓环境,并成功在手机上运行一个简单的点击计数器功能。本节我想要尝试打开摄像头画面,并实现前置后置摄像头的切换。值得一提的是,由于安卓对于硬件权限有动态申请的要求,所以即便是简单的摄像头画面显示,也显得稍显费力。不过经过一番测试下来,在较新版本(qt6.5+,我的是6.10),对于硬件权限这块已经适配得比较好了,且许我在之后慢慢介绍。
不过在此之前,我认为需要先对上一节的工程做一些基础的优化。
一、工程搭建
qt6默认是cmake构建,但我还是切换到自己习惯的qmake,也就是pro文件。
此外,我将不同的功能用不同的qml文件分隔开来,避免代码杂糅。
代码结构如下:
cpp
your_project/
├── main.qml ← 主入口(含 StackView)
├── CameraPage.qml ← 摄像头页面
└── CounterPage.qml ← 计数器页面
man.qml的代码如下:
cpp
// main.qml
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
ApplicationWindow {
id: window
width: 640
height: 480
visible: true
title: qsTr("功能选择")
StackView {
id: stackView
anchors.fill: parent
initialItem: mainPage
}
Component {
id: mainPage
// 用一个全屏 Item 作为根,再把内容居中
Item {
// 这个 Item 自动填满 StackView 页面区域
anchors.fill: parent
Column {
anchors.centerIn: parent // 相对于外层 Item 居中
spacing: 30
width: 280 // 按钮区域固定宽度,美观对齐
Text {
text: "Qt安卓测试程序"
font.pixelSize: 28
horizontalAlignment: Text.AlignHCenter
width: parent.width
color: "#333"
}
Button {
text: "摄像头演示"
font.pixelSize: 24
width: parent.width
onClicked: stackView.push(cameraPage)
}
Button {
text: "计数器演示"
font.pixelSize: 24
width: parent.width
onClicked: stackView.push(counterPage)
}
Button {
text: "退出程序"
font.pixelSize: 24
width: parent.width
onClicked: Qt.quit()
}
}
}
}
Component { id: cameraPage; CameraPage {} }
Component { id: counterPage; CounterPage {} }
}
结构比较简单,就不多赘述了。
效果图如下:

另外,优化过后的点击计数器代码也附上:
cpp
// CounterPage.qml
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
Page {
id: counterPage
property int num: 0
title: qsTr("计数器")
ColumnLayout {
anchors.centerIn: parent
spacing: 30
Text {
text: "点击计数器"
font.pixelSize: 24
color: "blue"
Layout.alignment: Qt.AlignHCenter
}
Text {
text: num.toString()
font.pixelSize: 48
color: "red"
Layout.alignment: Qt.AlignHCenter
}
Button {
text: "点它!"
font.pixelSize: 24
Layout.preferredWidth: 200
onClicked: num++
}
Button {
text: "返回"
font.pixelSize: 20
Layout.preferredWidth: 200
onClicked: stackView.pop()
}
}
}
接下来,我们就重点关注摄像头的实现,也就是CameraPage.qml的代码。
二、QML中摄像头的基础实现
对于QML来说,从Qt5到Qt6的版本迭代中,摄像头的实现经历了比较大的变化,所以一定要搞清楚自己的当前版本,不是所有代码都适配自己的。我只针对自己的Qt6.10版本来介绍。
先介绍最简单的摄像头打开,代码也非常简洁:
cpp
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import QtMultimedia
Page {
id: cameraPage
title: qsTr("摄像头")
CaptureSession {
id: captureSession
camera: cameraId
videoOutput: videoOutput
}
Camera {
id: cameraId
}
ColumnLayout {
anchors.fill: parent
anchors.margins: 10
Item {
Layout.fillWidth: true
Layout.fillHeight: true
VideoOutput {
id: videoOutput
anchors.fill: parent
}
}
RowLayout {
Layout.fillWidth: true
Layout.preferredHeight: 70
Button {
text: "返回"
Layout.fillWidth: true
onClicked: stackView.pop()
}
}
}
Component.onCompleted: {
cameraId.start()
}
}
主要聚焦在这三个组件之上。
CaptureSession是一个采集项,管理一组摄像头的指定、渲染、采集、录制等行为。代码中主要给它设置了一个camera摄像头,一个videoOutput视频输出,也就是一个图像显示窗口。
当我们不做其他多余设置时,此时显示的就是默认摄像头的画面,比如笔记本电脑的前置摄像头。显示区域则默认全屏适应。
cpp
CaptureSession {
id: captureSession
camera: cameraId
videoOutput: videoOutput
}
Camera {
id: cameraId
}
VideoOutput {
id: videoOutput
anchors.fill: parent
}
Component.onCompleted: {
cameraId.start()
}
在电脑运行看看,基础效果如下:

注意,问题来了。我们想当然地将这个代码放到手机端去跑,结果画面并没有出来。
打印信息有些乱七八糟的,提取不出什么特别有用的信息。
不过我思路还算比较清晰,这肯定是安卓环境和Windows环境之间有啥不一样。
于是自然而然想到了,我们运行安卓程序时需要申请动态设备权限的问题。
就是那种当我需要用到系统相机时,会弹出一个提示框,询问是否授予权限,权限是运行时可用,还是本次可用,还是不可用。
于是我们接下来需要解决的问题就很明确了,我们需要让我们的程序具备动态申请相机权限的能力。
三、安卓权限文件
在正式编写相关代码之前,我想结合ai再介绍一下安卓环境下使用硬件的权限申请流程。
简单说,我们需要一个AndroidManifest.xml。但它到底是什么呢?
AndroidManifest.xml 是每一个 Android 应用的"身份证"和"说明书"。
它告诉 Android 系统:
这个 App 叫什么?
需要哪些权限?
支持哪些硬件?
从哪个界面启动?
最低能装在什么版本的手机上?
这是ai帮我写的一个xml:
cpp
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.example.myapp" android:installLocation="auto" android:versionCode="1" android:versionName="1.0">
<uses-sdk android:minSdkVersion="28" android:targetSdkVersion="33"/>
<uses-permission android:name="android.permission.CAMERA"/>
<uses-feature android:name="android.hardware.camera"/>
<uses-feature android:name="android.hardware.camera.autofocus"/>
<application android:name="org.qtproject.qt.android.bindings.QtApplication" android:label="QtCameraApp" android:theme="@android:style/Theme.Light.NoTitleBar">
<activity android:name="org.qtproject.qt.android.bindings.QtActivity" android:exported="true" android:configChanges="orientation|uiMode|screenLayout|screenSize|smallestScreenSize|layoutDirection|locale|fontScale|keyboard|keyboardHidden|navigation|mcc|mnc|density" android:screenOrientation="unspecified" android:launchMode="singleTop" android:theme="@android:style/Theme.Light.NoTitleBar">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
<meta-data android:name="android.app.lib_name" android:value="-- %%INSERT_APP_LIB_NAME%% --"/>
<meta-data android:name="android.app.extra_libs" android:value="-- %%INSERT_EXTRA_LIBS%% --"/>
<meta-data android:name="android.app.background_running" android:value="true"/>
</activity>
</application>
</manifest>
因为没有安卓基础,我们就不为难自己全部读懂了,重点看这句:
cpp
<uses-permission android:name="android.permission.CAMERA"/>
这就意味着,我们给这个程序赋予了基本的摄像头权限。
还没完,这意味着这个程序具备摄像头相应的功能,并不意味着他天然就已经拿到权限,畅通无阻了。
别忘了,我们还需要动态申请。
这就需要在代码中就具体实现了。
不过在此之前,再稍微扩展了解一下这个xml具备的功能:

除了我们关心的权限之外,它还决定了安卓运行版本,app名字和图标等。哎嘿,这好像和Windows中的RC_FILE = xxxx.rc'有点像,这玩意包含在pro中,也是决定了exe的名称和图标,以及公司名称等信息。
下一个问题,我们该怎么使用上这个文件呢?
ai的建议是,将它放在我们代码目录下的android\AndroidManifest.xml,然后在pro文件中添加:
cpp
android {
# 确保包含AndroidManifest.xml
ANDROID_PACKAGE_SOURCE_DIR = $$PWD/android
}
不过我后面发现,其实高版本qt(Qt6.5+)在编译时会自动给我们生成AndroidManifest.xml文件的,路径就在\build\Qt_6_10_0_for_Android_arm64_v8a-Debug\android-build\AndroidManifest.xml
它的内容也附上:
cpp
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="org.qtproject.example.TestProjectQMake" android:installLocation="auto" android:versionCode="1" android:versionName="1.0">
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
<uses-permission android:name="android.permission.BLUETOOTH"/>
<uses-permission android:name="android.permission.CAMERA"/>
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS"/>
<uses-permission android:name="android.permission.RECORD_AUDIO"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<supports-screens android:anyDensity="true" android:largeScreens="true" android:normalScreens="true" android:smallScreens="true"/>
<application android:name="org.qtproject.qt.android.bindings.QtApplication" android:hardwareAccelerated="true" android:label="TestProjectQMake" android:requestLegacyExternalStorage="true" android:allowBackup="true" android:fullBackupOnly="false">
<activity android:name="org.qtproject.qt.android.bindings.QtActivity" android:configChanges="orientation|uiMode|screenLayout|screenSize|smallestScreenSize|layoutDirection|locale|fontScale|keyboard|keyboardHidden|navigation|mcc|mnc|density" android:label="TestProjectQMake" android:launchMode="singleTop" android:screenOrientation="unspecified" android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
<meta-data android:name="android.app.lib_name" android:value="TestProjectQMake"/>
<meta-data android:name="android.app.arguments" android:value=""/>
</activity>
<provider android:name="androidx.core.content.FileProvider" android:authorities="${applicationId}.qtprovider" android:exported="false" android:grantUriPermissions="true">
<meta-data android:name="android.support.FILE_PROVIDER_PATHS" android:resource="@xml/qtprovider_paths"/>
</provider>
</application>
</manifest>
可以看到,它甚至比ai写的还要完善,不仅赋予了摄像头权限,还有蓝牙、网络等。
那我们还纠结啥,直接到下一步,把动态申请权限的代码补充完毕吧!
四、动态申请权限
因为贪图方便,直接在main中新增一个权限申请类,并通过上下文属性暴露给qml侧。
代码其实比较简单,主要用到一个叫QCameraPermission的摄像头权限类。
main.cpp的完整代码如下:
cpp
#include <QGuiApplication>
#include <QQmlApplicationEngine>
#include <QQmlContext>
#include <QTimer>
#include <QCoreApplication>
#include <QPermission>
class PermissionHelper : public QObject {
Q_OBJECT
public:
explicit PermissionHelper(QObject *parent = nullptr) : QObject(parent) {}
Q_INVOKABLE void requestCameraPermission() {
#ifdef Q_OS_ANDROID
QCameraPermission cameraPermission;
// ✅ 关键修正:使用 qApp 调用
auto status = qApp->checkPermission(cameraPermission);
if (status == Qt::PermissionStatus::Granted) {
qDebug() << "相机权限已授予";
emit permissionGranted();
return;
}
if (status == Qt::PermissionStatus::Denied) {
qDebug() << "相机权限被拒绝";
emit permissionDenied();
return;
}
qDebug() << "正在请求相机权限...";
// ✅ 关键修正:使用 qApp 调用
qApp->requestPermission(cameraPermission, this,
[this](const QPermission &permission) {
if (permission.status() == Qt::PermissionStatus::Granted) {
qDebug() << "权限请求成功";
emit permissionGranted();
} else {
qDebug() << "权限请求失败";
emit permissionDenied();
}
});
#else
qDebug() << "非 Android 平台,跳过权限申请";
emit permissionGranted();
#endif
}
signals:
void permissionGranted();
void permissionDenied();
};
int main(int argc, char *argv[])
{
QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling);
QGuiApplication app(argc, argv);
QQmlApplicationEngine engine;
PermissionHelper permissionHelper;
engine.rootContext()->setContextProperty("permissionHelper", &permissionHelper);
const QUrl url(QStringLiteral("qrc:/main.qml"));
QObject::connect(&engine, &QQmlApplicationEngine::objectCreated,
&app, [url](QObject *obj, const QUrl &objUrl) {
if (!obj && url == objUrl)
QCoreApplication::exit(-1);
}, Qt::QueuedConnection);
engine.load(url);
return app.exec();
}
#include "main.moc"
qml侧主要用到的就是PermissionHelper::requestCameraPermission接口,里面代码也比较简单易懂,不多赘述。当申请成功后,会通过信号发送给qml侧。qml侧只需要做好调用接口,连接信号槽出发摄像头选择和开启即可。
我的摄像头打开页是CameraPage.qml,完整代码如下:
cpp
// CameraPage.qml
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import QtMultimedia
Page {
id: cameraPage
property bool hasCameraPermission: false
property int currentCameraIndex: -1
title: qsTr("摄像头")
// 必须实例化才能激活设备枚举
MediaDevices {
id: mediaDevices
onVideoInputsChanged: {
if (currentCameraIndex === -1 && videoInputs.length > 0) {
selectDefaultCamera()
}
}
}
CaptureSession {
id: captureSession
camera: cameraId
videoOutput: videoOutput
}
Camera {
id: cameraId
focusMode: Camera.FocusModeAutoNear
onErrorOccurred: console.log("Camera error:", errorString)
}
ColumnLayout {
anchors.fill: parent
anchors.margins: 10
Item {
Layout.fillWidth: true
Layout.fillHeight: true
VideoOutput {
id: videoOutput
anchors.fill: parent
fillMode: VideoOutput.PreserveAspectCrop
visible: hasCameraPermission
}
Text {
anchors.centerIn: parent
visible: !hasCameraPermission
text: "等待相机权限..."
font.pixelSize: 24
color: "gray"
}
}
RowLayout {
Layout.fillWidth: true
Layout.preferredHeight: 70
Button {
text: cameraId.active ? "停止" : "启动"
enabled: hasCameraPermission
Layout.fillWidth: true
onClicked: cameraId.active ? cameraId.stop() : cameraId.start()
}
Button {
text: "切换摄像头"
enabled: hasCameraPermission && mediaDevices.videoInputs.length > 1
Layout.fillWidth: true
onClicked: switchCamera()
}
Button {
text: "返回"
Layout.fillWidth: true
onClicked: stackView.pop()
}
}
}
Connections {
target: permissionHelper
function onPermissionGranted() {
hasCameraPermission = true
startTimer.start()
}
function onPermissionDenied() {
hasCameraPermission = false
}
}
Timer {
id: startTimer
interval: 300
running: false
onTriggered: {
if (mediaDevices.videoInputs.length > 0) {
selectDefaultCamera()
cameraId.start()
}
}
}
Component.onCompleted: {
permissionHelper.requestCameraPermission()
}
// --- 摄像头逻辑 ---
function selectDefaultCamera() {
for (let i = 0; i < mediaDevices.videoInputs.length; i++) {
if (mediaDevices.videoInputs[i].position === CameraDevice.FrontFace) {
setCameraDevice(i)
return
}
}
if (mediaDevices.videoInputs.length > 0) {
setCameraDevice(0)
}
}
function switchCamera() {
if (mediaDevices.videoInputs.length <= 1) return
let next = (currentCameraIndex + 1) % mediaDevices.videoInputs.length
setCameraDevice(next)
if (cameraId.active) {
cameraId.stop(); cameraId.start()
}
}
function setCameraDevice(index) {
currentCameraIndex = index
cameraId.cameraDevice = mediaDevices.videoInputs[index]
}
}
代码有点多,先挑简单的解释。
当这个qml页完成初始化时,触发权限接口调用,这个很好理解吧?
cpp
Component.onCompleted: {
permissionHelper.requestCameraPermission()
}
权限申请成功后,就回到它的信号槽绑定位置:
cpp
Connections {
target: permissionHelper
function onPermissionGranted() {
hasCameraPermission = true
startTimer.start()
}
function onPermissionDenied() {
hasCameraPermission = false
}
}
接下来就是标志置位和定时器开启:
cpp
Timer {
id: startTimer
interval: 300
running: false
onTriggered: {
if (mediaDevices.videoInputs.length > 0) {
selectDefaultCamera()
cameraId.start()
}
}
}
定时器开启后,300ms短暂延时,然后触发摄像头的选择和开启。理论是这里直接开启摄像头,已经能够显示出默认摄像头的画面了。
那我们如何实现选择摄像头呢?或者换个说法,我们如何实现前后置摄像头的切换呢?
五、前后置摄像头切换
思路很简单,我们需要拿到摄像头列表,就像C++中的QCameraInfoList,然后实现自我逻辑的设备切换指定。
关键是这个列表如何获取。这个方法在不同版本qt亦有不同实现(真是要谢了。)
在我的版本中,需要利用MediaDevices类。
这是一个媒体设备类,包含视频和音频。这里我们主要用到它里面的videoInputs属性。
我踩过的一个坑就是,我以为直接单例MediaDevices这个类就能get到videoInputs,即:
cpp
if (MediaDevices.videoInputs.length > 0) { ... }
结果运行时居然报错了,说length未定义,我心想明明videoInputs就是list类型的,怎么会没有length呢?就连帮助文档里都有。
搞了半天原来自己搞笑了,MediaDevices必须是创建实例的时候,才会生成videoInputs,获取设备信息。这可能也是为了效率方面的考量吧。
于是我创建了对应实例:
cpp
// 必须实例化才能激活设备枚举
MediaDevices {
id: mediaDevices
onVideoInputsChanged: {
if (currentCameraIndex === -1 && videoInputs.length > 0) {
selectDefaultCamera()
}
}
}
后续直接拿着mediaDevices这个id进行操作。这里我还实现了设备改变时的槽函数,让发生设备热插拔的时候自动充值默认摄像头。不过安卓设备应该没有这个烦恼吧,Windows倒是蛮多的。
这些都理清了之后,接下来就只剩下纯逻辑的代码:
cpp
// --- 摄像头逻辑 ---
function selectDefaultCamera() {
for (let i = 0; i < mediaDevices.videoInputs.length; i++) {
if (mediaDevices.videoInputs[i].position === CameraDevice.FrontFace) {
setCameraDevice(i)
return
}
}
if (mediaDevices.videoInputs.length > 0) {
setCameraDevice(0)
}
}
function switchCamera() {
if (mediaDevices.videoInputs.length <= 1) return
let next = (currentCameraIndex + 1) % mediaDevices.videoInputs.length
setCameraDevice(next)
if (cameraId.active) {
cameraId.stop(); cameraId.start()
}
}
function setCameraDevice(index) {
currentCameraIndex = index
cameraId.cameraDevice = mediaDevices.videoInputs[index]
}
这三个分别是选择默认摄像头、切换摄像头、设置摄像头......老实说,ai写的,但亲测前后置切换起来也没有问题。
最后在讲一下如何区分前后置摄像头的。
在安卓中,CameraDevice有对应的标志位:

所以我们的思路就是对比该设备的position是否和这两个值对应上,如果是后置摄像头,就对比QCameraDevice::FrontFace。
事实上代码也是这样写的,当然qml类和c++类名称有些区别:
cpp
if (mediaDevices.videoInputs[i].position === CameraDevice.FrontFace) { ... }
最后附上效果图:


六、总结
ε=(´ο`*)))唉,总算结束了,我其实也没想到自己真的能搞懂安卓下的相机显示,因为确实是有些复杂和难度的。但从结果上看,个人还是很满意的。
这样一个测试程序,也足以作为基底,去开发更多不一样的丰富的功能了。
不过我还是有一个体会想讲,就是早在用qt5来学习qml的时候我就发现了,在那上面开发摄像头实属困难,就连分辨率列表和设置都那么麻烦,一些本应该有的接口在qml中居然没有实现。这让在C++端有过摄像头经验的我实在头疼。
而转到Qt6后,这次的摄像头开发明显感觉Qt官方对摄像头这款有不少优化,虽然也带来了新的学习成本,但也让我们开发者能够实现更多样的设备功能。
如果还有下一节的话,我想尝试一下安卓端摄像头的分辨率切换、手动对焦,以及拍照保存。安卓环境下的文件系统我也挺感兴趣的。再然后就是网络通信的部分也想尝试......继续加油吧!!