Применение GDI+ в отчётах

В девятой версии Visual FoxPro система отчётов полностью переработана. Теперь во время выполнения отчёт можно «связать» с объектом ReportListener. Этот объект не только привносит в отчёт элементы объектно-ориентированного подхода; он позволяет динамически управлять отчётом. В принципе, при использовании этого объекта вы можете получить отчёт, совершенно не похожий на тот, который был создан в Конструкторе отчётов.
Объект ReportListener «следит» за выводом отчёта и генерирует различные события, обрабатывая которые, можно управлять содержимым печатаемого документа. Но основное достоинство объекта ReportListener — это то, что он предоставляет нам дескриптор объекта Graphics!

События, методы и свойства объекта ReportListener

Из достаточно большого количества событий, методов и свойств объекта ReportListener мы рассмотрим только тот минимальный набор, который необходим для использования этого объекта совместно с GDIPlus.
Событие BeforeBand возникает перед обработкой каждой очередной записи FRX файла. Метод, обрабатывающий это событие, получает два параметра:

  • BandObjCode — содержит одно из значений, определяющих раздел отчёта. Эти значения перечислены в таблице 23.2
  • FRXRecno — номер записи отчёта (FRX файла)

Таблица 23.2. Значения параметра BandObjCode

BandObjCode  
0  Title
1  Page Header
2  Column Header
3  Group Header
4  Detail
5  Group Footer
6  Column Footer
7  Page Footer
8  Summary
9  Detail Header
10  Detail Footer

Событие EvaluateContents возникает для каждого объекта отчёта (типа полей таблицы, выводимой в отчёт). Так, если вы выводите в отчёт по десять полей в строке, то на одно событие BeforeBand произойдёт десять событий EvaluateContents.
Метод EvaluateContents получает два параметра:

  • FRXRecno — номер записи отчёта (FRX файла)
  • ObjProperties — объект класса Empty, содержащий ряд свойств, которые перечислены в таблице 23.3.

Таблица 23.3. Свойства объекта ObjProperties

Свойство Описание

reload

Если вы собираетесь изменить способ вывода поля, то для сообщения об этом объекту ReportListener установите его в «истину» (.T.)
text Это свойство содержит текст, если тип данных для свойства value – символьный
value Это свойство содержит значение типа Variant (вы можете определить тип переменной, используя функцию VARTYPE). Если value имеет символьный тип, то это означает, что текст нужно получить из свойства text объекта.
fontname Строка. Содержит имя шрифта
fontstyle, fontsize Целочисленные значения. Определяют стиль и высоту шрифта
fillred, fillblue, fillgreen, penred, penblue, pengreen Свойства определяют цвет кистей и перьев, используемых для рисования поля отчёта. Могут принимать значения от 0 до 255
fillalpha Свойство определяет прозрачность кисти
penalpha Свойство определяет прозрачность пера

Метод Render выполняет построчное рисование элементов отчёта. Вызывается для каждого поля рисуемой строки. Метод получает большое количество параметров, в том числе номер записи в FRX файле, координаты левого верхнего угла, ширины и высоты области рисования поля и некоторые другие. Если вы дадите в этом методе команду NODEFAULT, то в отчёте ничего нарисовано не будет. При вводе своего кода в этот метод не забудьте вызвать функцию DODEFAULT() для выполнения кода класса-родителя.

Методы GetPageWidth и GetPageHeight возвращают размеры страницы отчёта (в точках). Документ отчёта имеет разрешение 960 точек на дюйм, или около сорока точек в миллиметре. Методы не имеют параметров.

Свойство GdiPlusGraphics содержит дескриптор объекта Graphics отчёта.

Свойство ListenerType определяет, каким образом ReportListener управляет выводом отчёта. Может принимать значения от -1 до 5. Подробности вы можете найти в справочной документации.

Создание отчёта

