Копирование листа в пределах книги


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

Решение

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

  1. Открыть документ электронной таблицы с помощью пакета Open XML SDK.
  2. Получить доступ к главному разделу книги, из которого можно получить доступ к ряду взаимосвязанных разделов, таких как отдельные листы.
  3. Получить доступ к листу, который необходимо скопировать.
  4. Создать клон найденного листа и всех связанных с ним разделов и добавить этот клон и все связанные с ним разделы в книгу.
  5. Выполнить очистку для обеспечения правильной работы таблиц, представлений и пр.
  6. Добавить ссылку на созданный лист в список листов главного раздела книги.
  7. Сохранить изменения в книге.

В моей демонстрации будет использоваться пакет SDK версии 2.

Чтобы более наглядно представить изложенный здесь материал, начнем работать с довольно сложной рабочей книгой, содержащей данные, условное форматирование, фигуру, изображение, таблицу, рисунок SmartArt и диаграмму. Книга включает три листа и имеет следующий вид:

Снимок экрана с примером книги Excel

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

Сравнение методов AddPart<T>() и AddNewPart<T>()

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

  1. Создает пустой раздел типа T и добавляет его в пакет.
  2. После создания раздела добавляет ссылку из ссылающегося раздела на новый раздел.

Следующим шагом после добавления нового раздела с помощью этого метода обычно является вызов метода FeedData() для направления данных в этот раздел.

Метод AddPart выполняет следующие действия:

  1. Если добавленный раздел еще не включен в пакет, метод добавит этот раздел и все связанные с ним разделы в пакет. Таким образом, если вы добавляете раздел А, а раздел А ссылается на раздел Б, который в свою очередь ссылается на раздел В, то при вызове этого метода, произойдет добавление разделов А, Б и В. Кроме этого, метод обеспечивает сохранность отношений добавляемых разделов. Эта функция чем-то напоминает импорт многоуровневого клона.
  2. Если добавляемый раздел уже присутствует в пакете, метод добавит ссылку из ссылающегося раздела на раздел, уже присутствующий в пакете. Например, разделы А и Б присутствуют в пакете, но раздел А не ссылается на раздел Б. При вызове данного метода добавляется ссылка из раздела А на раздел Б, если это поддерживается форматом Open XML.

Как видно, метод AddPart обладает более широкими возможностями, чем простой метод AddNewPart. Вы поймете его преимущества, когда я покажу, как выполняется создание клона раздела в пределах пакета.

Код

Как описано выше в разделе "Решение", для выполнения первых трех шагов требуется открыть книгу и получить доступ к листу, который необходимо скопировать. Для этого используются следующие фрагменты кода:

static void CopySheet(string filename, string sheetName, string clonedSheetName)
{
//Open workbook
using (SpreadsheetDocument mySpreadsheet = SpreadsheetDocument.Open(filename, true))
{
WorkbookPart workbookPart = mySpreadsheet.WorkbookPart;
//Get the source sheet to be copied
WorksheetPart sourceSheetPart = GetWorkSheetPart(workbookPart, sheetName);
...
}
}

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

static WorksheetPart GetWorkSheetPart(WorkbookPart workbookPart, string sheetName)
{
//Get the relationship id of the sheetname
string relId = workbookPart.Workbook.Descendants<Sheet>()
.Where(s => s.Name.Value.Equals(sheetName))
.First()
.Id;
return (WorksheetPart)workbookPart.GetPartById(relId);
}

Теперь, когда мы получили доступ к разделу листа, который необходимо скопировать, надо выполнить задачу создания клона. Именно здесь я и воспользуюсь преимуществом метода AddPart. Возможно, в будущей сборке пакета SDK будет включен метод клонирования для разделов. Как уже было сказано выше, метод AddPart отлично подходит для добавления раздела и всех разделов, на которые он ссылается. К сожалению, эта функция работает только при добавлении разделов, которые еще не присутствуют в пакете. Для обхода этого ограничения мы можем просто вызвать метод AddPart для временной книги, а затем снова вызвать метод AddPart в главной книге. Эта задача выполняется с помощью следующего кода:

static void CopySheet(string filename, string sheetName, string clonedSheetName)
{
...
//Take advantage of AddPart for deep cloning
SpreadsheetDocument tempSheet = SpreadsheetDocument.Create(new MemoryStream(), mySpreadsheet.DocumentType);
WorkbookPart tempWorkbookPart = tempSheet.AddWorkbookPart();
WorksheetPart tempWorksheetPart = tempWorkbookPart.AddPart<WorksheetPart>(sourceSheetPart);
//Add cloned sheet and all associated parts to workbook
WorksheetPart clonedSheet = workbookPart.AddPart<WorksheetPart>(tempWorksheetPart);
...
}

К этому моменту мы успешно клонировали лист и добавили его и связанные с ним разделы в книгу. Почти все готово...

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

static void CopySheet(string filename, string sheetName, string clonedSheetName)
{
...
//Table definition parts are somewhat special and need unique ids...so let's make an id based on count
int numTableDefParts = sourceSheetPart.GetPartsCountOfType<TableDefinitionPart>();
tableId = numTableDefParts;
//Clean up table definition parts (tables need unique ids)
if (numTableDefParts != 0)
FixupTableParts(clonedSheet, numTableDefParts);
//There should only be one sheet that has focus
CleanView(clonedSheet);
...
}

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

static void CleanView(WorksheetPart worksheetPart)
{
//There can only be one sheet that has focus
SheetViews views = worksheetPart.Worksheet.GetFirstChild<SheetViews>();
if (views != null)
{
views.Remove();
worksheetPart.Worksheet.Save();
}
}

Исправление разделов таблиц предполагает проверку наличия у каждой таблицы уникального имени и идентификатора.

static void FixupTableParts(WorksheetPart worksheetPart, int numTableDefParts)
{
//Every table needs a unique id and name
foreach (TableDefinitionPart tableDefPart in worksheetPart.TableDefinitionParts)
{
tableId++;
tableDefPart.Table.Id = (uint)tableId;
tableDefPart.Table.DisplayName = "CopiedTable" + tableId;
tableDefPart.Table.Name = "CopiedTable" + tableId;
tableDefPart.Table.Save();
}
}

Все в порядке. Последний шаг — это добавление ссылки на добавленный лист в основной раздел книги с помощью следующего кода:

static void CopySheet(string filename, string sheetName, string clonedSheetName)
{
...
//Add new sheet to main workbook part
Sheets sheets = workbookPart.Workbook.GetFirstChild<Sheets>();
Sheet copiedSheet = new Sheet();
copiedSheet.Name = clonedSheetName;
copiedSheet.Id = workbookPart.GetIdOfPart(clonedSheet);
copiedSheet.SheetId = (uint)sheets.ChildElements.Count + 1;
sheets.Append(copiedSheet);
//Save Changes
workbookPart.Workbook.Save();
...
}

Заключение

Объединив все фрагменты и выполнив предложенный код, мы получим книгу с четырьмя листами, где последний лист, названный "CopiedData", является точной копией первого листа.

Вот снимок экрана с созданной книгой:

Снимок экрана с книгой Excel после выполнения кода

Зияд Раджаби (Zeyad Rajabi)

Это локализованная запись блога. Исходную статью можно найти по адресу http://blogs.msdn.com/brian_jones/archive/2009/02/19/how-to-copy-a-worksheet-within-a-workbook.aspx.


Skip to main content