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

Источник: delphiplus
Сергей Хуторцев aka Linco

Сколько я себя помню программистом, в большинстве случаев писал программы, работающие с Базами данных (приложения БД). Причиной этому возможно послужил тот факт, что Базы Данных, как универсальные хранилища информации используются везде, начиная от хранения сведений о клиентах в крупных корпорациях и банках, кончая списками продукции в магазинах и документацией в любой бухгалтерии. И теперь хочется поделиться своими мыслями и наработками по поводу проектирования приложений БД. Собственно не надо быть семи пядей во лбу, чтобы сварганить простенькую программу с использованием Баз Данных, благо все средства для этого в Delphi есть, все-таки RAD. Многие ругают Delphi вообще и Delphi'стов в частности именно за это: "Дескать, любой ламер может взять нужный компонент, набросать на форму контролов, и получить готовое и РАБОТАЮЩЕЕ приложение". Хочется возразить, Delphi только среда, и что получится на выходе у программиста, зависит только от кривизны его рук, наличия знаний и желания. Мы "варганить" не будем, подойдем к процессу со всей ответственностью.

Итак, схема проектирования:

  1. Сбор информации. Вам необходимо знать все, что хотят пользователи, заказчик или Ваше руководство от этой системы. В цивилизованном обществе принято давать разработчику ТЗ, а также разделять программистов-аналитиков, проектирующих систему, от просто-программистов, пишущих код, и тем более различать администраторов и разработчиков Баз Данных. Однако нам до такого, как пешком до луны, поэтому программист должен быть "все-в-одном-флаконе", а вместо ТЗ мы получаем сомнительные руководства типа: "Хочу чтоб она все делала все, а я бы сидел в уютном кресле, чесал правую пятку, и отдавал мысленные приказания". Причем часто руководства письменные. Добро пожаловать новый пациент. На поиск приемлемого компромисса уходит определенное время. Также нелишним будет изучить конкурирующие системы, системы с похожей функциональностью, тут Google рулит.
  2. Выбор платформы. Включает в себя как выбор железа, в соответствии с планируемой нагрузкой на БД с учетом масштабируемости, так и выбор СУБД. Существует множество критериев, и для каждого они свои. Для кого-то важна цена/бесплатность продукта, для кого-то производительность. Однако нужно реально оценивать возможности СУБД и не использовать Oracle, если Ваша таблица за 2 года вырастет на 100000 записей. Или не использовать Access если…., ну вообще не использовать Добавьте к этому затраты на администрирование БД. Здесь главное иметь представление о том, что вы собираетесь сделать, и какой результат хотите получить, а также о возможностях различного железа и СУБД. Некоторые запущенные случаи требуют применения не клиент-сервер, а трехзвенки, что также надо учесть Для себя я давно выбрал Firebird, как мощную масштабируемую систему корпоративного уровня, удобную и легкую как по весу так и в эксплуатации/администрировании.
  3. Грамотное проектирование структуры БД, с максимальным вынесением логики работы на уровень сервера БД. Ибо зачем делать лишнюю работу на клиенте, если она лучше и быстрее сделается на сервере. Плюс унифицированность системы. Чем грамотней и продуманней начальная структура БД, тем меньше геморроя мы получаем на следующих этапах. Да и расходы на поддержку существенно уменьшаются. Здесь необходим опыт. Если программист разбирается в Oracle не факт, что он также качественно и сходу разберется, например, в Interbase/Firebird. У всех свои особенности работы, а знание особенностей приходит с опытом работы. И неважно каким образом осуществляется проектирование, с использованием технических средств типа ErWin и иже с ним, или на бумажке карандашиком, главное вcе равно в голове.
  4. Собственно проектирование и разработка интерфейса к БД. Ни один пользователь никогда не полезет в дебри утилит администрирования БД, а тем более не будет использовать SQL для получения или изменения каких-либо данных. Тут существует обратно-пропорциональная зависимость: чем универсальней программное средство, тем тяжелей оно в понимании и эксплуатации. Пользователю надо дать интерфейс, причем интерфейс довольно узкоспециализированный. Т.е. отрезать, разжевать и положить в рот необходимую ему информацию. Причем, в большинстве своем пользователи хотят чтобы все делалось при их минимальном участии, ну в крайнем случае согласны нажимать одну кнопку. Будучи главой компании, занимающейся разработкой такого программного обеспечения, или хотя бы начальником отдела кадров, я все-таки попытался бы совместить разработчика БД с программистом. Если в силу особой сложности проекта или иных технических причин это невозможно, то программист должен максимально тесно контактировать с разработчиком БД и ясно для себя представлять ее структуру. Не факт, что идеальная структура БД, которую с такой гордостью вчера представляли Вам, не заставит программиста рвать и метать, поскольку будет чрезвычайно тяжело реализовываться в программе, поддерживаться и масштабироваться. При проектировании интерфейса также можно выделить несколько этапов:
    • Примерно представить как все это должно выглядеть и какую функциональность выдавать пользователям. Исходя из этого, определиться с минимально необходимым набором компонентов для реализации. Из моего опыта работы, а также из общения с умными людьми были выделены несколько проектов: Поскольку работаю с Firebird, это FibPlus - лучшие компоненты доступа на сегодняшний момент, FastReport - лучший генератор отчетов, в качестве визуальных компонентов: таблица и часть контролов - EhLib, дерево VirtualTreeView, TBX toolbar для красоты, ну и JVCL, как бесплатный и огромный набор различных компонентов заменяющих и расширяющих VCL. Также полезно иметь наборы красивых картинок для кнопок, ибо ничто так не радует пользователей как красивые заставки и картинки.
    • Найти, скачать, купить данные компоненты.
    • Создать программу

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

