Delphi-int.ru: портал программистов

Вход Регистрация | Забыли пароль?

События

Сегодня:
Вопросы0    Ответы0    Мини-форумы0


Последние:
Вопрос09.08, 09:39 / #6696
Ответ29.03, 23:32 / #6682
Новости8 июля 2023


Сейчас онлайн:
На сайте — 11
На IRC-канале — 2

Ссылки

Записи (часть 2)

Автор:
© Ерёмин А.А., 2009
Если программа тебе понятна, значит, она уже устарела...
Номер урока:
26

Введение

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

Записи с вариантами

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

Описание отрезка

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

  1. Двумя точками, каждая из которых имеет координаты X и Y (т.е. X1,Y1,X2,Y2).
  2. Одной точкой (X,Y), длиной отрезка и углом между ним и какой-либо осью (например, осью X).

Оба метода проиллюстрированы. Совершенно очевидно, что такую структуру удобно хранить в виде записи. Опишем первый вариант:

type
  TLineSegment = record
    X1,Y1: Real;
    X2,Y2: Real;
  end;

Для наглядности точки описаны отдельно, хотя короче будет поместить их в одну строку (X1,Y1,X2,Y2: Real).

Теперь второй вариант:

type
  TLineSegment = record
    X,Y: Real; //Один из концов отрезка
    Angle: Real; //Угол наклона
    Length: Real; //Длина отрезка
  end;

Всё хорошо, оба варианта рабочие и удобные... Но теперь представьте, что в программе мы должны предоставить пользователю возможность ввода отрезка и первым, и вторым способом, т.е. он сам будет решать, как ему удобнее. Что делать в этом случае? Не писать же 2 программы, базируясь то на одной структуре, то на другой? Вот тут-то нам и придут на помощь записи с вариантами.

Сначала давайте опишем простой перечислимый тип данных, который содержит два значения - тип описания отрезка:

type TLineSegmentType = (lsPoints,lsPolar);

Второй тип я назвал полярным, потому что это ни что иное, как полярные координаты. Что ж, теперь добавим в нашу запись переменную, которая будет определять тип описания отрезка:

TLineSegment = record
  LType: TLineSegmentType;
end;

Но тип данных должен быть описан ещё до компиляции программы, поэтому оба набора данных следует описать прямо сейчас. А для выбора варианта применяется уже известный нам оператор case:

TLineSegment = record
  case LType: TLineSegmentType of
    lsPoints:
      //Здесь нужно описать первый набор полей...
    lsPolar:
      //...а здесь второй
  end;
end;

Ещё раз: мы заводим переменную-селектор, применяем к ней оператор множественного выбора case, и для каждого из значений описываем нужный набор полей. Наборы необходимо заключать в круглые скобки. В нашем случае получится вот что:

TLineSegment = record
  case LType: TLineSegmentType of
    lsPoints: (
      X1,Y1: Real;
      X2,Y2: Real;
    );
    lsPolar: (
      X,Y: Real;
      Angle: Real;
      Length: Real;
    );
end;

Здесь есть одна особенность: оператор case не требуется закрывать командой end. Варианты наборов должны располагаться всегда в конце списка полей (т.е. сначала описываются фиксированные поля, а затем вариантные) - это объясняет отсутствие end для case - запись так и так будет закрыта с помощью следующего end.

В нашем случае структура всё ещё не оптимальна: координаты одной из точек у нас описаны в обоих наборах. Давайте вынесем их как постоянные поля:

type
  TLineSegmentType = (lsPoints,lsPolar);
 
  TLineSegment = record
    X,Y: Real; //Один из концов отрезка
    case LType: TLineSegmentType of
      lsPoints: (
        X2,Y2: Real; //Второй конец отрезка
      );
      lsPolar: (
        Angle: Real; //Угол наклона
        Length: Real; //Длина
      );
  end;

Это окончательный вид нашей записи. Посмотрите ещё раз и осмыслите написанное.

Ну а теперь перейдём к более знакомым вещам - сделаем интерфейс для ввода информации об отрезке и запрограммируем внесение всех данных в запись.

Форма ввода

Уверен, что с интерфейсной частью вы справитесь сами, поэтому привожу лишь код кнопки:

