Inno Setup (Parte 8): Página personalizada II

Categoria: Tutoriais
Categoria Pai: Addicted 2 Delphi!
Acessos: 10412
Página anterior do tutorial atual Primeira página do tutoria altual Próxima página do tutorial atual
Imagem meramente ilustrativa

Se você leu a parte anterior deste tutorial (Inno Setup (Parte 7): Melhorando a execução de programas externos) você deve ter sido uma das pessoas que se chateou bastante por conta de eu ter complicado a forma de executar programas externos nas etapas de pré e pós instalação. Como eu expliquei neste artigo, eu só fiz essa mudança para permitir que coisas mais interessantes fossem realizadas e deixei até mesmo um spoiler (exibição gráfica avançada de status de instalação). Eu sei que todos vocês estão arrancando os cabelos para saber o que eu vou fazer, mas fiquem tranquilos, a espera acabou!

Indicando graficamente o status de configuração do PostgreSQL

Do ponto de vista visual, isso nem é tão sofisticado, no entanto torna o instalador mais amigável ao usuário final, seja ele um leigo, seja ele um usuário avançado, pois também pretendo exibir a saída da execução dos comandos diretamente na tela do instalador! Além dessas vantagens para o usuário final a implementação desta técnica servirá para (mais uma vez) mostrar que o Inno Setup não é uma ferramenta qualquer, muito menos uma ferramenta que apenas desenvolvedores de fundo de quintal usam.

Eu sei que tem por aí um monte de gente que não usa o Inno Setup por pura falta de conhecimento acerca de suas capacidades, normalmente são pessoas que preferem fazer seu próprio instalador a ter que usar o Inno Setup (ou pagar por um InstallShield). O que me deixa mais perplexo que existem algumas empresas até conceituadas que preferem desenvolver sua própria solução de instalação (normalmente muito bugada e complexa até para o usuário final), mesmo existindo tantas opções gratuitas, personalizáveis e altamente flexíveis como o Inno Setup.

Se você ficou curioso para saber do que eu estou falando, eis abaixo a imagem final daquilo que será conseguido ao término deste artigo:

Mãos à obra!

Nesta parte do tutorial existem muitas alterações a serem feitas no script e vou citar cada uma delas, entretanto vou me aprofundar apenas na parte relacionada ao Pascal Script, do contrário este artigo ficaria grande de mais. Para maiores detalhes a respeito de alterações que eu não me aprofundar, aconselho que você dê uma olhada no script anexado a este artigo, abrindo-o no Inno Script Studio, aliás, eu recomendo fortemente que você leia este artigo com este script aberto no Inno Script Studio. Daqui para a frente vou considerar que você está com o script de exemplo aberto.


Para começar adicionamos alguns arquivos novos na seção Files. Estes arquivos são os arquivos das imagens que são usadas pelos indicadores gráficos e uma dll (ISF.dll), da qual vou falar mais tarde:

Estes novos arquivos foram colocados na parte de cima da lista. Apesar de isso não ser obrigatório, é recomendável, porque posteriormente no Pascal Script as imagens serão extraídas (descompactadas) a partir do instalador e, caso você escolha utilizar o método de compressão "sólida"[1], ele precisará realizar a descompactação na memória de todos os arquivos que vem antes dos arquivos que você deseja extrair, logo, quanto mais próximo ao início da lista estiverem os arquivos que você pretende extrair manualmente, mais rápida será esta extração (descompressão).

Estes arquivos utilizam flags especiais que eu ainda não havia mencionado. Execute um duplo clique em cada um deles para ver quais são e acesse a ajuda do Inno Setup para entender para que eles servem.


Após a inclusão dos arquivos necessários, nós criamos uma página personalizada usando o ISFD. Esta página é um esboço do que deve aparecer durante a instalação:

Eu falei em "esboço" porque como eu já falei em outra parte deste tutorial, o ISFD é um gerador de código, ou seja, ele auxilia ao máximo para a criação de páginas personalizadas. Além disso, ele foi criado para auxiliar apenas na criação de novas páginas personalizadas e no caso desta página, nós não criamos uma página nova, mas sim, alteramos uma existente.

Isso mesmo! Ao contrário da nossa primeira página personalizada, a qual foi inserida completamente dentre as páginas preexistentes do instalador, esta nova página não foi inserida, eu usarei apenas o código gerado pelo ISFD para MODIFICAR, uma página do instalador preexistente, a página de progresso de instalação. Para isso, alterei o código gerado pelo ISFD, o qual será mostrado posteriormente.


Inicialmente, para começar a utilizar o código gerado pelo ISFD, eu copiei as seguintes mensagens personalizadas dispníveis na aba Custom Messages:

Eu copiei apenas as seis strings referentes aos Captions dos TLabel da página personalizada. A última string não é importante, ela representa o texto colocado no TMemo, o qual será sempre substituído dinamicamente, logo, não é necessário lidar com essa string.

Após copiar as strings eu as colei na seção CustomMessages do script, tal como pode ser visto abaixo:

Eu usei o código-fonte do script, clicando no item Inno Setup Script, para colar estas seis strings da forma como elas estiverem, diretamente ao final da seção CustomMessages.


De volta ao ISFD eu copiei as variáveis dos componentes que existem na página personalizada:

E dentro do Pascal Script eu colei estas variáveis abaixo da palavra-chave var, como de costume:

Carregando uma DLL

Imediatamente após o bloco de declaração de variáveis eu declarei a importação de uma função existente naquela dll que incluímos na seção Files. Sim! O Inno Setup permite que sejam importadas funções a partir de dlls como você faria em qualquer aplicação Delphi e não apenas dlls criadas no Delphi, qualquer dll comum criada em qualquer linguagem. O único requisito para que a importação das funções ocorra, é que tais funções usem a convenção de chamada stdcall. A linha que declara a importação de uma função a partir da dll e que eu adicionei ao script foi a seguinte:

function ExecuteAndCaptureOutput(const AStdOutHandle: THandle; 
                                 const ACommand
                                     , AParameters: String; 
                                   out AExitCode: Cardinal): Boolean;
                        external 'ExecuteAndCaptureOutput@files:ISF.dll stdcall';

