AppStructure.md
Арго Фреймворк Википедия
Содержание
- Введение
- Установка Фреймворка
- Приложения Фреймворка
- Создание Приложений
- Примеры Приложений
- Структура Приложений *
- Функция Запуска Приложения
- Коллбеки Приложения
- Описания Пользовательского Интерфейса
- Makefile Приложения
- Скрипт Установки Приложения
- Структура Управляющих Элементов
- Утилиты Фреймворка
Структура Приложений
Все приложения фреймворка имеют определенную структуру, которая необходима для взаимодействия с фреймворком. Когда приложение создается скриптом appmake.sh, эта структура реализуется в виде каркаса приложения. Далее каркас наполняется содержанием, которое реализует логику приложения. Скрипт создает необходимый набор файлов: файлы, реализующие логику приложений на выбранном языке (C/C++/Python), файлы описаний пользовательского интерфейса (FML) и файлы для постройки приложения и интеграции его в систему.
В данном разделе будут рассмотрены основные функции логической части и файлы описаний интерфейса, а также основной makefile приложения и скрипт установки.
Функция Запуска Приложения
Фунция запуска приложения предназначена для инициализации внутренней структуры приложения и регистрации приложения во фреймворке. Эта функция может иметь предопределенное имя (как правило, имя приложения) или специальное имя, в зависимости от того, в каком режиме собирается приложение (к примеру, для remote приложений она всегда называется appmain). Эта функция всегда вызывается через механизм dlopen/dlsym в библиотеке приложений при запуске.
Реализация функции зависит от выбранного языка программирования, но, в целом, все одинакого. Рассмотрим сначала реализацию на языке Си (applets/hello/src/hello.c):
#include "hello.h"
#ifndef APPNAME
#define APPNAME Hello
#endif
FrmResult APPNAME( void* pHandle, frm_uint32_t uArgc, char** ppArgv )
{
FrmResult result = FRMSUCCESS;
hello_t* pThis = NULL;
const char* pName = APPLICATION_NAME;
frm_uint32_t uPID = 0;
frm_app_register_t AppRegister = FRM_APP_REGISTER_INIT;
( void )uArgc;
( void )ppArgv;
FRM_PCHECK( pHandle, pName, "NULL pointer to application handle\n" );
pThis = ( hello_t* )FRMIL_Malloc( sizeof( hello_t ) );
FRM_PCHECK( pThis, pName, "Application structure memory allocation failed\n" );
FRMIL_Memset( ( void* )pThis, 0, sizeof( hello_t ) );
pThis->pHandle = pHandle;
pThis->pName = APPLICATION_NAME;
pThis->uPID = FRMAPP_GetPID( pHandle );
AppRegister.pAppData = ( void* )pThis;
AppRegister.pfnActive = hello_activate;
AppRegister.pfnHandle = hello_handle;
AppRegister.pfnSuspend = hello_suspend;
AppRegister.pfnResume = hello_resume;
AppRegister.pfnExit = hello_exit;
AppRegister.pCtrlRegTable = control_entry;
AppRegister.uCtrlRegSize = sizeof( control_entry ) / sizeof( frm_app_ctrlreg_t );
result = FRMAPP_Register( pHandle, &AppRegister );
FRM_ERROR( result, pThis->pName, "Application register failed\n" );
FRMLIBAPI_Init();
FRM_DEBUG( pThis->pName, "The Hello application is started (0x%08X)\n", uPID );
return result;
}
Имя функции, заданное макросом APPNAME, будет либо ‘Hello’ для статических и динамических приложений, либо ‘appmain’ для remote приложений. Это имя и будет искаться как символ в бинарном файле для запуска. Функция принимает 3 параметра: указатель pHandle, количество аргументов uArgc и массив аргументов ppArgv. Для нас важен только указатель pHandle (аргументы командной строки используются так же, как и в обычной функции main). Этот указатель имеет неопределенный тип и доступ по нему невозможен, по сути, это указатель на структуру внутри библиотеки приложений для идентификации экземпляра библиотеки. Он всегда используется при вызове функций API фреймворка.
Первым делом создается структура приложения с типом hello_t. Она аллоцируется функцией FRMIL_Malloc() и присваивается указателю pThis (можно использовать и стандартный malloc(), но нужно прописать и стандартный заголовок stdlib.h). Структура объявлена в файле applets/hello/inc/hello.h:
typedef struct _hello
{
void* pHandle;
const char* pName;
frm_uint32_t uPID;
} hello_t;
Это основная структура для хранения данных приложения, в ней 3 поля: тот же указатель pHandle, куда надо сохранить указатель переданный в функцию, поле pName - имя приложения для логирования и поле uPID для сохранения публичного идентификатора приложения. Эти параметры и сохраняются в функции после создания структуры. Значение поля pName может быть получено вызовом функции FRMAPP_GetName() вместо предопределенного макроса APPLICATION_NAME. Это более информативное имя приложения. Значение поля uPID, как правило, не используется, а для remote приложений будет равно нулю, так что можно его удалить. Структура может содержать дополнительно любые поля, необходимые для работы приложения.
Следующий этап - заполнение регистрационной структуры и регистрация приложения. Структура frm_app_register_t объявлена в заголовочном файле public/inc/frm_app_if.h. Первое поле pAppData имеет тип неопределенного указателя и предназначено для хранения структуры приложения pThis. Это значение будет передаваться во все функции обратного вызова (коллбеки) для доступа к структуре приложения. Следующие 5 полей (pfnActive … pfnExit) - это указатели на коллбеки, они будут далее использованы для их вызова. Поля pCtrlRegTable и uCtrlRegSize определяют таблицу статических управляющих элементов в пользовательском интерфейсе. Таблица объявлена в заголовке приложения applets/hello/inc/hello.h:
/*! Application events enumeration */
enum
{
APP_EXIT_EVT = FRM_EVT_APP_START
};
/*! Application control entry registration table */
static const frm_app_ctrlreg_t control_entry[] =
{
{ APP_EXIT_EVT, FRM_APP_EVT_FROM, FRM_EVT_LBUTTON_UP_ID, "exit_button", NULL, NULL }
};
Тут, в первую очередь, объявлено перечисление событий от управляющих элементов, APP_EXIT_EVT - первый элемент перечисления, он всегда должен иметь значение FRM_EVT_APP_START или большее (оно объявлено в заголовке public/inc/frm_events.h и имеет значение 0x10000000). Таблица control_entry содержит запись о событии, которое расшифровывается так: как только на кнопке ‘exit_button’ произойдет событие отжатия (FRM_EVT_LBUTTON_UP_ID), то из фреймворка (FRM_APP_EVT_FROM) должно быть послано событие APP_EXIT_EVT. Данное событие обрабатывается Handle коллбеком приложения. Также в таблице могут быть прописаны события, отправляемые во фреймворк, они имеют направление FRM_APP_EVT_TO, событие фреймворка при этом определяется как нуль. Такие события используются для установки параметров, состояний и других операций с активными виджетами. Для кнопки с идентификатором ‘exit_button’ это выглядело бы так:
static const frm_app_ctrlreg_t control_entry[] =
{
{ APP_EXIT_EVT, FRM_APP_EVT_FROM, FRM_EVT_LBUTTON_UP_ID, "exit_button", NULL, NULL },
{ APP_EXIT_SET_EVT, FRM_APP_EVT_TO, 0, "exit_button", NULL, NULL }
};
Структура frm_app_ctrlreg_t определена в заголовке public/inc/frm_types.h. Подробно останавливаться на этой структуре я не буду, во фреймворке много примеров её использования.
Регистрация приложения выполняется функцией FRMAPP_Register( pHandle, &AppRegister ). Она использует полученный handle и заполненную структуру. Это важнейший этап в запуске приложения, при вызове этой функции происходит создание окон приложения и перевод состояния приложенияв в активное. Если регистрация не будет выполнена, то приложение будет выгружено с ошибками (есть таймаут на регистрацию).
После регистрации дальнейшие шаги опциональны, в данном случае происходит только инициализация API библиотеки (FRMLIBAPI_Init()) и распечатка лога.
Важное замечание: в функции запуска НЕЛЬЗЯ запускать функции API фреймворка за исключением указанных. Вызов в этой функции API повлечет непредсказуемые последствия. Я не буду вдаваться в подробности, но это будет плохой идеей :). Для дальнейшей инициализации приложения существует Activate callback.
Теперь рассмотрим реализацию на C++ (applets/hello_cpp/src/hello_cpp.cpp):
#include "hello_cpp.h"
namespace FRM {
namespace HELLO_CPP {
#ifndef APPNAME
#define APPNAME HelloCpp
#endif
extern "C" FrmResult APPNAME( void* pHandle, frm_uint32_t uArgc, char** ppArgv )
{
FrmResult result = FRMSUCCESS;
hello_cpp* pThis = NULL;
const char* pName = APPLICATION_NAME;
FRM_PCHECK( pHandle, pName, "NULL pointer to application handle\n" );
pThis = new hello_cpp( pHandle, uArgc, ppArgv );
FRM_PCHECK( pThis, pName, "Application class memory allocation failed\n" );
FRM_ERROR( pThis->result, pName, "Appliaction instance creation failed\n" );
FRMLIBAPI_Init();
FRM_DEBUG( pThis->pName, "The hello_cpp application is started (0x%08X)\n", pThis->uPID );
return result;
}
Тут по сути выполнятся тоже самое, только использован оператор new, и вообще все упрятано в код конструктора в заголовке:
class hello_cpp
{
public :
void* pHandle = {nullptr};
const char* pName = {nullptr};
frm_uint32_t uPID = {0};
frm_uint32_t uArgc = {0};
char** ppArgv = {nullptr};
FrmResult result = {FRMSUCCESS};
hello_cpp( void* pHandle, frm_uint32_t uArgc, char** ppArgv )
{
frm_app_register_t AppRegister = FRM_APP_REGISTER_INIT;
this->pHandle = pHandle;
this->pName = FRMAPP_GetName( pHandle );
this->uPID = FRMAPP_GetPID( pHandle );
this->uArgc = uArgc;
this->ppArgv = ppArgv;
AppRegister.pAppData = this;
AppRegister.pfnActive = hello_cpp_activate;
AppRegister.pfnHandle = hello_cpp_handle;
AppRegister.pfnSuspend = hello_cpp_suspend;
AppRegister.pfnResume = hello_cpp_resume;
AppRegister.pfnExit = hello_cpp_exit;
AppRegister.pCtrlRegTable = control_entry;
AppRegister.uCtrlRegSize = sizeof( control_entry ) / sizeof( frm_app_ctrlreg_t );
result = FRMAPP_Register( pHandle, &AppRegister );
FRM_ERROR_VOID( result, this->pName, "Application register failed\n" );
}
~hello_cpp()
{
}
};
Важное замечание: функция запуска объявлена с модификатором extern “C” - это говорит о том, что ЛИНКОВКА функции будет выполняться по правилам языка Си. Без этого оператора функция получит mangled name и не будет доступна из Си. В результате символ функции найден не будет, и приложение не запустится. На содержимое функции этот оператор не влияет.
Второй момент, конструктор не возвращает значение, поэтому в классе есть поле result, которое проверяется после вызова конструктора. Это может быть лишним, но что-то подсказывает, что это не так, хотя указатель на объект и проверяется.
И третий момент, использование namespace FRM - функции API, определенные в заголовке frm_cppapi.h, используют этот namespace. Этот заголовок использовать необязательно, можно и прямо использовать функции API, но мне показалось это неудобным. Функции, объявленные в заголовке, более приспособлены к реалиям C++.
А вот с Python все вообще просто (applets/hello_py/python/hello_py.py):
# Global application class definition
class HelloPy :
def __init__( self ) :
self.handle = 0
self.argc = 0
self.argv = []
self.name = ""
app = HelloPy()
# Control events enumeration
class AppEvents( Enum ):
APP_EXIT_EVT = FrmEvents.EVT_APP_START.value
APP_DUMMY_EVT = auto()
# Control registration table
control_entry = (
( AppEvents.APP_EXIT_EVT.value, FrmEvtDir.APP_EVT_FROM.value, FrmEvents.EVT_LBUTTON_UP.value, 'exit_button', '', '' ),
( AppEvents.APP_DUMMY_EVT.value, FrmEvtDir.APP_EVT_FROM.value, FrmEvents.EVT_LBUTTON_UP.value, 'dummy_button', '', '' ) )
# Launch callback
def app_launch( handle, argc, argv, name ) :
app.handle = handle
app.argc = argc
app.argv = argv
app.name = name
result = FrmResult.FRMSUCCESS.value
res = frmapi.api_init( app.handle, app.name )
if res != FrmResult.FRMSUCCESS.value :
result = res
return ( result, 0 )
print( f"Launch: HelloPy {app.name} : 0x{app.handle:X} : {app.argc} : {app.argv}" )
return ( result, len( control_entry ), control_entry )
Функция запуска - app_launch() - просто инициализирует Python API и возвращает кортеж со списком зарегистрированных событий. Объект класса HelloPy создается глобально, поскольку иначе никак. Имя функции изменить нельзя, всегда будет искаться фиксированное имя ‘app_launch’, как и имена для других коллбеков.
Регистрация приложения происходит в приложении pyrunner, оно получает кортеж и по нему формирует регистрационную таблицу. Также оно передает свои коллбеки, которые в свою очередь будут вызывать коллбеки в приложении на Python. Приложение pyrunner написало на Си и доступно в SDK.
На этом о функции запуска, наверное, все изложено.
Коллбеки Приложения
Коллбеки приложения предназначены для обработки информации, поступающей из фреймфорка. Всего их пять: Activate, Handle, Suspend, Resume и Exit. Обязательным является только Handle коллбек, без него графическое приложение теряет смысл. Остальные коллбеки опциональны, но на практике, обычно, используются все. Рассмотрим их все.
Activate коллбек
Как уже говорилось выше, функция запуска не позволяет вызывать функции API фреймворка. Для дополнительной инициализации приложения фреймворк и вызывает Activate коллбек. В процессе активации также происходит и инициализация канала связи внутри фреймворка (до вызова функции). С точки зрения фреймворка приложение полностью запущено только после фазы активации. При отсутствии коллбека будет просто возвращен успешный статус. В функции можно выполнять какие угодно операции: чтение переменных, установка состояний управляющих виджетов, создание динамических элементов - словом все, что требуется для работы приложения. На языке Си этот коллбек реализован так:
static FrmResult hello_activate( void* pApp )
{
FrmResult result = FRMSUCCESS;
hello_t* pThis = ( hello_t* )pApp;
FRM_DEBUG( pThis->pName, "Application activate (%s)\n", FRMAPP_GetName( pThis->pHandle ) );
return result;
}
Функция получает единственный параметр - указатель на структуру приложений, который приводится к нужному типу, чтобы получить доступ к структуре. Сделаем пару изменений в функции:
static FrmResult hello_activate( void* pApp )
{
FrmResult result = FRMSUCCESS;
hello_t* pThis = ( hello_t* )pApp;
FRM_DEBUG( pThis->pName, "Application activate (%s)\n", FRMAPP_GetName( pThis->pHandle ) );
result = FRMAPP_SetControlState( pThis->pHandle, APP_EXIT_SET_EVT, "DISABLED", NULL, FRM_TRUE );
FRM_ERROR( result, pThis->pName, "Exit button disabled state set failed\n" );
result = FRMAPP_SetControlState( pThis->pHandle, APP_EXIT_SET_EVT, "PRESSED", NULL, FRM_TRUE );
FRM_ERROR( result, pThis->pName, "Exit button pressed state set failed\n" );
return result;
}
В данном случае дважды вызвана функция установки состояния кнопки Exit, первый вызов переводит её в состояние “DISABLED”, а второй в состояние “PRESSED”. Для указания именно этой кнопки используется событие APP_EXIT_SET_EVT, которое должно быть прописано в регистрационной таблице (см. примеры выше). Также в функцию передается параметр pHandle, сохраненный в структуре приложения. Для возврата кнопки в исходное состояние можно сделать такой же вызов с параметром “IDLE”. Состояния кнопки “IDLE”, “PRESSED”, “DISABLE” определяются в описании виджета кнопки, в данном случае в файле resources/argo/system/fs0/sysres/controls/resp_button.fml:
<states>
<state id="IDLE" default="true">
<bitmap fill="bgcolor"/>
<shape figure="roundrect" bcolor="@BCOLOR_IDLE_DEF" fcolor="@FCOLOR_IDLE_DEF" thickness="@THICKNESS_DEF" radius="@RADIUS_DEF"/>
</state>
<state id="PRESSED">
<bitmap fill="bgcolor"/>
<shape figure="roundrect" bcolor="@BCOLOR_PRESS_DEF" fcolor="@FCOLOR_PRESS_DEF" thickness="@THICKNESS_DEF" radius="@RADIUS_DEF"/>
</state>
<state id="DISABLED">
<bitmap fill="bgcolor"/>
<shape figure="roundrect" bcolor="@BCOLOR_DIS_DEF" fcolor="@FCOLOR_DIS_DEF" thickness="@THICKNESS_DEF" radius="@RADIUS_DEF"/>
</state>
</states>
Остальные параметры функции FRMAPP_SetControlState() мы здесь рассматривать не будем, она объявлена в заголовке public/inc/frm_app_if.h.
В приложении С++ функция активации выглядит примерно так же (сразу добавил функции для изменения состояния кнопки):
static FrmResult hello_cpp_activate( void* pApp )
{
FrmResult result = FRMSUCCESS;
hello_cpp& rThis = *static_cast< hello_cpp* >( pApp );
frm_int32_t iStateID = -1;
FRM_DEBUG( rThis.pName, "Application activate (%s)\n", FRMAPP_GetName( rThis.pHandle ) );
result = set_control_state( rThis.pHandle, APP_EXIT_SET_EVT, "DISABLED", iStateID );
FRM_ERROR( result, rThis.pName, "Control state set failed\n" );
FRM_PRINT( "iStateID = %d\n", iStateID );
result = set_control_state( rThis.pHandle, APP_EXIT_SET_EVT, "IDLE" );
FRM_ERROR( result, rThis.pName, "Control state set failed\n" );
return result;
}
Сигнатура функции осталась прежней, но здесь указатель приводится к ссылке на структуру приложения. Также вызывается несколько другая функция для установки состояния, при первом вызове она возвращает еще и идентификатор состояния - iStateID. Функция объявлена в файле public/inc/c++/frm_cppapi.h:
/*!
* @fn FrmResult set_control_state( void* pHandle, frm_uint32_t uEvtID, const char* pStateName, frm_int32_t& rStateID, frm_bool_t bBlock = FRM_TRUE )
* @fn FrmResult set_control_state( void* pHandle, frm_uint32_t uEvtID, const char* pStateName, frm_int32_t* pStateID = nullptr, frm_bool_t bBlock = FRM_TRUE )
* @brief Set control state by name or identifier.
* @param pHandle - Pointer to application class instance (IN).
* @param uEvtID - Event identifier from control registration table (IN).
* @param pStateName - Pointer to state name to set (IN).
* @param rStateID - Reference to store state identifier (INOUT).
* @param pStateID - Pointer to store state identifier (INOUT).
* @param bBlock - Flag to perform blocked call if FRM_TRUE (IN).
* @return Operation result (FrmResult).
*/
static __inline FrmResult set_control_state( void* pHandle, frm_uint32_t uEvtID, const char* pStateName, frm_int32_t& rStateID, frm_bool_t bBlock = FRM_TRUE )
{
return FRMAPP_SetControlState( pHandle, uEvtID, pStateName, &rStateID, bBlock );
}
static __inline FrmResult set_control_state( void* pHandle, frm_uint32_t uEvtID, const char* pStateName, frm_int32_t* pStateID = nullptr, frm_bool_t bBlock = FRM_TRUE )
{
return FRMAPP_SetControlState( pHandle, uEvtID, pStateName, pStateID, bBlock );
}
Как видно, вызовы этой функции сводятся к вызову той же самой функции FRMAPP_SetControlState(), но пользоваться ей удобнее.
В приложении на Python функция выглядит так:
# Activate callback
def app_activate() :
result = FrmResult.FRMSUCCESS.value
print( f"Activate: HelloPy {app.name}" )
stid = -1
res = frmapi.set_control_state( AppEvents.APP_EXIT_SET_EVT.value, "IDLE", stid, FrmBool.FRM_FALSE.value )
result = frmapi.logger_error( res[0], "Control state set failed" )
if FrmResult.FRMSUCCESS.value != result :
return ( result, )
return ( result, )
В данном случае функция не получает параметров (структура приложения глобальная) и всегда возвращает кортеж с единственным параметром result. Возврат кортежа вместо просто значения связан с механизмом вызова через C-API в приложении pyrunner:
pResult = PyObject_CallObject( pThis->pActivateCB, NULL );
if ( NULL == pResult )
{
PyErr_Print();
result = FRMFAILURE;
FRM_ERROR( result, pThis->pName, "Activate callback call failed\n" );
}
result = pyrunner_get_result( pThis, pResult );
Функция PyObject_CallObject() всегда возвращает объект типа кортеж, поэтому вызываемая функция тоже должна вернуть кортеж, иначе будет ошибка.
В примере также вызывается функция set_control_state() с аналогичными параметрами, и, в конечном итоге, так же вызывается функция FRMAPP_SetControlState(). Однако путь тут длиннее. В Python для вызова функций из Си используется механизм ctypes. Вкратце, этот механизм загружает DLL с необходимой функцией, находит ее и вызывает. Функция FRMAPP_SetControlState() определена в заголовке, а не в DLL, поэтому напрямую вызываться не может. Поэтому потребовалась DLL с обертками функций API: support/libapi/dll/src/frm_libapi_dll.c:
/*!
* @fn FrmResult set_control_state( frm_uint32_t uEvtID, const char* pStateName, frm_int32_t* pStateID, frm_uint32_t uBlock )
* @brief Set control state by name or identifier.
* @param uEvtID - Event identifier from control registration table (IN).
* @param pStateName - Pointer to state name to set (IN).
* @param pStateID - Pointer to store state identifier (INOUT).
* @param uBlock - Flag to perform blocked call if FRM_TRUE (IN).
* @return Operation result (FrmResult).
*/
FrmResult set_control_state( frm_uint32_t uEvtID, const char* pStateName, frm_int32_t* pStateID, frm_uint32_t uBlock )
{
return FRMAPP_SetControlState( pHandle, uEvtID, pStateName, pStateID, ( frm_bool_t )uBlock );
}
Но и это еще не все. Сама вызываемая функция на Python объявлена в файле frmapi.py (лежит в папке argo-sdk/target/python/ и копируется в приложение в процессе установки):
# Set control state
def set_control_state( evtid, stname, stid, block ) :
libapi.set_control_state.argtypes = [ctypes.c_int, ctypes.c_char_p, ctypes.POINTER( ctypes.c_int ), ctypes.c_int]
libapi.set_control_state.restypes = ctypes.c_int
cname = None if stname is None else stname.encode( 'utf-8' )
cstid = ctypes.c_int( stid )
res = libapi.set_control_state( evtid, cname, ctypes.byref( cstid ), block )
return ( res, cstid.value )
Именно этот API и инициализируется в функции запуска приложения на Python. Не забываем, что имя функции на Python (app_activate) предопределено и не может меняться.
Много написал о функции активации :) Ну дальше будет проще.
Handle коллбек
Handle коллбек или попросту функция обработки вызывается всякий раз, когда в пользовательском интерфейсе происходит какое-либо зарегистрированное событие: нажатие на элементы управления, ввод текста, нотифицируемые события и тому подобное. На языке Си функция выглядит так:
static FrmResult hello_handle( void* pApp, frm_uint32_t uMsg, frm_uint32_t uFirstParam, frm_uint32_t uSecondParam, void* pData )
{
FrmResult result = FRMSUCCESS;
hello_t* pThis = ( hello_t* )pApp;
FRM_DEBUG( pThis->pName, "Application handle (0x%X) message\n", uMsg );
( void )uFirstParam;
( void )uSecondParam;
( void )pData;
return result;
}
Помимо указателя на структуру приложения, функция принимает еще 4 параметра: идентификатор сообщения uMsg, первый и второй параметр сообщения - uFirstParam и uSecondParam и неопределенный указатель на структуру данных сообщения. Идентификатор сообщения используется всегда, это основной параметр. Первый параметр используется редко, второй не используется почти никогда, он содержит некую информацию, но для приложения бесполезную (может быть, некоторые события и используют его). Структура данных сообщения используется по результатам анализа первых двух параметров, там передаются данные разных типов и необходимо приведение к нужному типу. Может передаваться и нулевой указатель.
В новом приложении есть кнопка Exit и её события прописаны. Однако кнопка не работает, хотя событие по отжатию кнопки приходит в функцию обработки. Чтобы сделать кнопку рабочей добавим небольшой кусочек кода в функцию:
static FrmResult hello_handle( void* pApp, frm_uint32_t uMsg, frm_uint32_t uFirstParam, frm_uint32_t uSecondParam, void* pData )
{
FrmResult result = FRMSUCCESS;
hello_t* pThis = ( hello_t* )pApp;
FRM_DEBUG( pThis->pName, "Application handle (0x%X) message\n", uMsg );
switch ( uMsg )
{
case APP_EXIT_EVT :
FRMAPP_CloseApp( pThis->pHandle, 0, FRM_FALSE );
break;
default :
break;
}
( void )uFirstParam;
( void )uSecondParam;
( void )pData;
return result;
}
По событию APP_EXIT_EVT должна выполниться функция FRMAPP_CloseApp(), эта функция закрывает приложение. Мы не проверяем, что она вернула, в данном случае она всегда вернет успешный статус. Происходит это, потому что приложение посылает запрос на закрытие самого себя (приложение само закрыться не может, закрывает его только фреймворк). Это определяется вторым параметром функции, который в вызове нуль. Вообще этот параметр - это публичный идентификатор приложения. Если параметр валидный, то функция будет вызвана в блокирующем варианте и вернет успех, если приложение закрыто (если 0, то вызов будет не блокирующим и функция возвращает успех, не дожидаясь фактического выполнения). Функция с валидным значением параметра используется для закрытия других приложений, но на это нужна системная привилегия. Даже зная собственный идентификатор, приложение не сможет себя закрыть (кому тогда возвращать статус операции?). Кроме того, приложения с определенной привилегией вообще не могут закрыть сами себя - так сделаны все системные сервисы и закрыть их может только приложение с отладочной привилегией (degug shell). Третий параметр - это флаг форсированного закрытия, мы к нему еще вернемся. Пока обычный запрос на закрытие.
Если посмотреть на параметр uFirstParam, то можно обнаружить, что его значение равно значению FRM_EVT_LBUTTON_UP_ID, т.е. оригинальному событию что случилось с кнопкой. Произошло ее отжатие. Для зарегистрированных событий это всегда так, значение равно фактическому событию с управляющим виджетом. Значение второго параметра нас, в общем-то, не интересует, но его значение 0xB000002 совпадает с идентификатором внутреннего события (FRM_EVT_APP_CONTROL_ID), которое транслируется в событие APP_EXIT_EVT (никто ведь не думает, что события приложений обрабатываются фреймворком, верно? :)). Внутренние события не определены в открытом API. Не пустует и параметр pData - в нем валидный указатель на структуру frm_evt_stchange_t (определена в заголовке public/inc/frm_events.h). На данный момент мы эту структуру рассматривать не будем, тема требует отдельной главы. Достаточно сказать, что при приходе зарегистрированных в таблице событий и соответствующего первого параметра (FRM_EVT_LBUTTON_UP_ID, FRM_EVT_LBUTTON_DOWN_ID и некоторых других) будет эта структура (впрочем тип структуры можно определить по её контенту, что правильней).
В случае приложения на С++ ничего нового не прибавится:
static FrmResult hello_cpp_handle( void* pApp, frm_uint32_t uMsg, frm_uint32_t uFirstParam, frm_uint32_t uSecondParam, void* pData )
{
FrmResult result = FRMSUCCESS;
hello_cpp& rThis = *static_cast< hello_cpp* >( pApp );
FRM_DEBUG( rThis.pName, "Application handle (0x%X) message\n", uMsg );
switch ( uMsg )
{
case APP_EXIT_EVT :
close_app( rThis.pHandle );
break;
default :
break;
}
( void )uFirstParam;
( void )uSecondParam;
( void )pData;
return result;
}
Просто функция close_app() не содержит дополнительных и ненужных в данном случае параметров. Все остальное как и было рассказано выше.
В приложении на Python тоже все прозрачно, добавить тут нечего:
def app_handle( msg, first_param, second_param, data ) :
result = FrmResult.FRMSUCCESS.value
print( f"Handle: HelloPy {app.name} : 0x{msg:X} : {first_param:X} : {second_param:X} : {data}" )
if msg == AppEvents.APP_EXIT_EVT.value :
result = frmapi.close_app( 0, FrmBool.FRM_FALSE.value )
else :
print( f"Unknown event: 0x{msg:X} : {first_param:X} : {second_param:X}" )
return ( result, )
Suspend коллбек
Suspend коллбек вызывается в следующих случаях: закрытие приложения, минимизация окна приложения, приостановка работы приложения (последнее не реализовано). Реализация функции на Си ниже:
static FrmResult hello_suspend( void* pApp, frm_suspend_t eMode )
{
FrmResult result = FRMSUCCESS;
hello_t* pThis = ( hello_t* )pApp;
FRM_DEBUG( pThis->pName, "Application suspend (%s): %u\n", FRMAPP_GetName( pThis->pHandle ), eMode );
return result;
}
Ключевой моментs здесь в параметре eMode и возвращаемом значении. Параметр eMode может принимать 3 значения: FRM_SUSPEND_EXIT - выгрузка приложения, FRM_SUSPEND_APP - приостановка приложения и FRM_SUSPEND_MINIMIZE - cворачивание окна приложения. Не буду касаться последних двух режимов, один не работает (FRM_SUSPEND_APP), другой пока не используется (FRM_SUSPEND_MINIMIZE). Вот режим FRM_SUSPEND_EXIT интересен, он часто используется в приложениях. Приложение на момент выгрузки может содержать несохраненные данные, а Exit коллбек тоже не может вызывать API фреймворка.
Перед выгрузкой приложения фреймворк вызывает эту функцию с параметром FRM_SUSPEND_EXIT и ожидает результат её выполнения. Если вызов успешен (или коллбека просто нет), то происходит выгрузка приложения. Если же функция возвращает статус FRMREJECT (это специальный статус), то выгрузка приложения отменяется. Разработчик может сам вернуть такой статус, но это не требуется. При вызове почти любой функции API фреймворка библиотека приложений сразу вернет такой статус (есть маленький набор функций вроде FRMAPP_GetName(), которые не вызывают отправку такого статуса). Сделан такой механизм специально - менеджер приложений заблокирован вызовом этого коллбека до получения статуса, любой вызов API и отправка статуса разблокируют менеджер, что позволит выполнить необходимые операции. Если есть данные, которые надо сохранить, то они просто сохраняются, приложение не будет выгружено.
Вот теперь подошли к важному моменту. Данные-то сохранены, но и приложение не выгружено. Можно повторить выгрузку снова, но есть способ сделать это автоматически, а именно воспользоваться функцией FRMAPP_CloseApp():
static FrmResult hello_suspend( void* pApp, frm_suspend_t eMode )
{
FrmResult result = FRMSUCCESS;
hello_t* pThis = ( hello_t* )pApp;
FRM_DEBUG( pThis->pName, "Application suspend (%s): %u\n", FRMAPP_GetName( pThis->pHandle ), eMode );
/*
* Do something
*/
FRMAPP_CloseApp( pThis->pHandle, 0, FRM_TRUE );
return result;
}
Функция FRMAPP_CloseApp() вызвана с флагом форсированной выгрузки (FRM_TRUE в третьем параметре). Получив такой запрос, менеджер приложений не будет вызывать suspend коллбек, а просто закроет приложение.
При сворачивании окна приложения функция будет вызвана с параметром FRM_SUSPEND_MINIMIZE. При этом не играет роли, что она возвращает, это просто уведомление для приложения, что окно свернуто. Можно сделать какие-либо операции в этом случае, например, остановить таймеры приложения или что-нибудь подобное. Примеров использования функции в этом режиме пока нет.
Я не буду приводить тут примеры кода для C++ и Python, ничего нового там нет.
Resume коллбек
Эта функция предназначена для восстановления состояния приложения при разворачивании окна или после приостановки. Пока примеров использования этой функции нет, хотя она вызывается при разворачивании окна. Реализация функции на Си ниже:
static FrmResult hello_resume( void* pApp )
{
FrmResult result = FRMSUCCESS;
hello_t* pThis = ( hello_t* )pApp;
FRM_DEBUG( pThis->pName, "Application resume (%s)\n", FRMAPP_GetName( pThis->pHandle ) );
return result;
}
Exit коллбек
Функция выхода - это финальная часть работы приложения, в ней выполняются операции по освобождению памяти, остановке и удалению таймеров и тому подобное. В этой функции нельзя вызвать API фреймворка - для менеджера приложений данного приложения уже не существует, сама функция вызывается при выгрузке библиотеки приложений. Все, что требуется, должно быть вызвано из suspend коллбека. На языке Си функция реализована так:
static FrmResult hello_exit( void* pApp )
{
hello_t* pThis = ( hello_t* )pApp;
FRM_DEBUG( pThis->pName, "Application exit\n" );
FRMIL_Free( ( void* )pThis, sizeof( hello_t ) );
pThis = NULL;
return FRMSUCCESS;
}
Как видно тут произходит освобождение памяти под структуру приложения. Как и в случае аллокации памяти можно просто воспользоваться функцией free(). В С++ также можно воспользоваться оператором delete. В приложении на языке Python вообще ничего не делается, структура приложения объявлена глобально.
Общие замечания по коллбекам приложения
Все коллбеки должны возвращать статус успеха (FRMSUCCESS), за исключением suspend функции, которая может вернуть статус FRMREJECT, который ошибкой не является. Любой другой статус вызывает ошибку и вывод окна системного предупреждения.
В статических и динамических приложениях коллбеки вызываются под механизмом обработки исключений для того, чтобы предотвратить креш фреймворка в случае возникновения исключения. В remote приложениях это не делается, приложение просто покрешится, фреймворк продолжит работу.
Предполагается, что код приложения способен работать для всех вариантов сборки (за исключением приложений на Python - они работают только в remote режиме). Вообще приложение не имеет информации, в каком режиме запущено, и может даже одновременно запускаться под разными режимами. Правило выбора между вариантами просто: если указан исполняемый файл, то приложение запустится как remote, независимо от других возможных режимов. Если указана динамическая библиотека, а исполняемый файл нет, то приложение запускается в динамическом варианте. Если не указаны ни библиотека, ни исполняемый файл, то приложение запустится как статическое, разумеется, если оно слинковано с фреймворком. Отладочное приложение позволяет запускать приложение в любом доступном режиме.
Описания Пользовательского Интерфейса
Описание пользовательского интерфейса практически идентично для всех вариантов языка, на котором написана логическая часть. В каркасе приложения есть только два файла с описанием, для приложения hello это файлы applets/hello/resources/hello_root.fml и applets/hello/resources/hello_resource.fml. Первый, корневой файл, определяет наполнение окна приложения и подключает второй файл с описанием ресурсов.
Корневой файл интерфейса
Корневым файл называется, потому что все описания пользовательского интерфейса начинаются в нем, и нет более высокого уровня описаний. Пример файла applets/hello/resources/hello_root.fml представлен ниже:
<approot id="hello_root">
<window id="hello_window" dx="75" dy="75" bgcolor="0x080408" valign="center" halign="center" title="$HELLO_TITLE_ID">
<!-- Window style definitions -->
<style default="true">
<menubutton x="5" y="5" dx="25" dy="25" resid="$SYSONLY.$STYLE_BUTTON_ID">
<variable name="PRESSED_ID" vtype="string" value="$APPONLY.$HELLO_ICON_ID"/>
<variable name="IDLE_ID" vtype="string" value="$APPONLY.$HELLO_ICON_ID"/>
</menubutton>
<maxbutton />
<menubar />
<titlelabel />
</style>
<page id="main_page" dx="100" dy="100" bgcolor="0xFFFFFF">
<control id="exit_button" resid="!RESP_BUTTON" x="(@WIDTH - 90)" y="(@HEIGHT - 35)" dx="80" dy="25" scale="false">
<variable name="TEXT_DEF" vtype="string" value="$APP.$EXIT_BUTTON_TITLE_ID"/>
</control>
</page>
</window>
<!-- Resource definitions -->
<resource id="hello_res">
<resfile id="hello_resource" src="hello_resource.fml" btype="fml"/>
</resource>
</approot>
Описание начинается с ноды ‘approot’, это корневая нода приложения, и у нее нет родительской ноды. У нее есть единственный атрибут ‘id’ - он обязателен, но пока нигде не используется. Вообще у многих нод такой параметр есть, и они активно используются. Значения всех атрибутов проверяются - записать туда абы что не получится, вызовет ошибку. Для атрибута ‘id’ правила просты - использовать только буквы, цифры и символы подчеркивания, причем начинаться и заканчиваться должны только с буквы.
Нода ‘approot’ определяет две секции: описание окна, начинающееся с ноды ‘window’, и описание ресурсов под нодой ‘resource’. Доступно также описание переменных приложения под нодой ‘database’ (это взято из другого приложения):
<!-- Database definitions -->
<database id="testapp_cpp_dbase">
<variable name="APPNAME" vtype="string" value="TestAppCpp" perm="constant"/>
</database>
Все суб-ноды ‘approot’ могут быть в нескольких экземплярах, но только нода ‘window’ обязательна (это может быть пересмотрено в будущем). Идентификаторы суб-нод должны быть уникальными в пределах родительской ноды (это правило соблюдается не для всех нод, но в данном случае это так).
Нода ‘window’ содержит ряд атрибутов, определяющих размеры окна, его положение, цвет подложки и статический заголовок окна.
Аттрибуты ‘dx’ и ‘dy’ определяют ширину и высоту окна соответственно, в данном случае в процентах, от размера суб-дисплея, в котором окно расположено (как определяются суб-дисплеи и в каком окно расположено - уже было где-то, вроде, рассказано). Тут будет 75% от размера дисплея. Могут быть и другие атрибуты - ‘x’ и ‘y’, но они не будут работать из-за установки атрибутов выравнивания (‘valign’ и ‘halign’). Может быть еще атрибут ‘scale’ со значением ‘true’ или ‘false’, который определяет в каких единицах измеряются координаты. В данном случае атрибут по умолчанию ‘true’, и это определяет размеры в процентах. Если его значение ‘false’, то значения интерпретируются в размеры в пикселах. Это очень широко распостраненный набор параметров, очень многие ноды используют эти атрибуты. Все эти атрибуты опциональны, их отсутствие приведет лишь только к тому, что окно будет занимать 100% суб-дисплея (или в данном случае дисплея).
Атрибуты ‘valign’ и ‘halign’ определяют выравнивание окна на дисплее, в данном случае, по центру. Это тоже очень распостраненные атрибуты в описаниях. Значения этих атрибутов - перечислимое (enum). Для атрибута ‘valign’ доступны значения ‘top’, ‘center’, ‘bottom’. Для атрибута ‘halign’ - ‘left’, ‘center’ и ‘right’. А вот значения по умолчанию варьируются от ноды к ноде, часто выбираются значения верхний/левый. Если значения атрибутов указано, то параметры ‘x’, ‘y’ игнорируются. Как правило, это опциональные атрибуты.
Атрибут ‘bgcolor’ задает цвет фона окна, значение ‘0x080408’ определяет специальный “прозрачный” цвет. Пикселы, выкрашенные в такой цвет, не отрисовываются на подложке (это касается отрисовки виджетов, изображения рисуются нормально). Пока, к сожалению, цвета можно задавать только числами в формате ARGB. По умолчанию цвет окна черный (0x00000000).
Атрибут ‘title’ определяет статический заголовок окна. Это опциональный атрибут, заголовок окна можно определить и в стиле. Стили содержат два виджета для заголовка - статический и динамический. Статический заголовок используется в случае, когда не предполагается его менять в процессе работы приложения. Если смена заголовка предполагается, то надо воспользоваться динамическим заголовком. Чтобы воспользоваться динамическим заголовком, надо сделать статичекий заголовок пустым - в качестве параметра подставить пустую строку " " (пробел в строке обязателен). Заголовок окна можно прописать текстом, но лучше воспользоваться ресурсным идентификатором, как здесь и сделано (маркер $ определяет что это идентификатор ресурса).
Есть еще два опциональных атрибута окна: ‘wndstate’ и ‘orientation’. Первый определяет состояние окна, по умолчанию он ‘normal’, окно видимо. В некоторых случаях используется состояние ‘hidden’, окно скрыто, такой прием используется для приложений-сервисов, окна которых скрыты до прихода запроса. Можно еще сделать ‘minimized’, но это не имеет смысла. Атрибут ‘orientation’ хоть и есть, но не используется. Пока механизмов смены ориентации окон нет.
Нода ‘style’ определяет стиль окна - границы окна, заголовок, элементы управления и их положение. Вообще стили определяются в корневом конфигурационном файле resources/argo/system/argo_system.fml, но определение стиля в файле приложения позволяет его сконфигурировать как угодно разработчику приложения. Если её не указывать, то будет применен стиль по умолчанию, так как есть, что не всегда удобно. Чтобы вообще отменить стиль надо его определить так: <style />. Так убран стиль в приложении Idle Screen.
Нода имеет два атрибута: ‘id’ и ‘default’. В данном случае определен только атрибут ‘default’, что определяет выбор стиля по умолчанию, идентификатор тут не нужен. Если надо определить другой стиль то надо указать его идентификатор:
<!-- Window style definitions -->
<style id="space_blue_nt">
<wndborder resid="!WND_BORDER_NT" offsets="!WND_BORDER_NT_OFF"/>
</style>
Этот стиль определяет что у окна нет ни заголовка, ни кнопок управления, но определен бордер окна. Так определен стиль в приложении applets/fileview/resources/fileview_root.fml. В нашем случае бордер берется из стиля по умолчанию.
Рассмотрим, что делается внутри ноды ‘style’. Первым делом переопределяется кнопка menubutton:
<menubutton x="5" y="5" dx="25" dy="25" resid="$SYSONLY.$STYLE_BUTTON_ID">
<variable name="PRESSED_ID" vtype="string" value="$APPONLY.$HELLO_ICON_ID"/>
<variable name="IDLE_ID" vtype="string" value="$APPONLY.$HELLO_ICON_ID"/>
</menubutton>
В этой записи определяется позиция и ресурс кнопки, также переопределяются две переменные кнопки (resources/argo/system/fs0/sysres/controls/style_button.fml). Определение кнопки сильно напоминает определение обычного управляющего элемента, но есть отличия: отсутствует идентификатор (сама нода menubutton и есть идентификатор) и нельзя использовать атрибут ‘scale’, координаты всегда задаются в пикселах. Для переопределения переменных используется имя, тип и новое значение, все параметры обязательны. Если имя или тип переменной не совпадают с существующей переменной, то переопределение работать не будет. Значение, задаваемое атрибутом ‘value’, должно соответствовать типу переменной, иначе будет ошибка. Типы переменных могут быть ‘string’, ‘integer’ или ‘enum’.
В определениях ресурсных идентификаторов почти всегда используются префиксы ‘SYSONLY’, ‘APPONLY’, ‘SYS’ и, реже, ‘APP’. Эти префиксы определяют домен ресурса (‘SYSONLY’, ‘APPONLY’) или порядок поиска в обоих доменах (‘SYS’, ‘APP’). Первые два определяют системный домен или домен приложения, и поиск выполняется именно там. Другие определяют первый домен, и, если ресурс не найден, то ищется во втором. Это позволяет подменять ресурсы при необходимости. Если префикс не указан, то это эквивалентно префиксу ‘APP’.
Есть еще более сложная запись вида $prefix.$group.$resname. В данном случае под группой понимается идентификатор группы ресурсов, но это нигде пока не используется. Маркер $ может быть необязательным в некоторых случаях, когда точно известно, что это именно идентификатор ресурса (например, атрибут ‘resid’ не принимет ничего кроме идентификатора). Однако это плохая практика и я бы не рекомендовал её использовать.
Системные ресурсы определяются в файле resources/argo/system/fs0/sysres/window_default.fml, ресурсы приложения в файле applets/hello/resources/hello_resource.fml.
Некоторые элементы стиля в приложении не нужны, и их надо убрать. Это делается следующими строчками:
<maxbutton />
<menubar />
<titlelabel />
Данные записи удаляют лишние элементы стиля: кнопку максимизации окна (механизм максимизации не работает), меню и динамический заголовок. Само собой, в другом приложении они и потребуются, но здесь лишние.
Следующая секция определяет страницу окна:
<page id="main_page" dx="100" dy="100" bgcolor="0xFFFFFF">
<control id="exit_button" resid="!RESP_BUTTON" x="(@WIDTH - 90)" y="(@HEIGHT - 35)" dx="80" dy="25" scale="false">
<variable name="TEXT_DEF" vtype="string" value="$APP.$EXIT_BUTTON_TITLE_ID"/>
</control>
</page>
Нода ‘page’ имеет те же атрибуты, как и нода ‘window’, за исключением заголовка, состояния и ориентации (есть ещё атрибут pgtype, но пока отложим этот вопрос). Тут указаны ширина, высота и цвет страницы. Ширина и высота в 100% делают ненужными опции выравнивания и начальные координаты (‘x’, ‘y’). Но это другие ширина и высота. Окно само по себе тоже страница, но это еще и контейнер для других страниц. Иначе говоря, окно выделяет клиентскую область для вложенных страниц, и это определяется параметрами стиля. Наличие оконного бордера, меню, ограничивают доступную область, они не должны перекрываться. Область, свободная от этих элементов, и является клиентской областью. Поэтому страница занимает 100% от клиентской области. Страницы, вообще говоря, являются опциональным элементом, могут быть приложения без страниц.
В данном примере только создается управляющий элемент - кнопка с названием ‘exit_button’. Указывается ресурсный иденитфикатор, размер и позиция кнопки. Также переопределяется переменная, которая задает название кнопки. Кнопка с названием ‘exit_button’ уже фигурировала раньше, вот отсюда и ее название. Ресурсный идентификатор задан с использованием маркера !, что говорит о том, что ресурс берется из темы (resources/argo/system/fs0/sysres/themes/midnight_theme.fml или resources/argo/system/fs0/sysres/themes/darkgreen_theme.fml). Параметры темы обычно фиксированы, есть и кастомные параметры, но это не тот случай. В темах фреймворка это кнопка с ресурсным идентификатором $RESP_BUTTON_ID (resources/argo/system/fs0/sysres/controls/resp_button.fml). Обратите внимание на отсутствие префикса - если такой ресурс будет объявлен в приложении, то он и возьмется.
Особое внимание надо обратить на вычисление координат, в них фигурируют параметры @WIDTH и @HEIGHT. Это специальные переменные (переменные маркируются символом @), которые определяют ширину и высоту виджета, в данном случае страницы. Это автоматические переменные, их значения нельзя переопределять (хотя и нет на это ограничения, надо бы добавить). Они хранят ширину и высоту виджета в пикселах, поэтому в списке атрибутов стоит атрибут scale=“false”. Выражения для вычисления заключены в скобки, это способ дать понять фреймворку, что выражение надо вычислять. Он это делать умеет, но не очень хорошо, есть определенные баги, связанные с приоритетом операций. Поэтому при сложных вычислениях надо ставить скобки при выполнении любой операции в соответствии с приоритетом.
С переопределением переменной TEXT_DEF, надеюсь, все понятно.
Следующая секция определяет ресурсы приложения, точнее файлы, в которых ресурсы прописаны:
<!-- Resource definitions -->
<resource id="hello_res">
<resfile id="hello_resource" src="hello_resource.fml" btype="fml"/>
</resource>
В данном случае прописан только один файл hello_resource.fml, путь до этого файла относительный к пути до корневого файла, а поскольку он рядом, то путь не указывается. Тип файла определен как ‘fml’, поскольку он имеет текстовое описание. Файл ресурсов может быть и бинарным, в этом случае тип файла должен быть ‘fbin’ или ‘fbine’. Содержимое этого файла будет рассмотрено в следующем топике. Под этой нодой можно прописать несколько файлов описания ресурсов.
При описании ресурсных файлов могут использоваться и дополнительные атрибуты: ‘rgb’, ‘lang’ и ‘theme’. Атрибут ‘rgb’ определяет кодировку графических ресурсов в данном файле, доступные значения: ‘rgb565’, ‘rgb888’ и ‘none’. По умолчанию стоит ‘none’ и ресурсы загружаются с кодировкой, указанной в запросе. Если указана другая кодировка, то с указанной, а значение в запросе игнорируется. Этот атрибут используется в основном в описании системных ресурсов. Атрибут ‘lang’ широко используется в приложениях, если надо сделать поддержку языков, например, в приложении настроек эта секция выглядит так:
<!-- Resource definitions -->
<resource id="settings_res">
<resfile id="settings_resource" src="settings_resource.fml" btype="fml"/>
<resfile id="settings_lang" src="settings_res_en.fml" btype="fml" lang="en_US"/>
<resfile id="settings_lang" src="settings_res_ru.fml" btype="fml" lang="ru_RU"/>
<resfile id="settings_lang" src="settings_res_bel.fml" btype="fml" lang="be_BE"/>
</resource>
Обратите внимание, что идентификаторы в трех файлах с этим атрибутом идентичны - ‘settings_lang’. Это тот случай, когда идентификаторы совпадают, как говорилось выше. Дело в том, что менеджер ресурсов подгружает только тот файл, где параметр атрибута ‘lang’ совпадает с языком системы, остальные игнорируются. При смене языка, разумеется, меняется и файл. В этих файлах идентификаторы текстовых ресурсов идентичны, а вот их значения зависят от выбранного языка. При смене языка происходит перепроверка текстовых ресурсов и, если на один и тот же идентификатор подгружается другой ресурс, то происходит замена текущего ресурса на новый. Это и обеспечивает смену языка в приложениях.
Атрибут ‘theme’ работает аналогично, но влияет на графические ресурсы, используемые в разных темах. Он, как правило, используется в системных ресурсах:
<!-- Resource definitions (Main resource storage) -->
<resource id="sysres_1" filesys="fs0" path="sysres">
<resfile id="window" src="window_default.fbin" rgb="rgb888" btype="fbin"/>
<resfile id="window_ext" src="window_default_ext.fbine" rgb="rgb888" btype="fbine"/>
<resfile id="strings" src="string_res_en.fbin" btype="fbin" lang="en_US"/>
<resfile id="strings" src="string_res_ru.fbin" btype="fbin" lang="ru_RU"/>
<resfile id="strings" src="string_res_bel.fbin" btype="fbin" lang="be_BE"/>
<resfile id="themeres" src="theme_res_blue.fbin" btype="fbin" theme="midnight"/>
<resfile id="themeres" src="theme_res_green.fbin" btype="fbin" theme="darkgreen"/>
</resource>
Обратите внимание, что в данном случае используются бинарные ресурсы. В последних версиях SDK везде используются только бинарные файлы, как оказалось это удобнее.
Теперь перейдем к файлу описания ресурсов.
Файл описания ресурсов
Ниже представлено содержимое файла applets/hello/resources/hello_resource.fml:
<bin file="hello_resource">
<res name="HELLO_TITLE_ID" string="Hello World!!!" type="utf8"/>
<res name="EXIT_BUTTON_TITLE_ID" string="Exit" type="utf8"/>
<res name="HELLO_ICON_ID" src="images/hello_icon_25_25.bmp" type="bmp"/>
</bin>
Под нодой ‘bin’ находится единственный атрибут ‘file’, значение которого определяет имя бинарного файла, который должен быть сгенерирован из этого файлв. Дальше идет описание ресурсов. Атрибут ‘name’ определяет идентификатор ресурса, идентификатор должен быть уникальным в файле. Текстовые ресурсы определяются атрибутом ‘string’ и типом ‘utf8’ (или ‘text’, разницы никакой). Графические ресурсы определяются атрибутом ‘src’, где указывается путь до файла относительно корневого описания и типом ‘bmp’ в данном случае. Типов существует много, и не все поддерживаются. Поддерживаются следующие типы ресурсов:
- ‘utf8’ - текстовый ресурс
- ‘text’ - текстовый ресурс
- ‘bmp’ - файлы в формате BMP
- ‘src’ - FML файлы описаний управляющих виджетов
- ‘control’ - бинарные файлы описаний управляющих виджетов
- ‘font’ - бинарные описания шрифтов
- ‘dll’ - управляющие dll для пользовательских (custom) управляющих виджетов (виджеты с custom behavior)
Последний тип встречается крайне редко, он необходим для специальных dll, которые подключаются к виджетам с пользовательским поведением. У абсолютного большинства управляющих виджетов поведение предопределено, и специальная библиотека не требуется.
Менеджер ресурсов использует тип ресурса для правильной его загрузки.
Количество ресурсов в файле ограничено 1024 записями, но количество файлов хоть и ограничено, но очень большое.
На этом экскурс по описаниям пользовательского индерфейса я закончу. Это очень обширная тема и уместить её в одну статью не представляется возможным. В отдельных главах будут рассмотрены наиболее часто встречающиеся задачи по созданию интерфейса.
Makefile Приложения
В данном разделе рассмотрим makefile приложения. Будем рассматривать его на примере C++ приложения, а что не нужно для Си, то будет указано отдельно. Для приложений на Python makefile вообще нет. Называется он для всех компонент одинаково - component.mk, это основной makefile приложения. Скрипт appmake.sh создает также файл, который так и называетмя - Makefile, но он нам не интересен, рассматривать его не будем. Код файла component.mk представлен ниже:
# @brief Hello World!!! application makefile.
# @file component.mk
# @author andrey
# Component name (Project name is default)
NAME = hello_cpp
# Component type
EXECUTABLE = y
SHARED_LIB = n
STATIC_LIB = n
SHARED_STATIC_LIB = n
SHARED_DLL = y
DEP_LIST := integration integration/logger support/libapi
# Set install path
INSTALL_PATH = /applets/hello_cpp
# Set verbose mode
VERBOSE = n
# Set C++ standard
STD = c++17
include $(CONFIGS)/config.mk
# Get list of source files
CSRC = \
$(wildcard ${WORKSPACE}/${PROJECT}/src/*.cpp)
# Get list of include paths
INC_PATH = \
$(WORKSPACE)/$(PROJECT)/inc \
$(WORKSPACE)/public/inc \
$(WORKSPACE)/public/inc/c++
# Get required libraries
libs-shared-y =
libs-shared-$(FRM_CONFIG_API_LIB) += :libfrmapi.so
libs-shared-$(FRM_CONFIG_LOGGER_LIB) += :libfrmlogger.so
libs-shared-$(FRM_CONFIG_IL_LIB) += :libfrmil.so
SHARED_LIBS = $(libs-shared-y)
libs-exe-y =
libs-exe-$(FRM_CONFIG_APP_LIB) += :libfrmrmtapp.so
libs-exe-$(FRM_CONFIG_SUPPL_LIB) += :libfrmsuppl.so
SHARED_LIBS_EXE = $(libs-exe-y)
libs-platform-y += :libstdc++.so
LIBS = $(libs-platform-y)
CCFLAGS_EXE += -DAPPNAME=appmain
include $(CONFIGS)/common.mk
Первая запись - component name - определяет имя для файла, который будет построен без учета расширений и префиксов. Для исполняемых файлов он и останется как есть, для библиотек добавится префикс lib и расширение .so, для DLL только расширение .so, для статических библиотек префикс lib и расширение .a, для shared-static библиотек префикс lib и суффикс S.a. Если его не задать, то будет взято имя проекта, а оно у нас applets/hello_cpp - я полагаю что это приведет к ошибке. Нас интересуют имена исполняемого файла, DLL и статической библиотеки. Остальные варианты для приложений не применимы.
Следующая запись: Component type. Тип компонента определяется символом ‘y’ (yes) для выбранного типа. В данном случае выбраны исполняемый файл (EXECUTABLE) и DLL (SHARED_DLL), можно еще выбрать вариант статической библиотеки (STATIC_LIB).
Далее идет список зависимостей - DEP_LIST. Компоненты указанные в нем должны быть построены раньше, чем будет построено наше приложение. Тут есть некоторые тонкости, нельзя переносить эту строку, и нельзя заменить оператор присваивания :=. Здесь должны быть перечислены проекты SDK, которые требуются приложению. Например, если требуется библиотека libfrmeditor.so, то она должна быть добавлена в строку как support/libeditor.
Установочный путь INSTALL_PATH определяет, где будут размещены артефакты постройки в папке build, например, в данном случае это будет папка build/linux/x86_64/sdk/debug/install/applets/hello_cpp/. Переопределять этот путь не нужно, он фигурирует в скрипте установки приложения install.sh, что позволяет установить приложение быстро и легко. Для системного приложения путь может быть переопределен для общего соответствия, но необязательно.
Установка режима verbose была предназначена для вывода командной строки компилятора и линкера, но я так постарался, чтобы они не выводились, что даже преуспел :) В общем, вывести командную строку довольно нетривиальная задача, и просто установкой этой переменной в значение y (yes) это не решается. Так что пока отложим.
Установка стандарта языка C++ применима только к C++ приложениям, для Си стандарт не устанавливается. Может, это и неправильно, но это так. Можно установить любой стандарт, который поддерживается компилятором.
Инклюд файла config.mk обеспечивает настройки указанные выше, это обязательный параметр.
Список исходных файлов устанавливается в переменной CSRC, в данном случае возьмутся все .cpp файлы, лежащие в папке src приложения. Для приложений на языке Си расширения должны быть .c. Для C++ приложений доступно только расширение .cpp, файлы с другим расширением не скомпилируются. Переменная WORKSPACE задает путь до вашего рабочего пространства, а переменная PROJECT - это путь до вашего приложения (applets/hello_cpp, например).
Переменная INC_PATH содержит пути для заголовочнных файлов, они используются также для создания зависимостей при сборке. Путь $(WORKSPACE)/public/inc/c++ нужен только для C++ приложений, для приложения на Си он лишний.
Сдедующая секция задает общие разделяемые библиотеки фреймворка, билдовая процедура сама подставляет ключ -l для линковки и пути, где библиотеки находятся. Библиотеки объявляются с полным именем, но это просто так принято (можно использовать привычные записи без префикса lib и расширения, например frmapi). Переменные вида FRM_CONFIG_API_LIB содержат значение ‘y’ и определяются в конфигураторе библиотек configs/libdef.mk.
Далее добавляются библиотеки, необходимые для сборки исполняемого файла (для DLL достаточно уже добавленных). Эти библиотеки добавлять в DEP_LIST не надо, они уже построены. Дополнительные библиотеки могут быть добавлены в переменную LIBS, пути до них в переменнную RAW_CONFIG (в эту переменную надо все добавлять с соответствующими ключами).
Далее в список библиотек LIBS добавляется С++ библиотека. Разумеется, она нужна только для C++ приложений.
В переменную CCFLAGS_EXE добавляется макрос для переопределения имени функции запуска в исполняемом файле, для DLL он работать не будет. Существуют и общие переменные CCFLAGS и LDFLAGS, которые работают во всех вариантах.
В последней строчке выполняется инклюд файла common.mk, который собственно и выполняет компиляцию и линковку.
Скрипт Установки Приложения
Скрипт для установки приложения обеспечивает сборку бинарных файлов ресурсов и установку файлов приложения на таргет. Файл вызывается из install.sh скрипта для пользовательских приложений или корневого скрипта configs/install.bgs. Рассмотрим его на примере файла applets/hello/hello_install.bgs:
# @brief Hello World!!! application resource build file
# @file hello_install.bgs
# @author andrey
# Variable declarations
setvar USERAPP $FRMSDK_HOME_DIR/resources/argo/applets/userapp
setvar BUILDDIR $FRMBUILD_HOME_DIR/build/userapp
setvar RESBUILDDIR $FRMBUILD_HOME_DIR/resbuild/userapp
setvar HELLO $FRMSDK_HOME_DIR/resources/argo/applets/userapp/Hello
setvar HELLO_BUILD @BUILDDIR/Hello
setvar HELLO_RESBUILD @RESBUILDDIR/Hello
# Create directory structure
shell mkdir -p $FRM_SYSENV_HOMEDIR/install/
shell mkdir -p @USERAPP
shell mkdir -p @BUILDDIR
shell mkdir -p @RESBUILDDIR
shell mkdir -p @HELLO
shell mkdir -p @HELLO_BUILD
shell mkdir -p @HELLO_RESBUILD
# Copy resources into build directory
shell cp -rf $FRMBUILD_HOME_DIR/applets/hello/resources/* @HELLO_BUILD
# Generate resources
resgen -R -i @HELLO_BUILD/hello_resource.fml -s @HELLO_BUILD -o @HELLO_RESBUILD
resgen -A -i hello_root.fml -s @HELLO_BUILD -o @HELLO_RESBUILD
# Copy resources into target directory
shell cp -rf @HELLO_RESBUILD/*.fbin @HELLO
shell cp -rf @HELLO_RESBUILD/*.ainf @HELLO
shell cp $FRMBUILD_HOME_DIR/applets/hello/hello_icon.bmp @HELLO
shell cp $FRMBUILD_HOME_DIR/applets/hello/hello.install $FRM_SYSENV_HOMEDIR/install/
# Copy executable binaries
shell cp -rf $INSTALL/hello @HELLO
shell cp -rf $INSTALL/hello.so @HELLO
В первую очередь скрипт создает набор внутренних переменных, необходимых скрипту: USERAPP - определяет общую директорию на таргете, где будет размещено приложение; BUILDDIR - общая директория в билдовой папке, где будет выполнятся постройка; RESBUILDDIR - общая папка в рабочем пространстве, где будут сохранятся результаты билда; HELLO - папка приложения на таргете; HELLO_BUILD - папка, где будут размещаться промежуточные результаты простройки (в частности, бинарные файлы управляющих виджетов); HELLO_RESBUILD - папка для финальных результатов постройки ресурсов. Все эти папки на момент запуска скрипта могут существовать, а могут и отсутствовать. Поэтому следующим этапом является создание этих папок. Дополнительно создается папка install в домашней директории на таргете ($FRM_SYSENV_HOMEDIR/install) для сохранения install-файла приложения.
На следующем шаге выполняется копирование всех ресурсов в билдовую папку. Далее выполняется постройка ресурсного файла и корневого файла приложения, построенные файлы размещаются в папке, определенной переменной HELLO_RESBUILD. В данном случае будут построены файлы hello_resource.fbin и hello_root.ainf. В общем случае приложение имеет собственные управляющие виджеты, в этом случае их тоже надо построить ДО постройки ресурсного файла. Ниже приведен пример из приложения testapp.cpp:
# Generate resources
resgen -C -i @TESTAPP_BUILD/resources/controls/cursor.fml -o @TESTAPP_BUILD/resources/controls/ -f cursor
resgen -C -i @TESTAPP_BUILD/resources/controls/label.fml -o @TESTAPP_BUILD/resources/controls/
resgen -C -i @TESTAPP_BUILD/resources/controls/mainlist.fml -o @TESTAPP_BUILD/resources/controls/
resgen -C -i @TESTAPP_BUILD/resources/controls/text_entry.fml -o @TESTAPP_BUILD/resources/controls/
resgen -R -i @TESTAPP_BUILD/resources/testapp_cpp_resource.fml -s @TESTAPP_BUILD/resources -o @TESTAPP_RESBUILD
resgen -A -i @TESTAPP_BUILD/resources/testapp_cpp_root.fml -o @TESTAPP_RESBUILD
Бинарные файлы управляющих элементов являются промежуточными результатами, они не попадают на таргет в явном виде, только в составе общего ресурсного файла. Если управляющий элемент остается в виде FML описания в ресурсном файле (например, в процессе отладки), строить его не надо, но в ресурсном FML файле надо указывать исходный файл, а не построенный ресурс. В этом случае в ресурсном файле сохранится описание управляющего элемента в текстовом виде. Подробнее о постройке ресурсов можно прочитать в соответствующем разделе: Утилиты Фреймворка.
Следующим шагом является копирование построенных ресурсов на таргет, дополнительно копируются файлы иконки в директорию приложения и установочный файл в домашнюю директорию. Посленим шагом является копирование исполняемого файла и библиотеки приложения в папку приложения на таргете (переменная окружения $INSTALL определяется в скрипте install.sh). Папка приложения на таргете после работы скрипта содержит только бинарные файлы, FML-описаний там уже нет.
Для приложений на языке Python скрипт отличается последней секцией:
# Copy python scripts
shell cp -rf $FRMBUILD_HOME_DIR/applets/hello_py/python @HELLO_PY
shell rm -rf @HELLO_PY/hello_py
shell ln -s $FRMSDK_HOME_DIR/usr/bin/pyrunner @HELLO_PY/hello_py
shell cp -rf $FRM_PY_MODULES/* @HELLO_PY/python/
По сути на таргет здесь копируется папка python с кодом приложения, создается символический линк на исполняемый файл pyrunner с именем приложения, и в папку python копируются модули Python API.
На этом пожалуй все, что я хотел рассказать в этом разделе.