Inno Setup (Parte 11): Personalização Final (o "Road Map")

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

Finalmente chegamos na parte final deste tutorial e eu já vou pedindo desculpas pela demora para publicar. Não vou mentir, boa parte desse tempo de silêncio foi culpa da minha preguiça de escrever.

Algumas pessoas poderiam sugerir que eu fizesse vídeos, que são mais "simples" e atingem um número maior de pessoas (que tem preguiça de ler), mas a verdade é que, além de não ser tão simples como pensam, eu não tenho e nem quero adquirir uma câmera para isso. Fazer screencasts está fora de cogitação também, logo, vou continuar escrevendo enquanto meus dedos aguentarem.

Teaser

Se você não sabe o que é um RoadMap, eu deixo abaixo a imagem do projeto finalizado para te motivar a querer seguir os passos deste artigo (clique na imagem ou abra-a em uma nova aba para vê-la completamente).

Como se pode ver existem algumas novidades além do RoadMap e todas elas serão explicadas nos tópicos a seguir.

A tela de fundo

Vamos começar, claro, pela modificação mais simples. Se você foi curioso o suficiente, você já deve saber como habilitar a tela de fundo do instalador, no entanto, para quem está seguindo o tutorial à risca esta explicação é necessária.

A tela de fundo pode parecer um recurso fora de moda, mas a moda vem e vai, por isso, quem sabe você não relança esta moda, fazendo seus instaladores terem uma tela de fundo? Com a possibilidade de personalizar esta tela de fundo, com imagens, talvez você consiga dar uma roupagem moderna àquilo que até então era "coisa do passado".

Para utilizar uma tela de fundo, acesse o menu Project > Setup Options  e clique em Appearance, tal como na imagem a seguir:

A tela Setup Options vai aparecer com seção Appearance destacada. A seguir está a tela Setup Options / Apperarance:

Nesta tela, o grupo intitulado Background Window é onde se configura a tela de fundo. Marcando a opção Background window visible, irá fazer com que a tela de fundo apareça e adicionalmente irá habilitar algumas opções:


A imagem na tela de fundo

A imagem do teaser, mostrada anteriormente, exibe no canto inferior direito o logo do site. Este logo foi aplicado em cima do degradê da tela de fundo de forma suave e translúcida, tirando proveito do canal alpha existente no bitmap utilizado. Você pode usar qualquer bitmap (*.bmp), mas só vai conseguir um efeito interessante como o mostrado no teaser, se usar um bitmap especial, de 32 bits, que possui um canal alpha premultiplicado que identifica a transparência individual de cada pixel. A seguir uma ampliação do bitmap aplicado na tela de fundo:

Se você observar a sombra ao redor do logo, notará que bem perto do limiar entre a parte escura e a cor de fundo, há uma semi transparência. Caso não houvesse o canal alpha você só identificaria a cor escura, e em seguida a cor de fundo.

Foge ao escopo deste artigo mostrar como fazer um bitmap de 32 bits com canal alpha premultiplicado, mas vou dizer como eu fiz de forma bem básica para que algum entusiasta que trabalhe com edição de imagens possa se aventurar. Primeiramente, usando um editor de imagens capaz de trabalhar com PNG, eu criei a imagem do logo e apliquei o efeito de sombra translúcida, salvando em seguida como PNG normalmente e me certificando de ter salvo com o canal alpha. Em seguida, usando um programa chamado PixelFormer, eu importei o PNG criado no primeiro programa, e, sem alterar qualquer propriedade, eu exportei a imagem para o formato BMP. O programa então perguntou o formato e eu escolhi 32 bpp com a opção Premultiplied alpha marcada. Fazendo isso você agora tem um BMP com canal alpha e pode usar ele no seu instalador exatamente como eu fiz e com o mesmo tipo de visual suavizado

Para aplicar a imagem na tela de fundo do instalador será necessário realizar algumas mudanças no script de instalação. Para começar, na seção Files, inclua, no início, a linha que adiciona a imagem aos arquivos do instalador tal como pode ser visto na imagem a seguir:

Sim, eu sei que a imagem ficou cortada. Só a incluí para deixar bem claro que a referência a este arquivo precisa ser a primeira na lista de arquivos e o motivo disso eu vou explicar mais adiante. A linha sem cortes é esta:

Source: "recursos\zost240x135.bmp"; Flags: ignoreversion dontcopy noencryption

Os flags utilizados servem para dizer que o arquivo não possui versão (ignoreversion), que ele não deve ser copiado para o computador automaticamente durante a instalação (dontcopy) e que ele não deve ser criptografado (noencryption), sendo os dois últimos os mais importantes.


Usa-se dontcopy, pois a imagem aplicada a tela de fundo do instalador não faz parte dos arquivos instaláveis pelo mesmo, trata-se apenas de algo que o instalador usa durante sua execução e que deve ser excluído ao final. Usa-se noencryption porque a imagem precisa ser extraída do pacote de instalação ANTES do usuário informar a senha do instalador. Como nós estamos usando encriptação de arquivos (Project Options > Compiler Settings), os arquivos do instalador só serão acessíveis após o usuário informar a senha e se este flag não fosse utilizado não seria possível utilizar a imagem até que o usuário informasse uma senha válida na tela de solicitação de senha do instalador. Precisamos desta imagem desde o começo da execução do instalador!

Após adicionar a imagem na seção Files, precisamos usá-la, claro, e isso será feito dentro da função InitializeWizard existente na seção Code do script. Altere esta função de forma que ela fique exatamente como no exemplo a seguir:

procedure InitializeWizard();
begin
  // Cria e carrega a imagem a ser aplicada na tela de fundo do instalador
  with TBitmapImage.Create(MainForm) do
  begin
    Parent := MainForm;
    AutoSize := True;
    BackColor := clNone;
    Bitmap.AlphaFormat := afPremultiplied;
    ExtractTemporaryFile('zost240x135.bmp');
    Bitmap.LoadFromFile(ExpandConstant('{tmp}\zost240x135.bmp'));
    Left := MainForm.Width - Width;
    Top := MainForm.Height - Height - 4;
  end;
  // Cria a página personalizada #1
  PGConfigPageID := PGConfig_CreatePage(wpSelectTasks);
  // Cria os controles na página de status de instalação (Página personalizada #2)
  PGConfigStatus_CreatePage;
  // Cria os controles na página de seleção de componentes de instalação (Página personalizada #4)
  SetupTypeDescriptionPanelCreate;
