Ввод, сохранение в бинарный файл и визуализация данных в среде Lazarus

14 мая 2020 15:33

В данной статье рассмотрим решение основных задач, встречаемых в практике при разработке прикладной программы. Для начала нужно ознакомиться с предыдущей статьей, связанной с сохранением данных в текстовый файл. Он удобен для совместимости с другими программами, но занимает слишком много места. Работа с бинарными файлами отличается скоростью обработки, малым размером файла, но требует знания четкой структуры этого файла. Есть отличия и в программных методах работы. Хранение оперативных данных будет осуществляться в динамическом массиве.
Типовые задачи:
1. Ввести некие данные;
2. Визуализировать их в форме графика;
3. Сохранение в бинарный файл;
4. Загрузка созданного файла и использование этих данных;
5. Удаление некорректного значения.

Сделано в последней на данный момент версии Lazarus 2.0.8 (в других вервиях возможны незначительное изменения)

Задачей будет ввод одного числового параметра в форму, отображение его на графике в зависимости от текущего времени. Сохранение происходит при закрытии программы, а загрузка - при открытии.
Вначале необходимо создать пустой проект и настроить его в меню Проект -> Параметры проекта
Сохранить проект в отдельной папке: Проект -> Сохранить проект как...
На форму поместить следующие элементы:
1. TLabeledEdit (из вкладки Addiitional)
2. TButton (из вкладки Standart) - для ввода (Button1) и удаления (Button2) записи.
3. TChsrt (из вкладки Chart)
Должно получиться так: (Специально не изменялись названия имен компонентов, хотя это рекомендуется в настоящем проекте)

Image
Интерфейс

Далее нужно создать серию для визуализации: Правой кнопкой мыши по графику -> Редактор диаграммы -> Добавить -> График

Image
График

Теперь нужно создать обработку событий в исходном коде программы.
Понадобятся четыре процедуры:
1. Инициализации procedure TForm1.FormShow(Sender: TObject); 
В инспекторе объектов выбрать Form1 -> вкладка События -> OnShow (нажать ...) Функция автоматически создается средой 
2. Деинициализация procedure TForm1.FormClose(Sender: TObject; var CloseAction: TCloseAction);
В инспекторе объектов выбрать Form1 -> вкладка События -> OnClose (нажать ...) Функция автоматически создается средой 
3. Обработка ввода (нажатия на кнопку Button1) procedure TForm1.Button1Click(Sender: TObject);
На форме совершить двойное нажатие на кнопку Button1
4. Обработка удаления (нажатия на кнопку Button2) procedure TForm1.Button2Click(Sender: TObject);
На форме совершить двойное нажатие на кнопку Button2

Получаем следующий шаблон в разделе implementation

implementation
 
{$R *.lfm}
 
{ TForm1 }
// Инициализация
procedure TForm1.FormShow(Sender: TObject);
begin
 
end;
// Деинициализация
procedure TForm1.FormClose(Sender: TObject; var CloseAction: TCloseAction);
begin
 
end;
// Обработка ввода
procedure TForm1.Button1Click(Sender: TObject);
begin
 
end; 
// Удаление последнего измерения    
procedure TForm1.Button2Click(Sender: TObject);
begin
 
end; 

Для объявления структуры файла лучше всего обозначить её в виде записи. Для этого объявляется новый тип данных в разделе type кода:

type
  // запись в базе данных
  TData = record
    time:TDateTime; // Поле1 времени
    Param:Double;   // Поле2 параметр
    // Можно добавлять любое количество параметров с определенным размером
  end;   

Задаем хранилище данных в динамическом массиве нового типа TData, для этого в разделе var дописываем объявление переменной Data:TStringList. 

var
  Form1: TForm1;
  DB:array of TData; // Динамический массив записей базы данных  

Программа будет преобразовывать дату и время в метку времени, затем в миллисекунды, а затем в минуты. Из каждого нового измерения будет вычитаться первое измерение. Таким образом, отсчет времени будет от нуля минут.

Задаем алгоритм инициализации:

// Инициализация
procedure TForm1.FormShow(Sender: TObject);
var i: integer; // Объявление локальной переменной целого типа
    rec: TData; // Хранение текущей извлеченной записи из файла
    start: TDateTime; // Хранение начального времени
    fData: File of TData; // переменная доступа к файлу определенного типа TData