Лично для меня в написании приложений БД есть несколько сложностей, решение которых я и хочу подсказать. Самое тяжелое это конечно рутина. Во всех пунктах нашей схемы есть элементы творчества: общение с пользователями-пациентами и с продвинутыми пользователями, поиск аналогов программы в сети, обдумывание различных функций и фишек будущей системы, анализ и подбор железа (чаще всего сводится к принципу "бери-что-есть, новое нини"), проектирование структуры БД и описание бизнес-логики, и наконец, само программирование. Рутина - это минимальный необходимый набор действий для обеспечения работы приложения, т.е. до получения какого-либо результата. Когда знаешь в точности, как и что ты должен сделать, и все это проще простого, но приходится делать это десятки и сотни раз. Там, где отсутствует творчество. Боитесь?, я нет, ибо сегодня наш рассказ о том, как побороть одно из ее проявлений, а именно о проектировании универсальных форм-справочников.

Любая БД имеет много объектов внутри, таблиц, триггеров, процедур и другого барахла, о котором рядовому пользователю знать вовсе не обязательно. Исключим клинические случаи, которые недостойны гордого звания Базы данных, когда студент-недоучка делает на Access базу данных, содержащую одну таблицу со списком учащихся группы. Почти всегда в нормальной БД есть группа вспомогательных таблиц, созданных, например, для поддержания целостности данных, или для более полного охвата предметной области. Единственной функцией таких таблиц может быть просто подстановка значений в основную таблицу. Простой пример, телефонный справочник, в простейшем тривиальном случае он представляет собой таблицу соответствия город-абонент-номер. Поскольку один город соответствует множеству абонентов, можно и нужно вынести список городов в отдельную таблицу, таблицу-справочник, а в основной таблице оставить просто цифру которая будет указывать на город, и связать их по ForeignKey. Таким образом убиваем сразу десять зайцев ракетой: получаем нормализацию БД, экономия места в основной таблице(вместо города пишем цифру), при добавлении нового телефонного номера он выбирается из списка, одинаковое написание всех городов (к сожалению многие пользователи страдают хронической неграмотностью, да еще и требуют чтобы при поиске/выборке не было упущено ни одного значения, а программист выкручивайся, описывай методы нечеткой логики поиска, да еще в SQL переводи, чтобы находило и Москва и Масква, Мсква). Не нужно приписывать мне столь гениальное решение, это один из стандартных принципов проектирования БД. К городу можно добавить ряд дополнительных полей, например, население города, чтоб знать каков процент абонентов, код города и т.п. Хорошо если таблица-справочник одна или несколько, а если довольно много? В построении интерфейса к ней нет ничего особенно сложного, обычно такие таблицы редко редактируются или дополняются. Он сводится к таблице, как вариант группе DataAware контролов, средствам навигации и управления данными, поиску, выборке, и составлению отчетов. Само по себе все это сложности не представляет, однако повторенное двадцать раз начинает бесить. Во многих случаях даже визуальный ряд интерфейсов к разным таблицам полностью идентичен. Программисты, как известно, народ очень ленивый, разработка программы идет постепенно, делая интерфейс к одной таблице, еще не представляют как будет выглядеть интерфейс к другой, а когда делают второй - видят, что все идет идентично, но писать что-то универсальное лень, во-первых, copy-paste легче, во-вторых, придется исправлять и первый, рабочий уже интерфейс. На третьем и последующих интерфейсах приходит та самая рутина. И вот однажды, победив в героическом сражении лень, да и таблиц-справочников было много, решил написать универсальное решение, на основе которого можно было бы легко и просто добавлять интерфейсы к новым таблицам в приложение. Сразу оговорюсь, я привожу лишь одно из возможных решений, для очень ленивых программистов, кто по каким-либо причинам еще не добрался или не сподобился до решения проблемы. Наверняка у многих, кто работает с БД, подобные решения есть. Сначала была попытка пойти по пути наименьшего сопротивления, Delphi позволяет создавать полного наследника формы, т.е. не только ее свойств и методов, но также и визуального ряда. Но решение оказалось абсолютно немасштабируемым, убрать или добавить что-либо в наследник оказалось практически невозможно. И было решено оставить внешний вид интерфейса на откуп программисту и сосредоточиться на кодировании.