end;

A linha 4 faz uso do with para criar uma instância de TBitmapImage que seja propriedade de MainForm, o qual será responsável por liberar a memória utilizada por esta instância. A linha 6 configura o componente pai de TBitmapImage como sendo justamente o MainForm, aliás, MainForm é a tela de fundo do instalador! A linha 7 instrui que TBitmapImage tenha seu tamanho automaticamente definido de acordo com a imagem que ele conterá. A linha 8 configura a cor de fundo de TBitmapImage de forma que não haja qualquer cor de fundo a ser usada (clNone). A linha 9 configura a propriedade AlphaFormat da propriedade Bitmap de TBitmapImage como afPremultiplied. Isso é necessário porque as APIs do Windows que o Inno Setup usa, requerem que o formato alpha seja este. A linha 10, extrai nossa imagem na pasta temporária do Inno Setup. A linha 11 finalmente carrega a imagem que foi extraída no TBitmapImage. As linhas 12 e 13, configuram o posicionamento da imagem, de forma que ela fique no canto inferior direito da tela de fundo.

Neste momento, se você executar o instalador verá que ele tem uma tela de fundo com um degradê e com o logo do site no canto inferior direito dela. O logo está aparecendo corretamente, mostrando o canal alpha translúcido porque você manteve a opção "Start maximized" marcada em Setup Options / Appearance. Eu realmente não sei porque esta opção precisa estar marcada para que o efeito desejado apareça. Caso queira ver o que acontece quando a opção fica desmarcada, sinta-se à vontade para testar. Caso você pretenda aplicar uma imagem sem o canal alpha , marcar ou desmarcar a opção "Start maximized" não fará diferença alguma.

Usando esta mesma técnica é possível cobrir toda a área da tela de fundo com uma imagem esticada, ou com uma imagem adequada a resolução da tela, desde que você realize alguns ajustes via Script Pascal para detectar a resolução de tela do usuário. Com um pouco mais de empenho, você pode até mesmo cobrir a tela de fundo com várias cópias da mesma imagem, aplicadas lado a lado (tile).

O "Beveled Label"

Descobri por acaso a existência do Beveled Label (label chanfrado ou em baixo relevo) enquanto usava técnicas para descobrir os elementos existentes nas telas do Wizard. Notei que existia um Label que eu não sabia para que servia e que não mostrava nada na tela do instalador. Ao pesquisar na web, descobri que trata-se de um label que fica exatamente em cima da linha inferior que separa a tela do instalador em duas; a parte de cima, com o conteúdo que muda e a parte de baixo, que mostra os botões do Wizard. Para explicar melhor, eis uma ampliação que mostra o beveled label:

Olhe a imagem do teaser atentamente para ver este label de forma contextualizada.

Sim, é um recurso sem muita expressão, eu admito, no entanto, não deixa de ser algo que já havia visto em outros instaladores e como um de meus objetivos com este tutorial é mostrar como o Inno Setup pode ser usado em substituição plena de qualquer outro método de instalação, nenhum detalhe pode passar despercebido e este, sem dúvida, é um desses detalhes.

Para habilitar o beveled label basta escrever algo nele, e para isso, simplesmente acesse o item Messages na árvore Sections do Inno Script Studio, clique em New Item e preencha a tela Message Properties tal como na imagem a seguir:

Anteriormente neste tutorial eu já falei a respeito de mensagens personalizadas (Custom Messages). De fato, se você acessar o item Custom Messages na árvore de itens Sections, você verá várias mensagens personalizadas. Como o próprio nome sugere, as mensagens personalizadas são como variáveis que o usuário define e que podem ser usadas para internacionalizar o instalador. Normalmente as mensagens personalizadas são usadas para definir textos estáticos existentes em telas personalizadas ou textos dinâmicos gerados na seção Code do script. O item Messages, por outro lado, contém textos do próprio instalador, textos estáticos e dinâmicos mostrados pelo Inno Setup e que podem ser customizados, inclusive, de acordo com o idioma, por exemplo, se você não gosta de um texto que está sendo exibido para o idioma japonês, você pode incluir um override (sobreposição) para este texto neste idioma e escrever aquilo que deveria ser o correto. Um override, é o nome que se dá a cada item incluído na seção Messages, pois, tal como foi feito para o Beveled Label, nós precisamos escolher o ID da mensagem predefinida para só assim escrever o texto. Observe que o campo Language Override fica em branco, porque o texto do Beveled Label deve aparecer sempre, independentemente do idioma escolhido. Se eu estivesse fazendo um instalador multilingue, eu deveria criar um override para cada idioma suportado e para isso eu deveria escolher um idioma no campo Language Override. Para facilitar as coisas eu mantive o campo em branco, de forma que, na inicialização do instalador, caso o usuário escolha o idioma "English", ainda assim o texto do Beveled Label apareça, mas o certo seria incluir dois overrides na seção Messages, um para o idioma Português (Brasil), com o texto em português e outro para o idioma English, com o texto em inglês.

Não vou entrar em mais detalhes sobre como utilizar a seção Messages. Você pode encontrar ajuda facilmente na ajuda online do Inno Setup ou na web. Fica como lição de casa pra você :)

 


Após preencher os dados na tela Message Properties, clique OK para confirmar e o item BeveledLabel vai aparecer na lista de mensagens. Execute o instalador para ver o efeito. Você vai perceber que tem algo diferente. Na minha imagem do teaser e na imagem da ampliação vista anteriormente o texto do Beveled Label aparece alinhado à direita, ficando em cima dos botões do instalador, mas o que foi conseguido foi que o texto ficasse alinhado à esquerda. Por padrão o Beveled Label aparece à esquerda mesmo mas para que ele seja alinhado à direita, basta incluir uma linha no final da função InitializeWizard, a qual agora ficará assim:

procedure InitializeWizard();
begin
  // Cria e carrega a imagem a ser aplicada na tela de fundo do instalador
  with TBitmapImage.Create(MainForm) do
  begin
    Parent := MainForm;
    AutoSize := True;
    BackColor := clNone;
    Bitmap.AlphaFormat := afPremultiplied;
    ExtractTemporaryFile('zost240x135.bmp');
    Bitmap.LoadFromFile(ExpandConstant('{tmp}\zost240x135.bmp'));
    Left := MainForm.Width - Width;
    Top := MainForm.Height - Height - 4;
  end;
  // Cria a página personalizada #1
  PGConfigPageID := PGConfig_CreatePage(wpSelectTasks);
  // Cria os controles na página de status de instalação (Página personalizada #2)
  PGConfigStatus_CreatePage;
  // Cria os controles na página de seleção de componentes de instalação (Página personalizada #4)
  SetupTypeDescriptionPanelCreate;
  // Ajusta o BeveledLabel para que ele fique alinhado do lado direito da tela 
  // do wizard
  WizardForm.BeveledLabel.Left := WizardForm.Width - WizardForm.BeveledLabel.Width - 4;
end;

WizardForm é a janela principal do Wizard, aquela que possui os botões Anterior, Próximo e Cancelar, bem como o conteúdo do instalador propriamente dito. O código de alinhamento usado dispensa explicações, é muito simples. Execute novamente o instalador e note que o texto do Beveled Label agora está alinhado à direita :)

O "Road Map" (renderização básica)

Finalmente chegamos na parte mais complexa deste artigo. Acredito que esta seja a parte mais complexa de todo o tutorial, pois envolve manipulação de imagens (posição, tamanho, etc.) e de componentes criados dinamicamente, algo que pouca gente costuma fazer ou gostar, mas o esforço vai valer a pena com certeza.

Aos puristas e entendidos de todas as coisas, gostaria de falar a respeito do nome "road map": eu não sou super inteligente a ponto de conhecer todas as coisas que me cercam, sou um mero programador focado em resultados e não em assuntos intermediários de pouca importância. O que eu estou chamando de road map pode ser visto na imagem a seguir:

Aquilo que eu chamo de road map nada mais é do que um indicador visual de etapas de instalação, coisa que existia até mesmo no instalador do Delphi e muitos outros. Se isso tem outro nome peço desculpas, mas não vou mudar o artigo. Ele vai ficar conhecido aqui como road map e ponto final.

Bom, chega de explicações desnecessárias, vamos ao que interessa. Para começar precisamos incluir todas as imagens que serão utilizadas pelo Road Map (RM) na lista de arquivos do instalador. Estas imagens serão incluídas da mesma forma que a imagem da tela de fundo, por isso vou suprimir maiores detalhes sobre seus flags, já que isso já foi coberto anteriormente. No início da seção Files,  inclua as seguintes linhas:

Source: "recursos\RoadMapBackground.bmp"; Flags: ignoreversion dontcopy noencryption
Source: "recursos\RoadMapTop.bmp"; Flags: ignoreversion dontcopy noencryption
Source: "recursos\RoadMapRoad.bmp"; Flags: ignoreversion dontcopy noencryption
Source: "recursos\RoadMapBottom.bmp"; Flags: ignoreversion dontcopy noencryption
Source: "recursos\RoadMapStepUndone.bmp"; Flags: ignoreversion dontcopy noencryption
Source: "recursos\RoadMapStepDone.bmp"; Flags: ignoreversion dontcopy noencryption
Source: "recursos\RoadMapSkippedStep.bmp"; Flags: ignoreversion dontcopy noencryption
Source: "recursos\RoadMapCurrentStep.bmp"; Flags: ignoreversion dontcopy noencryption
Source: "recursos\RoadMapInstallingStep.bmp"; Flags: ignoreversion dontcopy noencryption

Cada uma dessas linhas instrui o Inno Setup a incluir cada um dos arquivos indicados no pacote de instalação, de forma que, durante o decorrer da instalação estes arquivos possam ser usados apenas pelo instalador. Cada uma destas imagens representa uma parte do RM, o qual é construído dinamicamente e é capaz de se ajustar a quantidade de passos de instalação que são definidos em uma função específica que será vista posteriormente neste artigo. A capacidade de construção dinâmica do RM torna ele bem flexível quanto a quantidade de passos que ele pode exibir, por exemplo, a imagem a seguir mostra o RM com apenas 5 passos:

Para desenhar este RM foi necessário apenas incluir os nomes de apenas 5 passos na sua função de criação, a qual fará todo trabalho duro. No exemplo atual, explicado neste tutorial, um máximo de 9 (nove) passos podem ser incluídos no RM apenas por conta do espaço vertical disponível, pois eu não aumentei a altura da janela do wizard do instalador. A quantidade máxima de passos no RM é definida apenas pela quantidade de espaço disponível para desenhá-los. Se você aumentar o tamanho vertical da janela do wizard, você poderá incluir mais passos sem que a imagem apareça cortada. Você poderá também alterar a função de renderização do RM a fim de usar imagens de passos menores e assim poder diminuir a distância entre eles. Esta é um tarefa mais avançada e que não será coberta neste tutorial, mas nada que um pouco de empenho e tempo livre não resolvam.


Na seção Code do script, inclua o seguinte trecho de código, imediatamente antes do bloco de declaração de variáveis existente:

type
  TStepId = (siStepUndone,siStepDone,siSkippedStep,siCurrentStep,siInstallingStep);

Este trecho de código define uma simples enumeração que identifica cada um dos tipos de passos disponíveis no RM. Em termos práticos, cada um destes tipos tem uma imagem associada. A utilização destes tipos evita ter que usar nomes de imagens diretamente, ou seja, se em algum momento eu precisar renderizar siCurrentStep eu saberei que devo carregar a imagem correspondente a um passo que está sendo executado neste exato momento, ou, mais especificamente, a tela do wizard que está sendo exibida no momento.

Ainda na seção Code, ao final do bloco de declaração de variáveis, inclua as seguintes variáveis de suporte ao RM:

BevelRoadMap: TBevel;
ImageRoadMapBackground: TBitmapImage;
ImageRoadMapTop: TBitmapImage;
ImageRoadMapRoad: TBitmapImage;
ImageRoadMapBottom: TBitmapImage;
ArrayRoadMapSteps: array of TBitmapImage;

