Introdução
No post anterior, colocamos mais um botão na interface para enviar e-mail. Agora, vamos colocar uma foto 3×4 para cada contato da agenda.
Novo campo na base
Inicialmente, vamos criar um novo campo na tabela da Agenda, para armazenar a imagem. No caso, vamos usar um campo do tipo “M” Memo do AdvPL, que por default aceita armazenar e recuperar conteúdos binários (bytes de valor 0 a 255, inclusive bytes que não têm representação no CP1252 — usado pelo Protheus).
// Cria o array com os campos do arquivo
aadd(aStru,{"ID" ,"C",06,0})
aadd(aStru,{"NOME" ,"C",50,0})
aadd(aStru,{"ENDER" ,"C",50,0})
aadd(aStru,{"COMPL" ,"C",20,0})
aadd(aStru,{"BAIRR" ,"C",30,0})
aadd(aStru,{"CIDADE","C",40,0})
aadd(aStru,{"UF" ,"C",02,0})
aadd(aStru,{"CEP" ,"C",08,0})
// Novos campos inseridos em 07/10
aadd(aStru,{"FONE1" ,"C",20,0})
aadd(aStru,{"FONE2" ,"C",20,0})
aadd(aStru,{"EMAIL" ,"C",40,0})
// Novos campos inseridos em 21/10
aadd(aStru,{"IMAGE" ,"M",10,0})
Lembrando que o fonte original possui uma implementação para, caso detectada alguma diferença na estrutura da tabela no momento da abertura, a aplicação automaticamente vai executar um TC_ALTER() para inserir o novo campo.
Inserindo a imagem padrão no programa
A partir desse momento, no espaço reservado da interface onde vamos mostrar a foto do contato da agenda, caso o contato ainda não tenha foto, vamos mostrar uma imagem de 120 x 160 pixels — razão 3×4 — com fundo branco, e o texto “Photo 3×4”. Foi relativamente simples montar esta imagem e salvá-la no formato PNG, ela ocupou míseros 1090 bytes — menos que 1KB.
Para passar a usar esta imagem no programa, podemos proceder de várias formas. Uma delas é distribuir a imagem junto do programa, criar uma pasta ou diretório chamado “images” (por exemplo) a partir do RootPath, e salvar a imagem nesta pasta.
Outra forma seria acrescentar a imagem dentro do Repositório de Objetos (RPO) da aplicação como um “resource”. E, de dentro do programa, fazer a carga deste resource diretamente como uma imagem.
Existe ainda a terceira forma, que seria criar um fonte AdvPL que fosse capaz de criar esta imagem. Esta é a forma mais interessante, e que foi adotada neste exemplo:
STATIC Function RAW3x4()
Local cRaw := ''
cRaw += HEx2Bin('89504E470D0A1A0A0000000D4948445200000078000000A00802000000486B3FC700000001735247')
cRaw += HEx2Bin('4200AECE1CE90000000467414D410000B18F0BFC61050000000970485973000012740000127401DE')
cRaw += HEx2Bin('661F78000003D749444154785EEDDC6D5AEA30140061F7D3F5B01FD7C37AD88F3760494E3E8A14CB')
cRaw += HEx2Bin('542E33BF34C4206F43451FEAC79721090D253494D0504243090D253494D0504243090D253494D050')
cRaw += HEx2Bin('4243090D253494D0504243090D253494D0504243090D253494D0504243090D253494D0504243090D')
cRaw += HEx2Bin('253494D0504243090D253494D0504243090D253494D0504243090D253494D0504243ED097D3A1E3E')
cRaw += HEx2Bin('BE9B3E4FF3D87FDB33A0B35F6C9AA6C3E7B1F23C7D4ED71B857EA0E2372A983E0DBA2C7C38CE43BB')
cRaw += HEx2Bin('8743A7AE8F5EE8DF15A0F3E33C1DA3FE3C2CF4EF1A41A7C2F0EC5A43A71F8D53FEBC3D9DCF9DD201')
cRaw += HEx2Bin('2BB352DF27FE3075F8F321558BFFBCCEE671D0C1A0874ECD1F96DADD980EC47C4BD794E72E4187E7')
cRaw += HEx2Bin('CC5DEB6CDEDFD8D1E302CE8F73AF77F5D38EBE779DCDC3CED1611B5D01EB079DF50B55910E7E69D7')
cRaw += HEx2Bin('5D4787ABA6CAC22DDBBA75B6ECB9D0C38A7E99181F5D19CEA3DD5927375C63117AE53A5B4643C747')
cRaw += HEx2Bin('31103DD70F2F4CBC14EE6D70046BE8B5EB6C19053DF8C570E971F7C36523F60623A03256CF5FBBCE')
cRaw += HEx2Bin('96713F0CFB7AD14BFDF0F2337EBC48195B82BE6F9D2D7B09E8B062AB30BCB332587F036BD7D9B297')
cRaw += HEx2Bin('808E4BA6C1F1AB85705FD57770993CFF2EB2729D2D7B0DE8F8AC1F57EFD07EF6F5F675EB6CD8AB40')
cRaw += HEx2Bin('9F89F2B26DFD2F749D67596AD53ADBF554E8F4EC9CC786A5676C9E18F7D1C270FA82157FA2389F0E')
cRaw += HEx2Bin('F2CC342D4EDAE14F1DCF80B6514243090D253494D0504243090D253494D0504243090D253494D050')
cRaw += HEx2Bin('4243090D253494D0504243090D253494D0504243090D253494D0504243090D253494D0504243ED07')
cRaw += HEx2Bin('7D7EEBECA1BA6276703DD123D56F806EDFFABB573B41DF7AE3FDEFDE0DDEAEFCDED0E1A280518F53')
cRaw += HEx2Bin('F70750E874A2086FB2AFE91F940E970AE40B9DDFFCD43128525FA1CB068D97979499D501A98683B9')
cRaw += HEx2Bin('D07501BAE08C06C7868DBED0C3D22B900CDAECD3B2A9E7F13C10A7759B5CE85CD8ACA1E6FAA954BD')
cRaw += HEx2Bin('A9F36711B04CC9A342E746D0E357D26153E7E2761EA10A9D1BEFE84B1D4E4B1D270CF7B8D00B9DAA')
cRaw += HEx2Bin('CBB17B9E463AECE71BC72BB43FF71F81BE14352B991EB3DC2CF4FA16A00365F8D348DED4ED6965D8')
cRaw += HEx2Bin('9B429FE99AABAFD3CB8978EE08300532D946F49B76F9ABF627FE6E37E89B957370993A8B850D1C5F')
cRaw += HEx2Bin('79B4097DE9E6D33DD084239255EFDBD442CFCDFFC921EEECFE7F3994E311B9EE9216FA5D131A4A68')
cRaw += HEx2Bin('28A1A18486121A4A6828A1A18486121A4A6828A1A18486121A4A6828A1A18486121A4A6828A1A184')
cRaw += HEx2Bin('86121A4A6828A1A18486121A4A6828A1A18486121A4A6828A1A18486121A4A6828A1A18486121A4A')
cRaw += HEx2Bin('6828A1A18486121A4A6828A1A18486121A4A6828A1A18486121AE9EBEB1FDCCEA468FA802AA60000')
cRaw += HEx2Bin('000049454E44AE426082')
Return cRaw
STATIC function HEx2Bin(cHex)
Local cBin := ''
For nI := 1 to len(cHex) STEP 2
cBin += chr(__HEXTODEC(substr(cHex,nI,2)))
Next
Return cBin
Usando um WebSite que gera o código Hexadecimal dos bytes de um arquivo, eu gerei um “Dump” em hexadecimal da imagem no formato PNG, e criei um fonte AdvPL capaz de remontar a imagem a partir destas informações, convertendo os valores hexadecimais em pares, para chegar ao valor do Byte original, e somando estes bytes em uma variável do tipo caractere no AdvPL.
A função que faz a conversão é a Hex2Bin(), que usa internamente a função __HexToDec() do binário, que converte um valor em String hexadecimal para numérico (decimal).
Acrescentando a imagem na tela
Vamos aproveitar a área abaixo do botão “Sair” para colocar a foto do contato. Para isso, acrescentamos as seguintes linhas no código, logo abaixo da criação do botão “Sair” — lembrando de declarar a variável oBmpFoto como “Local” no fonte da STATIC Function doInit(oDlg)
@ 65,05 BUTTON oBtn5 PROMPT "Sair" SIZE 60,15 ;
ACTION oDlg:End() OF oPanelMenu PIXEL
@ 90,05 BITMAP oBmpFoto OF oPanelMenu PIXEL
oBmpFoto:nWidth := 120
oBmpFoto:nHeight := 160
oBmpFoto:lStretch := .T.
Agora, vamos passar a variável oBmpFoto como último parâmetro para todas as chamadas da STATIC Function ManAgenda()., inclusive na declaração desta função, recebendo o objeto no parâmetro nomeado oBmpFoto. E, por fim, acrescentar o botão para alterar a imagem 3×4 do contato atual, logo abaixo do botão “G-Mail”:
@ 125,05 BUTTON oBtnImg PROMPT "Foto 3x4" SIZE 60,15 ;
WHEN ( nMode == 4 ) ; // Editar foto disponivel apenas durante a consulta
ACTION ChgImage(oDlg,aBtns,aGets,oBmpFoto) OF oPanelNav PIXEL
aadd(aBtns,oBtnImg) // [15] Foto 3x4
Lembram-se da função SetNavBtn(), que habilitavam ou desabilitavam os botões de navegação do lado direito da tela, fazendo um SetEnable() diretamente no painel, habilitando ou desabilitando todos os botões? Bem, como vamos poder ter alguns botões com controle de habilitação independente, a função foi alterada para atuar apenas do botão 7 ao 14:
STATIC Function SetNavBtn(aBtns,lEnable)
Local nI
For nI := 7 to 13
aBtns[nI]:SetEnable(lEnable)
Next
Return
Disparando a atualização da imagem
Como os dados sobre o contato mostrado na tela é carregado pela função ReadRecord(), vamos inserir manualmente após cada chamada da função ReadRecord() a chamada da função ShowImg(), responsável por atualizar a foto na tela.
// Atualiza na tela o conteudo do registro atual
ReadRecord(aGets)
// Mostra a imagem do contato
ShowImg(oBmpFoto)
E, finalmente, vamos a função que faz a mágica, a função ShowImg():
STATIC Function ShowImg(oBmpFoto)
Local cTmpPath
Local nH
Local cRAWImage
Local cId
// Lê o campo memo com o conteudo da imagem
// e o ID do contato da agenda
cId := AGENDA->ID
cRAWImage := AGENDA->IMAGE
If empty(cRawImage)
// Contato sem imagem, cria um cache em disco da imagem padrão
cId := 'photo_3x4'
cTmpPath := "\temp\tmp_"
cTmpPath += cID
cTmpPath += ".img"
if !file(cTmpPath)
nH := fCreate(cTmpPath)
// grava no disco o conteúdo binário da imagem
fWrite(nH,RAW3x4())
fclose(nH)
Endif
Else
// Contato com imagem, cria um cache da imagem usando o ID do contato
cTmpPath := "\temp\tmp_"
cTmpPath += cID
cTmpPath += ".img"
if !file(cTmpPath)
nH := fCreate(cTmpPath)
fWrite(nH,cRawImage)
fclose(nH)
Endif
Endif
oBmpFoto:Load(,cTmpPath)
Return
A função é relativamente simples, e ainda está sujeita a melhorias. Ela depende apenas da criação da pasta “\temp\” a partir do RootPath do ambiente, pois ela será usada exatamente para fins temporários. Por hora o componente tBitmap aceita realizar a carga de uma imagem do disco, ou de um RESOURCE compilado no RPO. Como o conteúdo binário da imagem foi gravado no banco de dados, ao ser recuperado precisamos criar um arquivo em disco para ser carregado.
Neste caso, lemos a imagem do Banco de Dados, e caso o arquivo em disco ainda não exista , ele é criado na hora usando o conteúdo da imagem, e o nome do arquivo usa o código identificador do contato, para não precisar ficar criando e apagando o mesmo arquivo várias vezes, também servindo de “Cache” para as fotos armazenadas nesta tabela.
Atribuindo uma imagem ao contato
Quase que eu esqueço do principal, a função ChgImage(), para permitir atribuir ou remover uma imagem a um contato. Vejamos:
STATIC Function ChgImage(oDlg,aBtns,aGets)
Local cTitle := 'Escolha uma imagem'
Local oDlgImg
Local cFile := space(40)
Local lOk := .F.
Local aFInfo
DEFINE DIALOG oDlgImg TITLE (cTitle) ;
FROM 0,0 TO 120,415 PIXEL;
FONT oDlg:oFont ;
OF oDlg ;
COLOR CLR_BLACK, CLR_HBLUE
@ 05,05 GET oGet1 VAR cFile SIZE CALCSIZEGET(40),12 OF oDlgImg PIXEL
@ 25,05 BUTTON oBtn1 PROMPT "Buscar" SIZE 60,15 ;
ACTION (BuscaFile(@cFile)) OF oDlgImg PIXEL
@ 25,85 BUTTON oBtn2 PROMPT "Ok" SIZE 60,15 ;
WHEN empty(cFile) .or. File(alltrim(cFile)) ;
ACTION ( lOk := .T. , oDlgImg:End() ) OF oDlgImg PIXEL
ACTIVATE DIALOG oDlgImg CENTER
IF lOk
cFile := alltrim(cFile)
aFInfo := Directory(cFile)
If len(aFInfo) > 0
If aFInfo[1][2] > ( 128 * 1024) // Até 128 KB
MsgStop("Arquivo muito grande ("+str(aFInfo[1][2]/1024,8,2)+" KB)","Imagem maior que 128 KB")
return
Endif
Else
MsgStop('Arquivo não encontrado',cFile)
return
Endif
// Chegou ate aqui, atualiza o campo memo
cMemoImg := AGENDA->IMAGE
If !empty(cMemoImg)
lOk := MsgYesNo("Este contato já tem uma foto. Deseja substituí-la ?")
Endif
If lOk
// Lê a imagem do disco
cMemoImg := ReadFile(cFile)
If empty(cMemoImg)
Return
Endif
DBSelectArea("AGENDA")
If DbrLock(recno())
AGENDA->IMAGE := cMemoImg
DBRUnlock()
MsgInfo("Imagem atualizada.")
Else
// Nao conseguiu bloqueio do registro
// Mostra a mensagem e permanece no modo de alteração
MsgStop("Registro não pode ser alterado, está sendo usado por outro usuário")
Endif
Endif
Endif
Return
A imagem escolhida não pode ter mais que 128 KB — é uma foto 3×4 de 120 x 160 pontos, e deve ser pequena para permitir recuperação e desenho de interface rápidos.
E, por final a função auxiliar ReadFile() que lê um arquivo binário do disco e retorna seu conteúdo em uma String AdvPL a seguir:
STATIC Function ReadFile(cFile)
Local cBuffer := ''
Local nH , nTam
nH := Fopen(cFile)
IF nH != -1
nTam := fSeek(nH,0,2)
fSeek(nH,0)
cBuffer := space(nTam)
fRead(nH,@cBuffer,nTam)
fClose(nH)
Else
MsgStop("Falha na abertura do arquivo ["+cFile+"]","FERROR "+cValToChar(Ferror()))
Endif
Return cBuffer
Pronto
Com tudo isso setado compile os fontes execute, teste,etc. Lembre-se de criar a pasta temp a partir do RootPAth do ambiente. Após entrar na Agenda e acessar a consulta, será mostrado o primeiro contato, com a imagem padrão.
Agora, clicamos no botão “Foto 3×4”, e será apresentada a caixa de diálogo abaixo:
No campo acima, você pode informar manualmente o caminho completo seguido do nome do arquivo de imagem a ser carregado — formatos BMP, PNG, JPG, TIFF — ou usar o botão “Buscar”
O Botão de busca abre a interface acima, para escolhermos um arquivo de imagem a partir do RootPath do servidor. Após selecionar o arquivo desejado, clique no botão de confirmação — esse da esquerda com um “v” vezinho verde. A caixa de diálogo de busca de arquivos será fechada, e o campo do formulário anterior será preenchido com o caminho e nome do arquivo escolhido.
Agora, ao clicarmos no botão OK, a imagem será carregada e salva no campo memo chamado “IMAGE” do contato atualmente na tela. Em caso de sucesso, será mostrada a imagem abaixo:
Ao fechar esta caixa de diálogo, a nova foto atribuída ao contato é mostrada na tela.
Ao usar os botões de navegação, cada contato posicionado mostrará a foto correspondente.
Conclusão
Não foi fácil chegar ao fim desta implementação, durante os testes vários comportamentos estranhos e ajustes foram necessários. Por exemplo, o componente BITMAP possui uma otimização, para evitar carregar o mesmo arquivo duas vezes. Quando eu resolvi trocar a foto, o arquivo temporário no disco precisava ser apagado e recriado, mas o fato dele usar o mesmo nome, fazia com que a foto não fosse recarregada. Contornei este comportamento simplesmente carregando a foto padrão antes de recarregar a nova foto após a alteração, além de criar o arquivo com a imagem default apenas uma vez na entrada da agenda, e criar uma função para apagar o arquivo do cache em disco ao inserir ou alterar uma foto. Nos próximos POSTS, vamos incrementar mais um pouco este programa !!!!
*** Não entre em pânico, entre no GITHUB e pegue a versão final deste fonte ***
Desejo a todos novamente TERABYTES DE SUCESSO 😀
Referências