Введение в DELPHI

  35790931     

Передача параметров


В Delphi процедурам и функциям (а, следовательно, и методам классов) могут передаваться параметры для того, чтобы обеспечить их необходимой для работы информацией. Программа PARAMS демонстрирует, как использовать передачу параметров в методы Delphi. Кроме того, мы узнаем, как: создавать свои собственные процедуры добавлять процедуру в класс, формируя метод класса вызывать одну процедуру из другой.

Программа PARAMS позволяет Вам вводить фразы в строки редактирования. После нажатия кнопки "Вызов процедуры WriteAll" строка из управляющего элемента EditSource скопируется в шесть управляющих элементов - строк редактирования, как показано на Рис. Ошибка! Текст указанного стиля в документе отсутствует.-D.

Далее мы не будем подробно останавливаться на том, как размещать компоненты на форме - считаем, что это Вы уже умеете. После того как Вы разместили на форме семь компонентов Edit, переименуйте с помощью Инспектора Объектов седьмой компонент (Edit7) в EditSource. Положите на форму компонент Button, и в Object Inspector измените его заголовок (свойство Caption) на "Вызов процедуры WriteAll" (естественно, Вы можете заменить его шрифт, цвет и т.д.).

Рис. Ошибка! Текст указанного стиля в документе отсутствует.-D: Программа PARAMS позволяет вызовом одной процедуры заполнить 6 строк редактирования

После завершения проектирования формы класс TForm1 будет выглядеть следующим образом: TForm1 = class(TForm) Edit1: TEdit; Edit2: TEdit; Edit3: TEdit; Edit4: TEdit; Edit5: TEdit; Edit6: TEdit; EditSource: TEdit; Button1: TButton; end;

Следующий шаг состоит в добавлении метода, вызываемого по нажатию пользователем кнопки Button1. Это, напомним, можно сделать двумя способами: Перейти в Инспекторе Объектов на страничку "Events" (предварительно выбрав компонент Button1 на форме), выбрать слово OnClick и дважды щелкнуть мышкой на пустой строчке справа от него Просто дважды щелкнуть на компоненте Button1 на форме.

Delphi сгенерирует следующую "заготовку": procedure TForm1.Button1Click(Sender: TObject); begin end;

Цель программы PARAMS - научить Вас писать процедуры и передавать в них параметры. В частности, программа PARAMS реагирует на нажатие кнопки Button1 путем вызова процедуры WriteAll и передачи ей в качестве параметра содержимого строки редактирования EditSource (EditSource.Text). procedure TForm1.Button1Click(Sender: TObject); begin WriteAll(EditSource.Text); end;

Важно понять, что объект EditSource является экземпляром класса TEdit и, следовательно, имеет свойство Text, содержащее набранный в строке редактирования текст. Как Вы уже, наверное, успели заметить, по умолчанию свойство Text содержит значение, совпадающее со значением имени компонента (Name)- в данном случае "EditSource". Свойство Text Вы, естественно, можете редактировать как в режиме проектирования, так и во время выполнения.

Текст, который должен быть отображен в шести строках редактирования, передается процедуре WriteAll как параметр. Чтобы передать параметр процедуре, просто напишите имя этой процедуры и заключите передаваемый параметр (параметры) в скобки - вот так: WriteAll(EditSource.Text);

Заголовок этой процедуры выглядит следующим образом: procedure TForm1.WriteAll(NewString: String);

где указано, что передаваемый процедуре параметр NewString должен иметь тип String.



Вспомним, что задача процедуры WriteAll состоит в копировании содержимого строки редактирования EditSource в шесть других строк редактировани Edit1-Edit6. Поэтому процедура должна выглядеть следующим образом: procedure TForm1.WriteAll(NewString: String); begin Edit1.Text := NewString; Edit2.Text := NewString; Edit3.Text := NewString; Edit4.Text := NewString; Edit5.Text := NewString; Edit6.Text := NewString; end;

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

Возвратимся еще раз к заголовку процедуры. Заголовок состоит из пяти частей: procedure TForm1.WriteAll(NewString: String);

Первая часть - зарезервированное слово "procedure"; пятая часть - концевая точка с запятой ";". Обе эти части служат определенным синтаксическим целям, а именно: первая информирует компилятор о том, что определен синтаксический блок "процедура", а вторая указывает на окончание заголовка (собственно говоря, все операторы в Delphi должны заканчиваться точкой с запятой).

Вторая часть заголовка - слово "TForm1", которое квалифицирует то обстоятельство, что данная процедура является методом класса TForm1.

Третья часть заголовка - имя процедуры. Вы можете выбрать его любым, по вашему усмотрению. В данном случае мы назвали процедуру "WriteAll".

Четвертая часть заголовка - параметр. Параметр декларируется внутри скобок и, в свою очередь, состоит из двух частей. Первая часть - имя параметра, вторая - его тип. Эти части разделены двоеточием. Если Вы описываете в процедуре более чем один параметр, нужно разделить их точкой с запятой, например: procedure Example(Param1: String; Param2: String);

После того как Вы создали "вручную" заголовок процедуры, являющейся методом класса, Вы должны включить его в декларацию класса, например, путем копирования (еще раз напомним, что для методов, являющихся откликами на дельфийские события, данное включение производится автоматически): TForm1 = class(TForm) Edit1: TEdit; Edit2: TEdit; Edit3: TEdit; Edit4: TEdit; Edit5: TEdit; Edit6: TEdit; EditSource: TEdit; Button1: TButton; procedure Button1Click(Sender: TObject); procedure WriteAll(NewString: String); end;

В данном месте нет необходимости оставлять в заголовке метода слово "TForm1", так как оно уже присутствует в описании класса.

Листинг Ошибка! Текст указанного стиля в документе отсутствует.-C показывает полный текст головного модуля программы PARAMS. Мы не включили сюда файл проекта, поскольку, как уже упоминалось, он практически одинаков для всех программ.

Листинг Ошибка! Текст указанного стиля в документе отсутствует.-C: Исходный код головного модуля программы PARAMS показывает, как использовать строки редактирования и как передавать параметры. Unit Main; interface uses WinTypes, WinProcs, Classes, Graphics, Controls, Printers, Forms, StdCtrls; type TForm1 = class(TForm) Edit1: TEdit; Edit2: TEdit; Edit3: TEdit; Edit4: TEdit; Edit5: TEdit; Edit6: TEdit; EditSource: TEdit; Button1: TButton; procedure Button1Click(Sender: TObject); procedure WriteAll(NewString: String); end; var Form1: TForm1; implementation {$R *.DFM} procedure TForm1.WriteAll(NewString: String); begin Edit1.Text := NewString; Edit2.Text := NewString; Edit3.Text := NewString; Edit4.Text := NewString; Edit5.Text := NewString; Edit6.Text := NewString; end; procedure TForm1.Button1Click(Sender: TObject); begin WriteAll(EditSource.Text); end; end.

При экспериментах с программой PARAMS Вы можете попробовать изменить имена процедур и параметров. Однако, следует помнить, что ряд слов в Delphi являются зарезервированными, и употреблять их в идентификаторах (именах процедур, функций, переменных, типов, констант) не разрешается - компилятор сразу же обнаружит ошибку. К ним относятся такие слова, как "procedure", "string", "begin", "end" и т.п.; полный же список их приведен в on-line справочнике Delphi.

Не старайтесь запомнить сразу все зарезервированные слова - компилятор "напомнит" Вам о неправильном их использовании выдачей сообщения типа "Identifier expected." (Ожидался идентификатор, а обнаружено зарезервированное слово).



Передача параметров через TDataSource


В предыдущем Уроке Вы видели способ создания отношения однин-ко-многим между двумя таблицами. Теперь речь пойдет о выполнении того же самого действия с использованием объекта TQuery. Этот способ более гибок в том отношении, что он не требует индексации по полям связи.

Объект TQuery имеет свойство DataSource, которое может использоваться для того, чтобы создать связь с другим DataSet. Не имеет значения, является ли другой DataSet объектом TTable, TQuery, или некоторый другим потомком TDataSet. Все что нужно для установления соединения - это удостовериться, что у того DataSet есть связанный с ним DataSource.

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

Рассмотрите следующий параметризованный запрос: select * from Orders where CustNo = :CustNo

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

Давайте переделаем пример из прошлого урока (LINKTBL - связывание двух таблиц). Создайте новый проект, положите на форму один набор TTable, TDataSource и TDBGrid. Привяжите его к таблице CUSTOMER. Положите на форму второй набор - TQuery, TDataSource и TDBGrid и свяжите объекты между собой. (см рис.4).

В свойстве SQL наберите текст запроса: select * from Orders where CustNo = :CustNo

В свойстве DatabaseName для Query1 укажите DBDEMOS.
В свойстве DataSource для Query1 укажите DataSource1.

Поставьте Active = True и запустите программу.

Рис.4: Программа LINKQRY - связанные курсоры с помощью SQL



Передача переменной в отчет


Следующий код показывает, как передать переменную в отчет. В примере строковой переменной отчета 'City' присваивается значение 'Bombey'. Подразумевается, что есть готовый отчет с данной переменной.

Поместите компонент TReport на форму и установите требуемые свойства для вызова печати отчета. Напишите обработчик OnClick для кнопки Button1 на форме (кнопка - для простоты) : procedure TForm1.Button1Click(Sender: TObject); begin Report1.InitialValues.Clear; Report1.InitialValues.Add('@City=<Bombey>'); Report1.Run; end;



Поддержка OLE 2.0, DDE и VBX


Это очень важная особенность для разработчиков в среде Windows, поскольку в уже существующие Windows-приложения программист может интегрировать то, что разработает при помощи Delphi.



Подробнее об Инспекторе Объектов


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

Лучший путь для изучения Инспектора объектов - поработать с ним. Для начала откройте новый проект, выбрав пункт меню File | New Project. Затем положите на форму объекты TMemo, TButton, и TListBox, как показано на рис.9.

Рис.9: Простой объект TForm с компонентами TMemo, TButton, и TListBox.

Сперва рассмотрим работу со свойствами на примере свойства Ctl3D (по умолчанию включено). Выберите форму, щелкнув на ней мышкой, перейдите в Инспектор Объектов и несколько раз с помощью двойных щелчков мышью переключите значение свойства Ctl3D. Заметьте, что это действие радикально меняет внешний вид формы. Изменение свойства Ctl3D формы автоматически изменяет свойство Ctl3D каждого дочернего окна, помещенного на форму.

Вернитесь на форму и поставьте значение Ctl3D в True. Теперь нажмите клавишу и щелкните на TMemo и затем на TListBox. Теперь оба объекта имеют по краям маленькие квадратики, показывающие, что объекты выбраны.

Рис.10: Пункт меню Edit дает Вам доступ к двум диалогам для выравнивания выбранного набора компонент. Первый диалог - управление размерами объектов в наборе.

Выбрав два или более объектов одновременно, Вы можете выполнить большое число операций над ними. Например, передвигать по форме. Затем попробуйте выбрать пункт меню Edit | Size и установить оба поля Ширину(Width) и Высоту(Height) в Grow to Largest, как показано на рис.10. Теперь оба объекта стали одинакового размера. Затем выберите пункт меню Edit | Align и поставьте в выравнивании по горизонтали значение Center (см. рис.11).

Рис.11: Диалог Alignment помогает выровнять компоненты на форме.

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

Рассмотрим изменение свойств объектов на примере свойства Color. Есть три способа изменить его значение в Инспекторе Объектов. Первый - просто напечатать имя цвета (clRed) или номер цвета. Второй путь - нажать на маленькую стрелку справа и выбрать цвет из списка. Третий путь - дважды щелкнуть на поле ввода свойства Color. При этом появится диалог выбора цвета.

Свойство Font работает на манер свойства Color. Чтобы это посмотреть, сначала выберите свойство Font для объекта TMemo и дважды щелкните мышкой на поле ввода. Появится диалог настройки шрифта, как показано на рис.12. Выберите, например, шрифт New Times Roman и установите какой-нибудь очень большой размер, например 72. Затем измените цвет фонта с помощью ComboBox'а в нижнем правом углу окна диалога. Когда Вы нажмете кнопку OK, Вы увидите, что вид текста в объекте TMemo радикально изменился.

Рис.12: Диалог выбора шрифта позволяет Вам задать тип шрифта, размер, и цвет.

В завершение краткого экскурса по Инспектору Объектов дважды щелкните на свойство Items объекта ListBox. Появится диалог, в котором Вы можете ввести строки для отображения в ListBox. Напечатайте несколько слов, по одному на каждой строке, и нажмите кнопку OK. Текст отобразится в ListBox'е.



Поля


В большинстве случаев, когда Вы хотите получить доступ из программы к индивидуальные полям записи, Вы можете использовать одно из следующих свойств или методов, каждый из которых принадлежат TDataSet: property Fields[Index: Integer]; function FieldByName(const FieldName: string): TField; property FieldCount;

Свойство FieldCount возвращает число полей в текущей структуре записи. Если Вы хотите программным путем прочитать имена полей, то используйте свойство Fields для доступа к ним: var S: String; begin S := Fields[0].FieldName; end;

Если Вы работали с записью в которой первое поле называется CustNo, тогда код показанный выше поместит строку "CustNo" в переменную S. Если Вы хотите получить доступ к имени второго поля в вышеупомянутом примере, тогда Вы могли бы написать: S := Fields[1].FieldName;

Короче говоря, индекс передаваемый в Fields (начинающийся с нуля), и определяет номер поля к которому Вы получите доступ, т.е. первое поле - ноль, второе один, и так далее.

Если Вы хотите прочитать текущее содержание конкретного поля конкретной записи, то Вы можете использовать свойство Fields или метод FieldsByName. Для того, чтобы найти значение первого поля записи, прочитайте первый элемент массива Fields: S := Fields[0].AsString;

Предположим, что первое поле в записи содержит номер заказчика, тогда код, показанный выше, возвратил бы строку типа "1021", "1031" или "2058". Если Вы хотели получить доступ к этот переменный, как к числовой величине, тогда Вы могли бы использовать AsInteger вместо AsString. Аналогично, свойство Fields включают AsBoolean, AsFloat и AsDate.

Если хотите, Вы можете использовать функцию FieldsByName вместо свойства Fields: S := FieldsByName('CustNo').AsString;

Как показано в примерах выше, и FieldsByName, и Fields возвращают те же самые данные. Два различных синтаксиса используются исключительно для того, чтобы обеспечить программистов гибким и удобным набором инструментов для программного доступа к содержимому DataSet.

Давайте посмотрим на простом примере, как можно использовать доступ к полям таблицы во время выполнения программы. Создайте новый проект, положите на форму объект TTable, два объекта ListBox и две кнопки - "Fields" и "Values" (см рис.4).

Соедините объект TTable с таблицей CUSTOMER, которая поставляется вместе с Delphi (DBDEMOS), не забудьте открыть таблицу (Active = True).

Рис.4: Программа FLDS показывает, как использовать свойство Fields.

Сделайте Double click на кнопке Fields и создайте a метод который выглядит так: procedure TForm1.FieldsClick(Sender: TObject); var i: Integer; begin ListBox1.Clear; for i := 0 to Table1.FieldCount - 1 do ListBox1.Items.Add(Table1.Fields[i].FieldName); end;

Обработчик события начинается с очистки первого ListBox1, затем он проходит через все поля, добавляя их имена один за другим в ListBox1. Заметьте, что цикл показанный здесь пробегает от 0 до FieldCount - 1. Если Вы забудете вычесть единицу из FieldCount, то Вы получите ошибку "List Index Out of Bounds", так как Вы будете пытаться прочесть имя поля которое не существует.

Предположим, что Вы ввели код правильно, и заполнили ListBox1 именами всех полей в текущей структуре записи.

В Delphi существуют и другие средства которые позволяют Вам получить ту же самую информацию, но это самый простой способ доступа к именам полей в Run Time.

Свойство Fields позволяет Вам получить доступ не только именам полей записи, но также и к содержимому полей. В нашем примере, для второй кнопки напишем: procedure TForm1.ValuesClick(Sender: TObject); var i: Integer; begin ListBox2.Clear; for i := 0 to Table1.FieldCount - 1 do ListBox2.Items.Add(Table1.Fields[i].AsString); end;

Этот код добавляет содержимое каждого из полей во второй listbox. Обратите внимание, что вновь счетчик изменяется от нуля до FieldCount - 1.

Свойство Fields позволяет Вам выбрать тип результата написав Fields[N].AsString. Этот и несколько связанных методов обеспечивают a простой и гибкий способ доступа к данным, связанными с конкретным полем. Вот список доступных методов который Вы можете найти в описании класса TField: property AsBoolean property AsFloat property AsInteger property AsString property AsDateTime

Всякий раз (когда это имеет смысл), Delphi сможет сделать преобразования. Например, Delphi может преобразовывать поле Boolean к Integer или Float, или поле Integer к String. Но не будет преобразовывать String к Integer, хотя и может преобразовывать Float к Integer. BLOB и Memo поля - специальные случаи, и мы их рассмотрим позже. Если Вы хотите работать с полями Date или DateTime, то можете использовать AsString и AsFloat для доступа к ним.

Как было объяснено выше, свойство FieldByName позволяет Вам получить доступ к содержимому определенного поля просто указав имя этого поля: S := Table1.FieldByName('CustNo').AsString;

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



Понимание событий


Событийное программирование есть не только в Windows, и данную черту можно реализовать не только в операционной системе. Например, любая DOS программа может быть основана на простом цикле, работающем все время жизни программы в памяти. Ниже вы найдете гипотетический пример, как данный код может выглядеть: begin InitializeMemory; repeat CheckForMouseEvent(Events); CheckForKeyPress(Events) HandleEvents(Events); until Done := True; DisposeMemory; end.

Это типичный пример программы, ориентированной на события. Она начинается и заканчивается инициализацией и освобождением памяти. В программе присутствует простой цикл repeat..until, который проверяет появление событий от мыши и клавиатуры и затем дает возможность программисту ответить на эти события.

Переменная Events может быть записью с простой структурой: TEvent = record X, Y: Integer; MouseButton: TButton; Key: Word; end;

Тип TButton, указанный выше, можно декларировать так: TButton = (lButton, rButton);

Эти структуры позволяют вам проследить, где находится мышь, каково состояние ее кнопок, и значение нажатой клавиши на клавиатуре. Конечно, это пример очень простой структуры, но заложенные здесь принципы отражают то, что происходит внутри Windows или внутри других систем, ориентированных на события, вроде Turbo Vision. Если бы программа, приведенная выше, была редактором текста, то обработчик HandleEvent для такой программы мог бы иметь вид: procedure HandleEvent(Events: TEvent); begin case Events.Key of 'A'..'z': Write(Events.Key); EnterKey: Write(CarriageReturn); EscapeKey: Done := True; end; end;

Согласно коду выше, программа будет печатать букву 'a' при нажатии этой клавиши и перейдет на новую строку, если нажата клавиша 'Enter'. Нажатие 'Esc' приведет к завершению программы.

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

Надеюсь, что приведенный пример дает некоторое понимание работы ориентированной на события системы. Единственное, что осталось пропущенным - почему система Windows так спроектирована.

Одной из основных причин, почему Microsoft сделал Windows по такой схеме, является тот факт, что множество задач работает в среде одновременно. В многозадачных системах операционная система должна знать, щелкнул ли пользователь мышкой на определенное окно. Если это окно было частично перекрыто другим, то это становится известно операционной системе и она перемещает окно на передний план. Понятно, что неудобно заставлять само окно выполнять эти действия. Операционной системе лучше обрабатывать все нажатия клавиш и кнопок на мыши и затем передавать их в остальные программы в виде событий.

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

Когда пользователь щелкает мышкой, операционная система обрабатывает это событие и передает его в окно, которое должно обработать данное событие. Созданное сообщение, в этом случае, пересылается в некую процедуру DefWindowProc окна (default window procedure). DefWindowProc - аналог процедуры HandleEvent из примера, приведенного выше.

