4

Qt安卓开发经验技巧总结V202308 - 飞扬青云

 8 months ago
source link: https://www.cnblogs.com/feiyangqingyun/p/17643802.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

01:01-05

  1. pro中引入安卓拓展模块 QT += androidextras 。
  2. pro中指定安卓打包目录 ANDROID_PACKAGE_SOURCE_DIR = $$PWD/android 指定引入安卓特定目录比如程序图标、变量、颜色、java代码文件、jar库文件等。
  • AndroidManifest.xml 每个程序唯一的一个全局配置文件,里面xml格式的数据,标明支持的安卓版本、图标位置、横屏竖屏、权限等。这个文件是最关键的,如果没有这个文件则Qt会默认生成一个。
  • android/res/drawable-hdpi drawable-xxxhdpi 等目录下存放的是应用程序图标。
  • android/res/layout 目录下存放的布局文件。
  • android/res/values/libs.xml 存储的一些变量值。
  • android/libs 目录下存放的jar库文件。
  • android/src 目录下存放的java代码文件,可以是根据包名建立的一层层子目录,也可以直接在src目录下。
  • 其他目录自行搜索安卓目录规范。
  • 后面的说明统一用的android目录举例,其实你可以改成任意目录,比如你的代码目录下是xxoo存放的安卓相关的打包文件,你就写成 ANDROID_PACKAGE_SOURCE_DIR = $$PWD/xxoo 。
  1. java类名必须和文件名完全一致,区分大小写。
  2. java类必须在android/src目录下不然不会打包到apk文件,可以是子目录比如 android/src/com/qt 。
  3. Qt代码中的QAndroidJniObject指定传入的java包名,必须严格和java文件package完全一致,不然程序执行到此处会因为找不到而崩溃。
  • android/scr/MainActivity.java 顶部 没有 package 则代码中必须是 QAndroidJniObject javaClass("MainActivity");
  • android/scr/MainActivity.java 顶部 package com.qandroid; 则代码中必须是 QAndroidJniObject javaClass("com/qandroid/MainActivity");
  • android/scr/com/example/MainActivity.java 顶部 package com.qandroid; 则代码中必须是 QAndroidJniObject javaClass("com/qandroid/MainActivity");
  • android/scr/com/example/MainActivity.java 顶部 package com.example.qandroid; 则代码中必须是 QAndroidJniObject javaClass("com/qandroid/example/MainActivity");
  • 总之这个包名是和代码中的package后面一段吻合,而不是目录路径。为了统一管理方便查找文件,建议包名和目录路径一致。

02:06-10

  1. Qt只能干Qt内部类的事情,做一些简单的UI交互还是非常方便,如果涉及到底层操作,还是需要熟悉java会如虎添翼,一般的做法就是写好java文件调试好,提供静态方法给Qt调用,这样通过QAndroidJniObject这个万能胶水可以做到Qt程序调用java中的函数并拿到执行结果,也可以接收java中的函数。
  2. pro中通过 OTHER_FILES += android/AndroidManifest.xml OTHER_FILES += android/src/JniMessenger.java 引入文件其实对整个程序的编译打包没有任何影响,就是为了方便在QtCreator中查看和编辑。
  3. 在Qt中与安卓的java文件交互都是用万能的QAndroidJniObject,可以执行java类中的普通函数、静态函数,可以传类对象jclass、类名className、方法methodName、参数,也可以拿到执行结果返回值。 (I)V括号中的是参数类型,括号后面的是返回值类型,void返回值对应V,由于String在java中不是数据类型而是类,所以要用Ljava/lang/String;表示,其他类作为参数也是这样处理。
  • 调用实例方法:callMethod、callObjectMethod。
  • 调用静态方法:callStaticMethod、callStaticObjectMethod。
  • 不带Object的函数名用来执行无返回值或者常规返回值int、float等的方法。
  • 如果返回值是String或者类则需要用带Object的函数名来执行,返回QAndroidJniObject类型再转换处理拿到结果,比如toString拿到字符串。
  1. 各种参数和返回值示例。
package org.qt;
import org.qt.QtAndroidData;

public class QtAndroidTest
{
    //需要通过实例来调用 测试发现不论 private public 或者不写都可以调用 我擦
    private void printText()
    {
        System.out.println("printText");
    }

    public static void printMsg()
    {
        System.out.println("printMsg");
    }

    public static void printValue(int value)
    {
        System.out.println("printValue:" + value);
    }

