SNK Software
Web Studio Монополия Metaproducts Утилиты Игры
Монополию Web Studio Библиотека
Вебмастер Дельфи Работа на ПК Самоучитель
Для PHP Для Delphi
Веб-дизайн Программирование Компьютеры Девайсы Заметки
SNK Software Индустрия hardware Индустрия software
О студии Портфолио Сопровождение сайтов

Новые материалы

Девайсы:
Сравнительный обзор Nokia Lumia 920 и HTC 8X
Девайсы:
Обзор Nokia Lumia 820 – смартфона на WP8
Вебмастеру:
Настройка Apache, PHP и MySQL для Linux-VPS
Вебмастеру:
VPS на домашнем ПК: настройка сети в VM VirtualBox и Debian
Вебмастеру:
VPS на домашнем ПК: устанавливаем Linux Debian 6
Вебмастеру:
VPS на домашнем ПК: установка VM VirtualBox
Работа на компьютере:
Иные возможности текстового процессора Word
Работа на компьютере:
Вставка объектов
Работа на компьютере:
Таблицы в Word
Работа на компьютере:
Печать и сохранение документов
Работа на компьютере:
Сноски, колонтитулы, оглавление и указатели в Word

Работа с данными

Теперь, когда нам известны все основные компоненты, необходимые для работы с СУБД - как обеспечивающие доступ к данным, так и их отображение, можно перейти к вопросам практического характера. Прежде всего, это вопросы, касающиеся полей данных, а так же состояния наборов данных, программная навигация, сортировка, поиск и фильтрация. Так же, разумеется, будут рассмотрены правка, добавление и удаление записей.

Состояния и режимы набора данных

Как мы уже знаем, одним из основных компонентов Delphi для доступа к данным является Table. Этот компонент происходит от общего для всех наборов данных класса - TDataSet - набор данных. Именно на уровне набора данных информация из БД представляются как совокупность строк и столбцов. В этом базовом классе так же сосредоточены все основные свойства и методы для работы с наборами данных, включая управление состоянием набора, поиск, фильтрацию, сортировку и изменение данных.

ВНИМАНИЕ
Набор данных в Delphi - это некая логическая таблица, с которой может работать приложение. При этом порядок следования записей в наборе данных может отличаться от имеющегося в реальной таблице, поскольку зависит от того, используется ли сортировка. Равно как и количество записей в наборе зависит от наличия и параметров фильтрации.

Основными свойствами набора данных как такового являются Active и State, а применительно к записям - RecordCount и RecNo. В частности, свойство RecordCount указывает на текущее количество записей в наборе данных (которое может не совпадать с количеством записей в таблице БД благодаря возможной фильтрации), а свойство RecNo указывает на индекс активной записи.

Что касается свойства Active, то мы уже рассматривали его в контексте компонента Table и знаем, что оно отвечает за непосредственное подключение к БД. Значение этого свойства может быть установлено как на этапе разработки приложения, так и во время выполнения. При этом следует учитывать, что попытка установить свойство Active в истину может вызвать исключительную ситуацию, если не указана, как минимум, физическая таблица с данными (при помощи свойства TableName):

Table1.TableName='..\DB1\bill.db'; // указываем физическую таблицу БД Table1.Active:=true; // делаем набор активным

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

Table1.Active:=false; // отключаемся Table1.TableName='..\DB1\customer.db'; // меняем таблицу БД Table1.Active:=true; // включаем внось

Альтернативным методом изменения состояния набора данных является использование методов Open и Close:

Table1.Close; // отключаемся Table1.TableName='..\DB1\customer.db'; // меняем таблицу БД Table1.Open; // включаем внось

Фактически, эти методы выполняют ту же работу, что и изменение свойства Active, т.е. обращение к методу Open устанавливает свойство Active в истину, а Close - в ложь. При написании программного кода принято использовать именно эти методы, а не изменять значение свойства Active.

Другое свойство набора данных - State - показывает текущее состояние набора данных, или режим его работы. Оно имеет тип TDataSetState и доступно для чтения во время выполнения программы. Основными значениями State являются следующие:

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

Рассмотрим работу с источником данных на примере, для которого нам потребуются компоненты Table, DataSource, DBGrid и OpenDialog. При этом компоненты для баз данных должны быть связаны между собой, т.е. у DataSource1 в свойстве DataSet должно быть указано Table1, а у DBGrid1 свойство DataSource должно иметь значение DataSource1. Теперь разместим на форме кнопку (Button), которая будет вызывать диалог выбора файла и производить работу по подключению данных, для чего нам понадобится обработчик события OnClick. А для компонента DataSource1 мы сделает обработчик события OnStatusChange, который будет выводить текущее состояние набора данных в заголовок окна. Вариант кода приведен в листинге 20.1.

Листинг 20.1. Подключение источника данных и слежение за его состоянием

procedure TForm1.Button1Click(Sender: TObject); begin if not OpenDialog1.Execute then exit; Table1.Close; Table1.TableName:=OpenDialog1.FileName; Table1.Open; end; procedure TForm1.DataSource1StateChange(Sender: TObject); begin case DataSource1.State of dsInactive: Caption:='Отключено'; dsBrowse: Caption:='Просмотр данных'; dsEdit: Caption:='Правка данных'; dsInsert: Caption:='Вставка данных'; dsOpening: Caption:='Открытие источника данных'; else Caption:='Состояние неопределено'; end; end;

Пример программы находится в каталоге Demo\Part4\DataSource.

Поля и класс TField

Если для данных в целом используют компоненты типа Table, т.е. потомки TDataSet, то для, полей, которые представляют собой отдельные столбцы данных в таком наборе, определен класс TField. В свою очередь, от этого класса происходят типизированные классы полей типа TIntegerField, TStringField и т.п.

Для доступа к полям записей у набора данных имеется ряд специальных методов и свойств, доступных во время выполнения. Чаще всего используются метод FieldByName, позволяющий обратиться к полю по его имени. Альтернативным и зачастую менее надежным способом является использование массива Fields, обеспечивающего доступ к полю по его порядковому номеру. И в том и в другом случае мы получим объект типа TField. Основные свойства этого класса приведены в таблице 20.1.

