How to add QtActivity to an Android Studio Project
如何将QtActivity添加到Android Studio项目
April 03, 2023 by Nikunj Arora | Comments
2023年4月3日:Nikunj Arora |评论
Qt has great tools for designing, developing, and testing Android apps using Qt Design Studio, Qt Creator, and Squish. But sometimes, there is an already existing codebase developed in Android Studio which needs a few functionalities from the Qt framework. This blog post demonstrates how we can integrate a Qt for Android project into an Android Studio Project.
Qt拥有强大的工具,可以使用Qt Design Studio、Qt Creator和Squish设计、开发和测试Android应用程序。但有时,Android Studio中已经开发了一个现有的代码库,它需要Qt框架中的一些功能。这篇博客文章展示了我们如何将Qt for Android项目集成到Android Studio项目中。
Qt for Android has been designed with the main focus of using Qt in a single Activity or Service. For this reason, navigation does not work exactly like in regular Android apps. Also, due to the nature of Android, it is not possible to embed the QtActivity into another Activity while using the public Android SDK.
Qt for Android的设计重点是在单个活动或服务中使用Qt。出于这个原因,导航的工作方式与普通的Android应用程序不同。此外,由于Android的性质,在使用公共Android SDK时,不可能将QtActivity嵌入到另一个“活动”中。
Building and running the demo
构建和运行演示
We will be making a simple app to demonstrate QtActivity working in an Android Studio project. In this app, we send messages from Android to Qt for changing the color of a QML rectangle based on the button pressed on the Android side.
我们将制作一个简单的应用程序来演示QtActivity在Android Studio项目中的工作。在这个应用程序中,我们从Android向Qt发送消息,根据Android侧按下的按钮更改QML矩形的颜色。
On the Qt side, we have a rectangle whose color we would change from the Android activity.
在Qt侧,我们有一个矩形,其颜色将从Android活动中更改。
On the Android side, we have just two buttons that set the color of the rectangle to green or cyan.
在Android侧,我们只有两个按钮可以将矩形的颜色设置为绿色或青色。
https://play.hubspotvideo.com/e820b1c8-3106-4600-9c56-7899f6ecbe82
Build the Qt project targeting the Android platform. Instructions for building using Qt Creator can be found here. This creates an "android-build" folder in the build directory of the Qt project. This folder is a standalone Android project which you can open in Android Studio and edit. For our use case, we will be copying parts of it to our Android Studio project.
针对Android平台构建Qt项目。使用Qt Creator构建的说明可以在这里找到。这会在Qt项目的构建目录中创建一个“android构建”文件夹。此文件夹是一个独立的Android项目,可以在Android Studio中打开并编辑。对于我们的用例,我们将把它的一部分复制到我们的Android Studio项目中。
Android Project = The folder created by Android Studio containing your Android project.
Android项目=由Android Studio创建的包含您的Android项目的文件夹。
Qt Build = The "android-build" folder generated by QtCreator when you build your project targeting Android. Usually, the path looks like "/QtProjects/build-ProjectName-Qt_version-DebugOrRelease/android-build".
Qt Build=当您针对android构建项目时,QtCreator生成的“android构建”文件夹。通常,路径看起来像“/QtProjects/build-ProjectName-Qt_version-DebugOrRelease/android build”。
1.Copy the files from <Qt Build>/libs to <Android Project>/app/libs
1.将文件从<Qt Build>/libs复制到<Android Project>/app/libs
2.Copy <Qt Build>/assets/ folder to <Android Project>/app/
2.将<Qt构建>/assets/文件夹复制到<Android项目>/app/
The folders to copy from <Qt Build> are shown below
要从<Qt Build>复制的文件夹如下所示