    public static void setValue(float value1, double value2, char value3)
    {
        System.out.println("value1:" + value1 + " value2:" + value2 + " value3:" + value3);
    }

    public static int getValue()
    {
        return 65536;
    }

    public static int getValue(int value)
    {
        return value + 1;
    }

    public static void setMsg(String message)
    {
        System.out.println("setMsg:" + message);
    }

    public static String getMsg()
    {
        return "hello from java";
    }

    public static void setText(int value1, float value2, boolean value3, String message)
    {
        System.out.println("value1:" + value1 + " value2:" + value2 + " value3:" + value3 + " message:" + message);
    }

    public static String getText(int value1, float value2, boolean value3, String message)
    {
        //同时演示触发静态函数发给Qt
        QtAndroidData.receiveData("message", "你好啊 java");

        //下面两种办法都可以拼字符串
        return "value1:" + value1 + " value2:" + value2 + " value3:" + value3 + " message:" + message;
        //return "value1:" + String.valueOf(value1) + " value2:" + String.valueOf(value2) + " value3:" + String.valueOf(value3) + " message:" + message;
    }
}
#include "androidtest.h"

//java类对应的包名+类名
#define className "org/qt/QtAndroidTest"

void AndroidTest::test()
{
    jint a = 12;
    jint b = 4;
    //可以直接调用java内置类中的方法
    jint max = QAndroidJniObject::callStaticMethod<jint>("java/lang/Math", "max", "(II)I", a, b);

    //jclass javaMathClass = "java/lang/Math";
    jdouble value = QAndroidJniObject::callStaticMethod<jdouble>("java/lang/Math", "random");

    qDebug() << "111" << max << value;
}

void AndroidTest::printText()
{
    QAndroidJniEnvironment env;
    jclass clazz = env.findClass(className);
    QAndroidJniObject obj(clazz);
    obj.callMethod<void>("printText");
}

void AndroidTest::printMsg()
{
#if 0
    //查看源码得知不传入jclass类的函数中内部会自动根据类名查找jclass
    QAndroidJniEnvironment env;
    jclass clazz = env.findClass(className);
    QAndroidJniObject::callStaticMethod<void>(clazz, "printMsg");
#else
    //没有参数和返回值可以忽略第三个参数
    QAndroidJniObject::callStaticMethod<void>(className, "printMsg");
    //QAndroidJniObject::callStaticMethod<void>(classNameTest, "printMsg", "()V");
#endif
}

void AndroidTest::printValue(int value)
{
    QAndroidJniObject::callStaticMethod<jint>(className, "printValue", "(I)I", (jint)value);
}

void AndroidTest::setValue(float value1, double value2, char value3)
{
    QAndroidJniObject::callStaticMethod<void>(className, "setValue", "(FDC)V", (jfloat)value1, (jdouble)value2, (jchar)value3);
}

int AndroidTest::getValue(int value)
{
    //java类中有两个 getValue 函数 一个需要传参数
    //jint result = QAndroidJniObject::callStaticMethod<jint>(className, "getValue");
    jint result = QAndroidJniObject::callStaticMethod<jint>(className, "getValue", "(I)I", (jint)value);
    return result;
}

void AndroidTest::setMsg(const QString &msg)
{
    QAndroidJniObject jmsg = QAndroidJniObject::fromString(msg);
    QAndroidJniObject::callStaticMethod<void>(className, "setMsg", "(Ljava/lang/String;)V", jmsg.object<jstring>());
}

QString AndroidTest::getMsg()
{
    QAndroidJniObject result = QAndroidJniObject::callStaticObjectMethod(className, "getMsg", "()Ljava/lang/String;");
    return result.toString();
}

void AndroidTest::setText(int value1, float value2, bool value3, const QString &msg)
{
    QAndroidJniObject jmsg = QAndroidJniObject::fromString(msg);
    QAndroidJniObject::callStaticMethod<void>(className, "setText", "(IFZLjava/lang/String;)V", (jint)value1, (jfloat)value2, (jboolean)value3, jmsg.object<jstring>());
}

QString AndroidTest::getText(int value1, float value2, bool value3, const QString &msg)
{
    QAndroidJniObject jmsg = QAndroidJniObject::fromString(msg);
    QAndroidJniObject result = QAndroidJniObject::callStaticObjectMethod(className, "getText", "(IFZLjava/lang/String;)Ljava/lang/String;", (jint)value1, (jfloat)value2, (jboolean)value3, jmsg.object<jstring>());
    return result.toString();
}
  1. 在原生Android开发中,不同页面会定义不同的Activity。但使用Qt Quick、Flutter等采用Direct UI方式实现的第三方开发框架则只定义了一个Activity。里面不同页面实际都是使用OpenGL等直接绘制的。https://blog.csdn.net/LCSENs/article/details/100182235

