Разрабатываем печать документов на .NET с помощью OpenXml. Часть 2
Всем привет! Я Александр Родов, ведущий разработчик в компании «БАРС Груп», автор и руководитель разработки сервиса генерации печатных форм Sprinter. Этой статьей мы продолжаем рассказ о возможностях использования библиотек DocumentFormat.OpenXml для генерации печатных файлов «офисных» форматов.
Напомним, в предыдущей части мы сформулировали постановку нашей демонстрационной задачи, а именно разработать печать данных заказа в некотором абстрактном интернет-магазине. Далее реализовали первую часть документа, содержащую шапку документа и колонтитул с логотипом магазина. Подробнее с постановкой задачи и примерами исходного кода можно ознакомиться по ссылке выше. Ну а на очереди у нас — печать таблиц в docx!

Создание и настройка таблицы
Для создания пустой таблицы в docx заполним объекты с настройками таблицы, которые будут являться дочерними для объекта таблицы в структуре OpenXml. Объект TableProperties содержит описание границ и макета таблицы, а объект TableGrid — описание набора столбцов таблицы. Для границ зададим цвет, размер и стиль — одинарную границу. Для столбцов — в нашей таблице их будет шесть — укажем ширину в единицах TWIP, описание которых приводилось в предыдущей статье.
var tableProps = new TableProperties
{
TableBorders = new TableBorders
{
LeftBorder = new()
{
Color = DefaultColor,
Size = 12u,
Val = BorderValues.Single
},
RightBorder = new()
{
Color = DefaultColor,
Size = 12u,
Val = BorderValues.Single
},
TopBorder = new()
{
Color = DefaultColor,
Size = 12u,
Val = BorderValues.Single
},
BottomBorder = new()
{
Color = DefaultColor,
Size = 12u,
Val = BorderValues.Single
},
},
TableLayout = new TableLayout { Type = TableLayoutValues.Fixed }
};
var tableGrid = new TableGrid(
new GridColumn { Width = CmToTwip(1).ToString() },
new GridColumn { Width = CmToTwip(3).ToString() },
new GridColumn { Width = CmToTwip(4.5f).ToString() },
new GridColumn { Width = CmToTwip(2.5f).ToString() },
new GridColumn { Width = CmToTwip(3).ToString() },
new GridColumn { Width = CmToTwip(3).ToString() });
var wordTable = new Table(tableProps, tableGrid);
document.MainDocumentPart.Document.Body.Append(wordTable);
Далее заполним титульную строку таблицу. Для этого инициализируем объект TableRow и добавим в него шесть объектов TableCell с указанием текста заголовка и настроек границ ячейки. К самому тексту в ячейке применим настройки стилей текста.
var cellBorders = new TableCellBorders
{
LeftBorder = new()
{
Color = DefaultColor,
Size = 12u,
Val = BorderValues.Single
},
RightBorder = new()
{
Color = DefaultColor,
Size = 12u,
Val = BorderValues.Single
},
TopBorder = new()
{
Color = DefaultColor,
Size = 12u,
Val = BorderValues.Single
},
BottomBorder = new()
{
Color = DefaultColor,
Size = 12u,
Val = BorderValues.Single
},
};
var titleRow = new TableRow(new TableRowProperties(new TableHeader()));
var tableTitles = new[] { "№", "Код", "Наименование", "Количество", "Цена", "Стоимость" };
foreach (var title in tableTitles)
{
var cell = new TableCell
{
TableCellProperties = new TableCellProperties
{
Shading = new Shading { Fill = "DDDDDD", Val = ShadingPatternValues.Clear },
TableCellBorders = (TableCellBorders)cellBorders.CloneNode(true)
}
};
SetCellText(cell, title, JustificationValues.Center);
titleRow.Append(cell);
}
wordTable.Append(titleRow);
void SetCellText(TableCell cell, string text, JustificationValues justificationValues, bool? bold = null)
{
var paragraphProperties = new ParagraphProperties
{
ParagraphStyleId = new ParagraphStyleId { Val = TableStyleId },
Justification = new Justification { Val = justificationValues },
SpacingBetweenLines = new SpacingBetweenLines
{
After = "0",
Line = "360",
LineRule = LineSpacingRuleValues.Auto
}
};
cell.Append(new Paragraph(new Run(new Text(text) { Space = SpaceProcessingModeValues.Preserve })
{
RunProperties = bold.HasValue ? new RunProperties { Bold = new Bold { Val = bold.Value } } : null
})
{ ParagraphProperties = paragraphProperties });
}
Заполнение ячеек
Теперь нам необходимо внести данные заказа в таблицу. Алгоритм вставки ячеек в таблицу здесь будет аналогичен тому, как это делалось для строки шапки таблицы. Разница будет лишь в параметрах стилей (жирное начертание, горизонтальное выравнивание), которые передаются в метод SetCellText.
Ещё раз стоит обратить внимание на то, что для переиспользования одних и тех же объектов OpenXml в документе их необходимо клонировать, таково требование API OpenXml. Именно поэтому для каждой ячейки параметры границ TableCellBorders инициализируются копией переменной cellBorders. Код внесения данных заказа в таблицу ниже:
var totalSum = 0m;
var counter = 1;
foreach (var purchasePosition in data.Items)
{
var row = new TableRow(new TableRowProperties());
var numberCell = new TableCell(new TableCellProperties { TableCellBorders = (TableCellBorders)cellBorders.CloneNode(true) });
SetCellText(numberCell, counter++.ToString(), JustificationValues.Center);
row.Append(numberCell);
var codeCell = new TableCell(new TableCellProperties { TableCellBorders = (TableCellBorders)cellBorders.CloneNode(true) });
SetCellText(codeCell, purchasePosition.ProductCode, JustificationValues.Center);
row.Append(codeCell);
var nameCell = new TableCell(new TableCellProperties { TableCellBorders = (TableCellBorders)cellBorders.CloneNode(true) });
SetCellText(nameCell, purchasePosition.ProductName, JustificationValues.Left);
row.Append(nameCell);
var countCell = new TableCell(new TableCellProperties { TableCellBorders = (TableCellBorders)cellBorders.CloneNode(true) });
SetCellText(countCell, purchasePosition.Count.ToString(), JustificationValues.Center);
row.Append(countCell);
var priceCell = new TableCell(new TableCellProperties { TableCellBorders = (TableCellBorders)cellBorders.CloneNode(true) });
SetCellText(priceCell, $"{purchasePosition.UnitPrice} руб.", JustificationValues.Center);
row.Append(priceCell);
var sum = purchasePosition.Count * purchasePosition.UnitPrice;
totalSum += sum;
var sumCell = new TableCell(new TableCellProperties { TableCellBorders = (TableCellBorders)cellBorders.CloneNode(true) });
SetCellText(sumCell, $"{sum} руб.", JustificationValues.Center);
row.Append(sumCell);
wordTable.Append(row);
}
Добавление строки итогов
Для вывода суммы заказа добавим в таблицу еще одну строку и добавим ей объединение ячеек по столбцам, как показано ниже:
var totalCell = new TableCell(new TableCellProperties
{
TableCellBorders = (TableCellBorders)cellBorders.CloneNode(true),
GridSpan = new GridSpan { Val = 6 }
});
SetCellText(totalCell, $"Общая сумма заказа: {totalSum} руб.", JustificationValues.Right, bold: true);
var totalRow = new TableRow(new TableRowProperties(), totalCell);
wordTable.Append(totalRow);
Горизонтальное объединение ячеек задается с помощью свойства TableCellProperties.GridSpan с указанием количества столбцов, которые входят в объединение. В нашем случае это все шесть столбцов таблицы.
Если бы нам требовалось вертикальное объединение ячеек, для этого мы использовали бы свойство TableCellProperties.VerticalMerge. Однако, в отличие от GridSpan, в VerticalMerge уже не задается количество строк, входящих в объединение. В вертикальное объединение попадут все строки, находящиеся под выбранной ячейкой, вплоть до новой ячейки, в которой будет задано свойство VerticalMerge со значением Val=MergedCellValues.Restart.
Заключение
В этой статье мы добавили таблицу в наш печатный документ заказа в интернет-магазине. Готовый документ выглядит так:

Для реалистичности примера сюда можно также добавить прочие элементы, такие как контактные данные магазина, место для подписи клиента о получении заказа, и т.д. Здесь же мы не будем приводить вставку таких элементов, так как все наши действия и приемы в этом случае будут аналогичны уже изложенным в этой и предыдущей статьях. Например, для вставки шаблона подписи клиента можно использовать таблицу, в которой скрыты границы и задано выравнивание по правому краю листа.
На этом наш демонстрационный docx-документ завершен, а вместе с ним и обзор основных возможностей печати docx с помощью DocumentFormat.OpenXml. Безусловно, здесь демонстрируется небольшая часть возможностей, доступных в спецификации OpenXML. Формат поддерживает как целые разделы элементов, не вошедшие в наш обзор (например, рисованные фигуры и диаграммы), так и разнообразные настройки упомянутых элементов: текста, таблиц, изображений. Однако в наших статьях мы постарались охватить наиболее популярное содержимое типичного документа — текст, таблицы и изображения, а также основные приемы работы с ними в .NET.
С исходным кодом всего проекта можно ознакомиться по ссылке. А в заключительной части нашего цикла статей об использовании DocumentFormat.OpenXml мы рассмотрим еще один распространённый Use-case – печать таблиц в xlsx.
Мы в «БАРС Груп» разрабатываем цифровые решения для государства, бизнеса и людей. Принимаем активное участие в реализации Национального проекта «Цифровая экономика» и создаем цифровые решения для импортозамещения программного обеспечения – 88 решений компании зарегистрировано в реестре российского ПО. Рассказываем о наших продуктах и ИТ-трендах в Telegram-канале. Сервис печати Sprinter также входит в реестр ПО. Он помогает разработчикам и аналитикам с печатью документов по заданным шаблонам и благодаря ему увидела свет эта статья!
Автор: barsgroup_blog