procedure TForm1.SaveButtonClick(Sender: TObject);
var L: TLineSegment;
begin
  if PointsRadio.Checked then
    L.LType:=lsPoints
  else
    L.LType:=lsPolar;
  case L.LType of
    lsPoints:
      begin
        L.X:=StrToFloat(X1Edit.Text);
        L.Y:=StrToFloat(Y1Edit.Text);
        L.X2:=StrToFloat(X2Edit.Text);
        L.Y2:=StrToFloat(Y2Edit.Text);
      end;
    lsPolar:
      begin
        L.X:=StrToFloat(XEdit.Text);
        L.Y:=StrToFloat(YEdit.Text);
        L.Angle:=StrToFloat(AngleEdit.Text);
        L.Length:=StrToFloat(LengthEdit.Text);
      end;
  end;
end;

Сначала мы смотрим, какой способа ввода у нас выбран на форме и соответствующим образом устанавливаем переменную-селектор LType. А затем уже переносим данные из полей ввода в нашу запись: если первый способ - из 4 левых полей, если второй - из 4 правых.

С какой целью мы ввели отрезок? Ну например давайте посчитаем его длину. Для этого введём собственную функцию, которая на вход будет принимать запись, а на выходе будет выдавать длину отрезка. Если забыли математику, напомню способ вычисления длины:

  1. По двум точкам: длина - это квадратный корень из суммы квадратов разностей соответствующих координат.
  2. По точке, углу и длине: промолчу, ага.
function GetLength(L: TLineSegment): Real;
begin
  case L.LType of
    lsPoints: Result:=Sqrt(Sqr(L.X2-L.X)+Sqr(L.Y2-L.Y));
    lsPolar: Result:=L.Length;
  end;
end;

Что может быть проще? Ну и в конец обработчика нажатия кнопки добавим:

MessageDlg('Длина отрезка: '+FloatToStr(GetLength(L)),mtInformation,[mbOk],0)

Проверьте правильность работы, запустив программу и введя какие-нибудь данные. Помните, что программа работает с расчётом на то, что исходные данные верны, т.е. что вместо чисел вы не вписали "всем привет!".

Упакованные записи

Пару слов о том, что такое упакованные записи, и с чем их едят. По умолчанию память под записи выделяется не очень экономно - помимо самих данных добавляются и служебные байты, которые отделяют блоки данных друг от друга. Существует принудительный способ заставить Delphi упаковать запись, т.е. минимизировать занимаемую ей память. Делается это указанием слова packed перед словом record. Разница порой может быть достаточно ощутимой. Пример: запись из строки длиной 5 символов, одного символа и трёх чисел разного типа. Объявим две разные записи: одна обычная, а другая упакованная:

TRecord1 = packed record
  Name: String[5];
  A: LongInt;
  C: Char;
  D: Double;
  N: Integer;
end;
 
TRecord2 = record
  Name: String[5];
  A: LongInt;
  C: Char;
  D: Double;
  N: Integer;
end;

А теперь самое интересное: посмотрим, сколько памяти занимает каждая из записей. Сделаем это функцией SizeOf():

var R1: TRecord1; R2: TRecord2;
begin
  ShowMessage(IntToStr(SizeOf(R1)));
  ShowMessage(IntToStr(SizeOf(R2)));
end;

В первом сообщении мы увидим 24, а во втором 32. Обычная запись занимает на треть больше памяти, чем упакованная! А теперь представьте, что у вас 100 000 таких записей?

Тем не менее, не стоит пренебрегать этим способом экономии памяти. В некоторых случаях использование пакованных записей может создавать разные ошибки в работе программы. Понять, что дело именно в packed, удаётся далеко не сразу. Так что, если храните десяток отрезков - не торопитесь паковать - если и выиграете, то не сильно.

Быстрый доступ к полям записей

В нашем примере у записи сравнительно мало полей - 4. Но бывают программы, где создаются записи с десятком полей и работа из-за этого замедляется. Хотя бы потому, что в коде приходится каждый раз набирать имя записи и точку, и лишь затем имя поля. Обрадую: мучаемся не только мы - компьютеру тоже приходится делать больше телодвижений. Каждый раз нужно определять адрес и искать запись в памяти, и лишь после этого можно найти значение поля. Чтобы облегчить участь и программисту и машине, был введён специальный оператор with (англ. "с"). К сожалению, о нём далеко не все знают, а ведь его использование увеличивает эффективность и кода, и работы самого программиста.