03:11-15

  1. 安卓中一个界面窗体对应一个Activity,多个界面就有多个Activity,而在Qt安卓程序中,Qt这边只有一个Activity那就是QtActivity(包名全路径 org.qtproject.qt5.android.bindings.QtActivity),这个QtActivity是固定的写好的,整个Qt程序都是在这个QtActivity界面中。你打开AndroidManifest.xml文件可以看到对应节点有个name=org.qtproject.qt5.android.bindings.QtActivity,所以如果要让Qt程序能够更方便通畅的与对应的java类进行交互(需要上下文传递Activity的,比如震动,消息提示等),建议新建一个java类,继承自QtActivity即可,这样相当于默认Qt启动的就是你java类中定义的Activity,可以很好的控制和交互。

  2. 由于AndroidManifest.xml文件每个程序都可能不一样,为了做成通用的组件,这就要求可能不能带上AndroidManifest.xml文件,这样的话每个Qt安卓程序都启动默认内置的Activity,如果依赖Activity上下文的执行函数需要传入Qt的Activity才行,这里切记Qt的Activity包名是 Lorg/qtproject/qt5/android/bindings/QtActivity; 之前顺手想当然的写的 Landroid/app/Activity; 发现死活不行,原来是包名错了。

  3. 一个Qt安卓程序中可以有多个Java类,包括继承自Activity的类(这样的Activity可以通过QtAndroid::startActivity函数来调用),但是只能有一个通过AndroidManifest.xml文件指定的Activity,不指定会默认一个。如果java类中不需要拿到Qt的Activity进行处理的,可以不需要继承任何Activity,比如全部是运算的静态函数。

  4. 在java类中如果上面没有主动引入包名,则下面需要写全路径,引入了则不需要全路径可以直接用(包括枚举值都可以直接写,比如 VIBRATOR_SERVICE 这种枚举值引入了包名后不需要写android.content.Context.VIBRATOR_SERVICE),建议引入包名,比如上面写了 import org.qtproject.qt5.android.bindings.QtActivity; 则下面继承类可以直接写 public class QtAndroidActivity extends QtActivity,如果没有引入则需要写成 public class QtAndroidActivity extends org.qtproject.qt5.android.bindings.QtActivity 。

  5. 建议搭配 android studio 工具开发,因为在 android studio 中写代码都有自动语法提示,包名会提示自动引入,可以查看有那些函数方法等,还可以校验代码是否正确,而如果在QtCreator中手写有时候可能会写错,尤其是某个字母写错,当然这种错误是编译通不过的,会提示错误在哪行。

04:16-20

  1. 用Qt做安卓开发最大难点两个,第一个就是传参数这些奇奇怪怪的字符(Ljava/lang/String;)啥意思,如何对应,这也不是Qt故意为难初学者啥的,因为这套定义机制是安卓系统底层要求的,系统层面定义的一套规范,其实这个在帮助文档中写的很清楚,都有数据类型对照表,用熟悉了几次就很简单了。第二个难点就是用java写对应的类,如果是会安卓开发的人来说那不要太简单,尤其是搜索那么方便一大堆,没有搞过安卓开发的人来说就需要学习下,这个没有捷径,只是希望Qt能够尽可能最大化的封装一些可以直接使用的类,比如后期版本就提供了权限申请的类 QtAndroid::requestPermissionsSync 之类的,用起来就非常的爽,不用自己写个java类调来调去的。

  2. 理论上来说按照Qt提供的万能大法类QAndroidJniObject,可以不用写java类也能执行各种处理,拿到安卓库中的属性和执行方法,就是写起来太绕太费劲,在java类中一行代码,这里起码三行,所以终极大法就是熟悉安卓开发,直接封装好java类进行调用。

  3. 测试发现GetStringUTFChars方法对应的数据字符串中不能带有temp字样,否则解析有问题,不知什么原因。

  4. 数据类型参数和返回值类型必须完全一致,否则执行会提示找不到对应的函数,有返回值一定要写上返回值。

  5. jar文件对包名的命名没有要求,只要放在android/libs目录下即可,安卓底层是通过包名去查找,而不是通过文件名,你甚至可以将原来的包名重新改成也可以正常使用,比如classes.jar改成test.jar也能正常使用。

