Отчёты с деревьями

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

Отчеты и разные документы являются неотъемлемой частью любой компании, различаются лишь формы этих документов. Но не всегда данные имеют “плоский” вид, некоторые задачи требуют построение иерархии или древовидных структур. Большинство генераторов отчетов работают именно с плоскими наборами данных или таблицами.

Далее будут описаны несколько вариантов решения проблемы печати иерархии средством генератора отчетов Fast Report.

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

Пример такой таблицы приведен на рисунке (EmpNo – уникальный номер сотрудника, EmpOwner – указывает у кого в подчинении находится данный сотрудник).

пример таблицы

Идея создания отчета проста, необходимо отсортировать данные в соответствии с номером сотрудника и номером руководителя. Т.е. в начале будут идти сотрудники без руководителей (пустое поле), далее за каждым руководителей должны идти подчиненные и так по всей цепочке для каждой записи. Для сортировки набора данных можно воспользоваться списком, в который будут заноситься номера сотрудников, и сортироваться в нужном порядке. Для перехода к нужной записи набора данных будем использовать функцию Locate.

Перейдем от описания непосредственно к созданию отчета. Создайте новый шаблон и установите подключение к базе данных, добавьте таблицу или запрос для выборки необходимых данных. Пример такой таблицы приложен к статье вместе с примером(HDemo.mdb). Добавьте в отчет бенды : “Заголовок отчета”, “Данные первого уровня” и “Подвал страницы”. Разместите на заголовке отчета объект “текст” с произвольным текстом (к примеру “Сотрудники”), а на подвале страницы объект “текст” с переменной [Page]. В нашем отчете данные бэнды несут декоративный характер для придания вида отчету, основная же обработка будет связана с бэндом данных. Шаблон отчета должен получиться приблизительно как на рисунке ниже.Сотрудники

Добавьте нужные поля из набора данных (перетащите из дерева данных) на бэнд данных, но не привязывайте бенд к данным. В итоге конечный вид шаблона будет иметь приблизительно такой вид:
Сотрудники

Для обработки, выбора данных и смещения будем использовать скрипт отчета. Для этого добавьте следующие события у перечисленных объектов(события можно добавить через инспектор объектов):

  • “Данные первого уровня”(MasterData1) – события: OnAfterPrint и OnBeforePrint.
  • “Отчет”(Report – можно выбрать в списке инспектора объектов), событие OnStopReport.

На этом подготовка шаблона закончена и можно приступать непосредственно к реализации сортировки и вывода через скрипт отчета. Для этого переключитесь на вкладку “код” и приступим к написанию скрипта. В самом верху скрипта объявим глобальные переменные:

var
TreeList: TStringList;// список сортировки
ShiftList: TStringList;// список сортировки
FDBdataSet: TfrxDBDataSet;// набор данных
FVal: variant;// Временная переменная для хранения значений
FPrevEmpNo: Integer;// переменная для хранения предыдущего номера
oldCurX: Extended; // переменная для хранения оригинальной позиции вывода бэнда

Перейдем к главной процедуре скрипта (код между begin … end.), в ней будут инициализироваться переменные и вызываться заполнение списка сортировки.

begin
{ получаем набор данных }
FDBdataSet := TfrxDBDataSet(Report.GetDataset('ADOTable1'));
{ создание списка сортировки }
TreeList := TStringList.Create;
{ создание списка смещения }
ShiftList := TStringList.Create;
if FDBdataSet = nil then Exit;

{ открываем набор данных, и переходим на первую запись}
FDBdataSet.Open;
FDBdataSet.First;
{ цикл по всем записям набора данных }
while not FDBdataSet.Eof do
begin
{ получаем номер текущего сотркудника }
FPrevEmpNo := FDBdataSet.Value('EmpNo');
AddEmp;
{ восстанавливаем курсок набора данных после выполнения AddEmp }
FDBdataSet.Locate('EmpNo', FPrevEmpNo, 0);
{ переход к следующей записи }
FDBdataSet.Next;
end;
MasterData1.RowCount := TreeList.Count;
end.

