Introdução

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.

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. Ao salvar este documento, podemos renomear o arquivo docx para zip e abri-lo com qualquer programa que abra arquivos zip:

Como podemos ver na Figura acima, 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.  O seguinte código 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>

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.

Para abrir os arquivos Zip, usaremos o componente TZipFile, que está disponível no Delphi a partir da versão XE2. Este componente permite manipular arquivos zip de maneira relativamente simples. 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”.

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

procedure TMainFrm.Button1Click(Sender: TObject);
var
  ZipStream: TStream;
  XmlNode: IXMLNode;
  i: Integer;
  AttType: String;
  ZipFile: TZipFile;
  LocalHeader: TZipHeader;
begin
  if OpenDialog1.Execute then begin
    ZipFile := TZipFile.Create();
    try
      ZipFile.Open(OpenDialog1.FileName, TZipMode.zmRead);
      try
        // Lê relações
        ZipFile.Read('_rels/.rels', ZipStream, LocalHeader);
        ZipStream.Position := 0;
        XMLDocument1.LoadFromStream(ZipStream);
        Memo1.Text := XMLDoc.FormatXMLData(XMLDocument1.XML.Text);
      finally
        ZipStream.Free;
      end;
    finally
      ZipFile.Close();
      ZipFile.Free;
    end;
  end;
end;

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

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: TStream;
  XmlNode: IXMLNode;
  i: Integer;
  AttType: String;
  ZipFile: TZipFile;
  LocalHeader: TZipHeader;
begin
  if OpenDialog1.Execute then begin
    ZipFile := TZipFile.Create();
    try
      ZipFile.Open(OpenDialog1.FileName, TZipMode.zmRead);
      // Lê relações
      ZipFile.Read('_rels/.rels', ZipStream, LocalHeader);
      try
        ZipStream.Position := 0;
        XMLDocument1.LoadFromStream(ZipStream);
        Memo1.Text := XMLDoc.FormatXMLData(XMLDocument1.XML.Text);
        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(XmlNode.Attributes['Type']);
          if AttType.EndsWith('core-properties') or
             AttType.EndsWith('extended-properties') then
            // Adiciona as propriedades no ValueListEditor
            LePropriedades(ZipFile, XmlNode.Attributes['Target']);
        end;
      finally
        ZipStream.Free;
      end;
    finally
      ZipFile.Close();
      ZipFile.Free;
    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(ZipFile: TZipFile; const Arquivo: String);
var
  ZipStream: TStream;
  i: Integer;
  XmlNode: IXMLNode;
  LocalHeader: TZipHeader;
begin
  ZipFile.Read(Arquivo, ZipStream, LocalHeader);
  try
    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;
  finally
    ZipStream.Free;
  end;
end;

Esta função é parecida com a anterior. Iremos ler o arquivo de propriedades 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 a seguir.

Como podemos ver, o acesso aos dados de um arquivo OpenXML é relativamente simples, e pode ser feito usando componentes 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.

Até agora, vimos como acessar um arquivo. 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
  • 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 XMLDocument. No evento OnClick do botão, coloque o seguinte código:

procedure TMainFrm.Button1Click(Sender: TObject);
var
  zipFile: TZipFile;
  contentTypes: TStream;
  rels: TStream;
  doc: TStream;
begin
  zipFile := TZipFile.Create();
  try
    zipFile.Open('ArquivoSimples.docx', TZipMode.zmWrite);
    contentTypes := CriaContentTypes();
    try
      zipFile.Add(contentTypes, '[Content_Types].xml');
    finally
      contentTypes.Free;
    end;
    rels := CriaRels();
    try
      zipFile.Add(rels, '_rels\.rels');
    finally
      rels.Free;
    end;
    doc := CriaDocumento();
    try
      zipFile.Add(doc, 'word\document.xml');
    finally
      doc.Free;
    end;
  finally
    zipFile.Close();
    zipFile.Free;
  end;
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] é:

function TMainFrm.CriaContentTypes(): TStream;
var
  Root: IXmlNode;
  Tipo: IXmlNode;
  XMLDoc: IXmlDocument;
begin
  Result := TMemoryStream.Create();
  XMLDoc := CriaXml;
  // Nó raiz
  Root := XMLDoc.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
  XMLDoc.SaveToStream(Result);
  Result.Position := 0;
end;

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

function TMainFrm.CriaRels(): TStream;
var
  Root: IXmlNode;
  Rel: IXmlNode;
  XMLDoc: IXmlDocument;
begin
  Result := TMemoryStream.Create();
  XMLDoc := CriaXml;
  // Nó raiz
  Root := XMLDoc.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
  XMLDoc.SaveToStream(Result);
  Result.Position := 0;
end;

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

function TMainFrm.CriaDocumento(): TStream;
var
  Root: IXmlNode;
  XMLDoc: IXmlDocument;
begin
  Result := TMemoryStream.Create();
  XMLDoc := CriaXml;
  // Nó raiz
  Root := XMLDoc.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
  XMLDoc.SaveToStream(Result);
  Result.Position := 0;
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  Altere a propriedade Caption do botão para Criar. No evento OnClick do botão, coloque:

procedure TMainFrm.Button1Click(Sender: TObject);
var
  ZipFile: TZipFile;
  MemStream: TMemoryStream;
begin
  ZipFile := TZipFile.Create();
  try
    ZipFile.Open('ArquivoComplexo.docx', TZipMode.zmWrite);
    MemStream := TMemoryStream.Create();
    try
      CriaContentTypes(MemStream);
      ZipFile.Add(MemStream, '[Content_Types].xml');
      MemStream.Clear;
      CriaRels(MemStream);
      ZipFile.Add(MemStream, '_rels\.rels');
      MemStream.Clear;
      CriaDocumento(MemStream);
      ZipFile.Add(MemStream, 'word\document.xml');
    finally
      MemStream.Free;
    end;
  finally
    ZipFile.Close();
    ZipFile.Free;
  end;
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, PgSz: IXMLNode;
  i: Integer;
  SectPr: IXMLNode;
  Header: IXMLNode;
begin
  LimpaXML;
  CriaCabecalho;
  // Nó raiz
  Root := XMLDocument1.addChild('w:wordDocument');
  Root.DeclareNamespace('w',
    'http://schemas.openxmlformats.org/wordprocessingml/2006/main');
  Body := Root.addChild('w:body');
  for i := 0 to Screen.Fonts.Count - 1 do
    AdicionaFonte(Body, Screen.Fonts[i]);
  
  // Grava no stream de saída
  XMLDocument1.SaveToStream(AStream);
  AStream.Position := 0;
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');

Criamos uma referência rId1 no documento. Devemos então criar uma função que cria este relacionamento no arquivo word\_rels\document.xml.rels:

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;
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;
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:

MemStream.Clear;
CriaDocRels(MemStream);
ZipFile.Add(MemStream, 'word\_rels\document.xml.rels');
MemStream.Clear;
CriaHeader(MemStream);
ZipFile.Add(MemStream, 'word\header1.xml');

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 a seguir:

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 componentes padrão do Delphi, utilizando apenas arquivos  zip e XML.

O código fonte para este projeto está em https://github.com/bsonnino/OpenXmlDelphiPort

 

Introduction

Office 2007 brought many significant changes for this app suite. The most visible was the Ribbon, an new user interface that eases the use of the applications.

Another change, less visible, allows the integration of Office applications with a large variety of programs: the new file format. Until the previous version, the file format was proprietary: when you wanted to open or save an Office document in our applications, you should use Ole Automation, what required that Office was installed in the client’s machine, or try to discover the internal file format, which wasn’t documented and could be changed at any time.

The new file format, besides being documented, is based in open standards, thus allowing that any aplication for any platform, written in any language to open or create Office 2007 files. This new standard, named OpenXML, is based on the zip packaging and XML files. It creates smaller files and allows other applications to open and change these files.

This opens a lot of possibilities:

  • Programs to index and search text from the files in the machine
  • Programs for batch generation of documents, based on databases and templates
  • Programs for batch text replacement
  • Simple text editors that generate Office files
  • Spreadsheet generation using data from many sources

In this article, we will show the new file format and how we can read and crete them using Delphi, with no need to install Ofiice.

Analyzing an OpenXML file

Any OpenXML file is, in fact, a zip file with many folders and XML files. We can see that in practice, by creating a Word file with some text and save it. If we rename this file to zip, we can open it with any program that can open zip files:

As you can see in the figure above, the file contains in the root three directories,  _rels, docProps and word and a file, [Content_Types].xml. This directory structure is created by Word and it’s not obligatory to maintain it. The files location is in the .rels file, located in the  _rels folder. This file contais the relations between the package and the files in the upper level. The following code shows the rels file from the example:

<?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>

Analyzing this file, we see the following:

  • The core properties are in the docProps/core.xml file.
  • The thumbnail is in the docProps/thumbnail.wmf file.
  • The main document is in word/document.xml.
  • The extended-properties are in docProps/app.xml.

Besides that, we can see that the word directory contains a _rels subdirectory, which contains the relations for the document. In the file document.txt.rels, we find the following relations:

<?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>

Here you can find the relations for the document. We see that the styles used in the document are in styles.xml and any image in the document is in the media folder. That way, we can access any part of the document.

Based on these informations, we can open the files that are in the OpenXML package. Now, we will create a small Delphi program that opens an Office file and lists its properties in a component TValueListEditor.

Accessing OpenXml files

To open OpenXml files, we need to divide our program in the following parts:

  • Open the OpenXml package with a component that allows to read and write zip files
  • Open the .rels file and read the relations, extracting the parts that interest us
  • Access the parts, executing what we want

To open the zip files, we will use the TZipFile component, which is available in Delphi since version XE2. This component allows to manipulate zip files in a relatively simple way. You should create a new Delphi project and add to the main form a button, an OpenDialog and a Memo.

Configure the Caption property of the button to Open. Configure the Filter property of the OpenDialog to “Word Files (*.docx, *.docm)|*.docx;*.docm| Excel Files(*.xlsx, *.xlsm)|*.xlsx;*.xlsm| Powerpoint Files(*.pptx, *.pptm)|*.pptx;*.pptm”.