Итак, общая форма записи:

with запись do
 {обращение к полям записи}

Не очень понятно? А теперь на нашем примере:

with L do
begin
  X:=StrToFloat(X1Edit.Text);
  Y:=StrToFloat(Y1Edit.Text);
  X2:=StrToFloat(X2Edit.Text);
  Y2:=StrToFloat(Y2Edit.Text);
end;

Что же мы имеем? А вот что: мы нашу запись "вынесли за скобку", и далее напрямую обращаемся к её полям. Удобно, не правда ли? Этот код абсолютно эквивалентен тому, что был написан нами ранее, только он более эффективен.

Несложно догадаться, что использование with для единичного обращения к записи бессмысленно:

with L do
  X:=5;

В этом случае мы ни в чём не выигрываем - только пишем больше кода.

Помните про оператор with и почаще его используйте - и себе жизнь облегчите, и программы станут профессиональнее.

Хранение записей в файлах

Ну вот мы и подошли к тому, для чего пришлось затронуть тему работы с файлами. Использовать записи мы научились, но тут вопрос: а как их хранить? Сохранять в файлах отдельно каждое поле - совершенно неудобно. А потом его нужно оттуда ещё как-то прочитать... Неужели нет способа проще? Есть!

Мы можем создать типизированный файл на основе имеющегося типа записи. Помните, как мы описывали файлы? file of ..., верно? Так вот, теперь в качестве типа будет выступать наша запись.

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

Начнём с создания указателя на файл, который опишем указанным образом:

F: file of TLineSegment;

Этим мы сказали, что каждый элемент нашего файла - запись типа TLineSegment. А дальше всё как обычно - ничего нового: связываем указатель с файлом, открываем, записываем, закрываем. Без комментариев, что называется:

AssignFile(F,ExtractFilePath(Application.ExeName)+'lines.dat');
Rewrite(F);
Write(F,L);
CloseFile(F);

Всё это нужно добавить в конец обработчика кнопки "Сохранить". Запустите программу, введите произвольный отрезок и нажмите кнопку. Если всё было сделано правильно, после сообщения о длине отрезка в папке с программой появится файл lines.dat. Расширение dat - стандартное для нестандартных данных (вот так фразу завернул!). Вы можете открыть этот файл любым текстовым редактором, но прочитать что-либо там будет затруднительно - это бинарный файл.

Теперь попробуем прочитать данные из этого файла. Создайте ещё одну кнопку "Загрузить". Код для неё будет такой:

procedure TForm1.LoadButtonClick(Sender: TObject);
var F: file of TLineSegment; L: TLineSegment;
begin
  AssignFile(F,ExtractFilePath(Application.ExeName)+'lines.dat');
  Reset(F);
  Read(F,L);
  CloseFile(F);
  MessageDlg('Длина отрезка: '+FloatToStr(GetLength(L)),mtInformation,[mbOk],0);
end;

Опять же, если всё было сделано верно, то вы увидите длину введённого ранее отрезка.

Теперь давайте изменим нашу программу таким образом, чтобы вводимые отрезки добавлялись в файл, т.е. чтобы файл содержал сразу несколько записей. Но здесь нас подстерегает проблема: функция Append(), предназначенная для добавления данных в конец в файла, работает только с текстовыми файлами. У нас же файл типизированный и здесь такой фокус не пройдёт. Будем выполнять обходной манёвр. Предлагаю вот что: создадим временный файл, перепишем туда все записи из существующего файла, добавим новую запись, после чего удалим старый файл, а новый переименуем. Хитро, сложно? На самом деле не очень.

Чтобы провернуть всё это, пришлось добавить 2 переменные - ещё одну запись и один файл. Под буквами "N" и "O" я подразумеваю "new" и "old" (новый и старый).

procedure TForm1.SaveButtonClick(Sender: TObject);
var
  L,OL: TLineSegment;
  F,NF: file of TLineSegment;
begin
  {предыдущий код здесь опущен}
 
  SetCurrentDir(ExtractFilePath(Application.ExeName)+'lines.dat');
  AssignFile(NF,'temp.dat');
  Rewrite(NF);
  if FileExists('lines.dat') then
  begin
    AssignFile(F,'lines.dat');
    Reset(F);
    while not EOF(F) do
    begin
      Read(F,OL);
      Write(NF,OL);
    end;
    CloseFile(F);
  end;
  Write(NF,L);
  CloseFile(NF);
  DeleteFile('lines.dat');
  RenameFile('temp.dat','lines.dat');