Функция Report.GetDataset возвращает объект набора данных по его имени(если он найден). Далее идет создание списков сортировки, смещения и открытие набора данных, после чего организован цикл по всем записям набора данных, в котором вызывается функция добавления значения в список AddEmp (описана ниже). Функция FDBdataSet.Locate устанавливает курсор в наборе данных в позицию, где значение указанного поля совпадает со значением, переданным в качестве второго параметра функции. MasterData1.RowCount задает кол-во повторений бэнда данных (кол-во строк).

Функция сортировки должна быть объявлена перед главной процедурой, ее реализация приведена ниже:

{ добавление номера сотрудника в позицию соответствующей ему иерархии } function AddEmp: Integer;
var
FEmpNo: Integer;
RNo: Integer;
s: String;
begin
FVal := FDBdataSet.Value('EmpOwner');// номер руководителя
FEmpNo := FDBdataSet.Value('EmpNo');// номер текущего сотрудника
s := IntToStr(FEmpNo);
Result := TreeList.IndexOf(s);// поиск сотрудника в списке, если найден не добавляем
if Result <> -1 then
exit;
if FVal <> NULL then
begin
{ сотрудник имеет руководителя }
{ переход к руководителю }
FDBdataSet.DataSet.Locate('EmpNo', FVal, 0);
{ добавляем сотрудника после руководителя }
Result := AddEmp;
{ добавляем сотрудника после руководителя }
Inc(Result);
if Result >= TreeList.Count then
TreeList.Add(s)
else
TreeList.Insert(Result, s);
end
else begin
{ сотрудник не имеет руководителей(как же ему повезло !), просто добавляем его в список}
TreeList.Add(s);
Result := TreeList.Count;
end;
end;

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

В событии OnStopReport удаляем созданные списки.

procedure ReportOnStopReport(Sender: TfrxComponent);
begin
{ удаление списков }
TreeList.Free;
ShiftList.Free;
end;

Выборка данных перед выводом в соответствии с сортировкой и смещение бэнда реализовано в событии MasterData1OnBeforePrint. Переход к нужной записи происходит с помощью уже знакомой функции Locate, в качестве ключа поиска используются значения из списка.

procedure MasterData1OnBeforePrint(Sender: TfrxComponent);
var
eOwner: String;
i: Integer;
begin
{ перехватываем добавление страницы, чтобы CurX не обнулилась}
if MasterData1.Height > Engine.FreeSpace then
Engine.NewPage;
FPrevEmpNo := ;
{ установка позиции в наборе данных }
FDBdataSet.DataSet.Locate('EmpNo', TreeList[ - 1], 0);
{ получаем текущего руководителя }
eOwner := IntToStr();
{ если руководитель не найден добавляем его }
{ иначе получаем индекс смещения для сотрудника }
i := ShiftList.IndexOf(eOwner);
if i = -1 then
begin
{ для корректного смещения узла проверяем предыдущий индекс }
i := ShiftList.IndexOf(IntToStr(FPrevEmpNo)) + 1;
{ если предыдущий ключ последний в списке, то добавляем новое смещение }
if i >= ShiftList.Count then
begin
ShiftList.Add(eOwner);
i := ShiftList.Count - 1;
end
{ иначе заменяем ключ поиска }
else
ShiftList[i] := eOwner;
end;
{ запоминаем текущую позицию бэнда для восстановления }
oldCurX := EnginE.CurX;
{ смещаем бэнд }
EnginE.CurX := EnginE.CurX + 20 * i;
end;

В событии MasterData1OnAfterPrint восстанавливаем позицию после смещения.

procedure MasterData1OnAfterPrint(Sender: TfrxComponent);
begin
{ восстанавливаем позицию бэнда }
EnginE.CurX := oldCurX;
end;

Отчет готов:Отчёт сотрудники

В заключении можно отметить, что использование Locate на больших наборах данных может снизить производительность построения отчета, в этом случае можно хранить все необходимые данные в списке, либо в древовидной структуре. Без использования Locate при выборке данных. Общий принцип построения остается прежним.

При наличии древовидных структур в вывод значительно упрощается, т.к. не нужно сортировать данные, и можно сразу перемещаясь по дереву в событии OnManualBuild выводить узлы на бэнде через Engine.ShowBand, но это уже тема для отдельной статьи.

Пример отчета из базы может быть запущен как из Fast Report 4 VCL, так и из Fast Report Studio.

Copyright© 2010 Денис Зубов,
ведущий разработчик FastReports

Comments are closed.