Qt for Android主要用于在单个Activity或Service中使用Qt。因此,其导航功能与常规Android应用的实现并不完全相同。另外,由于Android系统的特性,在使用公共Android SDK时,无法将QtActivity嵌入到另一个Activity中。
我们将制作简单的应用来演示如何在AndroidStudio项目中使用QtActivity。在这个应用中,我们从Android端向Qt发送消息,根据Android上的按钮来更改QML矩形的颜色。
我们在Qt端有一个矩形,根据Android Activity来改变其颜色。
在Android端,我们只有两个按钮,可以将矩形颜色设置为绿色或青色。
构建面向Android平台的Qt项目。您可以点击此处获取使用 Qt Creator构建的说明。这将在Qt项目的构建目录中创建一个android-build文件夹。这个文件夹是独立的Android项目,您可以在Android Studio中打开并编辑。在我们的演示项目中,要将部分文件复 制到AndroidStudio项目中。
下文中<Android Project>指由Android Studio创建的包含Android项目的文件夹。
<Qt Build>指使用QtCreator构建面向Android的项目时生成的android-build文件夹。常见路径如:/QtProjects/build-ProjectName-Qt_version-DebugOrRelease/android-build。
1. 复 制<QtBuild>/libs中的文件到<AndroidProject>/app/libs
2. 复 制<QtBuild>/assets/文件夹到<Android Project>/app/
从<Qt Build>中需要复 制的文件夹如下图所示
4. 在<QtBuild>/gradle.properties中复 制qtAndroidDir属性到<Android Project>/gradle.properties
在<Qt Build>/gradle.properties中需要复 制的属性如下
...
qtAndroidDir=/home/user/Qt/6.4.2/android_arm64_v8a/src/android/java
...
<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="appAndroidTest"
android:launchMode="singleInstance"
android:screenOrientation="unspecified"
android:exported="true">
<meta-data
android:name="android.app.lib_name"
android:value="appAndroidTest" />
<meta-data
android:name="android.app.arguments"
android:value="" />
<meta-data
android:name="android.app.extract_android_style"
android:value="minimal" />
</activity>
6. 在<Android Project>/app/build.gradle中添加以下代码
...
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar', '*.aar'])
}
...
android {
...
// Extract native libraries from the APK
packagingOptions.jniLibs.useLegacyPackaging true
sourceSets {
main {
java.srcDirs += [qtAndroidDir + '/src']
aidl.srcDirs += [qtAndroidDir + '/src']
resources.srcDirs = ['resources']
assets.srcDirs = ['assets']
jniLibs.srcDirs = ['libs']
}
}
}
实现原理
建立Qt和Android之间的通信
由于我们将本地库复 制到了AndroidStudio项目中,因此我们可以使用JavaNative Interface (JNI)在Java和C++之间进行通信。幸运的是,Qt提供了许多辅助函数来处理这个问题。对于通信,我们将使用JSON字符串,因为它们在QML中是本地可访问的(QML具有JavaScript引擎)。
注意:每次修改Qt项目时,您需要再次从<Qt Build>复 制资源和库文件夹到<Android Project>(前一节中的步骤1和2)。
Qt端
在Qt端,我们创建了名为JniMessenger的新类。以下是头文件。
class JniMessenger : public QObject
{
Q_OBJECT
QML_NAMED_ELEMENT(JniMessenger)
QML_SINGLETON
private:
explicit JniMessenger(QObject *parent = nullptr);
public:
Q_INVOKABLE void sendMessageToJava(const QString &message);
static JniMessenger *instance();
static JniMessenger *create(QQmlEngine *qmlEngine, QJSEngine *jsEngine);
signals:
void messageReceivedFromJava(QString message);
};
该类作为单例暴露给QML。每当有来自Java的消息时,就会信号被触发。因为我们将该类设置为单例,所以需要将构造函数设为私有,并添加一个”create”函数。在QML中实例化此类时,将调用此函数。请在https://doc.qt.io/qt-6/qqmlengine.html#QML_SINGLETON获取有关向QML暴露单例类的更多信息。
JniMessenger *JniMessenger::instance()
{
static JniMessenger instance;
return &instance;
}
JniMessenger *JniMessenger::create(QQmlEngine *qmlEngine, QJSEngine *jsEngine)
{
Q_UNUSED(qmlEngine)
JniMessenger *singletonInstance = JniMessenger::instance();
// The engine must have the same thread affinity as the singleton.
Q_ASSERT(jsEngine->thread() == singletonInstance->thread());
return singletonInstance;
}
将此函数添加到类中,用于向Java发送消息。此函数调用Java端的静态方法,我们稍后将创建该方法。使用新的JNI语法“Q_DECLARE_JNI_CLASS(javaMessageHandlerClass,com/example/androidapp/MainActivity)”定义了javaMessageHandlerClass。您可以阅读Volker的https://www.qt.io/blog/unstringifying-android-development-with-qt-6.4这篇博文来了解关于新JNI语法的更多内容。该语法用预定义的类和类型签名代替JNI方法调用中使用的字符串签名。
void JniMessenger::sendMessageToJava(const QString &message)
{
QJniObject::callStaticMethod<void, jstring>(
QtJniTypes::className<QtJniTypes::javaMessageHandlerClass>(),
"receiveMessageFromQt",
QJniObject::fromString(message).object<jstring>());
}
接下来在类的外部添加以下代码。sendMessageToQt函数是一个在Java中声明并在此处定义的本地方法。它将消息从Java发送到Qt。JNI_OnLoad函数由JNI调用,您应该在这里注册所有本地方法。通过使用新的JNI语法,如代码所示,本地方法的注册变得更容易。
void sendMessageToQt(JNIEnv *env, jclass cls, jstring message)
{
Q_UNUSED(cls)
QString string = env->GetStringUTFChars(message, nullptr);
emit JniMessenger::instance()->messageReceivedFromJava(string);
}
Q_DECLARE_JNI_NATIVE_METHOD(sendMessageToQt)
JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *reserved)
{
Q_UNUSED(vm)
Q_UNUSED(reserved)
static bool initialized = false;
if (initialized)
return JNI_VERSION_1_6;
initialized = true;
// get the JNIEnv pointer.
QJniEnvironment env;
if (!env.isValid())
return JNI_ERR;
const jclass javaClass = env.findClass(
QtJniTypes::className<QtJniTypes::javaMessageHandlerClass>());
if (!javaClass)
return JNI_ERR;
static JNINativeMethod methods[] = { Q_JNI_NATIVE_METHOD(sendMessageToQt) };
// register our native methods
if (!env.registerNativeMethods(javaClass, methods, std::size(methods)))
return JNI_ERR;
return JNI_VERSION_1_6;
}
在main函数中添加以下代码:
QTimer::singleShot(0, [argc, argv]() {
if (argc > 1) {
emit JniMessenger::instance()->messageReceivedFromJava(argv[1]);
}
});
将QtActivity作为第二个Activity加载的一个问题是,本地库还没有加载完毕,所以我们还不能立即调用本地方法。因此,我们使用AndroidIntents在第一次传递数据时进行操作,然后再继续执行常规的本地函数调用。Android端给出了正确地将Intent数据发送到Qt的方法。0毫秒延迟的QTimer::singleShot使得发出的信号进入主事件循环,确保 QML能够及时捕获信号。
最后,在QML侧,为按钮添加功能并建立连接以便监听信号。当按钮被点击时,它会向Java发送一个包含单一属性的JSON字符串:navigate:back。这个信号通知Java再次加载第一个Activity的开始部分。当我们从Java接收到消息时,我们会寻找color属性,并将矩形的颜色设定为该属性值。
Window {
...
Rectangle {
focus: true
Keys.onReleased: function(event) {
if (event.key === Qt.Key_Back) {
console.log("Back key pressed");
event.accepted = true;
JniMessenger.sendMessageToJava(JSON.stringify(
{
navigate: "back"
}
));
}
}
}
Connections {
target: JniMessenger
function onMessageReceivedFromJava(message) {
const data = JSON.parse(message);
for (let key in data) {
if (data.hasOwnProperty(key)) {
console.log("Setting " + key + " to " + data[key]);
if (data.color) {
rectangle.color = data.color;
}
}
}
}
}
...
}
Android端
public class MainActivity extends AppCompatActivity {
private interface MessageFromQtListener {
public void onMessage(String message);
};
private static final String TAG = "Android/MainActivity";
private static boolean firstTime = true;
private JsonHandler jsonHandler;
private static MessageFromQtListener m_messageListener;
public static native void sendMessageToQt(String message);
public static void setOnMessageFromQtListener(MessageFromQtListener listener) {
m_messageListener = listener;
}
public static void receiveMessageFromQt(String message) {
Log.d(TAG, "Message received from Qt: " + message);
if (m_messageListener != null) {
m_messageListener.onMessage(message);
}
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
jsonHandler = new JsonHandler();
MainActivity.setOnMessageFromQtListener(new MessageFromQtListener() {
@Override
public void onMessage(String message) {
try {
JSONObject jsonObject = new JSONObject(message);
Map<String, Object> json = JsonHandler.toMap(jsonObject);
if (json.containsKey("navigate") &&
Objects.equals((String) json.get("navigate"), "back")) {
Intent intent = new Intent(getApplicationContext(), MainActivity.class);
startActivity(intent);
}
} catch (JSONException e) {
Log.e(TAG, "Not valid JSON: " + message);
}
}
});
}
public void onBtnClicked(View view) {
String color = ((Button) view).getText().toString().toLowerCase();
Log.d(TAG, color);
Intent intent = new Intent(MainActivity.this, QtActivity.class);
intent.addFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION);
if (!firstTime) {
MainActivity.sendMessageToQt(jsonHandler.buildJson("color", color).toString());
} else {
intent.putExtra("applicationArguments",
jsonHandler.buildJson("color", color).toString());
firstTime = false;
}
startActivity(intent);
}
}