Доброго времени суток всем посетителям и пользователям !
Недавно я на эмоциях опубликовал статью, где я рассказывал о всех своих злоключениях при попытки создать qt приложение ( а именно вызвать и использовать
QApplication a(argc, argv);
и использовать его при помощи андроид студии. Было найдено «решение», которое было чрезвычайно костыльным. Теперь у меня были выходные, чтобы разобраться как надо работать с qt без таких костылей из андроид студии. Всем кому интересно — добро пожаловать под кат!
Постановка задачи
Итак, прежде чем двигаться дальше и для всех, кто не читал мою первую статью или забыл, напомню что мне надо сделать. У меня есть ios приложение на objective c, которое для расчёта и прорисовки некоторых вещей использует qt библиотеку. Подчёркиваю. Именно ios приложение и небольшая библиотека на qt (в первой статье я криво выразился и меня поняли, что у меня qt приложение было под ios. Нет. На qt только небольшая библиотека). Мне надо портировать его на андроид. Я благополучно реализовал графический интерфейс, логику, подключил qt библиотеку. В чём проблема? Проблема в том, что когда пытаюсь вызвать JNI функцию из Java части, а эта функция выполняет следующий код
QApplication a(argc, argv);
то я получаю исключение:
This application failed to start because it could not find or load the Qt platform plugin «android».
Мне необходим вызов конструктора qt приложения для того, чтобы я мог рисовать текст в qt. Поэтому мне необходимо разбираться с этой проблемой. Вариант: «Всё переписать на qt» меня не устраивал (ведь приложение уже было готово на Java в андроид студии), поэтому, после много-много часов мною был предложен варинт: создать в QtCreator проект под андроид. Собрать из него aar и импортировать его в андроид студию и пользоваться им (подробнобности всей этой кухни в предыдущей статье). Очевидно, что это решние… как бы помягче выразиться… велосипедно-костыльное. И более того, как оказалось содержащее изъян ( о нём в ps. первой статьи). Поэтому, здесь разберём как добиться нужного эффекта более правильным способом.
Решение
Вся кухня с qt для андроида работает в предположении, что у нас есть 2 класса:
- QtApplication
- QtActivity
Поэтому, нам необходимо их добавить к себе в проект. Эти два класса можно найти в папке, где установлено qt для андроида. У меня это C:QtQT.Android5.5android_armv7srcandroidjavasrcorgqtprojectqt5androidbindings.
QtApplication можно добавить себе в проект, только заменить там следующие участки кода:
ИМЯ_КЛАССА_НАШЕЙ_QT_АКТИВИТИ.class.getDeclaredMethod(delegateMethod.getName(), delegateMethod.getParameterTypes());
и
if (-1 == stackDeep) {
String activityClassName = ИМЯ_КЛАССА_НАШЕЙ_QT_АКТИВИТИ.class.getCanonicalName();
for (int it=0;it<elements.length;it++)
if (elements[it].getClassName().equals(activityClassName)) {
stackDeep = it;
break;
}
}
Отлично. Перейдём теперь к классу QtActivity. Всё, что там есть, должно быть и у нашей QtActivity. В моём случае это был MainViewController, в который я скопипастил весь код из QtActivity. Дальше, обратим внимание на метод OnCreate...
super.onCreate(savedInstanceState);
try {
m_activityInfo = getPackageManager().getActivityInfo(getComponentName(), PackageManager.GET_META_DATA);
for (Field f : Class.forName("android.R$style").getDeclaredFields()) {
if (f.getInt(null) == m_activityInfo.getThemeResource()) {
QT_ANDROID_THEMES = new String[] {f.getName()};
QT_ANDROID_DEFAULT_THEME = f.getName();
}
}
} catch (Exception e) {
e.printStackTrace();
finish();
return;
}
try {
setTheme(Class.forName("android.R$style").getDeclaredField(QT_ANDROID_DEFAULT_THEME).getInt(null));
} catch (Exception e) {
e.printStackTrace();
}
if (Build.VERSION.SDK_INT > 10) {
try {
requestWindowFeature(Window.class.getField("FEATURE_ACTION_BAR").getInt(null));
} catch (Exception e) {
e.printStackTrace();
}
} else {
requestWindowFeature(Window.FEATURE_NO_TITLE);
}
if (QtApplication.m_delegateObject != null && QtApplication.onCreate != null) {
QtApplication.invokeDelegateMethod(QtApplication.onCreate, savedInstanceState);
return;
}
m_displayDensity = getResources().getDisplayMetrics().densityDpi;
ENVIRONMENT_VARIABLES += "tQT_ANDROID_THEME=" + QT_ANDROID_DEFAULT_THEME
+ "/tQT_ANDROID_THEME_DISPLAY_DPI=" + m_displayDensity + "t";
if (null == getLastNonConfigurationInstance()) {
// if splash screen is defined, then show it
if (m_activityInfo.metaData.containsKey("android.app.splash_screen_drawable"))
getWindow().setBackgroundDrawableResource(m_activityInfo.metaData.getInt("android.app.splash_screen_drawable"));
else
getWindow().setBackgroundDrawable(new ColorDrawable(0xff000000));
if (m_activityInfo.metaData.containsKey("android.app.background_running")
&& m_activityInfo.metaData.getBoolean("android.app.background_running")) {
ENVIRONMENT_VARIABLES += "QT_BLOCK_EVENT_LOOPS_WHEN_SUSPENDED=0t";
} else {
ENVIRONMENT_VARIABLES += "QT_BLOCK_EVENT_LOOPS_WHEN_SUSPENDED=1t";
}
startApp(true);
}
}
Здесь есть тонкость. Где мы будем загружать разметку из layouta? То есть, куда вставить следующий участок кода:
setContentView(R.layout.form_name);
Если, его вставлять до startApp(true);, то ничего не заработает! Поэтому, я просто посоветую пока здесь без объяснений сделать нечто вроде такого:
startApp(true);
}
new Thread(new Runnable() {
@Override
public void run() {
while (!MyLibSo.isQtReady()) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
runOnUiThread(new Runnable() {
@Override
public void run() {
initializeGUI();
}
});
}
}).start();
Здесь MyLibSo.isQtReady() — моя нативная функция. Её основная задача проверять, было ли уже создано в с-части QApplication. Если создано, то запускаем рисование нашего layouta в initializeGUI(); Там уже и вызываем
setContentView(R.layout.form_name);
Размещение qt либ
Теперь наиболее интересная часть. Главная проблема возникает в том, что не грузятся по-нормальному qt-плагины ( в предыдущей статье об этом расписано). Скопипащенный код из QtActivity и QApplication должен решить все эти проблемы. Но для этого ему нужны либы. Где их взять?
- Скачать из интернета.
- Указать Qt, где это всё лежит.
- Загрузить из apk
Второй вариант удаляем сразу — мало ли, пользователь удалит каталог и всё. Первый вариант чуть более интересный. Он осуществляется при помощи сервиса Ministro. Нас будет интересовать третий вариант. Для начала идём в строковые ресурсы и определяем следующее:
<string name="ministro_not_found_msg">Can't find Ministro service.nThe application can't start.</string>
<string name="ministro_needed_msg">This application requires Ministro service. Would you like to install it?</string>
<string name="fatal_error_msg">Your application encountered a fatal error and cannot continue.</string>
<array name="qt_sources">
<item>https://download.qt-project.org/ministro/android/qt5/qt-5.4</item>
</array>
<array name="bundled_libs">
<item>MyLibWithQt</item>
</array>
<array name="qt_libs">
<item>gnustl_shared</item>
<item>Qt5Core</item>
<item>Qt5Gui</item>
<item>Qt5Widgets</item>
</array>
<array name="bundled_in_lib">
<item>
libplugins_platforms_android_libqtforandroid.so:plugins/platforms/android/libqtforandroid.so
</item>
<item>libplugins_platforms_libqminimal.so:plugins/platforms/libqminimal.so</item>
<item>libplugins_platforms_libqoffscreen.so:plugins/platforms/libqoffscreen.so</item>
</array>
<array name="bundled_in_assets">
</array>
Всё, что связано с министро — нам не интересно. MyLibWithQt — это с либа, использующая qt. Обратите внимание на libplugins_platforms_android_ Все плагины, которые идут дальше, должны содержать именно такое начало!!! Теперь, в манифесте вставляем следующий код:
<meta-data
android:name="android.app.lib_name"
android:value="MyLibWithQt " />
<meta-data
android:name="android.app.qt_sources_resource_id"
android:resource="@array/qt_sources" />
<meta-data
android:name="android.app.repository"
android:value="default" />
<meta-data
android:name="android.app.qt_libs_resource_id"
android:resource="@array/qt_libs" />
<meta-data
android:name="android.app.bundled_libs_resource_id"
android:resource="@array/bundled_libs" />
<!-- Deploy Qt libs as part of package -->
<meta-data
android:name="android.app.bundle_local_qt_libs"
android:value="1" />
<meta-data
android:name="android.app.bundled_in_lib_resource_id"
android:resource="@array/bundled_in_lib" />
<meta-data
android:name="android.app.bundled_in_assets_resource_id"
android:resource="@array/bundled_in_assets" />
<!-- Run with local libs -->
<meta-data
android:name="android.app.use_local_qt_libs"
android:value="1" />
<meta-data
android:name="android.app.libs_prefix"
android:value="/data/local/tmp/qt/" />
<meta-data
android:name="android.app.load_local_libs"
android:value="plugins/platforms/android/libqtforandroid.so" />
<meta-data
android:name="android.app.load_local_jars"
android:value="jar/QtAndroid.jar:jar/QtAndroidAccessibility.jar:jar/QtAndroid-bundled.jar:jar/QtAndroidAccessibility-bundled.jar" />
Здесь мы указываем, какие либы надо ему загрузить из apk файла и как называется наша либа с qt. Разумеется, не забываем положить все эти плагины в папку jniLibs. Компилируем, запускаем, создаём в С коде
QApplication a(argc, argv);
и… получаем то же самое прокл знакомое исключение. Не создаётся наше QApplication.
Последняя напасть
Как же так? Вроде всё сделано правильно, а почему не работает? Всё оказалось… судите сами. Плагин libqtforandroid.so будет корректно загружен только в том случае, если в нашей библиотеке есть функция main. Если её нет, то один из jni методов в данном плагине не доработает до конца и приложение не будет создано! Поэтому, идём в нашу библиотеку, использующую qt, и определяем там функцию main. Логично, в ней и создать QApplication. Теперь всё, всё работает.
Резюме
Итак, подведём итоги. Чтобы работать с qt из Android studio нам необходимо:
1. Взять и видоизменить QtApplication (пара мест в коде) и определить свою QtActivity (чтобы вызвать свою разметку, setContentView вызываем позже, как создано будет QtApplication). Всё это добавить себе в код.
2. Настроить манифест, чтобы он подгружал библиотеки из apk.
3. В либу, в которой используется qt, добавить функцию main.
4. ??????
5. Profit!!!
Автор: Rogvold91