Jogos em AdvPL – Tetris

Introdução

Como eu havia dito, no primeiro post deste blog, em AdvPL muita coisa pode ser feita, até um ERP. E, para dar ênfase em outras características do AdvPL, resolvi fazer algumas revisões e publicar um programinha interessante … um clone do famoso jogo Tetris. Sim, aquele que os blocos caem e você precisa alinhá-los para eliminar linhas da tela 😉

Tetris - Versão Final

O Algoritmo

Não foi tão difícil fazer o jogo funcionar, foi mais trabalhoso o refactoring para publicação e as explicações de como ele funciona por dentro do que bolar a lógica e interface do game. Ao ser executado, o programa já abre direto uma caixa de interface com uma peça sorteada em queda progressiva em intervalos de 1 segundo, mostrando a próxima peça a ser usada, atualizando um score lateral e um contador de tempo de jogo. Você pode usar as letras ASDW ou JKLI para mover a peça em queda para a esquerda, para baixo, para a direita e rotacioná-la, respectivamente, e a barra de espaços para dropar a peça até a linha inferior que ela pode alcançar na tela, sendo possível neste ponto ainda mover a peça para a direita ou esquerda, ou mesmo rotacioná-la caso exista espaço hábil para tais operações.Ainda pode ser usada a letra P para colocar e retirar o jogo de modo “Pause”. E, para sair do jogo instantaneamente, pressione a tecla [ESC].

Na versão inicial, o jogo não foi montado usando orientação a objeto. Ele ainda será passado a limpo, com classes e propriedades, mas já funciona muito bem com o paradigma estruturado. Em poucas linhas, o painel de jogo possui uma representação em memória usando array de strings, onde cada elemento do array representa uma linha da tela, e a sequência de números dentro de cada linha corresponde a um quadradinho colorido. A tela do jogo é desenhada na interface do Advpl em um Grid de 20 x 10 imagens, onde o jogo lida com a aplicação de peças e movimento de peças dentro desse array, e a camada de interface apenas atualiza essa matriz na interface trocando os resources das imagens. Como cada resource possui uma cor diferente, com uma matriz de 200 quadradinhos na tela e oito imagens, é possível montar a interface.

O miolo do jogo consiste em trabalhar com um array de strings, com 20 linhas, contendo em cada linha uma string com um valor numérico, onde “0” significa um espaço não preenchido, e cada valor maior que zero representa uma imagem 10×10 de uma cor diferente na respectiva posição do grid de bitmaps da interface. Como todo o jogo é baseado em array de strings, cada peça e sua respectiva representação de blocos é feita em um array multi-dimensional de peças, onde cada peça é representada por um grid de string 4×4 com “0” e “1”.

As funções de trabalho com as matrizes devem ser capazes de remover ou colocar uma peça do array que representa a interface, e no caso de colocar a peça, a função somente deve conseguir realizar esta operação caso as posições dos quadrados usados pela peça estejam vazias no Grid. A animação consiste apenas em remover a peça em jogo da posição atual do grid no array, inseri-la em uma nova posição, e repintar o grid de interface. Esta pintura é realizada simplesmente setando novamente o nome do resource (bitmap) usado naquela posição, caso ele esteja diferente do recurso indicado no array.

Cada STATIC FUNCTION do código tem um propósito distinto, e para economizar com a passagem de parâmetros, como este fonte é executado sem recursão, as variáveis contendo o estado do jogo também são STATIC. A interface para obter as teclas pressionadas foi feita com botões, criados fora da área visível da tela, onde cada letra usada é indicada no prompt do botão como uma tecla de atalho ( prefixada com & ). Neste caso especifico, quando o foco da interface está em um componente que não permite edição de conteúdo, o uso das teclas de atalho não precisa ser concomitante com a tecla [ALT], o que torna essa idéia de input de interface viável no AdvPL.

Dentro do fonte AdvPL as partes do código e suas funcionalidades estão bem documentadas. É claro que não é um fonte arroz com feijão, esse prato têm uns temperos um pouco mais “puxados”, e como ele não foi escrito usando orientação a objeto, ele requer um pouco mais de atenção para ser assimilado na íntegra. Posteriormente eu vou fazer a segunda versão desse clone, com todas as funcionalidades do primeiro, porém usando Orientação a Objetos do AdvPL, fazendo isso vai ficar muito visível o nível de clareza da aplicação quando utilizamos a Orientação a Objetos.

Fontes e Patch

O fonte deste aplicativo e os bitmaps (resources) necessários estão no GitHub https://github.com/siga0984/Tetris , bem como um patch gerado para o Protheus 11 ( RPO TOP , Português ) com apenas o fonte Tetris.PRW e as imagens do projeto. Para gerar a aplicação, basta ter um Protheus 10 ou superior, criar um projeto em branco, baixar o PRW e as imagens do GitHub, acrescentar o fonte no projeto, e acrescentar todas as imagens como “resources”. Para executá-lo, basta chamar o SmartClient, informando a função U_TETRIS

E, pra você que está se coçando de curiosidade, o fonte com todos os comentários e afins, ficou com pouco mais de 700 linhas. Segue abaixo o fonte AdvPL do clone do Tetris.

#include "protheus.ch"
/* ========================================================
Função U_TETRIS
Autor Júlio Wittwer
Data 03/11/2014
Versão 1.150226
Descriçao Réplica do jogo Tetris, feito em AdvPL
Para jogar, utilize as letras :
A ou J = Move esquerda
D ou L = Move Direita
S ou K = Para baixo
W ou I = Rotaciona sentido horario
Barra de Espaço = Dropa a peça
Pendencias
Fazer um High Score
Cores das peças
O = Yellow
I = light Blue
L = Orange
Z = Red
S = Green
J = Blue
T = Purple
======================================================== */
STATIC _aPieces := LoadPieces() // Array de peças do jogo 
STATIC _aBlockRes := { "BLACK","YELOW2","LIGHTBLUE2","ORANGE2","RED2","GREEN2","BLUE2","PURPLE2" }
STATIC _nGameClock // Tempo de jogo 
STATIC _nNextPiece // Proxima peça a ser usada
STATIC _GlbStatus := 0 // 0 = Running 1 = PAuse 2 == Game Over
STATIC _aBMPGrid := array(20,10) // Array de bitmaps de interface do jogo 
STATIC _aBMPNext := array(4,5) // Array de botmaps da proxima peça
STATIC _aNext := {} // Array com a definição e posição da proxima peça
STATIC _aDropping := {} // Array com a definição e posição da peça em jogo
STATIC _nScore := 0 // pontuação da partida
STATIC _oScore // label para mostrar o score e time e mensagens
STATIC _aMainGrid := {} // Array de strings com os blocos da interface representados em memoria
STATIC _oTimer // Objeto timer de interface para a queda automática da peça em jogo
 
// =======================================================
USER Function Tetris()
Local nC , nL
Local oDlg
Local oBackGround , oBackNext
Local oFont , oLabel , oMsg
// Fonte default usada na caixa de diálogo 
// e respectivos componentes filhos
oFont := TFont():New('Courier new',,-16,.T.,.T.)
DEFINE DIALOG oDlg TITLE "Tetris AdvPL" FROM 10,10 TO 450,365 ;
 FONT oFont COLOR CLR_WHITE,CLR_BLACK PIXEL