Таблица 20.1. Основные свойства TField
СвойствоТипОписание
AlignmentTAlignmentОпределяет выравнивание при выводе значения поля в визуальном компоненте
AsBCDTBcdСодержит значение поля в двоичном виде (BCD)
AsBooleanBooleanСодержит значение поля в виде булева значения
AsCurrencyCurrencyСодержит значение поля в виде Currency
AsDateTimeTDateTimeСодержит значение поля в виде даты и времени
AsFloatDoubleСодержит значение поля в виде вещественного числа
AsIntegerIntegerСодержит значение поля в виде целого
AsStringStringСодержит значение поля в виде строки
AsVariantVariantСодержит значение поля в виде вариантного типа
CalculatedBooleanОпределяет, является ли поле вычисляемым
CanModifyBooleanУказывает, может или нет быть изменено значение в данном поле
DataSetTDataSetОпределяет набор данных, которому принадлежит данное поле
DataTypeTFieldTypeУказывает на тип данных поля
DisplayLabelStringОпределяет текст, выводимый в качестве заголовка столбца в таблице DBGrid
DisplayWidthIntegerОпределяет число символов, необходимое для вывода значения поля в визуальном компоненте
FieldNameStringОпределяет имя поля в физической таблице БД
IndexIntegerОпределяет порядковый номер поля в наборе данных
IsIndexFieldBooleanУказывает, является ли данное поле индексированным
IsNullBooleanВозвращает истину, если текущее значение поля пустое
ReadOnlyBooleanОпределяет, может ли поле быть изменено пользователем
ValueVariantСодержит текущее значение поля
VisibleBooleanОпределяет, должно ли это поле быть видимым при отображении в таблице DBGrid

Здесь, прежде всего, следует выделить группу из 8 свойств, начинающихся с As. Все они, по сути, являются аналогами функций приведения к типу, поскольку, объект TField универсален и может содержать данные любого типа из встречающихся в СУБД. Например, если речь идет о полях из нашей таблицы bills, то обратиться к полям этой таблицы для получения их значений в "естественном" виде следует использовать подобный код:

x:=Table1.FieldByName('BILL_CUST').AsInteger; y:=Table1.FieldByName('BILL_SUMM').AsCurrency;

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

Label1.Caption:=Table1.FieldByName('BILL_CUST').AsString; Edit1.Text:=Table1.FieldByName('BILL_SUMM').AsString;

Если же при работе с записью нам не требуется явно приводить тип, то вполне подойдет свойство Value, содержащее текущее значение поля в БД. Оно же является одним из наиболее часто используемых, благодаря универсальности этого типа данных. Например, мы можем проверить значение поля на соответствие какому-либо условию, просто написав:

if Fields[1].Value > 100 then Fields[1].Value:=200;

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

Свойства DataSet и FieldName отвечают за привязку объекта к конкретным данным, а Calculated и IsIndexFiels позволяют определить параметры поля в СУБД. Большинство остальных свойств так или иначе влияют на параметры вывода информации из набора данных в визуальные компоненты наподобие DBGrid.

Что касается методов, то тут следует отметить метод Clear, устанавливающий значение поля в Null, и, пожалуй, IsValidChar, при помощи которого можно проверить, является ли указанный в качестве аргумента символ допустимым для данного поля:

if Table1.FieldByName('BILL_SUMM').IsValidChar('1') then caption:='Yes!' else caption:='No.'

В данном случае заголовок окна получит надпись "Yes!", поскольку символ "1" можно использовать для ввода в поле числового типа. Но если бы мы проверяли на допустимость ввода какой-либо буквы, то получили бы отрицательный ответ.

Типы полей и типы данных

Как уже было отмечено, тип Field является лишь предком для целого ряда (всего их около 30) типизированных полей. При этом каждый тип поля в Delphi соответствует определенному типу данных, используемому в той или иной СУБД. Чаще всего используются следующие классы:

Не все перечисленные классы происходят напрямую от TField - для некоторых из них существует "промежуточный" предок. Например, для всех числовых типов определен общий класс TNumericField.

В то же время, имеющееся у класса TField свойство DataType, позволяет получить информацию о том, какого типа значение хранится в поле. Таким образом, обращаясь к методу базового класса, можно не задумываться над тем, с каким конкретным типом объекта мы имеем дело. В Delphi 7 оно определено следующим образом:

type TFieldType = (ftUnknown, ftString, ftSmallint, ftInteger, ftWord, ftBoolean, ftFloat, ftCurrency, ftBCD, ftDate, ftTime, ftDateTime, ftBytes, ftVarBytes, ftAutoInc, ftBlob, ftMemo, ftGraphic, ftFmtMemo, ftParadoxOle, ftDBaseOle, ftTypedBinary, ftCursor, ftFixedChar, ftWideString, ftLargeint, ftADT, ftArray, ftReference, ftDataSet, ftOraBlob, ftOraClob, ftVariant, ftInterface, ftIDispatch, ftGuid, ftTimeStamp, ftFMTBcd);

Очевидно, что названия типов образуются от префикса ft и названия типа данных. Соответственно, если нам надо получить информацию о том, принадлежит ли интересующее нас поле к тому или иному типу, скажем, к Currency, то достаточно написать выражение вида:

if Table1.FieldByName('BILL_SUMM').DataType = ftCurrency then caption:='$';

Свойство DataType может пригодиться и в том случае, если нам требуется проверить, существует ли возможность привести данные поля к какому-то определенному виду. Например, если поле содержит целое или вещественное число, то оно может быть представлено как Currency, а если строкового, двоичного или какого-либо еще типа, то нет. Соответственно, проверка может выглядеть следующим образом:

var sum: Currency; ... if Table1.FieldByName('BILL_SUMM').DataType in [ftInteger, ftWord, ftFloat, ftCurrency] then sum:=Table1.FieldByName('BILL_SUMM').AsCurrency;

