Аннотация
Как создавать собственные отчеты, применяя функции печати GDI+ в
Microsoft .NET.
Исходный код к этой статье можно скачать по ссылке
printwinforms.exe sample code.
Содержание
Введение
Средства печати в .NET Framework
Создание реальных отчетов
Класс TabularReport
Заключение
Введение
Я не собираюсь разглагольствовать о технологиях безбумажного
документооборота, отмечу лишь, что времена этих технологий еще не
настали. Я создал много разных систем, отчасти избавляющих компании от
работы с бумажными документами и превращающих эти документы в хранящиеся
на компьютере данные, но (независимо от того, насколько хороша система)
одним из основных требований всегда оставалась функция перевода данных
обратно в бумажную форму. Начиная с систем, написанных на языке
программирования Clipper, затем Microsoft® FoxPro®, Microsoft® Access,
Microsoft® Visual Basic®, а теперь и Microsoft .NET, при создании
бизнес-систем неизменно одно: создание и тестирование отчетов - один из
самых длительных периодов в графике проекта. Предполагая, что это
справедливо не только для меня, а для всех, я собираюсь показать, как
использовать средства вывода графики GDI+ и классы GDI+ Printing для
создания табличного отчета. Значительную часть всей функциональности
вывода, которую вам когда-либо предстоит разработать, составляют отчеты
этого типа (рис. 1).
Рис. 1. Табличные отчеты применяются для распечатки списков,
например бухгалтерских счетов, заказов или других данных, которые можно
представить в виде сетки
Хотя я не рассматриваю другой распространенный тип отчетов -
форму-счет (рис. 2), многие принципы создания отчетов, обсуждаемые в
этой статье, относятся и к таким отчетам.
Рис. 2. Счета, налоговые декларации и другие документы этого типа
часто выводятся в отчетах-формах
Примечание - Если вы уже изучали возможности Microsoft® Visual
Studio® .NET, то скорее всего знаете, что вместе с ним поставляется
Crystal Reports - полноценная среда для разработки отчетов, содержащая
множество средств, и вы, вероятно, удивлены, почему я не рассматриваю ее
в этой статье. Crystal Reports - отличное средство создания отчетов, но
бесплатно его не развернешь, поэтому я хочу, чтобы вы поняли, чего можно
добиться, используя лишь средства .NET Framework.
Средства печати в .NET Framework
Прежде чем углубиться в разработку двух отчетов-примеров, я хочу
сделать обзор возможностей печати в .NET. Платформа .NET Framework
предоставляет набор средств печати, которые опираются на существующие
классы GDI+, позволяющие выполнять печать, предварительный просмотр и
другие операции с принтерами. Для программного использования этих
средств предназначены классы
System.Drawing.Printing и визуальные компоненты (PrintDialog,
PrintPreviewDialog,
PrintDocument и т. д.), доступные в приложениях Windows Forms.
Благодаря этим классам писать код для печати практически не нужно -
достаточно лишь пары компонентов Windows Forms.
Попробуйте сами
Чтобы получить первое впечатление о средствах печати, выполните
следующие действия и создайте небольшое тестовое приложение.
- Создайте новое Windows-приложение в Visual Studio .NET на
Microsoft® Visual C#® или Visual Basic .NET.
- На автоматически созданную форму Form1 добавьте по одному из
следующих компонентов: PrintDocument, PrintPreviewDialog
и Button.
- Теперь выберите компонент PrintPreviewDialog и
просмотрите его свойства. Установите свойство Document,
равное (none), в PrintDocument1 ('printDocument1', если вы пишете на
C#). Тем самым вы свяжете два компонента, и когда диалогу
предварительного просмотра потребуется вывести страницу в окно
предварительного просмотра или на принтер, он вызовет событие
PrintPage указанного PrintDocument.
- Добавьте код в обработчик события PrintPage объекта
PrintDocument1. Независимо от языка для доступа к событию два раза
щелкните компонент PrintDocument. Далее приведен фрагмент
кода, печатающий на странице имя зарегистрированного в системе
пользователя.
//C#
private void printDocument1_PrintPage(object sender,
System.Drawing.Printing.PrintPageEventArgs e)
{
Graphics g = e.Graphics;
String message = System.Environment.UserName;
Font messageFont = new Font("Arial",
24,System.Drawing.GraphicsUnit.Point);
g.DrawString(message,messageFont,Brushes.Black,100,100);
}
'Visual Basic .NET
Private Sub PrintDocument1_PrintPage(ByVal sender As System.Object, _
ByVal e As System.Drawing.Printing.PrintPageEventArgs) _
Handles PrintDocument1.PrintPage
Dim g As Graphics = e.Graphics
Dim message As String = System.Environment.UserName
Dim messageFont As New Font("Arial", 24, _
System.Drawing.GraphicsUnit.Point)
g.DrawString(message, messageFont, Brushes.Black, 100, 100)
End Sub
- Добавьте код к обработчику события щелчка кнопки (чтобы попасть
в обработчик события click, дважды щелкните кнопку в
дизайнере формы), вызывающий диалог предварительного просмотра.
Когда диалогу предварительного просмотра потребуется вывести
страницу, он вызовет только что созданный обработчик события
PrintPage.
//C#
private void button1_Click(object sender, System.EventArgs e)
{
printPreviewDialog1.ShowDialog();
}
'Visual Basic .NET
Private Sub Button1_Click(ByVal sender As System.Object, _
ByVal e As System.EventArgs) _
Handles Button1.Click
PrintPreviewDialog1.ShowDialog()
End Sub
Запустите приложение. Если все правильно, при щелчке кнопки откроется
диалоговое окно предварительного просмотра, содержащее почти пустую
страницу с именем пользователя.
Создание реальных отчетов
Короткого упражнения, приведенного выше, вполне достаточно, чтобы
понять, как создавать собственные отчеты; вся остальная работа - это
просто рисование средствами GDI+ и решение задач разметки, связанных с
границами и другими параметрами. Для создания законченных отчетов,
управляемых данными (data-driven reports), достаточно добавить код к
обработчику события PrintPage. Конечно, для одного никогда не
меняющегося отчета это неплохо, но если вы создаете отчеты постоянно,
поддерживать весь код в PrintPage будет трудновато. Вместо этого
разбейте печать страницы на отдельные задачи и для каждой задачи
напишите функцию, выполняющую свою часть работы. Если вам удастся
сделать этот код повторно используемым, вы сможете генерировать любые
отчеты без написания нового кода.
В прошлом я пользовался разными инструментами для генерации отчетов и
обнаружил, что все они разбивают отчеты на одни и те же общие элементы:
- верхние и нижние колонтитулы страницы;
- верхние и нижние колонтитулы отчета;
- верхние и нижние колонтитулы группы;
- строки с данными.
Чтобы облегчить себе задачу, я решил обойтись без реализации разделов
отчета (Report) и группы (Group) и сосредоточиться на верхних и нижних
колонтитулах страницы, а также, конечно, на строках с данными. Я начал с
новой Windows-формы, добавил PrintDocument точно так же, как в
приведенном выше упражнении, а затем создал несколько других функций -
PrintPageFooter, PrintPageHeader и PrintDetailRow,
- каждая их которых возвращает размер (высоту в пикселах) созданного ею
раздела. Каждой из этих функций я предоставил объект
PrintPageEventArgs, передаваемый обработчику события PrintPage,
чтобы они получили доступ к информации о параметрах принтера и к объекту
Graphics, нужному им для отрисовки. Кроме того, я передаю
прямоугольник, bounds, описывающий площадь страницы, на которой
требуется вывести раздел, и общий аргумент sizeOnly, указывающий,
надо ли печатать раздел или достаточно вычислить и вернуть его размер.
Функция PrintDetailRow принимает некоторые другие аргументы,
которые я опишу, когда мы перейдем к рассмотрению ее кода. После того
как функция для каждого отдельного раздела написана, отчет создается
быстро. Как я обнаружил, независимо от конкретного раздела приходится
решать несколько основных задач, в том числе настройка разрешения и
обработка нескольких страниц.
Проблемы разрешения: преобразование дюймов в пикселы
Имея дело с GDI+, вы всегда работаете в каких-то конкретных единицах
измерения, т. е. дюймах, пикселах, точках или в чем-то еще, причем на
связь между единицами измерения влияет устройство вывода (например,
принтер). Я обнаружил, что легче измерять все в аппаратно-независимых
дюймах (дюйм - всегда дюйм) и вообще избегать проблем с разрешением. Для
этого (поскольку дюйм не является единицей измерения по умолчанию) вам
придется установить свойство PageUnit объекта Graphics в
начале обработчика события PrintPage.
Public Sub PrintPage(ByVal sender As Object, _
ByVal e As PrintPageEventArgs)
Dim g As Graphics = e.Graphics
g.PageUnit = GraphicsUnit.Inch
Единственное неудобство при работе в дюймах - такие параметры Page и
Printer, как поля страницы (margins), измеряются в сотых долях дюйма.
Конвертировать из одних значений в другие легко, но помните, что всегда
следует использовать тип данных Single, чтобы после
конвертирования не потерять точность. В начале функции PrintPage,
я получаю значения границ и во избежание путаницы немедленно их
конвертирую.
Dim leftMargin As Single = e.MarginBounds.Left / 100
Dim rightMargin As Single = e.MarginBounds.Right / 100
Dim topMargin As Single = e.MarginBounds.Top / 100
Dim bottomMargin As Single = e.MarginBounds.Bottom / 100
Dim width As Single = e.MarginBounds.Width / 100
Dim height As Single = e.MarginBounds.Height / 100
Работа с несколькими страницами
Если ваш отчет занимает несколько страниц, нужно следить за номером
текущей страницы и текущей строкой данных, так как событие PrintPage
вызывается для печати каждой страницы. В любом случае переменная,
существующая вне кода обработчика события, позволяет сохранять текущую
позицию между вызовами PrintPage.
Dim currentPage As Integer = 0
Dim currentRow As Integer = 0
Public Sub PrintPage(ByVal sender As Object, _
ByVal e As PrintPageEventArgs)
...
currentPage += 1
Не забывайте каждый раз обнулять эти значения перед началом печати,
иначе при последующей печати вы начнете не с первой страницы. Это
следует делать в обработчике события
BeginPrint объекта PrintDocument.
Private Sub PrintDocument1_BeginPrint(ByVal sender As Object, _
ByVal e As PrintEventArgs) _
Handles PrintDocument1.BeginPrint
currentPage = 0
currentRow = 0
End Sub
Когда я только начинал изучение возможностей печати в .NET, я обнулял
счетчик страниц в обработчике нажатия кнопки Print, перед печатью
документа. Все отлично работало, пока я не решил просмотреть документ
перед печатью. В предварительном просмотре страницы нумеровались с 1 по
3, но на бумаге номера шли с 4 по 6, так как документ на самом деле
печатался два раза (один раз в окне предварительного просмотра, второй
раз - на принтере). На самом деле, так как из диалогового окна Print
Preview можно печатать сколько угодно раз, номера продолжат расти.
Cобытие BeginPrint позволяет избежать этих проблем.
Кроме того, через свойства
HasMorePages объекта
PrintPageEventArgs следует указывать, закончена печать отчета или
еще остались страницы. Перед завершением обработчика события
PrintPage установите это свойство в false, если печать закончена,
или в true, если напечатаны не все строки. При печати на нескольких
страницах нужно знать, когда достигнут конец страницы, поэтому все мои
процедуры печати (например, PrintDetailRow) принимают аргумент
"sizeOnly". Передавая True в параметре sizeOnly, я выясняю
размер строки, которую необходимо напечатать, что позволяет определить,
есть ли для этой строки место на странице (конечно, не забывая про
нижний колонтитул). Если места больше нет, я не печатаю строку и не
увеличиваю значение переменной currentRow; вместо этого я
устанавливаю свойство HasMorePages в True и печатаю строку
на следующей странице.
Примечание - Если размер строки превышает размер страницы, вы
никогда не сможете ее напечатать и войдете в бесконечный цикл, печатая
пустые страницы. После того как вы выясните размер раздела, проверьте,
разумно ли это значение (меньше ли оно свободного места на пустой
странице) до того, как работать с ним. Если оно окажется слишком велико,
генерируйте исключение.
Вывод текста
Независимо от того, какой тип имеют данные в полях, текст в каждом
столбце, верхнем и нижнем колонтитулах выводится через метод
DrawString класса Graphics. При выводе текста в отчете его
размер и местоположение зачастую ограничены, и именно поэтому метод
DrawString содержит параметр LayoutRectangle. С его помощью
текст ограничивается некоей областью, при этом параметр
StringFormat обеспечивает управление такими функциями, как
автоматический перенос слов.
Сочетание параметров StringFormat с LayoutRectangle
Каждый из этих рисунков - результат вызова DrawString со
строкой "The quick brown fox jumped high over the slow and lazy black
dog" и разными параметрами формата строки. Ограничивающий прямоугольник
обычно не виден, поэтому я выводил его потом вызовом функции
DrawRectangle - так легче понять смысл иллюстраций.
Рис. 3. Результат работы с параметрами StringFormat по умолчанию
На первой из этих иллюстраций показано поведение по умолчанию. Текст
переносится, видны неполные строки, а все, что находится за границами
прямоугольника, отсекается (не рисуется).
Рис. 4. Включение LineLimit предотвращает появление неполных строк
При включенном параметре
LineLimit выводятся только полные строки. Отсутствие дополнительной
строки слегка меняет перенос слов.
Рис. 5. При NoClip текст может появляться вне ограничивающего
прямоугольника
Как показано на рис. 5, при отключении параметра LineLimit и
включении NoClip части строк, отсеченные ограничивающим
прямоугольником, становятся видны.
Автоматическое добавление многоточий
Если текст обрезается ограничительным прямоугольником, с помощью
объекта StringFormat (через свойство
Trimming) можно добавлять многоточия взамен последних нескольких
символов (EllipseCharacter), за последним полностью видимым
словом (EllipseWord) или взамен средней части строки, так чтобы
начало и конец строки были видимы (EllipsePath). Все три варианта
(слева направо: EllipseCharacter, EllipseWord и
EllipsePath) показаны на рис. 6.
Рис. 6. Если строка не помещается полностью в ограничивающий
прямоугольник, с помощью свойства Trimming можно автоматически добавлять
многоточия
Индикаторы "горячих" клавиш
Еще одно настраиваемое свойство этого класса -
HotkeyPrefix, очень полезное для самостоятельной отрисоввки
элементов управления Windows Forms. Оно влияет на то, как DrawString
обрабатывает символы &, и может принимать значения Hide, Show
или None. Если оно равно Show, символ & перед буквой
означает, что буква является "горячей клавишей", и тогда буква
подчеркивается. При указании Hide буква не подчеркивается, но и
сам символ & не выводится, а в случае None этот символ
обрабатывается как обычный текст. На рисунке ниже показаны результаты
действия трех значений (Show, Hide и None) для
строки "&Print".
Рис. 7. Свойство HotkeyPrefix полезно при создании собственных
элементов управления
Выравнивание текста
Наконец, класс StringFormat позволяет задавать выравнивание (alignment)
с использованием far, near и center вместо left, right и center, так что
эти значения будут работать в любом языке независимо от направления
чтения текста. В английском языке текст читается слева направо, там эти
значения преобразуются в выравнивание по правому (far) и левому (near)
краю, а center так и остается выравниванием по центру. Независимо от
способа выравнивания оно влияет на положение текста относительно
ограничивающего прямоугольника. Если ограничивающий прямоугольник не
задан, выравнивание влияет на интерпретацию координат x и y, заданных
для вывода строки. Без ограничивающего прямоугольника параметр Alignment
= Center центрирует строку относительно координаты x; когда Alignment =
Far, координата x считается конечной точкой строки, а при значении
Alignment = Near (по умолчанию) строка начинается с позиции x. На рис. 8
показано три варианта этого параметра (near, far и center), при этом для
примеров в верхней половине рисунка указан ограничивающий прямоугольник,
а в нижней задана только пара координат x и y (верхний левый угол
нарисованной линии). Обратите внимание: в системе с направлением текста
справа налево результаты были бы другими.
Рис. 8. Эффект от применения свойства Alignment зависит от наличия
или отсутствия ограничивающего прямоугольника
Все примеры DrawString из этого раздела включены во второй
проект, PlayingWithDrawString (его можно скачать вместе с
исходным кодом, прилагаемым к этой статье). Фактически я использовал
этот проект и для создания всех изображений, представленных в данном
разделе, поэтому то, что вы видите, действительно соответствует тому,
что вы получаете!
Обработка столбцов
Во многих отчетах, особенно в табличных, данные состоят из столбцов.
Для каждого столбца необходимо определить ряд параметров, в том числе
источник данных столбца (скажем, поле в источнике данных), ширину
столбца на странице, шрифт, выравнивание содержимого столбца и т. д. Так
как основным содержанием моего табличного отчета являются столбцы, я
решил создать специальный класс ColumnInformation, а затем
пользоваться набором (collection) объектов этого типа, чтобы определить,
какие столбы (точнее - поля) должны быть в каждой строке данных. Этот
класс содержит всю информацию, необходимую для правильного отображения
всех столбцов, и даже два свойства (HeaderFont и HeaderText),
зарезервированные на случай, если в будущем мне захочется добавить к
коду отчета строку заголовка. Для облегчения восприятия листинга я не
привожу закрытые члены свойств и сам код процедур. Полный исходный код
см. в материалах, которые можно скачать для этой статьи.
Public Class ColumnInformation
Public Event FormatColumn(ByVal sender As Object, _
ByRef e As FormatColumnEventArgs)
Public Function GetString(ByVal Value As Object)
Dim e As New FormatColumnEventArgs()
e.OriginalValue = Value
e.StringValue = CStr(Value)
RaiseEvent FormatColumn(CObj(Me), e)
Return e.StringValue
End Function
Public Sub New(ByVal Field As String, _
ByVal Width As Single, _
ByVal Alignment As StringAlignment)
m_Field = Field
m_Width = Width
m_Alignment = Alignment
End Sub
Public Property Field() As String
Public Property Width() As Single
Public Property Alignment() As StringAlignment
Public Property HeaderFont() As Font
Public Property HeaderText() As String
Public Property DetailFont() As Font
End Class
Public Class FormatColumnEventArgs
Inherits EventArgs
Public Property OriginalValue() As Object
Public Property StringValue() As String
End Class
Кроме информации, описывающей столбцы, я создал событие
FormatColumn, позволяющее писать собственный форматирующий код для
преобразования значений из базы данных в строки. Ниже приведен пример
обработчика, преобразующего числовое поле базы данных в денежный формат
(currency).
Public Sub FormatCurrencyColumn(ByVal sender As Object, _
ByRef e As FormatColumnEventArgs)
Dim incomingValue As Decimal
Dim outgoingValue As String
incomingValue = CDec(e.OriginalValue)
outgoingValue = String.Format("{0:C}", incomingValue)
e.StringValue = outgoingValue
End Sub
Прежде чем выводить отчет, я заполняю
ArrayList объектами ColumnInformation, при необходимости
присоединяя обработчики FormatColumn.
Dim Columns As New ArrayList()
Public Sub PrintDoc()
Columns.Clear()
Dim titleInfo As _
New ColumnInformation("title", 2, StringAlignment.Near)
Columns.Add(titleInfo)
Dim authorInfo As _
New ColumnInformation("author", 2, StringAlignment.Near)
Columns.Add(authorInfo)
Dim bookPrice As _
New ColumnInformation("author", 2, StringAlignment.Near)
AddHandler bookPrice.FormatColumn, AddressOf FormatCurrencyColumn
Columns.Add(bookPrice)
Me.PrintPreviewDialog1.ShowDialog()
End Sub
Для каждой строки в источнике данных код PrintDetailRow
перебирает объекты в ArrayList и выводит содержимое каждого
столбца. Код этой функции см. в материалах для скачивания; загляните в
него, чтобы понять, как там используются возможности, описанные в
разделе
Вывод текста, для формирования столбцов.
Класс TabularReport
До сих пор я рассматривал отдельные части задачи создания отчета, но
в конечном итоге нужно собрать все эти части воедино и создать
компонент, позволяющий легко генерировать табличные отчеты (рис. 1). Я
создал новый класс, наследующий от PrintDocument, так что его
можно использовать со всеми существующими средствами печати типа
PrintDialog, PrintPreviewDialog и
PrintPreviewControl. Полный исходный код достаточно длинный, так что
я расскажу об этапах его разработки, а затем поясню, как применять такой
класс в ваших приложениях.
Добавление свойств
Чтобы вы могли настраивать табличный отчет, мне пришлось добавить к
классу массу свойств, позволяющих настраивать столбцы, указывать
источник данных и изменять общий вид всего отчета.
Настройка столбцов
В дополнение к классу ColumnInformation я мог бы создать
строго типизированный класс набора для хранения нескольких экземпляров
ColumnInformation, что сравнительно несложно сделать с помощью
Strongly Typed Collection Generator с Web-сайта GotDotNet. Я не стал
двигаться в этом направлении, так как мне хотелось, чтобы доступ ко всем
столбцам осуществлялся через мой класс; вместо этого я решил просто
воспользоваться ArrayList и создать в классе отчета несколько
методов для строго типизированного доступа к этому списку.
Protected m_Columns As New ArrayList()
Public Function AddColumn(ByVal ci As ColumnInformation) As Integer
Return m_Columns.Add(ci)
End Function
Public Sub RemoveColumn(ByVal index As Integer)
m_Columns.RemoveAt(index)
End Sub
Public Function GetColumn(ByVal index As Integer) As ColumnInformation
Return CType(m_Columns(index), ColumnInformation)
End Function
Public Function ColumnCount() As Integer
Return m_Columns.Count
End Function
Public Sub ClearColumns()
m_Columns.Clear()
End Sub
Настройка внешнего вида отчета
Мой класс отчета довольно гибок, так как его пользователи могут
настроить все шрифты и кисти, а также высоту каждого раздела отчета. Для
этого я предусмотрел открытые процедуры-свойства и внутренние
переменные, инициализируемые значениями по умолчанию. Я также создал
свойства DefaultReportFont и DefaultReportBrush,
позволяющие указывать объекты
Font и
Brush, применяемые по умолчанию ко всему отчету, если не заданы
более специфичные свойства. Код первого из них приведен ниже, а второго
- опущен, поскольку он делает практически те же, что и первый (только с
объектом Brush).
Protected m_DefaultReportFont As Font = _
New Font("Arial", 12, FontStyle.Bold, GraphicsUnit.Point)
Protected m_HeaderFont As Font
Protected m_FooterFont As Font
Protected m_DetailFont As Font
Public Property DefaultReportFont() As Font
Get
Return m_DefaultReportFont
End Get
Set(ByVal Value As Font)
If Not Value Is Nothing Then
m_DefaultReportFont = Value
End If
End Set
End Property
Public Property HeaderFont() As Font
Get
If m_HeaderFont Is Nothing Then
Return m_DefaultReportFont
Else
Return m_HeaderFont
End If
End Get
Set(ByVal Value As Font)
m_HeaderFont = Value
End Set
End Property
Public Property FooterFont() As Font
Get
If m_FooterFont Is Nothing Then
Return m_DefaultReportFont
Else
Return m_FooterFont
End If
End Get
Set(ByVal Value As Font)
m_FooterFont = Value
End Set
End Property
Public Property DetailFont() As Font
Get
If m_DetailFont Is Nothing Then
Return m_DefaultReportFont
Else
Return m_DetailFont
End If
End Get
Set(ByVal Value As Font)
m_DetailFont = Value
End Set
End Property
Кроме объектов Font и Brush, я реализовал свойства,
позволяющие управлять высотой разделов отчета (устанавливая минимальную
и максимальную высоту для раздела Detail).
Protected m_HeaderHeight As Single = 1
Protected m_FooterHeight As Single = 1
Protected m_MaxDetailRowHeight As Single = 1
Protected m_MinDetailRowHeight As Single = 0.5
Public Property HeaderHeight() As Single
Get
Return m_HeaderHeight
End Get
Set(ByVal Value As Single)
m_HeaderHeight = Value
End Set
End Property
Public Property FooterHeight() As Single
Get
Return m_FooterHeight
End Get
Set(ByVal Value As Single)
m_FooterHeight = Value
End Set
End Property
Public Property MaxDetailRowHeight() As Single
Get
Return m_MaxDetailRowHeight
End Get
Set(ByVal Value As Single)
m_MaxDetailRowHeight = Value
End Set
End Property
Public Property MinDetailRowHeight() As Single
Get
Return m_MinDetailRowHeight
End Get
Set(ByVal Value As Single)
m_MinDetailRowHeight = Value
End Set
End Property
Настройка источника данных
Чтобы создать отчет, нужен доступ к данным, поэтому я добавил
свойство, принимающее экземпляр DataView, строки которого
перебираются в перегруженной версии OnPrintPage.
Protected m_DataView As DataView
Public Property DataView() As DataView
Get
Return m_DataView
End Get
Set(ByVal Value As DataView)
m_DataView = Value
End Set
End Property
Protected Function GetField(ByVal row As DataRowView, ByVal fieldName As String) As Object
Dim obj As Object = Nothing
If Not m_DataView Is Nothing Then
obj = row(fieldName)
End If
Return obj
End Function
'важный фрагмент из OnPrintPage
Dim rowCounter As Integer
e.HasMorePages = False
For rowCounter = currentRow To Me.DataView.Count - 1
Dim currentRowHeight As Single = _
PrintDetailRow(leftMargin, _
currentPosition, Me.MinDetailRowHeight, _
Me.MaxDetailRowHeight, width, _
e, Me.DataView(rowCounter), True)
If currentPosition + currentRowHeight < footerBounds.Y Then
'поместится на странице
currentPosition += _
PrintDetailRow(leftMargin, currentPosition, _
MinDetailRowHeight, MaxDetailRowHeight, _
width, e, Me.DataView(rowCounter), False)
Else
e.HasMorePages = True
currentRow = rowCounter
Exit For
End If
Next
Печать отдельных разделов отчета
Одних только свойств недостаточно, в конце концов отчет нужно
напечатать. Но я постоянно обращаюсь ко всем этим свойствам, проверяя,
что результат печати совпадает с указанными пользователем параметрами.
Печать отчета контролируется процедурой OnPrintPage базового
класса (PrintDocument), перегруженной в моем классе-потомке;
именно оттуда вызываются процедуры печати отдельных разделов (PrintPageHeader,
PrintPageFooter и PrintDetailRow). Как и в ранее
приведенных примерах, эти процедуры достаточно велики и в силу этого их
код не приводится в статье. Вместо этого предлагаю скачать код и
запустить тестовое приложение.
Применение класса TabularReport
После того как я закончил класс TabularReport, для создания
отчета-примера понадобилось всего несколько шагов.
Шаг 1: создание экземпляра TabularReport
Сначала создайте экземпляр моего класса. Вообще-то это можно сделать
и прямо в коде, но, так как этот класс - наследник PrintDocument,
вы можете добавить мой компонент в окно инструментария (toolbox) и
перетащить его на разрабатываемую Windows-форму.
Шаг 2: получение данных
Чтобы получить данные для отчета, вам потребуется экземпляр
DataView, который вы можете получить, заполнив
DataTable результатами, возвращенными хранимой процедурой или
SQL-запросом.
Private Function GetData() As DataView
Dim Conn As New OleDbConnection(connectionString)
Conn.Open()
'Вариант с базой данных Access
Dim getOrdersSQL As String = _
"SELECT Customers.ContactName, Orders.OrderID, Orders.OrderDate,
Orders.ShippedDate, Sum([UnitPrice]*[Quantity]) AS Total FROM (Customers
INNER JOIN Orders ON Customers.CustomerID = Orders.CustomerID) INNER JOIN
[Order Details] ON Orders.OrderID = [Order Details].OrderID GROUP BY
Customers.ContactName, Orders.OrderID, Orders.OrderDate,
Orders.ShippedDate ORDER BY Orders.OrderDate"
Dim getOrders As New OleDbCommand(getOrdersSQL, Conn)
getOrders.CommandType = CommandType.Text
Dim daOrders As New OleDbDataAdapter(getOrders)
Dim orders As New DataTable("Orders")
daOrders.Fill(orders)
Return orders.DefaultView
End Function
В этом примере я получаю данные от базы данных Access, поэтому я
применяю классы OleDB, но вы можете использовать любую базу по
своему выбору: все, что нужно моему отчету, - результирующий DataView.
Шаг 3: настройка столбцов отчета
На этом шаге надо создать объекты ColumnInformation для всех
столбцов отчета и добавить их в набор столбцов TabularReport. Я
поместил этот код в процедуру SetupReport, настраивающую столбцы
и другие детали внешнего вида отчета. Я уже рассматривал объекты
ColumnInformation в разделе
Обработка столбцов, а здесь покажу, как они настраиваются в
приложении-примере.
Private Sub SetupReport()
'Получение данных
Dim orders As DataView
orders = GetData()
'Настройка столбцов
Dim contactName _
As New ColumnInformation("ContactName", 2, _
StringAlignment.Near)
Dim orderID _
As New ColumnInformation("OrderID", 1, _
StringAlignment.Near)
Dim orderDate _
As New ColumnInformation("OrderDate", 1, _
StringAlignment.Center)
AddHandler orderDate.FormatColumn, AddressOf FormatDateColumn
Dim shippedDate _
As New ColumnInformation("ShippedDate", 1, _
StringAlignment.Center)
AddHandler shippedDate.FormatColumn, AddressOf FormatDateColumn
Dim total _
As New ColumnInformation("Total", 1.5, _
StringAlignment.Far)
AddHandler total.FormatColumn, AddressOf FormatCurrencyColumn
With TabularReport1
.ClearColumns()
.AddColumn(contactName)
.AddColumn(orderID)
.AddColumn(orderDate)
.AddColumn(shippedDate)
.AddColumn(total)
.DataView = orders
.HeaderHeight = 0.5
.FooterHeight = 0.3
.DetailFont = New Font("Arial", _
12, FontStyle.Regular, _
GraphicsUnit.Point)
.DetailBrush = Brushes.DarkKhaki
.DocumentName = "Order Summary From Northwinds Database"
End With
End Sub
При настройке трех столбцов - orderDate, shippedDate и total - для
каждого из них вызывается
AddHandler, связывающий столбцы с процедурами, форматирующими их
данные в виде денежного значения или даты.
Public Sub FormatCurrencyColumn(ByVal sender As Object, _
ByRef e As FormatColumnEventArgs)
Dim incomingValue As Decimal
Dim outgoingValue As String
If Not IsDBNull(e.OriginalValue) Then
incomingValue = CDec(e.OriginalValue)
Else
incomingValue = 0
End If
outgoingValue = String.Format("{0:C}", incomingValue)
e.StringValue = outgoingValue
End Sub
Public Sub FormatDateColumn(ByVal sender As Object, _
ByRef e As FormatColumnEventArgs)
Dim incomingValue As Date
Dim outgoingValue As String
If Not IsDBNull(e.OriginalValue) Then
incomingValue = CDate(e.OriginalValue)
outgoingValue = incomingValue.ToShortDateString()
Else
outgoingValue = "-"
End If
e.StringValue = outgoingValue
End Sub
Что бы вы ни писали в обработчике события FormatColumn,
помните, что он должен работать максимально быстро, так как для каждого
столбца, к которому прикреплен такой обработчик, он вызывается при
печати одной строки дважды. В этой ситуации заметна даже минимальная
задержка. Такая реализация форматирования столбцов имеет свои плюсы и
минусы. Плюс заключается в том, что вы можете реализовать сколь угодно
сложное форматирование и отчет получается более гибким, а минус -
довольно трудно реализовать даже простое форматирование. С другой
стороны, вы можете создать класс ColumnInformation таким, чтобы
простые форматы вроде денежного конвертировались с помощью свойства.
Если встроить в класс поддержку форматирования на простом уровне,
обработчик события FormatColumn потребуется только в сложных
случаях и общая производительность увеличится.
Шаг 4: печать или предварительный просмотр документа
Теперь вызывая метод Print класса TabularReport или
один из элементов управления Windows Forms (а именно PrintDialog,
PrintPreviewDialog или PrintPreviewControl), способных
взаимодействовать с объектом PrintDocument, вы можете печатать
или просматривать настроенный отчет. Любой элемент управления,
содержащий свойство типа PrintDocument, прекрасно работает с
экземпляром TabularReport, так как наследует от класса
PrintDocument.
Заключение
Создание отчетов - очень распространенная задача, и вряд ли вам
захочется писать код каждый раз, когда возникает необходимость составить
какой-то отчет. Вместо этого я предлагаю вам создать средство для
создания мини-отчетов вроде моего класса TabularReport и сделать
его максимально гибким. Посмотрите, как это реализовано в моем примере.
Если ваше средство отчетов не поддерживает какой-то специфический отчет,
вы всегда можете создать новый класс, наследующий от PageDocument
или TabularDocument, или добавить новую функциональность,
необходимую для вашего отчета.