// Cria um fundo cinza, "esticando" um bitmap
@ 8, 8 BITMAP oBackGround RESOURCE "GRAY" ;
SIZE 104,204 Of oDlg ADJUST NOBORDER PIXEL
// Desenha na tela um grid de 20x10 com Bitmaps
// para ser utilizado para desenhar a tela do jogo
For nL := 1 to 20
 For nC := 1 to 10
 
 @ nL*10, nC*10 BITMAP oBmp RESOURCE "BLACK2" ;
 SIZE 10,10 Of oDlg ADJUST NOBORDER PIXEL
 
 _aBMPGrid[nL][nC] := oBmp
 
 Next
Next
 
// Monta um Grid 4x4 para mostrar a proxima peça
// ( Grid deslocado 110 pixels para a direita )
@ 8, 118 BITMAP oBackNext RESOURCE "GRAY" ;
 SIZE 54,44 Of oDlg ADJUST NOBORDER PIXEL
For nL := 1 to 4
 For nC := 1 to 5
 
 @ nL*10, (nC*10)+110 BITMAP oBmp RESOURCE "BLACK" ;
 SIZE 10,10 Of oDlg ADJUST NOBORDER PIXEL
 
 _aBMPNext[nL][nC] := oBmp
 
 Next
Next
// Label fixo, título do Score.
@ 80,120 SAY oLabel PROMPT "[Score]" SIZE 60,10 OF oDlg PIXEL
 
// Label para Mostrar score, timers e mensagens do jogo
@ 90,120 SAY _oScore PROMPT " " SIZE 60,120 OF oDlg PIXEL
 
// Define um timer, para fazer a peça em jogo
// descer uma posição a cada um segundo
// ( Nao pode ser menor, o menor tempo é 1 segundo )
_oTimer := TTimer():New(1000, ;
 {|| MoveDown(.f.) , PaintScore() }, oDlg )
// Botões com atalho de teclado
// para as teclas usadas no jogo
// colocados fora da area visivel da caixa de dialogo
@ 480,10 BUTTON oDummyBtn PROMPT '&A' ;
 ACTION ( DoAction('A'));
 SIZE 1, 1 OF oDlg PIXEL
@ 480,20 BUTTON oDummyBtn PROMPT '&S' ;
 ACTION ( DoAction('S') ) ;
 SIZE 1, 1 OF oDlg PIXEL
@ 480,20 BUTTON oDummyBtn PROMPT '&D' ;
 ACTION ( DoAction('D') ) ;
 SIZE 1, 1 OF oDlg PIXEL
 
@ 480,20 BUTTON oDummyBtn PROMPT '&W' ;
 ACTION ( DoAction('W') ) ;
 SIZE 1, 1 OF oDlg PIXEL
@ 480,20 BUTTON oDummyBtn PROMPT '&J' ;
 ACTION ( DoAction('J') ) ;
 SIZE 1, 1 OF oDlg PIXEL
@ 480,20 BUTTON oDummyBtn PROMPT '&K' ;
 ACTION ( DoAction('K') ) ;
 SIZE 1, 1 OF oDlg PIXEL
@ 480,20 BUTTON oDummyBtn PROMPT '&L' ;
 ACTION ( DoAction('L') ) ;
 SIZE 1, 1 OF oDlg PIXEL
@ 480,20 BUTTON oDummyBtn PROMPT '&I' ;
 ACTION ( DoAction('I') ) ;
 SIZE 1, 1 OF oDlg PIXEL
 
@ 480,20 BUTTON oDummyBtn PROMPT '& ' ; // Espaço = Dropa
 ACTION ( DoAction(' ') ) ;
 SIZE 1, 1 OF oDlg PIXEL
@ 480,20 BUTTON oDummyBtn PROMPT '&P' ; // Pause
 ACTION ( DoPause() ) ;
 SIZE 1, 1 OF oDlg PIXEL
// Na inicialização do Dialogo uma partida é iniciada
oDlg:bInit := {|| Start() }
ACTIVATE DIALOG oDlg CENTER
Return
/* ------------------------------------------------------------
Função Start() Inicia o jogo
------------------------------------------------------------ */
STATIC Function Start()
Local aDraw
// Inicializa o grid de imagens do jogo na memória
// Sorteia a peça em jogo
// Define a peça em queda e a sua posição inicial
// [ Peca, direcao, linha, coluna ]
// e Desenha a peça em jogo no Grid
// e Atualiza a interface com o Grid
InitGrid()
nPiece := randomize(1,len(_aPieces)+1)
_aDropping := {nPiece,1,1,6}
SetGridPiece(_aDropping,_aMainGrid)
PaintMainGrid()
// Sorteia a proxima peça e desenha 
// ela no grid reservado para ela 
InitNext()
_nNextPiece := randomize(1,len(_aPieces)+1)
aDraw := {_nNextPiece,1,1,1}
SetGridPiece(aDraw,_aNext)
PaintNext()
// Inicia o timer de queda automática da peça em jogo
_oTimer:Activate()
// Marca timer do inicio de jogo 
_nGameClock := seconds()
Return
/* ----------------------------------------------------------
Inicializa o Grid na memoria
Em memoria, o Grid possui 14 colunas e 22 linhas
Na tela, são mostradas apenas 20 linhas e 10 colunas
As 2 colunas da esquerda e direita, e as duas linhas a mais
sao usadas apenas na memoria, para auxiliar no processo
de validação de movimentação das peças.
---------------------------------------------------------- */
STATIC Function InitGrid()
_aMainGrid := array(20,"11000000000011")
aadd(_aMainGrid,"11111111111111")
aadd(_aMainGrid,"11111111111111")
return
STATIC Function InitNext()
_aNext := array(4,"00000")
return
//
// Aplica a peça no Grid.
// Retorna .T. se foi possivel aplicar a peça na posicao atual
// Caso a peça não possa ser aplicada devido a haver
// sobreposição, a função retorna .F. e o grid não é atualizado
//
STATIC Function SetGridPiece(aOnePiece,aGrid)
Local nPiece := aOnePiece[1] // Numero da peça
Local nPos := aOnePiece[2] // Posição ( para rotacionar ) 
Local nRow := aOnePiece[3] // Linha atual no Grid
Local nCol := aOnePiece[4] // Coluna atual no Grid
Local nL , nC
Local aTecos := {}
Local cTeco, cPeca , cPieceStr
cPieceStr := str(nPiece,1)
For nL := nRow to nRow+3
 cTeco := substr(aGrid[nL],nCol,4)
 cPeca := _aPieces[nPiece][1+nPos][nL-nRow+1]
 For nC := 1 to 4
 If Substr(cPeca,nC,1) == '1'
 If substr(cTeco,nC,1) != '0'
 // Vai haver sobreposição,
 // Nao dá para desenhar a peça
 Return .F.
 Endif
 cTeco := Stuff(cTeco,nC,1,cPieceStr)
 Endif
 Next
 // Array temporario com a peça já colocada
 aadd(aTecos,cTeco)
Next
// Aplica o array temporario no array do grid
For nL := nRow to nRow+3
 aGrid[nL] := stuff(_aMainGrid[nL],nCol,4,aTecos[nL-nRow+1])