05:21-25

  1. 关于权限设置,在早期的安卓版本,所有权限都写在全局配置文件AndroidManifest.xml中,这种叫安装时权限,就是安装的时候告诉安卓系统当前app需要哪些权限。大概从安卓6开始,部分权限需要动态申请,这种叫动态权限,这种申请到的权限也可以动态撤销,就是要求程序再次执行代码去向系统申请权限,比如拍照、存储读写等。也不是所有的权限都改成了动态申请,意味着兼容安卓6以上的系统你既要在AndroidManifest.xml中写上要求的权限,也要通过checkPermission申请你需要的权限。

  2. android studio 新建并生产jar包步骤。

  • 第一步:文件(File)-》新建(new)-》项目(new project)-》空白窗体(empty activity)。
  • 第二步:刚才新建好的项目鼠标右键新建(new)-》模块(new module)-》安卓库(android library)。
  • 说明:如果选择的不是安卓库(android library)而是java库(Java Library),则直接编译出来的就是jar文件,默认包名 com.example.lib.MyClass。推荐选择java库,编译后不用去一堆文件中找jar文件。
  • 第三步:写好库名字,根据项目需要选择好最低sdk版本-》完成。
  • 第四步:在刚才新建好的库项目mylibrary,依次找到子节点src/main/java/com.example.mylibrary上鼠标右键新建-》class类。切记是这个节点不是java节点或者其他节点。
  • 第五步:写好你的类方法函数等。
package com.example.mylibrary;
public class Test {
    public static int add(int a, int b) {
        return a + b;
    }
}
  • 第六步:选中库项目mylibrary,菜单执行编译(build)-》编译库(make module xxx)。
  • 第七步:此时在mylibrary/build目录下有outputs目录和intermediates目录,其中outputs/aar目录下是生成的Android库项目的二进制归档文件,包含所有资源,class以及res资源文件全部包含。有时候我们仅仅需要jar文件,只包含了class文件与清单文件 ,不包含资源文件,如图片等所有res中的文件。需要到intermediates/aar_main_jar/debug目录下,可以看到classes.jar,将这个拷贝出来使用即可。当然你也可以对刚才的aar文件用解压缩软件解压出来也能看到classes.jar,是同一个文件。
  • 其他:调用jar包非常简单,只需要将jar文件放在你的项目的libs目录下即可,对应的包名和函数一般jar包提供者会提供,没有提供的话,可以在android studio中新建空白项目,切换到project视图,找到libs目录,鼠标右键最下面作为包动态库添加到项目,导入包完成以后会自动在libs目录列出,双击刚刚导入的包然后就自动列出对应的类和函数。
  1. Qt安卓使用jar包步骤。
  • 第一步:将classes.jar放到android/libs目录下,为啥是这个目录?因为这是安卓的规则约定,这个目录就是放库文件,放在这个目录下的文件会自动打包编译到apk文件中。
  • 第二步:调用jar文件之前,前提是你知道jar文件中的函数详细信息,这个一般jar提供者会提供好手册,如果代码没有混肴的话,你可以在android studio中双击打开查阅具体的函数。
  • 第三步:如果jar文件中的函数简单,直接拿到结果不需要绕来绕去,可以直接写Qt类来调用;如果还是很复杂,建议再去新建java类处理完再交给Qt,当然也可以让jar的作者尽可能封装函数的时候就做好,尽量提供最简单的接口返回需要的数据。比如返回图片数据可以做成jar内部存储好图片,然后返回图片路径即可,不然有些数据转换也挺烦。
  • 第四步:编写最终的调用函数。