end;

Разберём этот код подробно. Сначала делаем папку с программой рабочей, чтобы каждый раз не писать путь. Далее ассоциируем указатель с новым файлом, и открываем этот файл для записи. Далее проверяем, есть ли файл с предыдущими записями. Как мы договорились, если он есть, то нужно переписать из него все записи: связываемся с файлом, открываем его для чтения, а далее цикл по всем полям записи. Функция EOF() позволяет узнать, дошли ли мы до конца файла. Таким образом, пока файл не кончился, читаем из него одну запись и переписываем её в новый файл. После завершения закрываем старый файл. Осталось самое простое - записать новую запись, что мы и делаем. После этого старый файл lines.dat удаляем, а временный temp.dat переименовываем в новый lines.dat. Таким образом, достигнута требуемая цель. Запустите программу и добавьте в наш файл ещё несколько отрезков. О том, что добавление происходит успешно, можно судить по увеличивающемуся объёму файла.

Следующая задача: узнать, сколько записей в файле. Делается это очень просто - функцией FileSize(). Когда мы объявляем "файл из байтов" (file of byte), то получаем объём файла в байтах. Сейчас же мы узнаем, сколько записей в файле:

var F: file of TLineSegment;
{...}
AssignFile(F,ExtractFilePath(Application.ExeName)+'lines.dat');
Reset(F);
ShowMessage('В файле '+IntToStr(FileSize(F))+' записей');
CloseFile(F);

Код очень простой и понятный.

Ну и наконец последнее, о чём мне хотелось бы рассказать - это о "перемотке" файла. Я имею ввиду о том, как добраться до записи в середине файла, не перебирая все предыдущие через Read(). Такая задача встречается очень часто и решается она достаточно просто. Процедура Seek() перемещается по файлу на указанный по номеру элемент:

Seek(указатель_на_файл,номер_записи);

Пример: добавим на форму TListBox и кнопку "Обновить список":

procedure TForm1.UpdateButtonClick(Sender: TObject);
var F: file of TLineSegment; I,N: Integer;
begin
  AssignFile(F,ExtractFilePath(Application.ExeName)+'lines.dat');
  Reset(F);
  N:=FileSize(F);
  CloseFile(F);
  ListBox1.Items.Clear;
  for I := 1 to N do
    ListBox1.Items.Add('Запись #'+IntToStr(I))
end;
Тестовая программа

Как видно, эта кнопка добавляет в ListBox список записей в файле. Теперь кнопка "Загрузить" должна загрузить выбранную в списке запись и отобразить длину отрезка:

procedure TForm1.LoadButtonClick(Sender: TObject);
var F: file of TLineSegment; L: TLineSegment;
begin
  AssignFile(F,ExtractFilePath(Application.ExeName)+'lines.dat');
  Reset(F);
  Seek(F,ListBox1.ItemIndex);
  Read(F,L);
  CloseFile(F);
  MessageDlg('Длина отрезка: '+FloatToStr(GetLength(L)),mtInformation,[mbOk],0);
end;

Свойство ItemIndex у ListBox определяет номер строки, выбранной в данный момент (строки нумеруются с нуля). После открытия файла мы прыгаем на запись с таким номером, читаем её и затем определяем длину.

Просто? Думаю, что да. Работа с типизированными файлами принципиально ничем не отличается от работы с текстовыми. Зато обратите внимание, как легко можно оперировать записями! Теперь вы легко сможете создать простейшую базу данных.

Да, и помните, что при работе с типизированными файлами нельзя использовать функции ReadLn() и WriteLn() - они предназначены исключительно для текстовых файлов.

Домашнее задание

Для закрепления пройденного не буду предлагать новой программы - давайте изменим эту. Итак, требуется:

  1. Создать функцию, которая определяет угол наклона заданного отрезка. Не забывайте о модуле Math - там есть вещи, которые вам пригодятся.
  2. В списке записей в ListBox в каждой строке нужно указать координату одного из концов отрезка. Например, вместо "Запись #2" должно выводиться "Запись #2 (1;2)".
  3. Кнопка "Сохранить" должна добавлять новую запись не в конец файла, а после той, что выбрана в ListBox. Например, если выбрана №2, то новая будет сохранена на 3-ю позицию, а все остальные таким образом сдвинутся вниз.