Итак, что мы имеем с гуся? Необходима универсальная форма-интерфейс к определенной таблице БД, с возможностями навигации, добавления, изменения, удаления записей, выборки/поиска нужных записей, составления по ним отчетов, определенного управления отчетами, с реакцией на изменения данных. Реализовывать будем в три этапа (каждый этап в отдельном модуле, чтобы эффективней использовать решения по отдельности): 1. Поскольку это форма, наследуем от Tform, а также вынесем в этот модуль все, что касается ее внешнего вида и поведения. В нашем случае форма должна отображаться как отдельно, так и внутри Twincontrol контейнера. Для этого используем CreateParams. Мы хотим добиться от формы определенной функциональности, поэтому воспользоваться Tform.CreateParented не сможем.

unit childform;

interface

uses windows,forms,classes,controls;

type
Tchildform = class(TForm)
     procedure FormClose(Sender: TObject; var Action: TCloseAction);
   private
     Faschild:boolean;
     Fparent:Twincontrol;
   protected
     procedure CreateParams(var params:Tcreateparams); override;
     procedure Loaded; override;
   public
     constructor Create(Aowner:Tcomponent); reintroduce; overload;
     constructor Create(Aowner:Tcomponent; Aparent:Twincontrol);reintroduce; overload;
   end;

implementation

{ Tchildform }

constructor Tchildform.Create(Aowner: Tcomponent);
begin
   Faschild := false;
   inherited Create(Aowner);
end;

constructor Tchildform.Create(Aowner: Tcomponent; Aparent: Twincontrol);
begin
   Faschild:=true;
   Fparent:=Aparent;
   inherited Create(Aowner);
end;

procedure Tchildform.CreateParams(var params: Tcreateparams);
begin
   inherited CreateParams(params);
   if Faschild then
     params.Style:=params.Style or WS_CHILD;
end;

procedure Tchildform.FormClose(Sender: TObject; var Action: TCloseAction);
begin
   action:=cafree;
end;

procedure Tchildform.Loaded;
begin
   //параметры отображения формы
   inherited;
   if Faschild then
     begin
       align:=alclient;
       borderstyle:=bsnone;
       bordericons:=[];
       Parent:=Fparent;
       Position:=poDefault;
     end
   else
     begin
       position:=pomainformcenter;
       borderstyle:=bsdialog;
     end;
end;

end.