Next
Return .T.
/* ----------------------------------------------------------
Função PaintMainGrid()
Pinta o Grid do jogo da memória para a Interface
Release 20150222 : Optimização na camada de comunicação, apenas setar
o nome do resource / bitmap caso o resource seja diferente do atual.
---------------------------------------------------------- */
STATIC Function PaintMainGrid()
Local nL, nc , cLine, nPeca
for nL := 1 to 20
 cLine := _aMainGrid[nL]
 For nC := 1 to 10
 nPeca := val(substr(cLine,nC+2,1))
 If _aBMPGrid[nL][nC]:cResName != _aBlockRes[nPeca+1]
 // Somente manda atualizar o bitmap se houve
 // mudança na cor / resource desta posição
 _aBMPGrid[nL][nC]:SetBmp(_aBlockRes[nPeca+1])
 endif
 Next
Next
Return
// Pinta na interface a próxima peça 
// a ser usada no jogo 
STATIC Function PaintNext()
Local nL, nC, cLine , nPeca
For nL := 1 to 4
 cLine := _aNext[nL]
 For nC := 1 to 5
 nPeca := val(substr(cLine,nC,1))
 If _aBMPNext[nL][nC]:cResName != _aBlockRes[nPeca+1]
 _aBMPNext[nL][nC]:SetBmp(_aBlockRes[nPeca+1])
 endif
 Next
Next
Return
/* -----------------------------------------------------------------
Carga do array de peças do jogo 
Array multi-dimensional, contendo para cada 
linha a string que identifica a peça, e um ou mais
arrays de 4 strings, onde cada 4 elementos 
representam uma matriz binaria de caracteres 4x4 
para desenhar cada peça
Exemplo - Peça "O"
aLPieces[1][1] C "O"
aLPieces[1][2][1] "0000" 
aLPieces[1][2][2] "0110" 
aLPieces[1][2][3] "0110" 
aLPieces[1][2][4] "0000"
----------------------------------------------------------------- */
STATIC Function LoadPieces()
Local aLPieces := {}
// Peça "O" , uma posição
aadd(aLPieces,{'O', { '0000','0110','0110','0000'}})
// Peça "I" , em pé e deitada
aadd(aLPieces,{'I', { '0000','1111','0000','0000'},;
 { '0010','0010','0010','0010'}})
// Peça "S", em pé e deitada
aadd(aLPieces,{'S', { '0000','0011','0110','0000'},;
 { '0010','0011','0001','0000'}})
// Peça "Z", em pé e deitada
aadd(aLPieces,{'Z', { '0000','0110','0011','0000'},;
 { '0001','0011','0010','0000'}})
// Peça "L" , nas 4 posições possiveis
aadd(aLPieces,{'L', { '0000','0111','0100','0000'},;
 { '0010','0010','0011','0000'},;
 { '0001','0111','0000','0000'},;
 { '0110','0010','0010','0000'}})
// Peça "J" , nas 4 posições possiveis
aadd(aLPieces,{'J', { '0000','0111','0001','0000'},;
 { '0011','0010','0010','0000'},;
 { '0100','0111','0000','0000'},;
 { '0010','0010','0110','0000'}})
// Peça "T" , nas 4 posições possiveis
aadd(aLPieces,{'T', { '0000','0111','0010','0000'},;
 { '0010','0011','0010','0000'},;
 { '0010','0111','0000','0000'},;
 { '0010','0110','0010','0000'}})
Return aLPieces
/* ----------------------------------------------------------
Função MoveDown()
Movimenta a peça em jogo uma posição para baixo.
Caso a peça tenha batido em algum obstáculo no movimento
para baixo, a mesma é fica e incorporada ao grid, e uma nova
peça é colocada em jogo. Caso não seja possivel colocar uma
nova peça, a pilha de peças bateu na tampa -- Game Over
---------------------------------------------------------- */
STATIC Function MoveDown(lDrop)
Local aOldPiece
 
If _GlbStatus != 0
 Return
Endif
// Clona a peça em queda na posição atual
aOldPiece := aClone(_aDropping)
If lDrop
 
 // Dropa a peça até bater embaixo
 // O Drop incrementa o score em 1 ponto 
 // para cada linha percorrida. Quando maior a quantidade
 // de linhas vazias, maior o score acumulado com o Drop
 
 // Guarda a peça na posição atual
 aOldPiece := aClone(_aDropping)
 
 // Remove a peça do Grid atual
 DelPiece(_aDropping,_aMainGrid)
 
 // Desce uma linha pra baixo
 _aDropping[3]++
 
 While SetGridPiece(_aDropping,_aMainGrid)
 
 // Encaixou, remove e tenta de novo
 DelPiece(_aDropping,_aMainGrid)
 
 // Guarda a peça na posição atual
 aOldPiece := aClone(_aDropping)
 
 // Desce a peça mais uma linha pra baixo
 _aDropping[3]++
// Incrementa o Score
 _nScore++
 
 Enddo
 
 // Nao deu mais pra pintar, "bateu"
 // Volta a peça anterior, pinta o grid e retorna
 // isto permite ainda movimentos laterais
 // caso tenha espaço.
 
 _aDropping := aClone(aOldPiece)
 SetGridPiece(_aDropping,_aMainGrid)
 PaintMainGrid()
 
Else
 
 // Move a peça apenas uma linha pra baixo
 
 // Primeiro remove a peça do Grid atual
 DelPiece(_aDropping,_aMainGrid)
 
 // Agora move a peça apenas uma linha pra baixo
 _aDropping[3]++
 
 // Recoloca a peça no Grid
 If SetGridPiece(_aDropping,_aMainGrid)
 
 // Se deu pra encaixar, beleza
 // pinta o novo grid e retorna
 PaintMainGrid()
 Return
 
 Endif
 
 // Opa ... Esbarrou em alguma coisa
 // Volta a peça pro lugar anterior
 // e recoloca a peça no Grid
 _aDropping := aClone(aOldPiece)
 SetGridPiece(_aDropping,_aMainGrid)
// Incrementa o score em 4 pontos 
 // Nao importa a peça ou como ela foi encaixada
 _nScore += 4
// Agora verifica se da pra limpar alguma linha
 ChkMainLines()
 
 // Pega a proxima peça
 nPiece := _nNextPiece
 _aDropping := {nPiece,1,1,6} // Peca, direcao, linha, coluna
If !SetGridPiece(_aDropping,_aMainGrid)
 
 // Acabou, a peça nova nao entra (cabe) no Grid
 // Desativa o Timer e mostra "game over"
 // e fecha o programa
_GlbStatus := 2 // GAme Over
// volta os ultimos 4 pontos ... 
 _nScore -= 4
// Cacula o tempo de operação do jogo 
 _nGameClock := round(seconds()-_nGameClock,0)
 If _nGameClock < 0 
 // Ficou negativo, passou da meia noite 
 _nGameClock += 86400
 Endif
// Desliga o timer de queda de peça em jogo
 _oTimer:Deactivate() 
 
 Endif
 
 // Se a peca tem onde entrar, beleza
 // -- Repinta o Grid -- 
 PaintMainGrid()
// Sorteia a proxima peça
 // e mostra ela no Grid lateral
If _GlbStatus != 2 
 // Mas apenas faz isso caso nao esteja em game over
 InitNext()
 _nNextPiece := randomize(1,len(_aPieces)+1)
 SetGridPiece( {_nNextPiece,1,1,1} , _aNext)
 PaintNext()
 Else
 // Caso esteja em game over, apenas limpa a proxima peça
 InitNext()
 PaintNext()
 Endif
 
 
