Лекция 22. Динамические массивы
В этой лекции вы познакомитесь с динамическими массивами, научитесь управлять размерностью и обработкой таких массивов, создадите программу "Телефонный справочник", в которой используется запись, динамический массив типа этой записи и типизированный файл.
В течение курса мы нередко употребляли в программах массивы, и знаем, что они объявляются таким образом:
Имя_массива: array [длина_массива] of тип_данных;
Мы знаем, что типы данных могут быть любыми, а длина массива указывается в виде диапазона от начального, до конечного значения. Работать с такими массивами очень удобно, однако проблема в том, что они не могут менять свой размер. А если мы заранее не знаем, какой размер должен быть у массива?
Допустим, у нас в программе есть пользовательские настройки. То есть, несколько пользователей зарегистрировались в программе и имеют право на ее использование. Каждый из них желает, чтобы программа сохраняла его настройки, чтобы потом ему не приходилось перенастраивать ее. К примеру, размер и положение главного окна. Одному удобно, когда окно справа. Другой желает видеть его слева. А третий и вовсе, хочет, чтобы оно было развернуто во весь экран! Выход? Создать запись для сохранения настроек пользователя, и объявить массив типа этой записи. Каждому пользователю будет принадлежать свой индекс этого массива. Однако, очень редко бывает, когда программист заранее знает количество зарегистрированных пользователей. Какой размер массива делать тогда, 10? А вдруг пользователей будет меньше, тогда у массива будут пустые индексы... А вдруг больше?! Тогда будет переполнение массива, программа выдаст ошибку и не сможет работать... На помощь приходят динамические массивы.
Динамическими называются массивы, при объявлении которых размер не указывается. А во время выполнения программы такие массивы могут изменять длину.
Объявляются такие массивы обычным образом, в разделе переменных:
var
da : array of Integer;
Как видите, здесь указано, что объявляется массив целых чисел, однако длина при этом не указывается. В дальнейшем с таким массивом мы можем проделывать различные действия – увеличивать или уменьшать его длину, узнавать текущую длину, узнавать низшее и высшее значения диапазона массива, и, конечно, присваивать значения любому из его элементов, или наоборот, считывать эти значения. Ну и конечно, записывать значения массива в файл и считывать их из файла. Познакомьтесь с функциями, которые все это проделывают.
SetLength(массив, длина); Устанавливает указанную длину у указанного массива. Пример:
SetLength(da, 10);
В результате динамический массив da содержит 10 элементов. Обратите внимание, что элементы динамических массивов всегда начинаются с нуля!
Если затем мы напишем:
SetLength(da, 8);
то длина массива уменьшится на 2 элемента. Если эти элементы содержали какое-то значение, они будут уничтожены!
Length(da); Эту функцию мы уже использовали, чтобы узнать длину строки. Она также показывает количество элементов в динамическом массиве. Вспомните, что строка – это просто массив символов, а если мы не оговариваем длины строки, а просто пишем "s : String", то получаем динамическую строку символов. Нам достаточно присвоить какое то значение строке, и память будет выделена автоматически под текущий размер этой строки. Однако в случае работы с динамическими массивами, нам самим придется устанавливать их размер.
Low(da); Указывает низший индекс массива da, как правило, это ноль.
High(da); Указывает высший индекс массива. Посмотрите пример:
SetLength(da, 10); //установили размер в 10 элементов
Low(da); //вернет 0, как низший элемент
High(da); //вернет 9, как высший элемент
Таким образом, эти функции удобно использовать в цикле обработки каждого элемента массива:
for i := Low(da) to High(da) do
da[i] := значение;
Однако перед такими операциями нужно установить какой-то размер массива, иначе произойдет ошибка. Следующий пример проверяет размер массива. Если у массива нет индексов, он устанавливает один индекс:
if Length(da) = 0 then SetLength(da, 1);
Часто бывает необходимо добавить один элемент к массиву, но программист не знает, сколько в данный момент у массива есть элементов. Это можно сделать таким образом:
SetLength(da, Length(da) + 1);
Здесь в качестве устанавливаемого размера мы вначале узнали текущий размер массива, и добавили к нему единицу. Получилось, что мы просто увеличили размер этого массива на 1 элемент.
Этих функций вполне достаточно для работы с динамическими массивами, однако перед применением их на практике, вам следует знать, как работа динамическими массивами выглядит с точки зрения компьютера.
Когда мы объявляем динамический массив, мы еще не выделяем под него память, просто указываем тип данных, чтобы компьютер знал размерность под каждый элемент.
Когда мы устанавливаем эту размерность функцией SetLength(), компьютер выделяет память под указанное количество элементов. При этом имя массива служит указателем на первый элемент массива в памяти. Компьютеру все равно, какие данные хранятся в элементах, его задача – выделить для этого достаточно памяти. Поэтому удалить какой то элемент внутри массива не так просто.
Теперь рассмотрим на практике работу с динамическим массивом и напишем программу – телефонный справочник.
Создайте форму, наподобие такой:
Рис. 22.1. Внешний вид приложения
Здесь у нас 7 компонентов Label, 2 ComboBox, 1 MaskEdit для ввода номера телефона и 4 Edit для ввода пользовательских данных. Кроме того, 2 кнопочки с надписями "Добавить телефон" и "Выйти из программы". В компоненте MaskEdit создайте маску для ввода телефона, наподобие указанной в рисунке, но с кодом своего города.
Расположите компоненты на форме так, как подсказывает вам дизайнерский вкус. Верхний ComboBox будет нужен для выбора телефона из списка, чтобы сразу же высвечивались данные этой записи.
Проверьте свойство TabOrder всех компонентов для ввода, они должны идти один за другим при нажатии клавиши <Tab>. Свойство TabOrder показывает индекс компонента на форме. Тот компонент, у которого TabOrder равен 0, при открытии формы будет иметь фокус ввода, то есть, будет выделенным. Когда пользователь нажмет клавишу <Tab>, выделение перейдет к компоненту с TabOrder равным 1, и так далее. С помощью этого свойства мы можем указывать очередность выделения компонентов, как правило, она идет сверху - вниз, и слева - направо. Разумеется, такие компоненты, как Label, фокуса ввода не имеют и у них отсутствует свойство TabOrder.
Форму переименуйте в fMain, модуль будет называться Main а проект – tfSprav.
Названия компонентов оставим по умолчанию, их не так много, и мы не запутаемся. Установите форму в стиль bsDialog, а позицию – по центру экрана.
Прежде всего, нужно объявить глобальную запись для хранения необходимых данных, а также глобальный массив элементов типа этой записи. В этот массив мы будем собирать данные, записывать их в файл и считывать из файла. Он должен быть глобальным, чтобы мы могли работать с ним из всех процедур. Поэтому выше раздела implementation пишем такой код:
type
myTFSprav = record
TelNum : String[15]; //номер телефона
Mobil : Boolean; //мобильник – да? Нет?
Imya : String[20]; //имя владельца телефона
Otchestvo : String[20]; //его отчество
Familiya : String[20]; //его фамилия
Adres : String[50]; //его адрес
end; //record
var
fMain: TfMain;
sprav: array of myTFSprav; //объявляем динамический массив нашей записи
Как видите, мы указали немало полей в записи. Однако, не все из них будут обязательны для заполнения. Есть данные? Запишем. Нет? Поля можно оставить пустыми. Фактически, необходимыми записями являются только две – номер телефона и имя его владельца. Номер телефона нам нужен, потому что это ведь телефонный справочник, и нет смысла в записи, если мы не указываем там поле с номером телефона. Имя нам тоже необходимо, ведь зачем нам в файле номер телефона, если мы не знаем, чей это номер?
Сразу же создадим обработчик событий для кнопки "Выйти из программы". Там пишем просто:
Close;
Далее обработаем кнопку "Добавить телефон". В самом начале у нас ведь еще нет записей, поэтому первым делом пользователь введет парочку – другую телефонных номеров.
{Добавить телефон}
procedure TfMain.Button1Click(Sender: TObject);
var
i : Integer; //для счетчика записей
begin
{Проверяем обязательные параметры:}
//если номера телефона нет, выходим:
if MaskEdit1.Text = '8(374)- - - ' then begin //здесь введите свой код города
ShowMessage('Впишите номер телефона!');
MaskEdit1.SetFocus;
Exit;
end; //if
//если имени нет, выходим:
if Edit1.Text = '' then begin
ShowMessage('Впишите имя владельца телефона!');
Edit1.SetFocus;
Exit;
end; //if
//действительно ли пользователь хочет сохранить телефон?
if Application.MessageBox('Вы уверены, что хотите сохранить этот телефон?',
'Внимание!', MB_YESNOCANCEL+MB_ICONQUESTION)<> IDYES then Exit;
{Пользователь указал номер телефона и имя, и желает сохранить телефон
в список.}
//проверяем номер телефона на совпадение с имеющимися номерами,
//если там есть записи. Для проверки используем цикл, сверяем каждую запись.
//Если такая запись уже есть, сообщаем об этом и выходим из процедуры:
if length(sprav) > 0 then
for i := Low(sprav) to High(sprav) do
if sprav[i].TelNum = MaskEdit1.Text then begin
ShowMessage('Такой номер уже есть!');
Exit;
end; //if
//добавляем новый элемент к массиву:
SetLength(sprav, Length(sprav)+1);
//записываем новый телефон в список:
sprav[High(sprav)].TelNum := MaskEdit1.Text;
if ComboBox2.ItemIndex = 0 then
sprav[High(sprav)].Mobil := True
else sprav[High(sprav)].Mobil := False;
sprav[High(sprav)].Imya := Edit1.Text;
sprav[High(sprav)].Otchestvo := Edit2.Text;
sprav[High(sprav)].Familiya := Edit3.Text;
sprav[High(sprav)].Adres := Edit4.Text;
//очищаем поля на форме:
MaskEdit1.Text := '8(374)- - - ';
ComboBox2.ItemIndex := 0;
Edit1.Text := '';
Edit2.Text := '';
Edit3.Text := '';
Edit4.Text := '';
ShowMessage('Телефон '+ sprav[High(sprav)].TelNum + ' добавлен!');
//обновим ComboBox с телефонами:
ChangeCombo;
end;
Процедуры ChangeCombo еще нет, мы предназначили ее для того, чтобы пользователь мог выбрать нужный телефон из списка, а процедура обновляет список. Поскольку эта процедура должна быть описана выше того места, где мы ее используем, то опишем ее самой первой, после директивы компилятору {$R *.dfm}:
{Процедура обновления ComboBox1}
procedure ChangeCombo;
var
i : Integer; //счетчик для цикла
begin
//если массив пустой - выходим:
if Length(sprav) = 0 then Exit;
//если что-то есть, то сначала очистим ComboBox:
fMain.ComboBox1.Items.Clear;
//затем добавим в него каждый номер телефона из массива:
for i := 0 to High(sprav) do
fMain.ComboBox1.Items.Add(sprav[i].TelNum);
end;
Сохраните проект и скомпилируйте его, впишите пару телефонов и убедитесь, что они попадают в ComboBox. Программа уже вовсю работает с динамическим массивом и сохраняет в него данные. Однако пока что данные хранятся только в памяти, нужно научить программу сохранять их в файл. Создаем для формы обработчик событий onDestroy, то есть, когда форма разрушается, наши данные попадут в файл. Файл будет создаваться, если его не было, или перезаписываться, если он уже был.
{при выходе из программы обновим файл:}
procedure TfMain.FormDestroy(Sender: TObject);
var
f : File of myTFSprav;
i : Integer;
begin
//создаем или перезаписываем файл:
try
AssignFile(f, 'mySprav.dat');
Rewrite(f);
//записываем все данные архива:
for i := Low(sprav) to High(sprav) do
Write(f, sprav[i]);
finally
CloseFile(f);
end; // try
end;
Как видите, мы объявили файл такого же типа, какой имеет массив. В прошлой лекции вы уже записывали запись в типизированный файл, однако теперь нам придется записывать не одну запись, а целый массив таких записей. Поэтому мы используем функцию:
Write(f, sprav[i]);
причем делаем это в цикле от первого до последнего элемента массива. Эта функция записывает в файл f запись нужного типа sprav[i]. В примере у нас в этой строке сохраняется только один элемент массива, все его поля, после чего указатель перемещается к концу. В следующем проходе цикла сохраняться будет уже следующий элемент массива, и так далее, увеличивая файл.
Ну и наконец, обратите внимание, что в качестве файла мы указали только имя и расширение, следовательно, он будет создаваться в той же папке, откуда запущена программа. Вы можете использовать примеры из прошлых лекций и изменить имя файла в функцииAssignFile(), указав там и адрес программы.
Сохраните пример и скомпилируйте его. Впишите пару записей и выйдите из программы. Посмотрите в директорию – там должен появиться файл – справочник.
Наша программа уже умеет сохранять данные в файл, теперь научим ее загружать их из файла. Создаем для формы обработчик onCreate, и вписываем туда код:
{При включении программы загружаем список}
procedure TfMain.FormCreate(Sender: TObject);
var
f : File of myTFSprav;
begin
try
AssignFile(f, 'mySprav.dat');
Reset(f);
//считываем все данные в архив:
while not Eof(f) do begin
//добавляем новый элемент массива
SetLength(sprav, Length(sprav)+1);
Read(f, sprav[High(sprav)]);
end; //while
finally
CloseFile(f);
end; // try
//обновим ComboBox с телефонами:
ChangeCombo;
end;
Обратите внимание, что мы не знаем заранее, сколько там записей, поэтому использовать цикл for не можем. Зато мы можем использовать while. Функция Eof(f) вернет истину, когда будет достигнут конец файла f. И каждый раз, для новой записи, мы добавляем один элемент к массиву. Мы должны сделать это раньше, чем будем считывать запись, потому что, если для записи не выделена память, то некуда будет считывать новую запись и программа выдаст ошибку.
В конце мы опять вызываем процедуру ChangeCombo, чтобы добавить в ComboBox все телефоны из массива. Сохраните проект, скомпилируйте его и проверьте работу. В ComboBox должны загружаться все телефоны из файла.
Однако еще программе не хватает "изюминки" - надо бы, чтобы данные этой записи отображались на форме, когда пользователь выберет тот или иной телефон в ComboBox. Выделяем этот компонент и пишем для него обработчик событий onChange:
{Выбрали другой объект в combobox1}
procedure TfMain.ComboBox1Change(Sender: TObject);
begin
MaskEdit1.Text := sprav[ComboBox1.ItemIndex].TelNum;
if sprav[ComboBox1.ItemIndex].Mobil then
ComboBox2.ItemIndex := 0
else ComboBox2.ItemIndex := 1;
Edit1.Text := sprav[ComboBox1.ItemIndex].Imya;
Edit2.Text := sprav[ComboBox1.ItemIndex].Otchestvo;
Edit3.Text := sprav[ComboBox1.ItemIndex].Familiya;
Edit4.Text := sprav[ComboBox1.ItemIndex].Adres;
end;
Все, программа готова! Это конечно, не полноценная база данных, тем не менее, такой прием программирования будет полезен при сохранении небольшого количества данных, например, для пользовательских настроек при многопользовательской программе. А для хранения больших объемов данных желательно использовать БД – базы данных, которые нам вскоре предстоит изучить.