Глава 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