Руководство программиста
Глава 2. Создание моделей блоков
§2.13. Вызов функций блоков
§2.13.6. Регистрация исполнителя функции
Рассматривается механизм регистрации блока в качестве исполнителя какой-либо функции, позволяющий остальным блокам схемы легко находить его идентификатор и вызывать эту функцию. В приводимом примере один блок регистрируется как исполнитель функции вывода сообщения, а другой выводит сообщение с его помощью.
Для того, чтобы вызвать функцию какого-либо блока (не важно, будет ли это прямой вызов или отложенный), необходимо знать его идентификатор. Если в блоке реализована какая-либо функция, к которой должны обращаться блоки в разных местах схемы, то как они узнают идентификатор блока, выполняющего функцию? До сих пор мы вызывали функцию либо у блоков, соединенных связями с вызывающим, либо у всех блоков подсистемы. Можно, конечно, вызвать интересующую нас функцию у всех блоков схемы (для этого нужно вызвать функцию у блоков корневой подсистемы и разрешить вызов вложенных в нее подсистем), тогда, рано или поздно, она будет вызвана у блока, который ее поддерживает, и он выполнит нужные нам действия. Однако, для этого RDS придется перебрать все блоки схемы, что сильно замедлит работу. Кроме того, если в схеме окажется несколько блоков, выполняющих интересующую нас функцию, придется принимать специальные меры, чтобы только один из этих блоков сработал. Чтобы избежать этих проблем, можно воспользоваться механизмом регистрации блока как исполнителя какой-либо функции, тогда все блоки схемы смогут получить идентификатор этого блока и вызывать функцию непосредственно у него.
Механизм регистрации исполнителя функции и получения доступа к его идентификатору похож на механизм создания динамических переменных и подписки на них. Блок, поддерживающий какую-либо функцию, может зарегистрироваться в RDS как ее исполнитель при помощи вызова rdsRegisterFuncProvider. RDS будет помнить факт его регистрации до тех пор, пока блок не отменит ее вызовом rdsUnregisterFuncProvider или не будет удален. Блок, которому нужен доступ к этой функции, подписывается на информацию о ее исполнителе вызовом rdsSubscribeToFuncProvider. Эта функция создает для вызвавшего ее блока структуру RDS_FUNCPROVIDERLINK и возвращает указатель на нее. В поле Block этой структуры находится идентификатор ближайшего в иерархии блока, зарегистрировавшегося как исполнитель данной функции (это очень похоже на подписку на динамическую переменную с поиском по иерархии). RDS постоянно отслеживает регистрацию новых блоков и ее отмену, и поддерживает поле Block в актуальном состоянии. Если, например, на момент подписки в системе не было зарегистрировано и одного исполнителя данной функции, в этом поле будет находиться значение NULL. Если, со временем, зарегистрируется один из блоков в корневой подсистеме, RDS сразу же запишет в поле Block его идентификатор. Если потом какой-либо из более близких к подписавшемуся блоков (например, блок в одной подсистеме с ним) тоже зарегистрируется как исполнитель этой функции, в поле Block запишется его идентификатор и т.д. Таким образом, подписавшись на информацию об исполнителе функции, блок всегда имеет доступ к идентификатору ближайшего такого исполнителя. Как и в случае динамических переменных, блок может получать информацию только об исполнителях в своей иерархической цепочке: в своей подсистеме, в родительской подсистеме своей подсистемы и т.д. до корневой подсистемы. Об исполнителе в соседней подсистеме блок не узнает.
В качестве примера создадим блок, который будет выводить пользователю сообщение, текст и заголовок которого будут передаваться в параметрах функции. Мы зарегистрируем этот блок в качестве исполнителя функции вывода сообщения, так что любой блок схемы, находящийся в той же иерархической цепочке, сможет найти его и вызвать эту функцию, если ему потребуется сообщить что-то пользователю. Разумеется, гораздо проще было бы вместо вызова функции у какого-то блока выводить сообщение при помощи сервисной функции rdsMessageBox, однако, в дальнейшем мы создадим еще один блок, который также будет поддерживать функцию вывода сообщения, но вместо демонстрации этого сообщения пользователю будет записывать его в файл. Таким образом, не переделывая модели блоков, которые будут формировать сообщения, мы сможем легко менять способ вывода этих сообщений, просто заменяя один блок-исполнитель на другой.
Прежде всего, нам нужно придумать имя функции вывода сообщений и описать структуру ее параметров. Функцию мы назовем «ProgrammersGuide.UserMessage», а в структуру ее параметров включим поле размера для проверки правильности передачи, указатель на строку с текстом сообщения, и целое поле, которое будет определять тип заголовка: 0 – информационное сообщение, 1 – предупреждение, 2 – сообщение об ошибке.
// Функция вывода сообщения #define PROGGUIDEMESSAGEFUNC "ProgrammersGuide.UserMessage" // Структура параметров функции typedef struct { DWORD servSize; // Размер этой структуры char *MessageStr; // Текст сообщения int Level; // Уровень важности (0, 1 или 2) } TProgGuideMessageFuncParams; //========================================= // Глобальная переменная для идентификатора функции int MessageFunc=0; //=========================================
Очевидно, функцию с такой структурой параметров нельзя использовать в отложенных вызовах (структура содержит указатель на «постороннюю» строку), но нам это и не потребуется, мы будем использовать только прямой вызов.
Напишем модель блока, который, при вызове этой функции, будет показывать сообщение пользователю:
// Блок-исполнитель функции, показывающей сообщение пользователю extern "C" __declspec(dllexport) int RDSCALL MessageFuncBlock_Box(int CallMode, RDS_PBLOCKDATA BlockData, LPVOID ExtParam) { RDS_PFUNCTIONCALLDATA func; switch(CallMode) { // Инициализация case RDS_BFM_INIT: // Регистрируем функцию if(MessageFunc==0) MessageFunc=rdsRegisterFunction(PROGGUIDEMESSAGEFUNC); // Объявляем этот блок ее исполнителем rdsRegisterFuncProvider(MessageFunc,FALSE); break; // Очистка case RDS_BFM_CLEANUP: // Отменяем регистрацию данного блока как исполнителя rdsUnregisterFuncProvider(MessageFunc); break; // Вызов функции case RDS_BFM_FUNCTIONCALL: // Приводим ExtParam к правильному типу func=(RDS_PFUNCTIONCALLDATA)ExtParam; if(func->Function==MessageFunc) { // Вызвана "ProgrammersGuide.UserMessage" TProgGuideMessageFuncParams *params= (TProgGuideMessageFuncParams*)(func->Data); DWORD icon; char *caption; // Проверяем наличие параметров и их размер if(params==NULL || params->servSize<sizeof(TProgGuideMessageFuncParams)) break; // Параметров нет или неверный размер // В зависимости от уровня важности сообщения // устанавливаем иконку и заголовок // ВАЖНО: Исходный текст программы должен быть записан в UTF8, // в противном случае необходимо использовать версии функций // с суффиксом "W" и символьные константы с префиксом "L" switch(params->Level) { case 0: icon=MB_ICONINFORMATION; caption="Информация"; break; case 1: icon=MB_ICONWARNING; caption="Предупреждение"; break; default: icon=MB_ICONERROR; caption="Ошибка"; } // Показываем сообщение пользователю rdsMessageBox(params->MessageStr,caption,icon | MB_OK); } break; } return RDS_BFR_DONE; } //=========================================
Модель получилась не особенно сложной. При ее инициализации мы, как обычно, регистрируем функцию «ProgrammersGuide.UserMessage» и записываем полученный идентификатор в глобальную переменную MessageFunc. Затем мы вызываем rdsRegisterFuncProvider, объявляя тем самым данный блок исполнителем этой функции. В первом параметре rdsRegisterFuncProvider передается идентификатор функции (MessageFunc), второй параметр важен только при вызове этой сервисной функции из моделей подсистем: значение TRUE указывает на то, что подсистема предоставляет доступ к этой функции только своим внутренним блокам, FALSE – внутренним блокам и блокам-соседям по родительской подсистеме. Мы вызываем rdsRegisterFuncProvider из модели обычного блока, у которого не может быть внутренних блоков, поэтому этот параметр будет проигнорирован RDS, и мы можем передать в нем любое значение. В результате этого вызова все блоки, находящиеся в одной подсистеме с нашим, а также все блоки, для которых эта подсистема находится в цепочке родителей (блоки всех вложенных подсистем, блоки всех подсистем, вложенных во вложенные, и т.д.) смогут получить доступ к этой функции, если подпишутся на идентификатор ее исполнителя.
При очистке (событие RDS_BFM_CLEANUP) мы отменяем регистрацию блока-исполнителя функции с идентификатором MessageFunc вызовом rdsUnregisterFuncProvider. При этом RDS переключит все подписавшиеся блоки на идентификатор другого блока, зарегистрировавшегося как исполнитель этой функции, если, конечно, такой имеется в их цепочке родителей. Если такого блока не окажется, вместо идентификатора исполнителя они будут получать значение NULL до тех пор, пока исполнитель снова не появится.
Наконец, мы должны включить в модель блока реакцию на функцию, исполнителем которой мы его регистрируем. В реакции на событие RDS_BFM_FUNCTIONCALL мы сравниваем идентификатор вызванной функции с MessageFunc и, если они совпали, проверяем наличие (переданный указатель на структуру параметров не должен быть равен NULL) и правильность (поле servSize структуры параметров должно быть не меньше ожидаемого размера структуры) переданных параметров. Если параметры переданы верно, мы, в зависимости от поля Level переданной структуры, записываем во вспомогательную переменную переменную icon одну из стандартных констант Windows API, используемых для указания иконок сообщений, а в переменную caption – указатель на строку заголовка окна сообщения, соответствующего этой иконке. Затем мы показываем текст, переданный в поле MessageStr структуры параметров функции, в стандартном окне сообщения при помощи сервисной функции rdsMessageBox. Здесь мы используем сервисную функцию RDS, а не стандартную функцию Windows MessageBox, поскольку сервисную функцию можно безопасно вызывать при запущенном расчете: при вызове из потока расчета она, в отличие от ее «тезки» из Windows API, не останавливает работу потока, а возвращает управление немедленно. В качестве заголовка окна мы передаем значение переменной caption, в качестве флагов, влияющих на внешний вид окна – значение переменной icon, объединенное битовым ИЛИ с константой MB_OK. Таким образом, на экране появится окно с переданным в параметрах функции текстом, имеющее заголовок и иконку, соответствующие уровню важности сообщения. Окно будет иметь единственную кнопку с надписью «».
Теперь напишем пример модели блока, который будет пользоваться этой функцией. Сделаем блок, который, при поступлении сигнала на вход, будет выводить сообщение, текст и уровень важности которого будет задаваться пользователем в настройках блока. Такой блок можно подключить, например, к сигналу переполнения счетчика, или к блоку сравнения, и он выдаст пользователю заранее определенное сообщение при наступлении соответствующего события. В качестве входного сигнала для выдачи сообщения мы будем использовать стандартный вход запуска модели (это всегда первая переменная блока), поэтому в параметрах блока нам нужно будет включить режим запуска по сигналу (если включить режим запуска каждый такт расчета, блок будет постоянно выдавать сообщения, игнорируя вход запуска). Текст сообщения и уровень его важности мы будем хранить в значениях по умолчанию переменных блока, как мы уже не раз делали. Для этого нам потребуется две переменных: строковая для текста и целая для уровня важности. Блок будет иметь следующую структуру переменных:
| Смещение | Имя | Тип | Размер | Вход/выход | Пуск | Начальное значение | Номер |
|---|---|---|---|---|---|---|---|
| 0 | Show | Сигнал | 1 | Вход | ✓ | 0 | 0 |
| 1 | Ready | Сигнал | 1 | Выход | 0 | 1 | |
| 2 | Message | Строка | 8 | Внутренняя | 2 | ||
| 10 | Type | int | 4 | Внутренняя | 0 | 3 |
Первую переменную блока, вход запуска модели, мы переименуем в «Show», чтобы название переменной лучше отражало назначение этого входа.
Для вывода сообщения нашему блоку необходимо знать идентификатор блока-исполнителя функции «ProgrammersGuide.UserMessage». Как и при подписке на динамическую переменную, при подписке на идентификатор блока-исполнителя RDS создает во внутренней памяти структуру, содержащую необходимую информацию, и возвращает указатель на нее. Этот указатель необходимо где-то хранить на протяжении всего существования нашего блока, поэтому блоку потребуется личная область данных. Оформим ее в виде класса, в конструкторе и деструкторе которого будем подписываться на идентификатор блока-исполнителя и прекращать эту подписку соответственно:
// Личная область данных блока, выводящего сообщение class TMessageFuncUserData { public: // Указатель на структуру подписки RDS_PFUNCPROVIDERLINK Link; // Функция настройки блока BOOL Setup(RDS_BHANDLE Block,int NumTypeVar,int NumMessVar); // Конструктор класса TMessageFuncUserData(void) { // Регистрируем функцию if(MessageFunc==0) MessageFunc=rdsRegisterFunction(PROGGUIDEMESSAGEFUNC); // Подписываемся на блок-исполнитель Link=rdsSubscribeToFuncProvider(MessageFunc); }; // Деструктор класса ~TMessageFuncUserData() { // Прекращаем подписку rdsUnsubscribeFromFuncProvider(MessageFunc); }; }; //=========================================
В поле Link описанного нами класса будет храниться указатель на структуру подписки на блок-исполнитель функции. Структура имеет всего два поля:
// Структура подписки на блок-исполнитель функции typedef struct { RDS_BHANDLE Block; // Идентификатор блока-исполнителя int FuncId; // Идентификатор функции } RDS_FUNCPROVIDERLINK; typedef RDS_FUNCPROVIDERLINK *RDS_PFUNCPROVIDERLINK;
В поле Block этой структуры всегда находится либо идентификатор ближайшего по иерархии блока, зарегистрировавшегося как исполнитель данной функции, либо NULL, если такого блока нет (RDS всегда поддерживает это поле в актуальном состоянии). В поле FuncId хранится идентификатор самой функции. На самом деле, идентификатор функции нам известен и без этой структуры: не зная его, мы не смогли бы подписаться на исполнителя. В структуру он включен только для удобства программирования.
В классе объявлена функция настройки блока, с помощью которой пользователь будет задавать текст и важность сообщения. В функцию передается идентификатор данного блока и порядковые номера переменных, в которых мы храним параметры (эти номера потребуются нам для получения значений переменных по умолчанию). Тело этой функции мы напишем позже.
В конструкторе класса мы регистрируем функцию «ProgrammersGuide.UserMessage», если она еще не регистрировалась ранее, а затем при помощи вызова rdsSubscribeToFuncProvider подписываемся на ее исполнителя. Возвращаемый указатель на структуру RDS_FUNCPROVIDERLINK мы записываем в поле класса Link. Теперь, если нам потребуется вызвать функцию вывода сообщения, мы сможем использовать конструкцию вида
if(Link!=NULL) rdsCallBlockFunction(Link->Block,Link->FuncId,…);
Сервисная функция rdsCallBlockFunction устроена таким образом, что, если вместо идентификатора блока в первом параметре передается значение NULL, она немедленно завершится, не выполнив никаких действий, поэтому поле Link->Block можно не сравнивать с NULL перед ее вызовом. Однако, со значением NULL необходимо сравнить само значение поля Link, поскольку функция rdsSubscribeToFuncProvider, которая вернула нам это значение, возвращает NULL в случае ошибки (например, если мы вызовем ее из модели корневой подсистемы, иерархическая цепочка которой не содержит ни одного блока, поэтому и исполнителя функции искать негде).
В деструкторе класса мы отменяем подписку на исполнителя функции при помощи вызова rdsUnsubscribeFromFuncProvider. Технически, можно было бы и не делать этого, поскольку при отключении модели от блока все подписки отменяются автоматически, однако явная отмена подписки улучшает читаемость текста программы.
Функция модели блока будет иметь следующий вид:
// Блок, выводящий сообщение по сигналу extern "C" __declspec(dllexport) int RDSCALL ShowMessage(int CallMode, RDS_PBLOCKDATA BlockData, LPVOID ExtParam) { // Указатель на личную область данных TMessageFuncUserData *data= (TMessageFuncUserData*)(BlockData->BlockData); // Макроопределения для статических переменных #define pShow ((char *)(BlockData->VarTreeData)) #define Show (*((char *)(pShow))) #define Ready (*((char *)(pShow+RDS_VSZ_S))) #define Message (*((char **)(pShow+2*RDS_VSZ_S))) #define Type (*((RDSINT32 *)(pShow+2*RDS_VSZ_S+RDS_VSZ_A))) switch(CallMode) { // Инициализация – создание личной области case RDS_BFM_INIT: BlockData->BlockData=new TMessageFuncUserData(); break; // Очистка – удаление личной области case RDS_BFM_CLEANUP: delete data; break; // Проверка типов переменных case RDS_BFM_VARCHECK: return strcmp((char*)ExtParam,"{SSAI}")? RDS_BFR_BADVARSMSG:RDS_BFR_DONE; // Настройка блока case RDS_BFM_SETUP: return data->Setup(BlockData->Block,3,2)?1:0; // Такт расчета (реакция на сигнал Show) case RDS_BFM_MODEL: if(data->Link!=NULL) // Есть структура подписки { TProgGuideMessageFuncParams params; // Готовим структуру параметров функции params.servSize=sizeof(params); params.MessageStr=Message; params.Level=Type; // Вызываем функцию по данным подписки rdsCallBlockFunction(data->Link->Block, data->Link->FuncId,¶ms); } break; } return RDS_BFR_DONE; // Отмена макрооперделений #undef Type #undef Message #undef Ready #undef Show #undef pShow } //=========================================
При инициализации и очистке модели мы здесь, как обычно, создаем и уничтожаем личную область данных блока (все действия по регистрации функции и подписке выполняются в конструкторе класса, по прекращению подписки – в деструкторе), а при проверке типов переменных – сравниваем переданную строку со строкой, соответствующей используемому нами набору переменных. Реагируя на вызов в режиме RDS_BFM_SETUP, модель вызывает функцию настройки Setup, которую нам еще предстоит написать, передавая ей порядковые номера переменных Type (3) и Message (2). Главные действия, ради которых, собственно, и создавалась эта модель, производятся в реакции на такт расчета RDS_BFM_MODEL, то есть при поступлении единицы на вход блока Show.
Прежде всего, указатель на структуру подписки, запомненный в поле Link класса личной области данных блока, сравнивается с NULL – равенство укажет на то, что подписка на блок-исполнитель функции принципиально невозможна, и вывести сообщение нельзя. Если поле Link указывает на структуру во внутренней памяти RDS, структура params заполняется параметрами функции (текст сообщения берется из переменной блока Message, важность сообщения – из переменной Type), после чего у блока, идентификатор которого хранится в Link->Params, вызывается функция Link->FuncId. В качестве идентификатора функции можно было бы использовать и значение глобальной переменной MessageFunc, они, очевидно, будут равны.
Нам осталось только написать функцию настройки параметров блока Setup, и модель блока будет готова:
// Функция настройки блока // ВАЖНО: Исходный текст программы должен быть записан в UTF8, // в противном случае необходимо использовать версии функций // с суффиксом "W" и символьные константы с префиксом "L" BOOL TMessageFuncUserData::Setup(RDS_BHANDLE Block, int NumTypeVar,int NumMessVar) { RDS_HOBJECT window; // Объект-окно BOOL ok; // Пользователь нажал "OK" char *defval; // Создание окна window=rdsFORMCreate(FALSE,-1,-1,"Сообщение"); // Важность сообщения // Получение значения переменной по умолчанию defval=rdsGetBlockVarDefValueStr(Block,NumTypeVar,NULL); // Добавление поля – выпадающий список rdsFORMAddEdit(window,0,1,RDS_FORMCTRL_COMBOLIST,"Тип:",150); // Установка списка вариантов rdsSetObjectStr(window,1,RDS_FORMVAL_LIST, "Информация\nПредупреждение\nОшибка"); // Установка текущего значения поля rdsSetObjectStr(window,1,RDS_FORMVAL_ITEMINDEX,defval); // Освобождение defval rdsFree(defval); // Текст сообщения // Получение значения переменной по умолчанию defval=rdsGetBlockVarDefValueStr(Block,NumMessVar,NULL); // Добавление поля – многострочное rdsFORMAddEdit(window,0,2,RDS_FORMCTRL_MULTILINE,"Текст:",80); // Установка текущего значения поля rdsSetObjectStr(window,2,RDS_FORMVAL_VALUE,defval); // Установка высоты поля в точках экрана (~3 строки) rdsSetObjectInt(window,2,RDS_FORMVAL_MLHEIGHT,3*24); // Освобождение defval rdsFree(defval); // Открытие окна ok=rdsFORMShowModalEx(window,NULL); if(ok) { // Пользователь нажал OK – запись измененных параметров defval=rdsGetObjectStr(window,1,RDS_FORMVAL_ITEMINDEX); rdsSetBlockVarDefValueStr(Block,NumTypeVar,defval); defval=rdsGetObjectStr(window,2,RDS_FORMVAL_VALUE); rdsSetBlockVarDefValueStr(Block,NumMessVar,defval); } // Уничтожение окна rdsDeleteObject(window); return ok; } //=========================================
Эта функция похожа на другие функции настройки, которые мы уже не раз делали. Единственное новшество в ней – использование многострочного поля ввода RDS_FORMCTRL_MULTILINE для текста сообщения и установка высоты этого поля вызовом rdsSetObjectInt с константой RDS_FORMVAL_MLHEIGHT.
Теперь можно приступать к тестированию модели. Необходимо подключить ее к блоку с указанной выше структурой переменных, включить в параметрах этого блока запуск только по сигналу и разрешить функцию настройки (см. рис. 7). К входу «Show» блока подключим выход «Click» стандартной кнопки (нажатие на эту кнопку будет выводить сообщение), а в настройках нашего блока зададим тип (важность) и текст сообщения (рис. 90). Нам также понадобится блок-исполнитель функции с созданной нами моделью MessageFuncBlock_Box: можно разместить его в той же подсистеме, что и блок с кнопкой, либо в одной из подсистем в иерархической цепочке, например, в корневой. Теперь, если запустить расчет и нажать на кнопку, подключенную к блоку, на экране появится сообщение, выведенное блоком-исполнителем (рис. 91).
Рис. 90. Блок, выводящий сообщение по сигналу,
и окно его настройки
Рис. 91. Сообщение,
выводимое блоком
Теперь сделаем другой блок-исполнитель для той же самой функции «ProgrammersGuide.UserMessage». Вместо немедленной демонстрации сообщения пользователю модель блока будет записывать его в файл, имя которого будет задаваться в настройках блока. Кроме того, чтобы этот файл не разрастался до бесконечности, сделаем в настройках блока возможность включения автоматической очистки этого файла при загрузке схемы. Параметры блока мы будем хранить в значениях статических переменных по умолчанию: имя файла – в строковой переменной «FileName», флаг необходимости очистки при загрузке схемы – в логической «ClearOnLoad». Блок будет иметь следующую структуру переменных:
| Смещение | Имя | Тип | Размер | Вход/выход | Пуск | Начальное значение | Номер |
|---|---|---|---|---|---|---|---|
| 0 | Start | Сигнал | 1 | Вход | ✓ | 0 | 0 |
| 1 | Ready | Сигнал | 1 | Выход | 0 | 1 | |
| 2 | FileName | Строка | 8 | Внутренняя | 2 | ||
| 10 | ClearOnLoad | Логический | 1 | Внутренняя | 0 | 3 |
Сначала напишем функцию настройки блока. В ее параметрах, как и у остальных функций настройки, работающих со значениями переменных по умолчанию, необходимо передавать идентификатор блока и порядковые номера переменных, с которыми она будет работать:
// Функция настройки блока вывода сообщения в файл // ВАЖНО: Исходный текст программы должен быть записан в UTF8, // в противном случае необходимо использовать версии функций // с суффиксом "W" и символьные константы с префиксом "L" BOOL MessageFuncBlockFileSetup( RDS_BHANDLE Block, // Идентификатор блока int NumFileVar, // Номер переменной имени файла int NumClearVar) // Номер переменной флага очистки { RDS_HOBJECT window; // Идентификатор объекта-окна BOOL ok; // Пользователь нажал "OK" char *defval; // Создание окна window=rdsFORMCreate(FALSE,-1,-1,"Запись в файл"); // Поле ввода для имени файла // Чтение значения переменной по умолчанию defval=rdsGetBlockVarDefValueStr(Block,NumFileVar,NULL); // Поле ввода – выбор файла с диалогом сохранения rdsFORMAddEdit(window,0,1,RDS_FORMCTRL_SAVEDIALOG,"Файл:",300); // Фильтр типов файлов для диалога сохранения rdsSetObjectStr(window,1,RDS_FORMVAL_LIST, "Текстовые файлы (*.txt)|*.txt\nВсе файлы|*.*"); // Запись значения в поле ввода rdsSetObjectStr(window,1,RDS_FORMVAL_VALUE,defval); // Освобождение defval rdsFree(defval); // Очистка при загрузке схемы // Чтение значения переменной по умолчанию defval=rdsGetBlockVarDefValueStr(Block,NumClearVar,NULL); // Поле ввода - флаг rdsFORMAddEdit(window,0,2,RDS_FORMCTRL_CHECKBOX, "Очищать при загрузке схемы",0); // Запись значения в поле ввода rdsSetObjectStr(window,2,RDS_FORMVAL_VALUE,defval); // Освобождение defval rdsFree(defval); // Открытие окна ok=rdsFORMShowModalEx(window,NULL); if(ok) { // Пользователь нажал OK – запись измененных значений defval=rdsGetObjectStr(window,1,RDS_FORMVAL_VALUE); rdsSetBlockVarDefValueStr(Block,NumFileVar,defval); defval=rdsGetObjectStr(window,2,RDS_FORMVAL_VALUE); rdsSetBlockVarDefValueStr(Block,NumClearVar,defval); } // Уничтожение окна rdsDeleteObject(window); return ok; } //=========================================
Рис. 92. Окно настройки с полем выбора файла
В этой функции мы применяем не встречавшийся раньше тип поля ввода: указание имени файла с кнопкой вызова диалога сохранения (тип RDS_FORMCTRL_SAVEDIALOG, см. рис. 92). Кроме имени файла, в него необходимо занести список шаблонов имен файлов, доступных пользователю в выпадающем списке в стандартном диалоге сохранения. Этот список передается в поле в виде строки обычной сервисной функцией rdsSetObjectStr с константой RDS_FORMVAL_LIST. Каждый элемент выпадающего списка состоит из текста, который видит пользователь, и шаблона имени (с использованием обычных метасимволов «*» и «?»), разделенных символом вертикальной черты «|». Можно указать несколько шаблонов, разделив их точкой с запятой. Элементы списка отделяются друг от друга символом перевода строки, который в языке C записывается как «\n». В нашем случае в списке будет два элемента: «Текстовые файлы (*.txt)» (с шаблоном «*.txt») и «Все файлы» (с шаблоном «*.*»). Если бы мы, например, захотели, чтобы при выборе в диалоге сохранения варианта «Текстовые файлы» отображались файлы не только с расширением «txt», но и с расширением «log», вызов установки списка шаблонов выглядел бы так:
// Фильтр типов файлов для диалога сохранения rdsSetObjectStr(window,1,RDS_FORMVAL_LIST, "Текстовые файлы|*.txt;*.log\nВсе файлы|*.*");
В остальном эта функция не отличается от других функций настройки, которые нам уже приходилось делать.
Теперь напишем модель блока:
// Блок-исполнитель функции, выводящий сообщение в файл // ВАЖНО: в модели используются функции Windows API для работы с файлом. // Поскольку в Windows используется кодировка UTF16, в модели вызываются // сервисные функции RDS для перевода UTF8 в UTF16. extern "C" __declspec(dllexport) int RDSCALL MessageFuncBlock_File(int CallMode, RDS_PBLOCKDATA BlockData, LPVOID ExtParam) { RDS_PFUNCTIONCALLDATA func; // Макроопределения для статических переменных #define pStart ((char *)(BlockData->VarTreeData)) #define Start (*((char *)(pStart))) #define Ready (*((char *)(pStart+RDS_VSZ_S))) #define FileName (*((char **)(pStart+2*RDS_VSZ_S))) #define ClearOnLoad (*((char *)(pStart+2*RDS_VSZ_S+RDS_VSZ_A))) switch(CallMode) { // Инициализация case RDS_BFM_INIT: // Регистрация функции if(MessageFunc==0) MessageFunc=rdsRegisterFunction(PROGGUIDEMESSAGEFUNC); // Объявляем данный блок ее исполнителем rdsRegisterFuncProvider(MessageFunc,FALSE); break; // Очистка case RDS_BFM_CLEANUP: // Отмена регистрации блока как исполнителя rdsUnregisterFuncProvider(MessageFunc); break; // Проверка типов статических переменных case RDS_BFM_VARCHECK: return strcmp((char*)ExtParam,"{SSAL}")? RDS_BFR_BADVARSMSG:RDS_BFR_DONE; // Вызов функции настройки case RDS_BFM_SETUP: return MessageFuncBlockFileSetup(BlockData->Block,2,3)?1:0; // Загрузка схемы только что завершилась case RDS_BFM_AFTERLOAD: if(ClearOnLoad) // Включена очистка файла при загрузке { // Формируем в fullpath полный путь к файлу, т.к. // введенный пользователем может быть неполным char *fullpath=rdsGetFullFilePath(FileName,NULL,NULL); if(fullpath) // Полный путь существует { // Преобразуем в UTF16 RDSWSTR fullpath_w=rdsUTF8toUTF16(fullpath,FALSE); DeleteFileW(fullpath_w); // Удаляем файл // Освобождаем память rdsFree(fullpath_w); rdsFree(fullpath); } } break; // Вызов функции case RDS_BFM_FUNCTIONCALL: func=(RDS_PFUNCTIONCALLDATA)ExtParam; if(func->Function==MessageFunc) { // Вызвана наша функция – приводим параметры // к правильному типу TProgGuideMessageFuncParams *params= (TProgGuideMessageFuncParams*)(func->Data); char *levelstr,*fullpath; RDSWSTR fullpath_w; // Для имени файла в UTF16 if(params==NULL || params->servSize<sizeof(TProgGuideMessageFuncParams)) break; // Параметров нет или неверный размер // Записываем в переменную levelstr строку, // соотвествующую важности сообщения switch(params->Level) { case 0: levelstr="Информация"; break; case 1: levelstr="Предупреждение"; break; default: levelstr="Ошибка"; } // Формируем в fullpath полный путь к файлу fullpath=rdsGetFullFilePath(FileName,NULL,NULL); if(fullpath) // Полный путь сформирован { HANDLE h; // Преобразуем в UTF16 fullpath_w=rdsUTF8toUTF16(fullpath,FALSE); // Открываем файл fullpath на запись h=CreateFileW(fullpath_w,GENERIC_WRITE,0,NULL, OPEN_ALWAYS,FILE_ATTRIBUTE_NORMAL,NULL); if(h!=INVALID_HANDLE_VALUE) { // Файл открыт char buf[100]; DWORD temp; SYSTEMTIME time; // Перемещаем указатель файла в конец SetFilePointer(h,0,NULL,FILE_END); // Получаем текущую дату и время GetLocalTime(&time); // Формируем строку с датой и временем в buf sprintf(buf,"%02d-%02d-%04d %02d:%02d:%02d " "%s: ",time.wDay,time.wMonth,time.wYear, time.wHour,time.wMinute,time.wSecond, levelstr); // Записываем дату, время и важность WriteFile(h,buf,strlen(buf),&temp,NULL); // Записываем текст сообщения if(params->MessageStr) WriteFile(h,params->MessageStr, strlen(params->MessageStr),&temp,NULL); // Записываем перевод строки WriteFile(h,"\r\n",2,&temp,NULL); // Закрываем файл CloseHandle(h); } // Освобождаем память, отведенную под полный путь rdsFree(fullpath_w); rdsFree(fullpath); } } break; } return RDS_BFR_DONE; // Отмена макроопределений #undef ClearOnLoad #undef FileName #undef Ready #undef Start #undef pStart } //=========================================
Реакции этой модели на события RDS_BFM_INIT и RDS_BFM_CLEANUP в точности совпадают с соответствующими реакциями модели MessageFuncBlock_Box – как исполнитель функции «ProgrammersGuide.UserMessage», этот блок выполняет те же самые действия по регистрации функции и ее отмене. В реакциях на RDS_BFM_VARCHECK и RDS_BFM_SETUP тоже нет ничего принципиально нового. Две оставшихся реакции мы рассмотрим подробно.
Как только загрузка схемы, в состав которой входит данный блок, завершится, модель блока будет вызвана в режиме RDS_BFM_AFTERLOAD. В этот момент модель должна проверить, включена ли в настройках блока очистка файла сообщений при загрузке схемы и, если этот так, стереть этот файл. Таким образом, если значение переменной ClearOnLoad не нулевое, мы должны удалить файл с именем FileName. Однако, не все так просто: в переменной FileName может содержаться только имя файла без пути. Например, если файл находится в одной папке со схемой, поле ввода RDS_FORMCTRL_SAVEDIALOG, которое мы использовали в окне настройки, автоматически отбросит этот путь, оставив только имя файла. Для удаления файла необходимо знать полный путь к нему, поэтому сначала нам необходимо воспользоваться сервисной функцией rdsGetFullFilePath. Эта функция формирует в динамической памяти строку, содержащую полный путь к файлу, имя которого передано в ее первом параметре. Функция автоматически подставляет вместо стандартных обозначений путей, используемых в RDS («$DLL$», «$INI$» и т.д.) реальные пути к соответствующим папкам. Если в имени файла, переданном в функцию, отсутствует путь, функция добавит к имени путь по умолчанию, указанный в ее втором параметре, либо, если этот параметр равен NULL, путь к файлу схемы. Нам нужен именно последний вариант, поэтому во втором параметре мы передаем NULL. В третьем параметре можно передать указатель на целое число, в которое функция запишет длину сформированной строки, но нам это не нужно, поэтому третий параметр в вызове rdsGetFullFilePath тоже равен NULL.
Указатель на сформированную функцией строку с полным путем к файлу записывается во вспомогательную переменную fullpath. Если fullpath не равен NULL, то есть если полный путь удалось сформировать, мы удаляем файл функцией Windows API DeleteFileW, предварительно преобразовав этот путь в кодировку UTF16 функцией rdsUTF8toUTF16, после чего освобождаем память, занятую строками, при помощи rdsFree.
Если у блока вызвана функция (режим RDS_BFM_FUNCTIONCALL), и идентификатор этой функции совпадает со значением переменной MessageFunc, в которую мы записали идентификатор нашей функции «ProgrammersGuide.UserMessage» при регистрации, модель проверяет наличие и размер переданной вместе с функцией структуры параметров и завершается, если параметров нет или размер недостаточен. В противном случае во вспомогательную переменную levelstr записывается указатель на строку «Информация», «Предупреждение» или «Ошибка» в зависимости от значения поля Level переданной структуры параметров (точно так же мы выбирали заголовок сообщения в модели MessageFuncBlock_Box, записывая указатель в переменную caption). Затем мы уже описанным способом формируем полный путь к файлу, в конец которого нам предстоит дописать сообщение. Если путь сформирован, мы открываем этот файл на запись функцией Windows API CreateFileW и перемещаем указатель в его конец функцией SetFilePointer. Теперь все, что мы запишем в файл, будет добавлено после уже имеющихся в нем данных.
Чтобы пользователь мог понять, когда произошло то или иное событие, сообщение о котором записано в файле, нам следует добавить к записываемому тексту текущую дату и время. Для этого мы используем функцию Windows API GetLocalTime, которая заполняет структуру time типа SYSTEMTIME. Затем мы, используя стандартную функцию sprintf (для ее использования должен быть включен файл заголовков «stdio.h»), формируем во вспомогательном массиве buf строку, содержащую дату и время в привычном для пользователя виде, после которых добавлена строка levelstr, характеризующая важность сообщения. Строка из массива buf записывается в файл функцией Windows API WriteFile, за ней записывается текст сообщения, переданный в поле MessageStr структуры параметров вызванной функции, а за ним – коды перевода строки «\r\n». Затем файл закрывается функцией CloseHandle, а память, отведенная под строку полного пути fullpath, как обычно, освобождается сервисной функцией rdsFree.
Следует отметить, что при ведении какого-либо журнала сообщений, как в нашем случае, лучше всего работать именно в таком режиме: открыть файл, дописать в него новое сообщение, и снова закрыть его. Если бы мы открыли файл при поступлении первого сообщения, и не закрывали бы его до завершения работы со схемой, у пользователя могли бы возникнуть проблемы с доступом к этому файлу, его удалением и т.п. Кроме того, если открывать файл только для записи в него очередного сообщения, несколько разных схем смогут работать с одним и тем же файлом (кончено, при этом лучше отключить очистку файла при загрузке). Постоянные открытия-закрытия будут замедлять работу схемы, но, если сообщения в системе возникают не очень часто, на это можно не обращать внимания.
Если теперь заменить блок с моделью MessageFuncBlock_Box на блок с только что написанной нами моделью, разрешить в его параметрах вызов функции настройки (см. рис. 7) и указать в ней какой-нибудь файл для записи сообщений, то при нажатии на кнопку в схеме, изображенной на рис. 90, в файл будет записана строка следующего вида:
04-07-2010 14:30:03 Информация: Нажата кнопка
Каждое новое нажатие на кнопку будет добавлять в файл новую строку.
Может возникнуть вопрос: зачем нам понадобилось делать новую модель для вывода сообщений в файл? Не лучше ли было бы добавить эту возможность в модель MessageFuncBlock_Box, и сделать в ее настройках возможность выбора – показывать сообщение пользователю или записывать его в файл? Дело в том, что в нашем случае мы имеем доступ к модели MessageFuncBlock_Box и можем, при желании, переделать ее. Однако, если другому разработчику понадобится реализовать какой-либо другой способ обработки сообщений (например, отправлять их по электронной почте), у него будет единственный выход – написать свою собственную модель блока-исполнителя функции «ProgrammersGuide.UserMessage». После этого ему достаточно будет заменить блок-исполнитель, имеющийся в схеме, на свой, и обработка сообщений в схеме изменится. При этом он может разместить свой блок не в корневой подсистеме, а в одной из вложенных, тогда только блоки этой подсистемы и входящих в нее дочерних подсистем будут пользоваться новым блоком-исполнителем, а сообщения всех остальных блоков будут обрабатываться по-старому.