Запустите Мастер отчётов. В окне Wizard Selection выберите Report Wizard. На первом шаге мастера выберите таблицу demotable (если вы ещё не создали эту таблицу, то сделайте это сейчас, воспользовавшись рекомендациями из раздела «Динамическое отображение диаграмм» в этой главе). Включите в отчёт все поля таблицы. На втором шаге всё оставьте без изменений, на третьем шаге выберите стиль Ledger. Завершите выполнение мастера и сохраните отчёт в файле demoreport.frx.
Запустите Конструктор отчётов и загрузите в него только что созданный отчёт. Добавьте полосу Summary. Придайте отчёту вид, показанный на рис. 23.4.

Рис. 23.4. Редактирование отчёта demoreport в Конструкторе отчётов

Для чего нужна полоса Summary? Получив сообщение о начале этой полосы, мы будем рисовать нашу гистограмму. Почему? Потому что эта полоса обрабатывается сразу после завершения полосы Detail. Конечно, мы все привыкли, что полоса Summary выводится после полосы Detail. Но это совсем не обязательно! Работая с GDIPlus, мы рисуем на листе отчёта, и поэтому можем разместить диаграмму в любом месте (но в пределах листа, конечно)! В методе BeforeBand объекта ReportListener мы всегда имеем возможность перехватить и обработать событие, извещающее нас о том, что пришло время печатать полосу Summary — и приступить к рисованию графика или ещё чего-нибудь.

Процедура запуска отчёта

Во-первых, мы должны связать отчёт с объектом ReportListener. На самом деле мы свяжем отчёт с объектом, созданным на базе нашего собственного класса, который является потомком класса ReportListener.
Во-вторых, мы должны создать объект — экземпляр класса GdipImages для рисования на листе отчёта.
Создайте программный файл с именем demoreport.prg и введите в него код из листинга 23.19.

Листинг 23.19. DemoReport.prg. Процедура запуска отчёта

 
 #DEFINE DETAIL_BAND 4
 #DEFINE SUMMARY_BAND 8
 PUBLIC oGP, oReportListener
 cPath = JUSTPATH(SYS(16))
 SET DEFAULT TO (cPath)
 SET CLASSLIB TO vfpgdiplus
 * Создаём объект GdipImages 
 oGP = CREATEOBJECT("gdipimages")
 * Создаём объект ReportListener из нашего класса GdipDemoReport
 oReportListener = CREATEOBJECT("GdipReport")
 oReportListener.ListenerType = 1
 REPORT FORM demoreport PREVIEW OBJECT oReportListener
 RELEASE oGP, oReportListener
 

Отчёт будет выводиться в окно предварительного просмотра.

Класс GdipDemoReport

Если вы хотите управлять отчётом при помощи объекта ReportListener, то, как правило, вам придётся создавать специализированные классы, наследуемые от класса ReportListener, и «заточенные» под конкретный отчёт. Попытка создать универсальный класс «на все случаи» вряд ли увенчается успехом. Так, в нашем случае мы будем рисовать гистограмму (подобную показанной на рис. 23.3). В другом отчёте вы захотите выводить строки заголовка вертикально или под углом, в третьем отчёте… Ну мало ли чего вы ещё захотите!
Поэтому приступим к созданию пользовательского класса GdipDemoReport. Так как это не универсальный класс, то мы допишем его код в программный модуль demoreport.prg — в тот самый, где мы уже поместили процедуру запуска отчёта.
Итак, добавьте в этот файл команду:

 
 DEFINE CLASS GdipReport As ReportListener
 

Свойства класса GdipDemoReport

Вот описание необходимых свойств:

 
 lDetail = .f.            && Флаг, определяющий, что обрабатывается полоса Detail
 nLines = 0               && Счётчик количества строк в полосе Detail
 nColumns = 0             && Счётчик количества объектов в строке
 DIMENSION aDates[12,3]   && Массив, в который копируются значения полей
 

Метод BeforeBand

Этот метод будет определять тип обрабатываемой полосы отчёта. Если это Detail, то устанавливается в «истину» свойство lDetail. Если это Summary, то вызывается метод ToDraw для рисования гистограммы. Для всех остальных полос свойство lDetail устанавливается в «ложь».