O bloco acima apresenta a assinatura de uma função que executará o comando definido em ACommand, usando os parâmetros definidos em AParameters e que retornará true, caso a execução tenha sido bem sucedida. Caso a execução falhe, o código de saída do comando será retornado em AExitCode e poderá ser averiguado para se obter maiores informações acerca do erro ocorrido. O parâmetro AStdOutHandle deve receber o handle do componente TMemo na tela do Inno Setup que será atualizado com a saída de console do comando sendo executado pela função.


Até este ponto trata-se de uma função ordinária que veríamos em qualquer aplicação Delphi. A diferença vem após a palavra-chave external, a qual indica que a declaração da função encontra-se em uma dll. Após esta palavra-chave nós vemos a seguinte string:

'ExecuteAndCaptureOutput@files:ISF.dll stdcall'

A primeira parte, antes do caractere @, representa o nome da função como ele está dentro da dll, pois é possível utilizar um nome diferente, tal como é possível no Delphi via palavra-chave name. A fim de tornar as coisas mais simples, eu não costumo usar nomes diferentes, portanto, o nome da função em sua assinatura deve ser o mesmo nome que existe antes do caractere @.

Após o caractere @, vemos a expressão files: (files + dois pontos), que indica que a dll especificada após os dois pontos (ISF.dll) encontra-se dentre os arquivos de instalação, na seção Files. Por fim, stdcall é a convenção de chamada da função.

A função ExecuteAndCaptureOutput

Como foi dito anteriormente, o objetivo desta função é executar um comando (normalmente uma aplicação de console) e direcionar sua saída para um componente TMemo cujo handle é especificado no seu parâmetro AStdOutHandle. Esta é atualmente a única função exportada por ISF.dll, cujo código-fonte encontra-se anexado a este artigo.

A fim de poupar seu tempo, caso você queira utilizar esta função fora do contexto do Inno Setup, segue abaixo seu código-fonte, o qual não vou explicar porque foge ao escopo deste artigo:

function ExecuteAndCaptureOutput(const AStdOutHandle: THandle; const ACommand, AParameters: String; out AExitCode: Cardinal): Boolean; stdcall;
const
  READBUFFERSIZE = High(BYTE);
var
  SCAT: TSecurityAttributes;
  ReadPipeHandle: THandle;
  WritePipeHandle: THandle;
  SRIN: TStartupInfo;
  PRIN: TProcessInformation;
  OEMBuffer: array [0 .. READBUFFERSIZE] of AnsiChar;
  CharBuffer: array [0 .. READBUFFERSIZE] of AnsiChar;
  BytesRead: DWORD;
  CommandRunning: DWORD;
  BytesToRead: DWORD;
begin
  Result := False;

  SCAT.nLength := SizeOf(TSecurityAttributes);
  SCAT.bInheritHandle := true;
  SCAT.lpSecurityDescriptor := nil;

  if CreatePipe(ReadPipeHandle, WritePipeHandle, @SCAT, 0) then
    try
      ZeroMemory(@SRIN,SizeOf(TStartupInfo));

      SRIN.cb := SizeOf(TStartupInfo);
      SRIN.hStdInput := ReadPipeHandle;
      SRIN.hStdOutput := WritePipeHandle;
      SRIN.hStdError := WritePipeHandle;
      SRIN.dwFlags := STARTF_USESTDHANDLES or STARTF_USESHOWWINDOW;
      SRIN.wShowWindow := SW_HIDE;

      if CreateProcess(nil, PChar(ACommand + ' ' + AParameters), @SCAT, @SCAT, True, NORMAL_PRIORITY_CLASS, nil, nil, SRIN, PRIN) then
        try
          repeat
            CommandRunning := WaitForSingleObject(PRIN.hProcess, 100);

            PeekNamedPipe(ReadPipeHandle, nil, 0, nil, @BytesToRead, nil);

            if (BytesToRead > 0) then
              repeat
                BytesRead := 0;
                ReadFile(ReadPipeHandle, OEMBuffer[0], READBUFFERSIZE, BytesRead, nil);
                OEMBuffer[BytesRead] := #0;
                OemToCharA(OEMBuffer, CharBuffer);

                AddLine(AStdOutHandle,String(CharBuffer));
              until (BytesRead < READBUFFERSIZE);

            Application.ProcessMessages;

          until (CommandRunning <> WAIT_TIMEOUT);

          GetExitCodeProcess(PRIN.hProcess,AExitCode);
          Result := True;
        finally
          CloseHandle(PRIN.hProcess);
          CloseHandle(PRIN.hThread);
        end
      else
        AExitCode := GetLastError;
    finally
      CloseHandle(ReadPipeHandle);
      CloseHandle(WritePipeHandle);
    end;
end;

Para atualizar o texto no TMemo, usamos seu handle para enviar mensagens específicas à janela do componente. Abaixo está a função interna (não exportada) que realiza o envio da mensagem com o texto a ser exibido:

procedure AddLine(AHandle: THandle; ALine: String);
begin
  SendMessage(AHandle, EM_SETSEL, 0, -1);
  SendMessage(AHandle, EM_SETSEL, -1, -1);
  SendMessage(AHandle, EM_REPLACESEL, 0, LPARAM(PChar(ALine)));
end;

Também não vou explicar como essa função funciona, apesar de ela ser bem simples, mas você pode buscar no Google a respeito de Windows Messages, SendMessage e as mensagens EM_* que foram usadas.


Modificando a página de progresso de instalação

Como já foi dito anteriormente, nós não pretendemos criar uma nova página, mas sim alterar uma existente e como o ISFD trabalha sempre com a ideia de criar uma página nova, eu precisei alterar o código que ele gerou e que era focado na ideia de uma nova página, de forma que, ao invés disso ele modifique uma página existente.

O código gerado pelo ISFD para a página personalizada teve de ser alterado e ficou, ao final, da seguinte forma:

procedure PGConfigStatus_CreatePage;
var
  Page: TWizardPage;
