Сергей Хуторцев aka Linco
Сколько я себя помню программистом, в большинстве случаев писал программы, работающие с Базами данных (приложения БД). Причиной этому возможно послужил тот факт, что Базы Данных, как универсальные хранилища информации используются везде, начиная от хранения сведений о клиентах в крупных корпорациях и банках, кончая списками продукции в магазинах и документацией в любой бухгалтерии. И теперь хочется поделиться своими мыслями и наработками по поводу проектирования приложений БД. Собственно не надо быть семи пядей во лбу, чтобы сварганить простенькую программу с использованием Баз Данных, благо все средства для этого в Delphi есть, все-таки RAD. Многие ругают Delphi вообще и Delphi'стов в частности именно за это: "Дескать, любой ламер может взять нужный компонент, набросать на форму контролов, и получить готовое и РАБОТАЮЩЕЕ приложение". Хочется возразить, Delphi только среда, и что получится на выходе у программиста, зависит только от кривизны его рук, наличия знаний и желания. Мы "варганить" не будем, подойдем к процессу со всей ответственностью.
Итак, схема проектирования:
- Сбор информации. Вам необходимо знать все, что хотят пользователи, заказчик или Ваше руководство от этой системы. В цивилизованном обществе принято давать разработчику ТЗ, а также разделять программистов-аналитиков, проектирующих систему, от просто-программистов, пишущих код, и тем более различать администраторов и разработчиков Баз Данных. Однако нам до такого, как пешком до луны, поэтому программист должен быть "все-в-одном-флаконе", а вместо ТЗ мы получаем сомнительные руководства типа: "Хочу чтоб она все делала все, а я бы сидел в уютном кресле, чесал правую пятку, и отдавал мысленные приказания". Причем часто руководства письменные. Добро пожаловать новый пациент. На поиск приемлемого компромисса уходит определенное время. Также нелишним будет изучить конкурирующие системы, системы с похожей функциональностью, тут Google рулит.
- Выбор платформы. Включает в себя как выбор железа, в соответствии с планируемой нагрузкой на БД с учетом масштабируемости, так и выбор СУБД. Существует множество критериев, и для каждого они свои. Для кого-то важна цена/бесплатность продукта, для кого-то производительность. Однако нужно реально оценивать возможности СУБД и не использовать Oracle, если Ваша таблица за 2 года вырастет на 100000 записей. Или не использовать Access если…., ну вообще не использовать Добавьте к этому затраты на администрирование БД. Здесь главное иметь представление о том, что вы собираетесь сделать, и какой результат хотите получить, а также о возможностях различного железа и СУБД. Некоторые запущенные случаи требуют применения не клиент-сервер, а трехзвенки, что также надо учесть Для себя я давно выбрал Firebird, как мощную масштабируемую систему корпоративного уровня, удобную и легкую как по весу так и в эксплуатации/администрировании.
- Грамотное проектирование структуры БД, с максимальным вынесением логики работы на уровень сервера БД. Ибо зачем делать лишнюю работу на клиенте, если она лучше и быстрее сделается на сервере. Плюс унифицированность системы. Чем грамотней и продуманней начальная структура БД, тем меньше геморроя мы получаем на следующих этапах. Да и расходы на поддержку существенно уменьшаются. Здесь необходим опыт. Если программист разбирается в Oracle не факт, что он также качественно и сходу разберется, например, в Interbase/Firebird. У всех свои особенности работы, а знание особенностей приходит с опытом работы. И неважно каким образом осуществляется проектирование, с использованием технических средств типа ErWin и иже с ним, или на бумажке карандашиком, главное вcе равно в голове.
- Собственно проектирование и разработка интерфейса к БД. Ни один пользователь никогда не полезет в дебри утилит администрирования БД, а тем более не будет использовать 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, кое-что исправит. Но решение хорошее и с успехом работает во многих моих проектах.