2. Наследуем от нашей формы. Во-первых, привяжем нашу форму к определенному набору данных, таблице. Во-вторых, реализуем все необходимые нам общие методы для работы с данными. В основном эти методы итак присутствуют в любом наследнике Tdataset, однако в ряде случаев нам необходимо провести некоторые подготовительные мероприятия перед их непосредственным применением. Например, уточнить у пользователя действительно ли он хочет удалить записи. Одновременно с этим мы получаем возможность централизованного управления и выполнения дополнительных действий. Для моего случая метод Delete вообще был заменен собственной формой, демонстрирующей пользователю все зависимые по FK CASCADE DELETE записи в других таблицах (они также были бы удалены, Firebird). В-третьих, реализуем методы составления и управления отчетами. Также сделаем нашу форму частично Dataaware, чтобы иметь возможность реагировать на события, происходящие с данными. Например, можем управлять доступностью кнопок сохранения записи, т.е. делать ее активной только в случае изменения данных. Методы поиска и фильтрации добавлять сюда не стал, поскольку в большинстве своем там очень важна визуальная реализация режима поиска. Например, в ehgrid выделение и фильтрация реализованы отлично. Так же, как пример, можно посмотреть реализацию организации фильтрации в DeveloperExpress grid. Там реализована древовидная структура фильтров. Для поддержки выборки части записей введен Bookmarklist:Tstringlist, содержащий список закладок выделенных записей, по такому принципу работает ehgrid, и реализация метода FastReport beforeprint обеспечивающая вывод только записей списка.

unit dbforms;

interface

uses windows,forms,controls,classes,db, dialogs,
   sysutils, childform,
   frxDesgn, frxDCtrl, frxClass, frxDBSet,
   frxExportHTML,frxExportRTF,frxExportPDF;

type

   TExportFilter=(EF_RTF, EF_PDF, EF_HTML );

   TMyDataLink=class(TDatalink)
   private
     Fediting:boolean;
     Fmodified:boolean;
     FonEditingchange:TnotifyEvent;
     procedure setediting(value:boolean);
   public
     procedure EditingChanged; override;
     property OnEditingChange: TNotifyEvent read FOnEditingChange write FOnEditingChange;
   end;

   TcustomDBForm =class(Tchildform)
   private
     FreportCreated:boolean;
     Fdatachange:Tnotifyevent;
     Fdatalink:Tmydatalink;
     Fdatasource:Tdatasource;
     Fbookmarklist:Tstringlist;
     Fcursor:Tbookmark;
     Fopendlg:Topendialog;
     FfrxDS:TfrxDBdataset;
     FfrxReport:TfrxReport;
     FfrxRTFExport: TfrxRTFExport;
     FfrxHTMLExport: TfrxHTMLExport;
     FfrxPDFExport: TfrxPDFExport;
     procedure frx_beforeprint(Sender: TfrxReportComponent);
     procedure DoOndatachange(Sender:Tobject);
   protected
     procedure savecursor;
     procedure gotocursor;
     procedure first;
     procedure last;
     procedure add;
     procedure delete;
     function applydata:boolean;
     procedure canceldata;
     procedure initfastreport;
     procedure freefastreport;
     function GetReportInitialDir:string;
     function GetReportName:string;
     procedure createreport(bookmarklist:Tstringlist=nil);
     procedure DesignReport(bookmarklist:Tstringlist=nil);
     procedure ExportReport(Exportfilename:string=''; filter:TExportfilter=EF_RTF);
   public
     constructor Create(Aowner:Tcomponent; DS:Tdatasource); reintroduce; overload;
     constructor Create(Aowner:Tcomponent; Aparent:Twincontrol;DS:Tdatasource); reintroduce; overload;
     destructor Destroy; override;
   published
     property OnDataChange:Tnotifyevent read Fdatachange write Fdatachange;
     property DataSource:Tdatasource read Fdatasource write Fdatasource;
     property ReportCreated:boolean read Freportcreated;
     property Report:Tfrxreport read Ffrxreport;
   end;

implementation

{ TcustomDBForm }

