FastReport. Истинный менеджер отчетов.
Хранение, отображение и использование отчетов в БД

© 2008 Сергей Плахов

Предисловие

Наверно нет нужды представлять генератор отчетов FastReport. Как человек, работающий с генератором с версии 2 , могу сказать, что он получился весьма удачным продуктом. Тому подтверждение - многочисленные награды. В своем развитии FastReport прошел путь от простого набора VCL-компонентов до HTTP-сервера, породив ряд сопутствующих технологий, например скриптовый язык (FastScript).

Как все начиналось...

Собственно на данную тему меня подвигла статья Савельева Андрея "Менеджер отчётов" на DelphiPlus.org. Увидел я в ней уже пройденный мною путь, ну и для того чтоб не все наступили на мои грабли, написаны эти строки.

Давным давно был запущен некий проект для работы в локальной сети. Все писалось на Delphi и Interbase. Требовалось большое количество отчетов с довольно сложной иерархической структурой. Поставляемый в комплекте с Delphi генератор отчетов оставлял желать лучшего. И тут как нельзя кстати оказался FastReport, тогда еще версии 2.5. Разработанные тогда отчеты сохранялись в файлы, и вся структура первоначально основывалась на каталогах и файлах.

Вся прелесть при использовании FastReport проявлялась в том, что его отчеты являлись автономными объектами ни коим образом независящие от исполняемых файлов проекта. Таким образом, структуру отчетом можно было изменять и дополнять, не трогая само приложение. Дальше речь пойдет именно о так называемых автономных отчетах.. Не вижу достаточно веских причин (при современном состоянии Fastreport) включать отчеты, которые производят выборки из БД, в исполняемые файлы (в том числе и во всякие библиотеки BPL, DLL и пр.).

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

Решение первой проблемы (хранение и обновление отчетов) подсказала документация очередного релиза FastReport'а. Приложение работало с БД, загнали туда же отчеты. В той версии FR существовал и метод TfrReport.LoadFromBlobField. Непонятно почему команда FastReport'а от него отказалась в последующих версиях.

Решение второй проблемы (отображение, навигация и поиск) органично вытекало из первого. Отчеты в базе в виде "дерева" - отобразим их в "дереве". Не буду описывать, что и как делалась в процессе эволюции продукта, опишу существующее положение. Исходники и демку приложу в архиве. Для компиляции понадобятся Delphi 7, собственно FastReport 4-ой версии, FIBPlus и библиотека EhLib. Все компоненты можно найти на сайтах разработчиков. Ну и база естественно реализована на сервере Firebird версии 1.5. На всякий пожарный добавил в архив примеров скомпилированный файл. Менеджер не лишен недостатков, Однако есть множество путей достижения цели, я прошел таким. Попробуйте найти свой.

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

И так, начнем...

Хранение

Построена вот такая структура в БД.

Она состоит из двух основных объектов таблицы и просмотра.

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

Для подробного знакомства с таблицей REPORT смотрите БД. Описание полей представлено в таблице. Связь ID_PARENT – ID_REPORT построена на триггерах, а не на внешнем ключе. Есть еще пара хранимых процедур, они выполняют вспомогательные роли.

/* Выдает полное имя отчета включая все категории */
CREATE PROCEDURE GET_FULLNAME_REPORT (
id_report integer, -- ID отчета или категории
separator varchar(10)) –- Разделитель (сепаратор) между именами категорий
returns (full_name varchar(5000))
as
declare variable id_parent integer;
declare variable name_level varchar(100);
begin
if (separator is null) then
separator = '/'; -- значение сепаратора по умолчанию
-- текущей узел
for
select r.id_report, r.id_parent, r.report_name
from report r
where r.id_report = :id_report
into :id_report, :id_parent, :full_name
do
begin
-- очередной узел, только если найден текущий
execute procedure get_fullname_report(:id_parent, :separator) returning_values :name_level;
-- добавление имени очередного узла
full_name = coalesce(:name_level||:separator, '')||:full_name;
end
-- вывод
suspend;
end

