среда, 16 февраля 2011 г.

Использование PDF шаблонов C#.

Исходники к статье

Продолжая тему шаблонов, хочу раскрыть использование PDF вместе с закрытым исходным кодом. В сети можно найти достаточное количество статей, которые описывают данный процесс, но в основном они используют GNU General Public License библиотеки, а это ведет к открытию своего кода. Проект менеджер услышав об этом, вновь может посылать вас в лес, искать бесплатные отвертки…

Хочу обратить ваше внимание на проект PdfSharp и его лицензию, разрешающую использование в коммерческих целях.

Как использовать шаблоны? Немного теории:

Стандартный подход прост – графически рисуется требуемая форма, расставляются закладки или некие ключевые слова (наподобие @Name@), которые после отыскиваются в документе и заменяются на необходимые значения. Но, к сожалению, PDFsharp не поддерживает парсинг документа, этот подход потерпит фиаско.

Как же быть? Есть два выхода из положения, каждый из них требует больших трудозатрат, чем использование GPL библиотек (можете поискать реализации на iText), первый это полностью генерировать бланки без использования шаблонов, но при сложном бланке на это дело можно потратить чуть ли не целый день на бланк. И второй способ предлагаемый нам PDFsharp, это использовать шаблон как холст для рисования. Представьте, что у нас на столе, под стеклом лежит бланк документа и есть карандаш или маркер, которым мы рисуем на стекле, если стекло достаточно тонкое, то человек, взглянувший на такое "художество", будет считать его единым целым. Далее, мы как раз и будем рисовать на шаблоне, с помощью System.Drawing, подставляя нужные нам данные в требуемые места.

Предположим, в некой мнимой компании «Ключи и Отвертки», для отдела кадров, необходимо создать бланк сотрудника, в котором будут находиться контактные данные, фотография и таблица прихода и ухода на работу за последнюю неделю, и как всегда все это должно быть на фирменном бланке компании.

Не обладая большими талантами дизайнера, я приготовил такой бланк, и самолично его утвердил. :)

c# pdf template

Создадим новое консольное приложение с именем UsingPdfTemplate, далее добавим к проекту ссылки на сборки PdfSharp.dll из каталога GDI+ (заранее скачанные с сайта проекта, на момент написания статьи версия 1_31), а так же на System.Drawing. Добавим к проекту файл template.pdf (взять можно из исходников к статье) и установим его свойство Build Action – Embedded Resource и Copy to Output Directory – Copy always.

Далее введем следующий код:



using System;
using System.Diagnostics;
using PdfSharp.Drawing;
using PdfSharp.Pdf;
using PdfSharp.Pdf.IO;

namespace UsingPdfTemplate
{
class Program
{
static void Main(string[] args)
{
PdfDocument inputDocument = PdfReader.Open("template.pdf", PdfDocumentOpenMode.Import);
PdfPage notEditablePage = inputDocument.Pages[0];

PdfDocument outputDocument = new PdfDocument();
PdfPage editablePage = outputDocument.AddPage(notEditablePage);

outputDocument.Save("newPdf.pdf");
Process.Start("newPdf.pdf");
}
}
}



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

Теперь заполним поля «Должность», «Имя», «Email», добавим фотографию и таблицу посещений:




PdfPage editablePage = outputDocument.AddPage(notEditablePage);

XGraphics gfx = XGraphics.FromPdfPage(editablePage);
DrawFields(gfx);
DrawImage(gfx);
DrawTable(gfx);


outputDocument.Save("newPdf.pdf");




Получаем объект XGraphics и передаем его в методы DrawFields(), DrawImage(), DrawTable().

DrawFields отрисует поля на шаблоне, код метода:



private static void DrawFields(XGraphics gfx)
{
XPdfFontOptions fontOptions = new XPdfFontOptions(PdfFontEncoding.Unicode, PdfFontEmbedding.Always);
XFont font = new XFont("Times New Roman", 10, XFontStyle.Bold, fontOptions);

gfx.DrawString("Программист", font, XBrushes.Black, 100, 135);
gfx.DrawString("Александр Кобелев", font, XBrushes.Black, 100, 160);
gfx.DrawString("Kobelev.Alexander@gmail.com", font, XBrushes.Black, 100, 185);
}



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

Для отрисовки фотографии добавим в проект фото (можно так же взять из исходников фото моего кота), и пометим его свойство Build Action – Embedded Resource и Copy to Output Directory – Copy always.

DrawImage отрисует фотографию, код метода:



private static void DrawImage(XGraphics gfx)
{
XImage img = XImage.FromFile("cat.jpg");
gfx.DrawImage(img, 350, 125, 120, 160);
}



Очень простой метод - мы лишь получаем изображение из файла (XImage можно так же получить, используя System.Drawing.Image, который можно получить из потока) и отрисовываем его на документе по координатам с заданными размерами.