Cada uma dessas variáveis vai representar uma característica específica do RM. BevelRoadMap é um simples TBevel que serve para separar o RM do resto da janela do wizard. Ele será colocado verticalmente ao lado direito do RM. ImageRoadMapBackground é uma imagem de 1 pixel de altura que tem um degradê horizontal (de preto até "teal"). Essa imagem, pode ser esticada verticalmente a fim de cobrir toda a área do RM, de forma que seja possível criar um RM com qualquer altura. ImageRoadMapTop é a parte superior do RM, a qual contém o circulo branco com um triângulo verde dentro. Esta imagem será colocada sempre na posição (0,0) do RM. ImageRoadMapRoad é uma imagem de 1 pixel de altura que representa um pedaço da linha vertical do RM. Tal como a imagem de fundo, ela pode ser esticada verticalmente, se ajustando assim a qualquer quantidade de passos sendo renderizados. ImageRoadMapBottom é a parte inferior do RM, a qual contém o círculo branco com um quadrado vermelho dentro. Esta imagem será colocada sempre na posição (0,h1-h2), onde h1 representa a altura do RM e h2 representa a altura de ImageRoadMapBottom. ArrayRoadMapSteps é um array que conterá tantos TBitmapImage quantos forem os passos a renderizar. Ele é usado por uma outra função que fará a troca das imagens dos passos e será carregado no procedure CreateRoadMap (veja a seguir).

Mais adiante, após a função PrepareToInstall, inclua a seguinte função nova:

procedure CreateRoadMap(ASteps: array of string);
var
  RoadMapWidth: Byte;
  BevelRoadMapWidth: Byte;
  Spacement: Byte;
  i: Byte;
begin
  // Extraindo as imagens utilizadas pelo Road Map
  ExtractTemporaryFile('RoadMapBackground.bmp');
  ExtractTemporaryFile('RoadMapTop.bmp');
  ExtractTemporaryFile('RoadMapRoad.bmp');
  ExtractTemporaryFile('RoadMapBottom.bmp');
  ExtractTemporaryFile('RoadMapStepUndone.bmp');
  ExtractTemporaryFile('RoadMapStepDone.bmp');
  ExtractTemporaryFile('RoadMapSkippedStep.bmp');
  ExtractTemporaryFile('RoadMapCurrentStep.bmp');
  ExtractTemporaryFile('RoadMapInstallingStep.bmp');
  
  Spacement := 29;
  RoadMapWidth := 164;
  BevelRoadMapWidth := 2;
  
  WizardForm.Width := WizardForm.Width + RoadMapWidth + BevelRoadMapWidth;
  WizardForm.Left := WizardForm.Left - (RoadMapWidth + BevelRoadMapWidth) div 2;
  WizardForm.Bevel.Width := WizardForm.Bevel.Width + (RoadMapWidth + BevelRoadMapWidth);
  WizardForm.CancelButton.Left := WizardForm.CancelButton.Left + (RoadMapWidth + BevelRoadMapWidth);
  WizardForm.NextButton.Left := WizardForm.NextButton.Left + (RoadMapWidth + BevelRoadMapWidth);
  WizardForm.BackButton.Left := WizardForm.BackButton.Left + (RoadMapWidth + BevelRoadMapWidth);
  WizardForm.OuterNotebook.Left := WizardForm.OuterNotebook.Left + (RoadMapWidth +BevelRoadMapWidth);
  
  BevelRoadMap := TBevel.Create(WizardForm);
  BevelRoadMap.Parent := WizardForm;
  BevelRoadMap.Top := 0;
  BevelRoadMap.Shape := bsLeftLine;
  BevelRoadMap.Width := BevelRoadMapWidth;
  BevelRoadMap.Height := WizardForm.OuterNotebook.Height;
  BevelRoadMap.Left := RoadMapWidth;

  ImageRoadMapBackground := TBitmapImage.Create(WizardForm);
  ImageRoadMapBackground.Parent := WizardForm;
  ImageRoadMapBackground.Bitmap.LoadFromFile(ExpandConstant('{tmp}\RoadMapBackground.bmp'));
  ImageRoadMapBackground.Top := 0;
  ImageRoadMapBackground.Left := 0;
  ImageRoadMapBackground.Height := BevelRoadMap.Height;
  ImageRoadMapBackground.Width := RoadMapWidth;
  ImageRoadMapBackground.Stretch := True;

  ImageRoadMapTop := TBitmapImage.Create(WizardForm);
  ImageRoadMapTop.Parent := WizardForm;
  ImageRoadMapTop.Bitmap.LoadFromFile(ExpandConstant('{tmp}\RoadMapTop.bmp'));
  ImageRoadMapTop.Top := 0;
  ImageRoadMapTop.Left := 0;
  ImageRoadMapTop.AutoSize := True;

  ImageRoadMapRoad := TBitmapImage.Create(WizardForm);
  ImageRoadMapRoad.Parent := WizardForm;
  ImageRoadMapRoad.Bitmap.LoadFromFile(ExpandConstant('{tmp}\RoadMapRoad.bmp'));
  ImageRoadMapRoad.Top := 31;
  ImageRoadMapRoad.Left := 37;
  ImageRoadMapRoad.Height := 18 + ((Length(ASteps) - 1) * Spacement);
  ImageRoadMapRoad.Width := 4;
  ImageRoadMapRoad.Stretch := True;

  ImageRoadMapBottom := TBitmapImage.Create(WizardForm);
  ImageRoadMapBottom.Parent := WizardForm;
  ImageRoadMapBottom.Bitmap.LoadFromFile(ExpandConstant('{tmp}\RoadMapBottom.bmp'));
  ImageRoadMapBottom.Top := 49 + ((Length(ASteps) - 1) * Spacement);
  ImageRoadMapBottom.Left := 0;
  ImageRoadMapBottom.AutoSize := True;

  SetLength(ArrayRoadMapSteps,Length(ASteps));

  for i := 0 to High(ASteps) do
  begin
    ArrayRoadMapSteps[i] := TBitmapImage.Create(WizardForm);
    ArrayRoadMapSteps[i].Parent := WizardForm;
    ArrayRoadMapSteps[i].Bitmap.LoadFromFile(ExpandConstant('{tmp}\RoadMapStepUndone.bmp'));
    ArrayRoadMapSteps[i].Top := 25 + (i * Spacement);
    ArrayRoadMapSteps[i].Left := 24;
    ArrayRoadMapSteps[i].AutoSize := True;

    with TLabel.Create(WizardForm) do
    begin
      Parent := WizardForm;
      Caption := ASteps[i];
      Left := ArrayRoadMapSteps[i].Left + ArrayRoadMapSteps[i].Width;
      Font.Color := clWhite; 
      Font.Style := [fsBold];
      Font.Size := 7;
      Top := ArrayRoadMapSteps[i].Height div 2 - Height div 2 + ArrayRoadMapSteps[i].Top;
    end;
  end;