С первого прочтения кажется сложным? На самом деле нет. Если начнёте делать - сообразите, что и как. И не бойтесь потратить на это полчаса, час... Программированию нельзя научить - ему можно только научиться самому. Одно лишь чтение статей ничего не даст - нужно пробовать, чем больше - тем лучше. В конце концов, позади 25 уроков - это достаточно много, пора начинать активно действовать.

Желаю успехов! До встречи на следующем уроке!

Автор: Ерёмин А.А.

Статья добавлена: 8 августа 2009

Рейтинг статьи: 5.00 Голосов: 11 Ваша оценка:

Зарегистрируйтесь/авторизируйтесь,
чтобы оценивать статьи.


Статьи, похожие по тематике

 

Для вставки ссылки на данную статью на другом сайте используйте следующий HTML-код:

Ссылка для форумов (BBCode):

Быстрая вставка ссылки на статью в сообщениях на сайте:
{{a:126}} (буква a — латинская) — только адрес статьи (URL);
{{статья:126}} — полноценная HTML-ссылка на статью (текст ссылки — название статьи).

Поделитесь ссылкой в социальных сетях:


Комментарии читателей к данной статье

Никонов Алексей
Репутация: нет

Никонов Алексей (2 августа 2011, 11:40):

Доброго всем времени. Очень хорошие уроки. Все подробно описано и разжевано. Самое то для начинающих. Подскажите пожалуйста, будут ли еще уроки? Судя по дате, то выходить больше не будут. Очень жаль, только начал в раж входить =) . В любом случае [b]Ерёмину А.А[/b] от меня низкий поклон и громадное спасибо.

P.S. Большая просьба, если есть возможность, продолжите пожалуйста статьи по Delphi. Очень хочется изучить этот язык поглубже =) . Большое спасибо. Удачи!
Учусь
Репутация: нет

Учусь (21 сентября 2010, 20:42):

А когда появятся новые статьи? Уж очень хочется. А то неделя прошла...
Ерёмин А.А.
Репутация: +40

Ерёмин А.А. (9 августа 2010, 22:45):

Цитата:

моменты как по TCP на делфи работать и вообще по инету шариться видимо тут не узнать...

В уроках это вряд ли будет. По этим темам полно статей. Из основ-то многое не рассказано ещё...
antoca
Репутация: +1

antoca (9 августа 2010, 20:19):

А следующего урока я так понял нет... мде... моменты как по TCP на делфи работать и вообще по инету шариться видимо тут не узнать...
antoca
Репутация: +1

antoca (9 августа 2010, 20:17):

Не знаю в чём дело, жрать очень охота.

По теме: по-моемему не записи с вариантами, а записи записей - так это чаще называется. Хотя могу путать.

Ну а добавление в файл записей еще одну запись - почему бы файл не открыть на запись, указатель в конец поставить и дописать ее туда?

Всё, жрать бегу...
габибыч
Репутация: -1

габибыч (5 июля 2010, 11:12):

Здарова!
Все вроде понятно, кроме одного.
повторил все как автор, но у меня на форме (в ListBox-е) исчезают старые записи (при каждом обновлении повляются новые).
третью задачу из-за этого не могу сделать.
в чем там дело??????
Prok
Репутация: нет

Prok (27 апреля 2010, 00:14):

Очень интересные уроки. Спасибо!
Правда хочется ещё уроков
padonak
Репутация: +1

padonak (7 февраля 2010, 14:17):

Ок... Выши уроки очень помогают... Спасибо.
Ерёмин А.А.
Репутация: +40

Ерёмин А.А. (7 февраля 2010, 12:31) | 1 отзыв:

Скоро появятся. Планирую написать на неделе.
padonak
Репутация: +1

padonak (7 февраля 2010, 11:56):

Здравствуйте! А когда появятся новые статьи?
Тамара
Репутация: нет

Тамара (3 декабря 2009, 19:32):

Мозголомная задачка! еле решила!!
А автор прав! пока не решишь сам задачи - не поймёшь сути предмета!

Оставлять комментарии к статьям могут только зарегистрированные пользователи.