С таблицей будет посложнее, дело в том что PdfSharp ничего не знает о таблицах и способен их нарисовать только из линий, но не стоит падать духом, так как есть замечательная библиотека MigraDoc, которая поставляется вместе с PdfSharp. Нам лишь надо добавить ссылку на библиотеку MigraDoc.DocumentObjectModel.dll и MigraDoc.Rendering.dll.

DrawTable отрисует таблицу, код метода:



...
using MigraDoc.DocumentObjectModel;
using MigraDoc.DocumentObjectModel.Tables;
using MigraDoc.Rendering;

private static void DrawTable(XGraphics gfx)
{

Document doc = new Document();

Table table = new Table();
table.Borders.Width = 0.3;
table.AddColumn(76);
table.AddColumn(78.2);
table.AddColumn(77.6);

for (int day = 7; day > 0; day--)
{
var row = table.AddRow();
row[0].AddParagraph(DateTime.Now.AddDays(-day).ToString("dd/MM/yyyy"));
row[1].AddParagraph(DateTime.Now.AddDays(-day).ToString("HH:mm"));
row[2].AddParagraph(DateTime.Now.AddDays(-day).AddHours(8).ToString("HH:mm"));
}

doc.AddSection().Add(table);

DocumentRenderer docRenderer = new DocumentRenderer(doc);
docRenderer.PrepareDocument();
docRenderer.RenderObject(gfx, 36, 257, 0, table);
}



В этом методе мы вначале создаем MigraDoc.DocumentObjectModel.Document, потом таблицу, выставляем у таблицы ширину бордюра и размеры колонок. После чего заполняем семь строк данными и добавляем таблицу в документ, после создаем объект класса MigraDoc.Rendering.DocumentRenderer, который принимает в качестве параметра конструктора документ содержащий таблицу, подготавливаем и отрисовываем его, используя объект XGraphics переданный в метод.

Как результат у нас появляется заполненный PDF документ на основе шаблона:

c# completed pdf template

Александр Кобелев aka Megano

среда, 9 февраля 2011 г.

Использование WORD шаблонов C# 4.0

Инструментарий:
• Microsoft Visual Studio 2010
• Microsoft Office 2010

исходники к статье


Данная статья является адаптацией более ранней статьи «Использование WORD шаблонов» с учетом новых возможностей вошедших в C# 4.0 таких как: именованные параметры, пропуск ключевого слова ref при работе с COM, индексируемые свойства и необязательные параметры. Вы можете сравнить количество кода с предыдущей статьей и увидеть, что кода сало много меньше, так же он стал более читабельным.

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

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

Предположим, существует некая компания «Ключи и Отвертки», которая занимается продажами обозначенных инструментов, и менеджеры компании хотят видеть ежедневный отчет о продажах в своих офисах.

Начнем с простого шаблона, который будет представлять из себя фирменный бланк компании и единственное поле для вставки данных. Создадим новый WORD документ и уменьшим отступы (Page Layout -> Margins -> Narrow).


Margins


После этого развернем документ в альбомную ориентацию (Page Layout -> Orientation -> Landscape).

Orientation

Далее создадим шапку (Insert -> Header –> Alpabet).

Footer

Напишем название. Далее впишем дополнительную информацию о компании и пометим ее курсивом с выравниванием по правому краю.

После создания шапки в документ автоматически был добавлен пустой подвал, перейти в редактирование которого можно просто перенеся туда курсор. Визуально отделим его от тела документа при помощи разделительной линии. (Insert –> Shapes -> Lines)

Line

Далее, в подвале сделаем выравнивание по правому краю и создадим закладку (Bookmark), которую будем отыскивать в коде и на ее место вставлять данные (Insert -> Bookmark).

InsertBookmark

И назовем ее AuthorName.

На этом завершим создание фирменного бланка, сохраним шаблон с именем screwdriver.dotx (Word Template)

Следующим шагом создадим из кода новый документ Word, на основе нашего шаблона, впишем автора документа и сохраним его на диск.

Создадим в Visual Studio WPF приложение с названием UsingWordTemplate ( File -> New -> Project -> Windows -> WPF Application).

Добавим на форму Label (Content="Автор:"), TextBox (Name=”autorTxtb”), в который будем вписывать имя автора документа и кнопку (Content="Сохранить" Name=”saveBtn”), с помощью которой будем сохранять созданный документ.

Добавим ссылку на .Net библиотеку Microsoft.Office.Interop.Word Version 14. В шапке кода главного окна добавим


using Word = Microsoft.Office.Interop.Word;


После чего определим переменную


Word._Application oWord = new Word.Application();


Переменная oWord будет представлять процесс WINWORD.EXE в памяти.

