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

Описания RDS

Руководство программиста

Глава 2. Создание моделей блоков

§2.8. Сохранение и загрузка параметров блока

§2.8.3. Сохранение параметров в текстовом формате

# Руководство программиста

## Глава 2. Создание моделей блоков

### §2.8. Сохранение и загрузка параметров блока

#### §2.8.3. Сохранение параметров в текстовом формате

Рассматривается сохранение и загрузка параметров блока в текстовом формате, описываются соответствующие реакции модели (RDS_BFM_SAVETXT и RDS_BFM_LOADTXT). В модель ранее описанного в примерах блока-генератора добавляются процедуры для хранения его параметров в текстовом виде.

Добавим в блок, выдающий на выход синусоиду, косинусоиду или прямоугольные импульсы, загрузку и сохранение параметров в текстовом формате. В личной области данных этого блока хранятся три параметра: целый Type, задающий тип формируемой функции (0 – синус, 1 – косинус, 2 – прямоугольные импульсы), и вещественные Period и Impulse, задающие период функции и длительность прямоугольного импульса соответственно. Поскольку этот блок вполне может использоваться на практике в качестве генератора сигналов, целесообразно хранить его параметры именно в текстовом виде. Это позволит, при необходимости, модифицировать его модель, не опасаясь того, что схемы с этим блоком перестанут читаться.