Определение типа поля актуально в том случае, если используются динамические поля, которые создаются автоматически при открытии набора данных. Однако поля можно определить жестко на этапе разработки приложения, в таком случае они будут статическими, и иметь какой-либо вполне определенный тип.

Для определения статических полей следует воспользоваться редактором полей, который можно вызвать двойным щелчком по компоненту Table. Внешне он напоминает редактор списка столбцов компонента DBGrid (см. рис. 19.2), что, впрочем, и не удивительно, так как в обоих случаях мы имеем дело с редактором коллекций. Выбрав их контекстного меню редактора полей пункт Add Fields, мы получим диалоговое окно со списком всех имеющихся в текущей таблице БД полей. Выбрав нужные поля, остается нажать OK и приступить к исследованию получившегося списка. В нем будут находиться выбранные поля (обозначенные по своим заголовкам в таблице БД), являющиеся объектами какого-либо из типов полей. Например, для таблицы клиентов это могут быть поля типа TAutoIncField (для CUST_ID) и TStringField (для CUST_NAME). Если щелкнуть по названию поля в списке, то в инспекторе объектов мы увидим все его опубликованные свойства. При этом автоматически будут созданы и помещены в объявление класса формы соответствующие переменные:

type TForm1 = class(TForm) Table1: TTable; Table1CUST_ID: TAutoIncField; Table1CUST_NAME: TStringField; ... end;

Соответственно, в дальнейшем мы сможем оперировать именно этими переменными, а не обращаться к полям таблицы при помощи методов FieldByName и подобных способов:

Caption:=Table1CUST_NAME;

Кроме того, определяя статические поля на уровне набора данных, становится возможность определить ряд параметров отображения непосредственно на этом этапе. Иначе говоря, задать выравнивание можно для самого поля, а при необходимости, (например, при выводе в таблицу DBGrid) оно будет учтено. Здесь же можно задать порядок следования полей и ограничить их состав.

ПРИМЕЧАНИЕ
Разумеется, можно определить параметры вывода и для динамических полей, но это менее удобно, поскольку на этапе разработки их свойства недоступны, в то время, как параметры статических полей можно определить визуально при помощи инспектора объектов.

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

Сортировка

Изначально порядок расположения записей в наборе данных бывает неопределенным. Так, для таблиц одних СУБД (например, dBASE) записи располагаются в порядке поступления в файл таблицы, а в других (например, в Paradox) они сортируются по первичному индексу. Однако очень часто требуется вывести записи в определенном порядке, для чего используется сортировка данных.

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

Для набора данных типа Table сортировка выполняется автоматически по выбранному индексу. Соответственно, если изменить индекс, например, при помощи свойств IndexFieldNames или IndexName, то записи будут упорядочены заново. В качестве значения для IndexName указывают имя индекса, а для IndexFieldNames указывают имена полей, из которых индекс состоит. Здесь следует отметить, что поскольку в таблицах Paradox первичный индекс не имеет имени, то для сортировки по нему можно использовать только свойство IndexFieldNames.

Если сортировку требуется провести по нескольким полям, то каждое из них должно быть включено в составной индекс. Из этого же следует, что при использовании компонента Table сортировка возможна только по индексированным полям.

ПРИМЕЧАНИЕ
Помимо компонента Table, для наборов данных имеется SQL-ориентированный компонент Query. В случае использования Query сортировка производится при помощи языка SQL и может проводиться по любым полям. Вопросы использования компонента Query и языка запросов SQL будут рассмотрены в следующей главе.

Для примера возьмем таблицу счетов и попробуем отсортировать ее по всем возможным индексам. Для этого нам понадобятся компоненты Table, DataSource и DBGrid, а так же RadioGroup. Для Table установим значение свойства DatabaseName в DATA1, TableName - в bill.db, а Active - в истину. После этого свяжем DBGridс Table через компонент DataSource.

Как видно, изначально данные отображаются точно так же, как хранятся в таблице - последовательно с 1 по 8-я запись (на самом деле, в данном случае это заслуга первичного индекса, построенного по полю BILL_ID). Теперь попробуем произвести сортировку по вторичным индексам - CUST_IDX и SECOND_IDX, определенных в структуре таблицы БД, для чего определим 3 варианта в группе переключателей, назвав их "по умолчанию", "индекс 1" и "индекс 2". Теперь в процедуре для события OnClick группы напишем следующий код:

case RadioGroup1.ItemIndex of 0: Table1.IndexName:=''; 1: Table1.IndexName:='CUST_IDX'; 2: Table1.IndexName:='SECOND_IDX'; end;

Если теперь запустить приложение и выбрать какой-либо вариант сортировки, то можно убедиться, что данные в таблице будут отсортированы по 1-му, 2-му, или 3-му столбцу, в зависимости от выбранного положения переключателя (рис. 20.1).

Сортировка по сумме счета (индекс SECOND_IDX)
Рис. 20.1. Сортировка по сумме счета (индекс SECOND_IDX)

Недостатком использования свойства IndexName в данном случае состоит в том, что для таблиц Paradox, как и в данном случае, не удастся задействовать поле с первичным индексом. В таком случае можно воспользоваться свойством IndexFieldNames, для чего добавим еще одну группу, на этот раз - из 4 переключателей, а в обработчике для нее напишем следующий код:

case RadioGroup2.ItemIndex of 0: Table1.IndexFieldNames:=''; 1: Table1.IndexFieldNames:='BILL_CUST'; 2: Table1.IndexFieldNames:='BILL_ID;BILL_CUST'; 3: Table1.IndexFieldNames:='BILL_SUMM;BILL_CUST'; end;

Теперь в 2 случаях (варианты 2 и 3) можно производить сортировку по нескольким полям сразу. Так же следует отметить, что свойства IndexName и IndexFieldNames являются взаимоисключающими, т.е. если установить какое-либо значение для одного из этих свойств, значение другого автоматически сбрасывается. Исходный код этой программы находится в каталоге Demo\Part4\Sort.

Что касается направления сортировки - по возрастанию или по убыванию, то за это отвечает флаг ixDescending свойства Options определения индекса. Эти определения, в свою очередь, задаются через свойство IndexDefs.