begin
  Page := PageFromId(wpInstalling);

  { PanelPostgreSQLStatus }
  PanelPostgreSQLStatus := TPanel.Create(Page);
  with PanelPostgreSQLStatus do
  begin
    Parent := Page.Surface;
    Left := 0
    Top := 0;
    Width := Parent.Width;
    Height := Parent.Height - 6;
    BevelInner := bvLowered;
    TabOrder := 0;
    Color := clInfoBk;
    ParentBackground := False;
    Visible := False;
  end;
  
  { BitmapImage1 }
  BitmapImage1 := TBitmapImage.Create(Page);
  with BitmapImage1 do
  begin
    Parent := PanelPostgreSQLStatus;
    Left := ScaleX(6);
    Top := ScaleY(6);
    Width := ScaleX(16);
    Height := ScaleY(16);
    Visible := False;
  end;

  { Label1 }
  Label1 := TLabel.Create(Page);
  with Label1 do
  begin
    Parent := PanelPostgreSQLStatus;
    Caption := ExpandConstant('{cm:PGConfigStatus_Label1_Caption0}');
    Left := ScaleX(26);
    Top := BitmapImage1.Top + 1;
  end;
  
  { BitmapImage2 }
  BitmapImage2 := TBitmapImage.Create(Page);
  with BitmapImage2 do
  begin
    Parent := PanelPostgreSQLStatus;
    Left := ScaleX(6);
    Top := BitmapImage1.Top + BitmapImage1.Height + 3;
    Width := ScaleX(16);
    Height := ScaleY(16);
    Visible := False;
  end;
  
  { Label2 }
  Label2 := TLabel.Create(Page);
  with Label2 do
  begin
    Parent := PanelPostgreSQLStatus;
    Caption := ExpandConstant('{cm:PGConfigStatus_Label2_Caption0}');
    Left := ScaleX(26);
    Top := BitmapImage2.Top + 1;
  end;
  
  { BitmapImage3 }
  BitmapImage3 := TBitmapImage.Create(Page);
  with BitmapImage3 do
  begin
    Parent := PanelPostgreSQLStatus;
    Left := ScaleX(6);
    Top := BitmapImage2.Top + BitmapImage2.Height + 3;
    Width := ScaleX(16);
    Height := ScaleY(16);
    Visible := False;
  end;
  
  { Label3 }
  Label3 := TLabel.Create(Page);
  with Label3 do
  begin
    Parent := PanelPostgreSQLStatus;
    Caption := ExpandConstant('{cm:PGConfigStatus_Label3_Caption0}');
    Left := ScaleX(26);
    Top := BitmapImage3.Top + 1;
  end;
  
  { BitmapImage4 }
  BitmapImage4 := TBitmapImage.Create(Page);
  with BitmapImage4 do
  begin
    Parent := PanelPostgreSQLStatus;
    Left := ScaleX(6);
    Top := BitmapImage3.Top + BitmapImage3.Height + 3;
    Width := ScaleX(16);
    Height := ScaleY(16);
    Visible := False;
  end;
  
  { Label4 }
  Label4 := TLabel.Create(Page);
  with Label4 do
  begin
    Parent := PanelPostgreSQLStatus;
    Caption := ExpandConstant('{cm:PGConfigStatus_Label4_Caption0}');
    Left := ScaleX(26);
    Top := BitmapImage4.Top + 1;
  end;

  { BitmapImage5 }
  BitmapImage5 := TBitmapImage.Create(Page);
  with BitmapImage5 do
  begin
    Parent := PanelPostgreSQLStatus;
    Left := ScaleX(6);
    Top := BitmapImage4.Top + BitmapImage4.Height + 3;
    Width := ScaleX(16);
    Height := ScaleY(16);
    Visible := False;
  end;
  
  { Label5 }
  Label5 := TLabel.Create(Page);
  with Label5 do
  begin
    Parent := PanelPostgreSQLStatus;
    Caption := ExpandConstant('{cm:PGConfigStatus_Label5_Caption0}');
    Left := ScaleX(26);
    Top := BitmapImage5.Top + 1;
  end;

  { BitmapImage6 }
  BitmapImage6 := TBitmapImage.Create(Page);
  with BitmapImage6 do
  begin
    Parent := PanelPostgreSQLStatus;
    Left := ScaleX(6);
    Top := BitmapImage5.Top + BitmapImage5.Height + 3;
    Width := ScaleX(16);
    Height := ScaleY(16);
    Visible := False;
  end;
  
  { Label6 }
  Label6 := TLabel.Create(Page);
  with Label6 do
  begin
    Parent := PanelPostgreSQLStatus;
    Caption := ExpandConstant('{cm:PGConfigStatus_Label6_Caption0}');
    Left := ScaleX(26);
    Top := BitmapImage6.Top + 1;
  end;

  { Memo1 }
  Memo1 := TMemo.Create(Page);
  with Memo1 do
  begin
    Parent := PanelPostgreSQLStatus;
    Left := ScaleX(6);
    Top := BitmapImage6.Top + BitmapImage6.Height + 3;
    Width := Parent.Width - 2 * Left;
    Height := Parent.Height - Top - 6;
    ReadOnly := True;
    Color := $00000000;
    Font.Color := 65280;
    Font.Height := ScaleY(-11);
    Font.Name := 'Courier New';
    TabOrder := 1;
    ScrollBars := ssVertical;
    Text := '';
    ReadOnly := True;
  end;
end;

O ISFD gera sempre uma função que cria a página personalizada, no entanto, como estamos modificando uma página existente, não é necessária uma função, pois ela não vai criar uma página, não sendo necessário retornar nada, logo, a primeira modificação foi transformar a função em procedure.

A segunda modificação feita foi remover o parâmetro da função, o qual servia para indicar o ID da página anterior à nossa página personalizada. Novamente, como não vamos inserir uma página nova, não precisamos informar nenhum ID e por isso o parâmetro tornou-se desnecessário.