end;

Esta função é a responsável por ajustar o tamanho da janela do wizard e desenhar o RM. Ela recebe apenas um parâmero (ASteps), um open array de strings, onde cada string representa o nome de um passo a ser exibido no RM. Como foi dito anteriormente, esta função consegue renderizar corretamente qualquer quantidade de passos, o que significa que você pode adicionar quantas strings quiser neste parâmetro, contudo, o exemplo atualmente sendo construído só conseguirá exibir 9 passos sem cortes, por conta do tamanho vertical da tela do wizard, que não foi alterado.


A fim de entender melhor a explicação do procedure CreateRoadMap será necessário conhecer cada parte predefinida da janela do wizard:

Na imagem anterior, a área azul é o nosso RM, o qual será criado por CreateRoadMap, a área vermelha é conhecida como OuterNotebook e a área verde é a parte inferior da janela do wizard que contém apenas 3 botões. Esta área inferior não tem qualquer painel mas eu vou me referir a ela como "painel dos botões" para facilitar o entendimento. As linhas amarelas são os TBevel. O TBevel vertical é o BevelRoadMap, que criamos para separar o RM do OuterNotebook. O TBevel horizontal já existia na janela do Wizard e separa a parte de cima do "painel dos botões". A janela do wizard originalmente possui apenas a área vermelha e a área verde separadas pelo TBevel horizontal. Além disso, a área verde e o TBevel horizontal originalmente são da mesma largura da área vermelha. Se você entendeu tudo que eu expliquei neste parágrafo vai entender o que deve ser feito na janela do wizard para que ela comporte o Road Map!

Voltando a falar sobre o procedure CreateRoadMap, quero dizer que ele não é complicado, mas também não é trivial. Vou tentar explicar cada parte importante do mesmo. As linhas 3 a 5 declaram 3 variáveis que serão usadas pelo procedure de forma estática, como constantes de configuração geral do RM. Seus valores fixos estão sendo definidos nas linhas 19 a 21. Spacement é o espaço vertical em pixels entre cada uma das imagens dos passos. O valor atual (29) faz com que sejam renderizáveis sem cortes até nove passos no RM. Aumente ou diminua este valor para entender melhor seu efeito. RoadMapWidth é a largura do RM, que neste exemplo coincide com a largura do bitmap usado no fundo dele (ImageRoadMapBackground). Eu poderia ter detectado a largura deste bitmap de fundo e simplesmente usado o valor onde eu quisesse, sem precisar utilizar uma variável para isso, no entanto isso faria com que o RM sempre precisasse de uma imagem de fundo, o que não seria bom. No exemplo deste tutorial eu estou usando sempre uma imagem de fundo com o intuito de mostrar o poder do Inno Setup, mas nada impediria que você não quisesse usar tal imagem e ajustasse o RM para funcionar de forma mais "limpa", neste caso, não haveria uma imagem para usar como referência e a variável seria essencial! BevelRoadMapWidth é a largura do TBevel que será usado como separador entre o RM e o resto da tela do Wizard. O valor atual (2) é perfeito para criar o efeito de uma linha tridimensional vertical básica. Aumente esse valor para entender melhor o efeito.

As linhas 23 a 29 fazem vários ajustes na tela principal do instalador (WizardForm). Inicialmente é aumentando seu tamanho horizontal para comportar o RM (linha 23) e alterado seu posicionamento horizontal (linha 24) para que a janela permaneça centralizada após o incremento horizontal. Nas linhas de 25 a 29, são feitos ajustes nos controles e componentes internos do WizardForm a saber:

As linhas 31 a 37 configuram o TBevel vertical que separa o RM do resto da janela do wizard. A maioria das propriedades são conhecidas nossas e já foram cobertas em outras partes desse tutorial quando falamos a respeito de telas personalizadas, nas quais os componentes são criados, posicionados e configurados dinamicamente. Quero apenas destacar a configurações de 3 propriedades:

As linhas 39 a 69 carregam as variáveis ImageRoadMapBackground, ImageRoadMapTop, ImageRoadMapRoad e  ImageRoadMapBottom com suas imagens correspondentes, as configuram e as posicionam nos seus locais adequados. Estes quatro blocos de código são muito parecidos, por isso não vou explicá-los um a um. Ao invés disso vou fazer as seguintes observações:

A linha 71 configura o tamanho do array de imagens ArrayRoadMapSteps, de forma que ele comporte a quantidade total de passos que serão renderizados (ASteps). 

As linhas 73 a 92 fazem o trabalho realmente legal do procedure CreateRoadMap; baseando-se na quantidade de passos informados no argumento ASteps, cada um deles será renderizado em uma posição específica dentro do RM. Como o objetivo do procedure CreateRoadMap é renderizar o RM básico, cada um dos passos será renderizado com sua descrição e uma imagem de um círculo vazio. No loop for, para cada elemento contido em ASteps, estão sendo realizados os seguintes passos:

O loop for se repete então para cada elemento do array passado como argumento do procedure CreateRoadMap até que ele seja completamente renderizado.

A última coisa a fazer para que o RM seja renderizado é executar o procedure CreateRoadMap, para isso, inclua a linha a seguir dentro da função InitializeWizard ANTES da linha que configura a posição horizontal do BeveledLabel:

CreateRoadMap(['Bem-vindo!','Licença','Senha de acesso','Informações','Itens a instalar','Tarefas a executar','Config. PostgreSQL','Pronto para instalar!','Instalando...']);

Esta linha precisa ser colocada antes da linha referente ao BeveledLabel por um motivo muito simples: o BeveledLabel usa a largura da janela do wizard como referência para se posicionar corretamente, e o procedure CreateRoadMap altera a largura da janela do wizard, portanto, a posição do BeveledLabel só deve ser definida depois que a janela do wizard estiver com seu tamanho final, configurado por CreateRoadMap.

Ao executar o instalador agora você já será capaz de ver o RM renderizado, mas vai notar que ele é estático, ou seja, não existem as imagens dos passos sendo alteradas de acordo com a etapa exibida no instalador:

Lembre-se que tudo que você viu até agora foi apenas para renderizar o RM básico. Eu disse que esse artigo seria um dos mais complexos e não foi à toa.


O "Road Map" (inteligência)