Навигация

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

Мы уже рассматривали навигацию при помощи специального компонента - DBNavigator. Теперь рассмотрим, как можно осуществлять ее программным способом, обращаясь непосредственно к методам набора данных.

Для перемещения указателя текущей записи можно использовать следующие процедуры набора данных:

Для перемещения на несколько записей сразу используют функцию MoveBy - она принимает в качестве аргумента число, на которое должно произойти перемещение, а возвращает число записей, на которое реально переместился курсор (разница может быть вызвана, например тем, что конец набора данных был достигнут "досрочно"). Перемещение при помощи этой функции можно производить в обоих направлениях:

Table1.MoveBy(10); //перемещение указателя на 10 записей вперед Table1.MoveBy(-3); //перемещение указателя на 3 записи назад x:=Table1.MoveBy(y); //использование возвращаемого значения if x<>y then Caption:='Перемещено только на '+IntToStr(x)+' записей';

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

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

Для примера возьмем все ту же таблицу счетов, задействовав компоненты Table, DataSource и DBGrid. Еще нам понадобятся 3 кнопки. Первая будет перемещать курсор на следующую запись, а вторая - на предыдущую. Соответственно, код для первой кнопки будет выглядеть так:

Table1.Next;

А у второй, соответственно:

Table1.Prior;

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

Для реализации этого замысла нам потребуется организовать цикл для обхода всех записей, начиная с первой, каждый раз добавляя значение поля BILL_SUMM к переменной. В завершение останется вывести результат в заголовок окна. В итоге мы получим процедуру, приведенную в листинге 20.2.

Листинг 20.2. Последовательный обход всех записей в источнике данных

procedure TForm1.Button3Click(Sender: TObject); var i: integer; x: currency; begin Table1.First; // начинаем с 1-й записи x:=0; for i:=0 to Table1.RecordCount-1 do begin x:=x+Table1.FieldByName('BILL_SUMM').AsCurrency; Table1.Next; // не забываем перевести курсор на следующую запись end; Caption:=CurrToStr(x); end;

Для того чтобы пройтись по всем записям, здесь мы воспользовались таким свойством набора данных, как RecordCount. Это вполне логичный вариант, однако, он не совсем идеален с той точки зрения, что нам требуется дополнительная переменная. В то же время, у набора данных имеется такое свойство, как Eof, при помощи которого можно узнать, достигнут или нет конец. Соответственно, чтобы избавиться от лишней переменной в своем коде, мы можем использовать цикл while:

while not Table1.Eof do begin x:=x+Table1.FieldByName('BILL_SUMM').AsCurrency; Table1.Next; end;

В любом случае, по завершении цикла мы получим нужный нам результат в переменной x. Исходный код приведен в примере в каталоге Demo/Part4/Navigate.

Помимо свойства Eof, существует и свойство Bof, проверяющее, находится ли указатель в начале набора данных. Он может потребоваться, например, в том случае, если необходимо произвести подсчет в обратном порядке:

Table1.Last; while not Table1.Bof do begin x:=x+Table1.FieldByName('BILL_SUMM').AsCurrency; Table1.Prior; end;

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

Фильтрация

Фильтрацией называют определение ограничений для записей, отбираемых для набора данных. По умолчанию фильтрация отключена, однако всегда можно ею воспользоваться, определив параметры фильтра в свойстве Filter и установив другое свойство - Filtered - в истину.

ПРИМЕЧАНИЕ
При работе с запросами SQL посредством компонента Query или ему аналогичного, фильтрация набора данных действует поверх ограничений, заданных в самом SQL-запросе. Иначе говоря, если при помощи запроса уже был произведен "отсев" данных, то фильтрация дополнительно ограничит число выводимых записей.

В качестве выражения, определяющего фильтр, используется строковое значение, составленное по определенным правилам. В состав фильтра могут входить такие элементы, как имена полей таблиц, литералы, скобки, операции сравнения, а так же арифметические и логические операции. Под литералом здесь понимается заданное явно значение - число, строка или символ. Из арифметических операций доступны такие, как сложение, вычитание, умножение и деление. В качестве логических операций можно использовать AND, OR и NOT. Ну а скобки используются, как обычно, для изменения порядка выполнения операций.

Для примера рассмотрим несколько вариантов фильтров:

BILL_SUMM > 150 BILL_CUST > 2 AND BILL_CUST < 5

В первом случае будут отобраны только те ряды, у которых в поле BILL_SUMM значение превышает 150, а во втором - ряды, поле BILL_CUST которых имеет значения больше 2 и меньше 5. Если в выражении фильтра используются строковые или символьные литералы, то они должны быть заключены в одинарные кавычки:

CUST_NAME = 'OOO "Alpha"'

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

Table1.Filter:='CUST_NAME = ' + QuotedStr(str);

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

Table1.Filter:='CUST_NAME = ''OOO "Gamma"'';

Теперь создадим небольшое приложение, которое будет осуществлять фильтрацию записей в наборе данных. В качестве набора вновь используем таблицу (Table), так же нам понадобятся компоненты DBGrid и DataSource. Для управления фильтром поместим на форму компонент-редактор (Edit) и 2 кнопки (Button). Первая кнопка, назовем ее "Применить" будет включать фильтрацию путем присвоения значения, определенного в редакторе, свойству Filter таблицы и назначения значения истины свойству Filtered:

Table1.Filter:=Edit1.Text; Table1.Filtered:=true;

Вторая же кнопка - "Сброс" - будет просто отключать фильтрацию:

Table1.Filtered:=false;

Теперь остается запустить приложение, ввести какое-либо подходящее выражение в строку фильтра и нажать кнопку "Применить".

Исходный код примера находится в каталоге Demo\Part4\Filter.

Еще одним свойством набора данных, имеющим отношение к фильтрации, является FilterOptions. Оно представляет собой набор из 2 флагов - foCaseInsensitive и foNoPartialCompare. Первый из них, будучи установленным, делает фильтрацию в строках регистронезависимой. Второй же заставит интерпретировать знак * (звездочку) как символ, в противном случае звездочка будет выполнять роль шаблона подстановки. Например, можно определить фильтр таким образом:

CUST_NAME = 'OOO *'

В том случае, если флаг foNoPartialCompare выключен, то будут отобраны все записи, начинающиеся с "ООО ". Если же этот флаг включен, то подойдет только запись, имеющая именно такое значение, т.е. "ООО *".

Поиск

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

Для поиска по любым полям используют функции Locate и Lookup. При этом для поиска с переходом курсора на совпавшую запись используют функцию Locate, а для простого считывания данных из совпавшей записи - Lookup.

Функция Locate производит поиск записи с удовлетворяющими условия поиска значениями полей. Если такая запись будет найдена, то курсор будет переведен на нее, а функция возвратит истину. Если же совпадений не найдется, то функция возвратит ложь. Определена эта функция следующим образом:

function Locate(const KeyFields: string; const KeyValues: Variant; Options: TLocateOptions): Boolean;

Здесь KeyFields определяет поля, по которым производится поиск, KeyValues - выражение для поиска, а Options задает такие параметры, как распознавание регистра в строках и возможность совпадения по части слова.

Для примера создадим приложение, которое сможет осуществлять поиск по названиям клиентов в нашей таблице Customers. Как обычно, понадобятся связка из компонентов Table, DataSource и DBGrid. Кроме них, задействуем однострочный редактор, кнопку и 2 переключателя типа CheckBox. Первый переключатель подпишем как "Без учета регистра", а второй - "Совпадение по части". Теперь остается написать код для процедуры обработки нажатия на кнопку, подобно тому, что приведен в листинге 20.3.

Листинг 20.3. Поиск с опциями

procedure TForm1.Button1Click(Sender: TObject); var lo: TLocateOptions; begin lo:=[]; if CheckBox1.Checked then lo:=lo+[loCaseInsensitive]; if CheckBox2.Checked then lo:=lo+[loPartialKey]; Table1.Locate('CUST_NAME',Edit1.Text,lo); end;

Данный пример иллюстрирует типичное применение поиска по одному полю (см. также пример в Demo\Part4\Search). В том же случае, если поиск необходимо провести по нескольким полям, то сами поля перечисляют через точку с запятой для параметра KeyFields и определяют массив значений для KeyValues:

Table1.Locate('CUST_NAME;CUST_ID',VarArrayOf(['OOO "Beta"',2]),lo);

Здесь производится поиск по полям CUST_NAME и CUST_ID, при этом совпавшими будут считаться записи, у которых в поле CUST_NAME будет значение "OOO "Beta"", а в поле CUST_ID - число 2. Что касается функции VarArrayOf, то она используется для одновременного создания и заполнения массива типа Variant.

Второй метод нахождения записей - Lookup - предназначен для нахождения искомого в источнике данных и возвращения нужных данных. Объявлена она следующим образом:

function Lookup(const KeyFields: string; const KeyValues: Variant; const ResultFields: string): Variant;

Первые 2 аргумента аналогичны тем, что имеются в функции Locate, а 3-й используется для того, чтобы определить поля, значения которых необходимо получить. Сами значения возвращаются либо в виде значения какого-либо простого типа, либо в виде массива, если для возврата запрашивается более одного поля.

Помимо того, что функция Lookup не перемещает курсор на найденное поле, она всегда работает в режиме "строгого" поиска, т.е. при отсутствии опций, понимается, что регистр надо различать, а частичное совпадение недопустимо.

Функции Locate и Lookup хороши тем, что могут использоваться для поиска по любым полям набора данных. Если же поиск требуется осуществлять по индексированным полям, то выбор методов существенно расширяется. В частности, для поиска одной записи можно использовать методы GotoKey и FindKey, производящие поиск по точному соответствию. А методы GotoNearest и FindNearest производят поиск по частичному соответствию. При этом перед вызовом методов GotoKey и GotoNearest необходимо вызывать метод EditKey или SetKey, чтобы перевести компонент Table в режим редактирования ключа поиска:

Table1.EditKey; Table1.FieldByName('CUST_NAME').AsString := Edit1.Text; if not Table1.GotoKey then ShowMessage('Совпадения не найдено!');

Здесь подразумевается, что поле CUST_NAME является индексированным.

Еще одна группа методов, работающих с индексированными полями, позволяет работать с диапазоном записей. К ним относятся: SetRangeStart, SetRangeEnd, EditRangeStart, EditRangeEnd, ApplyRange и CancelRange. Для использования поиска диапазона записей необходимо установить начало и конец диапазона вызовом функций SetRangeStart и SetRangeEnd, или EditRangeStart и EditRangeEnd, указывая при этом граничные значения полей. Затем, вызвав метод ApplyRange, указанный диапазон применяется к набору данных:

with Table1 do begin SetRangeStart; //включаем режим установки начала диапазона FieldByName('CUST_ID').AsString:=Edit1.Text; //определяем начало диапазона SetRangeEnd; //включаем режим установки конца диапазона FieldByName('CUST_ID').AsString:=Edit2.Text; //определяем конец диапазона ApplyRange; //Применяем диапазон end;

Результатом применения диапазона является набор данных, состоящий из удовлетворяющих условиям рядов. С этой точки зрения поиск диапазона очень похож на фильтрацию. Так же не следует забывать, что поля, участвующие в определении границ диапазона, должны быть индексированными.

Редактирование

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

ПРИМЕЧАНИЕ
Следует отметить, что предоставить пользователю возможность правки данных возможно при помощи визуальных компонент - без написания какого-либо кода вообще, для чего достаточно воспользоваться компонентами DBGrid и DBNavigator.

Редактирование записей заключается в изменении значений их полей. Отредактирована в один момент времени может только одна запись, причем только текущая (та, на которую ссылается курсор). Поэтому перед редактированием следует выбрать запись, например, при помощи метода Locate. После этого последовательно выполняются следующие операции:

В виде программного кода это реализуется следующим образом (см. пример в каталоге Demo\Part4\Edit):

