Qt安卓开发(二)摄像头打开

前文

上一节已经部署好安卓环境,并成功在手机上运行一个简单的点击计数器功能。本节我想要尝试打开摄像头画面,并实现前置后置摄像头的切换。值得一提的是,由于安卓对于硬件权限有动态申请的要求,所以即便是简单的摄像头画面显示,也显得稍显费力。不过经过一番测试下来,在较新版本(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官方对摄像头这款有不少优化,虽然也带来了新的学习成本,但也让我们开发者能够实现更多样的设备功能。

如果还有下一节的话,我想尝试一下安卓端摄像头的分辨率切换、手动对焦,以及拍照保存。安卓环境下的文件系统我也挺感兴趣的。再然后就是网络通信的部分也想尝试......继续加油吧!!

相关推荐
Sammyyyyy2 小时前
PHP 8.6 新特性预览,更简洁的语法与更严谨的类型控制
android·php·android studio
撩得Android一次心动2 小时前
Android Lifecycle 全面解析:掌握生命周期管理的艺术(1)
android·java·kotlin·lifecycle
AAA阿giao2 小时前
qoder-cli:下一代命令行 AI 编程代理——全面解析与深度实践指南
开发语言·前端·人工智能·ai编程·mcp·context7·qoder-cli
HalvmånEver2 小时前
Linux:深入剖析 System V IPC下(进程间通信九)
linux·运维·服务器·c++·system v·管道pipe
m0_748250032 小时前
C++ 修饰符类型
开发语言·c++
李日灐2 小时前
C++STL:仿函数、模板(进阶) 详解!!:“伪装术”和模板特化、偏特化的深度玩法指南
开发语言·c++·后端·stl
rgeshfgreh2 小时前
Python连接KingbaseES数据库全指南
开发语言·数据库·python
小北方城市网2 小时前
数据库性能优化实战指南:从索引到架构,根治性能瓶颈
数据结构·数据库·人工智能·性能优化·架构·哈希算法·散列表