begin
    Chart1LineSeries1.Clear; // Очистка данных в графике (серии)
    // Загрузка данных из файла
    if FileExists('data.dat') then // Если файл data.dat с данными существует
    begin                          // ... иначе фала data.dat нет (еще нет), а значит загружать ничего не надо
       AssignFile(fData,'data.dat'); // Указание имени файла для файловой переменной
       Reset(fData);    // Подключение к файлу для чтения
       while not Eof(fData) do  // Цикл пока не достигнут конец файла
       begin
             Read(fData, rec);  // Чтение из файла записи в переменную rec (указатель перемещается на следующую)
             SetLength(DB, Length(DB)+1); // Увеличить размер массива DB на одну запись.
             { Примечание: Length() - показывает количество записей в массиве }
             DB[HIGH(DB)].time := rec.time; // Присваивание считанного времени в запись массива
             DB[HIGH(DB)].Param := rec.Param; // Присваивание считанного параметра в запись массива
             { Примечание: HIGH() - выдает максимальный индекс в массиве, т.е. Length()-1}
             // Отображение загруженных данных на графике
             if Length(DB) = 1 then start := time; // Если это первое измерение, то записать время в переменную start
             rec.time:=TimeStampToMSecs(DateTimeToTimeStamp(rec.time)) - TimeStampToMSecs(DateTimeToTimeStamp(start)); // Расчет разницы текущего времени и начального тек. = тек. - нач.
           { Примечание:
             функция DateTimeToTimeStamp переводит переменную типа дата/время в спец. тип метки времени
             функция TimeStampToMSecs переводит переменную типа метка времени в миллисекунды
           }
           rec.time := rec.time /(1000*60); // Перевод из мс в минуты
           Chart1LineSeries1.AddXY(rec.time,rec.Param); // Добавить координаты времени и параметра в серию, отобразить их на графике
       end;
    CloseFile(fData); // Закрыть (освободить) файл
    end;
end;               

Задаем алгоритм деинициализации:

// Деинициализация
procedure TForm1.FormClose(Sender: TObject; var CloseAction: TCloseAction);
var i:Integer; // Объявление локальной переменной целого типа
    fData: File of TData; // переменная доступа файл определенного типа TData
begin
    // Сохранение данных в файл data.dat
    AssignFile(fData,'data.dat'); // Указание имени файла для файловой переменной
    Rewrite(fData);  // Подключение к файлу для записи (если был файл, то он будет перезаписан)
    if Length(DB)>0 then  // Если есть записи в базе, то ...
    begin
         for i:=0 to HIGH(DB) do // Перебрать все элементы  массива DB
         begin
             Write(fData, DB[i]); // ... и записать их в файл
         end;
    end;
    CloseFile(fData); // Закрыть (освободить) файл
    DB:=nil;  // Уничтожение объекта DB из памяти
end;           

Задаем алгоритм обработки ввода:

// Обработка ввода
procedure TForm1.Button1Click(Sender: TObject);
var
x, y: Double; // Хранение текущих координат графика
time: TDateTime; // Хранение текущего времени
start: TDateTime; // Хранение начального времени
begin
    y := StrToFloatDef(LabeledEdit1.Text, -1); // Конвертирование текста в поле LabeledEdit1 в число, если там некорректное число, то выдаст -1
    if y = -1 then exit; // "Защита от дурака" Если пользователь ввел не корректное число, то ничего не делать и выйти из функции. (считается, что числа будут больше 0)
    // Расчет периода времени от предыдущего измерения в минутах
    time := Now; // Запись текущего времени из системной переменной Now в переменную time
    start := time; // Присваивание переменной старт текущего времени. Если это не первое измерение, то переменную позже переписать.
    if Length(DB) > 0 then // Если были предыдущие измерения, то ...
    begin
         start := DB[0].time; // Копирование времени первого измерения в start. Здесь происходит её изменение, в случае не первого измерения.
    end;
    // добавление новой записи
    SetLength(DB, Length(DB)+1); // выделение памяти
    DB[HIGH(DB)].Param:=y;      // Запись параметра
    DB[HIGH(DB)].time:=time;    // Запись текущего времени
    // Отображение измерения
    x := TimeStampToMSecs(DateTimeToTimeStamp(time)) - TimeStampToMSecs(DateTimeToTimeStamp(start)); // Расчет разницы текущего времени и начального тек. = тек. - нач.
    { Примечание:
      функция DateTimeToTimeStamp переводит переменную типа дата/время в спец. тип метки времени
      функция TimeStampToMSecs переводит переменную типа метка времени в миллисекунды
    }
    x := x /(1000*60); // Перевод из мс в минуты
    Chart1LineSeries1.AddXY(x,y); // Добавление новых координат в серию и отображение графика
end;       

Задаем алгоритм удаления записи:

// Удаление последнего измерения
procedure TForm1.Button2Click(Sender: TObject);
begin
  if Length(DB)=0 then exit; // Если в базе нет записей, то завершить функцию
  SetLength(DB, Length(DB)-1); // Установка размера массива на одну запись меньше (удаление последней)
  Chart1LineSeries1.Delete(Chart1LineSeries1.Count-1); //Удалить последнюю точку на графике по индексу
  {Примечание: Chart1LineSeries1.Count - возвращает количество точек на графике}
end; 

Проверяем программу:

Image
Работа программы

После закрытия программы в папке появится файл data.dat, содержимое которого нельзя будет разобрать в текстовых редакторах.

При открытии программы, график должен опять появиться со введенными значениями!

Все работает! 


При возникновении вопросов, пишите в комментариях здесь или в сообществе ВК!

(с) Роман Исаков