Так же определим событие формы Closing, в котором мы будем закрывать процесс WINWORD.EXE


private void Window_Closing(object sender, System.ComponentModel.CancelEventArgs e)
{
oWord.Quit();
}



Примечание:

В предыдущем фрагменте кода мы незаметно использовали новую возможность C# 4.0 - необязательные параметры. Если сравнить с предыдущим вариантом статьи, то тогда нам потребовалось определить переменную oMissing представляющую из себя аргумент представляющий значение по умолчанию, который требуют методы из пространства Microsoft.Office.Interop. И вызов выглядел следующим образом:


object oMissing = System.Reflection.Missing.Value;

private void Window_Closing(object sender, System.ComponentModel.CancelEventArgs e)
{
oWord.Quit(ref oMissing, ref oMissing, ref oMissing);
}




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

Вызовем контекстное меню текущего проекта и добавим в него заранее созданный шаблон screwdriver.dotx (Add -> Existing Item -> screwdriver.dotx), в его свойствах определим Copy To Output Directory как Copy always.

Определим обработчик кнопки:


private void saveBtn_Click(object sender, RoutedEventArgs e)
{
_Document oDoc = GetDoc(Environment.CurrentDirectory + "\\screwdriver.dotx");
oDoc.SaveAs(FileName: Environment.CurrentDirectory + "\\New.docx");
oDoc.Close();
}


Загружаем сохраненный шаблон, заполняем его данными с формы и сохраняем на диск, после чего закрываем документ.


Примечание:

Здесь мы вновь используем пропуск ключевого слова ref,необязательные параметры и так же новую возможность – именованные параметры. Для сравнения посмотрим на код которым мы пользовались ранее:

private void SaveToDisk(Word._Document oDoc, string filePath)
{
object fileName = filePath;
oDoc.SaveAs(ref fileName, ref oMissing, ref oMissing, ref oMissing, ref oMissing, ref oMissing, ref oMissing, ref oMissing, ref oMissing, ref oMissing, ref oMissing, ref oMissing, ref oMissing, ref oMissing, ref oMissing, ref oMissing);
}





Примечание:

Остерегайтесь использования Environment.CurrentDirectory так как значение может измениться в процессе работы.


static void Main(string[] args)
{
Console.WriteLine(Environment.CurrentDirectory);
Environment.CurrentDirectory = "c:\\";
Console.WriteLine(Environment.CurrentDirectory);
}


Как показано выше мы можем сами изменить ее значение, в данном случае Environment.CurrentDirectory больше указывает не на каталог приложения, а на корень диска C.


Ниже приведен код вызванных методов:


private _Document GetDoc(string path)
{
_Document oDoc = oWord.Documents.Add(path);
SetTemplate(oDoc);
return oDoc;
}

private void SetTemplate(Word._Document oDoc)
{
oDoc.Bookmarks["AuthorName"].Range.Text = autorTxtb.Text;
}


Сигнатуры вызываемых методов из пространства имен Microsoft.Office.Interop можно посмотреть по адресу:

http://msdn.microsoft.com/ru-ru/library/microsoft.office.interop.word%28en-us%29.aspx

Введем в текст бокс данные автора документа “Александр Кобелев” и сохраним документ на диск нажав кнопку “Сохранить”. В папке Debug нашего приложения появится файл New.docx, содержащий введенные данные на месте bookmark AuthorName.

Теперь добавим еще одну кнопку (Content="Print" Name="prntBtn") на форму, которая будет распечатывать документ на принтер и определим ее обработчик.


private void prntBtn_Click(object sender, RoutedEventArgs e)
{
_Document oDoc = GetDoc(Environment.CurrentDirectory + "\\screwdriver.dotx");
PrintDoc(oDoc);
oDoc.Close(WdSaveOptions.wdDoNotSaveChanges)
}


Здесь все то же самое, что и в предыдущем случае, только вместо метода SaveToDisk вызывается PrintOut() и перед закрытием документа явно определяется, что документ не стоит сохранять. В предыдущем случае, при сохранении документа на диск, он помечался как сохраненный и не требовалось явно указывать этот параметр.

Теперь добавим на шаблон таблицу 3 на 4, которая будет отображать продажи по городам. Верхняя строчка обозначит ключи и отвертки, а первый столбик города: Москва, Санкт-Петербург, Челябинск. Разукрасим таблицу по своему вкусу.

table

Далее мы могли бы поставить шесть закладок (boomark), задать им уникальные имена и искать их как в предыдущем случае, но это слишком накладно. Можно поступить проще – выделить всю таблицу и добавить закладку на все выделение сразу.

selectTable

Назовем новую закладку “SaleTable”.

Добавим на форму DataGrid, со свойством AutoGenerateColumns="True", а в код новую переменную

