Criando arquivos OpenXML com Delphi

Por Bruno Sonnino

Junho 2013

Dn265965.060DE5057573180CEC6D227C6D3E2207(pt-br,MSDN.10).png

  • Analisando um arquivo OpenXML
  • Acessando os arquivos OpenXML
  • Alterando arquivos OpenXML
  • Criação de um arquivo OpenXML
  • Colocando mais informações no arquivo
  • Conclusões

Introdução: A nova versão do Office, o Office 2007 trouxe mudanças significativas para esta suíte de aplicativos: a mais visível foi o Ribbon, uma nova interface de usuário que facilita muito o uso dos aplicativos.

Dn265965.DBCD677862F40292EDCEA580FB8B8F39(pt-br,MSDN.10).png

Figura 1 – Ribbon do Word

Outra mudança, menos visível à primeira vista, permite que os aplicativos do Office se integrem com uma grande variedade de programas: o novo formato de arquivo. Até as versões anteriores, o formato de arquivo era proprietário: quando se queria abrir ou gravar um documento do Office em nossas aplicações, era necessário usar automação OLE, o que requeria que o Office estivesse instalado na máquina do cliente, ou então tentar descobrir o formato dos arquivos, que não era completamente documentado e podia ser alterado a cada nova versão.

O novo formato de arquivo, além de documentado, é baseado em padrões abertos, o que permite que qualquer aplicação, em qualquer linguagem ou plataforma possa abrir ou criar arquivos Office 2007. Este novo padrão, chamado de Open XML, é baseado em compactação zip e arquivos XML, gera arquivos com menor tamanho que os anteriores e permite que outras aplicações abram e alterem estes arquivos.

As possibilidades que se abrem são inúmeras:

  • Programas que permitem indexar e pesquisar textos a partir dos arquivos no disco
  • Programas para geração de documentos em lotes, a partir de bancos de dados e modelos
  • Programas para substituição de textos em lotes
  • Processadores de texto simplificados que geram arquivos Office
  • Geradores de planilhas eletrônicas a partir de dados provenientes de diversas fontes
  • ...

Neste artigo, iremos mostrar o novo formato de arquivos e como podemos acessá-los e criá-los usando o Delphi, sem a necessidade de instalar o Office.

Analisando um arquivo OpenXML

Um arquivo OpenXML, seja ele um documento (docx), planilha (xlsx) ou apresentação (pptx) é, na realidade, um arquivo zip composto de diversas pastas e arquivos XML. Podemos ver isso na prática, criando um documento no Word composto de algum texto e uma imagem:

Dn265965.B5D4B0C4B973D84E92FCD31D83AD25CF(pt-br,MSDN.10).png

Figura 2 – Exemplo de documento

Ao salvar este documento, podemos renomear o arquivo docx para zip e abri-lo com qualquer programa que abra arquivos zip:

Dn265965.36396609D9C83C3271D15272D557E0F1(pt-br,MSDN.10).png

Figura 3 – Arquivo docx aberto num editor de arquivos zip

Como podemos ver na Figura 3, o arquivo contém, na raiz, três diretórios, _rels, docProps e word e um arquivo, [Content_Types].xml. Esta estrutura de diretórios é criada pelo Word, e não é obrigatório que ela seja mantida. A localização dos arquivos fica no arquivo .rels, que está no diretório _rels. Este arquivo contém as relações entre o pacote e os arquivos de nível superior. A Figura 4 mostra o arquivo .rels criado em nosso exemplo:

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
  <Relationship Id="rId3" Type="http://schemas.openxmlformats.org/package/2006/relationships/metadata/core-properties" Target="docProps/core.xml"/>
  <Relationship Id="rId2" Type="http://schemas.openxmlformats.org/package/2006/relationships/metadata/thumbnail" Target="docProps/thumbnail.wmf"/>
  <Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="word/document.xml"/>
  <Relationship Id="rId4" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/extended-properties" Target="docProps/app.xml"/>
</Relationships>

Figura 4 – Arquivo .rels de exemplo

Analisando este arquivo, vemos o seguinte:

  • As propriedades principais (core-properties) estão no arquivo docProps/core.xml.
  • A imagem reduzida (thumbnail) está em docProps/thumbnail.wmf.
  • O documento principal (officeDocument) está em word/document.xml.
  • As propriedades estendidas (extended-properties) estão em docProps/app.xml.

Com base nestas informações, podemos abrir os arquivos que compõem o pacote OpenXML.

