Criando arquivos Office com OpenXML SDK

Introdução

Uma maneira muito comum de introduzir flexibilidade para nossas aplicações é exportar os dados para arquivos Office: ao exportar para o Word, podemos gerar relatórios poderosos, onde o usuário pode formatar os dados da maneira desejada, editar ou remover partes dos dados, ou mesmo complementar com dados de diversas fontes. Exportando para o Excel, podemos criar rapidamente análises diversas, elaborar gráficos ou fazer comparações facilmente.

Uma maneira de exportar os dados para arquivos Office é usar a automação Office, que usa os recursos de automação COM para abrir o programa e gerar os arquivos a partir de nossos dados. Isso, além de muito lento, traz uma segunda desvantagem: obriga que a máquina que está rodando a aplicação tenha o Office instalado.

A partir do Office 2007, o formato de arquivos Office mudou, para um formato aberto, o OpenXML (http://openxmldeveloper.org/). Isto trouxe algumas vantagens:

  • Documenta o formato de arquivos Office – antes do OpenXML, o formato de arquivos Office não era documentado, e para criar manualmente estes arquivos, você tinha que usar muita engenharia reversa e mudar o programa a cada nova versão do Office;
  • Cria a possibilidade de intercâmbio entre programas – qualquer um pode criar um processador de textos que usa o formato OpenXML sem se preocupar com licenciamento;
  • Permite que você possa criar um programa para gerar ou alterar arquivos Office facilmente.

Um arquivo OpenXML é um arquivo compactado (no formato ZIP), que contém arquivos XML e outros arquivos de dados. Você pode usar qualquer tecnologia que abra arquivos ZIP e acesse arquivos XML para criar ou modificar os novos arquivos Office.

Se você renomear um arquivo Office para .zip e abri-lo, terá algo semelhante a isso:

OpenXML: conteúdo

Você tem um arquivo ZIP com três pastas e um arquivo XML. No diretório _rels você tem um arquivo .rels, um XML com a estrutura do documento. A partir daí, você vai “desenrolando” seu documento e obtendo as diversas partes que o compõem: propriedades, textos, imagens, estilos, etc. Parece complicado? Mas nós nem começamos ainda!

Packaging API

Para facilitar a manipulação de arquivos OpenXML, a Microsoft introduziu no .Net 3.0 a Packaging API, uma API para acessar arquivos que tem este formato de pacotes. Esta não é uma API específica para arquivos Office, você pode, por exemplo, abrir um arquivo XPS com a mesma API ou então criar um arquivo que use uma estrutura semelhante, mas que não seja compatível com OpenXML.

Para exemplificar esta API, vamos criar um programa WPF que abre um arquivo Office e mostra as suas relações. No Visual Studio, crie um novo projeto WPF e, na janela principal, coloque o seguinte código:

<Grid>
    <Grid.RowDefinitions>
        <RowDefinition Height="*"/>
        <RowDefinition Height="40"/>
    </Grid.RowDefinitions>
    <ListBox x:Name="LbxDados" />
    <Button Content="Abre" HorizontalAlignment="Center" Grid.Row="1"
        VerticalAlignment="Center" Width="75" Height="25"
        Click="AbreArquivoClick"/>
</Grid>

No Code Behind, no manipulador do evento Click do botão, coloque o seguinte código:

private void AbreArquivoClick(object sender, RoutedEventArgs e)
{
    var openDialog = new OpenFileDialog
                     {
                         Filter =
                             "Arquivos Office (*.docx,*.xmlx,*.pptx)| "+
                             "*.docx;*.xlsx;*.pptx|Todos Arquivos (*.*)|*.*",
                     };
    if (openDialog.ShowDialog() == true)
    {
        using (Package package = Package.Open(openDialog.FileName, 
                   FileMode.Open, FileAccess.Read))
        {
            LbxDados.ItemsSource = package.GetRelationships();
        }
    }
}

Com isso, iremos pegar as relações do documento aberto e colocá-las na ListBox. Quando você executa o programa, irá ver algo semelhante ao seguinte:

OpenXML: execução

Isto é devido ao fato que a Packaging API abriu o arquivo .rels, fez a análise do arquivo XML e criou classes do tipo PackageRelationship. Para apresentar seus valores, basta colocar um ItemTemplate para a ListBox:

<ListBox x:Name="LbxDados">
    <ListBox.ItemTemplate>
        <DataTemplate>
            <StackPanel>
                <TextBlock Text="{Binding Id}"/>
                <TextBlock Text="{Binding RelationshipType}"/>
                <TextBlock Text="{Binding TargetUri}"/>
            </StackPanel>
        </DataTemplate>
    </ListBox.ItemTemplate>
</ListBox>

Agora os dados são apresentados como queremos:

OpenXML: execução

Podemos ver aqui dois arquivos de propriedades, docProps/app.xml (extended properties) e docProps/core.xml (core properties) e que o documento aberto é uma planiha Excel, que está no diretório xl e tem o nome workbook.xml. Poderíamos, em seguida, abrir o arquivo workbook.xml e analisá-lo, mas isso não é um trabalho simples. Teríamos que analisar as dependências do documento, ver quais as partes que compõem ele para montar toda sua estrutura.

Pensando nessa dificuldade, a Microsoft lançou a OpenXML SDK, um kit de desenvolvimento próprio para os arquivos OpenXML.

OpenXML SDK

A OpenXML SDK está baseada na Packaging API e traz classes voltadas para o desenvolvimento de arquivos OpenXML. Você pode encontrar o código fonte e documentação em https://github.com/OfficeDev/Open-XML-SDK.

A partir daí, basta adicionar uma referência a DocumentFormat.OpenXml e a WindowsBase no programa para começar a usar a OpenXML. Alternativamente, você pode usar o NuGet para adicionar a referência, sem precisar instalar a SDK.

Com a OpenXML SDK você não precisa manipular Packages, Relações ou propriedades. Você tem novas classes para manipular os arquivos do Office diretamente. Por exemplo, você tem as classes WordprocessingDocument, SpreadsheetDocument e PresentationDocument para trabalhar com documentos, planilhas ou apresentações. O código a seguir cria um arquivo Word com uma frase de texto:

class Program
{
    static void Main(string[] args)
    {
        if (args.Length < 2)
        {
            Console.WriteLine("uso: CriaWordDoc  ");
            return;
        }
        CriaDoc(args[0], args[1]);
    }
 
    public static void CriaDoc(string filepath, string msg)
    {
        using (WordprocessingDocument doc = WordprocessingDocument.Create(filepath, 
            WordprocessingDocumentType.Document))
        {
            MainDocumentPart mainPart = doc.AddMainDocumentPart();
 
            mainPart.Document = new Document();
            Body body = mainPart.Document.AppendChild(new Body());
            Paragraph para = body.AppendChild(new Paragraph());
            Run run = para.AppendChild(new Run());
            run.AppendChild(new Text(msg));
            para.AppendChild(new Run());
        }
    }
}

Inicialmente, você cria um WordprocessingDocument, adiciona um MainDocumentPart e atribui a propriedade Document a um novo Document. Em seguida, adicionamos o Body, um Paragraph e, a ele, um Run com o texto. Sem dúvida, isso é mais fácil que trabalhar com os Packages e arquivos XML!

Agora que já conhecemos as classes, podemos usá-las para exportar dados para os arquivos Office. Vamos ver como exportar dados de uma lista para uma planilha Excel.

private static void ExportaLivrosParaExcel()
{
    // Cria planilha Excel
    using (SpreadsheetDocument doc = SpreadsheetDocument.Create("livros.xlsx",
        SpreadsheetDocumentType.Workbook))
    {
        // Cria um Workbook
        WorkbookPart workbookpart = doc.AddWorkbookPart();
        workbookpart.Workbook = new Workbook();
       
        // Cria planilha no Workbook
        var worksheetPart = doc.WorkbookPart.AddNewPart();
        worksheetPart.Worksheet = new Worksheet();
 
        //Dados da planilha
        worksheetPart.Worksheet.AppendChild(new SheetData());
        var sheetData = worksheetPart.Worksheet.GetFirstChild();
 
        // Cria cabeçalho
        var rowIndex = 1u;
        var row = new Row { RowIndex = rowIndex++ };
        sheetData.Append(row);
        InsereDadoNaCelula(row, 0, "Id", CellValues.String);
        InsereDadoNaCelula(row, 1, "Autor", CellValues.String);
        InsereDadoNaCelula(row, 2, "Título", CellValues.String);
        InsereDadoNaCelula(row, 3, "Gênero", CellValues.String);
        InsereDadoNaCelula(row, 4, "Preço", CellValues.String);
        InsereDadoNaCelula(row, 5, "Data Publicação", CellValues.String);
        InsereDadoNaCelula(row, 6, "Descrição", CellValues.String);
 
        // Cria linhas com dados
        foreach (var livro in _livros)
        {
            row = new Row { RowIndex = rowIndex++ };
            sheetData.Append(row);
            InsereDadoNaCelula(row, 0, livro.Id, CellValues.String);
            InsereDadoNaCelula(row, 1, livro.Autor, CellValues.String);
            InsereDadoNaCelula(row, 2, livro.Titulo, CellValues.String);
            InsereDadoNaCelula(row, 3, livro.Genero, CellValues.String);
            InsereDadoNaCelula(row, 4, livro.Preco.ToString( 
                    CultureInfo.InvariantCulture), CellValues.Number);
            InsereDadoNaCelula(row, 5, livro.DataPublicacao.ToString("yyyy-MM-dd"), 
                    CellValues.Date);
            InsereDadoNaCelula(row, 6, livro.Descricao, CellValues.String);
            worksheetPart.Worksheet.Save();
        }
        worksheetPart.Worksheet.Save();
 
        // Adiciona planilha
        doc.WorkbookPart.Workbook.AppendChild(new Sheets());
 
       doc.WorkbookPart.Workbook.GetFirstChild().AppendChild(new Sheet()
        {
            Id = doc.WorkbookPart.GetIdOfPart(worksheetPart),
            SheetId = 1,
            Name = "Planilha 1"
        });

        workbookpart.Workbook.Save();
        doc.Close();
    }
}

A criação de uma planilha Excel é um pouco mais complexa. Inicialmente temos o documento (SpreadsheetDocument). Neste documento, inserimos um Workbook, e no Workbook, uma Worksheet. O Worksheet é composto de SheetData, onde vão os dados da planilha. Você deve inserir as linhas e, nas linhas, células. Com este processo, você pode criar uma nova planilha com dados. A função InsereDadoNaCelula, insere o dado na célula. Passamos a linha onde a célula será inserida, a coluna, o texto e o tipo do dado. A função é a seguinte:

private static void InsereDadoNaCelula(Row linha, int coluna, string dado, CellValues tipo)
{
    var indiceCelula = ObtemColuna(coluna) + linha.RowIndex.ToString();
    
    // Obtém célula pela referência (ex. A1)
    Cell refCell = linha.Elements().FirstOrDefault(
        cell => string.Compare(cell.CellReference.Value, indiceCelula, true) > 0);
 
    // Se a célula não existir, cria uma e insere na linha
    if (refCell == null)
    {
        refCell = new Cell() { CellReference = indiceCelula };
        linha.InsertBefore(refCell, null);
    }
 
    // Configura o dado da célula
    refCell.CellValue = new CellValue(dado);
    refCell.DataType = new EnumValue(tipo);
}

A partir da linha e da coluna, obtemos uma referência para a célula no formato “A1”, verificamos se ela já existe na planilha e, se não existir, ela será criada. Finalmente atribuímos o valor à célula. Embora o processo seja um pouco mais complicado, uma vez que dominamos os conceitos, a criação de planilhas fica bastante simples.

Alterando documentos com OpenXML SDK

Conhecendo a maneira de acessar arquivos OpenXML, podemos também alterar os documentos, da mesma maneira que os criamos. Isto é muito útil quando queremos fazer um processamento para vários arquivos. Como um exemplo, a companhia ACME deseja mudar seu logotipo como mostrado nas próximas figuras.

OpenXML: logo antigoOpenXML: logo novoEsta empresa tem diversas apresentações onde está o logotipo antigo e quer atualizar para o novo logotipo. Isto pode ser feito usando OpenXML SDK: abrimos todas apresentações, procuramos o logotipo no slide mestre e substituímos pelo novo, salvando a apresentação. Isto é feito da seguinte maneira:

public static void SubstituiLogo(string nomeArquivo)
{
    using (PresentationDocument doc = PresentationDocument.Open(nomeArquivo, true))
    {
        PresentationPart presentationPart = doc.PresentationPart;
        if (presentationPart != null && presentationPart.Presentation != null)
        {
            var masterParts = presentationPart.SlideMasterParts;
            var imageParts = presentationPart.SlideParts;
 
            var images = masterParts.SelectMany(s => s.ImageParts)
                .Concat(masterParts.SelectMany(s => s.SlideLayoutParts)
                  .SelectMany(s => s.ImageParts))
                .Concat(imageParts.SelectMany(s => s.ImageParts))
                .Concat(imageParts.Select(s => s.SlideLayoutPart)
                  .SelectMany(s => s.ImageParts));
            foreach (var imagePart in images)
                SubstituiLogoDaImagem(doc, imagePart);
        }
    }
}

As imagens podem estar tanto nos slides como nos slides master. Além disso, elas podem também estar nos slides de layout. Esta linha

var images = masterParts.SelectMany(s => s.ImageParts).Concat(masterParts.SelectMany(s => s.SlideLayoutParts).SelectMany(s => s.ImageParts)).Concat(imageParts.SelectMany(s => s.ImageParts)).Concat(imageParts.Select(s => s.SlideLayoutPart).SelectMany(s => s.ImageParts));

obtém todas as imagens, em qualquer uma destas partes da apresentação e permitem que elas sejam substituídas pelo novo logo, o que é feito com o seguinte código:

private static void SubstituiLogoDaImagem(PresentationDocument doc, ImagePart imagePart)
{
    if (imagePart.Uri.OriginalString.Contains("image1") || 
        imagePart.Uri.OriginalString.Contains("image2"))
    {
        using (FileStream imgStream = new FileStream("AcmeLogo.jpg", 
                FileMode.Open, FileAccess.Read))
            imagePart.FeedData(imgStream);
        doc.PresentationPart.Presentation.Save();
    }
}

A imagePart é substituída com o método FeedData, passando-se o stream da imagem. Assim, a imagem é facilmente substituída em toda a apresentação.

Conclusões

Como você pode ver, a manipulação de arquivos OpenXML é bastante simplificada com o uso da OpenXML SDK. Com ela, a manipulação de arquivos fica bastante simplificada, não necessitando manipular os arquivos ZIP e os XMLs que estão dentro deles.

O fato de termos um formato aberto para os arquivos Office traz muitas oportunidades de desenvolvimento e permite que os usuários de nossos programas não precisem ter o Office instalado na máquina para usá-los.