Каждое окно в Windows имеет свою DefWindowProc. Чтобы полностью понять данное утверждение, представьте, что каждая кнопка, каждый ListBox, каждое поле ввода и т.д. на самом деле являются окнами и имеют свою процедуру DefWindowProc. Это очень гибкая и мощная система, но она может заставить программиста писать очень сложный код. Delphi дает возможность быть защищенным от такой структуры программы.

Почти все, что происходит в Windows принимает форму сообщений и, если вы хотите их использовать в Delphi (в большей мере это необходимо при написании своих собственных компонент), то нужно понять, как эти сообщения работают.

Если посмотреть на DefWindowProc в справочнике по Windows API, то можно увидеть следующее определение: function DefWindowProc(Wnd: HWnd; Msg, wParam: Word; lParam: LongInt);

Каждое сообщение, посылаемое в окно, состоит из четырех частей: первая часть - handle окна, получающего сообщение, Msg сообщает, что произошло а третья и четвертая части (wParam и lParam) содержат дополнительную информацию о событии. Вместе эти четыре части являются аналогом показанной выше структуры TEvent.

Вторая часть сообщения имеет длину 16 бит и сообщает, что за событие произошло. Например, если нажата левая кнопка на мыши, то переменная Msg содержит значение WM_LBUTTONDOWN. Существуют десятки различного типа cообщений и они называются вроде WM_GETTEXT, WM_HSCROLL, WM_GETTEXTLENGTH и т.п. Список всех сообщений можно видеть в справочнике по Windows API (on-line help).

Последние две переменные, длиной 16 и 32 бита, называются wParam и lParam. Они сообщают программисту важную дополнительную информацию о каждом событии. Например при нажатии кнопки мыши, lParam содержит координаты указателя мыши.

Одна из хитростей заключается в том, как выделить нужную информацию из этих переменных. В большинстве случаев Delphi освобождает вас от необходимости выполнять данную задачу. Например, в обработчике события OnMouseDown для формы вы просто используете координаты X и Y. Как программисту, вам не нужно прилагать усилия для получения сообщения и связанных с ним параметров. Все, что связано с событиями, представлено в простом и непосредственном виде: procedure TForm1.FormMouseDown(Sender: TObject; Button: TMouseButton; Shift: TShiftState; X, Y: Integer);

Итак, если подвести итог, то должно стать ясным следующее: Windows является системой ориентированной на события; События в Windows принимают форму сообщений; В недрах VCL Delphi сообщения Windows обрабатываются и преобразуются в более простую для программиста форму; Обработка событий в Delphi сводится к написанию для каждого объекта своих обработчиков; События в программе на Delphi вызываются не только сообщениями Windows, но и внутренними процессами.



Понятие DLL


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

Программа1 Программа2 : : MyFunc(:) MyFunc(:) : : код функции MyFunc код функции MyFunc код других функций код других функций

Рис.1 : Вызов функций при использовании статической компоновки

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

Рис.2: Вызов функций при использовании динамической компоновки

Но, чем же отличаются Dynamic Link Library (DLL) от обычных приложений? Для понимания этого требуется уточнить понятия задачи (task), экземпляра (копии) приложения (instance) и модуля (module).

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

Рис.3 : Копии приложения и модуль приложения

DLL - библиотека также является модулем. Она находится в памяти в единственном экземпляре и содержит сегмент кода и ресурсы, а также сегмент данных (см. рис. 4).

Рис.4 : Структура DLL в памяти

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

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

Часто, в виде DLL создаются отдельные наборы функций, объединенные по тем или иным логическим признакам, аналогично тому, как концептуально происходит планирование модулей ( в смысле unit ) в Pascal. Отличие заключается в том, что функции из модулей Pascal компонуются статически - на этапе линковки, а функции из DLL компонуются динамически, то есть в run-time.



Предопределенные обработчики исключительных ситуаций


Ниже Вы найдете справочную информацию по предопределенным исключениям, необходимую для профессионального программирования в Delphi. Exception - базовый класс-предок всех обработчиков исключительных ситуаций. EAbort - "скрытое" исключение. Используйте его тогда, когда хотите прервать тот или иной процесс с условием, что пользователь программы не должен видеть сообщения об ошибке. Для повышения удобства использования в модуле SysUtils предусмотрена процедура Abort, определенная, как: procedure Abort; begin raise EAbort.CreateRes(SOperationAborted) at ReturnAddr; end; EComponentError - вызывается в двух ситуациях: при попытке регистрации компоненты за пределами процедуры Register; когда имя компоненты не уникально или не допустимо. EConvertError - происходит в случае возникновения ошибки при выполнении функций StrToInt и StrToFloat, когда конвертация строки в соответствующий числовой тип невозможна. EInOutError - происходит при ошибках ввода/вывода при включенной директиве {$I+}. EIntError - предок исключений, случающихся при выполнении целочисленных операций. EDivByZero - вызывается в случае деления на ноль, как результат RunTime Error 200. EIntOverflow - вызывается при попытке выполнения операций, приводящих к переполнению целых переменных, как результат RunTime Error 215 при включенной директиве {$Q+}. ERangeError - вызывается при попытке обращения к элементам массива по индексу, выходящему за пределы массива, как результат RunTime Error 201 при включенной директиве {$R+}. EInvalidCast - происходит при попытке приведения переменных одного класса к другому классу, несовместимому с первым (например, приведение переменной типа TListBox к TMemo). EInvalidGraphic - вызывается при попытке передачи в LoadFromFile файла, несовместимого графического формата. EInvalidGraphicOperation - вызывается при попытке выполнения операций, неприменимых для данного графического формата (например, Resize для TIcon). EInvalidObject - реально нигде не используется, объявлен в Controls.pas. EInvalidOperation - вызывается при попытке отображения или обращения по Windows-обработчику (handle) контрольного элемента, не имеющего владельца (например, сразу после вызова MyControl:=TListBox.Create(...) происходит обращение к методу Refresh). EInvalidPointer - происходит при попытке освобождения уже освобожденного или еще неинициализированного указателя, при вызове Dispose(), FreeMem() или деструктора класса. EListError - вызывается при обращении к элементу наследника TList по индексу, выходящему за пределы допустимых значений (например, объект TStringList содержит только 10 строк, а происходит обращение к одиннадцатому). EMathError - предок исключений, случающихся при выполнении операций с плавающей точкой. EInvalidOp - происходит, когда математическому сопроцессору передается ошибочная инструкция. Такое исключение не будет до конца обработано, пока Вы контролируете сопроцессор напрямую из ассемблерного кода. EOverflow - происходит как результат переполнения операций с плавающей точкой при слишком больших величинах. Соответствует RunTime Error 205. Underflow - происходит как результат переполнения операций с плавающей точкой при слишком малых величинах. Соответствует RunTime Error 206. EZeroDivide - вызывается в результате деления на ноль. EMenuError - вызывается в случае любых ошибок при работе с пунктами меню для компонент TMenu, TMenuItem, TPopupMenu и их наследников. EOutlineError - вызывается в случае любых ошибок при работе с TOutLine и любыми его наследниками. EOutOfMemory - происходит в случае вызовов New(), GetMem() или конструкторов классов при невозможности распределения памяти. Соответствует RunTime Error 203. EOutOfResources - происходит в том случае, когда невозможно выполнение запроса на выделение или заполнение тех или иных Windows ресурсов (например таких, как обработчики - handles). EParserError - вызывается когда Delphi не может произвести разбор и перевод текста описания формы в двоичный вид (часто происходит в случае исправления текста описания формы вручную в IDE Delphi). EPrinter - вызывается в случае любых ошибок при работе с принтером. EProcessorException - предок исключений, вызываемых в случае прерывания процессора- hardware breakpoint. Никогда не включается в DLL, может обрабатываться только в "цельном" приложении. EBreakpoint - вызывается в случае останова на точке прерывания при отладке в IDE Delphi. Среда Delphi обрабатывает это исключение самостоятельно. EFault - предок исключений, вызываемых в случае невозможности обработки процессором тех или иных операций. EGPFault - вызывается, когда происходит "общее нарушение защиты" - General Protection Fault. Соответствует RunTime Error 216. EInvalidOpCode - вызывается, когда процессор пытается выполнить недопустимые инструкции. EPageFault - обычно происходит как результат ошибки менеджера памяти Windows, вследствие некоторых ошибок в коде Вашего приложения. После такого исключения рекомендуется перезапустить Windows. EStackFault - происходит при ошибках работы со стеком, часто вследствие некорректных попыток доступа к стеку из фрагментов кода на ассемблере. Компиляция Ваших программ со включенной проверкой работы со стеком {$S+} помогает отследить такого рода ошибки. ESingleStep - аналогично EBreakpoint, это исключение происходит при пошаговом выполнении приложения в IDE Delphi, которая сама его и обрабатывает. EPropertyError - вызывается в случае ошибок в редакторах свойств, встраиваемых в IDE Delphi. Имеет большое значение для написания надежных property editors. Определен в модуле DsgnIntf.pas. EResNotFound - происходит в случае тех или иных проблем, имеющих место при попытке загрузки ресурсов форм - файлов .DFM в режиме дизайнера. Часто причиной таких исключений бывает нарушение соответствия между определением класса формы и ее описанием на уровне ресурса (например,вследствие изменения порядка следования полей-ссылок на компоненты, вставленные в форму в режиме дизайнера). EStreamError - предок исключений, вызываемых при работе с потоками. EFCreateError - происходит в случае ошибок создания потока (например, при некорректном задании файла потока). EFilerError - вызывается при попытке вторичной регистрации уже зарегистрированного класса (компоненты). Является, также, предком специализированных обработчиков исключений, возникающих при работе с классами компонент. EClassNotFound - обычно происходит, когда в описании класса формы удалено поле-ссылка на компоненту, вставленную в форму в режиме дизайнера. Вызывается, в отличие от EResNotFound, в RunTime. EInvalidImage - вызывается при попытке чтения файла, не являющегося ресурсом, или разрушенного файла ресурса, специализированными функциями чтения ресурсов (например, функцией ReadComponent). EMethodNotFound - аналогично EClassNotFound, только при несоответствии методов, связанных с теми или иными обработчиками событий. EReadError - происходит в том случае, когда невозможно прочитать значение свойства или другого набора байт из потока (в том числе ресурса). EFOpenError - вызывается когда тот или иной специфированный поток не может быть открыт (например, когда поток не существует). EStringListError - происходит при ошибках работы с объектом TStringList (кроме ошибок, обрабатываемых TListError).



Преобразование типов (CAST)


В SQL имеется возможность преобразовать значение столбца или функции к другому типу для более гибкого использования операций сравнения. Для этого используется функция CAST.

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

Из типа данныхВ тип данных
NUMERIC CHAR,VARCHAR, DATE
CHAR, VARCHARNUMERIC, DATE
DATE CHAR,VARCHAR, DATE
SELECT first_name, last_name, dept_no FROM employee WHERE CAST(dept_no AS char(20)) CONTAINING "00"

получить список сотрудников, занятых в отделах, номера которых содержат "00"

FIRST_NAMELAST_NAME DEPT_NO
RobertNelson600
TerriLee000
StewartHall900
WalterSteadman900
Mary S.MacDonald100
Oliver H.Bender000
KellyBrown600
MichaelYanowski100



реализованы все три вышеописанных


В примере (проект PRINTS.DPR, рис.1 ) реализованы все три вышеописанных ситуации.

Рис.1: Демо-программа

Пример использования Delphi + ReportSmith


Завершенное приложение Delphi + ReportSmith есть в примерах к данному уроку. Приложение позволяет выбрать имя отчета в диалоге открытия файлов и выполнить этот отчет. Код для кнопки PrintReport (Печатать отчета) показан ниже. procedure TForm1.PrintReportClick(Sender: TObject); begin if OpenDialog1.Execute then begin Report1.ReportName := OpenDialog1.Filename; Report1.Run end end;



Пример OLE приложения


Среди демонстрационных примеров, входящих в Delphi есть два, относящихся к работе с OLE-объектами (в директориях X:\DELPHI\DEMOS\OLE2 и X:\DELPHI\DEMOS\DOC\OLE2). Более полным является второй, который, кроме всего прочего является примером построения MDI приложения. Данная программа демонстрирует все основные возможности TOLEContainer и позволяет: создавать новый OLE контейнер во время выполнения программы; инициализировать OLE объект либо в стандартном диалоге Windows "Insert Object", либо с помощью Clipboard, либо с помощью техники "перенести и бросить" (drag-and-drop); сохранить OLE объект в файле и восстановить его оттуда;

Рис.4: MDI OLE приложение.

На рис.4 показан пример MDI приложения, содержащий два дочерних окна с OLE объектами. Для создания нового OLE объекта нужно выбрать пункт меню File|New и далее Edit|Insert Object. Появится стандартный диалог Windows для инициализации OLE объекта (см. рис.1). Если приложение OLE-сервер имеет возможность сохранять информацию об OLE объекте в Clipboard, то проинициализировать объект можно с помощью пункта меню Edit|Paste Special.