Adicionalmente, podemos ver que no diretório word existem um subdiretório _rels, que contém as relações para o documento. No arquivo document.txt.rels, encontramos as seguintes relações:

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
  <Relationship Id="rId3" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/webSettings" Target="webSettings.xml"/>
  <Relationship Id="rId2" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/settings" Target="settings.xml"/>
  <Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles" Target="styles.xml"/>
  <Relationship Id="rId6" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/theme" Target="theme/theme1.xml"/>
  <Relationship Id="rId5" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/fontTable" Target="fontTable.xml"/>
  <Relationship Id="rId4" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/image" Target="media/image1.png"/>
</Relationships>

Aqui são encontradas as relações para o documento. Vemos que os estilos aplicados no documento encontram-se no arquivo styles.xml e que a imagem que adicionamos está em media/image1.png. Assim, podemos acessar qualquer parte do documento.

A seguir iremos criar um pequeno programa em Delphi que abre um arquivo do Office e lista suas propriedades (tanto as principais como as estendidas) em um componente TValueListEditor.

Acessando os arquivos OpenXML

Para acessar os arquivos OpenXML , precisamos dividir nosso programa nas seguintes partes:

  • Abrir o pacote OpenXML com um componente que permite ler e gravar arquivos zip
  • Abrir o arquivo .rels em _rels e ler as relações, extraindo a localização das partes que nos interessam
  • Acessar as partes, executando a funcionalidade desejada.

O Delphi não tem nenhum componente nativo para acessar arquivos zip, porém pode-se utilizar o componente ZipMaster, que pode ser obtido gratuitamente em http://www.delphizip.org. Pode-se baixar um arquivo de instalação que adiciona o componente ao sistema, devendo-se instalar o pacote na IDE e abrir o arquivo dpk, que está no diretório Delphi.

Uma vez instalado, podemos criar um projeto para abrir os arquivos do Office. Crie um novo projeto no Delphi e adicione à Form um botão, um OpenDialog e um Memo.

Configure a propriedade Caption para Abrir. Configure a propriedade Filter do OpenDialog para “Arquivos Word (*.docx, *.docm)|*.docx;*.docm| Arquivos Excel (*.xlsx, *.xlsm)|*.xlsx;*.xlsm| Arquivos Powerpoint (*.pptx, *.pptm)|*.pptx;*.pptm”.

Coloque um componente TZipMaster para descompactar o arquivo que será aberto. No handler do evento OnClick do botão, coloque o seguinte código:

procedure TForm11.Button1Click(Sender: TObject);
var
  ZipStream : TZipStream;
begin
  if OpenDialog1.Execute then begin
    ZipMaster1.ZipFileName := OpenDialog1.FileName;
    ZipStream := ZipMaster1.ExtractFileToStream('_rels\.rels');
    ZipStream.Position := 0;
    Memo1.Lines.LoadFromStream(ZipStream);
  end;
end;

Se o usuário escolher um arquivo, indicamos o nome do arquivo para o componente ZipMaster e depois extraímos o arquivo .rels para um stream e carregamos as linhas do Memo com este stream. A figura 5 mostra o resultado da execução.

Dn265965.39C40779667839DBB5264E0A3701D888(pt-br,MSDN.10).png

Figura 5 – Memo com arquivo .rels carregado

Uma vez que temos o arquivo .rels, devemos lê-lo e interpretar as relações. Poderíamos usar as funções de leitura de arquivos texto e interpretar o documento, porém essa não é a melhor maneira de fazer esta operação. O ideal é usar um componente próprio para a leitura de arquivos XML, como o componente TXMLDocument, que vem com o Delphi na guia Internet da paleta de componentes.

Coloque dois componentes TXMLDocument e um componente TValueListEditor na Form. Modifique a propriedade TitleCaptions do TValueListEditor para Propriedade/Valor. No evento OnClick do botão, modifique o código para:

procedure TMainFrm.Button1Click(Sender: TObject);
var
  ZipStream : TZipStream;
  XmlNode : IXMLNode;
  i: integer;
  AttType : String;