Листинг 23.20. Код метода BeforeBand класса GdipDemoReport

 
 PROCEDURE BeforeBand(tnBandObjCode, tnFRXRecno)
    DO CASE 
       CASE tnBandObjCode = DETAIL_BAND         && Если это полоса Detail
          this.nLines = this.nLines + 1         && Инкремент счётчика строк
          this.nColumns = 0                     && Сброс счётчика полей строки
          this.lDetail = .t.
       CASE tnBandObjCode = SUMMARY_BAND        && Если это полоса Summary
          this.lDetail = .f.
          this.ToDraw()
       OTHERWISE                                && Все остальные полосы
          this.lDetail = .f.
    ENDCASE 
 ENDPROC

Такое построение метода позволяет вносить изменения в полосу Detail, но никак не влияет на вывод в отчёт содержимого других полос.

Метод EvaluateContents

В методе проверяется значение свойства lDetail, то есть относятся ли полученные данные к полосе Detail. Если да, то вычисляется номер поля; его значение заносится в массив aDates.

Листинг 23.21. Код метода EvaluateContents класса GdipDemoReport

 
 PROCEDURE EvaluateContents(tnFRXRecno, toProps)
    LOCAL lcValue
    IF this.lDetail
       this.nColumns = this.nColumns + 1 && Инкремент счётчика полей
       DO CASE 
          CASE VARTYPE(toProps.value) = "C"
             this.aDates[this.nLines,this.nColumns] = toProps.text
          CASE VARTYPE(toProps.value) = "N"
             this.aDates[this.nLines,this.nColumns] = toProps.value
       ENDCASE 
    ENDIF 
 ENDPROC
 

Метод Render

Напомню, что именно этот метод «рисует» отчёт. В нашем классе он используется для вычисления границы области рисования гистограммы.
Если вы хотите полностью изменить вид полосы, то дайте в нём команду NODEFAULT — в этом случае данные из полосы, сформированные в Конструкторе отчётов, рисоваться не будут. В нашем примере мы «разрешаем» нарисовать таблицу в полосе Detail, поэтому вызывается функция DODEFAULT(), которая заставляет выполниться код класса – родителя.

Листинг 23.22. Код метода Render класса GdipDemoReport

 
 PROCEDURE Render(nFRXRecNo, nLeft, nTop, nWidth, nHeight, ;
    nObjectContinuationType, cContentsToBeRendered, GDIPlusImage) 
    DODEFAULT(nFRXRecNo, nLeft, nTop, nWidth, nHeight, nObjectContinuationType, ;
              cContentsToBeRendered, GDIPlusImage)
    IF this.lDetail              && Если полоса - Detail
       IF !this.lStart           && и если это первое поле первой строки
          this.nTop = nTop       && то запоминаем его верхнюю границу
          this.lStart = .t.
       ENDIF 
 * Далее определяем значение координаты по X границы печатаемой таблицы
       IF this.nLeft < nLeft + nWidth
          this.nLeft = nLeft + nWidth
       ENDIF 
    ENDIF 
 ENDPROC

Как и все предыдущие методы, этот метод автоматически вызывается для каждого объекта отчёта. Помимо прочего, в метод передаются координаты и размеры областей для рисования объекта.

Метод ToDraw

А это уже наш, пользовательский, метод. Он вызывается из метода BeforeBand, когда возникает событие начала печати полосы Summary. В этом методе мы должны вычислить координаты всех столбцов гистограммы и нарисовать её.