A terceira modificação realizada foi a substituição do código que criava a página personalizada (CreateCustomPage) por um código que seleciona uma página preexistente. A linha Page := PageFromId(wpInstalling) é a responsável por obter uma instância (TWizardPage) da página de progresso de instalação a partir de seu ID, o qual é wpInstalling.


A quarta alteração é óbvia: removi a linha de Result (já que não se trata mais de uma função) e removi também o código que continha a atribuição de manipuladores de evento, pois eles não são necessários. Também realizei algumas alterações mínimas nos códigos de criação dos componentes, alterações estas tão pequenas que não vou citar, mas que basicamente serviram para corrigir alguns erros de tamanho e posicionamento de controles. O código de criação / modificação de páginas é bem direto, olhe-o e tente entender. Se tiver alguma dúvida sinta-se à vontade de perguntar nos comentários abaixo deste artigo!

Gostaria de abrir um parêntese para falar um pouco sobre o papel do ISFD. Como eu devo ter dito anteriormente, este programa é antigo, de 2006, e por conta disso ele não é perfeito, mas até o momento é a única ferramenta conhecida gratuita que é capaz de gerar código a partir de um design visual à-là Delphi. Sua utilização economiza horas de digitação maçante, de forma que você se mantenha focado no que é importante neste caso: o desenho da tela personalizada.

Sobre o ISFD não ser perfeito, vou citar apenas uma coisa que notei quando estava desenhando esta última tela personalizada. Eu notei que ele possui uma área útil de desenho menor do que a área útil real do Inno Setup, ou seja, ou o Inno Setup mudou e o ISFD não acompanhou, ou ele sempre esteve errado e só percebi agora. O que quero dizer com isso é que você pode e deve usar o ISFD para desenhar suas telas, mas saiba que podem haver algumas pequenas discrepâncias. A boa notícia é que você pode alterar o código manualmente, tal como eu fiz, para corrigir as imperfeições que o ISFD gerou, ou mesmo para incluir coisas que o ISFD não é capaz de manipular por ser velhinho :)

Após chegar a versão final do procedure PGConfigStatus_CreatePage, tudo que precisei fazer foi colar todo ele logo abaixo da função de criação de página personalizada anterior (PGConfig_CreatePage) dentro do script.

No início deste artigo eu falei dos novos arquivos que eu coloquei na seção Files. Além da ISF.dll, mais 4 arquivos de imagens BMP que precisam ser copiados para fora do instalador (extraídos) para que possam ser usados. A próxima modificação que fiz no script, portanto, foi a extração destas imagens, e para isso, modifique a função PrepareToInstall, que ficou da seguinte forma:

function PrepareToInstall(var NeedsRestart: Boolean): String;
begin
  // Cria o arquivo de senha com a senha especificada
  CriarArquivoDeSenha(PGPassword);

  if IsComponentSelected('PostgreSQL') and IsTaskSelected('PGConfig') then
  begin
    ExtractTemporaryFile('Seta.bmp');
    ExtractTemporaryFile('Nao.bmp');
    ExtractTemporaryFile('Sim.bmp');
    ExtractTemporaryFile('NA.bmp');
  end;

  Result := '';
end;

A parte nova no código acima basicamente verifica se as opções de instalação e configuração do PostgreSQL foram selecionadas, e se foram, será feita a extração de cada uma das imagens na pasta temporária de instalação que, dentro do Inno Setup, é identificada pela constante {tmp}. A função do Inno Setup responsável por fazer esta extração é a função ExtractTemporaryFile.

Se a pasta temporária no seu sistema for C:\Windows\Temp, o Inno Setup, durante a execução do instalador, cria uma pasta dentro desta pasta temporária, logo, {tmp} aponta para a pasta temporária do Inno Setup, localizada na pasta temporária do sistema no qual a instalação está sendo executada. Após a execução bem sucedida da extração dos arquivos, caso precisemos nos referenciar a qualquer um deles, basta usar a constante {tmp}, por exemplo, {tmp}\Seta.bmp!

Ao término da instalação, a pasta temporária criada pelo Inno Setup, juntamente com todo seu conteúdo (incluindo os arquivos extraídos pela função ExtractTemporaryFile), será excluída, portanto, é seguro e limpo extrair arquivos na pasta temporária.

Por fim, precisamos instruir o instalador a criar os controles na página de progresso da instalação e para fazer isso, modificamos a função InitializeWizard:

procedure InitializeWizard();
begin
  // Cria a página personalizada #1
  PGConfig_CreatePage(wpSelectTasks);
  // Cria os controles na página de status de instalação (Página personalizada #2)
  PGConfigStatus_CreatePage;
end;

A modificação além de autoexplicativa, encontra-se comentada. Foi absolutamente natural a escolha deste local para criação dos controles na página de progresso da instalação. Note que lá também se encontra a chamada a função que cria nossa primeira página personalizada.

Se você estiver bem atento vai perceber que eu estou incondicionalmente alterando a tela de progresso de instalação, ou seja, eu sempre estou incluindo os controles nesta tela, pois eu sempre estou executando PGConfigStatus_CreatePage, mas eles não vão aparecer sempre, como você pode estar imaginando, porque eu coloquei todos os componentes dentro de um TPanel com visible = false! Apenas em condição propícia, este TPanel será exibido. Você vai ver como isso é feito, mais adiante neste artigo.

Exibindo o progresso de configuração do PostgreSQL

Na seção anterior eu informei todas as alterações necessárias para que as alterações na página de progresso de instalação apareçam, agora eu vou mostrar o que precisa ser feito para que estas alterações ganhem vida! Pra começar, eu criei um procedure para alteração das imagens de status:

procedure ChangeStatusImage(AStep, AImage: Byte);
begin
  case AStep of
    1: begin 
      BitmapImage1.ReplaceColor := clFuchsia;
      BitmapImage1.ReplaceWithColor := clInfoBk;
      case AImage of
        1: BitmapImage1.Bitmap.LoadFromFile(ExpandConstant('{tmp}\Seta.bmp'));
        2: BitmapImage1.Bitmap.LoadFromFile(ExpandConstant('{tmp}\Sim.bmp'));
        3: BitmapImage1.Bitmap.LoadFromFile(ExpandConstant('{tmp}\Nao.bmp'));
        4: BitmapImage1.Bitmap.LoadFromFile(ExpandConstant('{tmp}\NA.bmp'));
      end;
      BitmapImage1.Visible := AImage > 0;
    end;
    2: begin
      BitmapImage2.ReplaceColor := clFuchsia;
      BitmapImage2.ReplaceWithColor := clInfoBk;
      case AImage of
        1: BitmapImage2.Bitmap.LoadFromFile(ExpandConstant('{tmp}\Seta.bmp'));
        2: BitmapImage2.Bitmap.LoadFromFile(ExpandConstant('{tmp}\Sim.bmp'));
        3: BitmapImage2.Bitmap.LoadFromFile(ExpandConstant('{tmp}\Nao.bmp'));
        4: BitmapImage2.Bitmap.LoadFromFile(ExpandConstant('{tmp}\NA.bmp'));
      end;
      BitmapImage2.Visible := AImage > 0;
    end;
    3: begin
      BitmapImage3.ReplaceColor := clFuchsia;
      BitmapImage3.ReplaceWithColor := clInfoBk;
      case AImage of
        1: BitmapImage3.Bitmap.LoadFromFile(ExpandConstant('{tmp}\Seta.bmp'));
        2: BitmapImage3.Bitmap.LoadFromFile(ExpandConstant('{tmp}\Sim.bmp'));
        3: BitmapImage3.Bitmap.LoadFromFile(ExpandConstant('{tmp}\Nao.bmp'));
        4: BitmapImage3.Bitmap.LoadFromFile(ExpandConstant('{tmp}\NA.bmp'));
      end;
      BitmapImage3.Visible := AImage > 0;
    end;
    4: begin
      BitmapImage4.ReplaceColor := clFuchsia;
      BitmapImage4.ReplaceWithColor := clInfoBk;
      case AImage of
        1: BitmapImage4.Bitmap.LoadFromFile(ExpandConstant('{tmp}\Seta.bmp'));
        2: BitmapImage4.Bitmap.LoadFromFile(ExpandConstant('{tmp}\Sim.bmp'));
        3: BitmapImage4.Bitmap.LoadFromFile(ExpandConstant('{tmp}\Nao.bmp'));
        4: BitmapImage4.Bitmap.LoadFromFile(ExpandConstant('{tmp}\NA.bmp'));
      end;
      BitmapImage4.Visible := AImage > 0;
    end;
    5: begin
      BitmapImage5.ReplaceColor := clFuchsia;
      BitmapImage5.ReplaceWithColor := clInfoBk;
      case AImage of
        1: BitmapImage5.Bitmap.LoadFromFile(ExpandConstant('{tmp}\Seta.bmp'));
        2: BitmapImage5.Bitmap.LoadFromFile(ExpandConstant('{tmp}\Sim.bmp'));
        3: BitmapImage5.Bitmap.LoadFromFile(ExpandConstant('{tmp}\Nao.bmp'));
        4: BitmapImage5.Bitmap.LoadFromFile(ExpandConstant('{tmp}\NA.bmp'));
      end;
      BitmapImage5.Visible := AImage > 0;
    end;
    6: begin
      BitmapImage6.ReplaceColor := clFuchsia;
      BitmapImage6.ReplaceWithColor := clInfoBk;
      case AImage of
        1: BitmapImage6.Bitmap.LoadFromFile(ExpandConstant('{tmp}\Seta.bmp'));
        2: BitmapImage6.Bitmap.LoadFromFile(ExpandConstant('{tmp}\Sim.bmp'));
        3: BitmapImage6.Bitmap.LoadFromFile(ExpandConstant('{tmp}\Nao.bmp'));
        4: BitmapImage6.Bitmap.LoadFromFile(ExpandConstant('{tmp}\NA.bmp'));
      end;
      BitmapImage6.Visible := AImage > 0;
    end;
  end;
end;

A tela de progresso possui seis passos, logo, no primeiro parâmetro deste procedure (AStep), nós informamos um número de 1 a 6 que indica qual das seis imagens queremos modificar. O segundo parâmetro (AImage) indica qual imagem queremos carregar. Ele pode variar de 1 a 4. Olhe o procedure e tente entendê-lo. Você verá que não é tão complicado. Eu criei este procedure para facilitar a troca de imagens, a qual será feita pelo método ExecuteStep, o qual é visto a seguir:

procedure ExecuteStep(AStepNumber: Cardinal; AStepDescription, ACommand, AParameters: String; ANoSuccessIsWarning: Boolean);
var
  ResultCode: Cardinal;
begin
  Memo1.Lines.Add(AStepDescription);
  ChangeStatusImage(AStepNumber,1);

  ExecuteAndCaptureOutput(Memo1.Handle
                         ,ExpandConstant(ACommand)
                         ,ExpandConstant(AParameters)
                         ,ResultCode);

  if ResultCode = 0 then
    ChangeStatusImage(AStepNumber,2)
  else
  begin
    if ANoSuccessIsWarning then
      ChangeStatusImage(AStepNumber,4)
    else
      ChangeStatusImage(AStepNumber,3);
  end;
end;

Este procedure faz duas coisas basicamente: executa um comando usando aquela função importada de ISF.dll (ExecuteAndCaptureOutput) e troca as imagens de status usando o procedure ChangeStatusImage. Inicialmente a imagem número 1 (seta) é carregada para indicar que o comando está sendo executado, em seguida a função ExecuteAndCaptureOutput é chamada e após sua execução haverá um valor em ResultCode. Caso ResultCode seja zero, significa que a execução foi bem sucedida e neste caso carregamos a imagem número 2 (tick verde). Caso a execução tenha falhado temos duas possibilidades; ou carregamos a imagem de um "X" vermelho indicando a condição de erro (imagem número 3) ou carregamos a imagem que contém um traço amarelo (imagem número 4) para indicar que houve uma falha, mas que ela não interfere na instalação.