Endif
Return
/* ----------------------------------------------------------
Recebe uma ação da interface, através de uma das letras
de movimentação de peças, e realiza a movimentação caso
haja espaço para tal.
---------------------------------------------------------- */
STATIC Function DoAction(cAct)
Local aOldPiece
// conout("Action = ["+cAct+"]")
If _GlbStatus != 0 
 Return
Endif
// Clona a peça em queda
aOldPiece := aClone(_aDropping)
if cAct $ 'AJ'
// Movimento para a Esquerda (uma coluna a menos)
 // Remove a peça do grid
 DelPiece(_aDropping,_aMainGrid)
 _aDropping[4]--
 If !SetGridPiece(_aDropping,_aMainGrid)
 // Se nao foi feliz, pinta a peça de volta
 _aDropping := aClone(aOldPiece)
 SetGridPiece(_aDropping,_aMainGrid)
 Endif
 // Repinta o Grid
 PaintMainGrid()
 
Elseif cAct $ 'DL'
// Movimento para a Direita ( uma coluna a mais )
 // Remove a peça do grid
 DelPiece(_aDropping,_aMainGrid)
 _aDropping[4]++'
 If !SetGridPiece(_aDropping,_aMainGrid)
 // Se nao foi feliz, pinta a peça de volta
 _aDropping := aClone(aOldPiece)
 SetGridPiece(_aDropping,_aMainGrid)
 Endif
 // Repinta o Grid
 PaintMainGrid()
 
Elseif cAct $ 'WI'
 
 // Movimento para cima ( Rotaciona sentido horario )
 
 // Remove a peça do Grid
 DelPiece(_aDropping,_aMainGrid)
 
 // Rotaciona
 _aDropping[2]--
 If _aDropping[2] < 1
 _aDropping[2] := len(_aPieces[_aDropping[1]])-1
 Endif
 
 If !SetGridPiece(_aDropping,_aMainGrid)
 // Se nao consegue colocar a peça no Grid
 // Nao é possivel rotacionar. Pinta a peça de volta
 _aDropping := aClone(aOldPiece)
 SetGridPiece(_aDropping,_aMainGrid)
 Endif
 
 // E Repinta o Grid
 PaintMainGrid()
 
ElseIF cAct $ 'SK'
 
 // Desce a peça para baixo uma linha intencionalmente 
 MoveDown(.F.)
 
 // se o movimento foi intencional, ganha + 1 ponto 
 _nScore++
 
ElseIF cAct == ' '
 
 // Dropa a peça - empurra para baixo até a última linha
 // antes de baer a peça no fundo do Grid
 MoveDown(.T.)
 
Endif
// Antes de retornar, repinta o score
PaintScore()
Return .T.
Static function DoPause()
If _GlbStatus == 0
 // Pausa
 _GlbStatus := 1
 _oTimer:Deactivate()
Else
 // Sai da pausa
 _GlbStatus := 0
 _oTimer:Activate()
Endif
// Antes de retornar, repinta o score
PaintScore()
Return
/* -----------------------------------------------------------------------
Remove uma peça do Grid atual
----------------------------------------------------------------------- */
STATIC Function DelPiece(aPiece,aGrid)
Local nPiece := aPiece[1]
Local nPos := aPiece[2]
Local nRow := aPiece[3]
Local nCol := aPiece[4]
Local nL, nC
Local cTeco, cPeca
// Como a matriz da peça é 4x4, trabalha em linhas e colunas
// Separa do grid atual apenas a área que a peça está ocupando
// e desliga os pontos preenchidos da peça no Grid.
For nL := nRow to nRow+3
 cTeco := substr(aGrid[nL],nCol,4)
 cPeca := _aPieces[nPiece][1+nPos][nL-nRow+1]
 For nC := 1 to 4
 If Substr(cPeca,nC,1)=='1'
 cTeco := Stuff(cTeco,nC,1,'0')
 Endif
 Next
 aGrid[nL] := stuff(_aMainGrid[nL],nCol,4,cTeco)
Next
Return
/* -----------------------------------------------------------------------
Verifica se alguma linha esta completa e pode ser eliminada
----------------------------------------------------------------------- */
STATIC Function ChkMainLines()
Local nErased := 0
For nL := 20 to 2 step -1
 
 // Sempre varre de baixo para cima
 // Pega uma linha, e remove os espaços vazios
 cTeco := substr(_aMainGrid[nL],3)
 cNewTeco := strtran(cTeco,'0','')
 
 If len(cNewTeco) == len(cTeco)
 // Se o tamanho da linha se manteve, não houve
 // nenhuma redução, logo, não há espaços vazios
 // Elimina esta linha e acrescenta uma nova linha
 // em branco no topo do Grid
 adel(_aMainGrid,nL)
 ains(_aMainGrid,1)
 _aMainGrid[1] := "11000000000011"
 nL++
 nErased++
 Endif
 
Next
// Pontuação por linhas eliminadas 
// Quanto mais linhas ao mesmo tempo, mais pontos
If nErased == 4
 _nScore += 100
ElseIf nErased == 3
 _nScore += 50
ElseIf nErased == 2
 _nScore += 25
ElseIf nErased == 1
 _nScore += 10
Endif
Return
/* ------------------------------------------------------
Seta o score do jogo na tela
Caso o jogo tenha terminado, acrescenta 
a mensagem de "GAME OVER"
------------------------------------------------------*/
STATIC Function PaintScore()
If _GlbStatus == 0
// JOgo em andamento, apenas atualiza score e timer
 _oScore:SetText(str(_nScore,7)+CRLF+CRLF+;
 '[Time]'+CRLF+str(seconds()-_nGameClock,7,0)+' s.')
ElseIf _GlbStatus == 1
// Pausa, acresenta a mensagem de "GAME OVER"
 _oScore:SetText(str(_nScore,7)+CRLF+CRLF+;
 '[Time]'+CRLF+str(seconds()-_nGameClock,7,0)+' s.'+CRLF+CRLF+;
 "*********"+CRLF+;
 "* PAUSE *"+CRLF+;
 "*********")
ElseIf _GlbStatus == 2
// Terminou, acresenta a mensagem de "GAME OVER"
 _oScore:SetText(str(_nScore,7)+CRLF+CRLF+;
 '[Time]'+CRLF+str(_nGameClock,7,0)+' s.'+CRLF+CRLF+;
 "********"+CRLF+;
 "* GAME *"+CRLF+;
 "********"+CRLF+;
 "* OVER *"+CRLF+;
 "********")
Endif
Return

Conclusão

Para se tirar o melhor de cada ferramenta, devemos conhecê-la, e quanto mais profundamente a conhecemos, melhores são os resultados que podemos obter dela. Espero que isso estimulem vocês a quererem saber mais, e colocar em prática o seu conhecimento. E, aprender também pode ser divertido !!! Espero que todos gostem !! Até o próximo post, pessoal 😉

Referências

TDN – Totvs Development Network. TOTVS, 2015. Disponível em http://tdn.totvs.com/display/tec/AdvPL . Acesso em: 26 fev. 2015.