Table1.Edit; Table1.FieldByName('CUST_NAME').AsString:=Edit1.Text; Table1.Post;

Здесь мы меняем значение поля CUST_NAME в текущей записи на содержимое однострочного редактора. Если бы требовалось изменить значение, скажем, числового поля, то следовало бы либо преобразовать значение Edit1.Text в число, либо использовать другой компонент, например, UpDown:

Table1.FieldByName('ANY_INT_VALUE').AsInteger:=UpDown1.Position;

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

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

with Table1 do begin First; while not Table1.Eof do begin Edit; FieldByName('BILL_SUMM').AsCurrency:=FieldByName('BILL_SUMM').AsCurrency*1.2; Post; Next; end; end;

Обратите внимание, что метод Edit вызывается при каждой итерации цикла, поскольку после обработки метода Post таблица возвращается к режиму чтения. Так же полезно знать, что метод Next, в случае необходимости, может сам вызывать метод Post, однако если вместо Next использовать, скажем, FindNext, то Post вызван не будет. Поэтому, во избежание возможных ошибок в процессе модернизации кода, желательно вызывать метод Post явно.

Добавление и удаление

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

Очевидно, что последовательность действий практически полностью аналогично тому, что мы уже видели при редактировании, с той лишь разницей, что вместо перехода к режиму редактирования используется переход в режим вставки. Для этих целей предусмотрено 4 метода - Insert, Append, InsertRecord и AppendRecord.

Методы Insert и Append переводят набор данных в режим вставки, и добавляют к нему новую пустую запись. Различие между этими методами состоит лишь в том, что если метод Append всегда добавляет запись в конец набора данных, то метод Insert - в позицию, на которой находится курсор ввода. Поэтому перед обращением к методу Insert обычно предварительно перемещают указатель в требуемую позицию набора данных. Например, для вставки нового ряда в самое начало набора, можно использовать следующий код:

Table1.First; Table1.Insert;

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

Table1.FieldByName('BILL_CUST').AsInteger:='5'; Table1.FieldByName('BILL_SUMM').AsCurrency:='155.99'; Table1.Post;

Если в программе были определены переменные, представляющие собой поля, то, разумеется, будет проще именно их и задействовать:

Table1BILL_CUST.AsInteger:='5'; Table1BILL_SUMM.AsCurrency:='155.99';

Более того, некоторые поля могут являться заполняемыми автоматически, например, поле BILL_ID в таблице счетов имеет автоинкрементный тип и получит нужное значение путем встроенных механизмов СУБД. Правда, произойдет это не сразу, а лишь при выполнении последней стадии редактирования - подтверждения, что, как и при обычной правке делается методом Post. Если же отказаться от внесенной правки (метод Cancel), то не будут сохранены не только введенные значения, но и сам добавленный ряд тоже.

В ряде случаев, особенно когда требуется изменять большинство полей записи, удобно пользоваться специальным методом - SetFields. С его помощью можно задать значения для произвольного количества полей записи одновременно. Так, предыдущий пример с изменением 2 полей можно написать так:

Table1.SetFields([Nil, 5, 155.99]);

Здесь в качестве значения для первого поля указано значение Nil, поскольку подразумевается, что это поле автоинкрементного типа и значение для него будет назначено автоматически во время подтверждения. Отметим так же, что этого самого подтверждения (использования метода Post) в данном случае явно указывать не надо, поскольку метод SetFields вызывает его самостоятельно. Метод SetFields можно использовать не только при добавлении, но и при редактировании записей. В таком случае для тех полей, значения которых менять не требуются, так же указывают Nil.

Наконец, наиболее комплексным подходом к вставке записей является использование методов InsertRecord и AppendRecord. Метод InsertRecord объединяет в себе методы Insert и SetFields. Соответственно, чтобы добавить новую запись с его помощью, достаточно написать:

Table1.InsertRecord([Nil, 5, 155.99]);

Аналогично, метод AppendRecord, объединяет в себе методы Append и SetFields, т.е. добавляет и заполняет значениями запись в конце набора данных.

Что касается удаления записей, то для этих целей используется метод Delete. Он удаляет текущую запись из набора данных и перемещает указатель текущей записи на следующую запись. При этом если запись в момент вызова этого метода находилась в режиме вставки, то вызов Delete, по сути, будет аналогичен обращению к методу Cancel. Если же набор данных не содержит ни одной записи, то вызов метода Delete инициирует исключение.

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

while not Table1.Eof do Table1.Delete;

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

Простое приложение БД

На основе полученных знаний в области баз данных, создадим приложение, которое будет работать с нашей базой данных. Оно должно будет производить такие действия, как вывод списка клиентов с возможностью поиска по нему и выводить сведения обо всех счетах на отдельно взятого клиента. Так же предусмотрим возможность создания новых клиентов и счетов путем заполнения специальных форм. Назовем мы создаваемую программу "База 1.0".

Чтобы не отвлекаться на особенности визуального программирования, при разработке этого приложения мы постараемся максимально использовать программный код для выполнения всех действий, относящихся к взаимодействию с СУБД. Окончательный вариант того, что должно будет получиться, можно найти в каталоге Demo\Part4\DBApp.

Прежде чем приступить собственно к написанию приложения, немного модифицируем тестовую базу данных, для чего в таблицу счетов добавим поле даты счета (тип Date), а в таблицу клиентов - строковое поле адреса (модифицированный вариант БД находится в подкаталоге data, вложенном в указанный выше каталог с рассматриваемым приложением).

Теперь приступим к разработке самой "Базы 1.0". Для начала назовем главное окно приложения MainFrm и сохраним его под именем "main.pas", а сам проект сохраним под названием "mydb". Затем поместим на главной форме компонент DBGrid, отведя для него основное пространство окна, а под ним в ряд расположим однострочный текстовый редактор и 4 кнопки - "Баланс", "Новый счет", "Новый клиент" и "Выход" (рис. 20.2).

Форма главного окна приложения базы даных в Delphi
Рис. 20.2. Форма главного окна приложения базы даных