Sobre os parâmetros deste procedure, AStepNumber indica o passo atual de forma que as chamadas internas a ChangeStatusImage carregue as imagens no local correto da tela (um dos seis componentes de imagem disponíveis). AStepDescription é um texto pequeno, de uma só linha, que será escrito no TMemo de status com o intuito de indicar o que será feito a seguir. ACommand é o caminho completo do programa a ser executado, o qual pode conter constantes, as quais serão automaticamente expandidas para seus valores reais. AParameters são os parâmetros que são passados para ACommand e tal como este último, pode conter constantes que serão expandidas automaticamente. ANoSuccessWarning é um flag que serve para indicar que uma falha no comando não deve ser exibida na tela como um erro, mas sim como um aviso (linha amarela, imagem número 4).

Depois de definir o procedure que executa um passo de cada vez, eu alterei o procedure que executa estes passos. Eu acredito que a partir deste ponto as coisas começarão a fazer um pouco mais de sentido:

procedure ConfigurarPostgreSQL;
begin
  if IsComponentSelected('PostgreSQL') and IsTaskSelected('PGConfig') then
  begin
    PanelPostgreSQLStatus.Visible := True;

    ExecuteStep(1
               ,'1. Parando o serviço do PostgreSQL...' 
               ,'{sys}\net.exe'
               ,'stop "' + PGConfiguration('servicename') + '"'
               ,True);
    
    Memo1.Lines.Add('----------------------');
    
    ExecuteStep(2
               ,'2. Removendo o serviço do PostgreSQL...'
               ,'{app}\pg\bin\pg_ctl.exe'
               ,'unregister -N "' + PGConfiguration('servicename') + '"'
               ,True);
    
    Memo1.Lines.Add('----------------------');
    
    ExecuteStep(3
               ,'3. Excluindo os arquivos da pasta "data"...'
               ,'{sys}\cmd.exe'
               ,'/C del /q "{app}\pg\data\*" && for /d %x in ("{app}\pg\data\*") do @rd /s /q "%x"'
               ,True);
    
    Memo1.Lines.Add('----------------------');

    ExecuteStep(4
               ,'4. Inicializando a pasta "data"...' 
               ,'{app}\pg\bin\initdb.exe'
               ,'-U "' + PGConfiguration('username') + '" -A password -E utf8 -D "{app}\pg\data" --pwfile="{tmp}\senha.xyz"'
               ,False);

    Memo1.Lines.Add('----------------------');

    ExecuteStep(5
               ,'5. Registrando o serviço do PostgreSQL...'
               ,'{app}\pg\bin\pg_ctl.exe'
               ,'register -N "' + PGConfiguration('servicename') + '" -U "NT AUTHORITY\NetworkService" -D "{app}\pg\data" -w -o "-p ' + PGConfiguration('port') + '"'
               ,False);
    
    Memo1.Lines.Add('----------------------');

    ExecuteStep(6
               ,'6. Iniciando o serviço do PostgreSQL...'
               ,'{sys}\net.exe'
               ,'start "' + PGConfiguration('servicename') + '"'
               ,False);

//    Memo1.Lines.SaveToFile(ExpandConstant('{src}\PGConfig.txt'));
  end;
end;

Como se pode observar, este procedure tenta executar os seis passos de configuração do PostgreSQL, caso a configuração do mesmo tenha sido selecionada durante a instalação (linha 3). A versão anterior deste procedure executava apenas 3, mas eu resolvi incluir mais 3 passos que são executados antes dos outros 3 originais para remover o PostgreSQL antes de configurar ele novamente, minimizando a chance de erros de configuração[2].

A linha 5 faz aparecer nosso TPanel com as imagens e os textos que criamos no ISFD anteriormente. Da forma como criamos este TPanel, ele vai aparecer ocupando toda a área da tela de progresso de instalação, cobrindo assim o label de status e a barra de progresso que lá existem. Não é necessário ocultar este TPanel, porque quando a configuração do PostgreSQL termina, a próxima página do instalador é automaticamente carregada pelo Inno Setup.

As linhas 7, 15, 23, 31, 39 e 47 são chamadas ao procedure ExecuteStep. Note que em cada uma delas utilizamos um primeiro parâmetro numérico incremental (de 1 a 6) que indica o passo sendo executado. Isso, na prática, informa qual das seis imagens deve ser manipulada durante a execução do comando. Vale ressaltar também que nas 3 primeiras chamadas, o último parâmetro é true, o que indica que erros decorrentes da execução destes 3 comandos devem exibir uma imagem diferente para indicar um aviso (a imagem de linha horizontal amarela). Essa imagem alternativa vai aparecer, por exemplo, quando configuramos o PostgreSQL pela primeira vez usando nosso instalador. Caso não haja uma instância anterior do PosgreSQL usando o mesmo nome de serviço, invariavelmente os passos 1 e 2 vão falhar, mas isso é esperado e não é grave, por isso exibimos uma imagem de falha diferente.


Alterando a tela de progresso de desinstalação

Neste ponto nós já realizamos todas as operações necessárias para que as imagens apareçam na tela de progresso de instalação, mas e quanto a tela de progresso de desinstalação? É claro que a alterei e isso foi muito semelhante àquilo que foi feito até aqui. O resultado final da tela de desinstalação pode ser visto abaixo:

Dentro do RAR anexado a artigo você encontra o arquivo PGDesconfigStatus.isf, que é o "esboço" gerado no ISFD daquilo que deve ser posto na tela de progresso de desinstalação. Abra-o para ver que ele contém um TPanel, dois TBitmapImage e dois TLabel. Você vai notar também que aquilo que existe no arquivo do ISFD está posicionado de forma diferente daquilo que é exibido. Isso é esperado, porque eu alterei o posicionamento via código para melhorar a visualização.

Como os passos para modificação da tela de progresso de desinstalação são muito semelhantes àqueles usados para modificar a tela de progresso de instalação eu vou poupar você, caro leitor, de ter que ler as mesmas explicações já apresentadas anteriormente, ao invés disso, vou direto ao ponto e dizer o que eu fiz.

