Печать итоговой суммы по группе в заголовке группы

       Этот довольно часто используемый прием требует использования скрипта. Ведь в обычном отчете значение суммы становится доступным только после того, как будут обработаны все записи группы. Чтобы вывести сумму в заголовке группы (т.е. до того, как будет обработана группа), используется следующий алгоритм:

- отчет делается двухпроходным;

- на первом проходе считается сумма по каждой группе и сохраняется в каком-нибудь массиве;

- на втором проходе значения извлекаются из массива и печатаются в заголовке группы.

 

       Продемонстрируем, как решить эту задачу двумя способами. Для начала создадим новый проект в Delphi, на форму положим компоненты TQuery, TfrxReport, TfrxDBDataSet. Настроим их следующим образом:

 

Query1:

DatabaseName = 'DBDEMOS'

SQL =

select * from customer, orders

where orders.CustNo = customer.CustNo

order by customer.CustNo, orders.OrderNo

 

frxDBDataSet1:

DataSet = Query1

UserName = 'Group'

 

       Зайдем в дизайнер и подключим наш источник данных к отчету. В настройках отчета (пункт меню "Отчет|Настройки...") включим двойной проход. Добавим в отчет два бэнда: "Заголовок группы" и "Данные 1 уровня". В редакторе бэнда "Заголовок группы" укажем условие – поле данных Group.CustNo. Дата-бэнд привяжем к источнику данных Group и разместим объекты следующим образом:

 

clip0186

 

       Выделенный на рисунке объект (его имя – Memo8) мы используем для вывода суммы.

 

 

Способ 1.

 

       Мы используем в качестве массива для хранения сумм класс TStringList. Значения будем хранить в виде строк. При этом первая строка в списке будет соответствовать значению первой группы, и т.д. Для подсчета номера группы будет использована целочисленная переменная, которую мы будем увеличивать после печати очередной группы.

 

       Итак, наш скрипт будет выглядеть следующим образом:

 

PascalScript:

 

var

List: TStringList;

i: Integer;

 

procedure frxReport1OnStartReport(Sender: TfrxComponent);

begin

List := TStringList.Create;

end;

 

procedure frxReport1OnStopReport(Sender: TfrxComponent);

begin

List.Free;

end;

 

procedure Page1OnBeforePrint(Sender: TfrxComponent);

begin

i := 0;

end;

 

procedure GroupHeader1OnBeforePrint(Sender: TfrxComponent);

begin

if Engine.FinalPass then

   Memo8.Text := 'Sum: ' + List[i];

end;

 

procedure GroupFooter1OnBeforePrint(Sender: TfrxComponent);

begin

if not Engine.FinalPass then

   List.Add(FloatToStr(SUM(<Group."ItemsTotal">,MasterData1)));

Inc(i);

end;

 

begin

 

end.

 

 

C++ Script:

 

TStringList List;

int i;

 

void frxReport1OnStartReport(TfrxComponent Sender)

{

List = TStringList.Create();

}

 

void frxReport1OnStopReport(TfrxComponent Sender)

{

List.Free();

}

 

void Page1OnBeforePrint(TfrxComponent Sender)

{

i = 0;

}

 

void GroupHeader1OnBeforePrint(TfrxComponent Sender)

{

if (Engine.FinalPass)

   Memo8.Text = "Sum: " + List[i];

}

 

void GroupFooter1OnBeforePrint(TfrxComponent Sender)

{

List.Add(FloatToStr(SUM(<Group."ItemsTotal">,MasterData1)));

i++;

}

 

{

 

}

 

       По именам процедур можно видеть, какие события мы использовали: Report.OnStartReport, Report.OnStopReport, Page1.OnBeforePrint, GroupHeader1.OnBeforePrint, GroupFooter1.OnBeforePrint. Что касается первых двух событий, то они, как уже говорилось, вызываются в начале и в конце отчета, соответственно. Чтобы создать обработчики для этих событий, надо выделить объект "Отчет" в окне "Дерево отчета" – его свойства появятся в инспекторе объектов. Далее действуем стандартным образом – переключаемся на закладку "События" инспектора и создаем обработчики.

 

       Почему мы не воспользовались для создания списка List главной процедурой, а сделали это в событии OnStartReport? Потому, что созданный объект надо после завершения отчета освободить. Поэтому логично создавать объекты в событии OnStartReport, а освобождать их в OnStopReport. В других случаях (когда не нужно освобождать память) можно пользоваться главной процедурой для инициализации переменных.

 

       С созданием и освобождением объекта List все понятно. Теперь рассмотрим, как работает скрипт. В начале страницы счетчик текущей группы (переменная i) сбрасывается в 0 и увеличивается на единицу после печати каждой группы (в событии GroupFooter1.OnBeforePrint). В этом же событии в список добавляется вычисленное значение суммы. Событие GroupHeader1.OnBeforePrint на первом проходе не срабатывает (проверка Engine.FinalPass). На втором проходе (когда список List заполнен значениями), в этом событии извлекается значение, соответствующее текущей группе, и записывается в текст объекта Memo8, который и показывает сумму в заголовке группы. В готовом отчете это выглядит так:

 

_img254

 

       Как видим, алгоритм достаточно простой. Но и его можно упростить.

 

 

Способ 2.

 

       Мы используем в качестве массива для хранения сумм список переменных отчета.  Как мы помним, обращение к таким переменным осуществляется с помощью функций Get и Set. Это избавит нас от необходимости создавать лишние объекты и освобождать память. Наш скрипт будет следующим:

 

PascalScript:

 

procedure GroupHeader1OnBeforePrint(Sender: TfrxComponent);

begin

if Engine.FinalPass then

   Memo8.Text := 'Sum: ' + Get(<Group."CustNo">);

end;

 

procedure GroupFooter1OnBeforePrint(Sender: TfrxComponent);

begin

Set(<Group."CustNo">,

   FloatToStr(SUM(<Group."ItemsTotal">,MasterData1)));

end;

 

begin

 

end.

 

 

C++ Script:

 

void GroupHeader1OnBeforePrint(TfrxComponent Sender)

{

if (Engine.FinalPass)

   Memo8.Text = "Sum:" + Get(<Group."CustNo">);

}

 

void GroupFooter1OnBeforePrint(TfrxComponent Sender)

{

Set(<Group."CustNo">,

   FloatToStr(SUM(<Group."ItemsTotal">,MasterData1)));

}

 

{

 

}

 

       Как видно, скрипт значительно упростился. Код в обработчике GroupFooter1.OnBeforePrint устанавливает значение переменной с именем, равным номеру клиента (можно использовать любой идентификатор, однозначно идентифицирующий клиента, например его имя <Group."Company">). Если такой переменной нет – она создается, если есть – меняется ее значение. В обработчике GroupHeader1.OnBeforePrint извлекается значение переменной с номером текущей группы.