/* Получить ID_REPORT по полному имени отчета (обратная GET_FULLNAME_REPORT) */
CREATE PROCEDURE GET_ID_REPORT (
full_name varchar(500), -- полное имя отчета
separator char(1)) -- –- Разделитель (сепаратор) между именами категорий
returns (id_report integer)
as
begin
if (separator is null) then
separator = '/'; -- значение сепаратора по умолчанию
select r.id_report from report r
where
exists(select * from get_fullname_report(r.id_report, :SEPARATOR) g
where upper(g.full_name) = upper(:FULL_NAME))
into :id_report;
suspend;
end

Более подробные детали смотрите в БД. Все объекты там имеют описание.

ПолеТипОписание
ID_REPORTINTEGERИдентификационный номер записи (первичный ключ на генераторе)
REPORT_BODYBLOB SUB_TYPE 0 SEGMENT SIZE 80Тело отчета
ID_PARENTINTEGERСсылка на категорию
REPORT_NAMEVARCHAR(100)Наименование отчета
NO_VISIBLEINTEGERПризнак невидимости

Подозреваю, что назначение основных полей понятно. Интерес представляет поле NO_VISIBLE, оно определяет доступность отчета в окне интерфейса. Зачем? Ну например, некоторым отчетам для построения требуется некий контекст: такое состоянии объектов или интерфейса при котором вызов данного отчета имел бы смысл, например, для получения печатной формы накладной, нужная накладная, как минимум, должна быть текущей и ее идентификационная информация готова для передачи в отчет. Невидимыми могут быть так же и те отчеты, чьи параметры определяются в приложении (не в диалоговой форме отчета). Все прочие отчеты являются, скажем, так, аналитическими и не требующими контекста или для их построения которых, необходим конечный набор параметров задаваемых в диалоговой форме отчета. Например, текущие складские остатки или справочник единиц измерения. Скрытым можно сделать отчет-прародитель, с введением наследования в последних версиях Fastreport'а.

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

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

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

Отображение

В использования смотрите в примерах. Обратите внимание. Я разделяю БД с отчетами (с таблицей report) и БД с данными для построения отчетов. Сделано это намерено. Много всяких "почему" было, не буду все перечислять. Напомню только, что Firebird не позволяет обращаться из одной базы в другую в запросах. Почему так сделано, не знаю. Нет этому сколь-нибудь приемлемого объяснения.

В БД создано два отчета, для примера. "Отчет с приемом параметра" выводит свой ID если был запущен по кнопке "Вызов", если же запустить его из менеджера, то параметр передан не будет, соответственно и ID не будет показан. Отчет "Список телефонов" простой пример вывода какого то списка.

Реальный вид того самого менеджера отчетов в реальном же приложении представлен на рисунке. Он призван выполнять все задачи по управлению системой отчетности, от создания отчета, включения его в структуру, настройки и запуска отчета по требованию пользователя. Любой отчет может быть вызван и из программы, для этого была написано несколько специальных процедур (об этом ниже). Структура (дерево) отчетов построена на компонентах DBGridEh и MemTable библиотеки EhLib. Изначально все строилось на Treeview. Сейчас менеджер переписывается для БД Oracle на компонентах Virtual Treeview и ODAC.

Использование

Использование для пользователя совсем простое. Ищем необходимый отчет по наименованию и запускаем (двойной щелчок или кнопка).

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

Менеджер состоит из двух модулей: frmMainFR41_DB.pas - визуальная форма с некоторыми дополнительными функциями и ReportsDM.pas — модуль данных содержащий, по мимо все прочего и компонент TfrxReport. Для использования в вашей программе достаточно подключения (uses) только frmMainFR41_DB.

Вызов менеджера из вашей программы прост.

Модальный вызов:

uses frmMainFR41_DB;
...
//модальный вызов
begin
frmReports:=TFormReports.Create(self, fDBRep);
try
frmReports.DefaultDataBase := fDBData;
frmReports.ShowModal;
finally
frmReports.Free;
end;
end;

Или так (для немодального вызова):

uses frmMainFR41_DB;
...
//модальный вызов
begin
if not Assigned(frmReports) then begin
frmReports:=TFormReports.Create(self, fDBRep, True);
frmReports.DefaultDataBase:=fDBData;
frmReports.Show;
end
else begin
BringWindowToTop(frmReports.Handle);
ShowWindow(frmReports.Handle, SW_SHOWNORMAL);
end;

