Телефонный справочник
Но этого мало, хотелось бы ещё и сохранять эти контакты в файл. Для этого нам лучше всего подойдет событие формы OnClose, которое будет выполняться только один раз за сеанс работы программы - перед её закрытием. Форма вся закрыта компонентами, но по прошлым лекциям вы должны помнить, что форму можно выделить в верхнем окошке Инспектора объектов, где компоненты указаны в виде дерева, в самой вершине которого форма fMain. Еще можно выделить компонент верхнего над формой уровня - панель или сетку, и нажать <Esc>. При этом выделение перейдет на более низкий уровень - на форму.
Итак, выделите форму, в Инспекторе объектов перейдите на вкладку События и сгенерируйте событие OnClose. Код события такой:
procedure TfMain.FormClose(Sender: TObject; var CloseAction: TCloseAction); var MyCont: Contacts; //для очередной записи f: file of Contacts; //файл данных i: integer; //счетчик цикла begin //если строки данных пусты, просто выходим: if SG.RowCount = 1 then exit; //иначе открываем файл для записи: try AssignFile(f, adres + 'telephones.dat'); Rewrite(f); //теперь цикл - от первой до последней записи сетки: for i:= 1 to SG.RowCount-1 do begin //получаем данные текущей записи: MyCont.Name:= SG.Cells[0, i]; MyCont.Telephon:= SG.Cells[1, i]; MyCont.Note:= SG.Cells[2, i]; //записываем их: Write(f, MyCont); end; finally CloseFile(f); end; end;
Как видите, мы объявили переменную типа нашей записи Contacts, и типизированный файл с таким же типом. Еще мы объявили целочисленную переменную, которая понадобится нам для организации цикла.
Вначале мы проверяем - есть ли в сетке данные? Если SG.RowCount = 1, то это означает, что в сетке есть только одна строка - фиксированная, та, где у нас находятся заголовки колонок (а меньше 1 строки и быть не может, пользователь не сможет удалить фиксированную строку). Другими словами, данных нет. В этом случае мы просто выходим из процедуры.
Если программа пошла дальше, значит, данные в сетке есть, правда? Тогда мы начинаем работу по сохранению данных в файл. Эту работу мы помещаем в блок try-finally-end, работу с файлами (в том числе типизированными) мы с вами неоднократно рассматривали, так что подробно останавливаться тут не буду. Но пару замечаний сделать все же нужно. Во-первых, при ассоциации с файлом мы указали не только имя, но и адрес файла, который должен находиться в переменной adres:
AssignFile(f, adres + 'telephones.dat');
Однако эту переменную мы объявили, но ничего туда пока не внесли. Не обращайте на это внимания - мы сделаем это в следующем коде. Пока считайте, что адрес там есть, однако не спешите запускать программу на выполнение, точнее, не пытайтесь закрыть эту программу, если там есть данные.
Во-вторых, запись данных в файл мы организовали в виде цикла:
for i:= 1 to SG.RowCount-1 do begin
То есть, мы делаем эту работу от строки с индексом 1 (нулевой индекс у фиксированной строки, она нам для обработки данных не нужна), до строки с индексом SG.RowCount-1. Так, если в сетке у нас 10 строк, то индекс последней строки = 9, а RowCount вернет 10, потому мы отнимаем единицу.
Внутри цикла мы считываем в переменную типа запись данные из сетки, из текущей строки данных:
MyCont.Name:= SG.Cells[0, i];
Когда считали все значения, мы записываем эту переменную в типизированный файл. Поскольку типы переменной и файла совпадают, то запись будет успешной, а указатель сместится в конец файла. Затем цикл продолжится. При каждом новом шаге цикла значение i будет увеличиваться на единицу, и код будет ссылаться на новую следующую строку. Записи будут записываться в файл одна за другой, до самого конца. В результате у нас получится нечто вроде базы данных.
Но чтобы от нашей базы был толк, программа при загрузке должна эти данные считывать обратно в сетку. Удобнее всего, как уже говорилось ранее, подготовительную работу помещать в событие формы OnCreate. Сгенерируйте это событие, код будет таким:
procedure TfMain.FormCreate(Sender: TObject); var MyCont: Contacts; //для очередной записи f: file of Contacts; //файл данных i: integer; //счетчик цикла begin //сначала получим адрес программы: adres:= ExtractFilePath(ParamStr(0)); //настроим сетку: SG.Cells[0, 0]:= 'Имя'; SG.Cells[1, 0]:= 'Телефон'; SG.Cells[2, 0]:= 'Примечание'; SG.ColWidths[0]:= 365; SG.ColWidths[1]:= 150; SG.ColWidths[2]:= 150; //если файла данных нет, просто выходим: if not FileExists(adres + 'telephones.dat') then exit; //иначе файл есть, открываем его для чтения и //считываем данные в сетку: try AssignFile(f, adres + 'telephones.dat'); Reset(f); //теперь цикл - от первой до последней записи сетки: while not Eof(f) do begin //считываем новую запись: Read(f, MyCont); //добавляем в сетку новую строку, и заполняем её: SG.RowCount:= SG.RowCount + 1; SG.Cells[0, SG.RowCount-1]:= MyCont.Name; SG.Cells[1, SG.RowCount-1]:= MyCont.Telephon; SG.Cells[2, SG.RowCount-1]:= MyCont.Note; end; finally CloseFile(f); end; end;
Здесь мы, прежде всего, получаем адрес запускаемой программы в переменную adres:
adres:= ExtractFilePath(ParamStr(0));
Как вы помните из "Консольные приложения и параметры программы" про консольные приложения, в любом проекте доступен параметр ParamStr(0). В этом параметре хранится адрес и имя загружаемой программы. Функция ExtractFilePath отсекает имя файла, возвращая только его адрес с завершающим "\", например:
C:\Program Files\MyProg\
Адрес, откуда запущена программа, мы и сохраняем в переменную adres. Поскольку она глобальная, то становится доступной во всем модуле главной формы. Поэтому мы воспользовались этой переменной для получения адреса в событии OnClose, в котором прописали код сохранения данных в файл. Так как событие OnCreate выполняется первым, то адрес в переменной adres действительно, уже будет. Так что теперь данные будут сохраняться.
Далее мы настраиваем сетку - указываем ширину столбцов, вписываем заголовки колонок. Как это делается, мы разбирали в прошлой лекции.
Затем мы смотрим - есть ли файл? Программа может запускаться впервые, или пользователь мог удалить старый файл, в любом из этих случаев мы выходим, не производя дальнейших действий. Сетка все равно уже настроена, так что программа будет выглядеть, как надо. Да и адрес программы мы уже поместили в переменную adres.
Но если файл есть, то нужно считать из него данные, и поместить их в сетку. Работа с файлом вам знакома, так что описывать процесс не буду. Замечу только, что в данном случае мы воспользовались циклом
while not Eof(f) do begin
То есть, делать, пока не конец файла. При открытии файла его указатель устанавливается в начало. Когда мы считываем первую запись в переменную MyCont, то указатель перемещается к следующей записи. Так, раз за разом, считывая запись в переменную, а затем, копируя из нее данные в сетку, мы двигаемся до конца файла, обрабатывая все записи, какие там есть. Причем при каждом шаге цикла мы сначала добавляем в сетку новую строку:
SG.RowCount:= SG.RowCount + 1;
а затем в эту, последнюю строку, мы записываем считанные из файла данные.
Итак, на данном этапе, мы научили программу добавлять новые записи (событие OnClick кнопки bAdd), научили её сохранять эти записи в файл (событие формы OnClose) и считывать их из файла (событие формы OnCreate). Осталось научить программу редактировать существующие записи - вдруг пользователь что то решит исправить, например, сменился телефон контакта, удалять контакты, ставшие ненужными, а также сортировать список по алфавиту. Ведь пользователь будет вносить контакты, как придется, не по порядку, а нужную запись гораздо проще найти в сортированном списке.
Итак, сгенерируйте событие OnClick для кнопки bEdit ("Редактировать контакт"). Код:
procedure TfMain.bEditClick(Sender: TObject); begin //если данных в сетке нет - просто выходим: if SG.RowCount = 1 then exit; //иначе записываем данные в форму редактора: fEdit.eName.Text:= SG.Cells[0, SG.Row]; fEdit.eTelephone.Text:= SG.Cells[1, SG.Row]; fEdit.CBNote.Text:= SG.Cells[2, SG.Row]; //устанавливаем ModalResult редактора в mrNone: fEdit.ModalResult:= mrNone; //теперь выводим форму: fEdit.ShowModal; //сохраняем в сетку возможные изменения, //если пользователь нажал "Сохранить": if fEdit.ModalResult = mrOk then begin SG.Cells[0, SG.Row]:= fEdit.eName.Text; SG.Cells[1, SG.Row]:= fEdit.eTelephone.Text; SG.Cells[2, SG.Row]:= fEdit.CBNote.Text; end; end;
Давайте разбираться с кодом. Как обычно, мы выходим из процедуры, если данных в сетке нет - редактировать ведь нечего. Иначе перед показом окна редактора контактов мы проделываем подготовительную работу - заносим в строки TEdit этой формы данные контакта - имя и телефон. Также свойству Text списка выбора TComboBox мы присваиваем текст примечания. Все это позволит представить пользователю данные текущей записи. Вы помните, как узнать индекс текущей строки сетки? Этот индекс храниться в свойстве Row:
fEdit.eName.Text:= SG.Cells[0, SG.Row];
Далее, ModalResult формы редактора мы устанавливаем в mrNone, чтобы потом можно было определить, каким образом пользователь закрыл этот редактор.
Потом мы выводим форму, как модальное окно. В заключение мы проверяем - нажал ли пользователь кнопку "Сохранить контакт"? Если да, то мы перезаписываем данные в сетке на новый вариант, из формы редактора.
Нажав на кнопку "Редактировать контакт", пользователь сможет воспользоваться этим кодом. Однако в хороших программах пользователю часто предоставляют различные инструменты для выполнения одной задачи. Например, хорошим тоном считается редактировать запись сетки при двойном клике по ней. Давайте, реализуем эту возможность, тем более что это совсем просто. Выделите сетку. В Инспекторе объектов перейдите на вкладку События. Только не торопитесь генерировать новое событие, ведь писать новый код нам не придется. В событии OnDblClick нажмите кнопку выбора, и выберите там событие bEditClick - то есть то, которое мы только что описали:
Таким образом, и при нажатии кнопки "Редактировать контакт", и при двойном клике по записи сетки будет выполнена одна и та же процедура.
Теперь научим программу удалять контакты. Сгенерируйте OnClick для кнопки "Удалить контакт":
procedure TfMain.bDelClick(Sender: TObject); begin //если данных нет - выходим: if SG.RowCount = 1 then exit; //иначе выводим запрос на подтверждение: if MessageDlg('Требуется подтверждение', 'Вы действительно хотите удалить контакт "' + SG.Cells[0, SG.Row] + '"?', mtConfirmation, [mbYes, mbNo, mbIgnore], 0) = mrYes then SG.DeleteRow(SG.Row); end;
Этот код проще, да? Опять мы выходим из процедуры, если данных нет, ведь тогда и удалять нечего. Удалить запись просто, но нужно ли это делать? Пользователь мог нажать на кнопку не подумав, или случайно. Поэтому перед удалением мы запросим подтверждения:
if MessageDlg('Требуется подтверждение', 'Вы действительно хотите удалить контакт "' + SG.Cells[0, SG.Row] + '"?', mtConfirmation, [mbYes, mbNo, mbIgnore], 0) = mrYes then SG.DeleteRow(SG.Row);
Программа выведет подобное сообщение:
И только если пользователь подтвердит удаление контакта, он будет удален:
SG.DeleteRow(SG.Row);
Нам осталось сгенерировать событие OnClick для кнопки "Сортировать список". Его код:
procedure TfMain.bSortClick(Sender: TObject); begin //если данных в сетке нет - просто выходим: if SG.RowCount = 1 then exit; //иначе сортируем список: SG.SortColRow(true, 0); end;
Код достаточно простой, мы выполняем сортировку, если есть данные, по первому столбцу (с именами контактов):
SG.SortColRow(true, 0);
Сохраните проект, запустите его и попробуйте в работе. Приложение должно работать, и выглядеть примерно так:
Если есть желание, можете дополнительно расширить проект - самостоятельно сделать окно "О программе" к этому приложению, реализовать механизм сортировок иначе, где пользователь мог бы выбрать способ сортировки: по именам контактов, по телефонам, или по типам телефонов.