Чтобы в дальнейшем, при написании кода, не возникало вопросов, какой элемент интерфейса за что отвечает, присвоим всем компонентам более осмысленные имена. Так, таблицу назовем CustGrd, редактор - NameEd, а кнопки - BalanceBtn, NewBillBtn, NewCustBtn и CloseBtn. Учитывая тот факт, что для ввода новых клиентов мы собираемся предусмотреть отдельное окно, то чтобы придать приложению более "профессиональный" вид, в свойстве Options таблицы включим флаг dgRowSelect, в результате чего записи будут выделяться целиком. В свойствах самой формы отключим кнопку разворачивания на полный экран путем выключения флага biMaximize у свойства BorderIcons, а рамку сделаем неизменяемой, установив свойство BorderStyle в bsSingle. Наконец, для свойства Position выберем значение poScreenCenter, а в Caption напишем название программы - "База 1.0".

Дабы не загромождать главную форму приложения компонентами доступа к данным, используем форму данных (File ' New ' Data Module), на которую, в свою очередь, поместим компонент Database и 2 пары компонентов Table и DataSource. Компонент Database назовем MainDB, это же значение используем как название базы данных (свойство DatabaseName), а для свойства DriverName выберем Standard. Компоненты-таблицы назовем CustTbl и BillTbl, а источники данных - CustBs и BillDs, после чего привяжем их к таблицам при помощи свойства DataSet. Саму форму данных назовем data, и сохраним файл под именем dm.pas.

Прежде, чем браться за дальнейшую работу, определим программный код, который будет выполнять собственно подключение к данным. Для этого нам потребуется определить параметры компонента Database (MainDB) и обеих таблиц в момент создания модуля. Сделать это можно, написав для события OnCreate формы data следующий код:

MainDB.Params.Add('path='+ExtractFilePath(paramstr(0))+'data'); MainDB.Connected:=true; CustTbl.DatabaseName:='MainDB'; CustTbl.TableName:='customer.DB'; CustTbl.Active:=true; BillTbl.DatabaseName:='MainDB'; BillTbl.TableName:='bill.DB'; BillTbl.Active:=true;

Теперь можно вернуться к главному окну приложения и после ключевого слова uses в части interface модуля добавить модуль dm. Это позволит нам уже на данном этапе указать в свойстве DataSource таблицы CustGrd нужное нам значение - data.CustDS.

Следующим этапом будет разработка формы отчета по счетам. Для этого нам понадобится еще одна форма (File ' New ' Form), на которой мы разместим таблицу DBGrid и одну единственную кнопку. Как и в главной форме, в части interface, в списке модулей после ключевого слова uses добавим dm. Теперь для свойства DataSource таблицы укажем значение data.BillDS, а саму таблицу назовем BillsGrd. Кроме того, подобно таблице в главном окне, включим флаг dgRowSelect в свойстве Options.

Единственную имеющуюся на этой форме кнопку назовем CloseBtn и сразу же напишем код для обработчика события OnClick, который будет состоять из единственного выражения: close. Наконец, свойство BorderStyle установим в bsDialog, а для свойства Position выберем значение poMainFormCenter.

Вновь перейдем к главному окну и напишем обработчик события для кнопки "Баланс", т.к. именно при помощи этой кнопки будет открываться окно счетов. Для того чтобы вывести все счета для выбранного клиента, нам понадобится фильтрация в таблице счетов по полю BILL_CUST. А для того, чтобы подсчитать сумму всех счетов, надо будет пройтись по всем отфильтрованным записям и просуммировать значение поля BILL_SUMM. Наконец, не помешает вывести в заголовок окна имя клиента. В результате мы получим код, приведенный в листинге 20.4.

Листинг 20.4. Вывод информации по счетам с подсчетом общей суммы

procedure TMainFrm.BalanceBtnClick(Sender: TObject); var summ: Currency; begin Data.BillTbl.Filter:= 'BILL_CUST='+Data.CustTbl.FieldByName('CUST_ID').AsString; Data.BillTbl.Filtered:=true; summ:=0; while not Data.BillTbl.Eof do begin summ:=summ+Data.BillTbl.FieldByName('BILL_SUMM').AsCurrency; Data.BillTbl.Next; end; BillsFrm.Caption:=Data.CustTbl.FieldByName('CUST_NAME').AsString+ ': '+CurrToStr(summ); BillsFrm.ShowModal; end;

Здесь сначала производится фильтрация таблицы счетов по текущему значению поля CUST_ID в таблице клиентов, после чего производится суммирование значений поля BILL_SUMM при помощи заранее объявленной переменной summ. После этого составляется заголовок окна, и оно показывается в качестве модального диалога. Если на данном этапе запустить приложение, то можно будет убедиться, что в главную форму выводится список клиентов, а при нажатии на кнопку "Баланс" открывается окно со счетами. Для упрощения навигации по списку клиентов (предположим, что их там у нас не несколько штук, а несколько десятков или сотен), реализуем поиск по вводу. Разумеется, подобный поиск должен быть регистро-независимым и осуществляться по части слова. Для этого в обработчике события OnChange текстового редактора NameEd следует предусмотреть соответствующий код:

Data.CustTbl.Locate('CUST_NAME',NameEd.Text,[loCaseInsensitive,loPartialKey]);

Теперь займемся интерфейсом для создания новых счетов и новых клиентов. Для этого нам понадобятся еще 2 формы, назовем их BillFrm и CustFrm.На первой нам понадобятся компонент-редактор для ввода суммы (назовем его SummEd) и компонент DateTimePicker для указания даты (DatePick). На второй - 2 редактора: один - для ввода имени клиента (NameEd), другой - для адреса (AddrEd). Так же на обоих формах нам потребуются кнопки "ОК" и "Отмена". Поскольку оба этих окна, подобно окну со счетами, будут модальными диалогами, то установим свойства BorderStyle в bsDialog, и Position - в poMainFormCenter. Для кнопок отмены в обоих случаях код будет выглядеть следующим образом:

ModalResult:=mrCancel;