Про разделение баз с отчетами и данными. БД с отчетами (таблицей report) передается параметром в конструкторе TFormReports.Create(self, fDBRep);. БД с данными указывается в свойстве DefaultDataBase. Если это одна и та же БД, то нет необходимости заполнять это свойство.

Иногда отчеты (возможно, те самые, невидимые, у которых поле NO_VISIBLE = TRUE) вызываются по некоторым событиям в программе, например при нажатии на кнопку, и для этого нет необходимости вызывать менеджер отчетов. Для этого в модуле frmMainFR41_DB.pas объявлено несколько независимых (не методов класса) функций RepresentReport и RepresentReportParams.

(* вызов отчета из внешних приложений *)
function RepresentReport(ADBRep: TpFIBDatabase;
ID_REPORT: Integer;
AIniFile: string='';
View: Boolean=True): Integer; overload;

function RepresentReport(ADBRep: TpFIBDatabase;
ADBData: TpFIBDatabase;
ID_REPORT: Integer;
AIniFile: string='';
View: Boolean=True): Integer; overload;

(* вызов отчета из внешних приложений с передачей параметра *)
function RepresentReportParams(ADBRep: TpFIBDatabase;
ADBData: TpFIBDatabase;
ID_REPORT: Integer;
Params: Variant;
AIniFile: string='';
View: Boolean=True): Integer; overload;

(* выозов отчета из внешних приложений с передачей параметра *)
function RepresentReportParams(ADBRep: TpFIBDatabase;
ID_REPORT: Integer;
Params: Variant;
AIniFile: string='';
View: Boolean=True): Integer; overload;

RepresentReportParams в отличие от RepresentReport может передавать параметр в вызываемый отчет из контекста программы. Все, что вы присвоите переменной Params, найдете затем в отчете. Для этого в списке переменных следует создать переменную с именем "ID".

Возможна передача параметров, т.к. тип Variant может принять массив (в т.ч. и вариантный), но для этого надо заменить строку dmReports.frxReport1.Variables['ID']:=Params новым методом, который создаст переменные в отчете и присвоит и значения. Это так сказать предложение к доработке.

Связь между элементом управления и отчетом занесена в специальную таблицу (VARIABLES). Поле NAME VARCHAR(20) NOT NULL содержит некую условную строку (идентификационное имя отчета), а в поле VAL VARCHAR(20) содержится ID отчета. Суть проста, если вы меняете печатную форму документа или отчета, а старую оставляете в БД, то достаточно в таблице VARIABLES заменить значение поля VAL. Ниже приведено объявление функции, которая вернет ID отчета по его идентификационному имени.

(* получение ID_REPORT по уникальному идентификатору отчета см. табл. VARIABLES *)
function GET_ID_REPORT_AS_UD(ADBRep: TpFIBDatabase; UIR: string): Integer;

Полученное ID отчета можно передать одной из функций RepresentReport для генерации готового отчета.

Возможно это не самое удачное решение, но оно имеет право быть.

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

Показ готового отчета возможен как во встроенном prewview так и в отдельном.

Реализация внешних функций вызываемых из отчета требует переделки, потому как для расширения их списка требуется перекомпиляция модуля, один из способов описан в документации FastReport. Сейчас там вписаны две:

frxReport1.AddFunction('function NumToStr(n: double; c: byte = 0): string',
'Функции определенные пользователем',
'Число прописью. формат вывода:'
+(*#13#10*)' c=0 - 21.05 : "Двадцать один рубль 05 копеек."'
+(*#13#10*)' с=1 - 21.05 : "двадцать один"'
+(*#13#10*)' c=2 - 21.05 : "21-05", 21.00 : "21="');
frxReport1.AddFunction('function StrReplace(const S, OldS, NewS: string): string',
'Функции определенные пользователем',
'Заменяет в строке S подстроку OldS на подстроку NewS');

Вот в прочем и все. Смотрите в исходники, ибо картинка заменит тысячи слов.

Copyright© 2008 Сергей Плахов Специально для Delphi Plus



Новости за месяц

  • Ноябрь
    2018
  • Пн
  • Вт
  • Ср
  • Чт
  • Пт
  • Сб
  • Вс