On the OnClick event handler of the button, add the following code:

procedure TMainFrm.Button1Click(Sender: TObject);
var
  ZipStream: TStream;
  XmlNode: IXMLNode;
  i: Integer;
  AttType: String;
  ZipFile: TZipFile;
  LocalHeader: TZipHeader;
begin
  if OpenDialog1.Execute then begin
    ZipFile := TZipFile.Create();
    try
      ZipFile.Open(OpenDialog1.FileName, TZipMode.zmRead);
      try
        ZipFile.Read('_rels/.rels', ZipStream, LocalHeader);
        ZipStream.Position := 0;
        XMLDocument1.LoadFromStream(ZipStream);
        Memo1.Text := XMLDoc.FormatXMLData(XMLDocument1.XML.Text);
      finally
        ZipStream.Free;
      end;
    finally
      ZipFile.Close();
      ZipFile.Free;
    end;
  end;
end;

If the user chooses a file, we open the file with the TZipFile and extract the .rels file to a stream and load the lines of the memo with this stream formatted with the FormatXMLData. The following figure shows the result of this operation:

Once we have the .rels file, we must read it and interpret the relations. We could use the functions to read text files and interpret the document, but this is not the best way to do this operation. The ideal is to use a component to read XML files, like the TXMLDocument component that comes with Delphi.

Put two TXMLDocument components and a TValueListEditor on the form. Modify the TileCaptions property of the TValueListEditor to Property/Value. On the OnClick handler of the button modify the code to this one:

procedure TMainFrm.Button1Click(Sender: TObject);
var
  ZipStream: TStream;
  XmlNode: IXMLNode;
  i: Integer;
  AttType: String;
  ZipFile: TZipFile;
  LocalHeader: TZipHeader;
begin
  if OpenDialog1.Execute then begin
    ZipFile := TZipFile.Create();
    try
      ZipFile.Open(OpenDialog1.FileName, TZipMode.zmRead);
      ZipFile.Read('_rels/.rels', ZipStream, LocalHeader);
      try
        ZipStream.Position := 0;
        XMLDocument1.LoadFromStream(ZipStream);
        Memo1.Text := XMLDoc.FormatXMLData(XMLDocument1.XML.Text);
        ValueListEditor1.Strings.Clear;
        for i := 0 to XMLDocument1.DocumentElement.ChildNodes.Count - 1 do begin
          XmlNode := XMLDocument1.DocumentElement.ChildNodes.Nodes[i];
          AttType := ExtractFileName(XmlNode.Attributes['Type']);
          if AttType.EndsWith('core-properties') or
             AttType.EndsWith('extended-properties') then
            ReadProperties(ZipFile, XmlNode.Attributes['Target']);
        end;
      finally
        ZipStream.Free;
      end;
    finally
      ZipFile.Close();
      ZipFile.Free;
    end;
  end;
end;

We load the stream in XMLDocument1 and process the nodes, to find the ones with the types we want (core-properties or extended-properties). When we find them, we pass the name of the file (which is in the Target attribute) to the ReadProperties function, which will read the property file and add them to the ValueListEditor. The ReadProperties  function is:

procedure TMainFrm.ReadProperties(ZipFile: TZipFile; const FileName: String);
var
  ZipStream: TStream;
  i: Integer;
  XmlNode: IXMLNode;
  LocalHeader: TZipHeader;
begin
  ZipFile.Read(FileName, ZipStream, LocalHeader);
  try
    ZipStream.Position := 0;
    XMLDocument2.LoadFromStream(ZipStream);
    for i := 0 to XMLDocument2.DocumentElement.ChildNodes.Count - 1 do begin
      XmlNode := XMLDocument2.DocumentElement.ChildNodes.Nodes[i];
      try
        ValueListEditor1.InsertRow(XmlNode.NodeName, XmlNode.NodeValue, True);
      except
        On EXMLDocError do;
        On EVariantTypeCastError do
          ValueListEditor1.InsertRow(XmlNode.NodeName, '', True);
      end;
    end;
  finally
    ZipStream.Free;
  end;
end;

 

This function is similar with the previous one. We will read the properties file in the second TXMLDcoument and insert a line in the ValueListEditor for each property found. We treat here two types of exceptions: EXMLDocError, which can be raised when the type of the information is not a single type, like a string or an integer and EVariantTypeCastError, which happens when the value is null. This way, we add the properties on the list, like in the next figure:

As we can see, the access to the data of an OpenXml file is relatively simple and can be made using components available in Delphi, but this isn’t everything that can be done: as we are working with zip and xml files, using open technology, we can also modify the files, using the same techniques. In the next section, we’ll see how to create a file from our data.

Creating an OpenXml file

To create an OpenXml file, we need to create some files that will be added to the package. The package should contain at least three files:

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

It’s not necessary to create a folder structure like the one created by Word, we just need to point the location of the files in the .rels file. When adding new functionalities, like images, headers, themes and styles, we must add new files to add these parts to the document. Initially, we will create a simple file, to show the file generation process and then, we will show how to create a more complex file.

Create a new project and place a Label, a Memo and a button on the Form. Change the Caption property of the Label to Text :, the Caption property of the Button to Create and clear the Lines property of the Memo.

Place an XMLDocument component. In the button’s OnClick event, place the following code:

procedure TMainFrm.Button1Click(Sender: TObject);
var
  zipFile: TZipFile;
  contentTypes: TStream;
  rels: TStream;
  doc: TStream;
begin
  zipFile := TZipFile.Create();
  try
    zipFile.Open('SimpleFile.docx', TZipMode.zmWrite);
    contentTypes := CreateContentTypes();
    try
      zipFile.Add(contentTypes, '[Content_Types].xml');
    finally
      contentTypes.Free;
    end;
    rels := CreateRels();
    try
      zipFile.Add(rels, '_rels\.rels');
    finally
      rels.Free;
    end;
    doc := CreateDoc();
    try
      zipFile.Add(doc, 'word\document.xml');
    finally
      doc.Free;
    end;
  finally
    zipFile.Close();
    zipFile.Free;
  end;
end;

The program will create the necessary files, add the streams to the zip file and create a file with the name SimpleFile.docx. The function that creates the file [Content_Types.xml] is:

function TMainFrm.CreateContentTypes(): TStream;
var
  Root: IXmlNode;
  Type: IXmlNode;
  XMLDoc: IXmlDocument;
begin
  Result := TMemoryStream.Create();
  XMLDoc := CriaXml;
  Root := XMLDoc.addChild('Types',
    'http://schemas.openxmlformats.org/package/2006/content-types');
  Type := Root.addChild('Default');
  Type.Attributes['Extension'] := 'rels';
  Type.Attributes['ContentType'] :=
    'application/vnd.openxmlformats-package.relationships+xml';
  Type := Root.addChild('Default');
  Type.Attributes['Extension'] := 'xml';
  Type.Attributes['ContentType'] :=
    'application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml';
  XMLDoc.SaveToStream(Result);
  Result.Position := 0;
end;

The function that create the relations file is:

function TMainFrm.CreateRels(): TStream;
var
  Root: IXmlNode;
  Rel: IXmlNode;
  XMLDoc: IXmlDocument;
begin
  Result := TMemoryStream.Create();
  XMLDoc := CriaXml;
  Root := XMLDoc.addChild('Relationships',
    'http://schemas.openxmlformats.org/package/2006/relationships');
  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';
  XMLDoc.SaveToStream(Result);
  Result.Position := 0;
end;

The code to write the document with the text entered in the Memo is:

function TMainFrm.CreateDoc(): TStream;
var
  Root: IXmlNode;
  XMLDoc: IXmlDocument;
begin
  Result := TMemoryStream.Create();
  XMLDoc := CriaXml;
  Root := XMLDoc.addChild('wordDocument',
    'http://schemas.openxmlformats.org/wordprocessingml/2006/main');
  Root.addChild('body').addChild('p').addChild('r').addChild('t').NodeValue :=
    Memo1.Text;
  XMLDoc.SaveToStream(Result);
  Result.Position := 0;
end;

Here we just need to write a node inside the wordDocument root node: it is the body of the document, which has a paragraph (node p), a “run” (node r) and the text, which is the content of the Memo. When compiling and running the program, we can type some text in the Memo and click on the Create button. The docx file is created with the typed text.

Putting more information in the file

Once we know how to create our files, we can add more information to what is being created. We will now create an example that shows all the fonts available in the system. This document will be generated in landscape format, and we will put a header with three columns and the page number.

Create a new project and place a button and an XmlDocument Change the Caption property of the button to Create. In the button’s OnClick event, put:

procedure TMainFrm.Button1Click(Sender: TObject);
var
  ZipFile: TZipFile;
  MemStream: TMemoryStream;
begin
  ZipFile := TZipFile.Create();
  try
    ZipFile.Open('ComplexFile.docx', TZipMode.zmWrite);
    MemStream := TMemoryStream.Create();
    try
      CreateContentTypes(MemStream);
      ZipFile.Add(MemStream, '[Content_Types].xml');
      MemStream.Clear;
      CreateRels(MemStream);
      ZipFile.Add(MemStream, '_rels\.rels');
      MemStream.Clear;
      CreateDoc(MemStream);
      ZipFile.Add(MemStream, 'word\document.xml');
    finally
      MemStream.Free;
    end;
  finally
    ZipFile.Close();
    ZipFile.Free;
  end;
end;

The functions CreateRels and CreateContentTypes are the same as the previous routine. The function CreateDocument is the following:

procedure TMainFrm.CreateDocument(AStream: TStream);
var
  Root, Body, PgSz: IXMLNode;
  i: Integer;
  SectPr: IXMLNode;
  Header: IXMLNode;
begin
  LimpaXML;
  CreateHeader;
  Root := XMLDocument1.addChild('w:wordDocument');
  Root.DeclareNamespace('w',
    'http://schemas.openxmlformats.org/wordprocessingml/2006/main');
  Body := Root.addChild('w:body');
  for i := 0 to Screen.Fonts.Count - 1 do
    AddFont(Body, Screen.Fonts[i]);
  
  XMLDocument1.SaveToStream(AStream);
  AStream.Position := 0;
end;