В конструкторе вызовем метод, который заполнит DataTable и свяжет его с DataGrid


public MainWindow()
{
InitializeComponent();
InitializeDataGrid();
}

private void InitializeDataGrid()
{
table = new DataTable();
table.Columns.Add();
table.Columns.Add();
table.Columns.Add();
var row = table.NewRow();
row[0] = "Город";
row[1] = "Ключи";
row[2] = "Отвертки";
table.Rows.Add(row);
table.Rows.Add(table.NewRow()[0] = "Москва");
table.Rows.Add(table.NewRow()[0] = "Санкт-Петербург");
table.Rows.Add(table.NewRow()[0] = "Челябинск");
dataGrid1.DataContext = table;
}


Добавим в метод SetTemplate() вызов метода SetTable() который будет заполнять данными таблицу.


private void SetTemplate(Word._Document oDoc)
{
. . .
SetTable(oDoc,"SaleTable",table);
}

private void SetTable(Word._Document oDoc, string bookmark, DataTable dataContext)
{
dynamic tbl = oDoc.Bookmarks[bookmark].Range.Tables[1];
int tblRow = 0;
int tblCell = 0;
foreach (Word.Column col in tbl.Columns)
{
foreach (Word.Cell cell in col.Cells)
{
SetCell(cell, (string)dataContext.Rows[tblRow][tblCell]);
tblRow++;
}
tblCell++;
tblRow = 0;
}
}


Как видно из листинга, мы перебираем по очереди все ячейки в шаблоне и сопоставляем их с данными из DataTable.

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

Для реализации немного изменим метод SetTable() , заменив явное присваивание на вызов метода SetCell().


...
foreach (Word.Cell cell in col.Cells)
{
SetCell(cell, (string)dataContext.Rows[tblRow][tblCell]);
tblRow++;
}
...

private void SetCell(Word.Cell cell, string text)
{
int val = 0;
if (int.TryParse(text, out val))
{
if (val < 100) cell.Shading.BackgroundPatternColor = Word.WdColor.wdColorRose;
if (val > 500) cell.Shading.BackgroundPatternColor = Word.WdColor.wdColorLightGreen;
}
cell.Range.Text = text;
}


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

Перейдем к графикам.

Для использования графиков потребуется ссылка на .Net библиотеку Microsoft.Office.Interop.Excel Version 14. Так же в шапке кода главного окна добавим using


using Excel = Microsoft.Office.Interop.Excel;


Сделаем несколько отступов вниз, для создания пространства между таблицей и графиком, добавим в шаблон график (Insert -> Chart -> Column -> 3d Column)

insertChart

3dColumn

и выставим все значения по нулям.

chartDefValue

После чего выделим график на шаблоне (кликнув по краю графика) и создадим новую закладку “ChartBookmark”, далее сохраним изменения в шаблоне.

Добавим в метод SetTemplate() вызов метода SetChart(), который будет заполнять данными график.


private void SetTemplate(Word._Document oDoc)
{
...
SetChart(oDoc, "ChartBookmark", table);
}

private void SetChart(Word._Document oDoc, string bookmark, DataTable dataContext)
{
Word.Chart chart = oDoc.Bookmarks[bookmark].Range.InlineShapes[1].Chart;
Word.ChartData chartData = chart.ChartData;
chartData.Activate();

Excel.Workbook dataWorkbook = (Excel.Workbook)chartData.Workbook;
Excel.Worksheet dataSheet = (Excel.Worksheet)dataWorkbook.Worksheets[1];
dataSheet.Cells.Range["B2"].FormulaR1C1 = dataContext.Rows[1][1];
dataSheet.Cells.Range["B3"].FormulaR1C1 = dataContext.Rows[1][2];
dataSheet.Cells.Range["C2"].FormulaR1C1 = dataContext.Rows[2][1];
dataSheet.Cells.Range["C3"].FormulaR1C1 = dataContext.Rows[2][2];
dataSheet.Cells.Range["D2"].FormulaR1C1 = dataContext.Rows[3][1];
dataSheet.Cells.Range["D3"].FormulaR1C1 = dataContext.Rows[3][2];
dataWorkbook.Close();
}


Теперь приложение полностью готово к работе, введем некоторые данные, чтобы провести функциональный тест. В строке автор я введу “Александр Кобелев” а значения продаж по городам у меня будут такие: Ключи/Отвертки Москва – 700/400, Санкт-Петербург – 50/300, Челябинск – 100/200. После нажатия кнопки “Сохранить” в папке Debug приложения появился файл New.docx, такого вида:

end

Как видно из статьи, изменения вышедшие в C# 4.0 - намного сокращают код необходимый для работы с СОМ, в частности при работе с Office.

Александр Кобелев aka Megano