Листинг 23.23. Код метода ToDraw класса GdipDemoReport

 
 PROCEDURE ToDraw
    LOCAL i, lnRow, lnCol, lnMax, lnPageWidth
    lnRow = ALEN(this.aDates,1)
    lnCol = ALEN(this.aDates,2)
    lnMax = 0
 * Определение максимального значения
    FOR i = 1 TO lnRow
       IF lnMax < this.aDates[i,2]
          lnMax = this.aDates[i,2]
       ENDIF 
       IF lnMax < this.aDates[i,3]
          lnMax = this.aDates[i,3]
       ENDIF 
    ENDFOR 
 * Метод GetPageWidth возвращает значение ширины страницы в точках
 * (для отчёта используется DPI 960 точек в дюйме)
 * Здесь мы отнимаем от полученного значения 500 точек; это будет 
 * отступ от правой границы страницы
    lnPageWidth = this.GetPageWidth() – 500
    lnDirect = lnPageWidth - this.nLeft   && Ширина области для рисования
 * Левая граница области для рисования должна располагаться
 * правее правого края таблицы. Устанавливаем сдвиг в 100 точек (2,5 мм)
    lnLeft = this.nLeft + 100             && Левая граница области
 * Определение нижней границы области рисования. Высота области
 * принимается равной 2850 точкам. В принципе, её можно увязать
 * с размерами напечатанной таблицы, выполнив необходимые вычисления
 * в методе Render
    lnBottom = this.nTop + 2850
 * Рисуем координатные оси
    lnGraphics = this.GdiPlusGraphics
    oGP.CreatePen(10)
    oGP.DrawLine(lnLeft, this.nTop - 100, lnLeft, lnBottom, lnGraphics)
    oGP.DrawLine(lnLeft, lnBottom, lnLeft + lnDirect + 200, lnBottom, lnGraphics)
    lnStep = 2850 / lnRow                 && Шаг для размещения 12 столбцов гистограммы
    lnWidthRect = 0.6 * lnStep            && Толщина столбца
    lnScale = lnDirect / lnMax            && Масштабный коэффициент
    oGP.CreateSolidBrush(0xFF0088EE)      && Создаём кисть (любого цвета)
    lnTop = this.nTop
    oGP.SetPenColor(0xFF00AA00)           && Делаем перо зелёным
    FOR i = 1 TO lnRow
       oGP.SetColorSolidBrush(0xFF0088EE) && Делаем кисть синей
       oGP.FillRectangle(lnLeft, lnTop, this.aDates[i,2] * lnScale, lnWidthRect, lnGraphics)
       oGP.SetColorSolidBrush(0xAAAAFF00) && Делаем кисть зелёной и чуть прозрачной
       oGP.FillRectangle(lnLeft, lnTop + 0.5 * lnWidthRect, this.aDates[i,3] * lnScale, ;
                         lnWidthRect, lnGraphics)
       oGP.DrawRectangle(lnLeft, lnTop + 0.5 * lnWidthRect, this.aDates[i,3] * lnScale, ;
                         lnWidthRect, lnGraphics)
       lnTop = lnTop + lnStep
    ENDFOR 
    oGP.SetPenColor(0xFFBBBBBB) && Делаем перо светло-серым
 * Рисуем вертикальные линии разметки
    lnStep = lnDirect / 10
    FOR i = lnLeft + lnStep TO lnDirect + lnLeft STEP lnStep
       oGP.DrawLine(i, this.nTop - 100, i, lnBottom, lnGraphics)
    ENDFOR 
 * Подготовка к рисованию текста
    oGP.CreateStringFormat()              && Создаём объект StringFormat
    oGP.SetStringFormatParameter(1)       && Режим центрирования строки
    oGP.CreateFont("Arial", 120)          && Ihban Arial высотой 4,9 мм
    oGP.SetColorSolidBrush(0xFF000000)    && Делаем кисть чёрной
    lnLeft = lnLeft – 300
    lnValue = 0
 * Рисуем значения разметки
    FOR i = 1 TO 11
       oGP.DrawString(lnLeft, lnBottom + 50, 600, 120, LTRIM(STR(lnValue)), lnGraphics)
       lnLeft = lnLeft + lnStep
       lnValue = lnValue + lnMax / 10
    ENDFOR 
 ENDPROC
 

Для упрощения кода в отчёте не выводятся наименования месяцев; вместо этого столбцы гистограммы располагаются напротив строк таблицы.
Вот и весь класс. Добавьте завершающий аккорд, допишите в файл команду

 
 ENDDEFINE
 

Запустите файл demoreport.prg на выполнение. Появится окно просмотра отчёта, в котором вы увидите примерно то, что показано на рис. 23.5.

Рис. 23.5. Окно Report Preview с загруженным отчётом demoreport