Para começar, eu colei as Custom Messages que o ISFD gerou, na seção CustomMessges do script, abaixo das outras strings existentes. São apenas duas strings (PGDesconfigStatus_UninstallLabel1_Caption0 e PGDesconfigStatus_UninstallLabel2_Caption0), por isso nem vou mostrar isso numa imagem. Em seguida, eu colei as variáveis que o ISFD gerou, no Pascal Script, logo abaixo de outras declarações semelhantes, tal como pode ser visto abaixo:

Estas são as variáveis declaradas para cada um dos componentes que a nossa modificação provê. Depois disso eu alterei o código de criação dos componentes que foi gerado pelo ISFD, tal como eu fiz com o código que ele gerou para a tela de progresso de instalação. O resultado foi o procedure PGDesconfigStatus_CreatePage, que pode ser visto abaixo:

procedure PGDesconfigStatus_CreatePage;
begin
  { UninstallPanelPostgreSQLStatus }
  UninstallPanelPostgreSQLStatus := TPanel.Create(UninstallProgressForm);
  with UninstallPanelPostgreSQLStatus do
  begin
    Parent := UninstallProgressForm.InstallingPage;
    Left := UninstallProgressForm.ProgressBar.Left;
    Top := UninstallProgressForm.ProgressBar.Top;
    Width := UninstallProgressForm.ProgressBar.Width;
    Height := ScaleY(46);
    BevelInner := bvLowered;
    TabOrder := 0;
    Visible := True;
    ParentBackground := False;
    Color := clInfoBk;
  end;
  
  { UninstallBitmapImage1 }
  UninstallBitmapImage1 := TBitmapImage.Create(UninstallProgressForm);
  with UninstallBitmapImage1 do
  begin
    Parent := UninstallPanelPostgreSQLStatus;
    Left := ScaleX(6);
    Top := ScaleY(6);
    Width := ScaleX(16);
    Height := ScaleY(16);
    Visible := False;
  end;
  
  { UninstallLabel1 }
  UninstallLabel1 := TLabel.Create(UninstallProgressForm);
  with UninstallLabel1 do
  begin
    Parent := UninstallPanelPostgreSQLStatus;
    Caption := ExpandConstant('{cm:PGDesconfigStatus_UninstallLabel1_Caption0}');
    Left := ScaleX(26);
    Top := ScaleY(7);
    Width := ScaleX(160);
    Height := ScaleY(13);
  end;

  { UninstallBitmapImage2 }
  UninstallBitmapImage2 := TBitmapImage.Create(UninstallProgressForm);
  with UninstallBitmapImage2 do
  begin
    Parent := UninstallPanelPostgreSQLStatus;
    Left := ScaleX(6);
    Top := ScaleY(24);
    Width := ScaleX(16);
    Height := ScaleY(16);
    Visible := False;
  end;
  
  { UninstallLabel2 }
  UninstallLabel2 := TLabel.Create(UninstallProgressForm);
  with UninstallLabel2 do
  begin
    Parent := UninstallPanelPostgreSQLStatus;
    Caption := ExpandConstant('{cm:PGDesconfigStatus_UninstallLabel2_Caption0}');
    Left := ScaleX(26);
    Top := ScaleY(25);
    Width := ScaleX(177);
    Height := ScaleY(13);
  end;
end;

Dentro do código acima, a única coisa que merece uma explicação é a forma como eu estou acessando a tela de desinstalação. Ao contrário da página de status de instalação, que é obtida por meio da função PageFromId, durante a desinstalação não temos este recurso, porque existe uma variável global que contém a instância do form de desinstalação, trata-se de UninstallProgressForm. Com esta variável podemos acessar a página InstallingPage, a qual é configurada como Parent do nosso TPanel e dessa forma garantimos que todos os nossos componentes apareçam nos seus devidos lugares.


Com o intuito de facilitar a exibição das imagens na tela de desinstalação eu criei o procedure ChangeUninstallStatusImage, visto abaixo:

procedure ChangeUninstallStatusImage(AStep, AImage: Byte);
begin
  case AStep of
    1: begin 
      UninstallBitmapImage1.ReplaceColor := clFuchsia;
      UninstallBitmapImage1.ReplaceWithColor := clInfoBk;
      case AImage of                                                 
        1: UninstallBitmapImage1.Bitmap.LoadFromFile(ExpandConstant('{app}\UninstallResources\Seta.bmp'));
        2: UninstallBitmapImage1.Bitmap.LoadFromFile(ExpandConstant('{app}\UninstallResources\Sim.bmp'));
        3: UninstallBitmapImage1.Bitmap.LoadFromFile(ExpandConstant('{app}\UninstallResources\Nao.bmp'));
      end;
      UninstallBitmapImage1.Visible := AImage > 0;
    end;
    2: begin
      UninstallBitmapImage2.ReplaceColor := clFuchsia;
      UninstallBitmapImage2.ReplaceWithColor := clInfoBk;
      case AImage of
        1: UninstallBitmapImage2.Bitmap.LoadFromFile(ExpandConstant('{app}\UninstallResources\Seta.bmp'));
        2: UninstallBitmapImage2.Bitmap.LoadFromFile(ExpandConstant('{app}\UninstallResources\Sim.bmp'));
        3: UninstallBitmapImage2.Bitmap.LoadFromFile(ExpandConstant('{app}\UninstallResources\Nao.bmp'));
      end;
      UninstallBitmapImage2.Visible := AImage > 0;
    end;
  end;
end;

Este procedure é bem semelhante à sua contraparte utilizada durante a instalação (ChangeStatusImage), a maior diferença fica por conta de onde estão as imagens que estão sendo carregadas. Durante a instalação as imagens precisaram ser extraídas na pasta temporária do Inno Setup. Como estamos desinstalando, não temos mais acesso aos arquivos a partir do desinstalador, porque ele simplesmente não carrega consigo nenhum arquivo. Ao invés disso, estamos carregando as imagens a partir de um local preexistente e conhecido: o diretório de instalação ({app}). Lá no início do artigo incluímos alguns arquivos adicionais na seção files. Alguns destes arquivos (não todos) foram instalados pelo nosso instalador na pasta {app}\UninstallResources, porque nós precisaríamos deles durante a desinstalação. O nome da pasta (UninstallResources) é absolutamente irrelevante. Use o nome que você quiser ou siga meu exemplo.

