Olá, recruta
Neste post iremos dar algumas dicas de usabilidade criando um Listbox estilo
WhatsApp.
Mas o que exatamente queremos dizer isso?
Muitas vezes desenvolvedores colocam botões de excluir, editar e outras funcionalidades em cada um dos itens do Listbox, desse forma repetindo cada um deles em cada registro da lista. Se repararmos no WhatsApp as únicas opções de um item é clicarmos nele para irmos para a tela de mensagens do contato, e a seleção do item ao manter-lo pressionado, que é justamente quando as demais opções aparecem. E é exatamente isso que iremos fazer, ao clicar em um item da nossa lista ir para uma aba de detalhe do item e habilitar botões como excluir ou alguma outra opção apenas quando houver um ou mais itens selecionados.
Vamos especificar exatamente nossas funcionalidades, nosso Listbox terá o seguinte comportamento:
- Ao clicar em um item irá para a tela de detalhe do item clicado;
- Ao segurarmos pressionado um item ele será selecionado;
- Quando um item já estiver selecionado ao clicar em um outro item ele também
será selecionado;
- Ao clicar novamente em um item selecionado a seleção do item é desfeita.
- Ao “deselecionar” o último item selecionado, o comportamento de click dos
itens irá voltar ao normal, ou seja, ao clicar irá para tela de detalhe;
- Quando um ou mais registros estiverem selecionados iremos exibir o botão de
excluir e um botão de deselecionar todos.
Ele terá essa aparência:
![]() |
(Tela com o Listbox implementado) |
Antes de tudo, quero deixar claro que esta é uma das formas que encontrei para criar esta usabilidade, é possível chegar no mesmo resultado de diversas formas, fique a vontade para adaptar a técnica a sua maneira, e se encontrar uma forma mais eficiente de chegar neste resultado, por favor não deixa de compartilhar com a comunidade Delphi.
Então vamos lá, bora criar uma nova aplicação mobile.
Clique no menu, File -> New -> Multi-Device Application – Delphi.
![]() |
(File -> New -> Multi-Device Application – Delphi) |
Na próxima janela selecione a opção Blank Application, para criarmos uma Aplicação em branco.
![]() |
(Blank Application) |
Vamos aos nomes!
Vou chamar nosso projeto de ListBoxWhatsApp.dproj, nossa Unit de FListBoxWhatsApp.pas e o Form irei renomear para frmListBoxWhatsApp.
Em nosso Form vamos colocar um TTabControl com dois TabItem.
No primeiro TabItem vamos colocar um TLayout alinhado ao topo e um TListBox
com alinhamento AlClient, esse será o ListBox que faremos tudo
acontecer.
Renomearemos o ListBox para lbxWhatsapp;
Já faz algum tempo que o Delphi trabalha com o conceito de estilos, e é isso que iremos fazer, criar um estilo para os ListBoxItems do lbxWhatsapp.
Adicione um novo ListBoxItem ao Listbox e click com o botão direito do mouse
e selecione a opção Edit Custom Style...
![]() |
(Edit Custom Style) |
Seremos direcionados ao Style Designer, onde será criado um TLayout com o nome do seu ListBoxItem acrescido de “Style1”, no nosso caso ficou o nome ficou “ListBoxItem1Style1”, esse TLayout representará o nosso Style customizado. Vamos aproveitar e renomear a propriedade StyleName do Layout para stlWhatsApp, o StyleName é a propriedade que representa o nome do componente no Style Designer. É pelo StyleName que iremos acessar os componentes do Style Designer em nosso formulário.
![]() |
(Style Designer) |
Agora é hora de montar nosso layout como consta na imagem a seguir.
![]() |
(stlWhatsApp) |
Abaixo segue a lista de componentes que usamos e os StyleNames que foram
dados:
stlWhatsApp = TLayout
ActiveStyleObject= TActiveStyleObject
recFundo
= TRectangle
cirIniciais = TCircle
txtIniciais =
TStyleTextObject
layTexto = TLayout
txtTitulo =
TStyleTextObject
txtDetalhe = TStyleTextObject
linSeparador =
TLine
imgSelecionado = TImage
![]() |
(Componentes contidos em nosso Layout) |
![]() |
(Structure completo do Style) |
Você pode baixar o arquivo com o layout neste link, basta copiar o texto
e colar no StyleContainer:
Quando criamos um layout customizado temos que ter alguns cuidados, um deles é que os componentes colocados nele também podem reagir a eventos como clicks e gestures, isso faz como que eventos do ListboxItem e Listbox possam não ser executados. Por exemplo, se usarmos um retângulo como fundo para nosso Style, os eventos do retângulo terão prioridade sobre os eventos do ListboxItem, fazendo com que eventos como OnIntemClick e OnGesture do Listbox não funcionem.
Mas tem uma solução, basta desabilitar a propriedade HitTest.
HitTest define se um controle irá reagir a eventos do mouse ou não. No
caso de um mobile, os eventos de touch, já que seu dedo é o mouse!
Então, como não vamos precisar de nenhum evento no nosso Style, garanta
que a propriedade HintTest dos componentes contidos nele estejam
desabilitadas.
Agora que já desenhamos nosso style e garantimos que ele não irá
atrapalhar os eventos do nosso Listbox vamos voltar a nossa programação
normal.😜
Vamos começar criando uma pequena lista de objetos que iremos utilizar
para popular nosso Listbox, estes objetos terão apenas os campos que
iremos utilizar para nosso exemplo.
1 2 3 4 5 6 7 8 9 10 11 12 | TRegistro = class private FId: Integer ; FTitulo: string ; FDetalhe: string ; FIniciais: string ; public property Id: Integer read FId write FId; property Titulo: string read FTitulo write FTitulo; property Detalhe: string read FDetalhe write FDetalhe; property Iniciais: string read FIniciais write FIniciais; end ; |
Para nossa lista iremos utilizar um TObjectList genérico da Unit Generics.Collections;
Dessa forma iremos declarar nossa variável, que será global, e chamaremos
de FListaRegistro, na seção private da nossa unit;
FListaRegistro: TObjectList<TRegistro>;
No evento OnCreate do nosso formulário iremos instanciar nossa lista e
populá-la com alguns registros.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 | procedure TfrmListboxWhatsApp . FormCreate(Sender: TObject); var lRegistro: TRegistro; begin FListaRegistro := TObjectList<TRegistro>.Create; lRegistro := TRegistro . Create; lRegistro . Id := 1 ; lRegistro . Iniciais := 'BW' ; lRegistro . Titulo := 'Bruce Wayne' ; lRegistro . Detalhe := 'Batman - Cavaleiro das Trevas' ; FListaRegistro . Add(lRegistro); lRegistro := TRegistro . Create; lRegistro . Id := 2 ; lRegistro . Iniciais := 'CK' ; lRegistro . Titulo := 'Clark Kent' ; lRegistro . Detalhe := 'Superman - Homem de Aço' ; FListaRegistro . Add(lRegistro); lRegistro := TRegistro . Create; lRegistro . Id := 3 ; lRegistro . Iniciais := 'DP' ; lRegistro . Titulo := 'Diana Prince' ; lRegistro . Detalhe := 'Mulher Maravilha - Princesa de Themysira' ; FListaRegistro . Add(lRegistro); lRegistro := TRegistro . Create; lRegistro . Id := 4 ; lRegistro . Iniciais := 'HJ' ; lRegistro . Titulo := 'Hal Jordan' ; lRegistro . Detalhe := 'Lanterna Verde - Cavaleiro Esmeralda' ; FListaRegistro . Add(lRegistro); lRegistro := TRegistro . Create; lRegistro . Id := 5 ; lRegistro . Iniciais := 'BA' ; lRegistro . Titulo := 'Barry Allen' ; lRegistro . Detalhe := 'Flash - Velocista Escarlate' ; FListaRegistro . Add(lRegistro); end ; |
Agora que temos nossa lista vamos usá-la para popular nosso ListBox. Para
isso iremos criar uma procedure chamada PopularListBox, conforme o fonte a
seguir;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | procedure TfrmListboxWhatsApp . PopularListbox; begin lbxWhatsapp . BeginUpdate; try lbxWhatsapp . Clear; for var lRegistro in FListaRegistro do begin var lListBoxItem := TListBoxItem . Create(lbxWhatsapp); lListBoxItem . Height := 65 ; lListBoxItem . Parent := lbxWhatsapp; lListBoxItem . StyleLookup := 'stlWhatsApp' ; lListBoxItem . StylesData[ 'txtTitulo' ] := lRegistro . Titulo; lListBoxItem . StylesData[ 'txtDetalhe' ] := lRegistro . Detalhe; lListBoxItem . StylesData[ 'txtIniciais' ] := lRegistro . Iniciais; lListBoxItem . StylesData[ 'imgSelecionado.visible' ] := False ; lListBoxItem . Tag := integer (lRegistro); end ; finally lbxWhatsapp . EndUpdate; end ; end ; |
Vamos analisar este código,
Sempre que vamos fazer uma alteração em nosso Listbox é importante chamar
o método BeginUpdate antes das alterações de layout, isso irá evitar que o
Listbox fique se redesenhando a todo momento, gastando processamento
desnecessário. Quando tudo estiver pronto, finalizamos com o método
EndUpdate, para que todo o desenho seja feito de uma única vez.
Percorremos nossa lista através de um for-in para criarmos os itens de
nosso ListBox.
Um recurso recente do Delphi (Inline Variable Declaration) permite que eu declare minhas variáveis locais diretamente no código,
eu particularmente gosto muito desse recurso, uma vez que estas variáveis
tem uma vida apenas no contexto onde são declarados, ou seja, ele irá
existir apenas dentro do bloco begin-end onde foram declaradas, no nosso
caso, nenhuma existirá ou será acessada fora do bloco do loop.
Para cada ListboxItem que criarmos iremos atribuir o Style que criamos
através da propriedade StyleLookup.
Os componentes utilizados em nosso Style não estão declarados em nenhuma
Unit, então não podemos acessa-lo como fazemos normalmente com qualquer
outro componente, sendo assim a forma que a Embarcadero criou para que
possamos acessa-los foi através de recursos de RTTI, as propriedades e
eventos dos componentes do Style poderão ser alterados através de uma
declaração em forma de string pela propriedade StylesData do
ListBoxItem.
Observe que para atribuir valor para a propriedade visible do componente
imgSelecionado escrevemos “imgSelecionado.visible” enquanto que, para
atribuir a propriedade texto dos componentes TStyleTextObject apenas
passamos o nome do componente. Isso ocorre porque a propriedade Text é a
propriedade Default do TStyleTextObject, e pode ser suprimida ao passarmos
como parâmetro para o StylesData.
Uma coisa que eu gosto de fazer quando trabalho com lista de objetos e Listbox, é atribuir o ponteiro do item da lista do objeto a propriedade Tag do ListboxItem, isso facilita quando quisermos buscar quaisquer informação do objeto, muito mais eficiente que atribuir apenas um id e depois ter que percorrer a lista de objetos para localizar o item em questão.
Para exemplificar essa técnica, criei uma segunda aba (tbiDetalhe) no
nosso PageControl (pgcPrincipal) onde será exibido os dados do contato que
estão em nosso objeto. Ao clicar em nosso ListBoxItem vamos para a pagina
de detalhe e populamos os Labels contidos lá.
1 2 3 4 5 6 7 8 9 10 11 12 13 | procedure TfrmListboxWhatsApp . lbxWhatsappItemClick( const Sender: TCustomListBox; const Item: TListBoxItem); begin if (pItem . Tag <> 0 ) then begin var lRegistro := TRegistro(pItem . Tag); lblId . Text := lRegistro . Id . ToString; lblIniciais . Text := lRegistro . Iniciais; lblTitulo . Text := lRegistro . Titulo; lblDetalhe . Text := lRegistro . Detalhe; tbcPrincipal . GotoVisibleTab(tbiDetalhe . Index); end ; end ; |
Observe no código acima que basta fazer um type cast da Tag do item
clicado para acessar o objeto referente a ele.
Vamos aproveitar o momento e já refatorar esse método, encapsulando esse
código em método com um nome que condiga com seu propósito.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | procedure TfrmListboxWhatsApp . lbxWhatsappItemClick( const Sender: TCustomListBox; const Item: TListBoxItem); begin MostrarDetalhesDoContato(Item); end ; procedure TfrmListboxWhatsApp . MostrarDetalhesDoContato(pItem: TListBoxItem); begin if (pItem . Tag <> 0 ) then begin var lRegistro := TRegistro(pItem . Tag); lblId . Text := lRegistro . Id . ToString; lblIniciais . Text := lRegistro . Iniciais; lblTitulo . Text := lRegistro . Titulo; lblDetalhe . Text := lRegistro . Detalhe; tbcPrincipal . GotoVisibleTab(tbiDetalhe . Index); end ; end ; |
Assim como o WhatsApp , ao clicarmos no nosso ListboxItem também estamos
sendo direcionados para uma outra aba referente ao item clicado. Nossa
próxima meta é selecionar um item ao segura-lo pressionado.
Para essa interação vamos utilizar um Gesture, para sermos mais específico, um LongTap.
Os Gestures são capturado facilmente no evento OnGesture de qualquer
controle, onde através do parâmetro EventInfo podemos identificar qual
Gesture foi executado.
Sendo assim no evento OnGesture do stlWhatsApp verificamos se o EventInfo.GestureID é igual a igiLongTap e assim efetuamos a seleção do nosso registro.
Como vamos ter múltipla seleção, vamos usar a propriedade isChecked do
ListBoxItem para marcar os itens selecionados.
Criei um método chamado SelecionarItem passando o ListBoxItem como parâmetro, ao seleciona-lo mudamos a cor de fundo e mostramos a imagem de seleção (imgSelecionado), caso o item já esteja selecionado, fazemos a operação inversa.
1 2 3 4 5 6 7 8 9 10 11 | procedure TfrmListboxWhatsApp . SelecionarItem(pListBoxItem : TListBoxItem); begin if pListBoxItem . IsChecked then pListBoxItem . StylesData[ 'recFundo.Fill.Color' ] := TValue . From<TAlphaColor>(TAlphaColorRec . White) else pListBoxItem . StylesData[ 'recFundo.Fill.Color' ] := TValue . From<TAlphaColor>(TAlphaColorRec . Papayawhip); pListBoxItem . StylesData[ 'imgSelecionado.Visible' ] := TValue . From< boolean >( not pListBoxItem . IsChecked); pListBoxItem . IsChecked := not pListBoxItem . IsChecked; pListBoxItem . IsSelected := False ; end ; |
No evento OnGesture do stlWhatsApp vamos identificar o item que está sendo
pressionado e chamar nosso método de seleção. O item sempre é selecionado
quando executamos um LongTap, então podemos pegar o ItemIndex para
identificar o ListBoxItem que queremos marcar a seleção.
1 2 3 4 5 6 7 8 9 | procedure TfrmListboxWhatsApp . lbxWhatsappGesture(Sender: TObject; const EventInfo: TGestureEventInfo; var Handled: Boolean ); begin if EventInfo . GestureID = igiLongTap then begin SelecionarItem(lbxWhatsapp . Selected); FLongTap := True ; end ; end ; |
Por padrão a captura do gesture LongTap não está habilitada, então selecione o ListBox, no Object Inpector na propriedade Touch > InteractiveGestures e habilite o item LongTap.
![]() |
Object Inspector - LongTap |
Mesmo capturando o LongTap, o evento de OnItemClick ainda é executado,
então criamos uma variável global booleana (FLongTap) para que saibamos
que o LongTap foi executado e assim não irmos para a aba de detalhe do
item, fazendo apenas a seleção.
No OnItemClick do ListBox, onde inicialmente vamos para a aba de detalhe
referente ao item clicado, vamos dar uma incrementada, para que se houver
pelo menos um item selecionado, quando clicarmos em um outro item ele
também será selecionado, se por acaso esse item já estiver selecionado, o
item é desmarcado, até não restar mais nenhum item selecionado, e assim a
função de click será restaurada para sua função padrão, que é exibir os
detalhes do item clicado.
Estamos utilizando a propriedade IsCheck para marcar os itens
selecionados, e vamos precisar saber se já temos pelo menos um item
selecionado para nosso ajuste do evento OnItemClick do ListBox.
Posteriormente podemos querer exibir o número de registros selecionados,
então nada melhor que criar um método que nos retorne isso, assim não
teremos que ficar fazendo um loop em vários pontos do nosso código, já que
o Listbox não possui um método que nos retorne essa quantidade.
Abrindo um adendo a nossa implementação, uma técnica muito boa para
implementarmos nosso contador de ListBoxsItens checados é a de
ClassHelper. Essa técnica nos permite adicionarmos métodos a uma classe sem
precisarmos estendê-las, ou seja, podemos adicionar uma função
CheckedCount ao Listbox sem ter que criar uma herança dele.
Para criar um Class Helper, declararmos um novo tipo como “Class helper
for” seguido pela classe que o helper irá se referenciar, no nosso caso um
TListBox.
Veja a seguir nossa declaração na seção Interface.
1 2 3 4 | type TListBoxHelper = class helper for TListBox function CheckedCount: Integer ; end ; |
Logo a baixo nossa implentação.
1 2 3 4 5 6 7 8 9 10 11 | { TListBoxHelper } function TListBoxHelper . CheckedCount: Integer ; begin Result := 0 ; for var li := 0 to Items . Count - 1 do begin if ListItems[li].IsChecked then inc(Result); end ; end ; |
Voltando ao nosso evento OnItemClick, testamos se este click não foi
executado em decorrência do LongTap e caso tenhamos algum item checado nós
executamos o método SelecionarItem, do contrário é chamado o método
MostrarDetalhesDoContato, por fim garantimos que a variável FLongTap será
atribuída com false.
1 2 3 4 5 6 7 8 9 10 11 12 13 | procedure TfrmListboxWhatsApp . lbxWhatsappItemClick( const Sender: TCustomListBox; const Item: TListBoxItem); begin if not FLongTap then begin if lbxWhatsapp . CheckedCount > 0 then SelecionarItem(Item) else MostrarDetalhesDoContato(Item); end ; Item . IsSelected := False ; FLongTap := False ; end ; |
Com isso nossa implementação do ListBox está concluída, mas podemos adicionar mais alguns itens para melhorar ainda mais nossa usabilidade, como por exemplo, ao selecionarmos um item do Listbox podemos realizar uma breve vibração, assim como ocorre no WhatsApp. Vou demonstrar como fazer isso no Android, uma pequena receita de bolo.
Criei o método vibrar passando como parâmetro o tempo de vibração em
milissegundos. Depois basta adicionar esse método no evento OnGesture quando
por executado um LongTap.
1 2 3 4 5 6 7 8 9 10 11 12 13 | procedure TfrmListboxWhatsApp . Vibrar(pTempo: cardinal ); {$IF DEFINED(ANDROID)} var VibratorObj: JObject; Vibrator: JVibrator; { $ENDIF } begin {$IF DEFINED(ANDROID)} VibratorObj := TAndroidHelper . Activity . getSystemService(TJActivity . JavaClass . VIBRATOR_SERVICE); Vibrator := TJVibrator . Wrap((VibratorObj as ILocalObject).GetObjectID); Vibrator . vibrate(pTempo); { $ENDIF } end ; |
Para exibir botões ou a quantidade de itens selecionados basta adicionar seu
código no método SelecionarItem e testar com se
o lbxWhatsapp.CheckedCount é maior que zero para saber se existe algum
item selecionado. No código a seguir eu mostro dois botões se houver alguma
seleção, um botão de exclusão e um para desmarcar todos.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | procedure TfrmListboxWhatsApp . SelecionarItem(pListBoxItem : TListBoxItem); begin if pListBoxItem . IsChecked then pListBoxItem . StylesData[ 'recFundo.Fill.Color' ] := TValue . From<TAlphaColor>(TAlphaColorRec . White) else pListBoxItem . StylesData[ 'recFundo.Fill.Color' ] := TValue . From<TAlphaColor>(TAlphaColorRec . Papayawhip); pListBoxItem . StylesData[ 'imgSelecionado.Visible' ] := TValue . From< Boolean >( not pListBoxItem . IsChecked); pListBoxItem . IsChecked := not pListBoxItem . IsChecked; pListBoxItem . IsSelected := False ; btnDesmarcar . Visible := lbxWhatsapp . CheckedCount > 0 ; btnExcluir . Visible := lbxWhatsapp . CheckedCount > 0 ; end ; |
Para finalizar segue o código para o click botão de desmarcar todos.
1 2 3 4 5 6 7 8 | procedure TfrmListboxWhatsApp . btnDesmarcarClick(Sender: TObject); begin for var li := 0 to lbxWhatsapp . Items . Count - 1 do begin if lbxWhatsapp . ListItems[li].IsChecked then SelecionarItem(lbxWhatsapp . ListItems[li]); end ; end ; |
Finalizamos aqui nosso conjunto de dicas para criar um Listbox com a cara e o jeito do WhatsApp. O código fonte deste post está em https://github.com/MukaDavid/ListBoxWhatsApp.
Fique ligado no blog que em breve teremos mais novidades.
Ficou interessado em saber mais sobre customizações e utilização de ListBoxs no firemonkey, conheça o treinamento "Exibindo Listagem de Dados no Delphi/Firemonkey" do nosso grande amigo Landerson Gomes na plataforma Eduzz;
Quer ser um programador de Elite?
Nunca serão! 😉