Até agora apenas vimos como renderizar o RM, mas ele não faz nada. Parte da inteligência dele fica por conta do procedure ChangeStepImage, o qual pode ser visto a seguir:

procedure ChangeStepImage(AIndex: Byte; AStepId: TStepId);
begin
  case AStepId of
    siStepUndone: ArrayRoadMapSteps[AIndex].Bitmap.LoadFromFile(ExpandConstant('{tmp}\RoadMapStepUndone.bmp'));
    siStepDone: ArrayRoadMapSteps[AIndex].Bitmap.LoadFromFile(ExpandConstant('{tmp}\RoadMapStepDone.bmp'));
    siSkippedStep: ArrayRoadMapSteps[AIndex].Bitmap.LoadFromFile(ExpandConstant('{tmp}\RoadMapSkippedStep.bmp'));
    siCurrentStep: ArrayRoadMapSteps[AIndex].Bitmap.LoadFromFile(ExpandConstant('{tmp}\RoadMapCurrentStep.bmp'));
    siInstallingStep: ArrayRoadMapSteps[AIndex].Bitmap.LoadFromFile(ExpandConstant('{tmp}\RoadMapInstallingStep.bmp'));
  end;
end;

Como se pode ver, este é um procedure bem simples, o que ele faz é alterar a imagem de um dos elementos do array ArrayRoadMapSteps (Identificado por AIndex). A seleção da imagem é feita no case, usando como seletor AStepId. Em outras palavras, a imagem contida em ArrayRoadMapSteps[AIndex] será carregada com o bitmap correspondente ao "tipo de passo" identificado por AStepId. Coloque este procedure antes do procedure CurPageChanged, o qual será implementado mais adiante, porque antes de continuar eu preciso abrir um parêntese para falar sobre uma alteração necessária.

O RM que eu implementei é muito simples, pois há um item para cada passo de instalação exibido pelo instalador, e isso inclui uma página personalizada totalmente nova, a página de configuração do PostgreSQL. Se você prestou atenção nas imagens do RM que estão aqui no artigo você verá que existe um item chamado Config. PostgreSQL, o qual corresponde a nossa página personalizada para configuração do PG. Até agora esta página era simplesmente exibida pelo instalador e não havíamos precisado interagir com ela de nenhuma forma, contudo, agora que o RM existe, precisamos de uma forma de identificar todas as páginas, incluindo qualquer página adicional que tenhamos criado. As páginas padrão do Inno Setup possuem identificadores constantes, tais como, wpSelectComponents ou wpWelcome, portanto, para estas páginas nada precisa ser feito a não ser ler a ajuda do Inno Setup para saber qual o identificador de cada uma das páginas.

Para páginas personalizadas totalmente novas, entretanto, não existe um identificador constante, o que existe é um identificador que é dado dinamicamente a cada página personalizada criada com a função CreateCustomPage. Dê uma olhada na função PGConfig_CreatePage, e veja que, no final dela, estamos retornando Page.ID, ou seja, esta função, ao ser executada, já retorna o ID da página de configuração do PostgreSQL! Como vamos precisar desta identificação dentro do procedure CurPageChanged (mais adiante), precisaremos criar uma variável na seção de variáveis do Pascal Script, portanto, crie agora uma variável de nome PGConfigPageID, do tipo Integer na seção de variáveis. Para concluir esta alteração, vá até o procedure InitializeWizard e, na linha que executa a função PGConfig_CreatePage capture seu resultado utilizando a variável PGConfigPageID. Ao fazer isso, teremos nesta variável o ID da página de configuração do PostgreSQL, o qual poderá ser usado em qualquer lugar do script.

O procedure CurPageChanged é um procedure do próprio Inno Setup que é executado sempre após uma página do instalador ser exibida. O identificador da página que acabou de ser exibida é o valor do parâmetro CurPageID. Como cada passo do RM corresponde a uma página do instalador, será dentro deste procedure que executaremos o procedure ChangeStepImage, definido anteriormente, para alterar a imagem do RM correspondente a página que acabou de ser exibida. A definição deste procedure segue:

procedure CurPageChanged(CurPageID: Integer);
begin      
  case CurPageID of
    wpWelcome: ChangeStepImage(0,siCurrentStep);
    wpLicense: ChangeStepImage(1,siCurrentStep);
    wpPassword: ChangeStepImage(2,siCurrentStep);
    wpInfoBefore: ChangeStepImage(3,siCurrentStep);
    wpSelectComponents: ChangeStepImage(4,siCurrentStep);
    wpSelectTasks: ChangeStepImage(5,siCurrentStep);
    PGConfigPageID: ChangeStepImage(6,siCurrentStep);
    wpReady: ChangeStepImage(7,siCurrentStep);
    wpInstalling: ChangeStepImage(8,siInstallingStep);
    wpInfoAfter: ChangeStepImage(8,siStepDone);
  end;
end;

Tal como o procedure anterior a implementação de CurPageChanged é muito simples e dispensa uma explicação detalhada. Basicamente, de acordo com a página que se está exibindo, será executado ChangeStepImage, com o primeiro parâmetro sendo o índice no array ArrayRoadMapSteps que corresponde a página em questão, por exemplo, para a página identificada por wpWelcome foi executado ChangeStepImage(0,siCurrentStep), isso significa que o elemento zero de ArrayRoadMapSteps terá a imagem de passo atual carregada. O elemento zero de ArrayRoadMapSteps corresponde ao primeiro item passado no open array do procedure CreateRoadMap, o qual foi definido como "Bem-vindo!", tal como pode ser visto na chamada da função em InitializeWizard.

Se você executar o instalador agora vai notar que as coisas ainda não funcionam como deveria. Ao pressionar "Avançar" a imagem correspondente do passo atual mudará para a imagem de "passo atual", contudo, os passos anteriores permanecerão com a mesma imagem e ao retornar (pressionando o botão "Voltar") nada parecerá acontecer. Como eu disse anteriormente o procedure CurPageChanged é apenas parte da inteligência do RM. Ele é apenas responsável por exibir a imagem de passo atual, ele não atua na exibição de qualquer outra imagem. Isso seria deduzível observando que todas as chamadas a ChangeStepImage contidas nele utilizam em seu segundo parâmetro a constante siCurrentStep. Para que o RM funcione plenamente, precisaremos implementar duas outras funções do Inno Setup: NextButtonClickBackButtonClick.