procedure TcustomDBForm.add;
begin
   //переписка всех этих простых методов управления Dataset дает нам возможность осуществления
   //определенного комплекса действий, например управления и контроля за транзакциями в Firebird
   // это мы увидим в реализации наследника этого класса
   Fdatasource.DataSet.Append;
end;

procedure TcustomDBForm.delete;
begin
   //диалог удаления в более сложных случаях можно вставить
   //сложный диалог, например с анализом зависимостей foreign keys
   if messagedlg('Удалить запись?', mtWarning,[mbYes,MbNo],0)=mrYes
     then Fdatasource.dataset.Delete;
end;

procedure TcustomDBForm.first;
begin
   Fdatasource.DataSet.First;
end;

procedure TcustomDBForm.last;
begin
   Fdatasource.DataSet.Last;
end;

procedure TcustomDBForm.savecursor;
begin
   //перед любыми действиями требующими движения курсора таблицы
   //(например составление отчета) сохраняем наше местоположение
   Fcursor:=Fdatasource.DataSet.GetBookmark;
end;

procedure TcustomDBForm.gotocursor;
begin
   // восстанавливаем положение курсора
   with Fdatasource.dataset do
     if BookmarkValid(Fcursor)
       then GotoBookmark(Fcursor);
end;

function TcustomDBForm.applydata: boolean;
begin
   if Fdatasource.DataSet.State in [dsedit,dsinsert] then
     try
       Fdatasource.DataSet.Post;
       result:=true;
     except
       result:=false;
     end;
end;

procedure TcustomDBForm.canceldata;
begin
   Fdatasource.DataSet.cancel;
end;

constructor TcustomDBForm.Create(Aowner: Tcomponent; DS: Tdatasource);
begin
   //заполняем и инициализируем необходимые поля
   if DS=nil then
     raise Exception.Create('Невозможно создать форму без привязки к данным.');

   Fdatasource:=DS;
   //здесь создаем Tmydatalink для реализации событий изменения данных таблицы
   Fdatalink:=Tmydatalink.create;
   Fdatalink.OnEditingChange:=Doondatachange;
   Fdatalink.DataSource:=Fdatasource;

   initfastreport;
   inherited Create(Aowner);
end;

constructor TcustomDBForm.Create(Aowner: Tcomponent; Aparent: Twincontrol; DS: Tdatasource);
begin
   if DS=nil then
     raise Exception.Create('Невозможно создать форму без привязки к данным.');

   Fdatasource:=DS;
   Fdatalink:=Tmydatalink.create;
   Fdatalink.OnEditingChange:=DoOndatachange;
   Fdatalink.DataSource:=Fdatasource;

   initfastreport;
   inherited Create(Aowner,Aparent);
end;

destructor TcustomDBForm.Destroy;
begin
   Fdatalink.Free;
   freefastreport;
   inherited;
end;

procedure TcustomDBForm.DoOndatachange(Sender: Tobject);
begin
   //событие изменения данных
   if assigned(Fdatachange) then
     Fdatachange(Self);
end;

procedure TcustomDBForm.ExportReport(Exportfilename: string; filter: TExportfilter);
var    FN:string;
begin
   //прямой экспорт отчета

   //проверяем создан отчет или нет
   if not(reportcreated) then
     createreport; //если нет создаем

   if exportfilename<>'' then
     begin
       if extractfilename(exportfilename)=exportfilename
         then FN:=getreportinitialdir+exportfilename
         else Fn:=exportfilename;
     end
   else
     with Tsavedialog.Create(self) do
       try
         initialdir:=getreportinitialdir;
         if execute then FN:=filename;
       finally
         free;
       end;

   if fn='' then exit;
   //экспортируем в зависимости от фильтра экспорта
   case filter of
     EF_RTF: begin
       FfrxRTFExport.FileName:=FN;
       FfrxRTFExport.defaultpath:=extractfilepath(FN);
       Ffrxreport.Export(Ffrxrtfexport);
     end;
     EF_PDF: begin
       FfrxPDFExport.FileName:=FN;
       FfrxPDFExport.defaultpath:=extractfilepath(FN);
       Ffrxreport.Export(FfrxPDFexport);
     end;
     EF_HTML: begin
       FfrxHTMLexport.filename:=FN;
       FfrxHTMLExport.defaultpath:=extractfilepath(FN);
       Ffrxreport.Export(FfrxHTMLexport);
     end;
   end;