We will loop for the system fonts, calling the function AddFont, which will add the formatted text in the Document.xml file:

procedure TMainFrm.AddFont(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;

For each font in the system, we add a paragraph and, in it, a Run. The Run must be formatted with the rPr element, placing the rFonts element and the font name as values of the ascii, hAnsi and cs attributes as children. We also changed the font size by adding the sz element. Then, we put the name of the font as text, adding the tab element to generate a tab and sample text. When running the program, we see that the list of fonts is generated in the document.

The next step is to have the document placed in landscape. To do this, we must add a sectPr element (section properties) to the end of the document, which indicates the formatting of the section. Place the following code at the end of CreateDocument, before the XMLDocument1.SaveToStream (AStream) line:

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;

In this code we add the element pgSz (Page size), giving the attributes w and h for the width and height of the page. These measurements are in twips (1/1440 of an inch), so we convert the page size from A4 to twips. Then, we put the pgMar element (Page margins), which determines the page margins and the position of the header and footer. When we run the program and open the document, we see that it is in landscape.

The last step is to place the header. We put the header in a separate file and, therefore, we must change all references so that this new document can be read.

Initially, we created a reference to the header in the section, as a child of sectPr. Place the following code in CreateDocument, after the line SectPr: = Body.AddChild (‘sectPr’):

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

To use the references, we must add a new namespace to the document. This is done by adding the following line after declaring the namespace in CreateDocument:

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

We created a reference rId1 in the document. We must then create a function that creates this relationship in the file word\_rels\document.xml.rels:

procedure TMainFrm.CreateDocRels(AStream: TStream);
var
  Root: IXMLNode;
  Rel: IXMLNode;
begin
  CleanXML;
  CreateHeader;
  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';
  XMLDocument1.SaveToStream(AStream);
  AStream.Position := 0;
end;

This function is similar to the one that creates the package relationship. The function that creates the header in the file  header1.xml is:

procedure TMainFrm.CreateHeader(AStream: TStream);
var
  Root, Header, PTab: IXMLNode;
begin
  CleanXML;
  CriaCabecalho;
  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';
  XMLDocument1.SaveToStream(AStream);
  AStream.Position := 0;
end;

Here we create the header with text left-aligned, a tab to align the centered text and another tab to align the page number to the right.

The page number is given by the fldSimple element, using the instr attribute with the value PAGE\*MERGEFORMAT. After creating these functions, we must place the code to call them, at the end of the button’s OnClick event:

MemStream.Clear;
CreateDocRels(MemStream);
ZipFile.Add(MemStream, 'word\_rels\document.xml.rels');
MemStream.Clear;
CreateHeader(MemStream);
ZipFile.Add(MemStream, 'word\header1.xml');

Now, we need only to make a small change in [Content_Types].xml, adding the Override element, to show the type of the header1.xml file. Put the following code in the  CreateContentTypes, before the line XMLDocument1.SaveToStream(AStream):

Tipo := Root.addChild('Override');
Tipo.Attributes['PartName'] := '/word/header1.xml';
Tipo.Attributes['ContentType'] :=
  'application/vnd.openxmlformats-officedocument.wordprocessingml.header+xml';

With that, our program is ready. When executing it, we generate a document similar to the one shown in the following figure:

Conclusions

The OpenXML format has great advantages for those who want to process and open Office files. As this format uses open technologies and is fully documented, we can access, change or even create Office files using any development tools (or even changing files manually), on any platform or language.

No proprietary APIs or special programs are required, which allows the information to be available to anyone who wants to access it. We show here how to manipulate Delphi files, noting that we use only standard Delphi components, using only zip and XML files.

The source code for this project is in https://github.com/bsonnino/OpenXmlDelphiEng

 

In the last post I’ve shown how to use the new WebView2 control in a WPF app and said that it could be used in any Windows version and in any platform. As a matter of fact, I can create a 64 bit VCL native application with Delphi, that uses the Win32 API with the same control, offering the same functionality that the previous app did. To do that, you must have Delphi 10.4 Sydney installed in your machine. It offers the TEdgeBrowser component, that can be used to browse the web using the Chromium component.

In Delphi, create a new VCL application. Add the 64 bit platform to it and set it default. Add a TPanel, docked at the top. Then add a TEdgeBrowser and doc it to fill the window. In the panel, add a TLabel and change the Caption property to Search Text, add a TEdit and clear its Text property. Then, add two buttons and set their property Caption to Find and Copy. Add two more buttons at the right of the panel and set their sizes to 23×23, and their font Height to 10, Name to Segoe MDL2 Assets and their caption to the Back and Forward icons in the Character Map:

You should have something like this:

Now you can set the OnCreate event of the main form to

EdgeBrowser1.Navigate('https://docs.microsoft.com');

and run the application, but you won’t see nothing in the window. You can check the initialization of the WebViewer in the OnCreatedWebViewCompleted event and check the AResult parameter.

procedure TForm1.EdgeBrowser1CreateWebViewCompleted(Sender: TCustomEdgeBrowser;
  AResult: HRESULT);
begin
  if AResult <> 0 then
    ShowMessage('Error initializing WebView: $'+IntToHex(AResult));
end;

You will see that there is an error code (in my machine it’s $80004005). This is due to the fact that the WebView dll is missing. The dll is in the Redist folder and you have to add a PostBuild command to the project. Go to Project/Options and select Build options and, in the command “Value from All Configurations – Windows 64 bits”, add the following command:

copy /Y "C:\Program Files (x86)\Embarcadero\Studio\21.0\Redist\win64\WebView2Loader.dll" $(OUTPUTDIR)

This will copy the dll to the output directory and will initialize the WebView correctly(you will see that there is no message in the OnCreatedWebViewCompleted event and the page is loaded. Now, we can add the event handlers for the buttons. The Find button will set up the address and call the Navigate method (You must add the NetEncoding unit to the Uses clause):

procedure TForm1.FindClick(Sender: TObject);
begin
  if Edit1.Text <> '' then begin
    var address := 'https://docs.microsoft.com/en-us/search/?terms=' +
      TNetEncoding.URL.Encode(Edit1.Text);
    EdgeBrowser1.Navigate(address);
  end;
end;

The Back and Forward buttons events are very simple to implement:

procedure TForm1.BackClick(Sender: TObject);
begin
  if EdgeBrowser1.CanGoBack then
    EdgeBrowser1.GoBack;
end;

procedure TForm1.ForwardClick(Sender: TObject);
begin
  if EdgeBrowser1.CanGoForward then
    EdgeBrowser1.GoForward;
end;

To remove the parts of the page that we don’t want, we use the OnNavigationCompleted event to inject the JavaScript code:

procedure TForm1.EdgeBrowser1NavigationCompleted(Sender: TCustomEdgeBrowser;
  IsSuccess: Boolean; WebErrorStatus: TOleEnum);
begin
  if IsSuccess then
    EdgeBrowser1.ExecuteScript(
' var rss = document.querySelector(''[data-bi-name="search-rss-link"]'');'+ #13#10 +
' console.log(rss);'+ #13#10 +
' if (rss)'+ #13#10 +
'   rss.style.display = "none";'+ #13#10 +
' var form = document.getElementById("facet-search-form");'+ #13#10 +
' console.log(form);'+ #13#10 +
' if (form)'+ #13#10 +
'   form.style.display = "none";'+ #13#10 +
' var container = document.getElementById("left-container");'+ #13#10 +
' console.log(container);'+ #13#10 +
' if (container)'+ #13#10 +
'   container.style.display = "none";'+ #13#10 +
' var hiddenClasses = ["header-holder", "footerContainer"];'+ #13#10 +
' var divs = document.getElementsByTagName("div");'+ #13#10 +
' for( var i = 0; i < divs.length; i++) {'+ #13#10 +
'   if (hiddenClasses.some(r=> divs[i].classList.contains(r))){'+ #13#10 +
'     divs[i].style.display = "none";'+ #13#10 +
'   }'+ #13#10 +
' }');
end;

You can see here the same JavaScript code we used in the previous post, it also logs to the console the value of the variables. You can also open the Developer Tools of the browser component by pressing F12.

Now, there is only the Copy button to get the results and copy them to the clipboard. We will inject the code that gets all the results and send them to the application:

procedure TForm1.CopyClick(Sender: TObject);
begin
EdgeBrowser1.ExecuteScript(
'var results = [...document.querySelectorAll(''[data-bi-name="result"]'')]'+
'.map(a => {'+ #13#10 +
' let aElement = a.querySelector("a");'+ #13#10 +
' return {'+ #13#10 +
' title: aElement.innerText,'+ #13#10 +
' link: aElement.getAttribute("href")'+ #13#10 +
' };'+ #13#10 +
'});'+ #13#10 +
'console.log(results);'+ #13#10 +
'if (results.length >= 1){'+ #13#10 +
' window.chrome.webview.postMessage(results);'+ #13#10 +
'}'+ #13#10 +
'else {'+ #13#10 +
' alert("There are no results in the page");'+ #13#10 +
'}');
end;

This is the same code that is injected in the WPF app, with a slight difference: in the WPF program we’ve sent a message and added a listener in the JavaScript code. Here, we are running the code when the user clicks the button. This will send a message to the app, that will be processed in the OnMessageReceived event:

procedure TForm1.EdgeBrowser1WebMessageReceived(Sender: TCustomEdgeBrowser;
  Args: TWebMessageReceivedEventArgs);
var
  json : PWideChar;
begin
  var msg := Args as ICoreWebView2WebMessageReceivedEventArgs;
  msg.Get_webMessageAsJson(json);
  Clipboard.AsText := json;
  ShowMessage('Results sent to clipboard');
end

Now, when you run the program, you will have the same results as in the WPF program:

As you can see, with the new WebView2 component you have a lot of flexibility, you can use it in .NET or Win32 programs with almost no change. You can use this component to browse the Web and get data from the browsing, or you can use it to complement your current app: let’s say you have parts of your app that are already written for the web and you don’t want to rewrite them, but use them to interact with your app, you can add these parts and include them in your desktop app.

The full source code for this project is at https://github.com/bsonnino/WebViewDelphi

 

 

In a previous post, I’ve shown how to publish your Delphi app to the Windows Store. If it is a paid app, you can start getting money with it as soon as it starts to sell. But sometimes, you don’t want to make it a paid app, but you still want to earn money with it.

One way to do it is to do the same that many games do: to use In-app purchases. You can offer a free basic version and put some paid add-ins in the app, so the user must pay to get the premium features. Even if it’s a paid app, you can also give the user a trial period, so, the user can use the premium version for some time and then, if he doesn’t want to pay for the app, he can still use the basic features.

Delphi 10.3 introduced a new component, TWindowsStore, that allows you to control the store features (add-ins, trial version) and enable or disable features depending on what the user pays. In this article, I will show how to use the TWindowsStore component to control the features and set the In-app purchases.

We will use the same Financial Calculator we’ve used in the previous article, but we’ll do some changes:

  • The basic calculator will be able to calculate only the Present Value of the investment. The other calculators won’t be available.
  • The user will have a trial period where all the calculators are available. Once the trial period is expired, the app will revert to the basic version
  • The user will be able to buy calculators as add-in purchases. He won’t need to buy all, he will be able to buy just the ones he needs as add-in purchases.

Developing the trial and in-app purchases

The first step in the development of the trial version is to add a TWindowsStore component in the main window of the calculator. Then, we will change the app to show the basic features for the app. To do this, change the PageIndex of the calculators:

  • Present Value – 0
  • Future Value – 1
  • Payments – 2
  • Return Rate – 3

Put a panel with a button over each calculator, except the first one, with this text: “To open this calculator, click on the button”. Change the ParentBackground property to false. Add another TabSheet, with a label with a caption with text “Trial version – % days remaining”. If you run the app, you should have something like this:

Now, let’s program the trial version. When the user is in trial period, he will be able to use all calculators. To do that, we’ll do something like this:

procedure TForm1.CheckIfTrial;
begin
  if WindowsStore1.AppLicense.IsActive then begin
    if WindowsStore1.AppLicense.IsTrial then begin
      var RemainingDays := WindowsStore1.AppLicense.TrialTimeRemaining.Days;
      Label21.Caption := Format(Label21.Caption, [RemainingDays]);
      EnableFullVersion;
    end
  end
  else begin
    CheckBoughtCalculators;
  end;
end;

The CheckIfTrial pocedure will be called in the OnCreate handler of the form, thus setting the UI accordingly at start. The EnableFullVersion procedure will hide all trial panels:

procedure TForm1.EnableFullVersion;
begin
  Panel1.Visible := False;
  Panel2.Visible := False;
  Panel3.Visible := False;
end;

The CheckBoughtCalculators will only hide the panels for the add-ons that had been bought:

procedure TForm1.CheckBoughtCalculators;
begin
  Panel1.Visible := not WindowsStore1.UserHasBought('FutureCalc');
  Panel2.Visible := not WindowsStore1.UserHasBought('PaymentCalc');
  Panel3.Visible := not WindowsStore1.UserHasBought('RateCalc');
end;

The code to buy the add-ons is:

function TForm1.PurchaseItem(Item: string) : string;
begin
  LogMessage('Will purchase item: ' +Item);
  for var i := 0 to WindowsStore1.AppProducts.Count - 1 do
    if TWindowsString.HStringToString(WindowsStore1.AppProducts[i].InAppOfferToken) = Item then begin
      BuyProduct(WindowsStore1.AppProducts[i]);
      exit;
    end;
  LogMessage('Item not found: ' +Item);
end;

procedure TForm1.BuyProduct(Product: IStoreProduct);
begin
  try
    var status := WindowsStore1.PurchaseProduct(Product);
    LogMessage('Got status: '+Integer(status).ToString);
    if status = StorePurchaseStatus.Succeeded then begin
      LogMessage('Item ' +TWindowsString.HStringToString(Product.Title)+' bought');
      CheckBoughtCalculators();
    end
    else begin
      ShowMessage('Item could not be purchased. Error: '+Integer(status).ToString);
    end;
  except
    On e : Exception do
      LogMessage('Exception while buying item.'+Chr(13)+
        E.ClassName+', with message : '+E.Message);
  end;
end;

The code for the buttons is:

procedure TForm1.Button1Click(Sender: TObject);
begin
  PurchaseItem('FutureCalc');
end;

While developing the app, I found that this code generates an exception, there is something in the code that doesn’t like the Delphi Window handle and crashes the app when the code is run. Not a good experience for a paid app. So, I searched the web for an alternative and found this article that shows how to create a dll in C# that can be used in Delphi, to allow buying add-in purchases in the Windows Store. If you follow the instructions there and compile the dll, you will have the IAPWrapper.dll that can be used in our Delphi program.

This dll exposes a function, Purchase, that makes the purchase of an add-in. It’s different than the TWindowsStore’s PurchaseProduct, because it receives the item id as a parameter and not the item itself. That way, we must add the dll to the project, declare the Purchase function and change the BuyProduct method:

function Purchase(StoreId : PAnsiChar) : PAnsiChar; stdcall; external 'IAPWrapper.dll';

procedure TForm1.BuyProduct(Product: IStoreProduct);
begin
  try
    var status := Purchase(PAnsiChar(AnsiString(TWindowsString.HStringToString(Product.StoreId))));
    LogMessage('Got status: '+status);
    if status = 'Succeeded' then begin
      LogMessage('Item ' +TWindowsString.HStringToString(Product.Title)+' bought');
      CheckBoughtCalculators();
    end
    else begin
      ShowMessage('Item could not be purchased. Error: '+Integer(status).ToString);
    end;
  except
    On e : Exception do
      LogMessage('Exception while buying item.'+Chr(13)+
        E.ClassName+', with message : '+E.Message);
  end;
end;

With this code, you can put your app in the store and monetize with it. When the user wants a calculator, he can buy the item and it will be available forever. You could also make it consumable, so it can be bought again at anytime, but for our purposes, that’s fine.

Creating the add-ins in the store

Once you have the program developed, you must create your add-ins in the store. Go to the Windows Development Center, select the app you have developed for the store and click on the “Create a new add-on” button.

There you can create many types of add-ons:

  • Developer-managed consumable – this kind of add-on is managed by your app, like some kind of strength. If this consumable is used on your app, the user can buy it again.
  • Store-managed consumable – this kind of add-on is managed by the store, like some kind of gun. You can query the store to know how many guns the user has bought
  • Durable – this kind of add-on is bought once and it is available until its lifetime
  • Subscription – this kind of add-on needs periodic payment to keep using it

Our add-ons will all be durable, once the user buys a calculator, he won’t need to buy it again. Create a new durable add-on and name it FutureCalc. Then start the submission for it:

Set the properties for the add-on, with a product lifetime Forever and Content type as Software as service:

Then, set the price and availability of the add-on:

We won’t change anything here – we will leave the add-in for free, so you just need to click the Save button.

The next step is to add the store listings. Add a new language and select English (United States) and edit it:

Set the title and description and click on the Save button. When you have everything set, just click on the Submit to the store to submit it. Do the same thing with the PaymentCalc and RateCalc add-ons.

The next step is to create a new flight for the package. Just change the version to 1.1 and compile the app for the store (don’t forget to change the Provisioning data to Store). Then, create a new flight for the submission and send the package there. It will be available to download as soon as it passes certification. It will show that it has in-app purchases:

Once you’ve installed it, it will show you the new version, and you can unlock the calculator you want:

 

Conclusions

As you can see you can send your Delphi apps to the Windows store and get some money with them The TWindowsStore component allows you to interact with the add-ins you’ve set in the store, so you can earn money by creating a paid version or even by adding purchases in the app. While developing the app, I’ve shown that you can also create a dll in C# that interacts with your Delphi program in the same way that a Win32 dll does. You can use this dll in the store to buy add-ins and monetize your app.

The full source code for this article is in https://github.com/bsonnino/FinCalcAddIn

 

 

In a previous post, I’ve shown how to package a Delphi application for the Windows Store, and in this post I’ve shown how to package a WPF application. In both cases, the apps were packaged alone or with their references, so there was no trouble to package them to the store.

In the last post, I’ve refactored the Financial Calculator into a main app and a dll, so the code for the calculators could be reusable and testable. Now we have to package the apps for the store again, but this time there will be an extra task: to package the dll together with the app. In this article, I’ll show how to do that for both applications, the Delphi and the WPF one.

Packaging a Delphi app

To package the Delphi app, we just need to go to the project, select the Application Store configuration, then go to Project/Options/Provisioning and select the Ad Hoc deployment and add the certificate we created earlier. Then, we run the packaging and install the appx file. When you install and run the app, you will see a message like this:

That’s because the dll wasn’t packaged with the executable. If you rename the appx file to zip and open it with a zip manager (yes, an appx file is a zip file with another extension), you will see something like this:

As you can see, the appx file is a zip file with all the content needed to run the app. If you take a look at it, you will see that the dll isn’t there, that’s why you get the message. There is nothing that says that the dll should be added to the package, there are no references and the load of the dll is only done at runtime, so the packager doesn’t knows the dll is needed. We must do some things, so the dll is packaged with the app.

Go to the Projects window and right click on the FinCalc.exe node and select the Add option. Then add the FinCalcDll.dll to the project:

Now, when you rebuild the app, you will see that FinCalcDll.dll was added to the appx file and it will be used when you install it from the store:

There is something to note, here: this setting won’t work for the normal app. If you want to run the app as a normal app, you must continue to xcopy the dll to the output directory, this setting won’t copy it to the output directory. So, to be safe, do both things:

  • Add the dll to the project
  • Xcopy the dll to the output dir

Packaging a WPF app

Now that we’ve packaged the Delphi app with the dll, we must package the WPF app. To create a package to a WPF app, we must create a packaging project in Visual Studio:

Once the new project is created, in the Solution Explorer, you must right-click in the Applications node and select Add Reference. Then you must add the WPF project to the package. Then, right click on the project node and select Store/Create App Packages. There you can select if you want to create packages for the Store or for Sideloading:

If you want to create an app for the store, you must have a developer account and associate a name for the app in the store. We will choose to sideload the app:

Here we must note some things: as our dll is a 32 bit one, we must choose the x86 package only. We don’t need and app bundle, where all the platforms are packaged in a single file – we will use the “Generate app bundle” to Never, then click Create. That will create the appx package and show the folder where it was created. Opening the folder, you will find some files to install the app, including the appx file.

If you double click on it and click on Install, you will see something like this (just for the first time):

That’s because the certificate you’ve used is not installed in the machine. To install it, you can open the Package.appxmanifest file in Visual Studio and go to the Packaging tab:

Then, click on the Choose Certificate button and then on View Full Certificate. That will open the install certificate window:

You must click on the Install Certificate button and select Local Machine as the store location. Then click on Next and then in Place all certificates in the following store, clicking on Browse and selecting Trusted People. The certificate will be installed and you can click again in the Appx file to install it.

This app will run without problems, because the dll will be added to the package. Adding the dll in the original project as content will make it to be packaged for the store, there’s nothing else to do.

Conclusions

As you can see, packaging an app with a dll for the Windows Store is not too difficult, but you must be aware of these things, so the app doesn’t fail when running, for the lack of the dll.

All the source code for these projects are at https://github.com/bsonnino/FinCalcDll

 

While writing my last article, something occurred to me: what if the app uses an external dll for some functions, how can I package them to send them to the store. Once you are sending an app to the Windows Store, everything it needs to start and run must be packaged, or it won’t be certified.

When using an installer to package an app, all you must do is to include the main executable and all the needed files in the install script and the installer will take care of packaging everything. But when your are packaging an app for the Windows Store, there is no such thing as an install script. If you are packaging a Delphi app, just compile it to the Store and voilà!, an appx file is created. With Visual Studio, you can create a packaging project, add all the projects in the solution that you want and it will create an appx file for you.

But sometimes, you need to use an external dll, which you may not have the source code, and it must be packaged with the main executable. In this article, I will show how to package an external dll with the main executable with Delphi and with Visual Studio.

For the article, we will take on the Financial Calculator that we created for the last article and refactor it: the financial functions will be in an external Win32 dll and the UI will be in the main executable. That way, we will do two things:

  • Separate the UI and the business rules
  • Create an external component that may be used in many situations: we will use it for our two apps – the Delphi and the WPF one. That is a great way to refactor your code when you have stable business rules that you don’t want to touch and you need to evolve the UI of your app

Refactoring the Delphi UI

As you can see from this code, the business rules are mixed with the UI, thus making it difficult to understand it and change the code, if it’s needed.

procedure TForm1.CalculatePV;
begin
  try
    var FutureValue := StrToFloat(FvPresentValue.Text);
    var InterestRate := StrToFloat(IrPresentValue.Text) / 100.0;
    var NumPeriods := StrToInt(NpPresentValue.Text);
    var PresentValue := FutureValue / Power((1 + InterestRate), NumPeriods);
    PvPresentValue.Text := FormatFloat('0.00', PresentValue);
  except
    On EConvertError do
      PvPresentValue.Text := '';
  end;
end;

For example, all the values are dependent on the text box values and the result is also posted in the result text box. This is bad design and not testable. A better thing would be something like this:

function TForm1.CalculatePV(FutureValue, InterestRate : Double; NumPeriods : Integer); double;
begin
  try
    Result := FutureValue / Power((1 + InterestRate), NumPeriods);
  except
    Result := NAN;
  end;
end;

This is cleaner, does not depend on the UI and easier to understand. But it is not testable, yet, because the method is in the code behind for the UI, so to test it you should need to instantiate a new Form1, which is not feasible under automated tests (unless you are doing UI tests, which is not the case). You could move this code to another unit, to allow it to be testable, but it won’t be reusable. If you want to use the same code in another program, you should have to copy the unit, with all the problems you may have with that:

  • Difficulty to change the code: if you find an error or want to refactor the code, you should fix the same thing in many places
  • Impossible to use in programs written in other languages, unless you rewrite the code

The best way in this case is to move the code to an external dll. That way, the code will be both testable and reusable: you can even use the same dll in programs written in other languages with no change.

The first step is to create a new project in the project group, a dynamic library. Save it and call it FinCalcDll. Then add a new unit to it and save it as PVCalculator. You should be asking why am I saving the unit with this name and not as FinancialCalculators. I am doing this because I want to treat each unit as a single class and respect the Single Responsibility Principle. Following that principle, the class should have only one reason to change. If I put all calculators in a single unit (class), there will be more than one reason to change it: any change in any of the calculators will be a reason to change. Then, we can add the first function:

unit PVCalculator;

interface

Uses Math;

function CalculatePV(FutureValue, InterestRate: Double; NumPeriods : Integer): double; stdcall;

implementation

function CalculatePV(FutureValue, InterestRate: Double; NumPeriods : Integer):
  double;
begin
  try
    Result := FutureValue / Power((1 + InterestRate), NumPeriods);
  except
    Result := NAN;
  end;
end;

end.

We must use the Math unit, to have the Power function available and declare the function in the Interface section, so it can be visible externally. It must be declared as stdcall to be called by other languages. Create new units and save them as FVCalculator, IRRCalculator and PmtCalculator and add these functions:

unit PVCalculator;

interface

Uses Math;

function CalculatePV(FutureValue, InterestRate: Double; NumPeriods : Integer):
  double; stdcall;

implementation

function CalculatePV(FutureValue, InterestRate: Double; NumPeriods : Integer):
  double;
begin
  try
    Result := FutureValue / Power((1 + InterestRate), NumPeriods);
  except
    Result := NAN;
  end;
end;

end.
unit FVCalculator;

interface

Uses Math;

function CalculateFV(PresentValue, InterestRate: Double;NumPeriods : Integer):
  Double; stdcall;

implementation

function CalculateFV(PresentValue, InterestRate: Double;NumPeriods : Integer):
  Double;
begin
  try
    Result := PresentValue * Power((1 + InterestRate), NumPeriods);
  except
    Result := NAN;
  end;
end;

end.
unit IRRCalculator;

interface

Uses Math;

function CalculateIRR(PresentValue, Payment: Double;NumPeriods : Integer):
  Double; stdcall;

implementation

function CalculateIRR(PresentValue, Payment: Double;NumPeriods : Integer):
  Double;
begin
  Result := Nan;
  try
    var FoundRate := False;
    var MinRate := 0.0;
    var MaxRate := 1.0;
    if Payment * NumPeriods < PresentValue then begin
      Result := -1;
      exit;
    end;
    if Payment * NumPeriods = PresentValue then begin
      Result := 0;
      exit;
    end;
    while not FoundRate do begin
      var Rate := (MaxRate + MinRate) / 2.0;
      var SumPayments := 0.0;
      for var I := 1 to NumPeriods do
        SumPayments := SumPayments + Payment / Power((1 + Rate), I);
      if Abs(SumPayments - PresentValue) > 0.01 then begin
        if PresentValue < SumPayments then begin
          MinRate := Rate;
        end
        else begin
          MaxRate := Rate;
        end;
      end
      else begin
        FoundRate := True;
        Result := Rate;
      end;
    end;
  except
  end;
end;

end.<span id="mce_marker" data-mce-type="bookmark" data-mce-fragment="1">​</span>
unit PmtCalculator;

interface

Uses Math;

function CalculatePmt(PresentValue, InterestRate: Double;NumPeriods : Integer):
  Double; stdcall;

implementation

function CalculatePmt(PresentValue, InterestRate: Double;NumPeriods : Integer):
  Double;
begin
  try
    Result := (PresentValue * InterestRate) * Power((1 + InterestRate),
      NumPeriods) / (Power((1 + InterestRate), NumPeriods) - 1);
  except
    Result := Nan;
  end;
end;

end.

In the dpr file, you must export the functions. In the Projects window, select the dll and right-click on it, selecting the View Source option. In the source for the dpr file, add the Exports clause:

{$R *.res}
Exports
  CalculatePV, CalculateFV, CalculateIRR, CalculatePmt;

Then, in the Unit1 for the executable, make the changes needed to use the new dll functions:

implementation

{$R *.dfm}
function CalculatePV(FutureValue, InterestRate : Double;NumPeriods : Integer) :
  Double; stdcall; external 'FinCalcDll.dll';

function CalculateFV(PresentValue, InterestRate : Double;NumPeriods : Integer) :
  Double; stdcall; external 'FinCalcDll.dll';

function CalculateIRR(PresentValue, Payment: Double;NumPeriods : Integer) :
  Double; stdcall; external 'FinCalcDll.dll';

function CalculatePmt(PresentValue, InterestRate : Double;NumPeriods : Integer) :
  Double; stdcall; external 'FinCalcDll.dll';

procedure TForm1.PaymentChange(Sender: TObject);
begin
  try
    var PresentValue := StrToFloat(PvPayment.Text);
    var InterestRate := StrToFloat(IRPayment.Text) / 100.0;
    var NumPayments := StrToInt(NpPayment.Text);
    var Payment := CalculatePmt(PresentValue,InterestRate, NumPayments);
    PmtPayment.Text := FormatFloat('0.00', Payment);
  except
    On EConvertError do
      PmtPayment.Text := '';
  end;
end;

procedure TForm1.PresentValueChange(Sender: TObject);
begin
  try
    var FutureValue := StrToFloat(FvPresentValue.Text);
    var InterestRate := StrToFloat(IrPresentValue.Text) / 100.0;
    var NumPeriods := StrToInt(NpPresentValue.Text);
    var PresentValue := CalculatePV(FutureValue, InterestRate, NumPeriods);
    if IsNan(PresentValue) then
      PvPresentValue.Text := ''
    else
      PvPresentValue.Text := FormatFloat('0.00', PresentValue);
  except
    On EConvertError do
      PvPresentValue.Text := '';
  end;
end;

procedure TForm1.IRRChange(Sender: TObject);
begin
  try
    var NumPayments := StrToInt(NpIRR.Text);
    var PresentValue := StrToFloat(PvIRR.Text);
    var Payment := StrToFloat(PmtIRR.Text);
    var Rate := CalculateIRR(PresentValue, Payment, NumPayments);
    if Rate < 0 then begin
      IRIRR.Text := 'Rate Less than 0';
      exit;
    end;
    if IsNan(Rate) then begin
      IRIRR.Text := 'Error calculating rate';
      exit;
    end;
    IRIRR.Text := FormatFloat('0.00', Rate * 100.0);
  except
    On EConvertError do
      IRIRR.Text := '';
  end;
end;

procedure TForm1.FutureValueChange(Sender: TObject);
begin
  try
    var PresentValue := StrToFloat(PvFutureValue.Text);
    var InterestRate := StrToFloat(IrFutureValue.Text) / 100.0;
    var NumPeriods := StrToInt(NpFutureValue.Text);
    var FutureValue := CalculateFV(PresentValue,InterestRate, NumPeriods);
    if IsNan(FutureValue) then
      FvFutureValue.Text := ''
    else
      FvFutureValue.Text := FormatFloat('0.00', FutureValue);
  except
    On EConvertError do
      FvFutureValue.Text := '';
  end;
end;

We declare the functions and use them in the OnChange handlers of the textboxes. When you build and run the program, you will see something like this:

That’s because the dll is not where it should be, in the same folder of the executable. For that, you must take some steps:

  • Build the dll before the executable. If you don’t do that, the executable will be built and will use an outdated dll
  • Copy the dll after building the executable

For the first step, you need to go to the Projects window, select the dll, right click and select the “Build Sooner”  option. That will move the dll up in the project list and will make it to be built before the executable.

For the second step, you need to add a post-build step for the executable and copy the dll to the output dir. For that, you need to select the Project Options and go to Build Events:

There, in the Post-build events, you should add a command to copy the dll to the executable output dir:

One thing must be noted here: you must build the dll and the executable for the same platform. If you build the dll for x64, it won’t run on a x86 executable. Once you’ve done the two steps, you can build all projects and run the executable, it will run the same way as before. Now, we’ve refactored all the business rules into a dll and we can reuse it in other languages. To show that, we will create a WPF project in C# that will use this dll.

Creating a WPF project that uses the DLL

Go to Visual Studio and create a new WPF project, and name it FinCalcWPF. Then go to solution explorer and add the dll file to the project. When adding the dll, select Add as Link, to avoid to make a physical copy of the dll in the source directory. This way, you are just adding a link to the dll and when it’s rebuilt, the new version will be used. In the properties window, select Build Action to None and Copy to Output Directory to Copy if newer.

One thing must be noted here: the dll is for Win32, so the executable should also be for Win32. When you build the WPF app with the default setting (Any CPU), you can’t be sure it if will run as a Win32 process:

  • For a Win32 operating system, it will run as a Win32 process, so that’s ok
  • For a Win64 operating system, it may run as a Win32 or Win64 process, depending on your settings. If you go to Project/Options/Build and check the “Prefer 32-bit”, it will run as a Win32 process, else it will run as a Win64 process

So, if you don’t want any surprises, just change the build from Any CPU to x86 and you will be sure that the program will run with the dll.

Then, in MainWindow.xaml, add this code:

<Window x:Class="FinCalcWpf.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        mc:Ignorable="d"
        Title="Financial Calulator WPF" Height="293.774" Width="419.623">
    <Grid>
        <TabControl>
            <TabItem Header="Present Value">
                <Grid>
                    <Grid.RowDefinitions>
                        <RowDefinition Height="40"/>
                        <RowDefinition Height="40"/>
                        <RowDefinition Height="40"/>
                        <RowDefinition Height="40"/>
                        <RowDefinition Height="40"/>
                    </Grid.RowDefinitions>
                    <Grid.ColumnDefinitions>
                        <ColumnDefinition Width="*"/>
                        <ColumnDefinition Width="2*"/>
                    </Grid.ColumnDefinitions>
                    <TextBlock Grid.Row="0" Grid.Column="0" VerticalAlignment="Center" 
                             Margin="5" Text="Future Value"/>
                    <TextBlock Grid.Row="1" Grid.Column="0" VerticalAlignment="Center" Margin="5" 
                            Text="Interest Rate"/>
                    <TextBlock Grid.Row="2" Grid.Column="0" VerticalAlignment="Center" Margin="5" 
                            Text="Num.Periods"/>
                    <TextBlock Grid.Row="3" Grid.Column="0" VerticalAlignment="Center" Margin="5" 
                            Text="Present Value"/>
                    <TextBox Grid.Row="0" Grid.Column="1" Margin="5" VerticalContentAlignment="Center"
                            x:Name="PvFvBox" TextChanged="PvOnTextChanged"/>
                    <TextBox Grid.Row="1" Grid.Column="1" Margin="5" VerticalContentAlignment="Center"
                             x:Name="PvIrBox" TextChanged="PvOnTextChanged"/>
                    <TextBox Grid.Row="2" Grid.Column="1" Margin="5" VerticalContentAlignment="Center"
                             x:Name="PvNpBox" TextChanged="PvOnTextChanged"/>
                    <TextBox Grid.Row="3" Grid.Column="1" Margin="5" VerticalContentAlignment="Center"
                             x:Name="PvPvBox" IsReadOnly="True"/>
                </Grid>
            </TabItem>
            <TabItem Header="Future Value">
                <Grid>
                    <Grid.RowDefinitions>
                        <RowDefinition Height="40"/>
                        <RowDefinition Height="40"/>
                        <RowDefinition Height="40"/>
                        <RowDefinition Height="40"/>
                        <RowDefinition Height="40"/>
                    </Grid.RowDefinitions>
                    <Grid.ColumnDefinitions>
                        <ColumnDefinition Width="*"/>
                        <ColumnDefinition Width="2*"/>
                    </Grid.ColumnDefinitions>
                    <TextBlock Grid.Row="0" Grid.Column="0" VerticalAlignment="Center" 
                               Margin="5" Text="Present Value"/>
                    <TextBlock Grid.Row="1" Grid.Column="0" VerticalAlignment="Center" Margin="5" 
                               Text="Interest Rate"/>
                    <TextBlock Grid.Row="2" Grid.Column="0" VerticalAlignment="Center" Margin="5" 
                               Text="Num.Periods"/>
                    <TextBlock Grid.Row="3" Grid.Column="0" VerticalAlignment="Center" Margin="5" 
                               Text="Future Value"/>
                    <TextBox Grid.Row="0" Grid.Column="1" Margin="5" VerticalContentAlignment="Center"
                             x:Name="FvPvBox" TextChanged="FvOnTextChanged"/>
                    <TextBox Grid.Row="1" Grid.Column="1" Margin="5" VerticalContentAlignment="Center"
                             x:Name="FvIrBox" TextChanged="FvOnTextChanged"/>
                    <TextBox Grid.Row="2" Grid.Column="1" Margin="5" VerticalContentAlignment="Center"
                             x:Name="FvNpBox" TextChanged="FvOnTextChanged"/>
                    <TextBox Grid.Row="3" Grid.Column="1" Margin="5" VerticalContentAlignment="Center"
                             x:Name="FvFvBox" IsReadOnly="True"/>
                </Grid>
            </TabItem>
            <TabItem Header="Payment">
                <Grid>
                    <Grid.RowDefinitions>
                        <RowDefinition Height="40"/>
                        <RowDefinition Height="40"/>
                        <RowDefinition Height="40"/>
                        <RowDefinition Height="40"/>
                        <RowDefinition Height="40"/>
                    </Grid.RowDefinitions>
                    <Grid.ColumnDefinitions>
                        <ColumnDefinition Width="*"/>
                        <ColumnDefinition Width="2*"/>
                    </Grid.ColumnDefinitions>
                    <TextBlock Grid.Row="0" Grid.Column="0" VerticalAlignment="Center" 
                               Margin="5" Text="Present Value"/>
                    <TextBlock Grid.Row="1" Grid.Column="0" VerticalAlignment="Center" Margin="5" 
                               Text="Interest Rate"/>
                    <TextBlock Grid.Row="2" Grid.Column="0" VerticalAlignment="Center" Margin="5" 
                               Text="Num.Periods"/>
                    <TextBlock Grid.Row="3" Grid.Column="0" VerticalAlignment="Center" Margin="5" 
                               Text="Payment"/>
                    <TextBox Grid.Row="0" Grid.Column="1" Margin="5" VerticalContentAlignment="Center"
                             x:Name="PmtPvBox" TextChanged="PmtOnTextChanged"/>
                    <TextBox Grid.Row="1" Grid.Column="1" Margin="5" VerticalContentAlignment="Center"
                             x:Name="PmtIrBox" TextChanged="PmtOnTextChanged"/>
                    <TextBox Grid.Row="2" Grid.Column="1" Margin="5" VerticalContentAlignment="Center"
                             x:Name="PmtNpBox" TextChanged="PmtOnTextChanged"/>
                    <TextBox Grid.Row="3" Grid.Column="1" Margin="5" VerticalContentAlignment="Center"
                             x:Name="PmtPmtBox" IsReadOnly="True"/>
                </Grid>
            </TabItem>
            <TabItem Header="Return Rate">
                <Grid>
                    <Grid.RowDefinitions>
                        <RowDefinition Height="40"/>
                        <RowDefinition Height="40"/>
                        <RowDefinition Height="40"/>
                        <RowDefinition Height="40"/>
                        <RowDefinition Height="40"/>
                    </Grid.RowDefinitions>
                    <Grid.ColumnDefinitions>
                        <ColumnDefinition Width="*"/>
                        <ColumnDefinition Width="2*"/>
                    </Grid.ColumnDefinitions>
                    <TextBlock Grid.Row="0" Grid.Column="0" VerticalAlignment="Center" 
                               Margin="5" Text="Present Value"/>
                    <TextBlock Grid.Row="1" Grid.Column="0" VerticalAlignment="Center" Margin="5" 
                               Text="Payment"/>
                    <TextBlock Grid.Row="2" Grid.Column="0" VerticalAlignment="Center" Margin="5" 
                               Text="Num.Periods"/>
                    <TextBlock Grid.Row="3" Grid.Column="0" VerticalAlignment="Center" Margin="5" 
                               Text="Return Rate"/>
                    <TextBox Grid.Row="0" Grid.Column="1" Margin="5" VerticalContentAlignment="Center"
                             x:Name="RrPvBox" TextChanged="RrOnTextChanged"/>
                    <TextBox Grid.Row="1" Grid.Column="1" Margin="5" VerticalContentAlignment="Center"
                             x:Name="RrPmtBox" TextChanged="RrOnTextChanged"/>
                    <TextBox Grid.Row="2" Grid.Column="1" Margin="5" VerticalContentAlignment="Center"
                             x:Name="RrNpBox" TextChanged="RrOnTextChanged"/>
                    <TextBox Grid.Row="3" Grid.Column="1" Margin="5" VerticalContentAlignment="Center"
                             x:Name="RrRrBox" IsReadOnly="True"/>
                </Grid>
            </TabItem>
        </TabControl>
    </Grid>
</Window>

We are adding the four tabs with the boxes, the same way we’ve added in the Delphi app. The code behind for the window is:

using System.Runtime.InteropServices;
using System.Windows;
using System.Windows.Controls;

namespace FinCalcWpf
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {
        [DllImport("FinCalcDll.dll")]
        private static extern double CalculatePV(double futureValue, double interestRate, int numPeriods);

        [DllImport("FinCalcDll.dll")]
        private static extern double CalculateFV(double presentValue, double interestRate, int numPeriods);

        [DllImport("FinCalcDll.dll")]
        private static extern double CalculatePmt(double presentValue, double interestRate, int numPeriods);

        [DllImport("FinCalcDll.dll")]
        private static extern double CalculateIRR(double presentValue, double payment, int numPeriods);

        public MainWindow()
        {
            InitializeComponent();
        }

        private void PvOnTextChanged(object sender, TextChangedEventArgs e)
        {
            if (double.TryParse(PvFvBox.Text, out double futureValue) &&
                double.TryParse(PvIrBox.Text, out double interestRate) &&
                int.TryParse(PvNpBox.Text, out int numPeriods))
              PvPvBox.Text = CalculatePV(futureValue, interestRate / 100.0, numPeriods).ToString("N2");
        }

        private void FvOnTextChanged(object sender, TextChangedEventArgs e)
        {
            if (double.TryParse(FvPvBox.Text, out double presentValue) &&
                double.TryParse(FvIrBox.Text, out double interestRate) &&
                int.TryParse(FvNpBox.Text, out int numPeriods))
                FvFvBox.Text = CalculateFV(presentValue, interestRate / 100.0, numPeriods).ToString("N2");
        }

        private void PmtOnTextChanged(object sender, TextChangedEventArgs e)
        {
            if (double.TryParse(PmtPvBox.Text, out double presentValue) &&
                double.TryParse(PmtIrBox.Text, out double interestRate) &&
                int.TryParse(PmtNpBox.Text, out int numPeriods))
                PmtPmtBox.Text = CalculatePmt(presentValue, interestRate / 100.0, numPeriods).ToString("N2");
        }

        private void RrOnTextChanged(object sender, TextChangedEventArgs e)
        {
            if (double.TryParse(RrPvBox.Text, out double presentValue) &&
                double.TryParse(RrPmtBox.Text, out double payment) &&
                int.TryParse(RrNpBox.Text, out int numPeriods))
                RrRrBox.Text = (CalculateIRR(presentValue, payment, numPeriods)*100.0).ToString("N2");
        }
    }
}

We’ve declared the functions in the dll and the we used them in the TextChanged event handlers. That will fill the result boxes in the tabs when you fill the input boxes. When you run the program, you will have the same result in both apps:

As you can see, refactoring the code into a dll brings many advantages: the code is not dependent on the UI, it is reusable and, best of all, it is testable. Creating unit tests for the code is a great way to be sure that everything works fine and, if you are making a change, you haven’t introduced a bug. Now, we’ll add the tests for the dll functions.

Adding tests to the dll

To add tests to the dll we must create a new test project to the group. Right click on the Project group and select “Add new project”. Then, select the DUnitX project, and set its settings:

When you click the OK button, Delphi will create a new test project with an unit with sample tests. You need to add the four calculator units to your new project and then, we can create the first test:

unit PVCalculatorTests;

interface
uses
  DUnitX.TestFramework, PVCalculator, Math;

type

  [TestFixture]
  TPvCalculatorTests = class(TObject)
  public
    [Test]
    [TestCase('FutureValue','-1,0,0')]
    [TestCase('Rate','0,-1,0')]
    [TestCase('Periods','0,0,-1')]
    procedure NegativeInputParametersReturnNan(const FutureValue : Double;
      const Rate : Double; const Periods : Integer);
  end;

implementation

procedure TPvCalculatorTests.NegativeInputParametersReturnNan(const FutureValue,
  Rate: Double; const Periods: Integer);
begin
  var result := CalculatePv(FutureValue,Rate,Periods);
  Assert.IsTrue(IsNan(result));
end;

initialization
  TDUnitX.RegisterTestFixture(TPvCalculatorTests);
end.

We named the unit PvCalculatorTests. Then we add the PVCalculator and Math units to the Uses clause. Then, we set the [TextFixture] attribute to the test class, to tell the test framework that this is a class that will have tests. Then we create a method and decorate it with the [Test] attribute. As this will be a parametrized test, we add the cases with the TestCase attribute.

The test is simple. We will run the test with the parameters (there will always be a negative parameter) and the result must always be NaN, thus pointing an invalid entry. If you run the project you will see something like this:

As you can see, the generated test project is a console app that runs the tests and shows the results. If you want a GUI app for the tests, you should install the TestInsight IDE plugin. As you can see from the image, all tests failed, because we have not checked the input parameters. We can change that in PVCalculator:

function CalculatePV(FutureValue, InterestRate: Double; NumPeriods : Integer):
  double;
begin
  if (FutureValue < 0) or (InterestRate < 0) or (NumPeriods < 0) then begin
    Result := NaN;
    exit;
  end;
  try
    Result := FutureValue / Power((1 + InterestRate), NumPeriods);
  except
    Result := NaN;
  end;
end;

Now, when you run the tests, all pass:

Now, we can create more tests for this calculator:

unit PVCalculatorTests;

interface
uses
  DUnitX.TestFramework, PVCalculator, Math;

type

  [TestFixture]
  TPvCalculatorTests = class(TObject)
  public
    [Test]
    [TestCase('FutureValue','-1,0,0')]
    [TestCase('Rate','0,-1,0')]
    [TestCase('Periods','0,0,-1')]
    procedure NegativeInputParametersReturnNan(const FutureValue : Double;
      const Rate : Double; const Periods : Integer);

    [Test]
    [TestCase('OnePeriod','100,1')]
    [TestCase('TenPeriods','100,10')]
    [TestCase('OneHundredPeriods','100,100')]
    procedure ZeroRatePresentValueEqualsFutureValue(const FutureValue : Double;
      const Periods : Integer);

    [Test]
    [TestCase('OnePeriodOnePercent','0.01,1')]
    [TestCase('OnePeriodTenPercent','0.10,1')]
    [TestCase('OnePeriodHundredPercent','1.00,1')]
    [TestCase('TenPeriodOnePercent','0.01,10')]
    [TestCase('TenPeriodTenPercent','0.10,10')]
    [TestCase('TenPeriodHundredPercent','1.00,10')]
    [TestCase('HundredPeriodOnePercent','0.01,100')]
    [TestCase('HundredPeriodTenPercent','0.10,100')]
    [TestCase('HundredPeriodHundredPercent','1.00,100')]
    procedure ZeroFutureValueEqualsZeroPresentValue(const Rate : Double;
      const Periods : Integer);
      
    [Test]
    [TestCase('OnePeriodOnePercent','100,0.01,1,99.01')]
    [TestCase('OnePeriodTenPercent','100,0.10,1,90.91')]
    [TestCase('OnePeriodHundredPercent','100,1.00,1,50')]
    [TestCase('TenPeriodOnePercent','100,0.01,10,90.53')]
    [TestCase('TenPeriodTenPercent','100,0.10,10,38.55')]
    [TestCase('TenPeriodHundredPercent','100,1.00,10,0.10')]
    [TestCase('HundredPeriodOnePercent','100,0.01,100,36.97')]
    [TestCase('HundredPeriodTenPercent','100,0.10,100,0.01')]
    [TestCase('HundredPeriodHundredPercent','100,1.00,100,0.00')]
    procedure VariablePeriodTests(const FutureValue : Double;
      const Rate : Double; const Periods : Integer; const Expected : Double);

  end;

implementation

procedure TPvCalculatorTests.NegativeInputParametersReturnNan(const FutureValue,
  Rate: Double; const Periods: Integer);
begin
  var result := CalculatePv(FutureValue,Rate,Periods);
  Assert.IsTrue(IsNan(result));
end;

procedure TPvCalculatorTests.VariablePeriodTests(const FutureValue, Rate: Double;
  const Periods: Integer; const Expected: Double);
begin
  var result := CalculatePv(FutureValue,Rate,Periods);
  Assert.AreEqual(Expected,Double(result));
end;

procedure TPvCalculatorTests.ZeroFutureValueEqualsZeroPresentValue(
  const Rate: Double; const Periods: Integer);
begin
  var result := CalculatePv(0,Rate,Periods);
  Assert.AreEqual(Double(0.0),Double(result));
end;

procedure TPvCalculatorTests.ZeroRatePresentValueEqualsFutureValue(
  const FutureValue Double; const Periods: Integer);
begin
  var result := CalculatePv(FutureValue,0,Periods);
  Assert.AreEqual(FutureValue,Double(result));
end;

initialization
  TDUnitX.RegisterTestFixture(TPvCalculatorTests);
end.

You should note one thing. When you run the tests, you will see that some of them fail:

This is not a failure in our code, but a failure in the test. As we are comparing double values, there are many decimals to compare and that’s not what you want. You can change your test to compare the difference to a maximum value. If the difference is greater than the maximum, the test fails:

procedure TPvCalculatorTests.VariablePeriodTests(const FutureValue, Rate: Double;
  const Periods: Integer; const Expected: Double);
begin
  var result := CalculatePv(FutureValue,Rate,Periods);
  Assert.AreEqual(Expected,Double(result), 0.01);
end;

Now, all tests pass. You can create tests for the other calculators the same way we did for this one. If you are using Delphi Rio and run the tests with debugging, you will see that some tests give a floating point error:

procedure TPmtCalculatorTests.ZeroPeriodsValueEqualsPresentValue(
  const PresentValue, Rate: Double);
begin
  var result := CalculatePmt(PresentValue,Rate, 0);
  Assert.AreEqual(Double(PresentValue),Double(result),0.01);
end;

procedure TPmtCalculatorTests.ZeroRatePmtEqualsPresentValueDivPeriods(
  const PresentValue: Double; const Periods: Integer);
begin
  var result := CalculatePmt(PresentValue,0,Periods);
  Assert.AreEqual(Double(PresentValue/Periods),Double(result),0.01);
end;

But the tests still pass. That’s because there is a bug in Delphi Rio (QC#RSP-19882), where comparisons with NaN return true, while they should return false. This can be solved by changing the tests to:

procedure TPmtCalculatorTests.ZeroPeriodsValueEqualsPresentValue(
  const PresentValue, Rate: Double);
begin
  var result := CalculatePmt(PresentValue,Rate, 0);
  Assert.IsFalse(IsNan(Result));
  Assert.AreEqual(Double(PresentValue),Double(result),0.01);
end;

procedure TPmtCalculatorTests.ZeroRatePmtEqualsPresentValueDivPeriods(
  const PresentValue: Double; const Periods: Integer);
begin
  var result := CalculatePmt(PresentValue,0,Periods);
  Assert.IsFalse(IsNan(Result));
  Assert.AreEqual(Double(PresentValue/Periods),Double(result),0.01);
end;

When you run the tests again, you will see that they will fail. We must change the calculator to solve this:

function CalculatePmt(PresentValue, InterestRate: Double;NumPeriods : Integer):
  Double;
begin
  if (PresentValue < 0) or (InterestRate < 0) or (NumPeriods < 0) then begin
    Result := NaN;
    exit;
  end;
  try
    if InterestRate = 0 then
      Result := PresentValue/NumPeriods
    else if NumPeriods = 0 then
      Result := PresentValue
    else
      Result := (PresentValue * InterestRate) * Power((1 + InterestRate),
        NumPeriods) / (Power((1 + InterestRate), NumPeriods) - 1);
  except
    Result := Nan;
  end;
end;

After this, our dll and its tests are ready to use and can be used in any language that supports Win32 dlls.

Conclusions

We’ve come a long way from the calculator code mixed with the UI to a new dll with unit tests. This architecture is more robust, reusable and easier to maintain. If we need to make changes to the dll, we are covered by unit tests, that can assure we are not introducing new bugs. And if some bug is found in the functions, it’s just a matter of writing a new test that fails, thus making sure of the bug, fix the code and rerun the test, making sure it’s passed. Using the Red-Refactor-Green procedure, we have a safety net for changing our code.

The full code for this article is at https://github.com/bsonnino/FinCalcDll

Until some time ago, creating an app to the Windows Store was only possible by creating an UWP (Universal Windows Platform) app and submitting it to the Windows Store. This was somewhat limiting because you had to know the technology and create a special new app using this platform.

With the introduction of the Desktop Bridge, Microsoft gave the developers the opportunity to submit their current Windows apps to the store, no matter which language they were programmed. And, besides that, you can also improve your app to use the new features and APIs introduced in Windows 10. That way, you can modernize your applications without having to learn a completely new technology.

Why add your app to the store?

There are a lot of advantages of adding your app to the store:

  • Discoverabilty – your app will be listed in a worldwide directory and can be discovered by people you never would have reached
  • Easy install and uninstall – installation and uninstalling Windows apps has always been a nightmare: you need admin rights, every installation program is different from the other, you need to create install scripts and, the worst, uninstalling the app always leave traces behind that will clutter your machine. With the store, install and uninstall are always a breeze
  • Security – apps downloaded from the store are guaranteed to be safe: no viruses or harm to your machine. The apps run in a sandbox and will not do anything harmful
  • Monetization – you don’t have to worry to sell your app. You can use several ways to monetize it: put a price to download it, in-app purchases, ads or even use a subscription-based app. You don’t have to setup a special platform to sell your app.

To add your app to the store you need to do special preparations and submit it there and wait for approval. It’s not as straightforward as posting an executable in the web, but the extra work is worth the benefits. The latest versions of Rad Studio make this procedure easier and Delphi Rio 10.3 bring a new component, TWindowsStore, that will ease the task of managing trial versions and in-app purchases.

This article will show how to package an app and send it to the Windows Store, so it can be installed and downloaded from there.

Packaging the app to the store

For this article, we will be using a financial calculator app, that has four tabs, to calculate payments for an installment payment, the rate used for an installment payment, the future value of an investment and the present value of an investment:

The user will fill the first three boxes of the page and the fourth will be calculated automatically. The first step is to add it to the store. We will not do anything right now, just add it there, so it can be installed and uninstalled easily. Even if you don’t want to add your app to the store, it’s interesting to do these steps, so you have an easy and safe way to distribute and install your app.

For this app, I’ve chosen to add a new feature, introduced in Delphi 10.3: inline variables and type inference. If you look at the calculation for the Present Value, you will see this:

procedure TForm1.CalculatePV;
begin
  try
    var FutureValue := StrToFloat(FvPresentValue.Text);
    var InterestRate := StrToFloat(IRPresentValue.Text)/100.0;
    var NumPeriods := StrToInt(NpPresentValue.Text);
    var PresentValue := FutureValue/Power((1+InterestRate),NumPeriods);
    PvPresentValue.Text := FormatFloat('0.00',PresentValue);
  except
    On EConvertError do
      PvPresentValue.Text := '';
  end;
end;

You will notice two things in this code:

  • There is no Variable definition section (var) in the method. All variables are defined inline, as they are needed
  • The variables have not an explicit type. Their type is inferred from the expression that follows it. That doesn’t mean that these variables have no type, like in Javascript or VB. It just means that the types are inferred by the compiler. FutureValue is an Extended variable, and if you try to assign it to something else (a string, for example), you will get a compiler error.

You may say that these two features can obfuscate your code and make it unreadable, but my experience with C#, that have had these features for a long time, is that it simplifies your code and make it easier to maintain. Anyway, it’s your call: if you want to keep using the old notation, it’s up to you. I really liked these ones.

But let’s go back to the subject of the article, creating a Windows Store app from this app. This is not new in Delphi 10.3, you can create a Windows Store app since Delphi Berlin. The first step is to install a Windows SDK, that can be downloaded and installed from https://developer.microsoft.com/en-us/windows/downloads/windows-10-sdk.

Once you have it installed, you must go to the SDK Manager, in Tools/Options/Deployment/SDK Manager and check if the SDK is setup and all the paths are ok:

 

The next step is to create a certificate. Your apps must be signed with this certificate to go to the store. You don’t need to have a certificate issued by a Certified Issuer, you can use a self-signed certificate. To create one, go to Provisioning in the options dialog and select the certificate you want to use:

The distribution type indicates how you will distribute your app: Ad Hoc will make an appx file that can distributed to your users and is used to install the app without going to the store. Store will create a package for the store. If you want to distribute the app Ad Hoc, you need to create a certificate, by clicking the “Create self-signed certificate” button.

Just select a file name and a password and it will be created. Then, you can use it for creating your apps for the store.

Once you have created the certificate, you can compile the app. For distribution without the store, you can select the Ad Hoc distribution, with the certificate you’ve just created. Build your app and Delphi will show a message box like this one:

You can go to the location where the appx file is located and double click on it. A dialog like this will be shown:

If you click in the Install button, the app will be installed and launched.

This is a Windows 10 app and it has the same features as any other Windows 10 app: you can uninstall it easily, pin it to the Start Menu, and so on. But, for this app, the only way to install it is to distribute the appx file and double click on it. We want more than that. We want to distribute it to the store.

Distributing the app in the store

To distribute the app in the store, you must create a submission for it. The first thing to do is to create a developer account in the Windows Dev Center. This account costs a one-time fee of $19.00 and the instructions to create it are at https://docs.microsoft.com/en-us/windows/uwp/publish/opening-a-developer-account.

When you have the account created, you can create your app submission. In the dashboard, click on the “Create New App” button. That will take you to the screen where you must reserve the name for your app. This name must be unique, and the availability of the name will be checked.

Once you click on the “Reserve product name” button, you will be directed to the screen to create the submission:

The next step is to create the submission, by clicking the “Start your submission” button:

You must walk through the options, setting the way you want to distribute the app:

  • Pricing and availability sets the markets, price and visibility for your app (you may make it public, or private). You can also set a free trial and sale price
  • Properties will set the category of your app and what kind of hardware is required/recommended
  • Age ratings will set what kind of app you are sending to the store and the age rating for you
  • Packages is where you send the package for the app. You must check the Windows Desktop box, as this is a Windows Desktop app and send the correct appx file.

You must build your app with the Distribution type to Store, set the Package Display Name to the one you reserved in the store and set the publisher display name to the one you created the account in the dev center:

Then you can drag the generated appx file to the package place in the store. The package will be validated and, if something is wrong, it will be pointed to you:

You must fix these things and rebuild the package:

Once everything is ok, you can save the package submission, and then set all the other options: the descriptions, screenshots and data for submission. Then you can check if everything is ok and submit to the store:

As the app needs full trust, the certification process will take some days. When it passes the certification, your app is available in the store and can be downloaded from there.

At this point, anyone with a Windows 10 machine will be able to download and install the app from the store.

In another article, I will show how to use the TWindowsStore component to manage trial versions and In-App purchases, so you can sell and get money from your app.

The source code for this article is at https://github.com/bsonnino/FinCalc

Os novos dispositivos 2-em-1 Windows 10 trouxeram uma novidade: você pode usar os seus diversos sensores para aumentar a experiência do usuário em seus programas.

O Delphi trouxe a possibilidade de usar estes sensores em seus programas, tanto usando a VCL como no FireMonkey. A minha palestra na CodeRage Brasil mostra como você pode criar seus programas Delphi que utilizam a classe TSensor para melhorar a experiência do usuário. Confira em  https://www.youtube.com/watch?v=U2uKyuKwJcY