A função NextButtonClick é executada sempre que se pressiona o botão "Avançar" e seu parâmetro CurPageID identifica a página sendo exibida no instalador quando o botão "Avançar" foi pressionado. Em outras palavras se estamos na página wpWelcome e pressionarmos o botão "Avançar", NextButtonClick será executado e CurPageID = wpWelcome. É muito importante entender este comportamento do parâmetro CurPageID para entender a implementação de NextButtonClick, vista a seguir:

function NextButtonClick(CurPageID: Integer): Boolean;
begin
  case CurPageID of
    wpWelcome: ChangeStepImage(0,siStepDone);
    wpLicense: ChangeStepImage(1,siStepDone);
    wpPassword: ChangeStepImage(2,siStepDone);
    wpInfoBefore: ChangeStepImage(3,siStepDone);
    wpSelectComponents: ChangeStepImage(4,siStepDone);
    wpSelectTasks: begin
      ChangeStepImage(5,siStepDone);
      if not IsTaskSelected('PGConfig') then
        ChangeStepImage(6,siSkippedStep);
     end;
    PGConfigPageID: ChangeStepImage(6,siStepDone);
    wpReady: ChangeStepImage(7,siStepDone);
  end;
  Result := True;
end;

(Coloque este código após a implementação do procedure ChangeStepImage)

Como dito anteriormente, CurPageID identifica a página sendo exibida pelo instalador no momento do pressionamento do botão "Avançar", ou seja, NextButtonClick acontece imediatamente antes da exibição da próxima página, identificando a página anterior! Fica claro, pois, que este é o local ideal para mudar a imagem de um passo que foi concluído. A função funciona basicamente da mesma forma que o procedure CurPageChanged, identificando a página e alterando sua imagem. As peculiaridades aqui ficam por conta da constante de imagem, que agora é siStepDone (imagem de passo concluído) e de um pequeno artifício utilizado para lidar com um passo opcional (a página de configuração do PostgreSQL).

A página de configuração do PostgreSQL é opcional de acordo com a seleção feita na página wpSelectTasks, a qual exibe um Check Box perguntando ao usuário se ele deseja configurar o PostgreSQL. A função NextButtonClick será executada com CurPageID = wpSelectTasks quando o usuário está na página de seleção de tarefas e pressiona o botão "Avançar". Neste momento, a etapa (página) de seleção de tarefas foi concluída, logo, executamos incondicionalmente ChangeStepImage(5,siStepUndone), que coloca a imagem de "passo concluído" no passo correspondente a página de seleção de tarefas. Depois da tela de seleção de tarefas, duas telas podem ser exibidas, segundo a opção do usuário. Caso o usuário tenha marcado a opção de configurar o PostgreSQL, a próxima tela será a tela de configuração do PostgreSQL. Caso o usuário não tenha marcado esta opção a tela a ser exibida será a última tela antes do início da instalação (wpReady). Ao sair da tela de seleção de tarefas, portanto, verificamos se a tarefa de configuração do PostgreSQL não será executada (if not IsTaskSelected('PGConfig') then) e neste caso, simplesmente configuramos a imagem da etapa correspondente a tela de configuração do PG como "etapa pulada" (ChangeStepImage(6,siSkippedStep)).


A função BackButtonClick é semelhante a função anterior. Eis seu código:

function BackButtonClick(CurPageID: Integer): Boolean;
begin
  case CurPageID of
    wpWelcome: ChangeStepImage(0,siStepUndone);
    wpLicense: ChangeStepImage(1,siStepUndone);
    wpPassword: ChangeStepImage(2,siStepUndone);
    wpInfoBefore: ChangeStepImage(3,siStepUndone);
    wpSelectComponents: ChangeStepImage(4,siStepUndone);
    wpSelectTasks: ChangeStepImage(5,siStepUndone);
    PGConfigPageID: ChangeStepImage(6,siStepUndone);
    wpReady: begin
      ChangeStepImage(7,siStepUndone);
      if not IsTaskSelected('PGConfig') then
        ChangeStepImage(6,siStepUndone);
    end;
  end;
  Result := True;
end;

(Coloque este código após a implementação do procedure ChangeStepImage)

Aqui, as únicas diferenças são que as imagens normalmente configuradas são as que correspondem a siStepUndone (etapa não concluída) e o artifício para detectar uma etapa opcional foi colocado em wpReady, pois como estamos indo do fim para o início do assistente, a página anterior a wpReady poderá ser ou a página de configuração do PostgreSQL ou a página de seleção de tarefas. Não vou explicar novamente. A lógica é muito similar àquela existente em NextButtonClick. Fica com lição para o leitor, interpretar o que eu fiz ;)

Em ambas as funções (BackButtonClick e NextButtonClick) caso retornemos False, a página do instalador não muda! É por este motivo que ambas as funções retornam True no final, já que não estamos fazendo qualquer validação, apenas estamos ajustando as imagens do RM.

Ajustes finais

Como me concentrei muito na entrega do material referente a construção do script e modificação do instalador com o intuito de mostrar todo o poder do Inno Setup, alguns detalhes ficaram para segundo plano, porém, hoje, com a entrega do último artigo deste tutorial, eu vou apresentar um ajuste muito importante, referente ao programa de exemplo, aquilo que o instalador que estamos construindo de fato instala, aquilo que seria de fato o objetivo de nosso instalador!

Este programa, muito simples, se conecta a instância do PostgreSQL instalada pelo nosso instalador, cria um banco de dados de nome inno, com apenas uma tabela de nome teste, se conecta a este banco de dados e exibe o conteúdo da tabela em um TDBGrid que permite edição diretamente nele. Este programa requer que exista um arquivo .ini, com o mesmo nome de seu executável na mesma pasta onde está esse executável. Para criar este arquivo .ini, o Inno Setup fornece uma seção chamada [INI], na qual podemos definir um nome de arquivo, uma seção, uma chave e seu respectivo valor. O Inno Script Studio fornece um meio prático, como sempre, de se adicionar itens na seção [INI], contudo, como temos 5 itens a serem adicionados e a fim de não tornar este artigo mais extenso do que já está, eu decidi mostrar como adicionar esta seção diretamente no script do Inno Setup. Quem já tem alguma intimidade com o Inno, prefere sempre escrever diretamente, sem usar assistentes ou telas, o que é bem mais rápido.