end;

procedure TcustomDBForm.createreport(bookmarklist: Tstringlist);
begin
   //создаем отчет здесь знатоки FR могут меня поправить
   //буду только рад, однако приведенная схема работает на ура
   //опция: выбор только существующих файлов
   Fopendlg.Options:=Fopendlg.Options+[offilemustexist];

   if Fopendlg.Execute then
   begin
     savecursor;
     //загружаем шаблон
     Ffrxreport.LoadFromFile(Fopendlg.FileName);
     //проверяем соответствует ли создаваемый отчет Dataset'у
     //для этого при создании отчета пишем в Dataset.name
     if Ffrxreport.ReportOptions.Name<>getreportname
       then
         if messagedlg('Внимание! Данный отчет не предназначен для этих данных. Все равно открыть?',mtwarning,[mbYes,mbNo],0)=mrNo
           then exit;
     //Если открыли, значит он уже для других данных ставим метку
     Ffrxreport.ReportOptions.Name:=getreportname;
     //добавляем dataset в список доступных в отчете
     if Ffrxreport.Report.DataSets.Find(FfrxDS)=nil
       then Ffrxreport.Report.DataSets.Add(FfrxDS);
     Ffrxreport.Report.DataSet:=nil;

     //bookmarklist - список выделенных записей для отчета
     //в ehgrid такой есть по умолчанию, в иных случаях можете создать и заполнить его сами
     if bookmarklist<>nil
       then Fbookmarklist.Assign(bookmarklist)
       else Fbookmarklist.Clear;

     Freportcreated:=true;
     Ffrxreport.ShowReport(true);
     gotocursor;
   end;
end;

procedure TcustomDBForm.DesignReport(bookmarklist: Tstringlist);
begin
   //создание нового отчета
   //включаем возможность создания нового имени файла
   //остальное по аналогии
   Fopendlg.Options:=Fopendlg.Options-[offilemustexist];
   if Fopendlg.Execute then
   begin
     savecursor;      if fileexists(Fopendlg.FileName) then
       begin
         Ffrxreport.LoadFromFile(Fopendlg.FileName);
         if Ffrxreport.ReportOptions.Name<>getreportname
           then
             if messagedlg('Внимание! Данный отчет не предназначен для этих данных. Все равно открыть?',mtwarning,[mbYes,mbNo],0)=mrNo
               then exit;
       end
     else
       begin
         Ffrxreport.ReportOptions.Name:=getreportname;
         if Ffrxreport.Report.DataSets.Find(FfrxDS)=nil
           then Ffrxreport.Report.DataSets.Add(FfrxDS);

         Ffrxreport.SaveToFile(Fopendlg.filename);
         Ffrxreport.loadfromfile(Fopendlg.filename);
     end;

   Ffrxreport.Report.DataSet:=nil;

   if bookmarklist<>nil then
     Fbookmarklist.Assign(bookmarklist)
     else Fbookmarklist.Clear;

   Ffrxreport.DesignReport;
   gotocursor;
end;
end;

procedure TcustomDBForm.initfastreport;
begin
   // инициализация отчета
   Fopendlg:=Topendialog.Create(self);
   Fopendlg.Filter:='Файлы отчета(*.fr3)/*.fr3';
   Fopendlg.DefaultExt:='fr3';
   Fopendlg.InitialDir:=GetReportInitialDir;

   Fbookmarklist:=Tstringlist.Create;

   Ffrxreport:=Tfrxreport.Create(self);
   Ffrxreport.OnBeforePrint:=frx_beforeprint;
   Ffrxreport.EngineOptions.DoublePass:=true;

   //userdataset/dbdataset
   FfrxDS:=TfrxDBdataset.Create(self);
   FfrxDS.DataSource:=Fdatasource;
   //имя локального датасета отчета
   FfrxDS.Name:='LocalfrxDS';
   FfrxDS.UserName:='LocalFRXDS';

   //создаем экспорты
   Ffrxrtfexport:=Tfrxrtfexport.Create(self);
   Ffrxhtmlexport:=Tfrxhtmlexport.Create(self);
   Ffrxpdfexport:=Tfrxpdfexport.Create(self);

   Fbookmarklist:=Tstringlist.create;
