﻿# Tutorial: как портировать проект с Interop Word API на Open XML SDK

С выходом \.NET5 дальнейшее развитие некоторых проектов оказалось под вопросом из\-за сложности портирования\. Если от небольших устаревших библиотек можно отказаться или найти им замену, то от зависимости Microsoft\.Office\.Interop\.Word\.dll очень сложно отказаться\. Microsoft не планирует добавлять совместимость с \.NET Core/5\+, поэтому в этой статье мы рассмотрим, как создавать документы Word с помощью Open XML SDK\.

![0856_WordToOpenXmlTutor_ru/image1.png](https://import.viva64.com/docx/blog/0856_WordToOpenXmlTutor_ru/image1.png)

## Введение

Office Open XML, также известный как OpenXML или OOXML, представляет собой формат на основе XML для офисных документов, включая текстовые документы, электронные таблицы, презентации, а также диаграммы, фигуры и другой графический материал\. В июне 2014 года Microsoft выпустила исходный код Open XML SDK на [GitHub](https://github.com/OfficeDev/Open-XML-SDK) для работы с таким форматом\.

У этой библиотеки есть серьёзные преимущества:

* совместима с \.NET 5\+,
* не требует установки Microsoft Office,
* высокая скорость работы,
* открытый исходный код\.

Без минусов тоже не обошлось:

* сложный API,
* скудная документация\.

Эти минусы определённо дополняют друг друга\. Собственно, это и стало причиной создания этого материала\.

А вот открытый исходный код является большим плюсом\. Если бы код COM\-библиотек был открыт, сообщество разработчиков помогло бы с портированием на \.NET Core/5\+\. Кроме привлечения сторонних разработчиков, публичный код даёт каждому возможность находить и исправлять ошибки и уязвимости или хотя бы сообщать о них\. Качество публичных библиотек очень [важно](https://pvs-studio.ru/ru/blog/posts/cpp/0762/) для всех проектов, которые могут их использовать\. Например, мы проводили небольшой [аудит кода](https://pvs-studio.ru/ru/blog/posts/csharp/0777/) Open XML SDK при первом знакомстве с этой библиотекой\.

## Боль разработчиков Office

Для продуктов Office было разработано очень много софта сторонними разработчиками\. Это плагины для Word, Excel, Outlook\. Многие компании наделали себе удобных плагинов и генераторов отчётов в формате Word\. А 3 июля 2021 года произошло страшное – все тикеты про поддержку \.NET 5\+ в VSTO / COM, разбросанные по разным ресурсам, были в одночасье закрыты с комментарием представителей Microsoft подобного рода:

> \.\.\.The VSTO/COM Add\-Ins platform is very important to Microsoft, and we plan to continue to support it in Office with \.NET Framework 4\.8 as the last major version\.\.\.VSTO/COM Add\-Ins cannot be created with \.NET Core and \.NET 5\+\. This is because \.NET Core/\.NET 5\+ cannot work together with \.NET Framework in the same process and may lead to add\-in load failures\. Microsoft will not be updating VSTO or the COM Add\-in platform to use \.NET Core or \.NET 5\+\.\.\.

По их информации, поддержка \.NET 5\+ не предвидится\. Вот одна из таких дискуссий, которая ещё долго не прекращалась после этого объявления: "[Please port Visual Studio Tools For Office \(VSTO\) to \.NET 5/7, to enable VSTO add\-in development in C\# in \.Net 5/7](https://developercommunity.visualstudio.com/t/Please-port-Visual-Studio-Tools-For-Offi/757925)"\.

Если у разработчиков плагинов совсем всё плохо – им предложили перейти на Office JavaScript API \(совсем другой язык, API не позволяет делать и малую часть того, что было\), то для создания документов из C\# кода можно попробовать перейти на библиотеку Open XML SDK \([nuget](https://www.nuget.org/packages/Open-XML-SDK)\)\.

## Основы

Прежде чем приступить к разбору примеров, стоит понять, с чем вообще работают эти две библиотеки и в чём состоит разница их подходов\.

Word документ — это набор запакованных xml\-документов\. Все элементы структурированы под тегами\.

Например, параграф внутри документа будет выглядеть примерно вот так:

```cpp
<w:p w:rsidR="007D2247" w:rsidRDefault="009A4B44"
         xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
  <w:r>
    <w:t>тест</w:t>
  </w:r>
  <w:bookmarkStart w:name="_GoBack" w:id="0" />
  <w:bookmarkEnd w:id="0" />
</w:p>
```

Сборка Interop\.Word немного абстрагируется от этой структуры и часто работает с некоторым участком – Range – документа\. А Open XML SDK идёт по пути отражения внутренней структуры документа в самом коде\. Параграфы _<w:p\>_, участки текста _<w:t\>_ и всё остальное становятся объектами в самом коде\. Если вы не создадите тело документа, параграф и других обязательных "родителей", то и добавлять текст будет некуда\.

![0856_WordToOpenXmlTutor_ru/image2.png](https://import.viva64.com/docx/blog/0856_WordToOpenXmlTutor_ru/image2.png)

На скриншоте как раз изображена внутренняя структура основного файла для документа Word – document\.xml\. Этот файл содержит само наполнение документа\.

Скриншот сделан в очень нужной для работы с Open XML утилите Open XML SDK 2\.5 Productivity Tool\. К моменту написания статьи эта утилита была удалена с сайта Microsoft, а в репозитории [Open\-XML\-SDK](https://github.com/OfficeDev/Open-XML-SDK) добавлена ссылка на некий [DocxToSource](https://github.com/rmboggs/DocxToSource), который должен стать заменой устаревшего Productivity Tool\. Однако эта замена всё ещё является прототипом, поэтому пока лучше постараться найти старый добрый Productivity Tool\. Старая утилита позволяет просмотреть строение документа, познакомиться с автогенерированным кодом\. 

![0856_WordToOpenXmlTutor_ru/image3.png](https://import.viva64.com/docx/blog/0856_WordToOpenXmlTutor_ru/image3.png)

Также она позволяет сравнить два разных документа \(и код для их создания, и их внутреннее строение\)\. 



![0856_WordToOpenXmlTutor_ru/image4.png](https://import.viva64.com/docx/blog/0856_WordToOpenXmlTutor_ru/image4.png)

## Примеры

Для Interop\.Word во всей статье примем такой псевдоним для удобства чтения:

```cpp
using MicrosoftWord = Microsoft.Office.Interop.Word;
```

Также для упрощения будем называть Open XML SDK просто Open XML\.

### Создание документа

_**Interop\.Word:**_

```cpp
MicrosoftWord.Application wordApp = new MicrosoftWord.Application();
MicrosoftWord.Document wordDoc = wordApp.Documents.Add();
MicrosoftWord.Range docRange = wordDoc.Range();
.... // тут при необходимости работаем с документом
wordDoc.SaveAs2(pathToDocFile);
wordApp.Quit();
```

Тут всё достаточно просто, но всё равно есть свои подводные камни\. При работе с Interop мы взаимодействуем не просто с некоторым объектом в памяти, а с COM\-объектом\. Поэтому возникает необходимость завершать все процессы после окончания работы программы\. Эта проблема не раз поднималась на Stack Overflow \([1](https://stackoverflow.com/questions/158706/how-do-i-properly-clean-up-excel-interop-objects?page=1&tab=active), [2](https://stackoverflow.com/questions/25134024/clean-up-excel-interop-objects-with-idisposable/25135685)\), и ей предложено множество разных решений\.

Есть решение с участием [Marshal Class](https://docs.microsoft.com/en-us/dotnet/api/system.runtime.interopservices.marshal?view=net-5.0), являющимся частью InteropServices\.

```cpp
finally
{
  if (Marshal.IsComObject(wordDoc))
    try
    {
      Marshal.FinalReleaseComObject(wordDoc);
    }
    catch { throw; }
 
  if (Marshal.IsComObject(wordApp))
    try
    {
      Marshal.FinalReleaseComObject(wordApp);
    }
    catch { throw; }
}
```

Однако в таком случае можно упустить какие\-нибудь процессы\.

Есть более надёжный вариант с обращением к [GC](https://docs.microsoft.com/en-us/dotnet/api/system.gc?view=net-5.0):

```cpp
GC.Collect();
GC.WaitForPendingFinalizers();
```

Эти методы надо вызвать после того, как вся работа с COM\-объектами будет завершена\.

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

![0856_WordToOpenXmlTutor_ru/image5.png](https://import.viva64.com/docx/blog/0856_WordToOpenXmlTutor_ru/image5.png)

Но даже если в коде присутствует закрытие процессов после окончания работы, при прерывании программы вручную или её падении процесс останется запущенным\. Такого недостатка нет при создании и работе с документом через Open XML\.

_**Open XML:**_

```cpp
using (WordprocessingDocument doc = 
         WordprocessingDocument.Create(pathToDocFile,
                                       WordprocessingDocumentType.Document,
                                       true))
{
  MainDocumentPart mainPart = doc.AddMainDocumentPart();
  mainPart.Document = new Document();
  Body body = mainPart.Document.AppendChild(new Body());
  SectionProperties props = new SectionProperties();
  body.AppendChild(props);
}
```

Обратите внимание на добавление _SectionProperties_, они понадобятся нам позже\.

### Добавление параграфа

_**Interop\.Word**_

```cpp
public static void InsertWordText(MicrosoftWord.Document doc,
                                      string text)
{
  MicrosoftWord.Paragraph paragraph = doc.Paragraphs.Add(Missing.Value);
  paragraph.Range.Text = text;
  paragraph.Range.InsertParagraphAfter();
}
```

Текст также можно сделать жирным или курсивным через параметр _Font_:

```cpp
paragraph.Range.Font.Bold = 1;
paragraph.Range.Font.Italic = 1;
```

Изменить размер шрифта можно через:

```cpp
paragraph.Range.Font.Size = 14;
```

Выравнивание текста выполняется через _ParagraphFormat\.Alignment_:

```cpp
paragraph.Range.ParagraphFormat.Alignment = MicrosoftWord.WdParagraphAlignment
                                                        .wdAlignParagraphCenter;
```

_**Open XML:**_

```cpp
public static void AddText(WordprocessingDocument doc, string text)
{
  MainDocumentPart mainPart = doc.MainDocumentPart;
  Body body = mainPart.Document.Body;
  Paragraph paragraph = body.AppendChild(new Paragraph());

  Run run = paragraph.AppendChild(new Run());
  run.AppendChild(new Text(text));
  run.PrependChild(new RunProperties());
}
```

В случае с Open XML жирным или курсивным текст можно сделать через:

```cpp
run.RunProperties.AddChild(new Bold());
run.RunProperties.AddChild(new Italic());
```

Изменение размера шрифта в этом случае немного неинтуитивно, но согласуется с общей логикой работы с Open XML:

```cpp
run.RunProperties.AddChild(new FontSize(){ Val = "14"});
```

Выравнивание текста:

```cpp
paragraph.ParagraphProperties.AddChild(new Justification()
                                       {
                                         Val = JustificationValues.Center
                                       });
```

Важно перед этим не забыть добавить к параграфу свойства:

```cpp
paragraph.AppendChild(new ParagraphProperties());
```

### Вставка заголовка

Предположим, что нам нужно вписать в документ заголовок\. В случае Interop\.Word нужно всего лишь небольшое дополнение к вставке текста, чтобы получить заголовок:

_**Interop\.Word:**_

```cpp
public static void InsertWordHeading1(MicrosoftWord.Document doc,
                                      string headingText)
{
  MicrosoftWord.Paragraph paragraph = doc.Paragraphs.Add(Missing.Value);
  paragraph.Range.Text = headingText;
  paragraph.Range.set_Style("Heading 1");
  paragraph.Range.InsertParagraphAfter();
}
```

В этом случае сначала задаём Range для записи нового текста и присваиваем ему стиль _Heading 1_\.

_**Open XML:**_

```cpp
public static void InsertWordHeading1(WordprocessingDocument doc,
                                      string headingText)
{
  MainDocumentPart mainPart = doc.MainDocumentPart;
  Paragraph para = mainPart.Document.Body.AppendChild(new Paragraph());
  Run run = para.AppendChild(new Run());
  run.AppendChild(new Text(headingText));
  para.ParagraphProperties = new ParagraphProperties(
                               new ParagraphStyleId() { Val = "Heading1" });
}
```

Тут, казалось бы, всё очень похоже\. Аналогично добавляем параграф и в случае с Open XML организуем нужную иерархию объектов\.

Однако на самом деле в случае с Open XML коварным оказывается добавление стиля\. Interop\.Word работает с реальным полноценным документом, как если бы вы запустили Word и нажали создать\. А вот Open XML работает только с тем, что было создано\. И если вы добавляете текст документу, созданному через Open XML, а не через Interop\.Word, то в нём будут отсутствовать, например, стили\. Соответственно, никакого стиля _Heading1_ в таком документе не будет\. Его нужно сначала добавить\. 

Удобнее всего добавлять нужный стиль при создании документа\. Есть два варианта: перенести стили из готового Word\-документа или добавить эти стили вручную\. 

В первом случае в документе, из которого будет браться стиль, нужно обязательно применить искомый стиль\. Сам перенос требует достаточно много кода, благо, в официальной документации есть [мануал на эту тему](https://docs.microsoft.com/en-us/office/open-xml/how-to-replace-the-styles-parts-in-a-word-processing-document)\.

Для второго варианта нам поможет Productivity Tool для Open XML, упоминавшийся ранее\. Чтобы получить код, нужный для добавления желаемого стиля, создаём чистый документ Word, используем в нём нужный стиль и "скармливаем" этот документ утилите\. Далее через использование кнопки Reflect Code на /word/styles\.xml в структуре документа мы получим реализацию метода _GeneratePartContent_\. В нём мы ищем реализацию нужного стиля и всё, что с ним связано, включая _StyleParagraphProperties_, _StyleRunProperties_ и т\.д\. 

Для стиля _Heading 1_ нужный нам автосгенерированный код будет выглядеть примерно так:

```cpp
Style style2 = new Style() { Type = StyleValues.Paragraph,
                             StyleId = "Heading1" };
StyleName styleName2 = new StyleName(){ Val = "heading 1" };
....
style2.Append(styleRunProperties1);
```

Чтобы добавить перенесённый стиль к генерируемому документу, нужно создать набор стилей Styles и добавить стиль к набору\. Далее к документу нужно добавить _StyleDefinitionsPart_ и присвоить группу стилей\. Выглядеть это будет вот так:

```cpp
var styles = new Styles();
styles.Append(style2);
wordDocument.MainDocumentPart.AddNewPart<StyleDefinitionsPart>();
wordDocument.MainDocumentPart.StyleDefinitionsPart.Styles = styles;
```

У себя мы решили использовать вариант с шаблонным документом, чтобы в будущем при появлении необходимости в каком\-либо стиле нужно было лишь использовать его в шаблоне и работать с ним в коде вместо того, чтобы каждый раз рыться в ProductivityTool и копировать себе полотна кода с объявлением нужного стиля\.

### Смена ориентации страницы

Для нашего отчёта нам нужна была именно ландшафтная ориентация страницы\.

_**Interop\.Word:**_

```cpp
MicrosoftWord.Document wordDoc = wordApp.Documents.Add();
MicrosoftWord.Range docRange = wordDoc.Range();
docRange.PageSetup.Orientation = MicrosoftWord.WdOrientation
                                              .wdOrientLandscape;
```

У документа получаем нужный Range \(страниц или всего документа\) и задаём ландшафтную ориентацию\.

_**Open XML:**_

```cpp
var sectionProperties = mainPart.Document
                                .Body
                                .GetFirstChild<SectionProperties>();
sectionProperties.AddChild(new PageSize()
{
  Width = (UInt32Value)15840U,
  Height = (UInt32Value)12240U,
  Orient = PageOrientationValues.Landscape
});
```

C Open XML в этом случае всё не настолько абстрактно, как хотелось бы\. Если вы инициализируете в _PageSize_ только поле _Orient_, то ничего не изменится\. _Width_ и _Height_ тоже нужно менять\.

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

```cpp
sectionProperties.AddChild(new PageMargin()
{
  Top = 720,
  Right = Convert.ToUInt32(1440.0),
  Bottom = 360,
  Left = Convert.ToUInt32(1440.0),
  Header = (UInt32Value)450U,
  Footer = (UInt32Value)720U,
  Gutter = (UInt32Value)0U
});
```

### Гиперссылки

_**Interop\.Word:**_

```cpp
public static void AddHyperlinkedText(MicrosoftWord.Document doc,
                                      string text,
                                      string url)
{
  MicrosoftWord.Range wrdRng = doc.Bookmarks
                                  .get_Item("\\endofdoc")
                                  .Range;
  doc.Hyperlinks.Add(wrdRng, url, TextToDisplay: text);
}
```

Тут всё просто: как обычно, получаем нужный Range и добавляем гиперссылку\. У метода [Add](https://docs.microsoft.com/en-us/dotnet/api/microsoft.office.interop.word.hyperlinks.add?view=word-pia) много параметров, и можно сконструировать более сложную ссылку\.

_**Open XML:**_

```cpp
public static void AddHyperlinkedText(WordprocessingDocument doc,
                                      string text,
                                      string url)
{
  MainDocumentPart mainPart = doc.MainDocumentPart;
  Body body = mainPart.Document.Body;
  Paragraph paragraph = body.AppendChild(new Paragraph());

  var rel = mainPart.AddHyperlinkRelationship(new Uri(url), true);

  Hyperlink hyperlink = new Hyperlink(new Run(
                                    new RunProperties(
                                      new RunStyle 
                                      {
                                        Val = "Hyperlink",
                                      },
                                      new Underline
                                      {
                                        Val = UnderlineValues.Single
                                      },
                                      new Color
                                      {
                                        ThemeColor = ThemeColorValues.Hyperlink
                                      }),
                                      new Text
                                      {
                                        Text = text
                                      })) 
                    {
                      Id = rel.Id 
                    };

  paragraph.AppendChild(hyperlink);
}
```

Из существенных отличий тут то, что _url_ нужно сначала обернуть в _Uri_ и создать связь _url_ с гиперссылкой через _AddHyperlinkRelationship_\. Потом при создании самой гиперссылки, нужно присвоить полю _Id_ новой гиперссылки Id созданной ранее связи\.

### Картинки

_**Interop\.Word:**_

```cpp
public static void InsertWordPicture(MicrosoftWord.Document doc,
                                     string picturePath)
{
  MicrosoftWord.Range wrdRng = doc.Bookmarks.get_Item("\\endofdoc")
                                            .Range;
  wrdRng.InlineShapes.AddPicture(picturePath);
}
```

Тут всё достаточно просто, а с Open XML всё оказалось крайне сложно\. 

_**Open XML:**_

Для добавления картинки необходимо соблюсти сложную иерархию объектов с определёнными параметрами\. Хорошо, что есть [документация на этот счёт](https://docs.microsoft.com/en-us/office/open-xml/how-to-insert-a-picture-into-a-word-processing-document)\. Поэтому пропустим код, требуемый для добавления картинки в этой статье\. Разберём ещё один момент, который почему\-то не упоминается в документации\. Можете заметить, что в том коде нигде не передаётся размер картинки\. Фиксируется её размер тут:

```cpp
new DW.Extent() { Cx = 990000L, Cy = 792000L }
```

и тут

```cpp
new A.Extents() { Cx = 990000L, Cy = 792000L }
```

Если использовать этот код, то вместо нормальной картинки, будет вставлено малюсенькое изображение\. Его реальный размер никак не изменится, но при отображении оно будет отмасштабировано до таких размеров:

![0856_WordToOpenXmlTutor_ru/image6.png](https://import.viva64.com/docx/blog/0856_WordToOpenXmlTutor_ru/image6.png)

Дело в том, что масштаб отображения картинки здесь завязан на такую вещь, как [EMU](https://en.wikipedia.org/wiki/Office_Open_XML_file_formats) \(English Metric Units\)\.

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

```cpp
double englishMetricUnitsPerInch = 914400;
double pixelsPerInch = 96;
double englishMetricUnitsPerPixel = englishMetricUnitsPerInch / pixelsPerInch;

double emuWidth = width * englishMetricUnitsPerPixel;
double emuHeight = height * englishMetricUnitsPerPixel;
```

Тут мы получаем количество EMU на пиксель, приняв значение PPI за 96, и умножаем полученное значение на нужное количество пикселей для ширины и высоты\. В итоге у наc есть нужная нам ширина и высота в EMU\. Их мы и передаём как _Cx_ и _Cy_ для Extent и Extents:

```cpp
Cx = (Int64Value)emuWidth, Cy = (Int64Value)emuHeight
```

### Таблицы

_**Interop\.Word:**_

Генерация таблицы через Interop\.Word достаточно прямолинейна\. Разберём пример, как можно было бы вставить таблицу из квадратной матрицы строк\.

```cpp
public static void InsertWordTable(MicrosoftWord.Document doc,
                                   string[,] table)
{
  MicrosoftWord.Table oTable;
  MicrosoftWord.Range wrdRng = doc.Bookmarks
                                  .get_Item("\\endofdoc")
                                  .Range;

  int rowCount = table.GetLength(0);
  int columnCount = table.GetLength(1);

  oTable = doc.Tables.Add(wrdRng,
                    rowCount,
                    columnCount,
                    DefaultTableBehavior: MicrosoftWord.WdDefaultTableBehavior
                                                       .wdWord9TableBehavior,
                    AutoFitBehavior: MicrosoftWord.WdAutoFitBehavior
                                                  .wdAutoFitWindow);

  for (int i = 0; i < rowCount; i++)
    for (int j = 0; j < columnCount; j++)
      oTable.Cell(i + 1, j + 1).Range.Text = table[i,j];
}
```

Параметры метода _Add_ — _DefaultTableBehavior_ и _AutoFitBehavior_ — как видно из их названия, отвечают за поведение таблицы при необходимости изменения размера под содержимое ячеек\. Им присваиваются значения перечислений [WdDefaultTableBehavior](https://docs.microsoft.com/en-us/dotnet/api/microsoft.office.interop.word.wddefaulttablebehavior?view=word-pia) и [WdAutoFitBehavior](https://docs.microsoft.com/en-us/dotnet/api/microsoft.office.interop.word.wdautofitbehavior?view=word-pia) соответственно\. Сам метод Add создаёт в документе таблицу с нужными нам параметрами\.

Стиль к таблице можно применить следующим образом:

```cpp
oTable.set_Style("Grid Table 4 - Accent 1");
```

Также для красивого выделения первого столбика, если он является заголовочным, можно присвоить _true_ полю _oTable\.ApplyStyleFirstColumn_\.

Расстояние между параграфами текста контролируется через _oTable\.Range\.ParagraphFormat\.SpaceAfter_\. Для компактного отображения таблицы можно использовать

```cpp
oTable.Range.ParagraphFormat.SpaceAfter = 0;
```

Также можно устанавливать тип написания текста к строкам или колонкам:

```cpp
oTable.Rows[1].Range.Font.Bold = 1;
oTable.Column[1].Range.Font.Italic = 1;
```

Используя эти возможности, можно получить вот такую таблицу:

![0856_WordToOpenXmlTutor_ru/image7.png](https://import.viva64.com/docx/blog/0856_WordToOpenXmlTutor_ru/image7.png)

_**Open XML:**_

```cpp
public static void InsertWordTable(WordprocessingDocument doc,
                                   string[,] table)
{
  DocumentFormat.OpenXml.Wordprocessing.Table dTable =
    new DocumentFormat.OpenXml.Wordprocessing.Table();

  TableProperties props = new TableProperties();

  dTable.AppendChild<TableProperties>(props);

  for (int i = 0; i < table.GetLength(0); i++)
  {
    var tr = new TableRow();

    for (int j = 0; j < table.GetLength(1); j++)
    {
      var tc = new TableCell();
      tc.Append(new Paragraph(new Run(new Text(table[i, j]))));

      tc.Append(new TableCellProperties());

      tr.Append(tc);
    }
    dTable.Append(tr);
  }
  doc.MainDocumentPart.Document.Body.Append(dTable);
}
```

При создании таблицы с нуля с Open XML стоит помнить о том, что никаких ячеек или строк к моменту ввода данных не существует\. Их нужно сначала создать, соблюдая внутреннюю иерархию\.

Поэтому при проходе по матрице мы для каждой строки создаём _TableRow_, а потом для каждого элемента в строке создаём _TableCell_, куда добавляем новые _Paragraph_, _Run_ и _Text_ с соответствующим значением из матрицы\. _TableCellProperties_ лучше также добавить сразу, чем потом при дальнейшей работе с таблицей наткнуться на _System\.NullReferenceException_ при попытке добавить свойство ячейке\.

Если мы не зададим в _TableProperties_ ни стиля, ни Borders, то таблица будет выглядеть вот так

![0856_WordToOpenXmlTutor_ru/image8.png](https://import.viva64.com/docx/blog/0856_WordToOpenXmlTutor_ru/image8.png)

Границы таблицы формируются через _TableBorders_\.

```cpp
var borderValues = new EnumValue<BorderValues>(BorderValues.Single);
var tableBorders = new TableBorders( 
                     new TopBorder { Val = borderValues, Size = 4 },
                     new BottomBorder {  Val = borderValues,  Size = 4 },
                     new LeftBorder { Val = borderValues, Size = 4 },
                     new RightBorder { Val = borderValues, Size = 4 },
                     new InsideHorizontalBorder { Val= borderValues, Size = 4 },
                     new InsideVerticalBorder { Val= borderValues, Size = 4 }));
```

Перечисление [BorderValues](https://docs.microsoft.com/en-us/dotnet/api/documentformat.openxml.vml.wordprocessing.bordervalues?view=openxml-2.8.1) здесь задаёт стиль границ\. 

_TableBorders_ нужно добавить к _TableProperties_ через

```cpp
props.Append(tableBorders);
```

Границы таблицы можно не задавать, если ей будет присвоен какой\-нибудь стиль\. Главное не забыть, что стиль сначала нужно добавить к документу\.

Задаётся стиль достаточно просто:

```cpp
TableStyle tableStyle = new TableStyle()
                        {
                          Val = "GridTable4-Accent5"
                        };
```

Его так же, как и границы, нужно добавить к _TableProperties_:

```cpp
props.Append(tableStyle);
```

Для того чтобы таблица заняла всю ширину страницы можно использовать _TableWidth_ заданную следующим образом:

```cpp
var tableWidth = new TableWidth()
                 {
                   Width = "5000",
                   Type = TableWidthUnitValues.Pct
                 };
```

Значение 5000 тут взято "не из воздуха"\. Тип единицы ширины здесь мы задаём _TableWidthUnitValues\.Pct_ – единицы ширины в одну пятидесятую процента страницы или 0,02%\. В итоге пять тысяч Pct это 100% ширины страницы\.

Этот параметр добавляется к _TableProperties_ аналогичным образом:

```cpp
props.Append(tableWidth);
```

Важный момент: _TableProperties_ должны быть добавлены к таблице до самих данных для того, чтобы они работали корректно\. Их можно добавить и после других объектов, но тогда уже стоит использовать

```cpp
dTable.PrependChild<TableProperties>(props);
```

### Раскраска таблиц

Для формирования нашего отчёта нам нужно было раскрасить ячейки в некоторых таблицах документа\.

_**Interop\.Word:**_

```cpp
oTable.Cell(i, j).Range.Shading.BackgroundPatternColor = MicrosoftWord.WdColor
                                                                    .wdColorRed;
```

где _oTable_ – это созданная нами ранее таблица, _i_ и _j_ — это индексы нужной ячейки\. Присваиваемое значение – перечисление [WdColor](https://docs.microsoft.com/en-us/dotnet/api/microsoft.office.interop.word.wdcolor?view=word-pia)\. 

_**Open XML:**_

```cpp
tc.Append(new TableCellProperties(
            new Shading { Fill = "FF0000" }));
```

где _tc_ – это _TableCell_, с которой идёт работа\. Полю _Fill_ присваивается строка с Hex\-значением цвета\.

### Разрыв страницы

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

_**Interop\.Word:**_

```cpp
public static void InsertWordBreak(MicrosoftWord.Document doc)
{
  MicrosoftWord.Range wrdRng = doc.Bookmarks.get_Item("\\endofdoc")
                                            .Range;
  wrdRng.InsertBreak();
}
```

_**Open XML:**_

```cpp
public static void InsertWordBreak(WordprocessingDocument doc)
{
  MainDocumentPart mainPart = doc.MainDocumentPart;
  mainPart.Document.Body.InsertAfter(new Paragraph(
                                       new Run(
                                         new Break()
                                         { 
                                           Type = BreakValues.Page
                                         })),
                                     mainPart.Document.Body.LastChild);
}
```

Тип разрыва меняется через перечисление [BreakValues](https://docs.microsoft.com/en-us/dotnet/api/documentformat.openxml.wordprocessing.breakvalues?view=openxml-2.8.1)\.

### Footer/Header

Также нам нужны были футеры/хедеры в документе\.

_**Interop\.Word:**_

```cpp
public static void InsertWordFooter(
  MicrosoftWord.Document doc,
  string headerText)
{
  MicrosoftWord.Range headerRange = doc.Sections
                                 .Last
                                 .Headers[MicrosoftWord.WdHeaderFooterIndex
                                                       .wdHeaderFooterPrimary]
                                 .Range;

  headerRange.Fields.Add(headerRange, MicrosoftWord.WdFieldType.wdFieldPage);
  headerRange.Text = headerText;
}
```

Через _headerRange\.Font_ можно поменять параметры текста, например размер, шрифт, цвет и т\.д\. А _headerRange\.ParagraphFormat\.Alignment_, как следует из названия, задаёт выравнивание текста\. Это поле принимает значения [WdParagraphAlignment](https://docs.microsoft.com/en-us/dotnet/api/microsoft.office.interop.word.wdparagraphalignment?view=word-pia)\.

_**Open XML:**_

Тут сложность состоит в том, что футер/хэдер сам по себе хранится в отдельном \.xml файлике\. Поэтому нам нужно связать хэдер/футер с содержанием документа через [SectionProperties](https://docs.microsoft.com/en-us/dotnet/api/documentformat.openxml.wordprocessing.sectionproperties?view=openxml-2.8.1)\.

```cpp
static void InsertWordHeader(HeaderPart part,
                             string headerText)
{
  MainDocumentPart mainPart = doc.MainDocumentPart;

  if (mainPart.HeaderParts.Any())
    return;

  HeaderPart headerPart = mainPart.AddNewPart<HeaderPart>();

  string headerPartId = mainPart.GetIdOfPart(headerPart);

  part.Header = new Header(
                  new Paragraph(
                    new ParagraphProperties(
                      new ParagraphStyleId() { Val = "Header" }),
                      new Run( new Text() { Text = headerText })));

  var sectionProperties = mainPart.Document
                                  .Body
                                  .GetFirstChild<SectionProperties>();
  sectionProperties.PrependChild<HeaderReference>(new HeaderReference()
                                                  {
                                                    Id = headerPartId
                                                  });
}
```

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

```cpp
if (mainPart.HeaderParts.Any())
  return;
```

можно использовать

```cpp
mainDocumentPart.DeleteParts(mainDocumentPart.HeaderParts);
```

Для футера нужно будет передать _mainDocumentPart\.FooterParts_\.

## Заключение

Описанные методы работы с Open XML SDK можно собрать в библиотеку классов для внутреннего использования в компании, что мы и сделали\. Создание Word документов стало даже удобнее, чем было с Word Interop API\.

Здесь может возникнуть закономерный вопрос, есть ли готовые библиотеки на основе Open XML SDK для упрощённой работы с документами? Ответ – однозначно да\. Но, к сожалению, поддержка таких библиотек быстро прекращается\. Истории создания таких проектов все одинаковые: программисты начинают работать с Word, осознают неудобство существующей инфраструктуры, дорабатывают её — и некоторые библиотеки публикуются на GitHub\. Даже если удастся найти относительно свежую версию подобной библиотеки, то, скорее всего, она была реализована под задачи конкретного проекта, и в вашем проекте всё равно будет неудобной в использовании\. Плюс появится риск остаться с неподдерживаемой библиотекой\.