Dentro do script, imediatamente a seção [Code] (mas poderia ser em qualquer lugar após a seção [Setup] e antes da seção [Code]), inclua o seguinte texto:

[INI]
Filename: "{app}\InstaladoPeloInnoSetup.ini"; Section: "banco de dados"; Key: "database"; String: "inno"; Components: PostgreSQL; Tasks: PGConfig
Filename: "{app}\InstaladoPeloInnoSetup.ini"; Section: "banco de dados"; Key: "hostname"; String: "127.0.0.1"; Components: PostgreSQL; Tasks: PGConfig
Filename: "{app}\InstaladoPeloInnoSetup.ini"; Section: "banco de dados"; Key: "password"; String: "{code:PGConfiguration|password}"; Components: PostgreSQL; Tasks: PGConfig
Filename: "{app}\InstaladoPeloInnoSetup.ini"; Section: "banco de dados"; Key: "port"; String: "{code:PGConfiguration|port}"; Components: PostgreSQL; Tasks: PGConfig
Filename: "{app}\InstaladoPeloInnoSetup.ini"; Section: "banco de dados"; Key: "username"; String: "{code:PGConfiguration|username}"; Components: PostgreSQL; Tasks: PGConfig

Cada uma das linhas da seção [INI] informa o nome do arquivo .ini (Filename) a ser criado ou alterado, o nome da seção (Section) a ser criada ou utilizada dentro dele, o nome da chave (Key) a ser adicionada ou alterada e o valor (String) a ser associada com essa chave. O nome do arquivo contém a constante {app} a qual, como já sabemos, representa o diretório de instalação e tal como foi dito anteriormente, o nome do arquivo tem o mesmo nome do executável que o utiliza, com a extensão trocada, claro. Cada uma das linhas mostradas acima, tem as propriedades Components e Tasks devidamente configuradas como PostgreSQL e PGConfig, respectivamente e este assunto já foi abordado anteriormente neste tutorial, portanto não vou me estender aqui.

A novidade encontrada no texto da seção [INI] fica por conta dos valores das chaves password, port e username, eles são diferentes, tratam-se daquilo que é conhecido no Inno Setup como Scripted Constants e tem a seguinte sintaxe {code:FunctionName|Param}. Trata-se de uma forma prática de acessar funções que foram definidas na seção [Code]. Em algum ponto deste tutorial a função PGConfiguration foi criada com o intuito de obter o valor de um dos parâmetros informados na tela de configurações do PostgreSQL. Sua assinatura é function PGConfiguration(AParam: String): String, isto é, trata-se de uma função que tem um único parâmetro do tipo string (o nome da configuração a ser retornada) e que retorna o valor do parâmetro informado (outra string). Esta é uma função perfeita para ser usada como Scripted Constant. Acima, por exemplo, {code:PGConfiguration|port}, tem exatamente o mesmo efeito que chamar PGConfiguration('port') dentro da seção [Code], portanto, o valor da chave port a ser gravado no arquivo .ini será exatamente aquilo que o usuário informou na tela de configurações do PostgreSQL, no campo porta.

Além da inclusão de uma nova seção no script será necessário adicionar mais seis entradas na seção [Files]. Ao realizar testes com o programa de exemplo eu constatei que ele precisa de algumas DLLs para funcionar. São DLLs do PostgreSQL que precisam estar na mesma pasta do executável do programa. Todas as DLLs já estão disponíveis e as seis entradas na seção [Files] apenas instruem o Inno Setup a copiar estas DLLs também para a pasta do programa ({app}), veja:

Source: "{src}\pg\bin\libeay32.dll"; DestDir: "{app}"; Flags: external; Components: PostgreSQL
Source: "{src}\pg\bin\libiconv-2.dll"; DestDir: "{app}"; Flags: external; Components: PostgreSQL
Source: "{src}\pg\bin\libintl-8.dll"; DestDir: "{app}"; Flags: external; Components: PostgreSQL
Source: "{src}\pg\bin\libpq.dll"; DestDir: "{app}"; Flags: external; Components: PostgreSQL
Source: "{src}\pg\bin\ssleay32.dll"; DestDir: "{app}"; Flags: external; Components: PostgreSQL
Source: "..\mscrl\msvcr120.dll"; DestDir: "{app}"; Components: PostgreSQL

Pronto! Com mais esta alteração o script finalmente está completo e 100% funcional :)

Erratas

Eu admito que a forma que eu elaborei este tutorial não foi muito boa para mim. Em determinados momentos eu me perdi, coloquei coisas erradas no script de exemplo e corrigi à medida que eu via um erro. Nesta parte final isso não será uma exceção. Corrigi algumas coisas que estavam erradas mas não sei se o script final contém mais algum erro (lamento, não vou testar ele ponto a ponto). O que eu posso garantir é que o script anexado a este artigo foi compilado e executado e gerou o instalador da forma como eu planejei, mostrando todo comportamento que foi explicado nos últimos 11 artigos. Eu acredito que mereço um desconto devido a quantidade de detalhes em um tutorial tão extenso...

Considerações finais

E eis que finalmente eu terminei meu tutorial sobre o Inno Setup. Depois de meia dúzia de pausas que fizeram este tutorial ser concluído quase dois anos depois de seu artigo de estréia, a sensação é de dever cumprido! Eu me esforcei ao máximo para produzir um material de fácil compreensão e certamente, por conta disso, eu devo ter deixado muita gente impaciente, devido a minha prolixidade.

Finalizo este material batendo na mesma tecla: não reinvente a roda, use as melhores ferramentas para alcançar seus objetivos, mesmo que você fique tentado a dar seu toque de programador a algo que existe, pesquise a respeito e veja que nem sempre fazer algo do zero é a melhor escolha.

Certamente criar um instalador do zero é uma tarefa louvável e difícil, mas pra quê? O que eu demonstrei neste tutorial é apenas 1% do que o Inno Setup é capaz de fazer e eu não me considero especialista, portanto, alguém que tem tempo livre e saco suficiente pra fazer um instalador do zero, certamente faria algo infinitamente melhor no Inno Setup. Eu tenho certeza que você conhece alguém que adora criar soluções pra problemas que não existem, motivados pela força do ego :) Bom, agora chega, vou jogar Ghost Recon...

  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 07/07/2020 às 20:40
Página anterior do tutorial atual Primeira página do tutoria altual Próxima página do tutorial atual