begin
  if OpenDialog1.Execute then begin
    ZipMaster1.ZipFileName := OpenDialog1.FileName;
    // Lê relações
    ZipStream := ZipMaster1.ExtractFileToStream('_rels\.rels');
    ZipStream.Position := 0;
    Memo1.Lines.LoadFromStream(ZipStream);
    XMLDocument1.LoadFromStream(ZipStream);
    ValueListEditor1.Strings.Clear;
    // Processa nós
    for i := 0 to XMLDocument1.DocumentElement.ChildNodes.Count-1 do begin
      XmlNode := XMLDocument1.DocumentElement.ChildNodes.Nodes[i];
      // Pega o tipo de relação.
      // Ela é a parte final do atributo Type
      AttType :=  
         ExtractFileName(StringReplace(XmlNode.Attributes['Type'],
         '/','\', [rfReplaceAll]));
      if AnsiSameText(AttType, 'core-properties') or
         AnsiSameText(AttType, 'extended-properties') then
        // Adiciona as propriedades no ValueListEditor
        LePropriedades(XmlNode.Attributes['Target']);
    end;
  end;
end;

Carregamos o stream em XMLDocument1 e processamos os nós, para encontrar aqueles com os tipos que queremos (core-properties ou extended-properties). Quando os encontramos, passamos o nome do arquivo (que está no atributo Target) para a função LePropriedades, que irá ler o arquivo de propriedades e adicioná-las ao ValueListEditor. A função LePropriedades é:

procedure TMainFrm.LePropriedades(const Arquivo : String);
var
  ZipStream : TZipStream;
  i : Integer;
  XmlNode : IXMLNode;
begin
  // Lê o arquivo no zip.
  // Devemos mudar as barras '/' por '\' para que o ZipMaster 
  // encontre o arquivo
  ZipStream := ZipMaster1.ExtractFileToStream(StringReplace(Arquivo,
      '/','\',[rfReplaceAll]));
  ZipStream.Position := 0;
  XMLDocument2.LoadFromStream(ZipStream);
  // Lê as propriedades
  for i := 0 to XMLDocument2.DocumentElement.ChildNodes.Count-1 do begin
    XmlNode := XMLDocument2.DocumentElement.ChildNodes.Nodes[i];
    try
      // Achou nova propriedade adiciona
      ValueListEditor1.InsertRow(XmlNode.NodeName,XmlNode.NodeValue, True);
    except
      // Propriedade não é um valor simples - despreza.
      On EXMLDocError do
        ;
      // Propriedade é nula - adiciona sting nulo  
      On EVariantTypeCastError do
        ValueListEditor1.InsertRow(XmlNode.NodeName,'', True);
    end;
  end;
end;

Esta função é parecida com a anterior. Iremos ler o arquivo no segundo TXMLDocument e inserir uma linha no ValueListEditor para cada propriedade encontrada. Tratamos aqui dois tipos de exceção: EXMLDocError, que pode acontecer quando o tipo de dado não é um tipo simples, como um string ou inteiro e EVariantTypeCastError, que acontece quando o valor é nulo. Desta maneira, adicionamos as propriedades na lista, como mostra a figura 6.

Dn265965.89DCA002EB14547E4FF2C157DF5E3778(pt-br,MSDN.10).png

Figura 6 – Lista de propriedades do arquivo

Como podemos ver, o acesso aos dados de um arquivo OpenXML é relativamente simples, e pode ser feito usando componentes gratuitos ou disponíveis no Delphi, mas isso não é tudo que pode ser feito: como estamos trabalhando com pacotes zip normais e arquivos XML, usando tecnologia aberta, podemos também modificar os arquivos, usando as mesmas técnicas.

Alterando arquivos OpenXML

Um problema muito comum acontece quando uma empresa muda seu logotipo e tem de alterar os seus documentos. Uma solução é abrir todos os arquivos, um a um, e substituir a imagem do logotipo antigo pelo novo. Se a empresa tiver poucos documentos, esta é uma tarefa fácil, mas se houver centenas ou milhares de documentos espalhados pelos discos dos usuários, como isso pode ser feito?

Nossa empresa fictícia, ACME Inc, está substituindo o logotipo da Figura 7 pelo da Figura 8.

Dn265965.E944C96597FBCFD21E42D8C731DA000C(pt-br,MSDN.10).png

Figura 7 – Logotipo antigo

Dn265965.ECD6D5399E93BA38CDE73BBAF0DAD4E7(pt-br,MSDN.10).png

Figura 8 – Logotipo novo

Seus documentos contêm o logotipo no cabeçalho da página, como mostra a Figura 9.

Dn265965.307BC31966E2B14B6396E0ADA23C47E0(pt-br,MSDN.10).png

Figura 9 – Estrutura do arquivo com logotipo

Devemos substituir o logotipo, sem que haja a necessidade de se abrir o arquivo. Nosso programa irá substituir o logotipo de apenas um documento, mas é fácil usar técnicas de pesquisa de arquivos para procurar todos os arquivos com extensão docx do disco e fazer esta substituição.

Crie um novo projeto e adicione na Form um componente OpenDialog, um ZipMaster e dois XMLDocument. Altere a propriedade Caption do botão para Abrir e a propriedade Filter do OpenDialog para “Arquivos Word (*.docx, *.docm)|*.docx;*.docm”.

No evento OnClick do botão, coloque o seguinte código:

procedure TMainFrm.Button1Click(Sender: TObject);
begin
  if OpenDialog1.Execute then
    AlteraLogotipo(OpenDialog1.FileName);
end;

A função AlteraLogotipo é semelhante à que criamos anteriormente para abrir o arquivo zip e ler suas relações:

procedure TMainFrm.AlteraLogotipo(const Arquivo : String);
var
  ZipStream : TZipStream;
  XmlNode : IXMLNode;
  i: integer;
  AttType : String;
  ArqCabecalho: String;
  ArqImagem: string;
begin
  ZipMaster1.ZipFileName := Arquivo;
  // Lê relações
  ZipStream := ZipMaster1.ExtractFileToStream('_rels\.rels');
  ZipStream.Position := 0;
  XMLDocument1.LoadFromStream(ZipStream);
  // Processa nós
  for i := 0 to XMLDocument1.DocumentElement.ChildNodes.Count-1 do begin
    XmlNode := XMLDocument1.DocumentElement.ChildNodes.Nodes[i];
    // Pega o tipo de relação.
    // Ela é a parte final do atributo Type
    AttType := 
       ExtractFileName(StringReplace(XmlNode.Attributes['Type'],
       '/','\', [rfReplaceAll]));
    if AnsiSameText(AttType, 'officeDocument') then begin
      ArqCabecalho := ProcuraCabecalho(XmlNode.Attributes['Target']);
      if ArqCabecalho <> '' then begin
        ArqImagem := ProcuraImagem(ArqCabecalho);
        if ArqImagem <> '' then
          SubstituiImagem(ArqImagem);
      end;
    end;
  end;
end;

Aqui precisamos explicar como iremos obter o local da imagem que fica no cabeçalho. Cada arquivo que tem outros relacionados tem um arquivo correspondente no diretório _rels de onde ele se encontra, com o mesmo nome do arquivo, acrescido da extensão .rels.

No caso de nosso exemplo, vemos que o arquivo de documento está em word/document.xml. Assim, as relações para o documento estão em word/_rels/document.xml.rels. Ali encontramos o local onde está o cabeçalho que queremos (no documento de exemplo da Figura 9, temos três arquivos, header1.xml, header2.xml e header3.xml). Com o nome do arquivo de cabeçalho em mãos, iremos procurar o seu arquivo de relação (em _rels/header1.xml.rels, por exemplo), para encontrar o local da imagem do cabeçalho. Finalmente, substituímos a imagem antiga pela nova no arquivo zip.

A procura do arquivo de cabeçalho que contém uma relação é feita na função ProcuraCabecalho:

function TMainFrm.ProcuraCabecalho(const Arquivo : String) : String;
var
  ZipStream : TZipStream;
  i : Integer;
  XmlNode : IXMLNode;
  NomeRel: string;
  AttType: string;
  function ArquivoRel(const NomeArq : String) : String;
  begin
    Result := StringReplace(NomeArq, '/','\',[rfReplaceAll]);
    Result := 
      ExtractFilePath(Result)+'_rels\'+ExtractFileName(Result)+'
      .rels';
  end;
begin
  // NomeRel tem o nome do arquivo de relacionamentos do documento
  NomeRel := ArquivoRel(Arquivo);
  // Adicionamos o diretório _rels e a extensão .rels
  // Se não encontrar arquivo, sai
  if ZipMaster1.IndexOf(NomeRel) < 0 then
    exit;
  ZipStream := ZipMaster1.ExtractFileToStream(NomeRel);
  // Lê arquivo de relacionamento de documento
  if ZipStream = nil then
    exit;
  ZipStream.Position := 0;
  XMLDocument2.LoadFromStream(ZipStream);
  // Lê as relações
  for i := 0 to XMLDocument2.DocumentElement.ChildNodes.Count-1 do begin
    XmlNode := XMLDocument2.DocumentElement.ChildNodes.Nodes[i];
    AttType := 
       ExtractFileName(StringReplace(XmlNode.Attributes['Type'],
       '/','\', [rfReplaceAll]));
    if AnsiSameText(AttType, 'header') then begin
      // Devemos adicionar o caminho do arquivo de documento 
      // ao relacionamento
      NomeRel := ExtractFilePath(StringReplace(Arquivo, 
         '/','\',[rfReplaceAll]))+
         ArquivoRel(XmlNode.Attributes['Target']);
      // Se achou cabeçalho com relacionamento, sai com o nome;
      if ZipMaster1.IndexOf(NomeRel) >= 0 then begin
        Result := NomeRel;
        exit;
      end;
    end;
  end;
  Result := '';
end;

A função pesquisa o arquivo de relações do documento para encontrar os arquivos de cabeçalho (tipo header). Quando encontra algum, verifica se há algum arquivo de relações para ele. Se encontrar, a função retorna o nome do arquivo de relações do cabeçalho, para que possamos encontrar a imagem. A função ProcuraImagem é a seguinte:

function TMainFrm.ProcuraImagem(const Arquivo : String) : String;
var
  ZipStream : TZipStream;
  i : Integer;
  XmlNode : IXMLNode;
  AttType: string;
begin
  ZipStream := ZipMaster1.ExtractFileToStream(Arquivo);
  // Lê arquivo de relacionamento de cabeçalho
  if ZipStream = nil then
    exit;
  ZipStream.Position := 0;
  XMLDocument2.LoadFromStream(ZipStream);
  // Lê as relações
  for i := 0 to XMLDocument2.DocumentElement.ChildNodes.Count-1 do begin
    XmlNode := XMLDocument2.DocumentElement.ChildNodes.Nodes[i];
    AttType := 
       ExtractFileName(StringReplace(XmlNode.Attributes['Type'],
       '/','\', [rfReplaceAll]));
    // Se achou imagem, sai com o nome da imagem
    if AnsiSameText(AttType, 'image') then begin
      // Devemos adicionar o caminho do arquivo de documento 
      // ao relacionamento
      Result := ExtractFilePath(ExtractFileDir(Arquivo))+
         StringReplace(XmlNode.Attributes['Target'], 
         '/','\',[rfReplaceAll]);
      exit;
    end;
  end;
  Result := '';
end;

Esta função lê o arquivo de relações do cabeçalho, procurando uma imagem. Caso encontre, ela retorna com o nome do arquivo. Este arquivo deve ser substituído na função SubstituiImagem:

procedure TMainFrm.SubstituiImagem(const Arquivo : String);
begin
  ZipMaster1.ZipStream.LoadFromFile(ExtractFilePath(ParamStr(0))+
    'Acme2.png');
  ZipMaster1.AddOptions := ZipMaster1.AddOptions + [AddDirNames];
  ZipMaster1.AddStreamToFile(Arquivo,0,0);
end;

A imagem que substitui a original chama-se Acme2.png e está no mesmo diretório do executável. Esta imagem é substituída no arquivo, com o mesmo nome da original, para que não tenhamos de mudar as referências ao nome do arquivo.

Até agora, vimos como acessar um arquivo ou modificar um já existente. Porém, nada impede que possamos criar um arquivo a partir de nossos dados.

Criação de um arquivo OpenXML

Para gerar um arquivo OpenXML, precisamos criar alguns arquivos que irão compor o pacote. O pacote mínimo deve conter três arquivos:

  • [Content_Types].xml
  • _rels/.rels
  • document.xml

Não é necessário criar uma estrutura de diretórios como a do Word, basta apenas que indiquemos no arquivo .rels a localização dos dados. À medida que vamos adicionando novas funcionalidades, como imagens, cabeçalhos, temas e estilos, precisamos incluir novos arquivos para adicionar estas partes ao documento. Inicialmente, iremos criar um arquivo simples, para mostrar o processo de geração de um arquivo e, em seguida, mostraremos como criar um arquivo mais complexo.

Crie um novo projeto e coloque um Label, um Memo e um botão na Form. Mude a propriedade Caption do Label para Texto:, a propriedade Caption do Button para Criar e limpe a propriedade Lines do Memo.

Coloque um componente ZipMaster e um XMLDocument. No evento OnClick do botão, coloque o seguinte código:

procedure TMainFrm.Button1Click(Sender: TObject);
begin
  ZipMaster1.ZipFileName := 'ArquivoSimples.docx';
  ZipMaster1.AddOptions := [AddDirNames];
  CriaContentTypes(ZipMaster1.ZipStream);
  ZipMaster1.AddStreamToFile('[Content_Types].xml',0,0);
  CriaRels(ZipMaster1.ZipStream);
  ZipMaster1.AddStreamToFile('_rels\.rels',0,0);
  CriaDocumento(ZipMaster1.ZipStream);
  ZipMaster1.AddStreamToFile('word\document.xml',0,0);
end;

O programa irá criar os diversos arquivos necessários, adicionar os streams para o arquivo zip e criar um arquivo com o nome ArquivoSimples.docx. A função que cria o arquivo [Content_Types.xml] é:

procedure TMainFrm.CriaContentTypes(AStream : TStream);
var
  Root: IXmlNode;
  Tipo: IXmlNode;
begin
  LimpaXML;
  XMLDocument1.Options := [doNodeAutoIndent];
  XMLDocument1.Active := True;
  // Cabeçalho
  XMLDocument1.Encoding := 'UTF-8';
  XMLDocument1.Version := '1.0';
  XMLDocument1.StandAlone := 'yes';
  // Nó raiz
  Root := XMLDocument1.addChild('Types',
    'http://schemas.openxmlformats.org/package/2006/content-types');
  // Definição de tipos
  Tipo := Root.AddChild('Default');
  Tipo.Attributes['Extension'] := 'rels';
  Tipo.Attributes['ContentType'] :=
    'application/vnd.openxmlformats-package.relationships+xml';
  Tipo := Root.AddChild('Default');
  Tipo.Attributes['Extension'] := 'xml';
  Tipo.Attributes['ContentType'] :=
    'application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml';
  // Grava no stream de saída
  XMLDocument1.SaveToStream(AStream);
  AStream.Position := 0;
  AStream.Size := Length(XMLDocument1.XML.Text);
end;

A função que cria o arquivo de relações é:

procedure TMainFrm.CriaRels(AStream : TStream);
var
  Root: IXmlNode;
  Rel: IXmlNode;
begin
  LimpaXML;
  XMLDocument1.Options := [doNodeAutoIndent];
  XMLDocument1.Active := True;
  // Cabeçalho
  XMLDocument1.Encoding := 'UTF-8';
  XMLDocument1.Version := '1.0';
  XMLDocument1.StandAlone := 'yes';
  // Nó raiz
  Root := XMLDocument1.addChild('Relationships',
    'http://schemas.openxmlformats.org/package/2006/relationships');
  // Definição de relações
  Rel := Root.AddChild('Relationship');
  Rel.Attributes['Id'] := 'rId1';
  Rel.Attributes['Type'] :=
    'http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument';
  Rel.Attributes['Target'] := 'word/document.xml';
  // Grava no stream de saída
  XMLDocument1.SaveToStream(AStream);
  AStream.Position := 0;
  AStream.Size := Length(XMLDocument1.XML.Text);
end;

O código para gravar o documento com o texto digitado no Memo é:

procedure TMainFrm.CriaDocumento(AStream : TStream);
var
  Root: IXmlNode;
begin
  LimpaXML;
  XMLDocument1.Options := [doNodeAutoIndent];
  XMLDocument1.Active := True;
  // Cabeçalho
  XMLDocument1.Encoding := 'UTF-8';
  XMLDocument1.Version := '1.0';
  XMLDocument1.StandAlone := 'yes';
  // Nó raiz
  Root := XMLDocument1.addChild('wordDocument', 'http://schemas.openxmlformats.org/wordprocessingml/2006/main');
  // Grava texto
  Root.AddChild('body').AddChild('p').AddChild('r').AddChild('t').NodeValue :=  Memo1.Text;
  // Grava no stream de saída
  XMLDocument1.SaveToStream(AStream);
  AStream.Position := 0;
  AStream.Size := Length(XMLDocument1.XML.Text);
end;

Aqui apenas precisamos gravar um nó dentro do nó raiz wordDocument: ele é o corpo do documento, que tem um parágrafo (nó p), um “run” (nó r) e o texto, que é o conteúdo do Memo. Ao compilar e executar o programa, podemos digitar um texto no Memo e clicar no botão Criar. O arquivo docx é criado com o texto digitado.

Colocando mais informações no arquivo

Uma vez que sabemos como criar nossos arquivos, podemos adicionar mais informações no que está sendo criado. Criaremos agora um exemplo que mostra as fontes disponíveis no sistema. Este documento será gerado em formato paisagem, e colocaremos um cabeçalho com três colunas e o número da página.

Crie um novo projeto e coloque um botão, um XmlDocument e um ZipMaster. Altere a propriedade Caption do botão para Criar. No evento OnClick do botão, coloque:

procedure TMainFrm.Button1Click(Sender: TObject);
begin
  ZipMaster1.ZipFileName := 'ArquivoComplexo.docx';
  ZipMaster1.AddOptions := [AddDirNames];
  CriaContentTypes(ZipMaster1.ZipStream);
  ZipMaster1.AddStreamToFile('[Content_Types].xml',0,0);
  CriaRels(ZipMaster1.ZipStream);
  ZipMaster1.AddStreamToFile('_rels\.rels',0,0);
  CriaDocumento(ZipMaster1.ZipStream);
  ZipMaster1.AddStreamToFile('word\document.xml',0,0);
end;

As funções CriaRels e CriaContentTypes são as mesmas da rotina anterior. A função CriaDocumento é a seguinte:

procedure TMainFrm.CriaDocumento(AStream : TStream);
var
  Root, Body: IXmlNode;
  i: Integer;
begin
  LimpaXML;
  CriaCabecalho;
  // Nó raiz
  Root := XMLDocument1.addChild('w:wordDocument');
  Root.DeclareNamespace('w', 'http://schemas.openxmlformats.org/wordprocessingml/2006/main');
  Root.DeclareNamespace('r','http://schemas.openxmlformats.org/officeDocument/2006/relationships');
  Body := Root.AddChild('w:body');
  for i := 0 to Screen.Fonts.Count - 1 do begin
    AdicionaFonte(Body, Screen.Fonts[i]);
  end;
  // Grava no stream de saída
  XMLDocument1.SaveToStream(AStream);
  AStream.Position := 0;
  AStream.Size := Length(XMLDocument1.XML.Text);
end;

Iremos varrer as fontes do sistema, chamando a função AdicionaFonte, que irá adicionar o texto formatado no arquivo Document.xml:

procedure TMainFrm.AdicionaFonte(Body : IXMLNode; NomeFonte : String);
var
  Fonte: IXMLNode;
  Run: IXMLNode;
  RunPr: IXMLNode;
begin
  Run := Body.AddChild('w:p').AddChild('w:r');
  RunPr := Run.AddChild('w:rPr');
  Fonte := RunPr.AddChild('w:rFonts');
  Fonte.Attributes['w:ascii'] := NomeFonte;
  Fonte.Attributes['w:hAnsi'] := NomeFonte;
  Fonte.Attributes['w:cs'] := NomeFonte;
  RunPr.AddChild('w:sz').Attributes['w:val'] := 30;
  Run.AddChild('w:t').NodeValue := NomeFonte;
  Run.AddChild('w:tab');
  Run.AddChild('w:t').NodeValue := 'The quick brown fox jumps over the lazy dog';
end;

Para cada fonte do sistema, adicionamos um parágrafo e, nele, um Run. O Run deve ser formatado com o elemento rPr, colocando-se como filho o elemento rFonts e o nome da fonte como valores dos atributos ascii, hAnsi e cs. Também mudamos o tamanho da fonte adicionando o elemento sz. Em seguida, colocamos o nome da fonte como texto, adicionando o elemento tab para gerar uma tabulação e um texto de exemplo. Ao rodar o programa, vemos que a lista de fontes é gerada no documento.

O próximo passo é fazer que o documento seja colocado em paisagem. Para isso, devemos adicionar ao final do documento um elemento sectPr (propriedades da seção), que indica a formatação da seção. Coloque o seguinte código ao final de CriaDocumento, antes da linha XMLDocument1.SaveToStream(AStream):

SectPr := Body.AddChild('sectPr');
  PgSz := SectPr.AddChild('w:pgSz');
  PgSz.Attributes['w:w'] := Round(297/25.4*1440);
  PgSz.Attributes['w:h'] := Round(210/25.4*1440);
  PgSz := SectPr.AddChild('w:pgMar');
  pgSz.Attributes['w:top'] := 1440;
  pgSz.Attributes['w:bottom'] := 1440;
  pgSz.Attributes['w:left'] := 720;
  pgSz.Attributes['w:right'] := 720;
  pgSz.Attributes['w:header'] := 720;
  pgSz.Attributes['w:footer'] := 720;

Neste código adicionamos o elemento pgSz (Page size), dando os atributos w e h para a largura e altura da página. Estas medidas são em twips (1/1440 de polegada), assim fazemos a conversão do tamanho da página A4 para twips. Em seguida, colocamos o elemento pgMar (Page margins), que determina as margens da página e a posição do cabeçalho e rodapé. Ao rodarmos o programa e abrirmos o documento, vemos que ele está em paisagem.

O último passo é colocar o cabeçalho. Colocamos o cabeçalho em um arquivo separado e, desta maneira, devemos alterar todas as referências para que este novo documento seja lido.

Inicialmente, criamos uma referência para o cabeçalho na seção, como filho de sectPr. Coloque o seguinte código em CriaDocumento, após a linha SectPr := Body.AddChild('sectPr'):

Header := SectPr.AddChild('w:headerReference');
  Header.Attributes['w:type'] := 'default';
  Header.Attributes['r:id'] := 'rId1';

Para usar as referências, devemos adicionar um novo namespace ao documento. Isto é feito adicionando a seguinte linha após a declaração do namespace em CriaDocumento:

Root.DeclareNamespace('r','http://schemas.openxmlformats.org/officeDocument/2006/relationships');

procedure TMainFrm.CriaDocRels(AStream : TStream);
var
  Root: IXmlNode;
  Rel: IXmlNode;
begin
  LimpaXML;
  CriaCabecalho;
  // Nó raiz
  Root := XMLDocument1.addChild('Relationships',
    'http://schemas.openxmlformats.org/package/2006/relationships');
  // Definição de relações
  Rel := Root.AddChild('Relationship');
  Rel.Attributes['Id'] := 'rId1';
  Rel.Attributes['Type'] :=
    'http://schemas.openxmlformats.org/officeDocument/2006/relationships/header';
  Rel.Attributes['Target'] := 'header1.xml';
  // Grava no stream de saída
  XMLDocument1.SaveToStream(AStream);
  AStream.Position := 0;
  AStream.Size := Length(XMLDocument1.XML.Text);
end;

Esta função é semelhante à que cria o relacionamento do pacote. A função que cria o cabeçalho no arquivo header1.xml é:

procedure TMainFrm.CriaHeader(AStream : TStream);
var
  Root, Header, PTab: IXmlNode;
begin
  LimpaXML;
  CriaCabecalho;
  // Nó raiz
  Root := XMLDocument1.addChild('w:hdr');
  Root.DeclareNamespace('w', 'http://schemas.openxmlformats.org/wordprocessingml/2006/main');
  Header := Root.AddChild('w:p');
  Header.AddChild('w:r').AddChild('w:t').NodeValue := 'Texto 1';
  PTab := Header.AddChild('w:r').AddChild('w:ptab');
  PTab.Attributes['w:relativeTo'] := 'margin';
  PTab.Attributes['w:alignment'] := 'center';
  PTab.Attributes['w:leader'] := 'none';
  Header.AddChild('w:r').AddChild('w:t').NodeValue := 'Texto 2';
  PTab := Header.AddChild('w:r').AddChild('w:ptab');
  PTab.Attributes['w:relativeTo'] := 'margin';
  PTab.Attributes['w:alignment'] := 'right';
  PTab.Attributes['w:leader'] := 'none';
  Header.AddChild('w:fldSimple').Attributes['w:instr'] := 'PAGE \* MERGEFORMAT';
  // Grava no stream de saída
  XMLDocument1.SaveToStream(AStream);
  AStream.Position := 0;
  AStream.Size := Length(XMLDocument1.XML.Text);
end;

Aqui criamos o cabeçalho com um texto alinhado à esquerda, uma tabulação para alinhar o texto centralizado e outra tabulação para alinhar o número da página à direita. O número da página é dado pelo elemento fldSimple, usando-se o atributo instr com o valor PAGE \* MERGEFORMAT. Após criar estas funções devemos colocar o código para chamá-las, ao final do evento OnClick do botão:

CriaDocRels(ZipMaster1.ZipStream);

ZipMaster1.AddStreamToFile('word\_rels\document.xml.rels',0,0);

CriaHeader(ZipMaster1.ZipStream);

ZipMaster1.AddStreamToFile('word\header1.xml',0,0);

Agora, precisamos apenas fazer uma pequena mudança em [Content_Types].xml, adicionando o elemento Override, para determinar o tipo de header1.xml. Coloque o seguinte código em CriaContentTypes, antes da linha XMLDocument1.SaveToStream(AStream):

Tipo := Root.AddChild('Override');

Tipo.Attributes['PartName'] := '/word/header1.xml';

Tipo.Attributes['ContentType'] := 'application/vnd.openxmlformats

-officedocument.wordprocessingml.header+xml';

Com isso, nosso programa está pronto. Ao executá-lo, geramos um documento semelhante ao mostrado na Figura 10.

Dn265965.70008B60E15482A4338131C6B1A325F5(pt-br,MSDN.10).png

Figura 10 – Arquivo gerado, em paisagem, com cabeçalho

Conclusões

O formato OpenXML traz grandes vantagens para quem quer processar e abrir arquivos do Office. Como este formato usa tecnologias abertas e está completamente documentado, podemos acessar, alterar ou mesmo criar arquivos do Office usando quaisquer ferramentas de desenvolvimento (ou mesmo alterando-se os arquivos manualmente), em qualquer plataforma ou linguagem.

Não são necessárias APIs proprietárias ou programas especiais, o que permite que a informação esteja disponível para qualquer um que queira acessá-la. Mostramos aqui como manipular os arquivos em Delphi, fazendo notar que utilizamos apenas um componente disponível gratuitamente (o ZipMaster) e componentes padrão do Delphi, utilizando apenas arquivos zip e XML.

Mostrar: