Текущий раздел:

Описания RDS

Описание пользователя

Глава 3. Использование стандартных модулей автокомпиляции

§3.6. Принципы создания автокомпилируемых моделей блоков

§3.6.13. Вызов функций блоков

§3.6.13.4. Регистрация и поиск исполнителя функции

# Описание пользователя

## Глава 3. Использование стандартных модулей автокомпиляции

### §3.6. Принципы создания автокомпилируемых моделей блоков

#### §3.6.13. Вызов функций блоков

##### §3.6.13.4. Регистрация и поиск исполнителя функции

Описывается способ регистрации блока как исполнителя какой-либо функции, что позволит другим блокам легко находить его. Также описывается поиск блока-исполнителя для известной функции.

Регистрация блока в качестве исполнителя функции позволяет другим блокам, расположенным в одной с ним подсистеме или в подсистемах внутри нее, находить этот блок в схеме и вызывать нужную им функцию непосредственно у него. Таких исполнителей может быть в схеме несколько – каждый из них будет обслуживать свою ветвь иерархии схемы, начиная с собственной подсистемы. Поиском исполнителей функции и слежением за их появлением и исчезновением занимается RDS. Любому блоку, модель которого запросила информацию об исполнителе, постоянно предоставляется информация о ближайшем исполнителе в иерархии – после первичного запроса, модели не нужно выполнять каких-либо действий для поддержания информации об исполнителе в актуальном состоянии. Так можно довольно просто добавлять в схему новые функции, доступные блокам независимо от их размещения: можно организовать ведение журналов событий, связь с внешними приложениями и т.п.