end;

procedure TcustomDBForm.freefastreport;
begin
   Fbookmarklist.Free;
   Ffrxrtfexport.Free;
   Ffrxhtmlexport.Free;
   Ffrxpdfexport.Free;
   Fopendlg.Free;
   Ffrxreport.Free;
   FfrxDS.Free;
end;

procedure TcustomDBForm.frx_beforeprint(Sender: TfrxReportComponent);
   function findbookmark(BM:string):boolean;
   begin
     result:=Fbookmarklist.IndexOf(BM)<>-1;
   end;
   procedure SHobjects(show:boolean);
   var i:integer;
   begin
     for i:=0 to Tfrxdataband(sender).Objects.count-1 do
       Tfrxcomponent(Tfrxdataband(sender).Objects.Items[i]).visible:=show;
   end;
  //mainbody====
  //вариант фильтрации отчета по содержимому
  //при генерации отчета все датазависимые компоненты не входящие в список
  //bookmarklist не отображаются
begin
   if Fbookmarklist.count>0 then
     if (sender is Tfrxdataband)
       or (sender is Tfrxgroupheader)
       or (sender is Tfrxgroupfooter)
         then sender.Visible:=findbookmark(Fdatasource.dataset.fieldbyname('ID').AsString);
end;

function TcustomDBForm.GetReportInitialDir: string;
begin
   //функция определяет директорию в которую будут писаться отчеты
   result:=extractfilepath(paramstr(0));
end;

function TcustomDBForm.GetReportName: string;
begin
   result:=Fdatasource.DataSet.Name;
end;

{ TMyDataLink }

procedure TMyDataLink.EditingChanged;
begin
   SetEditing(inherited Editing);
end;

procedure TMyDataLink.setediting(value: boolean);
begin
   if FEditing <> Value then
     begin
       FEditing := Value;
       FModified := False;
       if Assigned(FOnEditingChange) then FOnEditingChange(Self);
     end;
end;

end.

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

unit fibdbform;

interface

uses dbforms, pfibdataset,classes,db,controls,sysutils;

type
   TFIBDBForm=class(TcustomDBForm)
   protected
     function applydata:boolean;
     procedure canceldata;
   public
     constructor Create(Aowner:Tcomponent; DS:Tdatasource); reintroduce; overload;
     constructor Create(Aowner:Tcomponent; Aparent:Twincontrol;DS:Tdatasource); reintroduce; overload;
   end;

implementation

{ TFIBDBForm }

function TFIBDBForm.applydata: boolean;
begin
   with (datasource.dataset as Tpfibdataset) do
     try
       inherited applydata;
       if UpdateTransaction.InTransaction then updateTransaction.Commit;
       result:=true;
     except
       updatetransaction.Rollback;
       result:=false;
     end;
end;

procedure TFIBDBForm.canceldata;
begin
   with (datasource.dataset as Tpfibdataset)do
     begin
     inherited canceldata;
     if UpdateTransaction.InTransaction then UpdateTransaction.Rollback;
     fullrefresh;
   end;
end;

constructor TFIBDBForm.Create(Aowner: Tcomponent; DS: Tdatasource);
begin
   if not (DS.DataSet is Tpfibdataset) then
     raise Exception.create('Данный класс может использоваться только с FIB+ Dataset');
   inherited Create(Aowner,DS);
end;

constructor TFIBDBForm.Create(Aowner: Tcomponent; Aparent: Twincontrol; DS: Tdatasource);
begin
   if not (DS.DataSet is Tpfibdataset) then
     raise Exception.create('Данный класс может использоваться только с FIB+ Dataset');
   inherited Create(Aowner,Aparent,DS);
end;

end.

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


Страница сайта http://www.interface.ru
Оригинал находится по адресу http://www.interface.ru/home.asp?artId=20733