Это позволит определить дальнейшие действия программы после того, как окно закроется и выполнение должно будет быть продолжено в том месте, откуда данное окно было вызвано. Что касается кнопок ОК, то для формы нового счета она должна будет проверять на корректность значение, введенное в поле суммы. Реализовать такую проверку можно так:

if StrToCurrDef(SummEd.Text,0)<>0 then ModalResult:=mrOk else MessageDlg('Сумма накладной не может быть нулевой, либо число введено неверно',mtError,[mbOk],0);

Здесь подразумевается, что по логике работы программы сумма счета не может быть равной нулю. Таким образом, если пользователь попробует создать нулевой счет, либо в поле суммы напишет что-то, что невозможно преобразовать в вещественное число, то он получит соответствующее сообщение. Если же все указано верно, то окно закроется и вызвавшая его функция получит значение mrOk.

Аналогичным образом следует организовать проверку и в окне клиента, с той лишь разницей, что проверять в нем следует содержимое строки с названием - оно не должно быть пустым:

if NameEd.Text<>'' then ModalResult:=mrOk else MessageDlg('Не указано имя клиента',mtError,[mbOk],0);

Собственно, остается только написать код, который и будет вызывать эти окна из главной формы, и производить нужные манипуляции над данными. В частности, для создания нового счета нам надо будет показать окно-форму счета, не забыв при этом указать в заголовке окна имя клиента, затем, если форма возвратит результат mrOk, то выполнить вставку данных, введенных пользователем в окне счета, в таблицу bill. Такая процедура, написанная с использованием метода Append, представлена в листинге 20.5. В нем же представлен вариант добавления записи в таблицу клиентов, но с использованием комбинированного метода AppendRecord.

Листинг 20.5. Добавление новых записей в таблицу счетов и в таблицу клиентов

procedure TMainFrm.NewBillBtnClick(Sender: TObject); begin with BillFrm, Data.BillTbl do begin Caption:='Счет для '+Data.CustTbl.FieldByName('CUST_NAME').AsString; ShowModal; if ModalResult<>mrOk then exit; Append; FieldByName('BILL_CUST').AsInteger:= Data.CustTbl.FieldByName('CUST_ID').AsInteger; FieldByName('BILL_SUMM').AsString:=SummEd.Text; FieldByName('BILL_DATE').AsDateTime:=DatePick.Date; Post; end; end; procedure TMainFrm.NewCustBtnClick(Sender: TObject); begin with CustFrm do begin ShowModal; if ModalResult<>mrOk then exit; Data.CustTbl.AppendRecord([nil,NameEd.Text,AddrEd.Text]); Data.CustTbl.Post; end; end;

Таким образом, мы реализовали все задуманные функции программы. Единственным недостатком на данный момент является то, что программа не предоставляет интерфейс для изменения данных. Однако это довольно просто реализуется путем использования тех же форм, что мы уже создали для добавления данных. Все отличия будут состоять лишь в том, что при редактировании они должны будут появляться с уже заполненными полями, а при подтверждении изменений пользователем вместо добавления новой записи будет производиться модификация данных в текущей записи. Само же отображение формы логично будет ассоциировать с двойным щелчком мышкой по соответствующей таблице. Так, для редактирования записи клиента, можно определить следующий обработчик события OnDblClick таблицы CustGrd:

with CustFrm do begin NameEd.Text:=Data.CustTbl.FieldByName('CUST_NAME').AsString; AddrEd.Text:=Data.CustTbl.FieldByName('CUST_ADDRESS').AsString; ShowModal; if ModalResult<>mrOk then exit; Data.CustTbl.Edit; Data.CustTbl.FieldByName('CUST_NAME').AsString:=NameEd.Text; Data.CustTbl.FieldByName('CUST_ADDRESS').AsString:=AddrEd.Text; Data.CustTbl.Post; end;

Что касается изменения счетов, то в форме списка счетов (BillsFrm) определим вызов формы составления счета (BillFrm) так же по двойному щелчку, по таблице BillsGrd. При этом следует не забыть указать модуль формы счета в списке uses части implementation. Полный код этой части модуля приведен в листинге 20.6.

Листинг 20.6. Часть implementation модуля bills

implementation uses bill; {$R *.dfm} procedure TBillsFrm.CloseBtnClick(Sender: TObject); begin close; end; procedure TBillsFrm.BillsGrdDblClick(Sender: TObject); begin with BillFrm, Data.BillTbl do begin Caption:='Счет для '+Data.CustTbl.FieldByName('CUST_NAME').AsString; SummEd.Text:=FieldByName('BILL_SUMM').AsString; DatePick.Date:=FieldByName('BILL_DATE').AsDateTime; ShowModal; if ModalResult<>mrOk then exit; Edit; FieldByName('BILL_SUMM').AsString:=SummEd.Text; FieldByName('BILL_DATE').AsDateTime:=DatePick.Date; Post; end; end; end.

Теперь наша программа умеет даже несколько больше, чем планировалось ранее. А именно, помимо обеспечения удобной навигации по списку клиентов (под удобством мы здесь понимаем возможность поиска по первым введенным буквам), просмотра счетов для каждого клиента, и созданию новых записей клиентов и счетов, мы так же получили возможность изменять все эти записи. Теоретически, подобных функциональных возможностей мы могли бы добиться, просто поместив на форму 2 связанных по принципу "один ко многим" таблицы, как это уже было продемонстрировано в самом начале этой части книги. Однако для конечного пользователя тот вариант был бы гораздо менее понятным и удобным. Кроме того, здесь мы реализовали базовую проверку вводимых значений на допустимость, что на практике позволяет минимизировать возможные ошибки при вводе данных.

Избранное

SNK GSCP
SNK GSCP - новая библиотека для PHP 5!
Web Studio
Web Studio и Visual Workshop
Библиотека:
Стандарты на web-технологии
Монополия
Монополия Android
Загрузки:
скачать программы
Продукция:
программы и книги
Техподдержка / Связаться с нами
Copyright © 1999-2020 SNK. Все права защищены.
При использовании материалов с сайта ссылка на источник обязательна.
Рейтинг@Mail.ru