3.Copy <Qt Build>/res/values/libs.xml to <Android Project>/app/src/main/res/values
3.将<Qt Build>/res/values/libs.xml复制到<Android项目>/app/src/main/res/values
4.In <Qt Build>/gradle.properties copy the "qtAndroidDir" property to <Android Project>/gradle.properties
4.在<Qt Build>/gradle.properties中,将“qtAndroidDir”属性复制到<Android Project>/gradele.properties
The property to copy in <Qt Build>/gradle.properties is shown below
要在<Qt Build>/gradle.properties中复制的属性如下所示
...
qtAndroidDir=/home/user/Qt/6.4.2/android_arm64_v8a/src/android/java
...
5.Copy the <activity> tag of the QtActivity from <Qt Build>/AndroidManifest.xml and paste it into the AndroidManifest.xml of the <Android Project> and remove the MAIN Intent filter. Copy the relevant permissions as well.
5.从<Qt Build>/AndroidManifest.xml复制QtActivity的<activity>标签,并将其粘贴到<Android Project>的AndroidManifest.xml中,并删除MAIN Intent过滤器。同时复制相关权限。
Also change the property "android:launchMode" from "singleTop" to "singleInstance". This helps with navigation between Android Activity and QtActivity.
同时将属性“android:launchMode”从“singleTop”更改为“singleInstance”。这有助于在Android活动和QtActivity之间进行导航。
The activity tag in <Android Project>'s AndroidManifest.xml is shown below (cropped)
<Android Project>的AndroidManifest.xml中的活动标签如下所示(裁剪)
<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.In <Android Project>/app/build.gradle add the following code
6.在<Android项目>/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']
}
}
}
How it works
工作原理
Establishing communication between Qt and Android
在Qt和Android之间建立通信
Since we copied the native libs to the Android Studio project, we can communicate between Java and C++ using the Java Native Interface (JNI). Luckily, Qt provides a lot of helper functions for this. For communication, we would use JSON strings since they are natively accessible in QML (which has a JavaScript engine).
由于我们将原生libs复制到了AndroidStudio项目中,因此我们可以使用Java原生接口(JNI)在Java和C++之间进行通信。幸运的是,Qt为此提供了很多辅助功能。对于通信,我们将使用JSON字符串,因为它们在QML(它有一个JavaScript引擎)中是本地可访问的。
Note: Anytime you make changes to the Qt project, you will need to copy assets and libs folder from <Qt Build> to <Android Project> again (steps 1 and 2 in the previous section).
注意:任何时候对Qt项目进行更改时,都需要再次将资产和libs文件夹从<Qt Build>复制到<Android project>(上一节中的步骤1和2)。
Qt side
Qt侧
On the Qt side, we create a new class "JniMessenger". The header file is shown below.
在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);
};
The class is exposed to QML as a singleton. The signal gets emitted whenever there is a message from Java. Since we are making the class a singleton, make the constructor private, and add a "create" function. This function is called by QML when instantiating this class. More information about exposing a singleton class to QML can be found here.
该类作为一个单例公开给QML。只要有来自Java的消息,就会发出信号。由于我们将类设为singleton,因此将构造函数设为私有的,并添加一个“create”函数。QML在实例化此类时调用此函数。关于向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;
}
Add this function to the class for sending messages to Java. This function calls a static method on the Java side which we will create later. The javaMessageHandlerClass is defined using the new JNI syntax, "Q_DECLARE_JNI_CLASS(javaMessageHandlerClass, com/example/androidapp/MainActivity)". Read this blog post by Volker to learn more about the new JNI syntax. It replaces the string signatures used in JNI method calls with predefined class and type signatures.
将此函数添加到用于向Java发送消息的类中。这个函数在Java侧调用一个静态方法,我们稍后将创建该方法。javaMessageHandlerClass是使用新的JNI语法“Q_DECLARE_JNI_CLASS(javaMessageHandler CLASS,com/example/androidapp/MainActivity)”定义的。阅读Volker的这篇博客文章,了解有关新JNI语法的更多信息。它将JNI方法调用中使用的字符串签名替换为预定义的类和类型签名。
void JniMessenger::sendMessageToJava(const QString &message)
{
QJniObject::callStaticMethod<void, jstring>(
QtJniTypes::className<QtJniTypes::javaMessageHandlerClass>(),
"receiveMessageFromQt",
QJniObject::fromString(message).object<jstring>());
}
Now add the following code outside the class. The sendMessageToQt function is a native method declared in Java and defined here. It sends messages from Java to Qt. The JNI_OnLoad function is called by JNI and this is where you should register all your native methods. Using the new JNI syntax, the registering of native methods got easier as shown in the code.
现在在类外部添加以下代码。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;
}
In the main function, add the following code.
在主函数中,添加以下代码。
QTimer::singleShot(0, [argc, argv]() {
if (argc > 1) {
emit JniMessenger::instance()->messageReceivedFromJava(argv[1]);
}
});
One of the problems with loading QtActivity as a second activity is that the native libraries are not yet loaded, so we cannot call the native methods yet. Therefore, we make use of Android Intents to pass the data for the first time and then regular native function calls from then onwards. The Android side shows the correct way to send Intent data to Qt. The QTimer::singleShot with 0 ms delay puts the signal emitted into the main event loop, to make sure QML has time to catch it.
将QtActivity作为第二个活动加载的问题之一是尚未加载本机库,因此我们还不能调用本机方法。因此,我们首次使用Android Intents传递数据,然后从那时起定期调用本机函数。Android侧显示了向Qt发送Intent数据的正确方式。具有0毫秒延迟的QTimer::singleShot将发出的信号放入主事件循环,以确保QML有时间捕捉它。
Finally, on the QML side, add the functionality to the button and create a connection to listen to the signal. When the button is clicked, it sends a JSON string to Java with the single property: 'navigate': "back". This tells Java to load the start of the first activity again. When we receive a message from Java, we look for a property of 'color' and set the rectangle color to that.
最后,在QML侧,将功能添加到按钮并创建一个连接来监听信号。当单击该按钮时,它会向Java发送一个JSON字符串,其中只有一个属性:“navigation”:“back”。这告诉Java再次加载第一个活动的开始。当我们收到来自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 side
Android侧
On the Android side, we add new static methods to be called from the Qt side. The "MessageFromQtListener" is an interface used for adding a listener for messages from Qt. We also have a native method "sendMessageToQt" which we already defined on the Qt side. The following is the MainActivity class.
在Android侧,我们添加了新的静态方法,以便从Qt侧调用。“MessageFromQtListener”是一个用于为来自Qt的消息添加侦听器的接口。我们还有一个本地方法“sendMessageToQt”,我们已经在Qt端定义了它。以下是MainActivity类。
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);
}
}
onBtnClicked is called by the XML and sends a JSON string to the QML that updates the color. As explained on the Qt side, until the QtActivity is loaded, we can't call the native methods, therefore we make use of the "m_qtLibsLoaded" variable. When it is true, we make use of the regular native calls. When it is false, we load the QtActivity and pass Intent data to it. The QtActivity looks for an Intent field named "applicationArguments" while loading the libraries and passes them to the C++ code as environment variables. These can be accessed in the main function using the argv argument.
onBtnClicked由XML调用,并向QML发送一个JSON字符串以更新颜色。正如Qt侧所解释的,在加载QtActivity之前,我们不能调用本地方法,因此我们使用“m_qtLibsLoaded”变量。当这是真的时,我们会使用常规的本地调用。当它为false时,我们加载QtActivity并将Intent数据传递给它。QtActivities在加载库时查找名为“applicationArguments”的Intent字段,并将它们作为环境变量传递给C++代码。这些可以在主函数中使用argv参数进行访问。
That is all that should be needed for running a QtActivity from an Android Studio project.
这就是从Android Studio项目运行QtActivity所需要的全部内容。
本文介绍了如何将QtActivity添加到已有的AndroidStudio项目中,包括复制必要的Qt构建文件、修改AndroidManifest.xml、配置build.gradle以及使用JNI在Qt和Android之间建立通信。通过示例展示了如何在Android按钮点击后改变QtQML的矩形颜色。

被折叠的 条评论
为什么被折叠?



