Qt安卓開發經驗技巧總結V202308

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


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章