Достаточно интересной является возможность применения техники drag-and-drop в применении к OLE объектам. Запустите MS Word (разместите его окно так, чтобы было видно и OLE приложение), наберите какой-нибудь текст, выделите его и с помощью мышки перетащите и бросьте на главное MDI окно приложения. Появится новое дочернее окно с OLE контейнером, содержащим этот текст. Программирование данной возможности достаточно сложно. Полное описание технологии построения данного OLE приложения есть в документации в коробке с Delphi (User's guide), этому посвящена отдельная глава.



Пример Редактора Компонент


В качестве примера давайте создадим Редактор Компонент для класса TButton. Этот Редактор будет показывать сообщение и изменять свойство Caption у объекта TButton. В данном примере это будет срабатывать и при двойном щелчке мыши, и через контекстное меню.

Декларация нового класса Редактора Компонент: TButtonEditor = class(TComponentEditor) private procedure HiThere; public procedure Edit; override; procedure ExecuteVerb(Index: Integer); override; function GetVerb(Index: Integer): string; override; function GetVerbCount: Integer; override; end;

Процедура HiThere и будет показывать сообщение и изменять свойство Caption: procedure TButtonEditor.HiThere; begin MessageDlg('Hi! It replaces Default Component Editor.', mtInformation, [mbOK], 0); (Component as TButton).Caption:='Hi!'; Designer.Modified; end;

Процедуры Edit и ExecuteVerb только вызывают HiThere: procedure TButtonEditor.Edit; begin HiThere; end; procedure TButtonEditor.ExecuteVerb(Index: Integer); begin if Index = 0 then HiThere; end;

Процедуры GetVerb и GetVerbCount определяют вид контекстного меню: function TButtonEditor.GetVerb(Index: Integer): string; begin result:='&Get message ...' end; function TButtonEditor.GetVerbCount: Integer; begin result:=1; end;

Здесь в контекстное меню добавляется один пункт "Get message :".

Редактор Компонент готов.

Необходимо зарегистрировать новый Редактор Компонент, это делается аналогично регистрации Редактора Свойств, только проще: procedure Register; begin RegisterComponentEditor(TButton, TButtonEditor); end;

После того, как Вы подключите новый Редактор Компонент в среду Delphi, а это делается в пункте меню "Options|Install Components", создайте новый проект, положите на форму объект TButton и щелкните дважды на нем - появится диалог:

После того, как Вы нажмете "OK", текст на кнопке изменится.

Созданный нами Редактор Компонент заместит Редактор по умолчанию для всех объектов класса TButton и его наследников, например, TBitBtn.

Полный текст Редактора Компонент приведен в файле SBEDIT.PAS в примерах к данному уроку.



Пример создания компонента


Для примера создадим новый класс, мутант TButton, в котором изменим значение по умолчанию свойства ShowHint на True и добавим новое свойство - счетчик нажатий на кнопку. Заготовка модуля для создания нового компонента уже есть (см. пункт ). Теперь исходный текст выглядит так: unit New_btn; interface uses SysUtils, WinTypes, WinProcs, Messages, Classes, Graphics, Controls, Forms, Dialogs, StdCtrls; type TMyButton = class(TButton) private { Private declarations } FClickCount : Longint; protected { Protected declarations } public { Public declarations } constructor Create(AOwner : TComponent); override; procedure Click; override; property ClickCount : Longint read FClickCount write FClickCount; published { Published declarations } end; procedure Register; implementation constructor TMyButton.Create(AOwner : TComponent); begin inherited Create(AOwner); ShowHint:=True; FClickCount:=0; end; procedure TMyButton.Click; begin Inc(FClickCount); inherited Click; end; procedure Register; begin RegisterComponents('Samples', [TMyButton]); end; end.

Для того, чтобы переопределить начальное значение свойства при создании объекта, нужно переписать конструктор Create, в котором и присвоить этому свойству нужное значение (не забыв перед этим вызвать конструктор предка).

Новое свойство для подсчета нажатий на клавишу называется ClickCount. Его внутреннее поле для сохранения значения - FClickCount имеет тип Longint, емкости поля хватит надолго.



как во время выполнения программы


В первом примере (проект SHAPE.DPR, рис.1) показано, как во время выполнения программы можно изменять свойства объекта TShape. Изменение цвета объекта (событие OnChange для ColorGrid1): procedure TForm1.ColorGrid1Change(Sender: TObject); begin Shape1.Brush.Color:=ColorGrid1.ForeGroundColor; end;

Рис.A: Пример с TShape
Во втором примере (проект PIXELS.DPR, рис.2) показано, как осуществить доступ к отдельной точке на изображении (на канве). По нажатию кнопки "Fill" всем точкам изображения присваивается свой цвет: procedure TForm1.Button1Click(Sender: TObject); var i, j : Longint; begin Button1.Enabled:=False; with Canvas do for i:=1 to Width do begin Application.ProcessMessages; for j:=1 to Height do Pixels[i,j]:=i*j; end; Button1.Enabled:=True; end;

Рис.B: Работа с точками на канве.
В третьей программе (проект DRAW.DPR, рис.3) приведен пример использования методов, выводящих изображение - Draw и StretchDraw:

Рис.C: Вывод изображений на канву.
Прорисовка изображений происходит в обработчике события OnPaint для формы: procedure TForm1.FormPaint(Sender: TObject); begin with Canvas do begin Draw(0,0, Image1.Picture.BitMap); StretchDraw(Rect(250,0,350,50),Image1.Picture.BitMap) end; end;

Примеры обработки исключительных ситуаций


Ниже приведены процедуры A,B и C, обсуждавшиеся ранее, воплощенные в новом синтаксисе Object Pascal: type ESampleError = class(Exception); var ErrorCondition: Boolean; procedure C; begin writeln('Enter C'); if (ErrorCondition) then begin writeln('Raising exception in C'); raise ESampleError.Create('Error!'); end; writeln('Exit C'); end; procedure B; begin writeln('enter B'); C; writeln('exit B'); end; procedure A; begin writeln('Enter A'); try writeln('Enter A''s try block'); B; writeln('After B call'); except on ESampleError do writeln('Inside A''s ESampleError handler'); on ESomethingElse do writeln('Inside A''s ESomethingElse handler'); end; writeln('Exit A'); end; begin writeln('begin main'); ErrorCondition := True; A; writeln('end main'); end.

При ErrorCondition = True программа выдаст: begin main Enter A Enter A's try block enter B Enter C Raising exception in C Inside A's ESampleError handler Exit A end main

Возможно вас удивила декларация типа 'ESampleError =class' вместо '=object'; это еще одно новое расширение языка. Delphi вводит новую модель объектов, доступную через декларацию типа '=class'. Описание новой объектной модели дается в других уроках. Здесь же достаточно сказать, что исключительные ситуации (exceptions) являются классами, частью новой объектной модели.

Процедура C проверяет наличие ошибки (в нашем случае это значение глобальной переменной) и, если она есть (а это так), C вызывает(raise) исключительную ситуацию класса ESampleError.

Процедура A помещает часть кода в блок try..except. Первая часть этого блока содержит часть кода, аналогично конструкции begin..end. Эта часть кода завершается ключевым словом except, далее следует один или более обработчиков исключительных ситуаций on xxxx do yyyy, далее может быть включен необязательный блок else, вся конструкция заканчивается end;. В конструкции, назначающей определенную обработку для конкретной исключительной ситуации (on xxxx do yyyy), после резервного слова on указывается класс исключительной ситуации, а после do следует собственно код обработки данной ошибки. Если возникшая исключительная ситуация подходит по типу к указанному после on, то выполнение программы переходит сюда (на код после do). Исключительная ситуация подходит в том случае, если она того же класса, что указан в on, либо является его потомком. Например, в случае on EFileNotFound обрабатываться будет ситуация, когда файл не найден. А в случае on EFileIO - все ошибки при работе с файлами, в том числе и предыдущая ситуация. В блоке else обрабатываются все ошибки, не обработанные до этого.

Приведенные в примере процедуры содержат код (строка с writeln), который отображает путь выполнения программы. Когда C вызывает exception, программа сразу переходит на обработчик ошибок в процедуре A, игнорируя оставшуюся часть кода в процедурах B и C.

После того, как найден подходящий обработчик ошибки, поиск оканчивается. После выполнения кода обработчика, программа продолжает выполняться с оператора, стоящего после слова end блока try..except (в примере - writeln('Exit A')).

Конструкция try..except подходит, если известно, какой тип ошибок нужно обрабатывать в конкретной ситуации. Но что делать, если требуется выполнить некоторые действия в любом случае, произошла ошибка или нет? Это тот случай, когда понадобится конструкция try..finally.

Рассмотрим модифицированную процедуру B: procedure NewB; var P: Pointer; begin writeln('enter B'); GetMem(P, 1000); C; FreeMem(P, 1000); writeln('exit B'); end;

Если C вызывает исключительную ситуацию, то программа уже не возвращается в процедуру B. А что же с теми 1000 байтами памяти, захваченными в B? Строка FreeMem(P,1000) не выполнится и Вы потеряете кусок памяти. Как это исправить? Нужно ненавязчиво включить процедуру B в процесс, например: procedure NewB; var P: Pointer; begin writeln('enter NewB'); GetMem(P, 1000); try writeln('enter NewB''s try block'); C; writeln('end of NewB''s try block'); finally writeln('inside NewB''s finally block'); FreeMem(P, 1000); end; writeln('exit NewB'); end;

Если в A поместить вызов NewB вместо B, то программа выведет сообщения следующим образом: begin main Enter A Enter A's try block enter NewB enter NewB's try block Enter C Raising exception in C inside NewB's finally block Inside A's ESampleError handler Exit A end main

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

Почему вызов GetMem не помещен внутрь блока try? Этот вызов может окончиться неудачно и вызвать exception EOutOfMemory. Если это произошло, то FreeMem попытается освободить память, которая не была распределена. Когда мы размещаем GetMem вне защищаемого участка, то предполагаем, что B сможет получить нужное количество памяти, а если нет, то более верхняя процедура получит уведомление EOutOfMemory.

А что, если требуется в B распределить 4 области памяти по схеме все-или-ничего? Если первые две попытки удались, а третья провалилась, то как освободить захваченную область память? Можно так: procedure NewB; var p,q,r,s: Pointer; begin writeln('enter B'); P := nil; Q := nil; R := nil; S := nil; try writeln('enter B''s try block'); GetMem(P, 1000); GetMem(Q, 1000); GetMem(R, 1000); GetMem(S, 1000); C; writeln('end of B''s try block'); finally writeln('inside B''s finally block'); if P <> nil then FreeMem(P, 1000); if Q <> nil then FreeMem(Q, 1000); if R <> nil then FreeMem(R, 1000); if S <> nil then FreeMem(S, 1000); end; writeln('exit B'); end;

Установив сперва указатели в NIL, далее можно определить, успешно ли прошел вызов GetMem.

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



Приведение к первой нормальной форме


Когда поле в данной записи содержит более одного значения для каждого вхождения первичного ключа, такие группы данных называются повторяющимися группами. 1НФ не допускает наличия таких многозначных полей. Рассмотрим пример базы данных предприятия, содержащей таблицу ОТДЕЛ со следующими значениями (атрибут, выделенный курсивом, является первичным ключом):

Табл. A: ОТДЕЛ

Номер_отделаНазваниеРуководительБюджетРасположение
100продаж0001000000Москва
100продаж0001000000Зеленоград
600разработок1201100000Тверь
100продаж0001000000Калуга

Для приведения этой таблицы к 1НФ мы должны устранить атрибут (поле) Расположение из таблицы ОТДЕЛ и создать новую таблицу РАСПОЛОЖЕНИЕ_ОТДЕЛОВ, в которой определить первичный ключ, являющийся комбинацией номера отдела и его расположения (Номер_отдела+Расположение - см. табл. b). Теперь для каждого расположения отдела существуют различные строки; тем самым мы устранили повторяющиеся группы.

Табл. B: РАСПОЛОЖЕНИЕ_ОТДЕЛОВ

Номер_отделаРасположение
100Москва
100Зеленоград
600Тверь
100Калуга



Приведение к третьей нормальной форме


Третий этап процесса приведения таблиц к нормальной форме состоит в удалении всех неключевых атрибутов, которые зависят от других неключевых атрибутов. Каждый неключевой атрибут должен быть логически связан с атрибутом (атрибутами), являющимся первичным ключом. Предположим, например, что мы добавили поля Номер_руководителя и Телефон в таблицу ПРОЕКТ, находящуюся в 2НФ (первичным ключом является поле ИД_проекта). Атрибут Телефон логически связан с атрибутом Номер_руководителя, неключевым полем, но не с атрибутом ИД_проекта, являющимся первичным ключом (табл. d).

Табл. D: ПРОЕКТ

ИД_проектаНомер_ руководителяТелефонНазв_ проектаОписание_ проектаПродукт
БРЖ022-21Биржа <blob> программа
ДОК122-43Документы <blob> программа
УПР082-56Управление <blob> адм.меры

Для нормализации этой таблицы (приведения ее в 3НФ) удалим атрибут Телефон, изменим Номер_руководителя на Руководитель и сделаем атрибут Руководитель внешним ключом, ссылающимся на атрибут Номер_работника (первичный ключ) в таблице РАБОТНИКИ. После этого таблицы ПРОЕКТ и РАБОТНИКИ будут выглядеть следующим образом:

Табл. E: ПРОЕКТ

ИД_проектаРуководительНазв_ проектаОписание_ проектаПродукт
БРЖ02Биржа <blob> программа
ДОК12Документы <blob> программа
УПР08Управление <blob> адм.меры

Табл. F: РАБОТНИКИ

Номер_ работникаФамилияИмяОтчествоНомер_ отделаКод_ професТелефонЗарплата
04ИвановИванИванович100инж2-69500
08ПетровПетрПетрович200мндж2-561000
23СидоровИванПетрович200мндж2-45800

Теперь, когда мы научились проводить нормализацию таблиц с целью устранения избыточного дублирования данных и группирования информации в логически связанных единицах, необходимо сделать ряд замечаний по вопросам проектирования баз данных. Необходимо четко понимать, что разбиение информации на более мелкие единицы с одной стороны, способствует повышению надежности и непротиворечивости базы данных, а с другой стороны, снижает ее производительность, так как требуются дополнительные затраты процессорного времени (серверного или машины пользователя) на обратное "соединение" таблиц при представлении информации на экране. Иногда для достижения требуемой производительности нужно сделать отход от канонической нормализации, при этом ясно осознавая, что необходимо обеспечить меры по предотвращению противоречивости в данных. Поэтому всякое решение о необходимости того или иного действия по нормализации можно принимать только тщательно проанализировав предметную область и класс поставленной задачи. Может потребоваться несколько итераций для достижения состояния, которое будет желаемым компромиссом между четкостью представления и реальными возможностями техники. Здесь уже начинается искусство...

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



Приведение ко второй нормальной форме


Следующий важный шаг в процессе нормализации состоит в удалении всех неключевых атрибутов, которые зависят только от части первичного ключа. Такие атрибуты называются частично зависимыми. Неключевые атрибуты заключают в себе информацию о данной сущности предметной области, но не идентифицируют ее уникальным образом. Например, предположим, что мы хотим распределить работников по проектам, ведущимся на предприятии. Для этого создадим таблицу ПРОЕКТ с составным первичным ключом, включающим номер работника и идентификатор проекта (Номер_работника+ИД_проекта, в табл. c выделены курсивом).

Табл. C: : ПРОЕКТ

Номер_ работникаИД_проектаФамилияНазв_проектаОписание_ проектаПродукт
28БРЖИвановБиржа <blob> программа
17ДОКПетровДокументы <blob> программа
06УПРСидоровУправление <blob> адм.меры

В этой таблице возникает следующая проблема. Атрибуты Назв_проекта, Описание_проекта и Продукт относятся к проекту как сущности и, следовательно, зависят от атрибута ИД_проекта (являющегося частью первичного ключа), но не от атрибута Номер_работника. Следовательно, они являются частично зависимыми от составного первичного ключа. То же самое можно сказать и об атрибуте Фамилия, который зависит от атрибута Номер_работника, но не зависит от атрибута ИД_проекта.

Для нормализации этой таблицы (приведения ее в 2НФ) удалим из нее атрибуты Номер_работника и Фамилия и создадим другую таблицу (назовем ее РАБОТНИК_В_ПРОЕКТЕ), которая будет содержать только эти два атрибута, и они же будут составлять ее первичный ключ.



Проект Delphi


Любой проект имеет, по-крайней мере, шесть файлов, связанных с ним. Три из них относятся к управлению проектом из среды и напрямую программистом не меняются. Вот эти файлы : Главный файл проекта, изначально называется PROJECT1.DPR. Первый модуль программы /unit/, который автоматически появляется в начале работы. Файл называется UNIT1.PAS по умолчанию, но его можно назвать любым другим именем, вроде MAIN.PAS. Файл главной формы, который по умолчанию называется UNIT1.DFM, используется для сохранения информации о внешнем виде главной формы. Файл PROJECT1.RES содержит иконку для проекта, создается автоматически. Файл, который называется PROJECT1.OPT по умолчанию, является текстовым файлом для сохранения установок, связанных с данным проектом. Например, установленные Вами директивы компилятора сохраняются здесь. Файл PROJECT1.DSK содержит информацию о состоянии рабочего пространства.

Разумеется, если сохранить проект под другим именем, то изменят название и файлы с расширением RES, OPT и DSK.

После компиляции программы получаются файлы с расширениями:

DCU - скомпилированные модули
EXE - исполняемый файл
DSM - служебный файл для запуска программы в среде, очень большой, рекомендуется стирать его при окончании работы.
~PA, ~DP - backup файлы Редактора.



в освоении. Однако при изменении


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

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



Рис. 8: Программа SHAPDEM2 имеет возможность реагировать на изменение пользователем размера окна

Листинг B: Программа SHAPDEM2 включает метод FormOnResize. Представлен главный модуль. unit Main; interface uses WinTypes, WinProcs, Classes, Graphics, Forms, Controls, ColorDlg, StdCtrls, Menus, Dialogs, ExtCtrls; type TForm1 = class(TForm) Shape1: TShape; ColorDialog1: TColorDialog; ScrollBar1: TScrollBar; ScrollBar2: TScrollBar; MainMenu1: TMainMenu; Shapes1: TMenuItem; ShapeColor1: TMenuItem; FormColor1: TMenuItem; Shapes2: TMenuItem; Rectangle1: TMenuItem; Square1: TMenuItem; RoundRect1: TMenuItem; RoundSquare1: TMenuItem; Ellipes1: TMenuItem; Circle1: TMenuItem; Exit1: TMenuItem; procedure NewShapeClick(Sender: TObject); procedure ShapeColorClick(Sender: TObject); procedure FormColorClick(Sender: TObject); procedure ScrollBar2Change(Sender: TObject); procedure ScrollBar1Change(Sender: TObject); procedure FormResize(Sender: TObject); procedure Exit1Click(Sender: TObject); private { Private declarations } public { Public declarations } end; var Form1: TForm1; implementation {$R *.DFM} procedure TForm1.NewShapeClick(Sender: TObject); begin Shape1.Shape := TShapeType((Sender as TMenuItem).Tag); end; procedure TForm1.ShapeColorClick(Sender: TObject); begin if ColorDialog1.Execute then Shape1.Brush.Color := ColorDialog1.Color; end; procedure TForm1.FormColorClick(Sender: TObject); begin if ColorDialog1.Execute then Form1.Color := ColorDialog1.Color; end; procedure TForm1.ScrollBar2Change(Sender: TObject); begin Shape1.Height := ScrollBar2.Position; end; procedure TForm1.ScrollBar1Change(Sender: TObject); begin Shape1.Width := ScrollBar1.Position; end; procedure TForm1.FormResize(Sender: TObject); var Menu, Caption, Frame: Integer; begin Caption := GetSystemMetrics(sm_cyCaption); Frame := GetSystemMetrics(sm_cxFrame) * 2; Menu := GetSystemMetrics(sm_cyMenu); Scrollbar1.Max := Width; Scrollbar2.Max := Height; Scrollbar2.Left := Width - Frame - Scrollbar2.Width; Scrollbar1.Top := Height - ScrollBar2.Width - Frame - Caption - Menu; Scrollbar1.Width := Width - Scrollbar2.Width - Frame; Scrollbar2.Height := Height - Frame - Caption - Menu - Scrollbar1.Height; end; procedure TForm1.Exit1Click(Sender: TObject); begin Close; end; end.

Главное меню для программы создается с помощью компонента MainMenu (находится на страничке "Standard" палитры компонентов). Поместив его на форму, дважды щелкните по нему мышкой - откроется редактор меню, в котором Вы сможете ввести нужные Вам названия пунктов меню и, при желании, изменить их имена (задаваемые Delphi по умолчанию) для удобочитаемости. Создадим меню программы SHAPEDEM2 с тремя главными пунктами: "Цвета", "Фигуры", "Выход".

Для первого пункта создадим следующие подпункты: Цвет фигуры Цвет окна

Для второго: Прямоугольник Квадрат Закругленный прямоугольник Закругленный квадрат Эллипс Окружность

Третий пункт меню не будет содержать никаких подпунктов.

После создания всех пунктов и подпунктов меню для работы программы SHAPEDEM2 нужно назначить номера для каждого из подпунктов меню, связанных с типом фигуры. Для этого воспользуемся свойством Tag, имеющимся у каждого пункта меню. Свойство Tag (типа Integer) специально введено в каждый компонент Delphi с тем, чтобы программисты могли использовать его по своему усмотрению. Назначим 0 свойству Tag пункта "Прямоугольник", 1 - пункту "Квадрат", 2 - пункту "Закругленный прямоугольник" и т.д. Цель такого назначения будет объяснена позднее.

Два метода, созданные для подпунктов изменения цвета аналогичны тем, которые были в программе SHAPEDEM: procedure TForm1.ShapeColorClick(Sender: TObject); begin if ColorDialog1.Execute then Shape1.Brush.Color := ColorDialog1.Color; end; procedure TForm1.FormColorClick(Sender: TObject); begin if ColorDialog1.Execute then Form1.Color := ColorDialog1.Color; end;

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

Аналогично, методы, реализующие реакцию на выбор подпунктов меню изменения вида фигуры также очень похожи на методы выбора фигуры через выпадающий список: procedure TForm1.NewShapeClick(Sender: TObject); begin Shape1.Shape := TShapeType((Sender as TMenuItem).Tag); end;

Этот код "работает" правильно благодаря тому, что перечислимый тип TShapeType в качестве начального имеет значение 0 и в свойство Tag подпунктов меню мы также записали порядковые номера, начинающиеся с нуля.

Отметим, что мы использовали оператор as, который позволяет надежно преобразовывать типы из одного в другой: в частности, преобразовать параметр Sender (имеющий общий тип TObject) в тип TMenuItem. Как правило, параметр Sender в Delphi - это управляющий элемент, посылающий сообщения функции, в которой он фигурирует. В данном случае, Sender является пунктом меню, и, следовательно, Вы можете работать с этим параметром как если бы он был декларирован с типом TMenuItem.

Главная причина использования оператора as состоит в том, что он обеспечивает очень ясный синтаксис, даже если Вы проводите сложное двухуровневое преобразование типов. Более того, оператор as обеспечивает проверку преобразования в режиме выполнения программы. Когда Вы используете оператор as, Вы можете быть уверены в том, что преобразование Sender в TMenuItem реально будет произведено лишь в том случае, если Sender действительно имеет тип TMenuItem.

Две полосы прокрутки в программе SHAPEDEM2 всегда будут располагаться возле границ окна, независимо от его размеров. Выполнение этих действий требует от Вас написания несколько более сложного программирования, чем было ранее. Как было указано ранее, Delphi, хотя и скрывает от программиста детали Windows-программирования, однако не запрещает обращаться к функциям Windows API (прикладного пользовательского интерфейса). Таким образом, Delphi поддерживает низкоуровневое программирование на уровне Windows API. Короче говоря, если Вам нужно углубиться в дебри низкоуровневого программирования - пожалуйста! procedure TForm1.FormResize(Sender: TObject); var Menu, Caption, Frame: Integer; begin Caption := GetSystemMetrics(sm_cyCaption); Frame := GetSystemMetrics(sm_cxFrame) * 2; Menu := GetSystemMetrics(sm_cyMenu); Scrollbar1.Max := Width; Scrollbar2.Max := Height; Scrollbar2.Left := Width - Frame - Scrollbar2.Width; Scrollbar2.Height := Height - Frame - Caption - Menu; Scrollbar1.Top := Height - Scrollbar2.Width - Frame - Caption - Menu; Scrollbar1.Width := Width - Scrollbar2.Width - Frame; end;

Код, показанный здесь, является реакцией на событие OnResize. Это событие перечислено среди других на страничке "Events" Инспектора Объектов в состоянии, когда выбрана форма (окно). Как Вы можете ожидать, событие (сообщение) OnResize посылается форме (окну) каждый раз, когда пользователь "захватывает" мышкой за какой-либо край окна и делает размер окна большим или меньшим. Однако, это же сообщение (событие) посылается окну и тогда, когда происходит максимизация окна (но не минимизация).

Первое, что делается в данном методе - запрашиваются системные параметры, определяющие размеры заголовка окна, огибающей его рамки и меню. Эта информация "добывается" путем вызова функции GetSystemMetrics, являющейся частью Windows API. Функции GetSystemMetrics передается один аргумент в виде константы, определяющей вид запрашиваемой информации. Например, если Вы передадите функции константу sm_cyCaption, получите в качестве результата высоту заголовка окна (в пикселах). Полный список этих констант имеется в on-line справочнике Delphi (Help|Windows API|Alphabetical functions|User functions|GetSystemMetrics), здесь же мы приведем небольшую выдержку из справочника:
SM_CXBORDERШирина огибающей окно рамки, размер которой не может быть изменен.
SM_CYBORDERВысота огибающей окно рамки, размер которой не может быть изменен.
SM_CYCAPTIONВысота заголовка окна, включая высоту огибающей окно рамки, размер которой не может быть изменен (SM_CYBORDER).
SM_CXCURSORШирина курсора.
SM_CYCURSORВысота курсора.
SM_CXFRAMEШирина огибающей окно рамки, размер которой может быть изменен.
SM_CYFRAMEВысота огибающей окно рамки, размер которой может быть изменен.
SM_CXFULLSCREENШирина клиентской части для полноэкранного окна.
SM_CYFULLSCREENВысота клиентской части для полноэкранного окна (эквивалентна высоте экрана за вычетом высоты заголовка окна).
SM_CXICONШирина иконки.
SM_CYICONВысота иконки.
SM_CYMENUВысота полосы меню в одну строку. Это высота меню за вычетом высоты огибающей окно рамки, размер которой не может быть изменен (SM_CYBORDER).
SM_CXMINМинимальная ширина окна.
SM_CYMINМинимальная высота окна.
vSM_CXSCREENШирина экрана.
SM_CYSCREENВысота экрана.
SM_MOUSEPRESENTНе 0, если мышь установлена.
В методе FormResize программа вычисляет новые размеры полос прокрутки: Scrollbar1.Max := Width; Scrollbar2.Max := Height; Scrollbar2.Left := Width - Frame - Scrollbar2.Width; Scrollbar2.Height := Height - Frame - Caption - Menu; Scrollbar1.Top := Height - Scrollbar2.Width - Frame - Caption - Menu; Scrollbar1.Width := Width - Scrollbar2.Width - Frame;

Вычисления, приведенные здесь, включают простые математические действия. Например, левая сторона вертикальной полосы прокрутки должна быть равна ширине всего окна (формы) за вычетом ширины рамки и ширины самой полосы прокрутки. Это элементарная логика, и реализовав ее в программе, мы получим вертикальную полосу прокрутки, всегда располагающуюся возле правого края окна (формы).

В программе SHAPEDEM свойство Max каждой полосы прокрутки оставалось равным значению по умолчанию - 100; это означало, что после того как бегунок полосы прокрутки пройдет все доступное расстояние (как для вертикальной, так и для горизонтальной полосы прокрутки), свойство Position будет установлено в 100. Если бегунок возвращался к началу, свойство Position устанавливалось равным свойству Min, которое, по умолчанию, 0.

В программе SHAPEDEM2 Вы можете изменять значения свойств Min и Max так, чтобы диапазон значений Position полос прокрутки отражал текущий размер окна (формы), даже при изменении формой своего размера в режиме выполнения. Здесь приведены соответствующие строки из метода FormResize. procedure TForm1.FormResize(Sender: TObject); begin ... Scrollbar1.Max := Width; Scrollbar2.Max := Height; ... end;

Две строчки кода, показанные выше, просто устанавливают максимальные значения полос прокрутки равными ширине и высоте формы соответственно. После этого Вы всегда сможете сделать помещенную на форму фигуру такой же "большой", как и сама форма. После введения таких изменений Вам больше не потребуется умножать свойство Position на какой-либо множитель. procedure TForm1.Scrollbar2Change (Sender: TObject); begin Shape1.Height := Scrollbar2.Position; end;

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

В конце хотелось бы сделать одно маленькое замечание. Как Вы, возможно, уже заметили, каждая форма, по умолчанию, имеет две полосы прокрутки (HorzScrollbar и VertScrollbar), которые появляются автоматически всякий раз, когда размер формы становится меньше, чем область, занимаемая управляющими элементами, расположенными на этой форме. Иногда эти полосы прокрутки могут быть очень полезными, но в нашей ситуации они сделают совсем не то, что хотелось бы. Поэтому, для надежности, Вы можете установить их вложенные свойства Visible в False.


Простейшие конструкции команды SELECT


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

Список выбираемых элементов может содержать следующее: имена полей * вычисления литералы функции агрегирующие конструкции Список полей SELECT first_name, last_name, phone_no FROM phone_list

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

FIRST_NAMELAST_NAMEPHONE_NO
TerriLee(408) 555-1234
Oliver H.Bender(408) 555-1234
Mary S.MacDonald(415) 555-1234
MichaelYanowski(415) 555-1234
RobertNelson(408) 555-1234
KellyBrown(408) 555-1234
StewartHall(408) 555-1234
...  

Отметим, что PHONE_LIST - это виртуальная таблица (представление), созданная в InterBase и основанная на информации из двух таблиц - EMPLOYEE и DEPARTMENT. Она не показана на рис.1, однако, как мы уже указывали в общей структуре команды SELECT, к ней можно обращаться так же, как и к "настоящей" таблице.

Все поля SELECT * FROM phone_list

получить список служебных телефонов всех работников предприятия со всей необходимой информацией

EMP_NOFIRST_NAMELAST_NAMEPHONE_EXTLOCATIONPHONE_NO
12TerriLee256Monterey(408) 555-1234
105Oliver H.Bender255Monterey(408) 555-1234
85Mary S.MacDonald477San Francisco(415) 555-1234
127MichaelYanowski492San Francisco(415) 555-1234
2RobertNelson250Monterey(408) 555-1234
109KellyBrown202Monterey(408) 555-1234
14StewartHall227Monterey(408) 555-1234
...      

Все поля в произвольном порядке SELECT first_name, last_name, phone_no, location, phone_ext, emp_no FROM phone_list

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

FIRST_NAME LAST_NAME PHONE_NOLOCATIONPHONE_EXT EMP_NO
TerriLee(408) 555-1234 Monterey25612
Oliver H.Bender(408) 555-1234 Monterey255105
Mary S.MacDonald(415) 555-1234 San Francisco 47785
MichaelYanowski(415) 555-1234 San Francisco 492127
RobertNelson(408) 555-1234 Monterey2502
KellyBrown(408) 555-1234 Monterey202109
StewartHall(408) 555-1234 Monterey22714
...      

Блобы

Получение информации о BLOb выглядит совершенно аналогично обычным полям. Полученные значения можно отображать с использованием data-aware компонент Delphi, например, TDBMemo или TDBGrid. Однако, в последнем случае придется самому прорисовывать содержимое блоба (например, через OnDrawDataCell). Подробнее об этом см. на уроке, посвященном работе с полями. SELECT job_requirement FROM job

получить список должностных требований к кандидатам на работу JOB_REQUIREMENT: No specific requirements. JOB_REQUIREMENT: 15+ years in finance or 5+ years as a CFO with a proven track record. MBA or J.D. degree. ...

Вычисления

SELECT emp_no, salary, salary * 1.15 FROM employee

получить список номеров служащих и их зарплату, в том числе увеличенную на 15%

EMP_NOSALARY
2105900.00121785
4 97500.00112125
5102750.00118162.5
8 64635.0074330.25
9 75060.0086319
11 86292.9499236.87812499999
12 53793.0061861.95
14 69482.6279905.01874999999
...   

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

Например, в выражении col1 + col2 * col3 сначала находится произведение значений столбцов col2 и col3, а затем результат этого умножения складывается со значением столбца col1. А в выражении (col1 + col2) * col3 сначала выполняется сложение значений столбцов col1 и col2, и только после этого результат умножается на значение столбца col3.

Литералы

Для придания большей наглядности получаемому результату можно использовать литералы. Литералы - это строковые константы, которые применяются наряду с наименованиями столбцов и, таким образом, выступают в роли "псевдостолбцов". Строка символов, представляющая собой литерал, должна быть заключена в одинарные или двойные скобки. SELECT first_name, "получает", salary, "долларов в год" FROM employee

получить список сотрудников и их зарплату

FIRST_NAME SALARY
Robertполучает105900.00 долларов в год
Bruce получает97500.00 долларов в год
Kimполучает102750.00 долларов в год
Leslieполучает64635.00 долларов в год
Philполучает75060.00 долларов в год
K. J.получает86292.94 долларов в год
Terriполучает53793.00 долларов в год

Конкатенация

Имеется возможность соединять два или более столбца, имеющие строковый тип, друг с другом, а также соединять их с литералами. Для этого используется операция конкатенации (). SELECT "сотрудник " first_name " " last_name FROM employee

получить список всех сотрудников ============================================== сотрудник   Robert Nelson сотрудник Bruce Young сотрудник Kim Lambert сотрудник Leslie Johnson сотрудник Phil Forest сотрудник K. J. Weston сотрудник Terri Lee сотрудник Stewart Hall ... Использование квалификатора AS

Для придания наглядности получаемым результатам наряду с литералами в списке выбираемых элементов можно использовать квалификатор AS. Данный квалификатор заменяет в результирующей таблице существующее название столбца на заданное. Это наиболее эффективный и простой способ создания заголовков (к сожалению, InterBase, как уже отмечалось, не поддерживает использование русских букв в наименовании столбцов). SELECT count(*) AS number FROM employee

подсчитать количество служащих NUMBER =========== 42 SELECT "сотрудник " first_name " " last_name AS employee_list FROM employee

получить список всех сотрудников EMPLOYEE_LIST ============================================== сотрудник Robert Nelson сотрудник Bruce Young сотрудник Kim Lambert сотрудник Leslie Johnson сотрудник Phil Forest сотрудник K. J. Weston сотрудник Terri Lee сотрудник Stewart Hall ... Работа с датами

Мы уже рассказывали о типах данных, имеющихся в различных СУБД, в том числе и в InterBase. В разных системах имеется различное число встроенных функций, упрощающих работу с датами, строками и другими типами данных. InterBase, к сожалению, обладает достаточно ограниченным набором таких функций. Однако, поскольку язык SQL, реализованный в InterBase, соответствует стандарту, то в нем имеются возможности конвертации дат в строки и гибкой работы с датами. Внутренне дата в InterBase содержит значения даты и времени. Внешне дата может быть представлена строками различных форматов, например: "October 27, 1995" "27-OCT-1994" "10-27-95" "10/27/95" "27.10.95"

Кроме абсолютных дат, в SQL-выражениях можно также пользоваться относительным заданием дат: "yesterday" - вчера "today" - сегодня "now" - сейчас (включая время) "tomorrow" - завтра

Дата может неявно конвертироваться в строку (из строки), если: строка, представляющая дату, имеет один из вышеперечисленных форматов; выражение не содержит неоднозначностей в толковании типов столбцов. SELECT first_name, last_name, hire_date FROM employee WHERE hire_date > '1-1-94'

получить список сотрудников, принятых на работу после 1 января 1994 года

FIRST_NAMELAST_NAME HIRE_DATE
PierreOsborne3-JAN-1994
JohnMontgomery30-MAR-1994
MarkGuckenheimer2-MAY-1994

Значения дат можно сравнивать друг с другом, сравнивать с относительными датами, вычитать одну дату из другой. SELECT first_name, last_name, hire_date FROM employee WHERE 'today' - hire_date > 365 * 7 + 1

получить список служащих, проработавших на предприятии к настоящему времени более 7 лет

FIRST_NAMELAST_NAMEHIRE_DATE
RobertNelson28-DEC-1988
BruceYoung28-DEC-1988

Агрегатные функции

К агрегирующим функциям относятся функции вычисления суммы (SUM), максимального (SUM) и минимального (MIN) значений столбцов, арифметического среднего (AVG), а также количества строк, удовлетворяющих заданному условию (COUNT). SELECT count(*), sum (budget), avg (budget), min (budget), max (budget) FROM department WHERE head_dept = 100

вычислить: количество отделов, являющихся подразделениями отдела 100 (Маркетинг и продажи), их суммарный, средний, мини- мальный и максимальный бюджеты

COUNTSUMAVGMINMAX
53800000.00760000.00500000.001500000.00

Предложение FROM команды SELECT

В предложении FROM перечисляются все объекты (один или несколько), из которых производится выборка данных (рис.2). Каждая таблица или представление, о которых упоминается в запросе, должны быть перечислены в предложении FROM.



Пункт меню "File"


Если нужно сохранить проект, то Вы выбираете пункт главного меню "File" (с помощью мышки или по клавише Alt+F). Пункт меню "File" выглядит следующим образом: New Project Open Project Save Project Save Project As Close Project --------------------- New Form New Unit New Component Open File Save File Save File As Close File --------------------- Add File Remove File --------------------- Print --------------------- Exit --------------------- 1 PREV1.DPR 2 PREV2.DPR

Как Вы можете видеть, здесь есть шесть секций; вот их назначение: Первая секция дает возможность управления проектом вцелом. Вторая секция дает контроль над формами, модулями и компонентами проекта. Третья позволяет добавлять и удалять файлы из проекта. Четвертая управляет печатью. Пятая секция - выход из Delphi Шестая секция предоставляет список ранее редактировавшихся проектов; Вы можете быстро открыть нужный.

Как Вы увидите позже, большинство операций из пункта меню "File" можно выполнить с помощью Менеджера Проекта (Project Manager), который можно вызвать из пункта меню View. Некоторые операции доступны и через SpeedBar. Данная стратегия типична для Delphi: она предоставляет несколько путей для решения одной и той же задачи, Вы сами можете решать, какой из них более эффективен в данной ситуации.

Каждая строка пункта меню "File" объяснена в Справочнике. Выберите меню "File" и нажмите F1, появится экран справочника, как на рис.1.

Рис.1: Delphi включает подсказку, как использовать пункт меню "File".

Большинство из пунктов первой секции очевидны. "New Project" начинает новый проект, "Open Project" открывает существующий проект и т.д.

Первые два пункта второй секции позволяют Вам создать новую форму или новый модуль. Выбирая "New Form", Вы создаете новую форму и модуль, связанный с ней; выбирая "New Unit", Вы создаете один модуль.

"New Component" вызывает диалог для построения заготовки нового визуального компонента. В результате создается модуль, который можно скомпилировать и включить в Палитру Компонент.

"Open File" открывает при необходимости любой модуль или просто текстовый файл. Если модуль описывает форму, то эта форма тоже появится на экране.

При создании нового модуля Delphi дает ему имя по-умолчанию. Вы можете изменить это имя на что-нибудь более осмысленное (например, MAIN.PAS) с помощью пункта "Save File As".

"Save File" сохраняет только редактируемый файл, но не весь проект.

"Close File" удаляет файл из окна Редактора.

Нужно обратить внимание: Вы должны регулярно сохранять проект через File | Save Project либо через нажатие Ctrl+S.



Пункт меню Options | Project


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

Project
Environment
Tools
Gallery
--
Open Library
Install Components
Rebuild Library

Первые четыре пункта вызывают диалоговые окна. Ниже приведено общее описание пункта меню "Options": Project - выбор установок, которые напрямую влияют на текущий проект, это могут быть, к примеру, директивы компилятора проверки стека (stack checking) или диапазона (range checking). Environment - конфигурация самой среды программирования (IDE). Например, здесь можно изменить цвета, используемые в Редакторе. Tools - позволяет добавить или удалить вызов внешних программ в пункт главного меню "Tools". Например, если Вы часто пользуетесь каким-нибудь редактором или отладчиком, то здесь его вызов можно добавить в меню. Gallery - позволяет определить специфические установки для Эксперта Форм и Эксперта Проектов и их "заготовок". Эксперты и "заготовки" предоставляют путь для ускорения конструирования интерфейса программы. Последние три пункта позволяют сконфигурировать Палитру Компонент.

Диалог из пункта Options | Project включает пять страниц: На странице Forms перечислены все формы, включенные в проект; Вы можете указать, нужно ли автоматически создавать форму при старте программы или Вы ее создадите сами. На странице Application Вы определяете элементы программы такие, как заголовок, файл помощи и иконка. Страница Compiler включает установки для генерации кода, управления обработкой ошибок времени выполнения, синтаксиса, отладки и др. На странице Linker можно определить условия для процесса линковки приложения Страница Directories/Conditionals - здесь указываются директории, специфичные для данного проекта.

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

Все установки для проекта сохраняются в текстовом файле с расширением OPT и Вы можете вручную их исправить.

Страница Forms

Рис 4.

На странице Forms можно выбрать главную форму проекта. Изменения, которые Вы сделаете, отобразятся в соответствующем файле DPR. Например, в нижеследующем проекте, Form1 является главной, поскольку появляется первой в главном блоке программы: program Project1; uses Forms, Unit1 in 'UNIT1.PAS' {Form1}, Unit2 in 'UNIT2.PAS' {Form2}; {$R *.RES} begin Application.CreateForm(TForm1, Form1); Application.CreateForm(TForm2, Form2); Application.Run; end. Если изменить код так, чтобы он читался begin Application.CreateForm(TForm2, Form2); Application.CreateForm(TForm1, Form1); Application.Run; end. то теперь Form2 станет главной формой проекта.

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

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

Страница Applications

На странице Applications, см. рис.5, вы можете задать заголовок (Title), файл помощи (Help file) и пиктограмму (Icon) для проекта.

Рис.5: Страница общих установок для приложения.

Страница Compiler

Ранее уже говорилось, что установки из пункта меню "Options | Project" сохраняются в соответствующем файле с расширением OPT. Давайте рассмотрим директивы компилятора на странице Compiler (рис.6).

Рис.6: Страница для определения директив компилятора.

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

OPT FileOptions PageEditor Symbol
FForce Far Calls{$F+}
AWord Align Date{$A+}
UPentium-Safe FDIV{$U+}
KSmart Callbacks{$K+}
WWindows (3.0) Stack {$W+}
RRange Checking{$R+}
SStack Checking{$S+}
IIO Checking{$I+}
QOverflow Checking{$Q+}
VStrict Var Strings{$V+}
BComplete Boolean Evaluation{$B+}
XExtended Syntax{$X+}
TTyped @ Operator{$T+}
POpen Parameters{$P+}
DDebug Information {$D+}

Страница Linker

Теперь давайте перейдем к странице Linker, показанной на рис.7.

Рис.7: Страница линковщика.

Установки отладчика рассматриваются ниже. Если буфер линковщика расположен в памяти, то линковка происходит быстрее.

Размер стека (Stack Size) и локальной динамической памяти (Heap Size) весьма важны. Delphi устанавливает по умолчанию и Stack Size, и Heap Size в 8192 байт каждый. Вам может понадобиться изменить размер стека в программе, но обычно это не более 32Кб. В сумме эти два размера не должны превышать 64Кб, иначе будет выдаваться ошибка при компиляции программы.

Страница Directories/Conditionals

Страница Directories/Conditionals, рис.8, дает возможность расширить число директорий, в которых компилятор и линковщик ищут DCU файлы.

Рис.8: Страница Directories/Conditionals.

В файле DELPHI.INI содержится еще один список директорий. Запомните, что в OPT файле - список директорий для конкретного проекта, а в файле DELPHI.INI - список относится к любому проекту.

Output directory - выходная директория, куда складываются EXE и DCU файлы, получающиеся при компиляции.

Search path - список директорий для поиска DCU файлов при линковке. Директории перечисляются через точку с запятой ;

Conditional defines - для опытного программиста и на первом этапе создания проекта не требуется. Для информации можно вызвать Справочник (on-line help).



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


Следующие методы позволяют Вам изменить данные, связанные с TTable: procedure Append; procedure Insert; procedure Cancel; procedure Delete; procedure Edit; procedure Post;

Все эти методы - часть TDataSet, они унаследованы и используются TTable и TQuery.

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

Имеется a типичная последовательность, которую Вы могли бы использовать при изменении поля текущей записи: Table1.Edit; Table1.FieldByName('CustName').AsString := 'Fred'; Table1.Post;

Первая строка переводит БД в режим редактирования. Следующая строка присваивает значение 'Fred' полю 'CustName'. Наконец, данные записываются на диск, когда Вы вызываете Post.

При использовании такого подхода, Вы всегда работаете с записями. Сам факт перемещения к следующей записи автоматически сохраняет ваши данные на диск. Например, следующий код будет иметь тот же самый эффект, что и код показанный выше, плюс этому будет перемещать Вас на следующую запись: Table1.Edit; Table1.FieldByName('CustNo').AsInteger := 1234; Table1.Next;

Общее правило, которому нужно следовать - всякий раз, когда Вы сдвигаетесь с текущей записи, введенные Вами данные будут записаны автоматически. Это означает, что вызовы First, Next, Prior и Last всегда выполняют Post, если Вы находились в режиме редактирования. Если Вы работаете с данными на сервере и транзакциями, тогда правила, приведенные здесь, не применяются. Однако, транзакции - это отдельный вопрос с их собственными специальными правилами, Вы увидите это, когда прочитаете о них в следующих уроках.

Тем не менее, даже если Вы не работаете со транзакциями, Вы можете все же отменить результаты вашего редактирования в любое время, до тех пор, пока не вызвали напрямую или косвенно метод Post. Например, если Вы перевели таблицу в режим редактирования, и изменили данные в одном или более полей, Вы можете всегда вернуть запись в исходное состояние вызовом метода Cancel.

Существуют два метода, названные Append и Insert, который Вы можете использовать всякий раз, когда Вы хотите добавить новую запись в DataSet. Очевидно имеет больше смысла использовать Append для DataSets которые не индексированы, но Delphi не будет генерировать exception если Вы используете Append на индексированной таблице. Фактически, всегда можно использовать и Append, и Insert.

Продемонстрируем работу методов на простом примере. Чтобы создать программу, используйте TTable, TDataSource и TdbGrid. Открыть таблицу COUNTRY. Затем разместите две кнопки на форме и назовите их 'Insert' и 'Delete'. Когда Вы все сделаете, то должна получиться программа, показанная на рис.5

Рис.5: Программа может вставлять и удалять запись из таблицы COUNTRY.

Следующим шагом Вы должен связать код с кнопками Insert и Delete: procedure TForm1.InsertClick(Sender: TObject); begin Table1.Insert; Table1.FieldByName('Name').AsString := 'Russia'; Table1.FieldByName('Capital').AsString := 'Moscow'; Table1.Post; end; procedure TForm1.DeleteClick(Sender: TObject); begin Table1.Delete; end;

Процедура показанная здесь сначала переводит таблицу в режим вставки (новая запись с незаполненными полями вставляется в текущую позицию dataset). После вставки пустой записи, следующим этапом нужно назначить значения одному или большему количеству полей. Существует, конечно, несколько различных путей присвоить эти значения. В нашей программе Вы могли бы просто ввести информацию в новую запись через DBGrid. Или Вы могли бы разместить на форме стандартную строку ввода (TEdit) и затем установить каждое поле равным значению, которое пользователь напечатал в этой строке: Table1.FieldByName('Name').AsString := Edit1.Text;

Можно было бы использовать компоненты, специально предназначенные для работы с данными в DataSet.

Назначение этой главы, однако, состоит в том, чтобы показать, как вводить данные из программы. Поэтому, в примере вводимая информация скомпилирована прямо в код программы: Table1.FieldByName('Name').AsString := 'Russia';

Один из интересных моментов в этом примере это то, что нажатие кнопки Insert дважды подряд автоматически вызывает exception 'Key Violation'. Чтобы исправить эту ситуацию, Вы должны либо удалить текущую запись, или изменять поля Name и Capital вновь созданной записи.

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

Если после вызова Insert, Вы решаете отказаться от вставки новой записи, то Вы можете вызвать Cancel. Если Вы сделаете это прежде, чем Вы вызовете Post, то все что Вы ввели после вызова Insert будет отменено, и dataset будет находиться в состоянии, которое было до вызова Insert.

Одно дополнительное свойство, которое Вы должны иметь в виду называется CanModify. Если CanModify возвращает False, то TTable находиться в состоянии ReadOnly. В противном случае CanModify возвращает True и Вы можете редактировать или добавлять записи в нее по желанию. CanModify - само по себе 'read only' свойство. Если Вы хотите установить DataSet в состояние только на чтение (Read Only), то Вы должны использовать свойство ReadOnly, не CanModify.



RAD Pack for Delphi


В этом обзоре стоит упомянуть еще один продукт, выпущенный компанией Borland для Delphi. В RAD Pack for Delphi входит набор полезных дополнений, которые помогут разработчику при освоении и использовании Delphi. Это учебник по объектному паскалю, интерактивный отладчик самой последней версии, Borland Visual Solutions Pack (набор VBX для реализации редакторов, электронных таблиц, коммуникационные VBX, VBX с деловой графикой и т.п.), Resource WorkShop для работы с ресурсами Borland Pascal 7.0, а также дельфийский эксперт для преобразования ресурсов BP 7.0 в формы Delphi.



Разработка приложений БД


Delphi позволяет использовать библиотеку визуальных компонент для быстрого создания надежных приложений, которые легко расширяются до приложений с архитектурой клиент-сервер. Другими словами, Вы можете создать приложение, работающее с локальным сервером InterBase, а затем использовать созданное приложение, соединяясь с удаленным SQL-сервером через SQL-Links.



Редактор DataSet


Редактор DataSet может быть вызван с помощью объектов TTable или TQuery. Чтобы начать работать с ним, положите объект TQuery на форму, установите псевдоним DBDEMOS, введите SQL запрос "select * from customer" и активизируйте его (установив св-во Active в True).

Откройте комбобокс "Object Selector" вверху Инспектора Объектов - в настоящее время там имеется два компонента: TForm и TQuery.

Нажмите правую кнопку мыши на объекте TQuery и в контекстном меню выберите пункт "Fields Editor". Нажмите кнопку Add - появиться диалог Add Fields, как показано на рис.1

Рис.1: Диалог Add Fields Редактора DataSet.

По-умолчанию, все поля в диалоге выбраны. Нажмите на кнопку OK, чтобы выбрать все поля, и закройте редактор. Снова загляните в "Object Selector", теперь здесь появилось несколько новых объектов, (см. рис.2)

Рис.2: Object Selector показывает в списке все объекты созданные в Редакторе DataSet. Вы можете также найти этот список в определении класса TForm1.

Эти новые объекты будут использоваться для визуального представления таблицы CUSTOMER пользователю.

Вот полный список объектов, которые только что созданы: Query1CustNo: TFloatField; Query1Company: TStringField; Query1Addr1: TStringField; Query1Addr2: TStringField; Query1City: TStringField; Query1State: TStringField; Query1Zip: TStringField; Query1Country: TStringField; Query1Phone: TStringField; Query1FAX: TStringField; Query1TaxRate: TFloatField; Query1Contact: TStringField; Query1LastInvoiceDate: TDateTimeField;

Я вырезал и вставил этот список из определения класса TForm1, которое можно найти в окне Редактора исходного текста. Происхождение имен показанных здесь, должно быть достаточно очевидно. Часть "Query1" берется по-умолчанию от имени объекта TQuery, а вторая половина от имени поля в таблице Customer. Если бы мы сейчас переименовали объект Query1 в Customer, то получили бы такие имена: CustomerCustNo CustomerCompany

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

Любой объект, созданный в редакторе DataSet является наследником класса TField. Точный тип потомка зависит от типа данных в конкретном поле. Например, поле CustNo имеет тип TFloatField, а поле Query1City имеет тип TStringField. Это два типа полей, которые Вы будете встречать наиболее часто. Другие типы включают тип TDateTimeField, который представлен полем Query1LastInvoiceDate, и TIntegerField, который не встречается в этой таблице.

Чтобы понять, что можно делать с потомками TField, откройте Browser, выключите просмотр полей Private и Protected, и просмотрите свойства и методы Public и Published соответствующих классов.

Наиболее важное свойство называется Value. Вы можете получить доступ к нему так: procedure TForm1.Button1Click(Sender: TObject); var d: Double; S: string; begin d := Query1CustNo.Value; S := Query1Company.Value; d:=d+1; S := 'Zoo'; Query1CustNo.Value := d; Query1Company.Value := S; end;

В коде, показанном здесь, сначала присваиваются значения переменным d и S. Следующие две строки изменяют эти значения, а последний две присваивают новые значения объектам. Не имеет большого смысла писать код, подобный этому, в программе, но этот код служит лишь для того, чтобы продемонстрировать синтаксис, используемый с потомками TField.

Свойство Value всегда соответствует типу поля, к которому оно относится. Например у TStringFields - string, TCurrencyFields - double. Однако, если вы отображаете поле типа TCurrencyField с помощью компонент, "чувствительных к данным" (data-aware: TDBEdit, TDBGrid etc.), то оно будет представлена строкой типа: "$5.00".

Это могло бы заставить вас думать, что у Delphi внезапно отключился строгий контроль типов. Ведь TCurrencyField.Value объявлена как Double, и если Вы пробуете присвоить ему строку, Вы получите ошибку "type mismatch" (несоответствие типа). Вышеупомянутый пример демонстрирует на самом деле свойства объектов визуализации данных, а не ослабление проверки типов. (Однако, есть возможность получить значение поля уже преобразованное к другому типу. Для этого у TField и его потомков имеется набор методов типа AsString или AsFloat. Конечно, преобразование происходит только тогда, когда имеет смысл.)

Если нужно получить имена полей в текущем DataSet, то для этого используется свойство FieldName одним из двух способов, показанных ниже: S := Query1.Fields[0].FieldName; S := Query1CustNo.FieldName;

Если вы хотите получить имя объекта, связанного с полем, то вы должны использовать свойство Name: S := Query1.Fields[0].Name; S := Query1CustNo.Name;

Для таблицы CUSTOMER, первый пример вернет строку "CustNo", а любая из строк второго примера строку "Query1CustNo".



Редактор Компонент


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

Давайте взглянем на класс TComponentEditor в модуле DSGNINTF.PAS: TComponentEditor = class private FComponent: TComponent; FDesigner: TFormDesigner; public constructor Create(AComponent: TComponent; ADesigner: TFormDesigner); virtual; procedure Edit; virtual; procedure ExecuteVerb(Index: Integer); virtual; function GetVerb(Index: Integer): string; virtual; function GetVerbCount: Integer; virtual; procedure Copy; virtual; property Component: TComponent read FComponent; property Designer: TFormDesigner read FDesigner; end;

Редактор Компонент создается для каждого выбранного объекта на форме основываясь на классе объекта. При двойном щелчке на объекте вызывается метод Edit Редактора Компонент. При вызове контекстного меню (popup menu) по правой кнопке мыши, то для построения этого меню вызываются методы GetVerbCount и GetVerb. Если в этом меню выбирается пункт, то вызывается метод ExecuteVerb. Copy вызывается при копировании компонента в Clipboard.

Редактор Компонент по умолчанию (TDefaultEditor) при двойном щелчке на объекте создает (или переходит на) в Редакторе Исходного Текста заготовку для событий OnCreate, OnChanged или OnClick (какое первым попадется).

При создании Редактора Компонент вы должны переопределить либо метод Edit, либо три следующих метода: GetVerb, GetVerbCount и ExecuteVerb. Можно переопределять все четыре метода.

Если Редактор Компонент был вызван и изменил компонент, то всегда обязательно нужно вызвать метод Designer.Modified, чтобы Дизайнер об этом узнал.

Методы и свойства TComponentEditor:

Create(AComponent, ADesigner): Конструктор Редактора Компонент. AComponent - редактируемый компонент. ADesigner - интерфейс к Дизайнеру среды Delphi.

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

ExecuteVerb(Index): Выполняется, когда был выбран пункт номер Index из контекстного меню. Считается, что Редактор Компонент знает, как проинтерпретировать это значение.

GetVerb(Index): Редактор Компонент должен вернуть в этом методе строку, которая будет показана в виде пункта контекстного меню. Можно использовать обычные для пунктов меню символы, например &.

GetVerbCount: Возвращает число, которое определяет на какие значения будут отвечать методы GetVerb и ExecuteVerb. Например, если это число равно 3, то в меню будет добавлено три пункта со значениями Index от 0 до 2.

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



Редакторы свойств


Как Вы знаете, во время дизайна для настройки внешнего вида и поведения объекта нужно пользоваться Инспектором Объектов. Например, можно изменить цвет фона у объекта TLabel на форме.

Перейдем в окно Инспектора Объектов и выберем свойство Color - отметьте, что справа есть маленькая стрелка, она означает, что мы можем выбрать цвет из списка. Нажмите мышкой на эту стрелку (рис.1)

Рис.1 : Выбор цвета из списка

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



Регистрация Редактора Свойств


Новый Редактор Свойств готов, осталось только его зарегистрировать в среде Delphi. Для этого в интерфейсной части модуля с нашим редактором требуется поместить декларацию процедуры Register, а в части implementation написать следующее: procedure Register; begin RegisterPropertyEditor(TypeInfo(String), TControl, 'Hint', THintProperty); end;

Как уже сообщалось выше, один и тот же редактор свойств можно "привязать" к свойствам, в зависимости от их названия или типа объекта. Это определяется параметрами (второй и третий), которые передаются во время регистрации в процедуре RegisterPropertyEditor. Возможны четыре варианта:

Класс компонентыИмя свойстваДля каких свойств
Nil''совпадает тип свойства
Nil'Name'Тип свойства + Имя свойства
TClass''Тип свойства + класс компоненты
TClass'Name'Тип свойства + Имя свойства+ класс компоненты

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

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

Если указан только класс, то редактор относится ко всем свойствам указанного типа для объектов указанного класса.

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

В нашем случае Редактор Свойств зарегистрирован для всех свойств, которые имеют тип String, относятся к компоненте класса TControl или наследника от него и имеют имя 'Hint'.



Реляционные операции. Команды языка манипулирования данными


Наиболее важной командой языка манипулирования данными является команда SELECT. За кажущейся простотой ее синтаксиса скрывается огромное богатство возможностей. Нам важно научиться использовать это богатство!

На данном уроке предполагается, если не оговорено противное, что все команды языка SQL вводятся интерактивным способом. В качестве информационной основы для примеров мы будем использовать базу данных "Служащие предприятия" (employee.gdb), входящую в поставку Delphi и находящуюся (по умолчанию) в поддиректории \IBLOCAL\EXAMPLES.

Рис. 1: Структура базы данных EMPLOYEE

На рис.1 приведена схема базы данных EMPLOYEE для Local InterBase, нарисованная с помощью CASE-средства S-Designor (см. доп. урок). На схеме показаны таблицы базы данных и взаимосвязи, а также обозначены первичные ключи и их связи с внешними ключами. Многие из примеров, особенно в конце урока, являются весьма сложными. Однако, не следует на этом основании делать вывод, что так сложен сам язык SQL. Дело, скорее, в том, что обычные (стандартные) операции настолько просты в SQL, что примеры таких операций оказываются довольно неинтересными и не иллюстрируют полной мощности этого языка. Но в целях системности мы пройдем по всем возможностям SQL: от самых простых - до чрезвычайно сложных.

Начнем с базовых операций реляционных баз данных. Таковыми являются: выборка(Restriction) проекция(Projection) соединение(Join) объединение(Union)

Операция выборки позволяет получить все строки (записи) либо часть строк одной таблицы. SELECT * FROM country

Получить все строки таблицы Country

COUNTRYCURRENCY
USADollar
EnglandPound
CanadaCdnDlr
SwitzerlandSFranc
JapanYen
ItalyLira
FranceFFranc
GermanyD-Mark
AustraliaADollar
Hong KongHKDollar
NetherlandsGuilder
BelgiumBFranc
AustriaSchilling
FijiFDollar

В этом примере и далее - для большей наглядности - все зарезервированные слова языка SQL будем писать большими буквами. Красным цветом будем записывать предложения SQL, а светло-синим - результаты выполнения запросов. SELECT * FROM country WHERE currency = "Dollar".

Получить подмножество строк таблицы Country,удовлетворяющее условию Currency = "Dollar"

Результат последней операции выглядит следующим образом: COUNTRY CURRENCY =============== ========== USA Dollar

Операция проекции позволяет выделить подмножество столбцов таблицы. Например: SELECT currency FROM country.

Получить списокденежных единиц CURRENCY ========== Dollar Pound CdnDlr SFranc Yen Lira FFranc D-Mark ADollar HKDollar Guilder BFranc Schilling FDollar

На практике очень часто требуется получить некое подмножество столбцов и строк таблицы, т.е. выполнить комбинацию Restriction и Projection. Для этого достаточно перечислить столбцы таблицы и наложить ограничения на строки. SELECT currency FROM country WHERE country = "Japan".

Найти денежную единицу Японии CURRENCY ========== Yen SELECT first_name, last_name FROM employee WHERE first_name = "Roger"

Получить фамилии работников, которых зовут "Roger"

FIRST_NAMELAST_NAME
RogerDe Souza
RogerReeves

Эти примеры иллюстрируют общую форму команды SELECT в языке SQL (для одной таблицы):

SELECT(выбрать) специфицированные поля
FROM(из) специфицированной таблицы
WHERE(где) некоторое специфицированное условие является истинны
Операция соединения позволяет соединять строки из более чем одной таблицы (по некоторому условию) для образования новых строк данных. SELECT first_name, last_name, proj_name FROM employee, project WHERE emp_no = team_leader

Получить список руководителей проектов

FIRST_NAMELAST_NAMEPROJ_NAME
AshokRamanathanVideo Database
PeteFisherDigiPizza
ChrisPapadopoulosAutoMap
BruceYoungMapBrowser port
Mary S.MacDonaldMarketing project 3

Операция объединения позволяет объединять результаты отдельных запросов по нескольким таблицам в единую результирующую таблицу. Таким образом, предложение UNION объединяет вывод двух или более SQL-запросов в единый набор строк и столбцов. SELECT first_name, last_name, job_country FROM employee WHERE UNION SELECT contact_first, contact_last, country customer

Получить список работников и заказчиков, проживающих во Франции

FIRST_NAMELAST_NAMEJOB_COUNTRY
JacquesGlonFrance
MichelleRocheFrance

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

SELECT[DISTINCT] список_выбираемых_элементов (полей)
FROMсписок_таблиц (или представлений)
[WHERE предикат]
[GROUP BYполе (или поля) [HAVING предикат]]
[UNIONдругое_выражение_Select]
[ORDER BYполе (или поля) или номер (номера)];

Рис. 2: Общий формат команды SELECT

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

Гибкость и мощь языка SQL состоит в том, что он позволяет объединить все операции реляционной алгебры в одной конструкции, "вытаскивая" таким образом любую требуемую информацию, что очень часто и происходит на практике.



ReportSmith


Borland ReportSmith является инструментом для получения отчетов и интегрирован в среду Delphi. Он может быть вызван непосредственно из меню Tools. Отчет может быть добавлен к приложениям Delphi, для этого есть компонента TReport на странице Data Access Палитры Компонентов. Войти в ReportSmith можно, нажав правую кнопка мыши на компоненте TReport и выбрав пункт контекстного меню (popup menu) или двойным щелчком левой кнопки мыши на компоненте TReport на форме.

Отчеты могут быть созданы для SQL БД или локальных БД и не требуют знания сложных команд БД. Интерфейс ReportSmith использует стандартные инструменты Windows 3.1 типа tool bar, formatting ribbon, и "drag and drop". Если пользователь уже знаком с интерфейсом стандартных Windows-программ, типа Word for Windows или Quattro Pro for Windows, ему будет "знаком" и интерфейс ReportSmith. ReportSmith предлагает 4 типа отчетов: Табличный, Кросс-таблица(CrossTab), Форма(Form) и Наклейка(Label).

ReportSmith использует концепцию "живых данных", т.е. работа происходит с настоящими данными все время, а не только тогда, когда запускается просмотр (preview). Кроме этого, ReportSmith легко работает с чрезвычайно большими БД при помощи адаптивной технологии управления памятью. В ReportSmith можно управлять тем, где сохраняется результат выборки данных из БД: в локальный памяти клиентской PC, на жестком диске клиентской PC, или на сервере.

ReportSmith включает поддержку: Встроенных шаблонов и стилей Отчетов типа перекрестных таблиц (Cross tab) Отчетов в виде почтовых адресов Вычисляемых полей и полей суммирования Многоуровневой сортировки и группировки Многоуровневых отчетов (master-details)

Отчеты, созданные с помощью ReportSmith могут распространяться бесплатно вместе с ReportSmith runtime-модулем. Конечные пользователи могут купить полную версию ReportSmith, для того чтобы создать свои собственные отчеты. Информация о ReportSmith доступна в руководстве ReportSmith for Windows - Creating Reports из коробки Delphi.



и проектирования базы данных


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

II. Следующий шаг включает в себя анализ объектов реального мира, которые необходимо смоделировать в базе данных.

Формирование концептуальной модели базы данных включает в себя: идентификацию функциональной деятельности Вашей предметной области. Например, если речь идет о деятельности предприятия, то в качестве функциональной деятельности можно идентифицировать ведение учета работающих, отгрузку продукции, оформление заказов и т.п. идентификацию объектов, которые осуществляют эту функциональную деятельность, и формирование из их операций последовательности событий, которые помогут Вам идентифицировать все сущности и взаимосвязи между ними. Например, процесс "ведение учета работающих" идентифицирует такие сущности как РАБОТНИК, ПРОФЕССИЯ, ОТДЕЛ. идентификацию характеристик этих сущностей. Например, сущность РАБОТНИК может включать такие характеристики как Идентификатор Работника, Фамилия, Имя, Отчество, Профессия, Зарплата. идентификацию взаимосвязей между сущностями. Например, каким образом сущности РАБОТНИК, ПРОФЕССИЯ, ОТДЕЛ взаимодействуют друг с другом? Работник имеет одну профессию (для простоты!) и значится в одном отделе, в то время как в одном отделе может находиться много работников.

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

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

IV. На четвертом шаге определяются атрибуты, которые уникальным образом идентифицируют каждый объект. Это необходимо для того, чтобы система могла получить любую единичную строку таблицы. Вы должны определить первичный ключ для каждого из отношений. Если нет возможности идентифицировать кортеж с помощью одного атрибута, то первичный ключ нужно сделать составным - из нескольких атрибутов. Хорошим примером может быть первичный ключ в таблице работников, состоящий из фамилии, имени и отчества. Первичный ключ гарантирует, что в таблице не будет содержаться двух одинаковых строк. Во многих СУБД имеется возможность помимо первичного определять еще ряд уникальных ключей. Отличие уникального ключа от первичного состоит в том, что уникальный ключ не является главным идентифицирующим фактором записи и на него не может ссылаться внешний ключ другой таблицы. Его главная задача - гарантировать уникальность значения поля.

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

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

VI. На шестом шаге устанавливаются связи между объектами (таблицами и столбцами) и производится очень важная операция для исключения избыточности данных - нормализация таблиц.

Каждый из различных типов связей должен быть смоделирован в базе данных. Существует несколько типов связей: связь "один-к-одному" связь "один-ко-многим" связь "многие-ко-многим".

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

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

Связь "многие-ко-многим" в явном виде в реляционных базах данных не поддерживается. Однако имеется ряд способов косвенной реализации такой связи, которые с успехом возмещают ее отсутствие. Один из наиболее распространенных способов заключается во введении дополнительной таблицы, строки которой состоят из внешних ключей, ссылающихся на первичные ключи двух таблиц. Например, имеются две таблицы: КЛИЕНТ и ГРУППА_ИНТЕРЕСОВ. Один человек может быть включен в различные группы, в то время как группа может объединять различных людей. Для реализации такой связи "многие-ко-многим" вводится дополнительная таблица, назовем ее КЛИЕНТЫ_В_ГРУППЕ, строка которой будет иметь два внешних ключа: один будет ссылаться на первичный ключ в таблице КЛИЕНТ, а другой - на первичный ключ в таблице ГРУППА_ИНТЕРЕСОВ. Таким образом в таблицу КЛИЕНТЫ_В_ГРУППЕ можно записывать любое количество людей и любое количество групп.

Итак, после определения таблиц, полей, индексов и связей между таблицами следует посмотреть на проектируемую базу данных в целом и проанализировать ее, используя правила нормализации, с целью устранения логических ошибок. Важность нормализации состоит в том, что она позволяет разбить большие отношения, как правило, содержащие большую избыточность информации, на более мелкие логические единицы, группирующие только данные, объединенные "по природе". Таким образом, идея нормализации заключается в следующем. Каждая таблица в реляционной базе данных удовлетворяет условию, в соответствии с которым в позиции на пересечении каждой строки и столбца таблицы всегда находится единственное значение, и никогда не может быть множества таких значений.

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

Процесс нормализации заключается в приведении таблиц в так называемые нормальные формы. Существует несколько видов нормальных форм: первая нормальная форма (1НФ), вторая нормальная форма (2НФ), третья нормальная форма (3НФ), нормальная форма Бойса-Кодда (НФБК), четвертая нормальная форма (4НФ), пятая нормальная форма (5НФ). С практической точки зрения, достаточно трех первых форм - следует учитывать время, необходимое системе для "соединения" таблиц при отображении их на экране. Поэтому мы ограничимся изучением процесса приведения отношений к первым трем формам.

Этот процесс включает: устранение повторяющихся групп (приведение к 1НФ) удаление частично зависимых атрибутов (приведение к 2НФ) удаление транзитивно зависимых атрибутов (приведение к 3НФ).

Рассмотрим каждый из этих процессов подробней.



Синтаксис обработки исключительных ситуаций


Теперь, когда мы рассмотрели, что такое исключительные ситуации, давайте дадим ясную картину, как они применяются. Новое ключевое слово, добавленное в язык Object Pascal - try. Оно используется для обозначения первой части защищенного участка кода. Существует два типа защищенных участков: try..except try..finally

Первый тип используется для обработки исключительных ситуаций. Его синтаксис: try Statement 1; Statement 2; ... except on Exception1 do Statement; on Exception2 do Statement; ... else Statements; {default exception-handler} end;

Для уверенности в том, что ресурсы, занятые вашим приложением, освободятся в любом случае, Вы можете использовать конструкцию второго типа. Код, расположенный в части finally, выполняется в любом случае, даже если возникает исключительная ситуация. Соответствующий синтаксис: try Statement1; Statement2; ... finally Statements; { These statements always execute } end;



Системная информация утилиты настройки BDE (BDECFG)


Итак, мы познакомились с наиболее важной возможностью утилиты настройки BDE - созданием и редактированием алиасов, определяющих параметры доступа к базам данных. Однако, утилита настройки BDE позволяет специфицировать не только алиасы, но и драйверы для доступа к базам данных, а также различную системную информацию, составляющую операционное окружение этих самых алиасов. Системная информация располагается на страничках "System", "Date", "Time", "Number". Рассмотрим подробней эти странички. System: Определяет память и технические установки для таблиц в формате Paradox. Установленные по умолчанию значения обеспечивают оптимальные параметры работы с таблицами Paradox. Однако, если у Вас возникают проблемы, Вы можете изменить минимальный и максимальный размер кэш-буфера (MINBUFSIZE, MAXBUFSIZE; значения по умолчанию соответственно 128 и 2048 Кб - должны быть меньше размера физической памяти, доступной для Windows), а также максимальную величину стандартной (low) памяти, используемой BDE для доступа к базе (LOW MEMORY USAGE LIMIT, значение по умолчанию - 32 Кб). Вы можете также специфицировать языковый драйвер по умолчанию (LANGDRIVER), однако языковый драйвер, установленный в алиасе, имеет больший приоритет. Аналогичным образом (и с теми же оговорками относительно приоритета) Вы можете изменить параметр SQLQRYMODE, если у Вас установлен Borland SQL Links. С помощью параметра LOCAL SHARE можно управлять возможностью одновременного доступа к таблицам из разных приложений через BDE и не через BDE (например, с использованием своей библиотеки доступа). Значение по умолчанию - false, что означает запрет такой работы. Параметр AUTO ODBC определяет режим выборки параметров алиасов, основанных на ODBC-драйверах. Установленное по умолчанию значение false означает, что параметры берутся из конфигурационного файла BDE (IDAPI.CFG). Если Вы желаете брать ODBC-алиасы из файла ODBC.INI, установите этот параметр в true. Стоит упомянуть и о параметре DEFAULT DRIVER, который используется всякий раз, когда в названии таблицы отсутствует расширение и таблица имеет формат локальных СУБД. Остальные параметры (VERSION и SYSFLAGS) являются системными, и их не следует изменять. Date: Определяет установки, используемые при конвертации строковых значений в дату и обратно. Основаны на значениях, устанавливаемых для каждой страны и зафиксированных в файле WIN.INI (секция [intl]). Однако, все параметры формата даты, времени и чисел BDE берет не из конфигурационного файла BDE, куда попадают данные установки, а из соответствующих переменных модуля SysUtils. По-видимому, эта ситуация произошла по недосмотру разработчиков. Поэтому мы перечислим параметры страничек "Date", "Time", "Number" и укажем те переменные, которыми действительно можно управлять изменением системной информации.

Среди параметров даты имеются следующие:

SEPARATOR - символ, используемый для разделения дня, месяца и года в дате. Ему соответствует переменная DateSeparator (Char*). Обычно имеет значения '.', '-', '/'. Значение по умолчанию берется из параметра sDate секции [intl] файла WIN.INI.

Рис. 3: Программа DateTime демонстрирует работу с форматами даты и времени MODE - управляет порядком следования месяца, дня и года в дате и может иметь значения: 0 - для MDY (месяц-день-год), 1 - для DMY (день-месяц-год), или 2 - для YMD (год -месяц-день). Прямого соответствия переменным модуля SysUtils не имеет. Вместо него, а также вместо параметров FOURDIGITYEAR, YEARBIASED, LEADINGZEROM и LEADINGZEROD используются переменные ShortDateFormat (string[15]) и LongDateFormat (string[31]). В этих переменных могут применяться только символ-разделитель дат (DateSeparator) и символьные выражения типа 'm', 'mm', 'd', 'dd', 'yy' и 'yyyy', определяющие месяц, день и год. Например, формат "короткой" даты может выглядеть как "dd.MM.yy", а формат "длинной" даты - как "d MMMM yyyy 'г.'". Значения по умолчанию берутся из параметров sShortDate и sLongDate секции [intl] файла WIN.INI. Здесь уместно сделать небольшое замечание. При отображении даты и времени в качестве символа-разделителя можно использовать любой символ, в том числе и отличный от символа DateSeparator (или TimeSeparator). Однако при попытке вставить в таком формате дату или время BDE "выдаст" ошибку, связанную с неправильным форматом даты/времени. Поэтому для корректной вставки данных в таблицы необходимо, чтобы в переменной ShortDateFormat символ-разделитель совпадал с символом DateSeparator, а в переменной LongTimeFormat (и ShortTimeFormat) - с символом TimeSeparator. Для изучения работы с форматами даты и времени посмотрите программу DateTime, имеющуюся на вашем диске (рис. 3). Вы можете скопировать ее в свой директорий и поэскпериментировать с отображением и вводом данных. Time: Определяет установки, используемые при конвертации строковых значений во время и обратно. Также основаны на значениях, устанавливаемых для каждой страны и зафиксированных в файле WIN.INI (секция [intl]). Аналогично дате, для формата времени совместно с ShortDateFormat используются переменные LongTimeFormat (обращаем внимание - именно LongTimeFormat, а не ShortTimeFormat) и TimeSeparator. Значения по умолчанию вычисляются по параметрам iTime и iTLZero секции [intl] файла WIN.INI. Кроме указанных переменных, для форматирования можно использовать переменные TimeAMString (основана на параметре s1159 секции [intl]) и TimePMString (основана на параметре s2359 секции [intl]). Number: Описывает трактовку чисел BDE. В частности, определяет символ для десятичной точки (переменная DecimalSeparator, основана на параметре sDecimal секции [intl]), разделитель для тысяч (переменная ThousandSeparator, основана на параметре sThousand секции [intl]), количество знаков после запятой (переменная CurrencyDecimals, основана на параметре sCurrDigits секции [intl]) и наличие лидирующих нулей.

Как уже отмечалось выше, утилита настройки BDE сохраняет всю конфигурационную информацию в файле IDAPI.CFG. Этот файл с предустановленными ссылками на драйверы и некоторыми стандартными алиасами создается при установке Delphi. Кроме того, он создается при установке файлов редистрибуции BDE (т.е. когда Вы переносите BDE и SQL Links на другие компьютеры).



Объекты из библиотеки визуальных компонент


Объекты из библиотеки визуальных компонент (VCL) Delphi, равно как и объекты реального мира, имеют свой набор свойств и свое поведение - набор откликов на события, происходящие с ними. Список событий для данного объекта, на которые он реагирует, можно посмотреть, например, в Инспекторе Объектов на странице событий. (На самом деле, на этой странице представлен список свойств, которые имеют тип вроде TMouseMoveEvent и представляют из себя процедуры-обработчики событий. Существует соглашение по названиям данных свойств. Например, OnDblClick соответствует двойному щелчку мыши, а OnKeyUp - событию, когда нажатая клавиша была отпущена.) Среди набора событий для различных объектов из VCL есть как события, портируемые из Windows (MouseMove, KeyDown), так и события, порождаемые непосредственно в программе (DataChange для TDataSource).
Поведение объекта определяется тем, какие обработчики и для каких событий он имеет. Создание приложения в Delphi состоит из настройки свойств используемых объектов и создания обработчиков событий.
Простейшие события, на которые иногда нужно реагировать - это, например, события, связанные с мышкой (они есть практически у всех видимых объектов) или событие Click для кнопки TButton. Предположим, что вы хотите перехватить щелчок левой кнопки мыши на форме. Чтобы сделать это - создайте новый проект, в Инспекторе Объектов выберите страницу событий и сделайте двойной щелчок на правой части для свойства OnClick. Вы получите заготовку для обработчика данного события: procedure TForm1.FormClick(Sender: TObject); begin end;
Напишите здесь следующее: procedure TForm1.FormClick(Sender: TObject); begin MessageDlg('Hello', mtInformation, [mbOk], 0); end;
Каждый раз, когда делается щелчок левой кнопки мыши над формой будет появляться окно диалога (см. рис.1).

Рис.1: Диалог, появляющийся при щелчке мыши на форме.
Код, приведенный выше, представляет из себя простейший случай ответа на событие в программе на Delphi. Он настолько прост, что многие программисты могут написать такой код и без понимания того, что они на самом деле отвечают на сообщение о событии, посланное им операционной системой. Хотя программист получает это событие через третьи руки, тем не менее он на него отвечает.
Опытные программисты в Windows знают, что при возникновении события, операционная система передает вам не только уведомление о нем, но и некоторую связанную с ним информацию. Например, при возникновении события "нажата левая кнопка мыши" программа информируется о том, в каком месте это произошло. Если вы хотите получить доступ к такой информации, то должны вернуться в Инспектор Объектов и создать обработчик события OnMouseDown: procedure TForm1.FormMouseDown(Sender: TObject; Button: TMouseButton; Shift: TShiftState; X, Y: Integer); begin Canvas.TextOut(X, Y, 'X='+IntToStr(X)+' Y='+IntToStr(Y)); end;
Запустите программу, пощелкайте мышкой на форме:

Рис.2
Как видите, в Delphi очень просто отвечать на события. И не только на события, связанные с мышкой. Например, можно создать обработчик для OnKeyDown (нажата клавиша): procedure TForm1.FormKeyDown(Sender: TObject; var Key: Word; Shift: TShiftState); begin MessageDlg(Chr(Key), mtInformation, [mbOk], 0); end;
Теперь, когда вы имеете начальные знания о программировании событий в Delphi, самое время вернуться назад и посмотреть на теорию, стоящую за тем кодом, что вы написали. После получения представления о том, как работает система, можно вернуться к среде Delphi и посмотреть, как использовать полностью имеющиеся возможности.

Соединение (JOIN)


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

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

Операции соединения подразделяются на два вида - внутренние и внешние. Оба вида соединений задаются в предложении WHERE запроса SELECT с помощью специального условия соединения. Внешние соединения (о которых мы поговорим позднее) поддерживаются стандартом ANSI-92 и содержат зарезервированное слово "JOIN", в то время как внутренние соединения (или просто соединения) могут задаваться как без использования такого слова (в стандарте ANSI-89), так и с использованием слова "JOIN" (в стандарте ANSI-92).

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

Внутренние соединения

Внутреннее соединение возвращает только те строки, для которых условие соединения принимает значение true. SELECT first_name, last_name, department FROM employee, department WHERE job_code = "VP"

получить список сотрудников, состоящих в должности "вице-президент", а также названия их отделов

FIRST_NAMELAST_NAMEDEPARTMENT
RobertNelsonCorporate Headquarters
Mary S.MacDonaldCorporate Headquarters
RobertNelsonSales and Marketing
Mary S.MacDonaldSales and Marketing
RobertNelsonEngineering
Mary S.MacDonaldEngineering
RobertNelsonFinance
Mary S.MacDonaldFinance

Этот запрос ("без соединения") возвращает неверный результат, так как имеющиеся между таблицами связи не задействованы. Отсюда и появляется дублирование информации в результирующей таблице. Правильный результат дает запрос с использованием операции соединения: SELECT first_name, last_name, department

получить список сотрудников, состоящих в должности "вице-президент", а также названия их отделов

FIRST_NAMELAST_NAMEDEPARTMENT
RobertNelsonEngineering
Mary S.MacDonaldSales and Marketing

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

Замечание 1: в одном запросе нельзя смешивать использование написания имен таблиц и их алиасов.

Замечание 2: алиасы таблиц могут совпадать с их именами.

получить список сотрудников, состоящих в должности "вице-президент", а также названия их отделов

FIRST_NAME LAST_NAME DEPARTMENT
RobertNelsonEngineering
Mary S.MacDonaldSales and Marketing

А вот пример запроса, соединяющего сразу три таблицы: SELECT first_name, last_name, job_title, department FROM employee e, department d, job j WHERE d.mngr_no = e.emp_no AND e.job_code = j.job_code AND e.job_grade = j.job_grade AND e.job_country = j.job_country

получить список сотрудников с названиями их должностей и названиями отделов

FIRST_NAMELAST_NAMEJOB_TITLEDEPARTMENT
RobertNelsonVice PresidentEngineering
PhilForestManagerQuality Assurance
K. J.WestonSales RepresentativeField Office: East Coast
KatherineYoungManagerCustomer Support
ChrisPapadopoulosManagerResearch and Development
JanetBaldwinSales Co-ordinatorPacific Rim Headquarters
RogerReevesSales Co-ordinatorEuropean Headquarters
WalterSteadmanChief Financial OfficerFinance

В данном примере последние три условия необходимы в силу того, что первичный ключ в таблице JOB состоит из трех полей - см. рис.1.

Мы рассмотрели внутренние соединения с использованием стандарта ANSI-89. Теперь опишем новый (ANSI-92) стандарт: условия соединения записываются в предложении FROM, в котором слева и справа от зарезервированного слова "JOIN" указываются соединяемые таблицы; условия поиска, основанные на правой таблице, помещаются в предложение ON; условия поиска, основанные на левой таблице, помещаются в предложение WHERE. SELECT first_name, last_name, department FROM employee e JOIN department d ON e.dept_no = d.dept_no AND department = "Customer Support" WHERE last_name starting with "P"

получить список служащих (а заодно и название отдела), являющихся сотрудниками отдела "Customer Support", фамилии которых начинаются с буквы "P"

FIRST_NAMELAST_NAMEDEPARTMENT
LesliePhongCustomer Support
BillParkerCustomer Support

Самосоединения

В некоторых задачах необходимо получить информацию, выбранную особым образом только из одной таблицы. Для этого используются так называемые самосоединения, или рефлексивные соединения. Это не отдельный вид соединения, а просто соединение таблицы с собой с помощью алиасов. Самосоединения полезны в случаях, когда нужно получить пары аналогичных элементов из одной и той же таблицы. SELECT one.last_name, two.last_name, one.hire_date FROM employee one, employee two WHERE one.hire_date = two.hire_date AND one.emp_no < two.emp_no

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

LAST_NAMELAST_NAMEHIRE_DATE
NelsonYoung28-DEC-1988
ReevesStansbury25-APR-1991
BishopMacDonald1-JUN-1992
BrownIchida4-FEB-1993
SELECT d1.department, d2.department, d1.budget FROM department d1, department d2 WHERE d1.budget = d2.budget AND d1.dept_no < d2.dept_no

получить список пар отделов с одинаковыми годовыми бюджетами

DEPARTMENTDEPARTMENTBUDGET
Software DevelopmentFinance400000.00
Field Office: East CoastField Office: Canada 500000.00
Field Office: JapanField Office: East Coast500000.00
Field Office: JapanField Office: Canada500000.00
Field Office: JapanField Office: Switzerland500000.00
Field Office: SingaporeQuality Assurance300000.00
Field Office: SwitzerlandField Office: East Coast500000.00
Внешние соединения

Напомним, что внутреннее соединение возвращает только те строки, для которых условие соединения принимает значение true. Иногда требуется включить в результирующий набор большее количество строк.

Вспомним, запрос вида SELECT first_name, last_name, department FROM employee e, department d WHERE e.dept_no = d.dept_no

возвращает только те строки, для которых условие соединения (e.dept_no = d.dept_no) принимает значение true.

Внешнее соединение возвращает все строки из одной таблицы и только те строки из другой таблицы, для которых условие соединения принимает значение true. Строки второй таблицы, не удовлетворяющие условию соединения (т.е. имеющие значение false), получают значение null в результирующем наборе.

Существует два вида внешнего соединения: LEFT JOIN и RIGHT JOIN.

В левом соединении (LEFT JOIN) запрос возвращает все строки из левой таблицы (т.е. таблицы, стоящей слева от зарезервированного словосочетания "LEFT JOIN") и только те из правой таблицы, которые удовлетворяют условию соединения. Если же в правой таблице не найдется строк, удовлетворяющих заданному условию, то в результате они замещаются значениями null.

Для правого соединения - все наоборот. SELECT first_name, last_name, department FROM employee e LEFT JOIN department d ON e.dept_no = d.dept_no

получить список сотрудников и название их отделов, включая сотрудников, еще не назначенных ни в какой отдел

FIRST_NAMELAST_NAME DEPARTMENT
RobertNelsonEngineering
BruceYoungSoftware Development

В результирующий набор входит и отдел "Software Products Div." (а также отдел "Field Office: Singapore", не представленный здесь), в котором еще нет ни одного сотрудника.

|



Соглашения по наименованиям


Если вы рассматривали исходные тексты VCL, то могли видеть, что они следуют нескольким простым соглашениям при определении новых классов. Delphi этого не требует, имена методов, свойств и т.п. могут быть любыми, компилятору это безразлично. Но если следовать этим соглашениям, то разработка новых компонентов и чтение исходных текстов станет существенно проще.

Итак: Все декларации типов начинаются на букву T. Еще раз, Delphi не требует этого, но это делает очевидным, что "TEdit", например, есть определение типа, а не переменная или поле класса. Имена свойствам нужно давать легко читаемые и информативные. Нужно помнить, что пользователь будет их видеть в Инспекторе Объектов. И имя вроде "TextOrientation" много удобнее, нежели "TxtOr". То же самое относится к методам. Методы, доступные пользователю, должны иметь удобные названия. При создании свойств типа Event, имя такого свойства должно начинаться с "On" (например, OnClick, OnCreate и т.д.). Имя метода для чтения свойства должен начинаться со слова "Get". Например, метод GetStyle должен выполнять чтение для свойства Style. Имя метода для записи свойства должен начинаться со слова "Set". Например, метод SetStyle должен выполнять запись в свойство Style. Внутреннее поле для хранения данных свойства должно носить имя, начинающееся с буквы "F". Например, свойство Handle могло бы храниться в поле FHandle.

Конечно же, есть исключения из правил. Иногда бывает удобнее их нарушить, например, класс TTable имеет свойства типа Event, которые называются BeforePost, AfterPost и т.п.



Сохранение OLE объекта в базе данных


Иногда необходимо хранить OLE объекты не в файлах, а в базе данных (BLOB поле в таблице). Конечно, в данном случае OLE объект должен быть присоединенным (embedded) в целях переносимости. К сожалению, в стандартной поставке Delphi нет специального объекта типа TDBOLEContainer для данных целей, но OLE объект можно сохранять и восстанавливать с помощью методов SaveToStream и LoadFromStream. Например: procedure TOLEForm.SaveOLE(Sender: TObject); var BlSt : TBlobStream; begin With Table1 do BlSt:=TBlobStream.Create(BlobField(FieldByName('OLE')), bmReadWrite); OLEContainer.SaveToStream(BlSt as TStream); BlSt.Free; end;



Сохранение программы


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

Первый шаг - создать поддиректорию для программы. Лучше всего создать директорию, где будут храниться все Ваши программы и в ней - создать поддиректорию для данной конкретной программы. Например, Вы можете создать директорию MYCODE и внутри нее - вторую директорию TIPS1, которая содержала бы программу, над которой Вы только что работали.

После создания поддиректории для хранения Вашей программы нужно выбрать пункт меню File | Save Project. Сохранить нужно будет два файла. Первый - модуль (unit), над которым Вы работали, второй - главный файл проекта, который "владеет" Вашей программой. Сохраните модуль под именем MAIN.PAS и проект под именем TIPS1.DPR. (Любой файл с расширением PAS и словом "unit" в начале является модулем.)



Состав языка SQL


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

Поэтому, в язык SQL в качестве составных частей входят: язык манипулирования данными (Data Manipulation Language, DML) язык определения данных (Data Definition Language, DDL) язык управления данными (Data Control Language, DCL).

Подчеркнем, что это не отдельные языки, а различные команды одного языка. Такое деление проведено только лишь с точки зрения различного функционального назначения этих команд.

Язык манипулирования данными используется, как это следует из его названия, для манипулирования данными в таблицах баз данных. Он состоит из 4 основных команд:

SELECT(выбрать)
INSERT(вставить)
UPDATE(обновить)
DELETE(удалить)

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

CREATE DATABASE(создать базу данных)
CREATE TABLE(создать таблицу)
CREATE VIEW(создать виртуальную таблицу)
CREATE INDEX(создать индекс)
CREATE TRIGGER(создать триггер)
CREATE PROCEDURE(создать сохраненную процедуру)
ALTER DATABASE(модифицировать базу данных)
ALTER TABLE(модифицировать таблицу)
ALTER VIEW(модифицировать виртуальную таблицу)
ALTER INDEX(модифицировать индекс)
ALTER TRIGGER(модифицировать триггер)
ALTER PROCEDURE(модифицировать сохраненную процедуру)
DROP DATABASE(удалить базу данных)
DROP TABLE(удалить таблицу)
DROP VIEW(удалить виртуальную таблицу)
DROP INDEX(удалить индекс)
DROP TRIGGER(удалить триггер)
DROP PROCEDURE(удалить сохраненную процедуру)

Язык управления данными используется для управления правами доступа к данным и выполнением процедур в многопользовательской среде. Более точно его можно назвать "язык управления доступом". Он состоит из двух основных команд:

GRANT(дать права)
REVOKE(забрать права)

С точки зрения прикладного интерфейса существуют две разновидности команд SQL: интерактивный SQL встроенный SQL.

Интерактивный SQL используется в специальных утилитах (типа WISQL или DBD), позволяющих в интерактивном режиме вводить запросы с использованием команд SQL, посылать их для выполнения на сервер и получать результаты в предназначенном для этого окне. Встроенный SQL используется в прикладных программах, позволяя им посылать запросы к серверу и обрабатывать полученные результаты, в том числе комбинируя set-ориентированный и record-ориентированный подходы.

Мы не будем приводить точный синтаксис команд SQL, вместо этого мы рассмотрим их на многочисленных примерах, что намного более важно для понимания SQL, чем точный синтаксис, который можно посмотреть в документации на Вашу СУБД.

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



Создание DLL в Delphi (экспорт)


Для программирования DLL Delphi предоставляет ряд ключевых слов и правил синтаксиса. Главное - DLL в Delphi такой же проект как и программа.

Рассмотрим шаблон DLL: library MyDll; uses <используемые модули>; <объявления и описания функций> exports < экспортируемые функции> begin <инициализационная часть> end.

Имя файла проекта для такого шаблона должно быть MYDLL.DPR.

!!!! К сожалению, в IDE Delphi автоматически генерируется только проект программы, поэтому Вам придется проект DLL готовить вручную. В Delphi 2.0 это неудобство устранено.

Как и в программе, в DLL присутствует раздел uses. Инициализационная часть необязательна. В разделе же exports перечисляются функции, доступ к которым должен производится из внешних приложений.

Экспортирование функций (и процедур ) может производится несколькими способами: по номеру (индексу); по имени.

В зависимости от этого используется различный синтаксис: {экспорт по индексу} procedure ExportByOrdinal; export; begin ..... end; exports ExportByOrdinal index 10; {экспорт по имени} procedure ExportByName; export; begin ..... end; exports ExportByName name 'MYEXPORTPROC'; { имя для экспорта может не совпадать с именем функции ! }

Так как в Windows существует понятие "резидентных функций" DLL, то есть тех функций, которые находятся в памяти на протяжении всего времени существования DLL в памяти, в Delphi имеются средства для организации и такого рода экспорта: exports ExportByName name 'MYEXPORTPROC' resident;

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

Если же Вы будете экспортировать функции следующим образом: exports MyExportFunc1, MyExportFunc2, .....;

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



Создание методов с помощью визуальных средств


В предыдущем уроке Вы видели, что синтаксический "скелет" метода может быть сгенерирован с помощью визуальных средств. Для этого, напомним, нужно в Инспекторе Объектов дважды щелкнуть мышкой на пустой строчке напротив названия интересующего Вас события в требуемом компоненте. Заметим, если эта строчка не пуста, то двойной щелчок на ней просто переместит Вас в окне Редактора Кода в то место, где находится данный метод.

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

Давайте рассмотрим процесс создания программы CONTROL1, которая поможет нам изучить технику написания методов в Delphi.

Рис. Ошибка! Текст указанного стиля в документе отсутствует.-A: Главная форма программы CONTROL1

Для создания программы CONTROL1 поместите с помощью мышки компонент Edit (находится на страничке "Standard" Палитры Компонентов) на форму. После этого ваша форма будет иметь вид, показанный на Рис. Ошибка! Текст указанного стиля в документе отсутствует.-A.

Рис. Ошибка! Текст указанного стиля в документе отсутствует.-B:Чтобы создать метод, просто дважды щелкните справа от слова OnDblClick

Теперь перейдите в Object Inspector, выберите страничку "Events" и дважды щелкните в пустой строчке напротив события OnDblClick, как показано на Рис. Ошибка! Текст указанного стиля в документе отсутствует.-B. После этого в активизировавшемся окне Редактора Вы увидите сгенерированный "скелет" метода Edit1DblClick, являющегося реакцией на событие OnDblClick: procedure TForm1.Edit1DblClick(Sender: TObject); begin end;

После генерации процедуры Вы можете оставить ее имя таким, каким "установил" Delphi, или изменить его на любое другое (для этого просто введите новое имя в указанной выше строке Инспектора Объектов справа от требуемого события и нажмите Enter).

Теперь в окне Редактора Кода введите смысловую часть метода: procedure TForm1.Edit1DblClick(Sender: TObject); begin Edit1.Text:= 'Вы дважды щелкнули в строке редактирования'; end;

Сохраните программу. Во время выполнения дважды щелкните на строке редактирования. Текст в этой строке изменится в соответствии с тем, что мы написали в методе Edit1DblClick: см. Рис. Ошибка! Текст указанного стиля в документе отсутствует.-C.

Рис. Ошибка! Текст указанного стиля в документе отсутствует.-C: Содержимое управляющего элемента TEdit изменяется после двойного щелчка по нему

Листинг Ошибка! Текст указанного стиля в документе отсутствует.-A и Листинг Ошибка! Текст указанного стиля в документе отсутствует.-B предоставляют полный код программы CONTROL1.

Листинг Ошибка! Текст указанного стиля в документе отсутствует.-A: Программа CONTROL1 демонстрирует, как создавать и использовать методы в Delphi. program Control1; uses Forms, Main in 'MAIN.PAS' {Form1}; begin Application.CreateForm(TForm1, Form1); Application.Run; end.

Листинг Ошибка! Текст указанного стиля в документе отсутствует.-B: Головной модуль программы CONTROL1. unit Main; interface uses WinTypes, WinProcs, Classes, Graphics, Controls, Printers, Menus, Forms, StdCtrls; type TForm1 = class(TForm) Edit1: TEdit; procedure Edit1DblClick(Sender: TObject); end; var Form1: TForm1; implementation {$R *.DFM} procedure TForm1.Edit1DblClick(Sender: TObject); begin Edit1.Text := 'Вы дважды щелкнули в строке редактирования'; end; end.

После того, как Ваша программа загрузится в память, выполняются две строчки кода в CONTROL1.DPR, автоматически сгенерированные компилятором: Application.CreateForm(TForm1, Form1); Application.Run;

Первая строка запрашивает память у операционной системы и создает там объект Form1, являющийся экземпляром класса TForm1. Вторая строка указывает объекту Application, "по умолчанию" декларированному в Delphi, чтобы он запустил на выполнение главную форму приложения. В данном месте мы не будем подробно останавливаться на классе TApplication и на автоматически создаваемом его экземпляре - Application. Важно понять, что главное его предназначение - быть неким ядром, управляющим выполнением Вашей программы.

Как правило, у большинства примеров, которыми мы будем оперировать в наших уроках, файлы проектов .DPR практически одинаковы. Поэтому в дальнейшем там, где они не отличаются кардинально друг от друга, мы не будем приводить их текст. Более того, в файл .DPR, автоматически генерируемый Delphi, в большинстве случаев нет необходимости заглядывать, поскольку все действия, производимые им, являются стандартными.

Итак, мы видели, что большинство кода Delphi генерирует автоматически. В большинстве приложений все, что Вам остается сделать - это вставить одну или несколько строк кода, как в методе Edit1DblClick: Edit1.Text := 'Вы дважды щелкнули в строке редактирования';

Хотя внешний интерфейс программы CONTROL1 достаточно прост, она (программа) имеет строгую внутреннюю структуру. Каждая программа в Delphi состоит из файла проекта, имеющего расширение .DPR и одного или нескольких модулей, имеющих расширение .PAS. Модуль, в котором содержится главная форма проекта, называется головным. Указанием компилятору о связях между модулями является предложение Uses, которое определяет зависимость модулей.

Нет никакого функционального различия между модулями, созданными Вам в Редакторе, и модулями, сгенерированными Delphi автоматически. В любом случае модуль подразделяется на три секции: Заголовок Секция Interface Секция Implementation

Таким образом, "скелет" модуля выглядит следующим образом: unit Main; {Заголовок модуля} interface {Секция Interface} implementation {Секция Implementation} end.

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



Создание отчета


В данной главе показан пример построения достаточно простого отчета на основе данных из таблиц, которые находятся в каталоге \DELPHI\DEMOS\DATA. В отчете для каждого заказчика будет выводиться список его заказов с сортировкой по имени заказчика. Для этого потребуется использовать таблицы ORDERS.DB (заказы) и CUSTOMER.DB (заказчики).

Запустите ReportSmith. Он попросит вас открыть отчет (если отчет уже существует, то можно выбрать имя отчета). Чтобы построить новый отчет, нажмите кнопку Cancel и затем в меню ReportSmith выберите пункт File|New. ReportSmith попросит выбрать тип отчета, который вы хотите построить (см. рис.2). В нашем примере мы будем строить табличный отчет (Columnar report).

Рис.2: Диалог выбора типа отчета

Если данных в таблицах много, то лучше выбрать режим Draft прежде, чем нажать OK. В этом случае ReportSmith спросит, сколько записей вы хотите использовать при построении отчета. Когда отчет запускается на выполнение, то будут использоваться все записи или то число, которое вы определяете в свойстве MaxRecords.

После выбора типа отчета укажите ReportSmith таблицу(ы), по которым вы хотите сделать отчет (см. рис.3).

Рис. 3: Диалог добавления таблиц в отчет.

Для добавления таблицы в отчет нажмите кнопку "Add table...", выберите тип таблицы Paradox (IDAPI) (см. рис.4), и выберите таблицу CUSTOMER.DB из каталога \DEMOS. Точно также добавьте таблицу ORDERS.DB. Следующим шагом нужно установить условия, по которым будет выполняться соединение таблиц при выполнении отчета. В данном случае они связаны по полю CustNo - код заказчика. Установки связи между таблицами происходит в соответствующем диалоге при нажатии кнопки "Add new link:" (см. рис.5). Предварительный этап закончен и можно нажать кнопку "Done".

Рис. 4: Диалог добавления таблицы в отчет

Рис. 5: Определение связи между таблицами

ReportSmith считает данные из таблиц и создаст начальный табличный отчет. В нашем отчете слишком большое количество столбцов и он не умещается по ширине на стандартной странице. Можно убрать ненужные колонки, выбирая столбец и нажимая клавишу Del. Удалите все столбцы кроме OrderNo, SaleDate, ShipDate, paymentMethod, AmountPaid. ReportSmith позволяет изменить ширину колонки с помощью мыши. Теперь отчет выглядит примерно так, как на рис.6.

Рис. 6: Отчет с выбранными полями

Однако, пока это не то, что нам нужно - в отчете нет информации о заказчике, для которого данный заказ предназначен. Прежде, чем добавить эту информацию нужно сгруппировать записи в отчете по принадлежности заказчику. Это делается в пункте меню Tools|Report Grouping: (см. рис.7). Просто выберите поле группировки (в нашем случае нужно сгруппировать записи по коду заказчика - поле CustNo) и нажмите кнопку "OK". Следующим шагом добавим Header и Footer (т.е. поле перед группой и после нее) для каждой группы. Это выполняется в пункте меню Insert|Headers/Footers (см. рис.8).

Рис. 7: Диалог группировки записей в отчете

Рис. 8: Добавление Header/Footer для группы

В дальнейшем, в Header для группы мы поместим информацию о заказчике, а в Footer - итоговую сумму всех заказов (т.е. сумму по полю AmountPaid для данной группы). А теперь наш отчет имеет вид, представленный на рис.9. (Для того, чтобы показывались названия полей в каждой группе нужно вызвать пункт меню Insert|Field Labels:)

Рис. 9: Отчет с группами записей и полями перед и после них

Для того чтобы добавить данные в Header нужно выбрать пункт меню Insert|Field. Появится соответствующий диалог (см. рис.10). В этом диалоге требуется указать поле, которое вы хотите вставить в отчет, нажать кнопку "Insert" и щелкнуть мышью в то место на отчете, куда его нужно поместить. В нашем отчете это будет поле Company и размещаться оно будет в заголовке группы (Header). Кроме того, если нужно, чтобы названия компаний в отчете шли в алфавитном порядке, то это можно указать в пункте меню Tools|Sorting: В диалоге укажите поля для сортировки - Company и OrderNo (номера заказов внутри каждой группы должны быть также упорядочены). После этого нажмите "Done".

Рис. 10: Диалог добавления поля в отчет

Теперь добавим суммирующее поле в Footer для группы. Для этого выберите пункт меню Tools|Summary Fields: В диалоге нужно выбрать группу CustNo_Group, поле AmountPaid, операцию Sum и нажать "Add To Group"(см. рис.11). Далее, по аналогии с полем Company, добавьте суммирующее поле в Footer группы (в диалоге вставки поля пункта меню, рис.10, в верхнем ComboBox'е нужно выбрать Summary Fields).

Отчет готов, его вид показан на рис.12.

Рис. 11: Диалог определения полей суммирования

Рис. 12: Готовый отчет.

| |



Создание Редактора Свойств


При создании нового Редактора Свойств, конечно, не нужно всегда переписывать его заново от базового класса TPropertyEditor. Может оказаться достаточным выбрать в качестве предка уже существующий для данного свойства редактор и переопределить некоторые его методы. Давайте рассмотрим простейший пример нового Редактора Свойств. Как Вы знаете, у всех видимых объектов есть свойство Hint - подсказка, появляющаяся во время выполнения программы, если задержать на некоторое время мышь на объекте. Это свойство имеет тип String и во время дизайна для его редактирования используется Редактор типа TStringProperty. Обычно, подсказка бывает однострочной, но в некоторых случаях ее нужно сделать многострочной. В принципе, здесь проблемы нет, достаточно во время выполнения программы присвоить свойству Hint нужное значение, например: Button1.Hint:='Line1'#13#10'Line2';

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

В нашем случае будет достаточно выбрать в качестве предка редактор TStringProperty и переписать некоторые методы. Во-первых, нужно переопределить метод Edit, в котором будет вызываться диалог для ввода строк подсказки. Во-вторых, нужно переопределить функцию GetAttributes, которая возвращает набор параметров, описывающих данное свойство. В частности, должен быть установлен атрибут paDialog, при этом в Инспекторе Объектов у свойства появится кнопка ':' для вызова диалога. И вообще-то нужно изменить метод GetValue, который используется для отображения значения свойства в Инспекторе Объектов.

Назовем новый Редактор Свойств THintProperty, декларация нового класса: THintProperty = class(TStringProperty) public function GetAttributes: TPropertyAttributes; override; function GetValue : String; override; procedure Edit; override; end;

Рассмотрим по порядку методы нового класса.

Функция GetAttributes добавляет к унаследованному множеству атрибуты paDialog (появляется кнопка ':') и paReadOnly (свойство нельзя редактировать непосредственно в Инспекторе Объектов, а только в диалоге, вызываемом через кнопку ':'): function THintProperty.GetAttributes: TPropertyAttributes; begin Result := inherited GetAttributes + [paDialog, paReadOnly]; end;

Функция GetValue заменяет "неправильные" символы #10 и #13 (перевод каретки и переход на новую строку) на символ ">": function THintProperty.GetValue : string; var i : Byte; begin result:=inherited GetValue; for i:=1 to Byte(result[0]) do if result[i]<#32 then result[i]:='>'; end;

Процедура Edit вызывает диалог для ввода строк подсказки. Диалог можно было бы нарисовать свой собственный, однако можно воспользоваться уже готовым. Несколько разных диалогов лежит в директории X:\DELPHI\SOURCE\LIB. Мы воспользуемся модулем STREDIT.PAS, в котором есть необходимый диалог редактирования строк. Итак, процедура Edit: procedure THintProperty.Edit; var HintEditDlg : TStrEditDlg; s : string; begin HintEditDlg:=TStrEditDlg.Create(Application); with HintEditDlg do try Memo.MaxLength := 254; s:=GetStrValue+#0; Memo.Lines.SetText(@s[1]); UpdateStatus(nil); ActiveControl := Memo; if ShowModal = mrOk then begin s:=StrPas(Memo.Lines.GetText); if s[0]>#2 then Dec(Byte(s[0]),2); SetStrValue(s); end; finally Free; end; end;

Строка if s[0]>#2 then Dec(Byte(s[0]),2) нужна, так как Memo.Lines.GetText возвращает все строки с символами #13#10.



Создание Связанных Курсоров (Linked cursors)


Связанные курсоры позволяют программистам определить отношение один ко многим (one-to-many relationship). Например, иногда полезно связать таблицы CUSTOMER и ORDERS так, чтобы каждый раз, когда пользователь выбирает имя заказчика, то он видит список заказов связанных с этим заказчиком. Иначе говоря, когда пользователь выбирает запись о заказчике, то он может просматривать только заказы, сделанные этим заказчиком.

Программа LINKTBL демонстрирует, как создать программу которая использует связанные курсоры. Чтобы создать программу заново, поместите два TTable, два TDataSources и два TDBGrid на форму. Присоедините первый набор таблице CUSTOMER, а второй к таблице ORDERS. Программа в этой стадии имеет вид, показанный на рис.8

Рис.8: Программа LINKTBL показывает, как определить отношения между двумя таблицами.

Следующий шаг должен связать таблицу ORDERS с таблицей CUSTOMER так, чтобы Вы видели только те заказы, которые связанные с текущей записью в таблице заказчиков. В первой таблице заказчик однозначно идентифицируется своим номером - поле CustNo. Во второй таблице принадлежность заказа определяется также номером заказчика в поле CustNo. Следовательно, таблицы нужно связывать по полю CustNo в обоих таблицах (поля могут иметь различное название, но должны быть совместимы по типу). Для этого, Вы должны сделать три шага, каждый из которых требует некоторого пояснения: Установить свойство Table2.MasterSource = DataSource1 Установить свойство Table2.MasterField = CustNo Установить свойство Table2.IndexName = CustNo

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

Свойство MasterSource в Table2 определяет DataSource от которого Table2 может получить информацию. То есть, оно позволяет таблице ORDERS знать, какая запись в настоящее время является текущей в таблице CUSTOMERS.

Но тогда возникает вопрос: Какая еще информация нужна Table2 для того, чтобы должным образом отфильтровать содержимое таблицы ORDERS? Ответ состоит из двух частей: Требуется имя поля по которому связанны две таблицы. Требуется индекс по этому полю в таблице ORDERS (в таблице 'многих записей'),которая будет связываться с таблицей CUSTOMER(таблице в которой выбирается'одна запись').

Чтобы правильно воспользоваться информацией описанной здесь, Вы должны сначала проверить, что таблица ORDERS имеет нужные индексы. Если этот индекс первичный, тогда не нужно дополнительно указывать его в поле IndexName, и поэтому Вы можете оставить это поле незаполненным в таблице TTable2 (ORDERS). Однако, если таблица связана с другой через вторичный индекс, то Вы должны явно определять этот индекс в поле IndexName связанной таблицы.

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

Недостаточно, однако, просто yпомянуть имя индекса, который Вы хотите использовать. Некоторые индексы могут содержать несколько полей, так что Вы должны явно задать имя поля, по которому Вы хотите связать две таблицы. Вы должны ввести имя 'CustNo' в свойство Table2.MasterFields. Если Вы хотите связать две таблицы больше чем по одному полю, Вы должны внести в список все поля, помещая символ '|' между каждым: Table1.MasterFields := 'CustNo | SaleData | ShipDate';

В данном конкретном случае, выражение, показанное здесь, не имеет смысла, так как хотя поля SaleData и ShipDate также индексированы, но не дублируются в таблице CUSTOMER. Поэтому Вы должны ввести только поле CustNo в свойстве MasterFields. Вы можете определить это непосредственно в редакторе свойств, или написать код подобно показанному выше. Кроме того, поле (или поля) связи можно получить, вызвав редактор связей - в Инспекторе Объектов дважды щелкните на свойство MasterFields (рис.10)

Рис.10: Редактор связей для построения связанных курсоров.

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



Создание таблиц с помощью компонента TTable


Для создания таблиц компонент TTable имеет метод CreateTable. Этот метод создает новую пустую таблицу заданной структуры. Данный метод (процедура) может создавать только локальные таблицы формата dBase или Paradox.

Компонент TTable можно поместить на форму в режиме проектирования или создать динамически во время выполнения. В последнем случае перед использованием его необходимо создать, например, с помощью следующей конструкции: var Table1: TTable; ... Table1:=TTable.Create(nil); ...

Перед вызовом метода CreateTable необходимо установить значения свойств TableType - тип таблицы DatabaseName - база данных TableName - имя таблицы FieldDefs - массив описаний полей vIndexDefs - массив описаний индексов.

Свойство TableType имеет тип TTableType и определяет тип таблицы в базе данных. Если это свойство установлено в ttDefault, тип таблицы определяется по расширению файла, содержащего эту таблицу: Расширение .DB или без расширения: таблица Paradox Расширение .DBF : таблица dBASE Расширение .TXT : таблица ASCII (текстовый файл).

Если значение свойства TableType не равно ttDefault, создаваемая таблица всегда будет иметь установленный тип, вне зависимости от расширения: ttASCII: текстовый файл ttDBase: таблица dBASE ttParadox: таблица Paradox.

Свойство DatabaseName определяет базу данных, в которой находится таблица. Это свойство может содержать: BDE алиас директорий для локальных БД директорий и имя файла базы данных для Local InterBase локальный алиас, определенный через компонент TDatabase.

Свойство TableName определяет имя таблицы базы данных.

Свойство FieldDefs (имеющее тип TFieldDefs) для существующей таблицы содержит информацию обо всех полях таблицы. Эта информация доступна только в режиме выполнения и хранится в виде массива экземпляров класса TFieldDef, хранящих данные о физических полях таблицы (т.о. вычисляемые на уровне клиента поля не имеют своего объекта TFieldDef). Число полей определяется свойством Count, а доступ к элементам массива осуществляется через свойство Items: property Items[Index: Integer]: TFieldDef;

При создании таблицы, перед вызовом метода CreateTable, нужно сформировать эти элементы. Для этого у класса TFieldDefs имеется метод Add: procedure Add(const Name: string; DataType: TFieldType;Size: Word; Required: Boolean);

Параметр Name, имеющий тип string, определяет имя поля. Параметр DataType (тип TFieldType) обозначает тип поля. Он может иметь одно из следующих значений, смысл которых ясен из их наименования: TFieldType = (ftUnknown, ftString, ftSmallint, ftInteger, ftWord, ftBoolean, ftFloat, ftCurrency, ftBCD, ftDate, ftTime, ftDateTime, ftBytes, ftVarBytes, ftBlob, ftMemo,ftGraphic);

Параметр Size (тип word) представляет собой размер поля. Этот параметр имеет смысл только для полей типа ftString, ftBytes, ftVarBytes, ftBlob, ftMemo, ftGraphic, размер которых может сильно варьироваться. Поля остальных типов всегда имеют строго фиксированный размер, так что данный параметр для них не принимается во внимание. Четвертый параметр - Required - определяет, может ли поле иметь пустое значение при записи в базу данных. Если значение этого параметра - true, то поле является "требуемым", т.е. не может иметь пустого значения. В противном случае поле не является "требуемым" и, следовательно, допускает запись значения NULL. Отметим, что в документации по Delphi и online-справочнике допущена ошибка - там отсутствует упоминание о четвертом параметре для метода Add.

Если Вы желаете индексировать таблицу по одному или нескольким полям, используйте метод Add для свойства IndexDefs, которое, как можно догадаться, также является объектом, т.е. экземпляром класса TIndexDefs. Свойство IndexDefs для существующей таблицы содержит информацию обо всех индексах таблицы. Эта информация доступна только в режиме выполнения и хранится в виде массива экземпляров класса TIndexDef, хранящих данные об индексах таблицы. Число индексов определяется свойством Count, а доступ к элементам массива осуществляется через свойство Items: property Items[Index: Integer]: TIndexDef;

Метод Add класса TIndexDefs имеет следующий вид: procedure Add(const Name, Fields: string; Options: TIndexOptions);

Параметр Name, имеющий тип string, определяет имя индекса. Параметр Fields (также имеющий тип string) обозначает имя поля, которое должно быть индексировано, т.е. имя индексируемого поля. Составной индекс, использующий несколько полей, может быть задан списком имен полей, разделенных точкой с запятой ";", например: 'Field1;Field2;Field4'. Последний параметр - Options - определяет тип индекса. Он может иметь набор значений, описываемых типом TIndexOptions: TIndexOptions = set of (ixPrimary, ixUnique, ixDescending, ixCaseInsensitive, ixExpression);

Поясним эти значения. ixPrimary обозначает первичный ключ, ixUnique - уникальный индекс, ixDescending - индекс, отсортированный по уменьшению значений (для строк - в порядке, обратном алфавитному), ixCaseInsensitive - индекс, "нечувствительный" к регистру букв, ixExpression - индекс по выражению. Отметим, что упоминание о последнем значении также отсутствует в документации и online-справочнике. Опция ixExpression позволяет для таблиц формата dBase создавать индекс по выражению. Для этого достаточно в параметре Fields указать желаемое выражение, например: 'Field1*Field2+Field3'. Вообще говоря, не все опции индексов применимы ко всем форматам таблиц. Ниже мы приведем список допустимых значений для таблиц dBase и Paradox:

Опции индексовdBASEParadox
ixPrimary+
ixUnique++
ixDescending++
ixCaseInsensitive +
ixExpression+ 

Необходимо придерживаться указанного порядка применения опций индексов во избежание некорректной работы. Следует отметить, что для формата Paradox опция ixUnique может использоваться только вместе с опцией ixPrimary (см. пример на диске - Рис. Ошибка! Текст указанного стиля в документе отсутствует.-1).

Итак, после заполнения всех указанных выше свойств и вызова методов Add для FieldDefs и IndexDefs необходимо вызвать метод класса TTable - CreateTable: with Table1 do begin DatabaseName:='dbdemos'; TableName:='mytest'; TableType:=ttParadox; {Создать поля} with FieldDefs do begin Add('Surname', ftString, 30, true); Add('Name', ftString, 25, true); Add('Patronymic', ftString, 25, true); Add('Age', ftInteger, 0, false); Add('Weight', ftFloat, 0, false); end; {Сгенерировать индексы} with IndexDefs do begin Add('I_Name', 'Surname;Name;Patronymic', [ixPrimary, ixUnique]); Add('I_Age', 'Age', [ixCaseInsensitive]); end; CreateTable; end;

Рис. Ошибка! Текст указанного стиля в документе отсутствует.-1: Программа CREATABL демонстрирует технику создания таблиц во время выполнения

Индексы можно сгенерировать и не только при создании таблицы. Для того чтобы сгенерировать индексы для существующей таблицы, нужно вызвать метод AddIndex класса TTable, набор параметров которого полностью повторяет набор параметров для метода Add класса TIndexDefs: procedure AddIndex(const Name, Fields: string; Options: TIndexOptions);

При этом для метода AddIndex справедливы все замечания по поводу записи полей и опций индексов, сделанные выше.



Создание таблиц с помощью SQL


Если Вы хотите воспользоваться компонентом TQuery, сначала поместите его на форму. После этого настройте свойство DatabaseName на нужный Вам алиас (если базы данных еще не существует, удобней создать ее в WISQL командой File|Create Database..., а затем уже настроить на нее новый алиас). После этого можно ввести SQL-предложение в свойство SQL. Для выполнения запроса, изменяющего структуру, вставляющего или обновляющего данные на сервере, нужно вызвать метод ExecSQL компонента TQuery. Для выполнения запроса, получающего данные с сервера (т.е. запроса, в котором основным является оператор SELECT), нужно вызвать метод Open компонента TQuery. Это связано с тем, что BDE при посылке запроса типа SELECT открывает так называемый курсор, с помощью которого осуществляется навигация по выборке данных (подробней об этом см. в уроке, посвященном TQuery).

Как показывает опыт, проще воспользоваться утилитой WISQL. Для этого в WISQL выберите команду File|Run an ISQL Script... и выберите файл, в котором записан ваш скрипт, создающий базу данных. После нажатия кнопки "OK" ваш скрипт будет выполнен, и в нижнее окно будет выведен протокол его работы.

Приведем упрощенный синтаксис SQL-предложения для создания таблицы на SQL-сервере InterBase (более полный синтаксис можно посмотреть в online-справочнике по SQL, поставляемом с локальным InterBase): CREATE TABLE table (<col_def> [, <col_def> | <tconstraint> ...]);

где
table - имя создаваемой таблицы,
<col_def> - описание поля,
<tconstraint> - описание ограничений и/или ключей (квадратные скобки [] означают необязательность, вертикальная черта | означает "или").

Описание поля состоит из наименования поля и типа поля (или домена - см. урок 9), а также дополнительных ограничений, накладываемых на поле: <col_def> = col {datatype | COMPUTED BY (<expr>) | domain} [DEFAULT {literal | NULL | USER}] [NOT NULL] [<col_constraint>] [COLLATE collation]

Здесь
col - имя поля;
datatype - любой правильный тип SQL-сервера (для InterBase такими типами являются - см. урок 11 - SMALLINT, INTEGER, FLOAT, DOUBLE PRECISION, DECIMAL, NUMERIC, DATE, CHAR, VARCHAR, NCHAR, BLOB), символьные типы могут иметь CHARACTER SET - набор символов, определяющий язык страны. Для русского языка следует задать набор символов WIN1251;
COMPUTED BY (<expr>) - определение вычисляемого на уровне сервера поля, где <expr> - правильное SQL-выражение, возвращающее единственное значение;
domain - имя домена (обобщенного типа), определенного в базе данных;
DEFAULT - конструкция, определяющая значение поля по умолчанию;
NOT NULL - конструкция, указывающая на то, что поле не может быть пустым;
COLLATE - предложение, определяющее порядок сортировки для выбранного набора символов (для поля типа BLOB не применяется). Русский набор символов WIN1251 имеет 2 порядка сортировки - WIN1251 и PXW_CYRL. Для правильной сортировки, включающей большие буквы, следует выбрать порядок PXW_CYRL.

Описание ограничений и/или ключей включает в себя предложения CONSTRAINT или предложения, описывающие уникальные поля, первичные, внешние ключи, а также ограничения CHECK (такие конструкции могут определяться как на уровне поля, так и на уровне таблицы в целом, если они затрагивают несколько полей): <tconstraint> = [CONSTRAINT constraint <tconstraint_def>] <tconstraint>

Здесь <tconstraint_def> = {{PRIMARY KEY | UNIQUE} (col[,col...]) | FOREIGN KEY (col [, col ...]) REFERENCES other_table | CHECK (<search_condition>)} search_condition = {<val> operator {<val> | (<select_one>)} | <val> [NOT] BETWEEN <val> AND <val> | <val> [NOT] LIKE <val> [ESCAPE <val>] | <val> [NOT] IN (<val> [, <val> ...] | <val> = { col [array_dim] | <constant> | <expr> | <functiont> | NULL | USER | RDB$DB_KEY } [COLLATE collation] <constant> = num | "string" | charsetname "string" <functiont> = { COUNT (* | [ALL] <val> | DISTINCT <val>) | SUM ([ALL] <val> | DISTINCT <val>) | AVG ([ALL] <val> | DISTINCT <val>) | MAX ([ALL] <val> | DISTINCT <val>) | MIN ([ALL] <val> | DISTINCT <val>) | CAST (<val> AS <datatype>) | UPPER (<val>) | GEN_ID (generator, <val>) } <operator> = {= | < | > |<= |>= | !< | !> | <> | !=}

<select_one> = выражение SELECT по одному полю, которое возвращает в точности одно значение.

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

Пример A: Простая таблица с конструкцией PRIMARY KEY на уровне поля CREATE TABLE REGION ( REGION REGION_NAME NOT NULL PRIMARY KEY, POPULATION INTEGER NOT NULL);

Предполагается, что в базе данных определен домен REGION_NAME, например, следующим образом: CREATE DOMAIN REGION_NAME AS VARCHAR(40) CHARACTER SET WIN1251 COLLATE PXW_CYRL;

Пример B: Таблица с предложением UNIQUE как на уровне поля, так и на уровне таблицы CREATE TABLE GOODS ( MODEL SMALLINT NOT NULL UNIQUE, NAME CHAR(10) NOT NULL, ITEMID INTEGER NOT NULL, CONSTRAINT MOD_UNIQUE UNIQUE (NAME, ITEMID));

Пример C: Таблица с определением первичного ключа, внешнего ключа и конструкции CHECK, а также символьных массивов CREATE TABLE JOB ( JOB_CODE JOBCODE NOT NULL, JOB_GRADE JOBGRADE NOT NULL, JOB_REGION REGION_NAME NOT NULL, JOB_TITLE VARCHAR(25) CHARACTER SET WIN1251 COLLATE PXW_CYRL NOT NULL, MIN_SALARY SALARY NOT NULL, MAX_SALARY SALARY NOT NULL, JOB_REQ BLOB(400,1) CHARACTER SET WIN1251, LANGUAGE_REQ VARCHAR(15) [5], PRIMARY KEY (JOB_CODE, JOB_GRADE, JOB_REGION), FOREIGN KEY (JOB_REGION) REFERENCES REGION (REGION), CHECK (MIN_SALARY < MAX_SALARY));

Данный пример создает таблицу, содержащую информацию о работах (профессиях). Типы полей основаны на доменах JOBCODE, JOBGRADE, REGION_NAME и SALARY. Определен массив LANGUAGE_REQ, состоящий из 5 элементов типа VARCHAR(15). Кроме того, введено поле JOB_REQ, имеющее тип BLOB с подтипом 1 (текстовый блоб) и размером сегмента 400. Для таблицы определен первичный ключ, состоящий из трех полей JOB_CODE, JOB_GRADE и JOB_REGION. Далее, определен внешний ключ (JOB_REGION), ссылающийся на поле REGION таблицы REGION. И, наконец, включено предложение CHECK, позволяющее производить проверку соотношения для двух полей и вызывать исключительное состояние при нарушении такого соотношения.

Пример D: Таблица с вычисляемым полем CREATE TABLE SALARY_HISTORY ( EMP_NO EMPNO NOT NULL, CHANGE_DATE DATE DEFAULT "NOW" NOT NULL, UPDATER_ID VARCHAR(20) NOT NULL, OLD_SALARY SALARY NOT NULL, PERC_CHANGE DOUBLE PRECISION DEFAULT 0 NOT NULL CHECK (PERC_CHANGE BETWEEN -50 AND 50), NEW_SALARY COMPUTED BY (OLD_SALARY + OLD_SALARY * PERC_CHANGE / 100), PRIMARY KEY (EMP_NO, CHANGE_DATE, UPDATER_ID), FOREIGN KEY (EMP_NO) REFERENCES EMPLOYEE (EMP_NO));

Данный пример создает таблицу, где среди других полей имеется вычисляемое (физически не существующее) поле NEW_SALARY, значение которого вычисляется по значениям двух других полей (OLD_SALARY и PERC_CHANGE).

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