Работа с базами данных Глава 4

advertisement
Глава 4
Работа с базами данных
Работа с базами данных всегда была моей самой любимой темой. Помню, как в летние каникулы, когда я был еще только начинающим программистом, колдовал над
программой "Видеотека", предназначенной для учета домашней видеоколлекции и
использующей Paradox в качестве СУБД.
Я думаю, что со мной согласятся многие, если я скажу, что тема данной главы затрагивает одно из самых популярных направлений программирования в наши дни.
Мы подробно поговорим о СУБД MS Access, которую некоторые программисты
ухитряются использовать в качестве клиент-серверной СУБД, хотя я никогда не понимал такой выбор. Также обратимся к работе с MySQL, Firebird, MS SQL Server,
Oracle и, конечно же, поговорим о том, как улучшить интерфейс пользовательского
приложения: рассмотрим темы построения деревьев, усовершенствования стандартного компонента TDBGrid и многое другое.
Вопрос 1. Как сделать, чтобы компонент TDBGrid автоматически подстраивал ширину колонок под длину максимальной записи?
Ответ. Разместите на форме компоненты TADOTable, TDataSource, TDBGrid. В
папке с примером к вопросу вы найдете базу MS Access, на работу с которой нужно
настроить компонент TADOTable. Далее свяжите компоненты TADOTable,
TDataSource и TDBGrid. Теперь разместите на форме кнопку, для которой напишите следующий обработчик:
procedure TForm1.Button1Click(Sender: TObject);
var
i,j: integer;
value_width: integer;
t:integer;
koef:byte;
Глава 4
170
begin
// Отключаем автопрорисовку DBGrid
DBGrid1.DefaultDrawing:=false;
// Задаем коэффициент длины
koef:=10;
// Обрабатываем все колонки DBGrid
for i:=0 to DBGrid1.Columns.Count-1 do
begin
// Задаем начальную длину
value_width := 0;
// Пока не проверили все записи
while not(ADOTable1.Eof) do
begin
// Получаем длину очередной записи
t:= Length(ADOTable1.Fields[i].Value);
// Если полученная длина больше чем самая большая, то
// меняем значение value_width
if value_width<t then value_width:=t;
// Переходим к следующей записи
ADOTable1.Next;
end;
// Устанавливаем для колонки длину, равную самой длинной
// записи, помноженной на заданный коэффициент
DBGrid1.Columns[i].Width:=value_width*koef;
// Возвращаемся к первой записи
ADOTable1.First;
end;
// Включаем автопрорисовку DBGrid
DBGrid1.DefaultDrawing:=true;
end;
К о м п а к т- д и с к
Пример — в Database\auto_grid.
Работа с базами данных
171
Вопрос 2. Как сделать DBGrid "полосатым": одна строка одного цвета, другая строка
— другого цвета?
Ответ. Разместите на форме компоненты TADOTable, TDataSource, TDBGrid, настройте их на работу с определенной базой. Откройте набор данных. Теперь в свойстве DefaultDrawing компонента TDBGrid установите False и создайте для данного компонента обработчик OnDrawColumnCell:
procedure TForm1.DBGrid1DrawColumnCell(Sender: TObject;
const Rect: TRect;
DataCol: Integer; Column: TColumn; State: TGridDrawState);
begin
// Определяем, каким цветом будем закрашивать
if TDBGrid(Sender).DataSource.DataSet.RecNo mod 2 = 1
then
TDBGrid(Sender).Canvas.Brush.Color:=clBlue
else
TDBGrid(Sender).Canvas.Brush.Color:=clGreen;
// Если это выбранная запись, то ее будем рисовать другим цветом
if (gdSelected in State)
then
begin
TDBGrid(Sender).Canvas.Brush.Color:= clHighLight;
TDBGrid(Sender).Canvas.Font.Color := clHighLightText;
end;
with TDBGrid(Sender).Canvas do
begin
// Рисуем
FillRect(Rect);
// Выводим текст
TextOut(Rect.Left+2,Rect.Top+2,Column.Field.Text);
end;
end;
К о м п а к т- д и с к
Пример — в Database\color_grid.
Глава 4
172
Вопрос 3. Как сделать только одно из полей в компоненте DBGrid доступным только
для чтения?
Ответ. Следующий код запрещает редактирование поля Answer (конечно же, оно
должно существовать):
var
i: integer;
begin
for i:=0 to DBGrid1.DataSource.DataSet.Fields.Count-1 do
if DBGrid1.DataSource.DataSet.Fields[i].DisplayName='Answer' then
DBGrid1.DataSource.DataSet.Fields[i].ReadOnly:=TRUE;
end;
К о м п а к т- д и с к
Пример — в Database\readonly_field_grid.
Вопрос 4. Как сделать, чтобы в компоненте DBGrid нормально работал скроллинг
мыши, т. е. переход осуществлялся не только по видимым записям, но и по всем существующим?
Ответ. Просто модифицируем стандартный компонент TDBGrid, создав нижеприведенный модуль:
unit ScrollGrid;
interface
uses
Math,
Windows, Messages, SysUtils, Classes, Controls, Grids, DBGrids;
type
// Создаем потомка от стандартного компонента DBGrid
TScrollGrid = class(TDBGrid)
private
procedure WMWheel(var Msg:TWMMouseWheel); message WM_MOUSEWHEEL;
{ Private declarations }
end;
Работа с базами данных
173
procedure Register;
implementation
// Регистрируем новый компонент в палитре Delphi
procedure Register;
begin
RegisterComponents('Samples', [TScrollGrid]);
end;
procedure TScrollGrid.WMWheel(var Msg: TWMMouseWheel);
begin
DataSource.DataSet.MoveBy(-sign(Msg.WheelDelta));
end;
end.
Далее выберите пункт Component | Install Component и установите новый компонент в системе, а затем используйте его как обычный компонент TDBGrid.
К о м п а к т- д и с к
Пример — в Database\scroll_grid.
Вопрос 5. Как сделать, чтобы для логических полей в компоненте TDBGrid выводились флажки?
Ответ. Разместите на форме компоненты TADOTable, TDataSource, TDBGrid, настройте их на работу с определенной базой. Откройте набор данных.
В нашем примере предполагается существование логического поля REM. Произведите
двойной щелчок левой кнопкой мыши на компоненте TADOTable, в появившемся
окне щелкните правой кнопкой мыши и выберите команду Add all fields.
Далее в свойстве DefaultDrawing компонента TDBGrid установите False и создайте обработчик OnDrawColumnCell:
procedure TForm1.DBGrid1DrawColumnCell(Sender: TObject;
const Rect: TRect;
DataCol: Integer; Column: TColumn; State: TGridDrawState);
var
Глава 4
174
// Стиль CheckBox
Style:Cardinal;
begin
// Если обрабатывается нужное нам поле
if Column.FieldName='Rem'
then
begin
// Определяем стиль CheckBox в зависимости от значения поля
if Column.Field.Value=0 then Style := DFCS_CHECKED
else Style := DFCS_BUTTONCHECK;
// Рисуем CheckBox
DrawFrameControl(TDBGrid(Sender).Canvas.Handle, Rect,
DFC_BUTTON, Style)
end
else
// Если обрабатывается любое другое поле,
// то просто выводим его значение
TDBGrid(Sender).DefaultDrawColumnCell(Rect,DataCol,Column,State);
end;
Чтобы флажки работали полнофункционально, т. е. их можно было снимать и устанавливать, необходимо для компонента TDBGrid создать обработчик OnCellClick:
procedure TForm1.DBGrid1CellClick(Column: TColumn);
begin
// Если щелчок был произведен по нужному нам полю
if Column.FieldName='Rem' then
begin
// Переводим набор данных в режим редактирования
DataSource1.DataSet.Edit;
// Проверяем, пользователь снимает
// или устанавливает флаг
if Column.Field.Value=0
then
Работа с базами данных
175
Column.Field.Value:=1
else
Column.Field.Value:=0;
// Сохраняем новое установленное значение
DataSource1.DataSet.Post;
end;
end;
К о м п а к т- д и с к
Пример — в Database\checkbox_grid.
Вопрос 6. Как можно обработать ситуацию некорректного ввода значений в поле
DBGrid?
Ответ. Разместите на форме компоненты TADOTable, TDataSource, TDBGrid, настройте их на работу с определенной базой. Откройте набор данных. Произведите
двойной щелчок левой кнопкой мыши на компоненте TADOTable, в появившемся
окне щелкните правой кнопкой мыши и выберите команду Add all fields. Теперь выделите нужное вам поле и создайте для него обработчик события OnSetText вида:
if Text='1' then ShowMessage('Единица недопустима в данном поле.')
else (Sender as TStringField).Value:=Text;
Вопрос 7. Как отменить автодобавление новой пустой записи в компоненте
TDBGrid?
Ответ. В обработчике BeforeInsert вашего набора данных напишите Abort.
Предположим, вы используете компонент TADOTable, тогда ваш код будет выглядеть следующим образом:
procedure TForm1.ADOTable1BeforeInsert(DataSet: TDataSet);
begin
Abort;
end;
Вопрос 8. Как вывести картинку в DBGrid?
Ответ. Разместите на форме компоненты TADOTable, TDataSource, TDBGrid, настройте их на работу с определенной базой. Также нам понадобится компонент
TImageList, загрузите в него любую картинку. Откройте набор данных. В нашем
примере предполагается существование логического поля REM. В свойстве
Глава 4
176
DefaultDrawing компонента TDBGrid установите False и создайте обработчик
OnDrawColumnCell:
// В данном примере картинка выводится в поле REM
procedure TForm1.DBGrid1DrawColumnCell(Sender: TObject;
const Rect: TRect;
DataCol: Integer; Column: TColumn; State: TGridDrawState);
begin
// Если обрабатывается нужное нам поле
if Column.FieldName='Rem'
then
// Рисуем самую первую картинку из TImageList (нумерация с 0)
ImageList1.Draw(TDBGrid(Sender).Canvas,Rect.Left,Rect.Top, 0)
else
// Если обрабатывается любое другое поле,
// то просто выводим его значение
TDBGrid(Sender).DefaultDrawColumnCell(Rect,DataCol,Column,State);
end;
К о м п а к т- д и с к
Пример — в Database\image_grid.
Вопрос 9. Мне необходимо вставить большое количество записей при открытом наборе данных, при этом DBGrid, с которым я работаю, начинает работать медленнее.
Такая же ситуация наблюдается, когда я пытаюсь вывести в DBGrid результат запроса, возвращающий большое количество данных. Как можно повлиять на данное положение вещей?
Ответ. Достаточно просто отключить прорисовку данных на время длительной операции. Например, далее представлен листинг, реализующий простейший пример
вставки в набор данных 1000 записей:
procedure TForm1.Button1Click(Sender: TObject);
var
i:integer;
begin
// Отключаем визуализацию
DBGrid1.Datasource.Dataset.DisableControls;
Работа с базами данных
177
// Вставляем 1000 записей
for i:=1 to 1000 do
begin
Application.ProcessMessages;
ADOTable1.Insert;
ADOTable1.Fields.Fields[1].Value:=DateToStr(Date)+' '+TimeToStr(Time);
AdoTable1.Post;
end;
// Включаем визуализацию
DBGrid1.Datasource.Dataset.EnableControls;
end;
К о м п а к т- д и с к
Пример — в Database\big_data_grid.
Вопрос 10. У меня есть таблица, содержащая значения температуры воздуха за последний месяц. Как построить график для этих данных, используя TDBChart?
Ответ. Разместите на форме компоненты TADOQuery, TDataSource, TDBGrid. В
папке с примером к вопросу имеется база данных MS Access, с которой вы можете
связать TADOQuery. Также для этого компонента вам надо написать следующий SQLзапрос:
SELECT tempreture, day FROM wheather
Свяжите компонент TADOQuery с TDataSource посредством свойства DataSource,
а также через это же свойство свяжите TDataSource с TDBGrid. Теперь разместите
компонент TDBChart (рис. 4.1).
Глава 4
178
Рис. 4.1. Работа с компонентом TDBChart
Приступим к настройке TDBChart. Произведите на нем двойной щелчок левой кнопкой мыши. В появившемся окне нажмите кнопку Add, появится окно TeeChart
Gallery (рис. 4.2).
По умолчанию выбрана диаграмма типа Line. Нас это устраивает, поэтому нажмите
кнопку OK. После чего станет доступной вкладка Series. Перейдите на нее и активизируйте подвкладку Data Source (рис. 4.3). В выпадающем списке выберите пункт
Dataset. А во втором выпадающем списке с именем Dataset выберите значение
ADOQuery1. Теперь в выпадающем списке X выберите значение day, а в выпадающем списке Y — значение tempreture. Нажмите кнопку Close. В результате диаграмма будет готова.
Запустите пример. Если вы добавите новую запись в набор данных, то автоматически
обновится и диаграмма. А теперь усовершенствуем пример, сделаем, чтобы фон диаграммы отображал нашу картинку. Разместите на форме кнопку, для которой напишите следующий код (только путь укажите к реальной картинке):
DBChart1.BackImage.LoadFromFile('1.bmp');
Работа с базами данных
179
Рис. 4.2. Выбор типа диаграммы
Рис. 4.3. Настройка диаграммы
Глава 4
180
Сделаем одно усовершенствование, а именно по щелчку мыши на диаграмме у нас
будут выводиться значения по оси y, причем следующий щелчок уберет значения.
Создайте событие OnClick для TDBChart и напишите для него следующий обработчик:
procedure TForm1.DBChart1Click(Sender: TObject);
var
i:integer;
begin
with DBChart1 do
for i := 0 to SeriesCount - 1 do
if Series[i] is TLineSeries then
Series[i].Marks.Visible := not Series[i].Marks.Visible
end;
К о м п а к т- д и с к
Пример — в Database\chart_from_sql.
Вопрос 11. Как сделать универсальный запрос, который будет выводить разное количество полей в зависимости от того, какие именно поля передал пользователь?
Ответ. Разместите на форме компоненты TADOQuery (свойство Active должно быть
установлено в False), TDataSource, TDBGrid. Свяжите их с таблицей wheather
(найти ее можно в примере к вопросу). Теперь разместите две кнопки, для первой
напишите следующий обработчик:
procedure TForm1.Button1Click(Sender: TObject);
begin
if ADOQuery1.Active then ADOQuery1.Close;
ADOQuery1.SQL.Clear;
ADOQuery1.SQL.Add(Format('select %s from wheather',['day,tempreture']));
ADOQuery1.Open;
end;
А для второй — следующий:
procedure TForm1.Button2Click(Sender: TObject);
begin
if ADOQuery1.Active then ADOQuery1.Close;
ADOQuery1.SQL.Clear;
ADOQuery1.SQL.Add(Format('select %s from wheather',['day']));
ADOQuery1.Open;
Работа с базами данных
181
end;
К о м п а к т- д и с к
Пример — в Database\sql_with_parameter.
Вопрос 12. Я бы хотел, чтобы пользователь в TStatusBar при работе с TDBGrid всегда видел номер текущей записи и общее количество данных в наборе. Как это сделать?
Ответ. Разместите на форме компоненты TADOTable, TDataSource, TDBGrid. В
папке с примером к вопросу имеется база данных MS Access, которую вы можете
использовать. Настройте связку TADOTable — TDataSource — TDBGrid. Сразу откройте набор данных. Теперь добавьте компонент TStatusBar, произведите двойной
щелчок мыши на нем и создайте две панели. Задайте для первой ширину побольше,
чтобы выводимая информация могла полностью поместиться в ней. Далее для события AfterScroll компонента TADOTable напишите следующий обработчик:
procedure TForm1.ADOTable1AfterScroll(DataSet: TDataSet);
begin
StatusBar1.Panels.Items[0].Text :=
'№ текущей записи: '+IntToStr(DBGrid1.DataSource.DataSet.RecNo);
StatusBar1.Panels.Items[1].Text :=
'Всего записей: '+IntToStr(DBGrid1.DataSource.DataSet.RecordCount);
end;
Замечание
Учтите один важный момент: если вы будете выполнять фильтрацию набора
данных, то значения могут выводиться неправильно. В данном случае лучше
использовать для получения данных SQL-запрос, либо создать отдельный набор данных, который будет подсчитывать количество записей по условию
фильтра.
К о м п а к т- д и с к
Пример — в Database\status_bar.
Вопрос 13. У меня есть три справочника, все они используют один вариант интерфейса формы, только запросы разные. Хотел бы создать универсальную форму, чтобы можно было работать с моими справочниками с помощью нее. Как лучше это
сделать?
Ответ. Пусть главная форма нашего приложения будет выглядеть так, как представлено на рис. 4.4.
Глава 4
182
Создай еще одну форму (рис. 4.5).
Рис. 4.4. Главная форма программы "Универсальный справочник"
Рис. 4.5. Форма-справочник
Настройте связку TADOTable — TDataSource — TDBGrid (базу можно найти в примере к вопросу). Свойство SQL компонента TADOQuery оставьте пустым. Теперь приведите модуль для только что созданной формы к следующему виду:
unit Unit2;
interface
uses
Windows, Messages, SysUtils, Variants, Classes, Graphics,
Controls, Forms, Dialogs, DB, ADODB, StdCtrls, Grids, DBGrids;
Работа с базами данных
183
type
TReference = array[0..2] of String;
const
// Название справочника
Zagolovok: TReference=('Люди', 'Звери', 'Машины');
// Запросы для каждого из справочников
Zapros: TReference = ('SELECT id,name FROM people',
'SELECT id,kind_of_animal FROM animal',
'SELECT id,kind_of_car FROM car');
type
TForm2 = class(TForm)
DBGrid1: TDBGrid;
ADOQuery1: TADOQuery;
DataSource1: TDataSource;
Label1: TLabel;
private
{ Private declarations }
public
{ Public declarations }
procedure name_of_table(TypeReference: integer);
end;
var
Form2: TForm2;
implementation
uses DateUtils;
{$R *.dfm}
procedure TForm2.name_of_table(TypeReference: integer);
begin
ADOQuery1.SQL.Clear;
ADOQuery1.SQL.Add(Zapros[TypeReference]);
Глава 4
184
ADOQuery1.Open;
Form2.Caption:=Zagolovok[TypeReference];
end;
end.
Как видите, мы создаем специальную процедуру name_of_table, которая будет отвечать за выборку данных для конкретного справочника, а также за заголовок формы-справочника. Теперь для кнопки, расположенной на главной форме, создайте
следующий обработчик:
procedure TForm1.Button1Click(Sender: TObject);
begin
if Form2.ADOQuery1.Active=true then Form2.ADOQuery1.Close;
Form2.name_of_table(RadioGroup1.ItemIndex);
Form2.Show;
end;
К о м п а к т- д и с к
Пример — в Database\reference_books.
Вопрос 14. Как работать с несколькими базами MS Access в одном запросе?
Ответ. Разметите на форме компоненты TADOQuery, TDataSource, TDBGrid.
В папке с примером к вопросу вы сможете найти две базы данных MS Access, посмотрите их структуру. Теперь настройте свойство Connection компонента
TADOQuery на работу с базой base1.mdb, далее настройте связку TADOQuery —
TDataSource — TDBGrid и в свойстве SQL компонента TADOQuery напишите следующий запрос:
SELECT kind_of_animal,type FROM animal
UNION
SELECT animal.kind_of_animal,type FROM animal IN base2.mdb
Первый SELECT берет данные из базы, прописанной в свойстве Connection компонента TADOQuery, второй SELECT цепляет данные из внешней базы — base2.mdb, а
UNION объединяет результаты этих двух запросов в единый набор данных.
Разместите на форме кнопку, для которой напишите следующий обработчик:
procedure TForm1.Button1Click(Sender: TObject);
Работа с базами данных
185
begin
ADOQuery1.Open;
end;
Замечание
Точно таким же образом вы можете обращаться к различным источникам данных, например, к Excel или Paradox. Правда, синтаксис запроса будет немного
различаться.
К о м п а к т- д и с к
Пример — в Database\data_from_outside.
Вопрос 15. Как программно сжать и восстановить базу данных MS Access
(т. е. необходимо программно выполнить действие, вызываемое пунктом меню Сервис | Служебные программы | Сжать и восстановить базу данных данной
СУБД)?
Ответ. Создайте следующую форму (рис. 4.6).
Рис. 4.6. Главная форма к примеру COMPRESS_BASA
Теперь измените модуль формы в соответствии со следующим листингом:
unit Unit1;
interface
uses
Глава 4
186
Windows, Messages, SysUtils, Variants, Classes, Graphics,
Controls, Forms, Dialogs, StdCtrls, ComObj;
type
TForm1 = class(TForm)
Button1: TButton;
Edit1: TEdit;
Label1: TLabel;
Label2: TLabel;
Label3: TLabel;
Label4: TLabel;
Label5: TLabel;
procedure Button1Click(Sender: TObject);
private
{ Private declarations }
public
{ Public declarations }
end;
var
Form1: TForm1;
implementation
{$R *.dfm}
function razmer_of_file:string;
var f:TFileStream;
razmer:int64;
full_path:string;
begin
try
full_path:=ExtractFileDir(Application.ExeName)+'\'+Form1.Edit1.Text;
f:=TFileStream.Create(full_path,fmOpenRead);
Работа с базами данных
187
except
ShowMessage('Такой файл не существует!');
exit;
end;
razmer:=f.Size;
f.Free;
Result:=IntToStr(razmer)+' байт';
end;
procedure CompactDatabase(DatabaseName: string; Password: string = '');
const
Provider = 'Provider=Microsoft.Jet.OLEDB.4.0;';
TempFile='temp.mdb';
var
FullTempFileName: string;
Src, Dest: WideString;
V: Variant;
begin
try
Src := Provider + 'Data Source=' + DatabaseName;
if DatabaseName='' then
begin
ShowMessage('Не указано имя базы данных,
выполнение процедуры приостановлено.');
exit;
end;
FullTempFileName := ExtractFileDir(Application.ExeName)+'\'+TempFile;
// Этот файл не должен существовать
DeleteFile(PChar(FullTempFileName));
Dest := Provider + 'Data Source=' + FullTempFileName;
if Password <> '' then
begin
Download