TETRIS. In: WIKIPÉDIA, a enciclopédia livre. Flórida: Wikimedia Foundation, 2015. Disponível em: [http://pt.wikipedia.org/w/index.php?title=Tetris&oldid=41347620]. Acesso em: 23 fev. 2015.

Tetris. (2015, February 16). In Wikipedia, The Free Encyclopedia. Retrieved 04:38, February 23, 2015, from http://en.wikipedia.org/w/index.php?title=Tetris&oldid=647443725

Fontes do Blog no GitHub

Para facilitar o processo de utilização dos exemplos postados no blog, criei um repositório público no GitHub. O repositório pode ser acessado através do link https://github.com/siga0984/Blog

Nele, estão todos os fontes utilizados nos exemplos postados no blog, e no cabeçalho de cada fonte existe um link que aponta para qual post o fonte está relacionado. Ainda estou ajustando a codificação dos fontes, para a visualização direta no navegador o encoding (codificação de caracteres) dos fontes deve ser UTF-8, porém o AdvPL trata as strings acentuadas dentro dos códigos como CP1252, interferindo no resultado final esperado. Ainda esta noite os fontes serão reconvertidos para CP1252 e atualizados.

Para compilar os fontes, basta baixá-los para o seu equipamento, e usando o IDE ou o TDS, criar um projeto e acrescentar todos os códigos dentro do projeto e compilá-los. Alguns fontes tiveram os seus nomes ou o nome de suas funções trocados dentro do código, para haver uma padronização da nomenclatura, e podem estar diferentes dos nomes publicados nos posts, para viabilizar a compilação de todos dentro de um projeto único.

Espero que todos façam bom proveito dos códigos de exemplo, e que estes de alguma forma sejam úteis para aprofundar o conhecimento de vocês no AdvPL. Caso alguém tenha alguma dúvida sobre os fontes, pode postar uma pergunta no próprio post onde o fonte foi utilizado.

Por enquanto é só, mas para a próxima semana estou preparando mais alguns códigos interessantes! Até o próximo post, pessoal 😉 E obrigado pela audiência 😀

Escalabilidade e performance – Fila com job

Introdução

Em um tópico anterior, sobre escalabilidade e performance, foi abordado o tema das filas, e a sua importância na busca por desempenho em processos. Hoje, vamos ver mais de perto o que a linguagem AdvPL nos oferece para criarmos um ou mais processos dedicados sem interface visual, os “Jobs”, e com poderíamos usá-los para processar uma fila de requisições de processamento.

Processamento assíncrono

Pelo pouco que vimos nos posts anteriores sobre AdvPL e SmartClient, vimos que a interface visual nativa do AdvPL possui a arquitetura client-server, e que trabalha de modo síncrono, com conexão persistente. E, voltando um pouco nas técnicas para obter performance e escalabilidade, que quando for possível (e vamos entender essa palavra no contexto de “onde for cabível) utilize processos assíncronos.

O exemplo que serve como uma luva neste paradigma é a criação de uma fila, e um processo separado para o processamento das requisições colocadas nesta fila, onde o programa cliente da fila, ao colocar a solicitação de processamento na fila, não precisa esperar uma resposta imediata do processo para dar um retorno para a camada de interface.

Acredito que eu já tenha mencionado isso no tópico a respeito de filas, por exemplo, para o envio de e-mails de notificação ou processos secundários.

Jobs

Antes de nos afundarmos nas filas, vamos ver um pouco sobre os Jobs no AdvPL. Entendemos como “Job” o processo de executar um programa desamarrado da camada de interface. O AdvPL nos fornece algumas formas de fazer isso.

Função StartJob()

Permite, a partir de um programa AdvPL em execução, iniciar um novo contexto de execução de Advpl para a execução de uma função em um determinado ambiente do Application Server, sem relação nenhuma com a camada de interface.

Configuração JOBS da seção ONSTART

O arquivo de configuração do Application Server permite que seja configurados um ou mais jobs, executados no momento da subida ou inicialização do serviço do Application Server. Os jobs não explicitamente configurados com um tipo especial são interpretados como jobs de execução direta, onde inclusive podemos especificar quantas execuções serão disparadas ao mesmo tempo (paralelo) de um determinado job.

Partindo para a prática

No exemplo de hoje, vamos usar a configuração ONSTART, para iniciar um job dedicado a remover itens de uma fila global, e depois algumas funções interessantes para dar mais resiliência no processo. A fila de requisições possui um formato desenhado para ser um container independente de requisições, para armazenar um valor AdvPL qualquer (exceto CodeBlock e Objeto). No nosso exemplo, vamos apenas simular um processamento, que por baixo vai manter o job de processamento de fila em SLEEP() por 5 segundos. A fila vai ser mantida em memória, e independente de quantas instâncias de SmartClient forem usadas para abrir o programa que coloca requisições na fila, um Job será mantido no ar para o processamento dos elementos da fila.

O exemplo abaixo utiliza uma classe de fila global, com escopo por environment e por instância de Application Server. Para fazer o Download do arquivo que contém a classe AdvPL de fila global de exemplo, clique no link a seguir: APGlbQueue

#include 'protheus.ch'
/* ===================================================================
Função U_TSTQueue
Autor Júlio Wittwer
Data 08/02/2015
Desrição Teste de fila global com processamento 
 das requisições em JOB
Configuração de onstart para o Job de processamento
[ONSTART]
Jobs=JOBFILA01
[JOBFILA01]
Environment=NOMEDOSEUAMBIENTE
main=U_FILAPROC
nParms=1
Parm1=FILA01
=================================================================== */
User Function TSTQueue()
Local oDlg
Local oButton1
Local oSay1
Local oSay2
Local oQueue
Local nStatus
Local cMsg1 := space(40)
Local cMsg2 := space(40)
Local cMsg3 := space(40)
 
// Cria o objeto da fila global 
// Informa o nome da fila global e 
// a quantidade máxima de elementos
oQueue := APGlbQueue():New( "FILA01", 10 )
DEFINE DIALOG oDlg TITLE "Cliente da Fila" FROM 0,0 TO 160,300 PIXEL
// Botao para acrescentar na fila uma requisicao de processamento
@ 10,10 BUTTON oButton1 PROMPT "&Inserir requisição" ;
 ACTION ( InsertReq(oQueue,oSay1,oSay2) ) ;
 SIZE 080, 013 of oDlg PIXEL
@ 30,10 SAY oSay1 VAR cMsg1 SIZE 080, 013 of oDlg PIXEL
@ 40,10 SAY oSay2 VAR cMsg2 SIZE 080, 013 of oDlg PIXEL
ACTIVATE DIALOG oDlg CENTER ;
 VALID ( MsgYesNo("Deseja encerrar a aplicação ?") )
Return
/* ----------------------------------------------------------------
Inserção de requisição na fila, disparada pelo botão de interface
Atualiza valor na tela com o total de requisicoes enviadas 
---------------------------------------------------------------- */
STATIC Function InsertReq(oQueue,oSay1,oSay2)
Local nValue 
Local nStatus
// Requisicao fixa, 5 segundos ( 5000 ms ) 
nValue := 5000
nStatus := oQueue:Enqueue( nValue )
If nStatus < 0 
 // Falha ao inserir na fila
 MsgStop(oQueue:GetErrorStr(),"Falha ao acrescentar elemento na fila")
Else
 // Inseriu com sucesso, atualiza informaçoes na tela
 oSay1:SetText("Requisições inseridas ... "+cValToChar(oQueue:nEnqCnt))
 oSay2:SetText("Itens na fila ........... "+cValToChar(nStatus))
endif
Return
/* --------------------------------------------------------------
JOB dedicado para retirar e processar elementos colocados na fila
Deve ser iniciado na subida do servidor, no [ONSTART] como um
job, informando como parametro o ID da Fila Global
------------------------------------------------------------- */
USER Function FILAPROC( cQueueId )
Local nStatus
Local oQueue
Local xRequest := NIL
// Coloca observação do processo para Protheus Monitor
PtInternal(1,"Job de Processamento - Fila "+cQueueId)
If empty(cQueueId)
 // Se esta funcao foi chamada sem o ID da Fila ... 
 UserException("Missing QueueId configuration for U_FILAPROC")
Endif
conout("["+dtos(date())+" "+time()+"][JOB] Thread ["+cValToChar(ThreadId())+"] Iniciado.")
// Cria a instância para manutenção da fila
oQueue := APGlbQueue():New( cQueueId )
While !KillApp() 
 
 // Loop de processamento permanece em execução
 // desde que esta thread nao tenha recebido
 // uma notificação de finalização 
 
 // Tenta remover o primeiro item da fila
 nStatus := oQueue:Dequeue( @xRequest )
 
 If ( nStatus >= 0 )
 
 conout("["+dtos(date())+" "+time()+"][JOB] Thread ["+cValToChar(ThreadId())+"] Tamanho da fila = "+cValToChar(nStatus))
 
 // Pegou o primeiro item da fila
 // e retornou o numero de itens pendentes
 // neste momento na fila
 
 // Informa no console que vai fazer um "Sleep"
 conout( "["+dtos(date())+" "+time()+"][JOB] Thread ["+cValToChar(ThreadId())+"] Processando ... " )
 
 // Aqui eu poderia chamar qqer coisa
 // neste exemplo será feito um sleep mesmo 
 Sleep(xRequest)
 
 conout( "["+dtos(date())+" "+time()+"][JOB] Thread ["+cValToChar(ThreadId())+"] Fim de processamento " )
Else
 
 // Nao pegou nenhum item ... 
 // Falha de lock ou Fila vazia
 conout("["+dtos(date())+" "+time()+"][JOB] Thread ["+cValToChar(ThreadId())+"] "+oQueue:GetErrorStr())
Endif
 
Enddo
conout("["+dtos(date())+" "+time()+"][JOB] Thread ["+cValToChar(ThreadId())+"] Finalizado.")
Return

Funcionamento

Todas as explicações específicas sobre o que cada parte do código fazem estão dentro do próprio código. Aqui vamos avaliar a execução da aplicação como um todo. Após compilar o fonte client de exemplo, e compilar o fonte que gerencia a fila global em memoria, e configurar o JOB para ser executado na subida do Totvs Application Server, o programa U_FILAPROC será colocado em execução via Job, sem interface, e ficará em loop fazendo Dequeue() da lista.

Ao iniciarmos a aplicação U_TSTQUEUE através do SmartClient, cada acionamento do botão “Inserir requisição” acrescenta na fila de requisições global uma requisição de processamento, que contém um número de milissegundos que a função de processamento deve permanecer em Sleep(). Podemos subir mais de um programa client ao mesmo tempo. O programa usa uma classe de exemplo, APGlbQueue, que realiza as operações de empilhamento e desempilhamento, que internamente garante a semaforização da operação de enqueue() e dequeue() (acrescentar e remover requisição da fila, respectivamente). A fila utilizada é do tipo FIFO (First In, First Out), isto é, o primeiro elemento acrescentado será o primeiro a sair da fila (e ser processado).

Cada acionamento da aplicação Client atualiza informações na tela, mostrando quantas requisições foram acrescentadas pela interface atual e quantas requisições ainda estão pendentes na fila de requisições. Podemos acompanhar o processamento da fila através das mensagens mostradas no log de console ( console.log) do Application Server. Caso a fila receba uma inserção, mas o numero de elementos na fila já está no máximo (no nosso caso, 10 elementos), a operação de Enqueue() retorna um erro, indicando que a fila está “cheia”.

Como cada requisição mantém o Job de processamento (Dequeue()) ocupado por 5 segundos, basta aguardar um pouco para a fila ficar menor, e a operação de acrescentar um novo item será possível. Mesmo que todos os programas de interface sejam encerrados, o job de processamento está em um looping separado e independente da interface, e continua em execução.

A classe de fila global criada para este exemplo utiliza um container de armazenamento global de memória do Appplication Server, onde basicamente usamos duas funções ( Set / Get ) para armazenar conteúdo nomeado na memória da instância atual de um TOTVS Application Server, e recuperá-lo dentro de qualquer job ou aplicação em execução no mesmo Appplication Server.

Considerações sobre Balanceamento de Carga

Caso este recurso seja colocado por exemplo, em um ambiente com balanceamento de carga no SmartClient, o servidor “Master” poderá redirecionar a conexão para qualquer um dos serviços configurados para balanceamento. Logo, neste caso cada serviço slave precisaria ter na seção OnStart uma chamada para colocar o job de Dequeue() em execução, senão o client vai acrescentar elementos na fila, que nunca serão processados, até ele recusar novos elementos caso a fila atinja 10 itens, ou ainda apenas um serviço seja configurado para fazer o processamento da fila, com um ou mais instâncias do job, e a aplicação client da fila utilize um RPC por exemplo, para conectar-se com o serviço que processa a fila, para acrescentar na memória deste serviço as novas requisições.

Considerações sobre a persisência da fila

Uma vez que a fila, no nosso exemplo, está sendo persistida em memória, se o serviço do Application Server for finalizado, o processamento pode ser encerrado antes de todas as requisições da fila terem sido processadas, então este exemplo não pode ser usado para etapas importantes de processo, que exijam por exemplo que a requisição desta fila não possa se perder em caso de uma falha grave ou finalização anormal do serviço. Caso seja necessária uma persistência segura, o correto a fazer é criar uma classe que persista a informação da fila em um banco de dados, para que quando o processo seja recolocado no ar, as requisições pendentes sejam processadas. E, remover uma requisição da fila não quer dizer que o processamento foi feito com sucesso, mas sim que alguém removeu para processar. Podem ser necessários mais mecanismos para garantir que o processo tenha chego ao final com sucesso. Vamos abordar mais destas questões em um exemplo um pouco mais rebuscado do que este, em um próximo post.

Conclusão

Este post demorou quase 2 semanas para sair, afinal eu queria publicar algo realmente diferente, e relativamente simples, para aplicar conceitos como o das filas, paralelismo e jobs, e este exemplo teve pelo menos duas refatorações até estar “no ponto” de ser servido !!! Até o próximo post, pessoal 😉

Interface visual do Advpl – SmartClient

Introdução

Nos posts anteriores, vimos um pouco do que o SmartClient é capaz de fazer, mas há muito mais a ser visto. O que não vimos ainda são alguns detalhes, particularidades e características do SmartClient. Agora que já vimos um pouco do que ele faz, vamos ver mais algumas coisas, e um pouco de como isto é feito por dentro.

Conexão TCP

Entre o SmartClient e o Application Server, existe uma conexão TCP estabelecida, que parte do SmartClient. No arquivo de configuração do SmartClient, chamado smartclient.ini, podemos criar seções nomeadas para especificar IP e Porta para a conexão com um Application Server. Na tela inicial do SmartClient, podemos especificar o programa a ser executado, qual a conexão nomeada deve ser utilizada, e qual o nome do ambiente (Environment) no Application Server deve atender esta requisição de processamento.

Time-Out de Comunicação

Quando ativamos uma interface, como por exemplo uma janela ou caixa de diálogo, o APPlication Server permanece dentro do método Activate(), aguardando por interações de interface. Nesta situação, o controle de execução da aplicação está na interface. Mesmo que o operador não faça nada na interface, apenas fique olhando para a tela, a cada 60 segundos o SmartClient envia um pulso na conexão com o Application  Server, apenas para informar que ele (SmartClient) está “vivo”.

Existe um mecanismo de time-out de inatividade de comunicação TCP no Application Server, ativo quando o controle de execução está na interface. Caso o Application  Server não receba nada (nem mesmo um pulso) em 3 minutos, ele (AppServer) assume que algo horrivel aconteceu com o SmartClient, ou com a conexão de rede entre ele e o Application  Server. Neste caso, o APPlication Server encerra a conexão, mostrando uma mensagem no log de console do Application  Server “MsgManager – TimeOut waiting for data”, e encerra a aplicação em execução, fecha as conexões em uso com gateways de dados ( DBAccess, c-Tree), solta locks de tabelas, e todos os demais recursos consumidos e alocados para aquele programa. Afinal, se a interface não está mais lá, não faz sentido manter o contexto da aplicação aberto segurando recursos.

Time-Out por Inatividade de Interface

Existe uma configuração diferenciada para o ambiente, chamada de “INACTIVETIMEOUT”, que significa um tempo de inatividade de interação do usuário na interface. Quando o servidor está aguardando por interação do usuário, se este fica apenas olhando para a tela, o Application  Server recebe a cada um minuto um pulso, mas este pulso não conta como interação do usuário. Deve haver o disparo de algum evento de componente de interface para que este contador de inatividade retorne ao zero novamente, como por exemplo uma troca de foco entre componentes. Caso esta configuração esteja habilitada, e o operador ficar mais do que o tempo configurado sem interagir com a interface, o SmartClient deste operador é encerrado com uma mensagem de Time-Out atingido por inatividade.

Balanceamento de Carga

O TOTVS Application Server permite, de forma nativa, configurar um serviço para balanceamento de conexões de SmartClient entre vários serviços do Application Server, na mesma máquina e/ou em outras máquinas. Todo o procedimento de configuração desta funcionalidade está documentada na TDN, vide link http://tdn.totvs.com/pages/viewpage.action?pageId=6064861

Uma característica interessante do balanceamento de carga do TOTVS Application Server, é que o serviço de balanceamento não atua como um gateway ou proxy reverso. O balanceador mantém uma conexão persistente de monitoramento com cada serviço slave configurado nele, e no momento que o balanceador recebe uma conexão do SmartClient, ele verifica entre os serviços disponívels, quem está com o menor número de processos em execução, e retorna ao SmartClient uma informação de redirecionamento, para que o SmartClient conecte diretamente no serviço slave que ele escolheu.

Por isso, se o serviço do balanceador for interrompido, isto não interfere nas conexões já estabelecidas e em execução nos serviços slave. E, também por esta razão, todos os IPs e portas de todos os serviços slaves devem estar visíveis para que qualquer SmartClient que conectar com o Balance, seja capaz de conectar com qualquer um dos slaves utilizazdos para balanceamento.

Outro ponto interessante é que o serviço de Balance leva em conta um flag de aceite de conexão por parte do Slave. Meso que o serviço slave esteja no ar, caso você use o TOTVS Monitor, e bloqueie as conexões naquele serviço, o serviço de Balance vai saber que as conexões daquele slave estão bloqueadas, e não vai mais considerar aquele slave na distribuição, até que ele seja novamente desbloqueado para aceitar novas conexões. Esta característica foi muito útil quando foi implementado um mecanismo de proteção de alocação de memória no Application Server, onde caso a memória em uso por um Slave ultrapasse 90% do limite suportado pelo Application Server, este Slave automaticamente desabilita a entrada de novas conexões, e o Balance passa a desconsiderá-lo. Somente quando a memória daquele slave volta ao patamar de 80% do limite, o slave automaticamente libera novas conexões, e o Balance volta a considerá-lo no algoritmo de distribuição.

Conexão SSL

A conexão TCP entre SmartClient e Application Server troca pacotes em formato proprietário, que não é interpretável facilmente, porém nesta camada não é aplicada nenhuma criptografia. Dependendo do tamanho do pacote, ao utilizar por exemplo um “Sniffer” de rede, você poderá eventualmente ver strings com textos e dados de campos serem trafegados. Para oferecer uma forma de criptografia segura das informações trafegadas entre Application Server e SmartClient, podemos configurar o Application Server para utilizar um certificado digital — seção SSLCONFIGURE — e configurar uma porta SSL para receber as conexões do SmartClient. Assim, desta forma, os dados trafegados entre estas aplicações vão estar efetivamente criptografados.

Log de comunicação

Todas as mensagens trocadas entre o APPlication Server e o SmartClient podem ser registradas no log de console (Console.log) do APPlication Server, utilizando a chave LOGMESSAGES=1 na seção [GENERAL] do arquivo de configuração do APPlication Server (Appserver.ini). Experimente pegar um destes exemplos de interface, e executar com este log habilitado no APPlication Server, e você vai ver quais e quando são trocadas as mensagens entre as aplicações. Faça isso em um ambiente separado, de testes, pois a geração deste log ao mesmo tempo por múltiplas conexões pode onerar um ambiente de produção. Este tipo de LOG somente é necessário e utilizado quando existe a necessidade de investigar um comportamento anormal da interface ou do APPlication Server relacionado ao uso de algum componente de interface, habilitado em ambientes específicos de desenvolvimento e testes.

Versões e Plataformas de Smartclient

Até o momento, o AdvPL possui 3 versões de interface SmartClient: O SmartClient em formato executável, compilado para as plataformas Windows, Linux e MAC; o SmartClient ActiveX, que pode ser publicado em um servidor HTTP / WEB e instalado como uma extensão / Plug-In do Internet Explorer, e por último o SmartClient HTML, que pode ser instanciado pela maioria dos Internet Browses de mercado sem a necessidade de instalação de nenhuma extensão ou Plug-In. Existem algumas limitações no Smartclient ActiveX e no SmartClient HTML, por exemplo a impossibilidade de usar as funções de baixo nível de arquivo ( fopen, fcreate, … ) no sistema de arquivos da máquina onde o SmartClient está sendo utilizado.

Como ainda não falamos nada sobre o sistema de arquivos do AdvPL, por hora vamos apenas saber que, usando as funções de acesso a disco e arquivos do AdvPL, podemos endereçar arquivos na máquina onde está sendo executado o TOTVS Application Server, a partir do RootPath do ambiente, e podemos também acessar o sistema de arquivos da máquina onde o SmartClient que iniciou a execução de uma aplicação está rodando. Este tema também será abordado posteriormente, em tópico específico.

Boas práticas

O cenário ideal para a utilização de interface Client-Server persistente é para processamento de requisições de retorno rápido. Relatórios extensos ou processamentos que demoram mais do que alguns minutos devem ser evitados de serem iniciados através de uma conexão de interface, afinal se esta conexão cair por qualquer razão durante o processamento, normalmente em até 3 minutos o APPlication Server deve perceber que a conexão com o SmartClient foi perdida, e assim que isso acontecer, ele vai encerrar a execução da aplicação, não importa se ela tenha terminado o seu processamento ou não. Em ambientes intranet, onde pressupõe-se uma estabilidade maior na conexão, isto pode não ser tão impactante, mas em um acesso remoto ou via internet, isso pode ser um pouco traumático.

Para situações como essas, existem relatórios que podem ser executados no sistema ERP em Job (processo sem interface), ou colocados para serem executados em um Scheduler (agendador) do ERP, onde você pode programar a execução de um relatório ou recalculo que vai demorar muitas horas, e programá-lo para ser executado durante a noite, o processamento não vai depender de interface, e se tudo correr bem, estará pronto na manhã seguinte.

As boas práticas de interface vão um pouco além do código, vão desde a disposição dos componentes em tela de forma visualmente agradável e lógica, dada a sequência da operação do sistema, até a coerência e clareza das mensagens e informações solicitadas aos usuários ou operadores durante o uso da aplicação. Nem sempre uma mensagem que é muito clara para o programador é entendida pelo usuário. Normalmente colca-se uma mensagem com teor explicativo ao operador do sistema, e em um segundo ponto da mensagem, informações técnicas úteis para o departamento de suporte e programação.

Existem ainda outras boas práticas da programação para a interface AdvPL. Uma delas é não interromper uma operação transacionada na base de dados com uma operação de interface que aguarde uma decisão de usuário. Isso mantém a transação aberta por tempo indeterminado, e mantém o bloqueio (lock) de registros na base de dados, o que pode prejudicar outros processos concorrentes. Caso o operador tenha iniciado um processo e saído de sua estação para tomar um café, ele somente vai ver a mensagem quando ele voltar, e durante este tempo registros da base de dados permaneceram bloqueados, impedindo outros processos concorrentes que dependam do bloqueio nestes registros para serem executados. Normalmente a operação transacionada em base de dados é realizada usando as instruções BEGIN TRANSACTION e END TRANSACTION. Vamos entrar nesse assunto em maior profundidade em posts posteriores sobre a o acesso a tabelas e dados no AdvPL.

Exemplo AdvPL

E, quase no final do tópico, vamos a mais um exemplo AdvPL, de uma rotina que procura obter o máximo de informações possíveis do SmartClient em execução, que iniciou o programa atual. Segue fonte abaixo:

#include 'protheus.ch'
/* ======================================================================
Função U_RmtDet()
Autor Júlio Wittwer
Data 01/02/2015
Descrição Monta uma caixa de diálogo com todas as informações possíveis
de se obter do SmartClient que iniciou este programa.
====================================================================== */
User Function RmtDet()
Local oDlg
Local nRmtType
Local cRmtType
Local cRmtLib
Local oFont
Local cInfo := ''
// Habilita interface com data mostrada com 4 digitos no ano
// e Habilita data em formato britânico ( Dia/Mes/Ano )
SET CENTURY ON
SET DATE BRITISH
cRmtLib := ''
nRmtType := GetRemoteType ( @cRmtLib )
DO CASE
 CASE nRmtType == -1
 UserException("Invalid JOB/BLIND call to U_RmtDet")
 CASE nRmtType == 0
 cRmtType := 'SmartClient Delphi'
 CASE nRmtType == 1
 cRmtType := 'SmartClient QT Windows'
 CASE nRmtType == 2
 cRmtType := 'SmartClient QT Linux/Mac'
 CASE nRmtType == 5
 cRmtType := 'SmartClient HTML'
 OTHERWISE
 cRmtType := 'Remote Type '+cValToChar(nRmtType)
ENDCASE
If !empty(cRmtLib)
 cRmtType += ' ('+cRmtLib+')'
Endif
// Usa uma fonte mono-espaçada 
DEFINE FONT oFont NAME 'Courier New'
// Cria uma caixa de diálogo com área util de 640x480 PIXELs
DEFINE DIALOG oDlg TITLE (cRmtType) FROM 0,0 TO 480,640 PIXEL
// Informações da Interfae remota
cRmtBuild := GetBuild(.T.)
cRmtIp := GetclientIP()
cUsrName := LogUserName()
dRmtDate := GetRmtDate()
cRmtTime := GetRmtTime()
aRmtInfo := GetRmtInfo()
cRmtTmp := GetTempPath(.T.)
lActivex := IsPlugin()
lSSLConn := IsSecure()
cInfo += 'SmartClient Build ....... ' + cRmtBuild + CRLF
cInfo += 'SmartClient Activex ..... ' + IIF(lActivex,"SIM","NAO") + CRLF 
cInfo += 'SmartClient Connection .. ' + IIF(lSSLConn ,"SSL","TCP") + CRLF 
cInfo += 'Remote IP ............... ' + cRmtIp + CRLF 
cInfo += 'Remote User Name ........ ' + cUsrName + CRLF 
cInfo += 'Remote DateTime ......... ' + dtoc(dRmtDate)+' '+cRmtTime + CRLF
cInfo += 'Remote Temp Path ........ ' + cRmtTmp + CRLF
cInfo += 'Remote Computer Name .... ' + aRmtInfo[1] + CRLF
cInfo += 'Remote O.S. ............. ' + aRmtInfo[2] + CRLF
cInfo += 'Remote O.S. Detais ...... ' + aRmtInfo[3] + CRLF
cInfo += 'Remote Memory (MB) ...... ' + aRmtInfo[4] + CRLF
cInfo += 'Remote CPU Count ........ ' + aRmtInfo[5] + CRLF
cInfo += 'Remote CPU MHZ .......... ' + aRmtInfo[6] + CRLF
cInfo += 'Remote CPU String ....... ' + aRmtInfo[7] + CRLF
cInfo += 'Remote O.S. Language .... ' + aRmtInfo[8] + CRLF
cInfo += 'Remote Web Browser ...... ' + aRmtInfo[9] + CRLF
// Coloca a string com os dados dentro de um Get Multiline
@ 5,5 GET oGet1 VAR cInfo MULTILINE FONT oFont SIZE 310,230 OF oDlg PIXEL

ACTIVATE DIALOG oDlg CENTER

Return

O resultado obtido com a execução do programa deve ser algo parecido com a caixa de diálogo abaixo:

Exemplo da execução do programa U_RmtDet

Exemplo da execução do programa U_RmtDet

Conclusão

Como eu já havia dito, com AdvPL podemos fazer muitas coisas, inclusive um ERP. Eu (ainda) não tenho todas as demais interfaces de SmartClient configuradas no meu ambiente, então pode ser que alguma das funções utilizadas no exemplo acima retornem conteúdos em branco ou apresentem erro de execução caso este exemplo seja executado dentro de um SmartClient ActiveX ou SmartClient HTML. De qualquer modo, eles foram concebidos para serem capazes de renderizar a interface da mesma forma.

Muitas vezes o arroz com feijão será usado na maioria das rotinas e interfaces do sistema, porém algumas rotinas podem precisar de algo um pouco mais específico, um pouco mais rebuscado. E a linguagem é capaz de prover isso. Até o próximo post, pessoal 😉

Referências

http://tdn.totvs.com/display/tec/GetRemoteType
http://tdn.totvs.com/display/tec/GetBuild
http://tdn.totvs.com/display/tec/GetComputerName
http://tdn.totvs.com/display/tec/GetRmtDate
http://tdn.totvs.com/display/tec/GetRmtTime
http://tdn.totvs.com/display/tec/GetRmtInfo
http://tdn.totvs.com/display/tec/GetTempPath
http://tdn.totvs.com/display/tec/IsPlugin
http://tdn.totvs.com/display/tec/IsSecure
http://tdn.totvs.com/display/tec/LogUserName