Novamente, a fim de facilitar a minha vida, eu criei um procedure para executar um passo de desconfiguração e exibir as imagens corretamente, trata-se do procedure ExecuteUninstallStep, visto a seguir:

procedure ExecuteUninstallStep(AStepNumber: Cardinal; ACommand, AParameters, ACurrentDir: String);
var
  ResultCode: Integer;
begin
  ChangeUninstallStatusImage(AStepNumber,1);

  Exec(ExpandConstant(ACommand)
      ,ExpandConstant(AParameters)
      ,ExpandConstant(ACurrentDir)
      ,SW_HIDE
      ,ewWaitUntilTerminated
      ,ResultCode);

  if ResultCode = 0 then
    ChangeUninstallStatusImage(AStepNumber,2)
  else
    ChangeUninstallStatusImage(AStepNumber,3);
end;

Como se pode ver, ele é muito semelhante ao seu antagonista (ExecuteStep), utilizado durante a instalação. A diferença é que aqui eu estou usando a função padrão do Inno Setup Exec para executar os comandos de parada do serviço do PostgreSQL e sua posterior remoção da lista de serviços do Windows. Eu não precisei (e nem quis) executar ExecuteAndCaptureOutput, pois eu não pretendia, durante a desinstalação, exibir qualquer saída de console.

O outro procedure que precisou ser modificado, foi o procedure DesconfigurarPostgreSQL, o qual passou a ser assim:

procedure DesconfigurarPostgreSQL;
begin
  if ComponentWasInstalled('PostgreSQL') and TaskWasPerformed('PGConfig') then
  begin
    ExecuteUninstallStep(1
                        ,'net.exe'
                        ,'stop "' + PGConfiguration('servicename') + '"'
                        ,'{sys}');

    ExecuteUninstallStep(2
                        ,'pg_ctl.exe'
                        ,'unregister -N "' + PGConfiguration('servicename') + '"'
                        ,'{app}\pg\bin');

    UninstallPanelPostgreSQLStatus.Visible := False;
  end;
end;

As modificações foram bem óbvias e consistiram na troca das chamadas diretas a função Exec do Inno Setup por chamadas ao nosso procedure ExecuteUninstallStep. Houve outras alterações mínimas que você mesmo pode observar. O procedure DesconfigurarPostgreSQL é bem parecido com o procedure ConfigurarPostgreSQL, portanto não há muito mais o que explicar, a não ser que, ao contrário deste último, o TPanel que contém as imagens e os textos de status é ocultado ao final, pois o procedure DesconfigurarPostgreSQL é executado sempre ANTES da desinstalação propriamente dita começar, logo, é natural que o ocultamento do TPanel de Status só aconteça imediatamente antes da barra de progresso existente na tela de status de desinstalação começar a exibir o progresso.

Para finalizar as alterações foi necessário executar o procedure que cria os componentes na tela de progresso de desinstalação (PGDesconfigStatus_CreatePage). Durante a instalação, os componentes e páginas adicionais são criados no procedure de evento InitializeWizard. Durante a desinstalação esse procedure não existe, mas existe sua contraparte InitializeUninstallProgressForm:

procedure InitializeUninstallProgressForm();
begin
  if ComponentWasInstalled('PostgreSQL') and TaskWasPerformed('PGConfig') then
  begin
    // Cria os controles na página de status de desinstalação (Página personalizada #3)
    PGDesconfigStatus_CreatePage;
  end;
end;

Esse procedure é executado durante a inicialização da tela de progresso de desinstalação e é o local ideal para realizar alterações nesta tela. No nosso caso, as alterações nesta tela estão sendo feitas pelo procedure PGDesconfigStatus_CreatePage, que já foi mencionado anteriormente.

Testando, 1, 2, 3!

Após realizar todas as alterações você pode executar o instalador com a opção de configuração do PostgreSQL ativada para ver o progresso de instalação com imagens de status. Para ver o progresso de desinstalação e desconfiguração do PostgreSQL, simplesmente desinstale o programa :).

Se você tem dúvidas, não deixe de comentar com perguntas no final deste artigo, mas antes disso, não deixe de olhar o arquivo anexado, pois ele contém todos os arquivos necessários para um melhor entendimento. Na próxima parte do tutorial vou falar a respeito da assinatura do instalador. Até lá!


  Arquivos anexados  
Arquivo Descrição Tamanho Modificado em
Access this URL (https://sourceforge.net/projects/addicted2delphi/files/inno.rar) inno.rar Arquivos necessários para criação do instalador e do programa de exemplo (que será instalado). Este arquivo será atualizado à medida que o tutorial for sendo feito e ao final conterá a solução completa 0.1 KB 12/05/2018 às 00:22

1 É possível configurar vários níveis de compressão para os arquivos da seção Files do Inno Setup, bem como utilizar um modo de compressão chamado "sólido" que consegue uma maior compressão de dados, à custa de um consumo maior de memória durante a descompactação. Eu não falarei a respeito desse assunto. Sinta-se à vontade de explorar as possibilidades do Inno Setup por conta própria
2 Isso introduziu um problema grave, mas esperado: A instalação de um PostgreSQL em cima de outro preexistente vai remover todos os dados que porventura você tenha salvo em todos os esquemas providos pela instância anterior do mesmo! É evidente que isso é perigoso, mas eu só incluí estes 3 passos de desconfiguração e remoção do PostgreSQL anterior porque nosso script é um script de exemplo. Em seus scripts de produção você não deve fazer isso, mas se fizer, tome medidas para que o usuário não queira te matar, por exemplo, realizando um backup da pasta de dados do PostgreSQL antes de apagar seu conteúdo
Página anterior do tutorial atual Primeira página do tutoria altual Próxima página do tutorial atual