Личная область данных блока оформлена в виде класса. Добавим в его описание две новых функции-члена: функцию SaveText для сохранения параметров и функцию LoadText для их загрузки. Теперь описание класса будет выглядеть следующим образом (изменения выделены цветом):

      //====== Класс личной области данных ======
      class TTestGenData
      { public:
          int Type;         // Тип (0-sin,1-cos,2-прямоугольные)
          double Period;    // Период
          double Impulse;   // Длительность импульса

          RDS_PDYNVARLINK Time; // Связь с динамической
                                // переменной времени

          int Setup(void);           // Функция настройки
          void SaveText(void);       // Сохранение параметров
          void LoadText(char *text); // Загрузка параметров
          TTestGenData(void)         // Конструктор класса
            { Type=0; Period=1.0; Impulse=0.5;
          // … далее без изменений …
      };

В функцию модели блока необходимо добавить операторы case для вызова этих функций:

      //============= Модель блока ==============
      extern "C" __declspec(dllexport)
        int RDSCALL TestGen(int CallMode,
                            RDS_PBLOCKDATA BlockData,
                            LPVOID ExtParam)
      {
      // Макроопределения для статических переменных
      #define pStart ((char *)(BlockData->VarTreeData))
      #define Start  (*((char *)(pStart)))
      #define Ready  (*((char *)(pStart+1)))
      #define y      (*((double *)(pStart+2)))
        // Вспомогательная переменная – указатель на личную область
        // данных блока, приведенный к правильному типу
        TTestGenData *data;

        switch(CallMode)
          { // Инициализация
            case RDS_BFM_INIT:
              BlockData->BlockData=new TTestGenData();
              break;
            // Очистка
            case RDS_BFM_CLEANUP:
              data=(TTestGenData*)(BlockData->BlockData);
              delete data;
              break;
            // Проверка типа переменных
            case RDS_BFM_VARCHECK:
              if(strcmp((char*)ExtParam,"{SSD}")==0)
                return RDS_BFR_DONE;
              return RDS_BFR_BADVARSMSG;

            // Запись параметров в текстовом формате
            case RDS_BFM_SAVETXT:
              data=(TTestGenData*)(BlockData->BlockData);
              data->SaveText();
              break;
            // Загрузка параметров в текстовом формате
            case RDS_BFM_LOADTXT:
              data=(TTestGenData*)(BlockData->BlockData);
              data->LoadText((char*)ExtParam);
              break;
            // Функция настройки
            case RDS_BFM_SETUP:
              data=(TTestGenData*)(BlockData->BlockData);
              return data->Setup();

            // … далее без изменений …

          }
        return RDS_BFR_DONE;
      // Отмена макроопределений
      #undef y
      #undef Ready
      #undef Start
      #undef pStart
      }
      //=========================================

При сохранении схемы модель блока будет вызвана с параметром RDS_BFM_SAVETXT, что приведет к вызову функции-члена SaveText класса личной области данных блока TTestGenData (перед этим указатель на личную область данных, приведенный к нужному типу, записывается во вспомогательную переменную data). Эта функция должна сформировать текст, который будет записан в схему вместе с остальными данными, относящимися к описанию этого блока. При загрузке схемы модель будет вызвана с параметром RDS_BFM_LOADTXT, при этом в ExtParam будет содержаться указатель на начало текста, считанного из файла схемы. Это тот самый текст, который был сформирован функцией-членом SaveText в момент сохранения. После приведения указателя на текст к типу char* он передается в функцию-член LoadText, задача которой – разобрать его и присвоить нужные значения параметрам блока.

Рассмотрим сначала функцию сохранения параметров SaveText, поскольку от выбранного формата записи будет зависеть способ разбора текста в функции загрузки. Для каждого параметра будем записывать пару «ключевое_слово значение»: значение параметра Type после слова «type», значение Period после слова «period», значение Impulse – после «impulse». На самом деле, значение параметра Impulse нужно сохранять только тогда, когда выбрано формирование прямоугольных импульсов (значение Type равно 2), поскольку для синуса и косинуса длительность импульса не задается. Тем не менее, пока, для упрощения примера, мы будем всегда сохранять все три параметра. Функция SaveText будет выглядеть следующим образом:

      // Функция сохранения параметров
      void TTestGenData::SaveText(void)
      { char buffer[100]; // Буфер для формирования текста
        // Формирование текста в буфере при помощи функции sprintf
        sprintf(buffer,
                "type %d period %lf impulse %lf",
                Type,Period,Impulse);
        // Передача сформированного текста в RDS
        rdsWriteBlockDataText(buffer,FALSE);
      }
      //=========================================

Прежде всего в функции создается символьный массив buffer размером в 100 символов, в котором будет формироваться текст. Необходимо сохранить целое число, два вещественных числа двойной точности и три ключевых слова – ста символов должно хватить для этого с большим запасом. Далее, при помощи стандартной функции sprintf, в этот массив записывается строка, содержащая ключевые слова и значения параметров. Из-за присутствия функции sprintf для успешной компиляции этого примера необходимо включить в исходный текст стандартный файл заголовка «stdio.h», в котором она описана. Ключевые слова содержатся в строке формата (второй аргумент функции sprintf). В сформированном в массиве buffer тексте вместо спецификаторов формата «%d» и «%lf» появятся значения полей класса Type, Period и Impulse, в том порядке, в котором они были переданы в функцию sprintf. В результате, в массиве buffer окажется строка вида «type 0 period 1.000000 impulse 0.500000» (в данном случае для примера использованы значения параметров блока по умолчанию, заданные в конструкторе).

После того, как строка со значениями параметров блока сформирована, необходимо передать ее в RDS при помощи сервисной функции rdsWriteBlockDataText:

      void RDSCALL rdsWriteBlockDataText(
        LPSTR String,   // Передаваемый текст
        BOOL NewLine);  // Перевод строки перед текстом

Эта функция добавляет к тексту, уже записанному для данного блока, фрагмент, указатель на который передается в параметре String. При этом перед добавляемым фрагментом вставляется перевод строки, если значение параметра NewLine истинно, или пробел, если значение NewLine ложно. В данном примере мы одним вызовом передаем в RDS весь сформированный текст, но при необходимости, можно передавать его по частям несколькими вызовами rdsWriteBlockDataText. Например, можно было бы записывать значение параметра Impulse, только если блок формирует прямоугольные импульсы. В этом случае функция SaveText выглядела бы так:

      // Функция сохранения параметров – вариант с условием
      void TTestGenData::SaveText(void)
      { char buffer[100]; // Буфер для формирования текста
        // Формирование текста для Type и Period
        sprintf(buffer,
                "type %d period %lf",
                Type,Period);
        // Передача сформированного текста в RDS
        rdsWriteBlockDataText(buffer,FALSE);
        // Запись Impulse если Type равно 2
        if(Type==2)
         { // Формирование текста
           sprintf(buffer,"impulse %lf",Impulse);
           // Передача текста в RDS
           rdsWriteBlockDataText(buffer,FALSE);
         }
      }
      //=========================================

Здесь сначала формируется и передается текст для параметров Type и Period, а затем, если значение параметра Type равно двум, в том же массиве buffer формируется и передается в RDS текст для параметра Impulse. Перед ключевым словом «impulse» будет автоматически добавлен пробел, поскольку второй параметр rdsWriteBlockDataText равен FALSE, и это слово не сольется со значением параметра Period, которым заканчивается текст, переданный в первом вызове. Можно было бы вместо FALSE указать TRUE, тогда ключевое слово «impulse» и значение параметра разместились бы на отдельной строке. Заметим, что первый вызов функции также производится с параметром FALSE, то есть перед ключевым словом «type» тоже должен был бы быть добавлен пробел, однако RDS автоматически удаляет начальные пробелы и переводы строк в сохраняемом тексте, поэтому этот пробел не будет записан.

Теперь для блока с такой моделью при сохранении в текстовом формате будет записан текст, выглядящий примерно так (результат работы функции SaveText выделен цветом):

      dllblock name "Block1"
       begin
        pos 40 30
        layer id 1
        vars
         begin
          signal name "Start" in menu run default 0
          signal name "Ready" out menu default 0
          double name "y" out menu default 0
         end
        dll file "p2_8_3.32.dll" func "TestGen@12" file64 "p2_8_3.64.dll" func64 "TestGen" hint setup "" auto
        dlldata text
         type 2 period 1.000000 impulse 0.500000
        enddlldata
       end

Текст, сформированный моделью блока, размещается между строками «dlldata text» и «enddlldata». По ключевому слову «enddlldata» RDS опознает конец текста, поэтому при сохранении параметров блока в текстовом формате ни в коем случае нельзя начинать какую-либо строку с этого слова. При загрузке блока RDS выделяет из файла или буфера обмена текст, заключенный между этими двумя строками, удаляет из каждой строки начальные и конечные пробелы и символы табуляции, после чего этот текст передается в функцию модели как единый массив символов.

Теперь рассмотрим функцию загрузки параметров LoadText, которая будет обрабатывать текст, сформированный при сохранении. Эта функция должна опознавать в переданном ей тексте ключевые слова «type», «period» и «impulse», и считывать из следующего слова значение конкретного параметра. Разбор текста и поиск ключевых слов – достаточно часто встречающаяся задача. Чтобы не загромождать пример, будем использовать в нем сервисную функцию RDS, выделяющую первое слово из текста – это стандартная операция, и нет большого смысла расписывать ее полностью. Функция LoadText будет извлекать из переданного ей текста слово за словом и сравнивать их с ключевыми, до тех пор, пока текст не закончится:

      // Функция загрузки параметров
      void TTestGenData::LoadText(char *text)
      { char *word,*ptr,c;

        // Установка указателя ptr на начало переданного текста
        ptr=text;
        // Цикл по словам в тексте
        for(;;)
          { // Получить из текста очередное слово
            word=rdsGetTextWord(ptr,&ptr,&c,TRUE);
            if(c==0) // Текст закончился – выход из цикла
              break;
            if(c=='\n') // Перевод строки – пропускаем и продолжаем
              continue;
            if(strcmp(word,"type")==0) // Тип сигнала
              { // Следующее слово – целое число
                word=rdsGetTextWord(ptr,&ptr,NULL,FALSE);
                Type=atoi(word);
              }
            else if(strcmp(word,"period")==0) // Период
              { // Следующее слово - число double
                word=rdsGetTextWord(ptr,&ptr,NULL,FALSE);
                Period=atof(word);
              }
            else if(strcmp(word,"impulse")==0) // Длительность импульса
              { // Следующее слово - число double
                word=rdsGetTextWord(ptr,&ptr,NULL,FALSE);
                Impulse=atof(word);
              }
            else // Неопознанное ключевое слово
              break; // Ошибка – прекращаем обработку
          } // Конец цикла for(;;)
      }
      //=========================================

В начале функции описываются три вспомогательные переменные: word, в которую будет помещаться указатель на очередное слово строки; ptr, в которой будет храниться указатель на текущую позицию в тексте; и c, в которую сервисная функция RDS будет помещать первый символ считанного из строки слова или служебную информацию. Сама функция состоит из «бесконечного» цикла for(;;), перед которым в переменную ptr помещается указатель на начало переданного в функцию текста text.

Для извлечения слова из текста будет использоваться сервисная функция RDS rdsGetTextWord:

      LPSTR RDSCALL rdsGetTextWord(
        LPSTR Start,      // Начало текста
        LPSTR *pNext,     // Указатель на следующее слово
        char *pSym,       // Тип слова или первый символ
        BOOL LowerCase);  // Перевести в нижний регистр

В параметре Start в эту функцию передается указатель на текст, из которого нужно извлечь первое слово. Функция считает словом любую последовательность символов, ограниченную пробелами, табуляциями или переводами строк, или любую последовательность символов в двойных кавычках. Перевод строки сам по себе тоже считается словом из одного символа с кодом 10 (0x0A или «\n» в терминах языка C). Первое слово текста копируется во внутренний буфер RDS и, если параметр LowerCase равен TRUE, переводится в нижний регистр. Функция возвращает указатель на этот внутренний буфер, при этом указатель на начало следующего слова записывается по адресу, переданному в параметре pNext, а однобайтовый тип считанного слова – по адресу, переданному в параметре pSym. В качестве типа слова возвращается первый символ этого слова или одна из следующих констант:

- 0 – текст закончился, больше нет слов;
- 10 («\n») – перевод строки;
- двойная кавычка («\"») – считанное слово было строкой в кавычках.

Нас здесь будут интересовать только два первых варианта – конец текста, при обнаружении которого нужно выйти из цикла, и перевод строки, который нужно игнорировать.

Поскольку текст с параметрами блока состоит из пар слов вида «ключевое_слово значение», в цикле необходимо считывать ключевое слово, опознавать его, после чего брать из следующего за ключевым слова значение соответствующего параметра. В самом первом операторе цикла производится чтение из текста очередного слова, которое будет потом сравниваться с ключевыми – указатель на внутренний буфер, в котором находится слово, присваивается переменной word:

      word=rdsGetTextWord(ptr,&ptr,&c,TRUE);

В качестве начала текста в функцию передается указатель на текущую позицию в тексте ptr, в эту же переменную будет записан указатель на следующее слово текста. Тип слова будет записан в переменную c. В параметре LowerCase в функцию передано значение TRUE, поэтому слово, извлеченное из текста, будет переведено в нижний регистр. Может показаться, что в данном случае перевод в нижний регистр не нужен – при сохранении параметров блока функцией SaveText все ключевые слова записываются в нижнем регистре, поэтому при чтении верхнему регистру будет просто неоткуда взяться. Однако, следует помнить, что файл схемы или блока в текстовом формате может быть отредактирован пользователем вручную, и, если он напишет «TYPE» или «Type» вместо «type», ключевое слово может быть не опознано функцией LoadText. Поэтому, желательно либо переводить все считанные слова в нижний регистр, либо сравнивать их с ключевыми без учета регистра.

После того, как слово считано, нужно проверить его тип. Если достигнут конец текста, и больше слов в нем нет (c==0), выполняется выход из цикла. Если вместо слова считан перевод строки (c=='\n'), его нужно пропустить и считать следующее слово. В противном случае следует сравнить считанное слово с одним из трех ключевых.

Сначала, при помощи стандартной функции strcmp (описана в файле заголовков «string.h»), считанное слово сравнивается со строкой «type». Если функция вернула 0, значит, строки совпали, и следующее слово представляет собой символьное представление целого числа, которое нужно занести в переменную Type. После первого вызова rdsGetTextWord указатель на начало следующего слова был записан в переменную ptr, поэтому еще один вызов вида word=rdsGetTextWord(ptr,&ptr,…) считает из текста следующее слово. Вообще, каждый такой вызов продвигает указатель ptr на одно слово вперед и возвращает очередное считанное слово через переменную word. Считанное таким образом значение типа формируемого сигнала переводится в целое число при помощи стандартной функции atoi и присваивается переменной Type.

Если strcmp вернула ненулевое значение, строки не совпадают, и word необходимо сравнить с другими ключевыми словами аналогичным образом. Проверка на ключевые слова «period» и «impulse» отличается от уже рассмотренного фрагмента только тем, что за этими ключевыми словами следуют вещественные значения, поэтому для преобразования их в число следует использовать функцию atof вместо atoi.

Следует отметить, что если между ключевым словом и значением попадется перевод строки, функция сработает неправильно, поскольку при вызове rdsGetTextWord для чтения значения мы не проверяем тип считанного слова – в параметре pSym передается NULL. Однако, это не является большим недостатком, поскольку перевод строки может оказаться там только после редактирования файла пользователем, а пользователю вряд ли придет в голову располагать название параметра и его значение на разных строках. В конце концов, при необходимости, можно запретить делать это в описании пользователя для разрабатываемого блока.

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

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

      for(;;)
        { // Получить из текста первое слово
          word1=rdsGetTextWord(ptr,&ptr,&c,TRUE);
          if(c==0) // Текст закончился – выход из цикла
            break;
          if(c=='\n') // Перевод строки – пропускаем и продолжаем
            continue;
          // Получить из текста второе слово (word1 будет утеряно!)
          word2=rdsGetTextWord(ptr,&ptr,NULL,FALSE);
          // Анализ
          if(strcmp(word1,"type")==0) // Тип сигнала
            Type=atoi(word2);
          else if(strcmp(word1,"period")==0) // Период
            Period=atof(word2);
          // …

Однако, приведенный фрагмент программы не будет работать. Первый в цикле вызов rdsGetTextWord считает ключевое слово во внутренний буфер и присвоит указатель на этот буфер переменной word1. Второй вызов считает следующее слово в этот же буфер, присвоив указатель на него word2. В результате, в лучшем случае первое считанное слово будет просто потеряно, в худшем – указатель, содержащийся в переменной word1, будет ссылаться на уже освобожденную область памяти (RDS может заново отвести память под буфер другого размера при очередном вызове rdsGetTextWord). В любом случае, после второго вызова rdsGetTextWord переменную word1 уже нельзя использовать, и вызов strcmp(word1,"type") приведет к непредсказуемым последствиям. Если необходимо считать все слова из текста до их анализа, можно использовать функцию rdsGetTextWordDyn, которая работает с текстом точно так же, как rdsGetTextWord, и имеет те же самые параметры, но вместо того, чтобы возвращать указатель на слово во внутреннем буфере, создает и возвращает динамическую строку с этим словом. Разумеется, строка, возвращенная rdsGetTextWordDyn, должна быть потом обязательно освобождена вызовом rdsFree.