Работа с блоками-исполнителями функций состоит из двух частей: регистрации, выполняемой блоком, у которого будет вызываться функция, и подписки на исполнителя, выполняемой блоком, который будет вызывать функцию. Модуль автокомпиляции автоматизирует обе части: для того, чтобы объявить блок исполнителем функции, достаточно просто включить одноименный флажок при добавлении функции в модель. Точно так же, флажком при добавлении функции в модель, включается и подписка блока на исполнителя функции. Для вызова функции у исполнителя, на который подписан блок, у объекта, созданного для этой функции, вызывается функция-член Call без указания идентификатора блока. Следует, однако, помнить, что такой вызов возможен только непосредственно из модели блока, то есть из функции, являющейся членом класса блока. Если вызов производится из функции, не принадлежащей классу блока (например, из функции обратного вызова, подобной функции ControlValChanged_Callback из §3.6.13.3, Call без идентификатора блока будет недоступна. В этом случае нужно как-то передать в эту функцию идентификатор исполнителя, полученный через функцию-член Provider (подробнее о технической реализации вызовов функций через объекты можно прочесть в §3.6.13.5).

Рассмотрим простой пример: создадим блок, который, при вызове у него функции, будет выводить сообщение пользователю. В параметрах функции будет содержаться текст сообщения и его тип: информационное сообщение, предупреждение или сообщение об ошибке. Назовем эту функцию «UserManual.Message», а параметры ее оформим в виде структуры, первым полем которой будет размер этой структуры. Структуру мы запишем в файл «UserMessage.h», который разместим в одной папке с файлом модели блока. Создадим этот файл «UserMessage.h» (например, в «блокноте» Windows) и введем в него следующий текст:

      struct TUserMessageFuncParam
      { DWORD servSize;        // Размер структуры
        int Type;              // Тип сообщения
        const char *Message;   // Текст сообщения
      };
      // Типы сообщений
      #define UMFP_TYPE_INF  0 // Информационное
      #define UMFP_TYPE_WARN 1 // Предупреждение
      #define UMFP_TYPE_ERR  2 // Ошибка

Первое поле структуры (servSize) будет содержать ее собственный размер, чтобы вызванный блок мог сравнить его с действительным размером структуры и понять, правильно ли переданы параметры. В поле Type будет храниться тип сообщения: 0 – информационное, 1 – предупреждение, 2 – сообщение об ошибке. В поле Message должен быть записан указатель на строку с текстом сообщения. Чтобы не запоминать, какое целое число соответствует какому типу сообщения, после описания структуры мы вводим define-константы для каждого из перечисленных выше типов.

Создадим в схеме новый блок с автокомпилируемой моделью и сразу введем в эту модель команду для включения файла с описанной нами структурой: на вкладке «события» левой панели редактора раскроем раздел «описания», дважды щелкнем на его пункте «глобальные описания» и на открывшейся справа пустой вкладке «описания» введем единственную строчку:

      #include "UserMessage.h"

Теперь добавим в модель функцию «UserManual.Message». На вкладке «функции» левой панели нажмем кнопку «+» и заполним открывшееся окно согласно рис. 477.

Рис. 477. Добавление в модель блока функции «UserManual.Message»
(рамкой выделен флажок, регистрирующий блок как исполнителя функции)

На верхней панели окна выбран флажок «произвольная функция» – мы добавляем функцию, которую придумали сами. На панели «имя функции в RDS» введено название нашей функции. Поле «объект для функции в программе» можно оставить без изменений: хотя модуль автокомпиляции и предлагает, как обычно, слишком длинное имя для объекта, в этой модели мы не будем обращаться к этому объекту вручную, поэтому не важно, как он называется. В поле «тип параметра функции» введен тип указателя на описанную нами структуру TUserMessageFuncParam, то есть «TUserMessageFuncParam*». Наконец, на панели «дополнительные действия», в отличие от предыдущих примеров, выбран флажок «объявить блок исполнителем функции».

После нажатия «OK» новая функция появится в списке функций на вкладке (рис. 478). Можно заметить, что, кроме иконки с пустым листом (указывающей, что мы еще не ввели текст реакции на эту функцию), рядом с ее именем будет находиться знак «+» – так помечаются функции, исполнителем которых является блок. Теперь с момента подключения модели к блоку и до ее отключения (например, из-за удаления блока из схемы) наш блок будет считаться исполнителем функции «UserManual.Message».

Рис. 478. Список функций в редакторе модели после добавления «UserManual.Message»
и регистрации блока как ее исполнителя

Функция добавлена в модель и блок зарегистрирован как ее исполнитель – теперь нужно ввести реакцию на эту функцию. Проще всего сделать это, выбрав функцию в списке на панели «функции» (она там единственная) и нажав кнопку с желтым листком в левой нижней части вкладки. На открывшейся справа вкладке с именем функции («UserManual.Message») введем следующий текст:

      if(Param!=NULL && // Параметр передан
         Param->servSize>=sizeof(TUserMessageFuncParam)) // Размер верен
        { int icon;
          const char *msg;
          // В зависимости от типа сообщения выбираем иконку для него
          switch(Param->Type)
            { case UMFP_TYPE_INF:  // Информация
                icon=MB_ICONINFORMATION; break;
              case UMFP_TYPE_WARN: // Предупреждение
                icon=MB_ICONWARNING; break;
              default:             // Ошибка
                icon=MB_ICONERROR;
            }
          if(Param->Message) // Передан текст сообщения
            msg=Param->Message;
          else // Текст не передан
            msg="Сообщение";
          // Вывод сообщения пользователю
          rdsMessageBox(msg,rdsbcppBlockData->BlockName,icon|MB_OK);
        }

Сначала, как и в предыдущих примерах, мы проверяем, переданы ли для функции правильные параметры. Параметр Param должен указывать на структуру TUserMessageFuncParam: если он равен NULL, или если поле servSize (оно в структуре первое) меньше размера этой структуры, значит, параметры переданы неверно, и блок не может ничего вывести. Если обе проверки прошли, то, в зависимости от поля Type переданной структуры, мы присваиваем целой переменной icon одну из стандартных констант Windows API: MB_ICONINFORMATION, MB_ICONWARNING или MB_ICONERROR. Эти константы при выводе сообщения пользователю указывают иконку, изображаемую рядом с текстом сообщения. Затем в переменную msg записывается либо указатель на текст сообщения, полученный из поля Message структуры параметров функции, либо, если вместо текста почему-то передали NULL, указатель на строку «сообщение». После этого вызывается сервисная функция RDS rdsMessageBox, выводящая пользователю окно с сообщением msg, иконкой icon и единственной кнопкой «OK». От стандартной функции Windows API MessageBox она отличается только тем, что не блокирует выполнение расчета.

После компиляции модели созданного блока все блоки в одной с ним подсистеме и подсистемах, вложенных в нее, получат возможность вызывать у него функцию «UserManual.Message». Создадим еще один блок, который будет выводить сообщение пользователю при превышении значением входа некоторого заданного уровня. У блока будет три вещественных входа: «x», «L» и «delta». Как только «x» превысит «L» на «delta», блок выведет пользователю сообщение об этом, и будет «молчать», пока «x» не станет меньше «L» минус «delta». Такой гистерезис мы вводим для того, чтобы небольшие колебания «x» вокруг «L» не приводили к каскаду сообщений, каждое из которых пользователь должен будет закрывать кнопкой «OK». Для того, чтобы блок помнил, можно ли ему сейчас выводить сообщение или нет, мы введем в него внутреннюю логическую переменную «out»: она будет получать значение 1, если «x» превысит «L»+«delta» и сообщение будет выведено, и сбрасываться в 0, как только «x» станет меньше «L»−«delta». Таким образом, наш блок будет иметь следующую структуру переменных:

Имя | Тип | Вход/выход | Пуск | Начальное значение
--- | --- | --- | --- | ---
Start | Сигнал | Вход | ✓ | 0
Ready | Сигнал | Выход |  | 0
x | double | Вход | ✓ | 0
L | double | Вход | ✓ | 0
out | Логический | Внутренняя |  | 0
delta | double | Вход | ✓ | 0.1

Создадим в схеме новый блок с автокомпилируемой моделью и зададим для него указанную структуру переменных. Этот блок будет искать исполнителя функции «UserManual.Message» и вызывать эту функцию для сообщения, поэтому в его модели нам потребуется описание структуры TUserMessageFuncParam. Откроем вкладку глобальных описаний модели (вкладка «события» – раздел «описания» – двойной щелчок на пункте «глобальные описания») и введем на ней команду включения файла «UserMessage.h», в котором описана эта структура:

      #include "UserMessage.h"

Теперь добавим в модель функцию «UserManual.Message» и укажем, что блок должен найти исполнителя этой функции. На вкладке «функции» левой панели редактора нажмем кнопку «+» и заполним открывшееся окно согласно рис. 479.

Рис. 479. Добавление в модель блока функции «UserManual.Message»
(рамкой выделен флажок, включающий поиск исполнителя функции)

В целом, окно заполнено почти так же, как и у предыдущего блока (рис. 477), за несколькими исключениями. На верхней панели окна опять выбран флажок «произвольная функция» – добавляемая функция не является стандартной. На панели «имя функции в RDS» снова введено название нашей функции. Поле «объект для функции в программе», в отличие от предыдущей модели, мы меняем на «rdsfuncUM_Message»: этот блок вызывает функцию, поэтому имя объекта, который мы будем использовать для этого, сделаем покороче. В поле «тип параметра функции» введен уже знакомый нам тип указателя на структуру «TUserMessageFuncParam*». А на панели «дополнительные действия» теперь выбран флажок «найти в схеме исполнителя функции».

Рядом с именем добавленной нами функции в списке функций (рис. 480), помимо иконки с пустым листом (текст реакции на эту функцию не введен), будет находиться изображение лупы: оно указывает на то, что блок будет запрашивать у RDS поиск исполнителя этой функции, и, поэтому, ее можно вызывать без указания идентификатора вызываемого блока. Теперь, с момента подключения модели к блоку и до ее отключения, наш блок будет знать ближайшего по иерархии исполнителя функции «UserManual.Message», если, конечно, такой исполнитель существует в схеме.

Рис. 480. Список функций в редакторе модели после добавления «UserManual.Message»
и включения поиска ее исполнителя

Теперь введем реакцию блока на такт расчета, из которой мы и будем вызывать функцию. На вкладке «события» левой панели редактора раскроем раздел «моделирование и режимы», дважды щелкнем на пункте «модель» и введем на открывшейся одноименной вкладке следующий текст:

      if(x>L+delta) // Вход выше уровня с запасом
        { if(!out) // Сообщение об этом еще не выводилось
            { TUserMessageFuncParam param; // Параметры функции
              param.servSize=sizeof(param);// Размер структуры
              param.Type=UMFP_TYPE_WARN;   // Предупреждение
              // Формируем динамическую строку с текстом сообщения
              param.Message=
                rdsDynStrCat(rdsbcppBlockData->BlockName,
                             " - превышение уровня",FALSE);
              // Вызываем функцию у исполнителя, кем бы он ни был
              rdsfuncUM_Message.Call(&param);
              // Освобождаем динамическую строку
              rdsFree(param.Message);
              out=1; // Запоминаем факт вывода сообщения
            }
        }
      else if(x<L-delta) // Вход ниже уровня с запасом
        out=0; // Теперь снова разрешаем вывод сообщения

Сначала мы проверяем, не превысило ли значение x значение L с запасом delta. Если это так, и сообщение о превышении уровня еще не выводилось (то есть логический флаг out не взведен), мы должны вывести пользователю сообщение через исполнителя функции «UserManual.Message». Для вызова функции нам потребуется структура типа TUserMessageFuncParam – назовем объект этой структуры param. В поле servSize этой структуры мы записываем ее размер, полученный при помощи оператора sizeof, а в поле Type – константу UMFP_TYPE_WARN из файла «UserMessage.h» (он уже включен в глобальных описаниях), означающую предупреждающее сообщение. В поле Message при помощи функции rdsDynStrCat мы формируем динамическую строку, состоящую из имени данного блока и слов «превышение уровня». Эту строку потом нужно будет освободить вызовом rdsFree.

Теперь все поля структуры параметров заполнены – можно вызывать функцию. Для этого мы используем функцию-член Call объекта rdsfuncUM_Message, созданного для «UserManual.Message». В эту функцию-член мы передаем только указатель на структуру param, идентификатор вызываемого блока не передается. Именно отсутствие идентификатора блока в вызове Call говорит о том, что функция вызывается у ее найденного исполнителя, а не у какого-то другого блока. Вызвав функцию, мы освобождаем ранее созданную строку param.Message при помощи rdsFree и взводим флаг out, чтобы не выводить сообщение повторно, пока x не снизится.

Если же значение x оказалось ниже значения L с запасом delta, мы сбрасываем out, чтобы при следующем превышении снова вывести сообщение.

Для проверки работы созданных моделей можно собрать схему, изображенную на рис. 481. Если запустить расчет и дать x значение, большее суммы L и delta, на экране появится сообщение о превышении уровня, выведенное блоком-исполнителем, с указанием имени блока, в котором это превышение возникло. Если создать подсистему и поместить туда блоки, проверяющие уровень (копии блока «Block22» на рисунке), эти блоки будут выводить такие же сообщения, поскольку RDS будет предоставлять им доступ к блоку-исполнителю.

Рис. 481. Схема для тестирования исполнителя функции и выводимое сообщение:
здесь «Block1» – исполнитель, «Block22» – блок проверки уровня

Может показаться, что вывод сообщения было бы гораздо проще включить непосредственно в модель блока, проверяющего уровень. Это действительно так, но если в схеме будет много блоков с разными моделями, выводящими сообщения, и мы захотим что-то изменить в процедуре вывода, нам придется изменять все эти модели блоков. В данном же случае нам достаточно будет изменить модель блока-исполнителя или просто заменить его на другой.

Сделаем новую модель блока-исполнителя той же самой функции «UserManual.Message», которая, вместо вывода сообщения на экран, будет записывать его в текстовый файл с указанием времени его поступления. Проще всего будет не создавать новый блок с нуля, а сделать копию старого блока и его модели, а затем изменить эту модель. Заодно мы продемонстрируем влияние иерархии подсистем на доступность исполнителя – поместим копию блока в отдельной подсистеме, и посмотрим, какие блоки свяжутся с каким исполнителем.

Создадим в той же подсистеме, в которой мы тестировали два недавно созданных блока (см. рис. 481), новую подсистему. Скопируем старый блок-исполнитель в буфер обмена (в режиме редактирования выделим его и нажмем Ctrl + C), а затем перейдем в созданную подсистему, нажмем в том месте ее рабочего поля, куда будет вставляться копия блока, правую кнопку мыши, и выберем в меню пункт «вставить». Модуль автокомпиляции при этом спросит, хотим ли мы оставить у копии блока старую модель или нужно копировать и модель тоже (рис. 482, см. также §3.4). Мы хотим сделать копию модели, поэтому следует выбрать варианты «создать копию файла модели» или «создать копию файла модели, введя имя вручную».

Рис. 482. Запрос модуля автокомпиляции на создание копии модели

Откроем редактор модели скопированного блока и начнем исправлять ее. Прежде всего, для того, чтобы записывать сообщения в файл, нам потребуется как-то задавать имя этого файла. Кроме того, нужно предусмотреть возможность стирания этого файла в момент загрузки схемы (новый сеанс работы со схемой – новый журнал), поэтому мы введем в новую модель два настроечных параметра: строку «FileName» с именем файла и логический параметр «ClearOnLoad», который будет управлять стиранием файла.

Выберем на левой панели редактора вкладку «настройки», нажмем в ее верхней части кнопку «+» (см. рис. 344) и заполним окно добавления настроечного параметра следующим образом:

- на панели «переменная»:
  - «имя» – «FileName»;
  - «тип» – «rdsbcppString» (это специальный класс для хранения строк, создаваемый модулем автокомпиляции);
  - «по умолчанию» – оставлено пустым;
- на панели «поле ввода»:
  - «добавить для этой переменной поле ввода» – установлен флажок;
  - «заголовок» – «имя файла»;
  - «ширина» – 200;
  - «тип» – «сохранение файла».

После нажатия кнопки «OK» в модели блока появится настроечный параметр с именем «FileName», для которого, по желанию пользователя, будет открываться стандартный диалог сохранения файла Windows. Точно так же, еще раз нажав «+», добавим второй настроечный параметр:

- на панели «переменная»:
  - «имя» – «ClearOnLoad»;
  - «тип» – «BOOL» (стандартный логический тип Windows);
  - «по умолчанию» – «TRUE»;
- на панели «поле ввода»:
  - «добавить для этой переменной поле ввода» – установлен флажок;
  - «заголовок» – «очищать при загрузке»;
  - «ширина» – не важно;
  - «тип» – «флаг».

Окно для ввода параметров и процедуры их загрузки и сохранения вместе со схемой будут добавлены в модель автоматически (см. §3.5.6).

Дадим окну настройки заголовок «журнал» (для этого нужно вызвать панель параметров окна кнопкой «размеры и параметры окна» в правой верхней части нижней половины вкладки «настройки» (см. рис. 350). Теперь вкладка «настройки» должна выглядеть так, как на рис. 483.

Рис. 483. Настроечные параметры нового блока-исполнителя

Для того, чтобы добавлять к сообщению текущее время, нам потребуется стандартная функция sprintf, а это значит, что нам нужно включить в модель файл «stdio.h». Откроем вкладку глобальных описаний (вкладка «события» – раздел «описания» – двойной щелчок на пункте «глобальные описания») и добавим к уже имеющейся там команде включения файла «UserMessage.h» новую (изменения выделены цветом):

      #include "UserMessage.h"
      #include <stdio.h>

Мы решили дать пользователю возможность стирать текстовый файл с сообщениями в момент загрузки схемы, чтобы он не разрастался до бесконечности. В этом нам поможет реакция на событие RDS_BFM_AFTERLOAD, возникающее у всех блоков немедленно после загрузки схемы из файла. Откроем ее вкладку: на вкладке «события» раскроем раздел «загрузка и запись данных» и дважды щелкнем на его пункте «после загрузки схемы». Введем на этой вкладке следующий текст:

      char *path; // Переменная для пути к файлу

      if(!ClearOnLoad) // Не нужно стирать файл
        return;
      // Файл должен быть стерт

      // Добавляем к имени файла путь, если его там нет
      path=rdsGetFullFilePath(FileName.c_str(),NULL,NULL);
      if(path!=NULL) // Удачно
        { DeleteFile(path); // Удаляем файл
          rdsFree(path); // Освобождаем память строки
        }

Здесь мы сначала проверяем, нужно ли стирать файл. Если настроечный параметр ClearOnLoad не равен TRUE, стирать файл не нужно, мы немедленно завершаем реакцию. В противном случае мы вызываем функцию rdsGetFullFilePath, которая преобразует сокращенный путь к файлу из настроечного параметра FileName в полный (например, добавит к нему путь к файлу схемы, если в самом параметре путь отсутствует). Поскольку FileName – это объект класса rdsbcppString, а функция rdsGetFullFilePath требует строки типа char*, мы вызываем у FileName функцию-член c_str, которая возвращает указатель на строку, хранящуюся в этом классе. Путь, который возвращает rdsGetFullFilePath, это динамическая строка, поэтому нам нужно будет потом освободить ее при помощи rdsFree.

Если переменная path, которой мы присвоили результат вызова rdsGetFullFilePath, не равна NULL (то есть строку из FileName удалось преобразовать в полный путь к файлу), мы удаляем файл по этому пути функцией Windows API DeleteFile и освобождаем строку path вызовом rdsFree.

Теперь изменим реакцию на вызов функции «UserManual.Message» – фактически, ее нужно полностью переписать. Откроем вкладку этой реакции (вкладка «события» – раздел «функции блока» – двойной щелчок на пункте «UserManual.Message»), и заменим текст, который там находится, на следующий:

      // Локальные переменные
      const char *msg,*stype;
      char *path,buf[100];
      HANDLE h; // Дескриптор файла
      DWORD temp;
      SYSTEMTIME time; // Дата и время из Windows

      if(Param==NULL) // Параметр не передан
        return;
      if(Param->servSize<sizeof(TUserMessageFuncParam))
        return; // Размер параметра неверен

      // Тип сообщения
      switch(Param->Type)
        { case UMFP_TYPE_INF:  stype="Информация"; break;
          case UMFP_TYPE_WARN: stype="Предупреждение"; break;
          default: stype="Ошибка";
        }
      // Текст сообщения
      if(Param->Message)
        msg=Param->Message;
      else
        msg="Сообщение";

      // Получение полного пути к файлу
      path=rdsGetFullFilePath(FileName.c_str(),NULL,NULL);
      if(path==NULL) // Не удалось сформировать путь
        return;
      // Открываем файл path на запись
      h=CreateFile(path,GENERIC_WRITE,0,NULL,
                   OPEN_ALWAYS,FILE_ATTRIBUTE_NORMAL,NULL);
      rdsFree(path); // Строка path больше не нужна
      if(h==INVALID_HANDLE_VALUE) // Ошибка открытия файла
        return;
      // Перемещаем указатель файла в конец
      SetFilePointer(h,0,NULL,FILE_END);
      // Получаем текущую дату и время
      GetLocalTime(&time);
      // Формируем строку с датой и временем в buf
      sprintf(buf,"%02d-%02d-%04d %02d:%02d:%02d ",
              time.wDay,time.wMonth,time.wYear,
              time.wHour,time.wMinute,time.wSecond);
      // Записываем дату и время в файл
      WriteFile(h,buf,strlen(buf),&temp,NULL);
      // Записываем тип сообщения
      WriteFile(h,stype,strlen(stype),&temp,NULL);
      // Переводим строку и добавляем два пробела
      WriteFile(h,"\r\n  ",4,&temp,NULL);
      // Выводим текст сообщения
      WriteFile(h,msg,strlen(msg),&temp,NULL);
      // Переводим строку
      WriteFile(h,"\r\n",2,&temp,NULL);
      // Закрываем файл
      CloseHandle(h);

Реакция получилась довольно длинной, но большая ее часть состоит из команд записи в файл. В самом ее начале описано несколько вспомогательных переменных, которые будут использоваться для работы с файлом и получения текущего времени.

Прежде всего мы проверяем, был ли передан параметр при вызове функции (Param==NULL – не был передан) и достаточен ли размер переданной структуры для работы функции (Param->servSize не меньше размера структуры TUserMessageFuncParam). Если это не так, работа блока невозможна, и реакция немедленно завершается. В противном случае вспомогательной переменной stype, в зависимости от константы из Param->Type, присваивается указатель на строку «информация», «предупреждение» или «ошибка», а переменной msg – указатель на текст сообщения из Param->Message или на строку «сообщение», если в Param->Message находится NULL.

Теперь нужно открыть текстовый файл на запись. Сначала, как и в реакции на загрузку всей схемы, рассмотренной ранее, вызовом rdsGetFullFilePath мы добавляем к имени файла из настроечного параметра FileName путь к схеме, если в этом параметре отсутствует путь. Возвращенный указатель на созданную динамическую строку записывается в переменную path, потом эту строку нужно будет удалить вызовом rdsFree. Затем мы открываем файл на запись при помощи функции Windows API CreateFile с константой GENERIC_WRITE и присваиваем дескриптор открытого файла переменной h. Сразу после этого строка path освобождается при помощи вызова rdsFree – она больше не нужна. Если файл открыть не получилось, CreateFile вернет вместо дескриптора константу INVALID_HANDLE_VALUE, и, в этом случае, мы завершаем выполнение реакции.

Ели файл открыт, мы перемещаем указатель записи в его конец вызовом функции Windows API SetFilePointer – теперь мы должны дописать в него сообщение с текущей датой и временем. Дату и время мы записываем в структуру time стандартного типа SYSTEMTIME вызовом GetLocalTime, а потом формируем во вспомогательном массиве buf ее символьное представление в виде «день–месяц–год час:минута:секунда» функцией sprintf ( «stdio.h», в котором она описана, мы уже включили в модель). После этого, вызывая функцию Windows API WriteFile, в открытый файл с дескриптором h мы последовательно записываем:

- дату и время из buf;
- тип сообщения из stype;
- перевод строки с возвратом каретки («\r\n») и два пробела;
- текст сообщения из msg;
- перевод строки с возвратом каретки.

Закончив запись, мы закрываем файл функцией Windows API CloseHandle.

Может показаться, что открытие файла перед записью каждого сообщения и закрытие после нее – не самый лучший способ ведения журнала сообщений. Можно было бы открыть файл в момент поступления первого сообщения и не закрывать его до конца работы, однако, это привело бы к некоторым проблемам. Пока файл открыт, пользователь не смог бы получить к нему доступ – например, удалить его. Кроме того, если мы закрываем файл после записи сообщения, мы можем использовать этот файл в нескольких блоках одновременно, и они смогут дописывать свои сообщения в его конец, не мешая друг другу. Мы предполагаем, что сообщения в схеме будут возникать не очень часто (в противном случае стоит пересмотреть способ ведения журнала), и постоянные открытия-закрытия файла не будут существенно замедлять ее работу.

Рис. 484. Настройка блока записи сообщений в файл

Модель закончена – перейдем к ее тестированию. Скомпилируем ее, закроем редактор, вызовем настройки блока (щелчок правой кнопкой мыши на нем – пункт меню «настройка») и введем в поле имени файла «Log.txt» (рис. 484). Поскольку мы не указали путь, файл Log.txt будет размещен в той же папке, что и схема (а также и модели, если мы не указывали пути к файлам моделей при их создании), так что его будет легко найти. Теперь скопируем в созданную для этого блока подсистему еще и блок, проверяющий превышение уровня, со всеми подключенными к нему полями ввода («Block22» на рис. 481). Для этого выделим блок вместе с полями ввода, нажмем Ctrl + C, перейдем в подсистему, нажмем в том месте ее рабочего поля, куда будет вставляться копия блока, правую кнопку мыши, после чего выберем в меню пункт «вставить». На запрос модуля автокомпиляции о том, нужно ли создать копию модели (см. рис. 482), нужно ответить «использовать тот же файл модели», чтобы обе копии блока имели одну модель. В результате всех этих действий схема станет похожа на рис. 485. На нем «Block22» и «Block2» – блоки проверки уровня, «Sys1» – подсистема, в которую мы поместили новый блок-исполнитель, «Block1» – старый исполнитель, «Block11» – новый исполнитель.

Рис. 485. Тестирование второго блока-исполнителя

Если теперь запустить расчет, превышения уровня в блоке «Block22» будут, как и прежде, приводить к выводу сообщений на экран. Но если уровень будет превышен в блоке «Block2», сообщение об этом будет записано в файл «Log.txt» в виде следующих двух строчек:

      18-08-2013 15:11:44 Предупреждение
        Block2 - превышение уровня

Модели блоков «Block2» и «Block22» одинаковы, каждая из них для вывода сообщения обращается к блоку-исполнителю функции «UserManual.Message». Но для «Block22» этим исполнителем будет «Block1», а для «Block2» – «Block11», поскольку он находится ближе в иерархии подсистем. Если удалить из схемы «Block11», превышение уровня в любом из блоков будет выводить сообщение на экран: для «Block22» ничего не изменится, а для «Block2» ближайшим в иерархии исполнителем функции станет «Block1». Если же удалить «Block1», «Block22» вообще не сможет вывести сообщение, поскольку ни в его подсистеме, ни выше по иерархии исполнителей не осталось, а «Block2» будет продолжать выводить сообщения в файл.