int AndroidJar::add(int a, int b)
{
#ifdef Q_OS_ANDROID
    const char *className = "com/example/mylibrary/Test";
    jint result = QAndroidJniObject::callStaticMethod<jint>(className, "add", "(II)I", (jint)a, (jint)b);
    return result;
#endif
}
  1. Qt6中对安卓支持部分做了大的改动,目前还不完善,如果是不涉及到与java交互的纯Qt项目,可以正常移植,涉及到的暂时不建议移植到Qt6,等所有类完善了再说。
  • 移除了安卓插件androidextras,将其中部分功能类移到core模块中,不需要额外引入。
  • 类名发生了变化,比如QAndroidJniObject改成了QJniObject、QAndroidJniEnvironment改成了QJniEnvironment,可能是为了统一移动开发平台类,弱化安卓的影响。
  • 对应的安卓jdk要用jdk11而不是jdk1.8,Qt5.15两个都支持,建议就统一用jdk11。
  • 对应封装的java类包名去掉了qt5标识,org.qtproject.qt5.android.bindings.QtActivity改成了org.qtproject.qt.android.bindings.QtActivity、org.qtproject.qt5.android.bindings.QtApplication改成了org.qtproject.qt.android.bindings.QtApplication。
  • 对安卓最低sdk有要求,所以建议在配置AndroidManifest.xml文件的时候不要带上最低版本要求。
  • 对AndroidManifest.xml文件内容有要求,之前Qt5安卓的不能在Qt6安卓下使用,具体内容参见示例下的文件。
  • 对应示例demo在 C:\Qt\Examples\Qt-6.3.0\corelib\platform 目录下,之前是 C:\Qt\Examples\Qt-5.15.2\androidextras ,目前就一个示例,可能因为其他类还没有移植好。
  • Qt6中安卓模块介绍在这里 https://doc.qt.io/qt-6/qtandroidprivate.html
  1. 如果想要安卓全屏遮挡住顶部状态栏,可以在main函数中将show改成showFullScreen即可,当然也可以采用java的方式在onCreate函数中加一行 getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN);

06:25-30

  1. 横竖屏切换的识别,在Qt中会同时反映到resizeEvent事件中,你可以在这个是尺寸变化后读取下当前屏幕是横屏还是竖屏,然后界面上做出调整,比如上下排列改成左右排列。

  2. 由于不同Qt版本对应的安卓配置文件 AndroidManifest.xml 内容格式不一样,高版本和低版本模板格式互不兼容,所以建议使用自己的Qt版本创建的 AndroidManifest.xml 文件,创建好以后如果使用的是自己重新定义的java文件的启动窗体则需要将 AndroidManifest.xml 文件中的 android:name="org.qtproject.qt5.android.bindings.QtActivity" 换掉就行。

  3. 如果自己用android studio编译的jar文件放到Qt项目的libs目录下,导致编译通不过,提示 com.android.dx.cf.iface.ParseException: bad class file magic 之类的,那是因为jdk版本不一致导致的,你可能需要在android studio项目中找到模块编jdk版本设置的地方降低版本,比如你用的ndk是r14,则需要选择jdk1.6或者jdk1.7。一般来说高版本兼容低版本,因为ndk版本太低无法兼容jdk1.8。后面发现如果直接新建的是java库(Java Library)则不存在这个问题,如果选择的是安卓库(android library)就可能有这个问题。

  4. 安卓项目配置文件是固定的名字 AndroidManifest.xml ,改成其他名字就不认识,不要想当然改成其他名字导致无法正常识别。

  5. AndroidManifest.xml文件中的package="org.qtproject.example"是包名,也是整个apk程序的内部唯一标识,如果多个apk这个包名一样,则会覆盖,所以一定要注意不同的程序记得把这个包名改成你自己的。这个包名也决定了java文件中需要使用资源文件时候的引入包名 import org.qtproject.example.R; 如果包名不一样则编译都通不过。

  6. 新版的qtc搭建安卓开发环境非常简单,早期版本的非常复杂,要东下载西下载,折腾好多天才行。现在只需要安装jdk文件(jdk_8.0.1310.11_64.exe),全部默认一步到位,然后在qtc中安卓配置界面,设置jdk的安装目录。然后打开 D:\Qt\Qt6\Tools\QtCreator\share\qtcreator\android\sdk_definitions.json 和 C:\Users\Administrator\AppData\Roaming\QtProject\qtcreator\android\sdk_definitions.json,将里面的 cmdline-tools;latest 修改为 cmdline-tools;6.0 ,这一步非常关键,默认是latest导致待会自动下载sdk/ndk的时候会下载不全。改好以后,设置sdk保存目录,单击右侧的 Set Up SDK 按钮,自动下载一堆文件,最后下面有个openssl的目录文件也设置下。该文件网上可以非常简单就能直接下载到,右侧就有按钮单击打开下载页面。然后就可以开始愉快的安卓开发之旅了。

项目大全 https://qtchina.blog.csdn.net/article/details/97565652

63094-20230820114650922-111302057.jpg
63094-20230820114655323-1074237548.jpg
63094-20230820114658887-31642094.jpg

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK