Flet: ListTile e ListView

Outros controles de Layout: ListTile e ListView

Controle ListTile

ListTile é um controle que exibe uma linha de altura fixa que pode conter outros controles, geralmente imagens, ícones, textos, botões e menus popups. Ele admite ícones à direita ou à esquerda do título, podendo seus objetos interiores responder a certos eventos.

Um exemplo mínimo de código aparece listado abaixo, com a ilustração de seu resultado ao ser executado. Um menu popup foi incluído porque esses controles são usados frequentemente juntos com ListTile.

import flet as ft

def main(page):

    lTile1=ft.ListTile(title=ft.Text("Você gosta de animais?", size=18))

    pop_menu=ft.PopupMenuButton(
        icon=ft.icons.MORE_VERT,
        items=[ft.PopupMenuItem(text="Jacaré"), ft.PopupMenuItem(text="Leão")]
    )
    lTile2=ft.ListTile(
        title=ft.Text("Qual é o seu animal favorito?"),
        subtitle=ft.Text("Selecione no menu popup"),
        trailing=pop_menu
    )
    page.add(ft.Card(ft.Column([lTile1, lTile2]), width=300))

ft.app(target=main)

A figura 1 mostra o resultado desse código, renderizado no desktop.

Figura 1:  (a) estado inicial do aplicativo; (b) estado após um clique no controle PopupMenuItem

Propriedades de ListTile

Propriedade Descrição
autofocus booleano, autofocus=True se o controle receberá o foco inicial (o cursor está sobre o controle). Se mais de um controle tiver esse ajuste o primeiro deles (na ordem da página) receberá o foco.
content_padding Espaçamento interno (padding) do controle. Separação entre os controles internos: leading, title, subtitle, e trailing. O default é padding.symmetric(horizontal=16). Veja Container.padding para maiores informações.
dense define se o controle tem exibição densa, com menor altura e caracteres compactos.
is_three_line booleano, se is_three_line=True o controle ListTile deve exibir 3 linhas de texto, e o subtitulo nao pode ser Null, uma vez que ele devera conter as linhas 2 e 3 de texto.Se is_three_line=False o ListTile deve conter uma linhas se subtitle=Null e duas linhas caso contrário.

Se um controle flet.Text for usado para title ou subtitle é possível estabelecer um limite máximo para o número de linhas com Text.max_lines.

leading controle a ser exibido antes (à esquerda) do título.
selected booleano; se selected=True para um tile então os ícones e textos terão a mesma cor. Por default a cor dos elementos em selected é a cor primária do tema.
subtitle Linhas (ou linhas de texto) a serem exibidas abaixo do título. Em geral um flet.Text.Se is_three_line=False o subtítulo não quebrará em linhas longas. Caso contrário deve ser configurado o número máximo de linhas (por exemplo com Text.max_lines).
title controle a ser exibido como conteúdo principal ou título, em geral um flet.Text. Linhas de título não são quebradas e um limite para essa linha única pode ser ajustado com Text.max_lines.
toggle_inputs booleano; define se um clique na ListTile deve abrir (ou fechar) uma caixa de Radio, Checkbox ou Switch. O default é toggle_inputs=False.
trailing controle a ser exibido após (à direita ) do título. Em geral é usado um controle de ícone.
url a URL a ser aberta quando o ListTile é clicado. O evento on_click, se definido, será acionado.
url_target a URL a ser aberta, no modo Web.

  • _blank: (default) – nova janela ou guia.
  • _self: na atual janela aberta.

Eventos de ListTile

Evento Descrição
on_click dispara com um clique (ou toque em touch screens) no controle ListTile.
on_long_press dispara com um clique longo (ou toque longo em touch screens) no controle ListTile

Exemplo de uso de ListTile

import flet as ft

def main(page):
    page.title = "Exemplo de uso do Widget ListTile"
    page.horizontal_alignment=ft.CrossAxisAlignment.CENTER

    def clk(e):
        lt1.data +=1
        lt1.title.value=f"Você clicou {lt1.data} {'vez' if lt1.data==1 else 'vezes'} no título"
        page.update()

    def clicado(e):
        ct_info.content.value=f"Foi clicado o item: {e.control.text}"
        page.update()

    class PI(ft.PopupMenuItem):
        def __init__(self, texto):
            super().__init__()
            self.text=texto
            self.on_click=clicado

    lt1=ft.ListTile(title=ft.Text("Linha única no título", size=18), data=0, on_click=clk)
    lt2=ft.ListTile(title=ft.Text("Uma linha densa nessa listTile"), dense=True)
    lt3=ft.ListTile(
        leading=ft.Icon(ft.icons.DELETE_FOREVER_ROUNDED),
        title=ft.Text("Essa linha abre selecionada"),
        selected=True
    )
    lt4=ft.ListTile(
        leading=ft.ElevatedButton(text="X", bgcolor="#cc8866", on_click=clicado),
        title=ft.Text("Linha com controle anterior"),
        subtitle=ft.Text("Clique para apagar caixa de info")
    )
    lt5=ft.ListTile(
        title=ft.Text("Linha com controle posterior"),
        trailing=ft.PopupMenuButton(
            icon=ft.icons.MORE_VERT,
            items=[PI("Popup 1.1"), PI("Popup  1.2")]
        )
    )
    lt6=ft.ListTile(
        leading=ft.Icon(ft.icons.BATTERY_1_BAR),
        title=ft.Text("Linha com controles anterior e posterior"),
        trailing=ft.PopupMenuButton(
            icon=ft.icons.MORE_VERT,
            items=[PI("Popup 2.1"), PI("Popup  2.2")]
        )
    )
    lt7=ft.ListTile(
        leading=ft.Icon(ft.icons.MENU_BOOK),
        title=ft.Text("Título e subtítulo com controles anterior e posterior"),
        subtitle=ft.Text("Aqui vai um subtítulo"),
        trailing=ft.PopupMenuButton(
            icon=ft.icons.MORE_VERT,
            items=[PI("Popup 3.1"), PI("Popup  3.2")]
        )
    )
    ct_info=ft.Container(
        content=ft.Text("Aqui você verá os itens clicados!"),
        width=500, height=40, bgcolor=ft.colors.AMBER_100,
        alignment=ft.alignment.center,
        border=ft.border.all(1, ft.colors.BLUE_100),
        border_radius = ft.border_radius.all(10)
    )
    titulo=ft.Tooltip(
        message="Exemplo de tooltip",
        content=ft.Text("Informações de cliques:"),
        padding=10
    )

    page.add(
        ft.Container(
            content=ft.Column(
                [lt1, lt2, lt3, lt4, lt5, lt6, lt7, titulo, ct_info],
                spacing=20
            ),
            width=500,
            bgcolor=ft.colors.BLUE_50,
            border=ft.border.all(1, ft.colors.BLUE_500),
            border_radius = ft.border_radius.all(15),
            padding=ft.padding.all(15),
            shadow=ft.BoxShadow(
                blur_radius=15,
                color=ft.colors.BLUE_GREY_500,
                offset=ft.Offset(0, 0),
                blur_style=ft.ShadowBlurStyle.OUTER
            )
        )
    )

ft.app(target=main)
Figura 2: (a) estado inicial do aplicativo; (b) estado após 3 cliques no título e um clique no popup 2.2.

A figura 2 exibe o código em execução.

Um clique no controle lt1 aciona a função clk(e) que incrementa o valor de lt1.data e o exibe em lt1.title.value.


Os controles lt5, lt6 e lt7 recebem em ListTile.trailing um menu popup PopupMenuButton. Cada um deles contém PopupMenuButton.items=[PI("Texto 1"), PI("Texto 2")], onde PI herda da classe PI(ft.PopupMenuItem). Isso cria menus popups que abrem ao serem clicados o ícone à direita, icon=ft.icons.MORE_VERT (três pontos verticais). Todos esses ítens respondem ao evento click abrindo a função clicado. Essa função recebe exibe o texto do controle que enviou o evento em ct_info.content.value. O texto é capturado em e.control.text.

Observe que ct_info.content.value é o texto dentro do container ct_info, um texto e não um objeto flet.Text. O nome do controle clicado está em e.control.text.

Como exemplo foi inserido um Tooltip (ferramenta de dicas) no controle titulo, responsável por exibir uma messagem quando o cursor percorre a área do controle.

Controle ListView

Ainda que seja possível adicionar listas longas nos controles Column e Row esses controles serão pouco eficazes e lentos nessa exibição, principalmente porque adicionam seu conteúdo de uma só vez, mesmo que não estejam visíveis. A solução para essa questão consiste em usar os controles ListView e GridView.

ListView é um controle especialmente útil para a exibição de conteúdo longo. Ele insere controle sucessivamente na direção estabelecida para o rolamento, permitindo percorrer sua extensão automaticamente (com o autoscroll). Controles podem ser adicionados na vertical (o default) ou na horizontal.

Apesar de que ListView insere gradualmente seu conteúdo e renderiza seus filhos com eficiência, o rolamento pode ser melhorado ajustando uma altura fixa para todos os ítens filhos no rolamento vertical, ou largura fixa no rolamento horizontal. Para fazer isso podemos determinar uma altura absoluta na propriedade item_extent ou, alternativamente marcando a propriedade first_item_prototype = True no primeiro filho, o que obriga todos os demais a ter a mesma extensão.

O seguinte exemplo mostra como se pode renderizar rapidamente 5000 linhas de texto com esse controle.

import flet as ft

def main(page: ft.Page):
    lv = ft.ListView(expand=True, spacing=10)
    for i in range(5000):
        lv.controls.append(ft.Text(f"Linha {i+1}"))
    page.add(lv)

ft.app(target=main)


Nesse caso o rolamento não é automático. Para que a tela role junto com inserção de controle modificamos a definição do controle usando:

lv = ft.ListView(expand=True, auto_scroll=True, spacing=10)

.
Note que foi usado expand=True no construtor de ListView. Para que essa propriedade funcione corretamente temos que ajustar a altura (height), ou largura (width) no rolamento horizontal. Podemos marcar ListView(height=300, spacing=10), por exemplo. No exemplo acima forçamos o controle a usar todo o espaço disponível com ListView.expand=True.

Propriedades de ListView

Propriedade Descrição
auto_scroll booleano, auto_scroll=True se a barra de rolamento (scrollbar) deve ser mover automaticamente até o final do último filho acrescentado. Para que o método scroll_to() funcione esse controle deve ser ajustado como auto_scroll=False.
controls A lista de controles a serem exibidos dentro do ListView.
divider_thickness Se esse valor for maior que zero um controle Divider é usado no espaçamento entre os ítens. O default é divider_thickness=0.
first_item_prototype booleano, first_item_prototype=True para que as dimensões do primeiro filho devem ser usadas como “protótipo” para os demais itens inseridos. Nesse caso todos os controles terão a mesma altura ou largura que o primeiro filho. O default é False.
horizontal booleano, horizontal=True para que os itens sejam dispostos horizontalmente.
item_extent uma altura (ou largura, no rolamento horizontal) fixa para que um item tenha renderização otimizada.
on_scroll_interval passo ou salto em milisegundos para o disparo do evento on_scroll. Default é on_scroll_interval=10.
padding espaçamento interno para posicionamento dos fihos. Veja a propriedade Container.padding.
reverse booleano, define a direção do rolamento. Default é reverse=False.
spacing altura do controle Divider entre itens do ListView. Espaçamento zero é usado se esse valor não é especificado.

Método de ListView

scroll_to(offset, delta, key, duration, curve) Move a posição da barra de rolagem (e, portanto, da posição visível dentro do controle) para uma marca absoluta, um salto relativo ou para uma chave especificada.

Veja Column.scroll_to() para detalhes.

Evento de ListView

on_scroll dispara quando a posição da barra de rolamento é alterada pelo usuário.

Veja Column.scroll_to() para detalhes.

Exemplo de uso de ListView

O exemplo mostra a montagem de conteúdo dentro de um ListView, com intervalo de tempo de .1 segundoprovocado por sleep(.1).

from time import sleep
import flet as ft

def main(page: ft.Page):
    page.title = "Use ListView com auto-rolagem"

    arr=[ft.Text("<h1>Título do conteúdo da Web</h1> ", size=20, bgcolor="#aabbff")]
    lv = ft.ListView(
        controls=[ft.Text("<h1>Título do conteúdo da Web</h1> ", size=20)],
        expand=True,
        spacing=1,
        padding=40,
        auto_scroll=True,
        divider_thickness=1
    )

    ct=ft.Container(
        content=lv,
        width=500, height=500, bgcolor=ft.colors.AMBER_50,
        alignment=ft.alignment.center,
        border=ft.border.all(1, ft.colors.BLUE_100),
        border_radius = ft.border_radius.all(10)
    )
    page.add(ct)

    def escreve_texto(texto):
        sleep(.1)
        lv.controls.append(ft.Text(texto))
        page.update()

    escreve_texto("<p>Podemos construir uma lista não-ordenada:</p>")
    escreve_texto("<ul>")

    for i in range(0, 3):
        escreve_texto(f"    <li>Exibindo conteúdo da linha {i}</li>")
    escreve_texto("</ul>")
    escreve_texto("<p>Podemos construir uma tabela</p>")
    escreve_texto("<table>")
    escreve_texto("<tr><th>Palavra</th><th>Número</th>")
    escreve_texto("<th>Palavra</th><th>Número</th>")
    escreve_texto("<th>Palavra</th><th>Número</th></tr>")

    for i in range(0, 9, 3):
        escreve_texto("<tr>")
        escreve_texto(f"    <td>palavra</td><td>{i}</td>")
        escreve_texto(f"    <td>palavra</td><td>{i+1}</td>")
        escreve_texto(f"    <td>palavra</td><td>{i+2}</td>")
        escreve_texto("</tr>")
    escreve_texto("</table>")

ft.app(target=main)
Figura 3: estado final do tivo após execução, com rolagem ajustada para o início.

O controle ListView é criado e inserido na página com apenas um controle (flet.Text). Controles filhos são adicionados depois por meio da função escreve_texto(texto). A figura 3 mostra o aplicativo resultante da execução desse código.

O texto gerado é parte de uma página HTML, algo usado apenas como uma demonstração da montagem gradual no controle, sem um significado especial pois esse controle não pode renderizar texto com marcação HTML. O texto gerado seria representado em um navegador como mostrado na figura 4, incluído aqui apenas por completeza.

Figura 4: texto html renderizado.

Flet: ExpansionTile

Outro controle de Layout

Controle ExpansionTile

Controle ExpansionTile é um controle que exibe uma linha contendo, por default, um ícone com setas, um título, subtítulo e admitindo como conteúdo um array de controles como filhos que serão exibidos como conteúdo. Um clique nas setas ou cabeçalho da linha provoca a a expansão ou colapso do controle. Quando colapsado o titulo e subtítulo continuam visíveis.

Um exemplo mínimo de código aparece listado abaixo, com a ilustração de seu resultado ao ser executado.

import flet as ft

class ETile(ft.ExpansionTile):
    def __init__(self, titulo, subtitulo, arrCtl):
        super().__init__()
        self.title=ft.Text(titulo, size=22, weight=ft.FontWeight.BOLD)
        self.subtitle=ft.Text(subtitulo)
        self.controls=arrCtl

def main(page: ft.Page):

    exp_tile1 = ETile("Título do primeiro ExpansionTile", "Primeiro subtítulo", [ft.Text("Conteúdo interno 1")])
    exp_tile2 = ETile("Título do segundo ExpansionTile", "Segundo subtítulo", [ft.Text("Conteúdo interno 2")])
    arr=[ft.Text("Conteúdo interno 3"), ft.Text("Conteúdo interno 4"), ft.Text("Conteúdo interno 5")]
    exp_tile3 = ETile("Título do terceiro ExpansionTile", "Terceiro subtítulo", arr)
    page.add(exp_tile1, exp_tile2, exp_tile3)

ft.app(target=main)
Figura 1: Estado inicial; Primeiro e segundo tiles expandidos; apenas terceiro tile expandido.

Propriedades de ExpansionTile

ExpansionTile tem as seguintes propriedades:

Propriedade Descrição
affinity usado para forçar a exibição da ícone de expansão e contração (setas por deafault) sejam colocados à esquerda (no início) ou direita (no final) do controle. O valor de affinity pode ser dado por um enum TileAffinity que tem os seguintes valores:

  • LEADING, no início do controle,
  • TRAILING, no final do controle,
  • PLATFORM (default), seguindo as convenções da plataforma.
bgcolor cor de fundo do controle, exibida por trás da sublista, quando exibida.
controls uma lista de controles que serão exibidos quando o ExpansionTile está expandido. Um controle ListTile é normalmente usado como filho, embora outros controles possam ser inseridos.
controls_padding define os espaçamentos (padding) em torno dos controles. Mais informações em Container.padding.
clip_behavior define o comportamento de recorte (clipping) doo contéudo. O valor da propriedade é um enum ClipBehavior com os seguintes valores:

  • NONE (default),
  • ANTI_ALIAS.
  • ANTI_ALIAS_WITH_SAVE_LAYER.
  • HARD_EDGE.
collapsed_bgcolor cor de fundo do controle ExpansionTile quando a sublista está colapsada.
collapsed_icon_color cor do ícone de expansão do controle quando a sublista está colapsada.
collapsed_shape o formato da borda do controle quando a sublista está colapsada. O valor é uma instância do tipo OutlinedBorder do qual se pode herdar:

  • StadiumBorder
  • RoundedRectangleBorder
  • radius: raio da borda, uma instância de BorderRadius ou um número.
  • CircleBorder
  • BeveledRectangleBorder
  • radius: raio da borda, uma instância de BorderRadius ou um número.
  • ContinuousRectangleBorder
  • radius: raio da borda, uma instância de BorderRadius ou um número.
collapsed_text_color cor do título do controle quando a sublista está colapsada.
expanded_alignment define a alinhamento dos filhos, que são distribuídos em uma coluna, quando o controle está expandido.
Veja o propriedade Mais informações em Container.alignment para maiores informações.
expanded_cross_axis_alignment Define o alinhamento de cada controle filho quando o controle está expandido.
O valor da propriedade é um enum CrossAxisAlignment com os valores:

  • START
  • CENTER (default)
  • END
  • STRETCH
  • BASELINE
icon_color cor do ícone de expansão quando a sublista está expandida.
initially_expanded booleno, define se o controle é inicializado expandido ou colapsado. Default = False.
leading um controle a ser exibido antes (à esquerda) do título.
maintain_state booleano, define se o estado do controle é mantido após ser expandido ou colapsado. Default = False.
shape formato da borda do controle quando a sublista está expandida. O valor é uma instância do tipo OutlinedBorder da qual herdam:

  • StadiumBorder
  • RoundedRectangleBorder
  • radius: raio da borda, uma instância de BorderRadius ou um número.
  • CircleBorder
  • BeveledRectangleBorder
  • radius: raio da borda, uma instância de BorderRadius ou um número.
  • ContinuousRectangleBorder
  • radius: raio da borda, uma instância de BorderRadius ou um número.
subtitle conteúdo a ser exibido abaixo do título. Pode ser um widget de texto (flet.Text).
text_color cor de texto do título quando a sublista está expandida.
tile_padding Espaçamento interno (padding) do controle. O valor default é padding.symmetric(horizontal=16.0).
Veja Container.padding.
title controle a ser exibido como título principal. Pode ser um widget de texto (flet.Text).
trailing controle a ser exibido depois (à direita) do título. Pode ser um controle de ícone

Eventos de ExpansionTile

ExpansionTile tem os seguintes eventos:

Evento Descrição
on_change dispara quando o usuário clica (ou toca) no controle.
on_long_press dispara quando o usuário clica (ou toca) demoradamente sobre o controle.

Exemplo de uso de ExpansionTile

O código abaixo faz uma demonstração do controle ExpansionTile. Abaixo segue uma imagem, figura 2, mostrando o resultado da execução desse código.

import flet as ft
import random

cor=["#DA2222", "#2CDA22", "#2275DA", "#5622DA", "#C99032", "#A7D7DE"]
bgcor=["#E7DADA", "#DCE8DB", "#CFE3FA", "#D2CEDB", "#F9E9CE", "#D2E9EC"]

def main(page: ft.Page):
    page.spacing = 0
    page.theme_mode = ft.ThemeMode.LIGHT # DARK
    page.padding = ft.padding.all(5)
    page.bgcolor="#7CC2D1"

    def mostrar_snack(texto):
       return ft.SnackBar(
           ft.Row(
               [ft.ElevatedButton("Mudar Cores", bgcolor="WHITE", on_click=mudar_cor),
               ft.Text(texto,size=18)]
            ), duration=5000 )

    def apagar(e):
        if e.control.data==1:
            page.controls.remove(etile1)
        elif e.control.data==2:
            page.controls.remove(etile2)
        else:
            page.controls.remove(etile3)
        page.show_snack_bar(mostrar_snack(f"Apagado o controle {e.control.data}"))
        page.update()

    def mudar_cor(e):
        i = random.randrange(0,6)
        etile1.bgcolor=bgcor[i]
        etile1.collapsed_bgcolor=bgcor[(i+1)%6]
        etile2.bgcolor=bgcor[(i+2)%6]
        etile2.collapsed_bgcolor=bgcor[(i+3)%6]
        etile3.bgcolor=bgcor[(i+4)%6]
        etile3.collapsed_bgcolor=bgcor[(i+5)%6]
        page.update()

    def alterou_etile(e):
        qual = f"Barra com título '{e.control._ExpansionTile__title.value}' foi " #a.__dict__
        qual += f"{'expandida' if e.data=='true' else 'colapsada'}"
        page.show_snack_bar(mostrar_snack(qual))

    class ETile(ft.ExpansionTile):
        def __init__(self, titulo, subtitulo, arrCtl,cor1,id):
            super().__init__()
            self.id = id
            self.title=ft.Text(titulo, size=22, weight=ft.FontWeight.BOLD)
            self.subtitle=ft.Text(subtitulo)
            self.controls=arrCtl
            self.controls.append(ft.TextButton("Apagar", on_click=apagar, data=id))
            self.text_color=cor[cor1]
            self.collapsed_text_color=cor[5]
            self.on_change=alterou_etile
            self.bgcolor=bgcor[(cor1+1)%6]
            self.collapsed_bgcolor=bgcor[(cor1+2)%6]
            self.controls_padding = ft.padding.all(-5)

    titulo="Primeiro ExpansionTile"
    subtitulo="Demonstrando ícone no final do controle"
    arrCtl=[ft.ListTile(title=ft.Text("Texto interno do ExpansionTile 1"))]
    etile1=ETile(titulo, subtitulo, arrCtl, 0, 1)

    etile1.affinity=ft.TileAffinity.TRAILING
    etile1.maintain_state=True
    etile1.initially_expanded=True

    titulo="Segundo ExpansionTile"
    subtitulo="Demonstrando ícone personalizado"
    arrCtl=[ft.ListTile(title=ft.Text("Texto interno do ExpansionTile 2"))]
    etile2=ETile(titulo, subtitulo, arrCtl, 2, 2)
    etile2.trailing=ft.Icon(ft.icons.ADD_A_PHOTO_ROUNDED)

    titulo="Terceiro ExpansionTile"
    subtitulo="Ícone no início do controle e várias linhas internas"
    arrCtl=[ft.ListTile(title=ft.Text("3.1: Texto interno 1 do ExpansionTile 3")),
            ft.ListTile(title=ft.Text("3.2: Texto interno 2 do ExpansionTile 3")),
            ft.ListTile(title=ft.Text("3.3: Texto interno 3 do ExpansionTile 3"))]
    etile3=ETile(titulo, subtitulo, arrCtl, 4, 3)

    etile3.affinity=ft.TileAffinity.LEADING

    page.add(etile1, etile2, etile3)

    page.show_snack_bar(mostrar_snack("Clique nas barras ou ícones"))

ft.app(target=main)
Figura 2: estado inicial do aplicativo. Segundo e terceiro tiles expandidos. Segundo tile apagado.

A classe ETile é usada para construir os controles ExpansionTile no aplicativo. Ela adiciona um botão a cada um deles que pode apagar esse controle. Para identificar qual controle deve ser apagado cada botão recebe um id anexado por meio de flet.TextButton.data. O apagamento consiste na remoção do controle da lista page.controls.

O apagamento ou mudança de estado entre colapsado ou expandido de cada controle são relatados em um flet.SnackBar, uma barra mostrada temporariamente no pé da página. Eesa barra também contém um botão que aciona o evento que troca as cores de fundo dos elementos colapsados ou não.

Flet: Dismissible

Outro controle de Layout

Controle Dismissible

Dismissible é um controle que pode ser arrastado em uma direção especificada. Quando arrastado ele gera eventos que podem ser capturados para efetuar ações sobre o aplicativo. O conteúdo desenhado dentro do controle permanece visível e acompanha o arrasto do controle que o contém. O controle é dispensado (eliminado) do aplicativo quando atinge um deslocamento mínimo determinado em código.

Nota: chamaremos de dispensa a ação de dismiss do controle. Antes da dispensa o controle encolhe em tamanho, gerando os eventos descritos abaixo.

Um exemplo bem reduzido de uso do dismiss é mostrado abaixo.

import flet as ft
import copy

def main(page):

    dm1=ft.Dismissible(content=ft.Text("Primeira Linha", size=20, bgcolor=ft.colors.AMBER_100))
    (dm2 := copy.deepcopy(dm1)).content.value="Segunda Linha"
    (dm3 := copy.deepcopy(dm1)).content.value="Terceira Linha"

    page.add(ft.Column([dm1, dm2, dm3]))
ft.app(main)

Esse código gera o seguinte aplicativo, mostrado na figura 1 em três estados:

Figura 1: Três linhas de controle Dismissible: estado original, arrasto para a direita, linha 1 eliminada.

Nesse exemplo criamos um controle dm1=ft.Dismissible() e o copiamos, usando deepcopy para dois outros controles. O método copy.deepcopy é necessário para que controles internos sejam copiados e a cópia seja feita por valor. Isso significa que dm2 e dm3 são novos objetos e não apenas uma nova referência para dm1. Com isso podemos mudar dm2.content.value="Segunda Linha" (o valor do conteúdo interno).

Nota: O operador walrus (:=) de atribuição tem o efeito de fazer uma atribuição e já retornar o objeto para operações posteriores. Portanto:

    (dm2 := copy.deepcopy(dm1)).content.value="Segunda Linha"
    # é o mesmo que
    dm2 = copy.deepcopy(dm1)
    dm2.content.value="Segunda Linha"

Em um caso mais elaborado podemos inserir linhas formatadas (por exemplo inserindo o texto dentro de containers), capturar outros eventos e definir outras propriedades. A figura 2 mostra um caso um pouco mais elaborado e o exemplo de uso de Dismissible no final desse texto mostra um código mais completo.

Figura 2: linhas formatadas com container: arrasto da linha 1 para ambos os lados e exclusão da linha 1.

Propriedades de Dismissible

Dismissible tem as seguintes propriedades:

Propriedade Descrição
background um controle que fica por trás do controle principal que exibe conteúdo.
Se secondary_background também for especificado esse controle secundário aparece como fundo quando o conteúdo é arrastado para os lados ou verticalmente.
content um controle filho que fica exibido dentro do Dismissible e se move com ele.
cross_axis_end_offset especifica um valor para deslocamento vertical quando o controle é movido horizontalmente, fazendo o controle se mover na diagonal, para baixo se o valor for positivo, para cima se negativo.
direction direção usada para dispensar o controle. Os valores possível estão no enum DismissDirection:

  • DismissDirection.UP
  • DismissDirection.DOWN
  • DismissDirection.LEFT
  • DismissDirection.RIGHT
  • DismissDirection.START_TO_END
  • DismissDirection.END_TO_START
dismiss_thresholds o valor mínimo de deslocamento considerado para dispensar o controle. A partir desse valor o controle é removido do aplicativo. Por exemplo, se o valor é de 0.4 (o default) a barra tem que ser movida de 40% ou mais de sua extensão para ser dispensada. Seu valor é especificado como um dicionário tendo como chaves um objeto DismissDirection e o valor um valor numérico entre 0.0 e 1.0. O bloco de código mostra isso:

    ft.Dismissible(
    # ...
    dismiss_thresholds={
        ft.DismissDirection.VERTICAL: 0.1,
        ft.DismissDirection.START_TO_END: 0.7
    }
)

Esse dicionário define que os valores mínimos (thresholds) são 10% na vertical e 70% na horizontal, se o movimento for do início para o final (para a direita).

movement_duration a duração do movimento para que o controle seja dispensado ou retorne para sua posição original.
resize_duration a duração do movimento de contração antes que o evento de dispensa seja acionado.
secondary_background um controle desenhado por tras do conteúdo principal, que é exposto quando o conteúdo é arrastado para os lados. Só pode ser especificado se o background também for definido.

Eventos de Dismissible

Evento Descrição
on_confirm_dismiss um evento que ocorre antes que o controle seja dispensado, dando a oportunidade de se confirmar ou negar essa operação. O controle não pode ser arrastado novamente até que essa pendência seja resolvida.

Para resolver essa pendência deve ser chamado o método confirm_dismiss(dismiss) passando um booleano: True para confirmar, provocando a dispensa do controle, False para negar, o que faz com que ele volte para sua posição original. Uma possibilidade consiste em apresentar a pergunta em uma caixa como AlertDialog.

on_dismiss dispara quando o controle é dispensado. Antes da dispensa o controle é contraído, diminuindo de tamanho.
on_resize dispara quando o controle muda de tamanho, o que ocorre, por exemplo, na contração ao ser dispensado.
on_update dispara quando o controle é arrastado, independentemente de ser ou não dispensado.

Método de Dismissible

Método Descrição
confirm_dismiss(dismiss: bool) resolve a pendência quando a dispensa é requerida. Esse evento pode ser chamado quando se trata do evento on_confirm_dismiss.

Exemplo de uso de Dismissible

Um exemplo mais completo de código está listado abaixo. Para manter a simplicidade e não usar caixas de diálogo (ainda não consideradas nessas notas) o evento on_confirm_dismiss=informe_dispensa apenas aciona a função informe_dispensa que registra na caixa de informações as ações de remoção dos controles pela direita ou pela esquerda, sem pedir a confirmação.

import flet as ft

def main(page):

    def informe_dispensa(e: ft.DismissibleDismissEvent):
        if e.direction == ft.DismissDirection.END_TO_START:
            msg.value += "\nSaída pela direita!"
        else:
           msg.value += "\nSaída pela esquerda!"
        e.control.confirm_dismiss(True)
        page.update()

    def dispensar(e):
        coluna.controls.remove(e.control)
        page.update()

    def atualizar(e: ft.DismissibleUpdateEvent):
        if e.direction == ft.DismissDirection.END_TO_START:
            icone.name=ft.icons.ARROW_BACK
        else:
            icone.name=ft.icons.ARROW_FORWARD
        texto.value=f"Progresso: {e.progress}"
        page.update()

    cor = [ft.colors.AMBER_100,ft.colors.BLUE_GREY_100, ft.colors.BLUE_50]
    array=[
        ft.Dismissible(
            content=ft.Container(
                ft.Text(f"Esta é a linha {i+1}", size=15),
                height=35, width=380, 
                border = ft.border.all(2, ft.colors.BLACK54),
                bgcolor= cor[i%2],
                padding = ft.padding.all(6),
                margin = ft.margin.all(2)
            ),
            dismiss_direction=ft.DismissDirection.HORIZONTAL,
            background=ft.Container(bgcolor=ft.colors.CYAN_100),
            secondary_background=ft.Container(bgcolor=ft.colors.BROWN_50),
            dismiss_thresholds={
                ft.DismissDirection.END_TO_START: 0.2,
                ft.DismissDirection.START_TO_END: 0.2
            },
            on_dismiss=dispensar, 
            on_update=atualizar,
            on_confirm_dismiss=informe_dispensa
        ) for i in range(10)
    ]

    coluna=ft.Column(array, spacing=1)
    icone=ft.Icon(name=ft.icons.KEYBOARD, color=ft.colors.BLUE, size=30)
    texto=ft.Text("Posição dos controles", size=18)
    linha_info = ft.Row([icone, texto])
    msg = ft.Text(f"Eventos capturados (Dispensar):\n" , size=15)
    ver_acao = ft.Container(
        msg,
        height=100, width=380, 
        expand=True,
        border = ft.border.all(2, ft.colors.BLACK54),
        bgcolor= cor[2],
        padding = ft.padding.all(12),
        margin = ft.margin.all(3)
    )

    page.add(coluna, linha_info, ver_acao)

ft.app(main)
Figura 3: (a) o estado inicial do aplicativo; (b) arrasto da linha 1 para a direita; (c) arrasto da linha 2 para a esquerda; (d) removidas linhas 1 e 3.

A operação array=[(objeto(i)) for i in range(10)] cria uma lista com 10 objetos objeto(i) passando o valor de i internamente para o objeto em cada loop.

A função informa_dispensa(e) recebe o evento e que contém uma descrição da direção do evento de arrastar. Ela atualiza a variável msg que é exibida dentro do container ver_acao.

A função dispensar(e) usa a referência ao controle e.control para removê-lo, usando coluna.controls.remove(e.control).

A função atualizar(e) escolhe um ícone de direção e insere texto mostrando a porcentagem de arrasto a cada momento, atualizando a variável texto que está em linha_lnfo.

Flet: Exemplo 4

Controles de Layout

Card, Tabs, SafeArea, Divider, VerticalDivider, ExpansionPanelList são outros controles que facilitam a divisão, exibição ou ocultação de conteúdo dentro de uma página de aplicativo.

Seguem-se alguns exemplos de códigos usando esses controles.

Exemplo de uso de Card

Leia sobre a descrição do controle, suas propriedades e evento em Controles de Layout: Card.
O código abaixo exibe a construção de uma página com 3 cards.

import flet as ft

def main(page):
    page.title = "Exemplo usando \"card\""

    listitulo = ft.ListTile(leading=ft.Icon(ft.icons.ACCESS_ALARM,  size=50, color="#D3143E"),
                            title=ft.Text("Compromisso: 28 de junho de 2024"),
                            subtitle=ft.Text("Aniversário de seu filho. Não esqueça o presente!"),)
    
    def apagar(e):
        listitulo.title= ft.Text("")
        listitulo.subtitle=ft.Text("* apagado", color="#aaaaaa")
        cartao.color="WHITE"
        page.update()

    def proximo(e):
        listitulo.title=ft.Text("Compromisso: 01 de agosto de 2024")
        listitulo.subtitle=ft.Text("Viagem para São Paulo", color="#000000")
        cartao.color="#CBEBA8"
        page.update()

    bt1 = ft.TextButton("Apagar", on_click=apagar)
    bt2 = ft.TextButton("Próximo compromisso", on_click=proximo)

    linha = ft.Row([bt1, bt2], alignment=ft.MainAxisAlignment.END)
    coluna = ft.Column([listitulo, linha,])
    contem = ft.Container(coluna, width=500, padding=20,)
    cartao = ft.Card(contem, color="YELLOW", shadow_color="white", elevation=8.0)

    page.add(cartao)

ft.app(target=main)

Na figura 1 o resultado desse código, quando executado.

Figura 1: O Card amarelo mostra o estado incial do app. Card verde, se for clicado “Próximo compromisso”. Último Card, se o clique for no botão “Apagar”.

Tabs

Leia sobre a descrição do controle, suas propriedades e evento em Controle Tabs.

O controle Tabs, usualmente chamado de guias ou abas em português, é usado para separar conteúdo de categorias diferentes, facilitando a navegação do usuário. Ele simula a marcação de bookmarks em um livro físico, para indicar setores de conteúdos diversos, sendo um padrão de desenho com o qual os usuários estão acostumados, no desktop ou web.

Exemplo de uso de Tabs

O código abaixo exibe a construção de uma página o uso de Tabs.

import flet as ft

def main(page: ft.Page):
    page.bgcolor=ft.colors.BROWN_200
    
    ct_interno=ft.Container(content=ft.Text("Conteúdo do Tab 1 dentro de um Card",
                            size=20, weight=ft.FontWeight.BOLD), padding=45)
    
    card = ft.Card(content= ct_interno, elevation=20, expand=1)

    ct_tab1 = ft.Container(content=card, bgcolor="#BAE3ED", alignment=ft.alignment.center)

    tab1 = ft.Tab(text="Tab 1", content=ct_tab1)

    ct_tab2 = ft.Container(content=ft.Text("Conteúdo de texto do Tab 2", size=30),
                           bgcolor="#BBF3EF",
                           margin = ft.margin.all(10.0),
                           padding=ft.padding.all(1.0),
                           alignment=ft.alignment.top_right,
                           border=ft.border.all(2, "#55555"),
                           border_radius=20)

    tab2 = ft.Tab(text="Tab 2", icon=ft.icons.SEARCH, content=ct_tab2)

    ct_tab3 = ft.Container(content=ft.Text("Conteúdo de texto do Tab 3", size=30),
                           bgcolor="#FCF5CC", alignment=ft.alignment.bottom_left)

    tab3 = ft.Tab(text="Tab 3", icon=ft.icons.SETTINGS, content=ct_tab3)

    tabs = ft.Tabs(tabs=[tab1, tab2, tab3],
                   selected_index=0,
                   divider_color="#ABCAD0",
                   label_color="WHITE",
                   unselected_label_color="BLACK",
                   overlay_color = "YELLOW",
                   indicator_padding=10,
                   indicator_color="RED")

    page.add(ft.Container(tabs, bgcolor=ft.colors.BROWN_200, expand=1))

ft.app(target=main)

O resultado desse código está mostrado na figura 2.

Figura 2: Tabs 1, 2 e 3 exibidos, com cliques nas respectivas guias ou abas (tab).

O primeiro tab exibe um card com texto interno, centralizado. O segundo tab tem um container formatado com bordas arredondadas e texto no alto, à direita. O terceiro tab tem texto em baixo, à esquerda.

Observe que no código acima, e na maioria dos blocos de código nessas notas, os controles são montados por partes, separando controles que serão depois inseridos dentro de outros. Para montar o tab1, criamos ct_interno, um container com um texto e o inserimos dentro de card, que é inserido em ct_tab1 e finalmente em tab1. Isso é feito por motívo didático, para que o código fique mais fácil de ser entendido, e não tem efeito no resultado no momento da execução. Esse estilo é uma preferência do programador. Uma forma alternativa para a construção do mesmo controle pode ser vista abaixo.

    tab1 = ft.Tab(
        text="Tab 1",
        content=ft.Container(
            content=ft.Card(
                content=ft.Container(
                    content=ft.Text("Conteúdo do Tab 1 dentro de um Card",
                    size=20, weight=ft.FontWeight.BOLD), padding=45
                ),
                elevation=20, expand=1
            ),
            bgcolor="#BAE3ED",
            alignment=ft.alignment.center
        )
    )

De qualquer forma é importante estabelecer um critério de indentação e se manter coerente com ele.

SafeArea

SafeArea insere conteúdo com preenchimento (padding, as distâncias entre as margens internas do container com as bordas externas do controle) de modo a impedir que os elementos de interface do sistema operacional se sobreponham às do aplicativo. Leia sobre a descrição do controle, suas propriedades em Controles de Layout: SafeAreaCard.

Um exemplo é mostrado no código abaixo.

import flet as ft

class Contador:
    contagem = 0

def main(page: ft.Page):    
    def add_click(e):
        cliques.contagem += 1
        contador.value = f"Contador: {cliques.contagem}"
        contador.update()

    page.horizontal_alignment=ft.CrossAxisAlignment.CENTER
    page.vertical_alignment=ft.MainAxisAlignment.CENTER
    page.floating_action_button = ft.FloatingActionButton(icon=ft.icons.ADD, on_click=add_click)

    cliques = Contador()

    contador = ft.Text("Contador: 0", size=30, color="WHITE", weight=ft.FontWeight.BOLD)

    page.add(
        ft.SafeArea(
            expand=False,
            content=ft.Container(
                content=contador,
                padding=35, width=300, height=190,
                alignment=ft.alignment.center,
                bgcolor="BLUE",
                gradient=ft.LinearGradient(colors=[ft.colors.BLACK, ft.colors.BLUE_900]),
                border=ft.border.all(3, ft.colors.BLUE_100),
                border_radius=ft.border_radius.all(10)
            )
        )
    )

ft.app(main)

A execução desse código gera um aplicativo mostrado na figura 3.

Figura 3: Controle SafeArea tendo um container no seu interior.

Observe que, para manter um contador global foi criada a classe e propriedade State.counter. O objeto cliques = Contador() tem, portanto a propriedade cliques.contagem, que é incrementada a cada clique no botão floating_action_button.

Divider e VerticalDivider

São controles usados para desenhar uma linha divisória, horizontal e vertical, respectivamente. Leia sobre a descrição dos controles e suas propriedades em Controle Divider e VerticalDivider.

import flet as ft

def main(page: ft.Page):
    ct1 = ft.Container(bgcolor="#797EF6", expand=True)
    ct2 = ft.Container(bgcolor="#4ADEDE", expand=True)
    divisorVertical = ft.VerticalDivider(width=50, thickness=10, color="#960cc9")
    ct3 = ft.Container(bgcolor="#1AA7EC", expand=True)
    ct4 = ft.Container(bgcolor="#1E2F97", expand=True)
    divisorHorizontal= ft.Divider(height=50, thickness=40, color="#F6E750")

    page.add(
        ft.Row([ct1, divisorVertical, ct2], spacing=0, expand=True),
        ft.Column([ct3, divisorHorizontal, ct4], spacing=0, expand=True)
    )

ft.app(target=main)

O código executado é renderizado como a figura 4.

Figura 4: Divider e VerticalDivider.

A única diferença entre propriedades de Divider e VerticalDivider são as propriedades Divider.height e VerticalDivider.width.

ExpansionPanel e ExpansionPanelList

ExpansionPanelList é o controle usado para criar painéis expansíveis, que podem ser contraídos até um mínimo, expandidos para exibir conteúdo, ou removidos da página. ExpansionPanelsList recebe uma lista de controles ExpansionPanel. Leia sobre a descrição dos controles e suas propriedades em Controle ExpansionPanelList e ExpansionPanel.

import flet as ft

class Painel(ft.ExpansionPanel):
    def __init__(self, cabecalho, titulo, texto, cor, botao=None, indice=0):
        super().__init__()
        self.can_tap_header=True
        self.bgcolor=cor
        self.header=ft.Text("  " + cabecalho, size=22, weight=ft.FontWeight.BOLD)
        self.titulo=ft.Text(titulo, size=18)
        self.texto=ft.Text(texto, size=17)
        self.conteudo=ft.ListTile(
            title=self.titulo,
            subtitle=self.texto,
            trailing=botao
        )
        self.content=self.conteudo
        self.indice = indice

def main(page: ft.Page):
    cor = [ft.colors.GREEN_100, ft.colors.BLUE_100, ft.colors.RED_100,
           ft.colors.WHITE10, ft.colors.BLUE_500, ft.colors.WHITE70]
    
    def pegar_painel(index):
        for pn in panel.controls:
            if index == pn.indice: return pn
        return None    
            

    def mudou(e: ft.ControlEvent):
        if int(e.data)==4: return
        atividade.conteudo.subtitle.value += f"\nO painel {e.data} foi modificado"
        page.update()

    def apagar(e: ft.ControlEvent):
        pn = pegar_painel(e.control.data)
        if pn:
            panel.controls.remove(pn)
            atividade.conteudo.subtitle.value += f"\nFoi apagado o Painel {e.control.data}"
        page.update()

    panel = ft.ExpansionPanelList(
        elevation=8,
        spacing=6,
        expand_icon_color=ft.colors.AMBER_100,
        divider_color=ft.colors.CYAN_ACCENT_100,
        on_change=mudou
    )
    panel.controls.append(
        Painel("Atividades Online", "Lista das ações realizadas no aplicativo.", "",
               cor[4], None,0
        )
    )
  
    panel.controls.append(
        Painel(
            "Painel 1", "Conteúdo do painel 1", "Clique no ícone para apagar o painel 1.", cor[0],
            ft.ElevatedButton("Apagar", icon=ft.icons.DELETE, width=150, height=30, data=1,
            on_click=apagar), 1
        )
    )

    panel.controls.append(
        Painel(
            "Painel 2", "Conteúdo do painel 2", "Clique no ícone para apagar o painel 2.", cor[1],
            ft.ElevatedButton("Apagar", icon=ft.icons.DELETE, width=150, height=30, data=2,
            on_click=apagar), 2
        )
    )

    panel.controls.append(
        Painel(
            "Painel 3", "Conteúdo do painel 3", "Clique no ícone para apagar o painel 3.", cor[2],
            ft.ElevatedButton("Apagar", icon=ft.icons.DELETE, width=150, height=30, data=3,
            on_click=apagar), 3
        )
    )

    atividade = Painel("Resumo de Atividades", "", "Conteúdo de texto da informação de uso", cor[5])
    panel.controls.append(atividade)

    page.add(panel)

ft.app(target=main)

O resultado pode ser visto na figura 5.

Figura 5: à esquerda o painel com todos os painéis recolhidos. À direita o estado do app após algumas operações de expandir e apagar.

Como são gerados no código vários painéis flet.ExpansionPanel é interessante criar a classe Class Painel que herda de ExpansionPanel, personalizando na inicialização cada caso a ser inserido. O último dos painéis exibidos, atividade é usado para armazenar e exibir as ações feitas no aplicativo, no campo atividade.conteudo.subtitle.value. Essas ações são executadas nos eventos ExpansionPanelList.on_change e pelo clique nos botões ElevatedButton colocados em cada painel, exceto o último, e pela ação de apagar um painel. Os painéis são identificados pela propriedade painel.indice, usada para encontrar e deletar um deles.

Outros controles de Layout


Outros controles de montagem de Layout

Nessa seção veremos os controles Card, Tabs, SafeArea, Divider, VerticalDivider, ExpansionPanelList, todos eles widgets que facilitam a divisão, exibição ou ocultação de conteúdo dentro de uma página de aplicativo.

Card

Um card no flet é um painel de bordas arredondadas e com um sombreado colorido simulando uma elevação sobre o fundo onde está desenhado.

Propriedades de Card

Propriedade Descrição
color indica a cor de fundo do card.
content o controle a ser exibido pelo controle. card só pode ter um filho. Para inserir mais de um widget dentro de card insira nele controles que podem ter vários filhos, como Row, Column ou Stack.
elevation: controla o tamanho da sombra abaixo do card, simulando que esse esteja elevado sobre sua base. O valor default é 1.0.
margin distância entre as bordas e a borda de seu container.
shadow_color cor da sombra abaixo do card.
shape a forma do card. Esse valor é uma instância de uma das implementações:

  • StadiumBorder
  • RoundedRectangleBorder
    • radius: raio da borda, uma instância da classe BorderRadius ou um número.
  • CircleBorder
  • BeveledRectangleBorder
    • radius: raio da borda, uma instância da classe BorderRadius ou um número.
  • ContinuousRectangleBorder
    • radius: raio da borda, uma instância da classe BorderRadius ou um número.

O formato default é RoundedRectangleBorder com radius=4.0.

surface_tint_color cor usada como sobreposição de cor para indicar elevação.
Se ajustada para None, nenhuma sobreposição será aplicada. Caso contrário, esta cor será composta sobre a cor de fundo com opacidade relacionada à elevação e usada para desenhar fundo do card. O default é None.
Não consegui usar essa propriedade. Se algum leitor tem essa informação peço que me envie.

Exemplo de uso de Card

Um exemplo mais completo pode ser encontrado em Uso de Card.

import flet as ft

def main(page):
    icon=ft.Icon(ft.icons.ADB_ROUNDED, color=ft.colors.BLUE, size=50 )
    texto1 = ft.Text("Título do Card", size=30)
    texto2 = ft.Text("Conteúdo de texto do Card")
    linha = ft.Row([icon, ft.Column([texto1, texto2])])
    cartao = ft.Card(ft.Container(linha, width=300, padding=20))
    page.add(cartao)

ft.app(target=main)



Como vemos, card tem um único filho, um container que, que sua vez, tem uma linha com um ícone e uma coluna (com dois controles de textos).

Tabs

O controle Tabs, usualmente chamado de guias ou abas em protuguês, é usado para separar conteúdo de categorias diferentes, facilitando a navegação do usuário. Ele simula a marcação de bookmarks em um livro físico, para indicar setores de conteúdos diversos, sendo um padrão de desenho com o qual os usuários estão acostumados, no desktop ou web.

Propriedades de Tabs

Propriedade Descrição
animation_duration tempo de duração, em milisegundos, da animação de trocas entre tabs. O default é 50.
divider_color a cor do divisor entre os tabs ou guias.
indicator_border_radius o raio de arredondadmento de cantos do indicador.
indicator_border_side cor e largura da linha horizontal desenhada abaixo da guia selecionada. Essa linha indica que uma guia está em uso.
indicator_color cor da linha exibida abaixo da guia selecionada.
indicator_padding localiza a linha sublinhada abaixo da guia selecionada, relativa a sua borda. A propriedade indicator_tab_size pode ser usada para indicar a localização das bordas. A propriedade é Falsa para centralizar o conteúdo ou True para usar toda a extensão do tab.
indicator_tab_size booleano, True para que o indicador ocupe todo o tab.
label_color cor dos rótulos das guias selecionadas.
overlay_color define a resposta da cor inicial quando o controle está em foco, hover ou splash. Quando especificada essa propriedade usa MaterialState.FOCUSED, MaterialState.HOVERED ou MaterialState.PRESSED.
selected_index índice do tab selecionado. Pode ser ajustado programaticamente ou selecionado pelo usuário.
scrollable booleano, indica se a barra de tab pode ser rolada horizontalmente. Se scrollable=True cada tab tem a largura necessária para comportar seu rótulo e o controle inteiro é rolável. Se scrollable=False o espaço disponível é distribuído igualmente entre os tabs.
tab_alignment especifica o alinhamento horizontal dos tabs dentro do controle Tabs.
A propriedade TabAlignment é um enum com os valores:

  • NONE
  • START (é o default se scrollable=True)
  • START_OFFSET
  • FILL (é o default se scrollable=False)
  • CENTER
tabs uma lista com os controles tab a serem exibidos.
unselected_label_color a cor das guias (tabs) não selecionadas.

Evento de Tab

Evento Descrição
on_change Dispara quando o índice da guia selecionada (selected_index) é alterado.

Um exemplo de uso de tabs pode ser visto em Uso de Tabs.

Propriedades de Tab

Propriedade Descrição
content o controle a ser exibido quando o Tab é selecionado.
icon ícone a ser exibido à esquerda do texto de Tab.
tab_content um controle com o conteúdo personalizado a substituir o texto e o ícone.
text o nome ou rótulo do Tab.

SafeArea


Outro controle voltado para a diagramação das páginas de aplicativos, SafeArea insere conteúdo com preenchimento (padding, as distâncias entre as margens internas do container com as bordas externas do controle) de modo a impedir que os elementos de interface do sistema operacional se sobreponham às do aplicativo. Ele recua o conteúdo interno evitando sobreposição pela barra de status no alto da tela.

Em particular esse recurso permite o recuo do conteúdo para evitar obstrução pelo “entalhe” no iPhone, (notch) ou outras modificações na geometria das telas em aparelhos menores.

Propriedades de SafeArea

Propriedade Descrição
bottom booleano, se o aplicativo deve evitar intrusão do SO na parte inferior da tela. Default é True.
content recebe o controle filho a ser exibido dentro SafeArea.
left booleano, se o aplicativo deve evitar intrusão do SO na parte esquerda da tela. Default é True.
maintain_bottom_view_padding booleano, default é False. Se True o controle SafeArea deve manter em exibição o MediaQueryData.viewPadding no parte inferior da tela, em vez do MediaQueryData.padding (que é o default).

Por exemplo, caso um teclado na tela seja exibido acima da SafeArea, o preenchimento interno (padding) pode ser mantido abaixo da obstrução sem ser ocultado. Pode ser útil quando o layout contém controles flexíveis que podem ser movidos na interface do usuário. Definir SafeArea=True evita alteração na IU.

minimum preenchimento (padding) mínimo a ser aplicado. Quando o valor de minimum é especificado ele será adotado como padding exceto se esse valor não for suficiente para garantir o preenchimento da área segura.
right booleano, se o aplicativo deve evitar intrusão do SO na parte direita da tela. Default é True.
right booleano, se o aplicativo deve evitar intrusão do SO na parte superior da tela, que em geral contém uma barra de status. Default é True.
Nota: Denotamos nessa tabela, SO = sistema operacional, IU = interface de usuário (ou interface gráfica)

Um exemplo de uso do controle pode ser visto em Uso de SafeArea.

Divider


Um controle voltado para a diagramação das páginas de aplicativos, desenhando uma linha horizontal externa sobre o container pai, com uma linha interna de mesma extensão mas altura possível diferente. Ele admite preenchimento (padding) em ambas as bordas. Pode ter cor, comprimento e espessura definidas.

Propriedades de Divider

Propriedade Descrição
color cor da linha.
height altura da linha, no sentido horizontal. O centro da linha, desenhada no container, fica no centro de sua altura horizontal. O default é 16.0 se esse valor for null.
thickness espessura da linha desenhada dentro do Divider. Se thickness=0.0 a altura será de 1 pixel. Se esse valor for null esse será 0.0.

Exemplos de uso dos controles Vertical e VerticalDivider podem ser vistos em Uso de Divider.

VerticalDivider

Um controle voltado para a diagramação das páginas de aplicativos, desenhando uma linha vertical sobre o container pai, com exatamente análoga à Divider mas com propriedades cor, largura e espessura. A única propriedade diferente é width que define a largura no sentido horizontal da linha externa.

color cor da linha.
width largura linha externa. O centro da linha, desenhada no container, fica no centro de sua altura horizontal. O default é 16.0 se esse valor for null.
thickness espessura da linha dentro do VerticalDivider. Se thickness=0.0 a altura será de 1 pixel. Se esse valor for null esse será 0.0.

ExpansionPanel e ExpansionPanelList

O controle ExpansionPanelList permite a criação de painéis expansíveis, ou seja, podem ser contraídos até um mínimo, expandidos para exibir conteúdo, ou removidos da página. Como organizador de conteúdo na diagramação de uma interface gráfica ele é similar aos Tabs, mas separando o espaço da página em faixas horizontais. Além disso vários painéis podem ficar simultaneamente abertos.

O controle ExpansionPanelsList recebe uma lista de controles ExpansionPanel.

Propriedades de ExpansionPanelList

Propriedade Descrição
controls uma lista de controles ExpansionPanel a serem exibidos dentro do controle pai.
divider_color a cor do divisor quando ExpansionPanel.expanded=False.
elevation define a elevação dos controles filhos ExpansionPanel quando extendido. O valor default é 2.
expanded_header_padding define o espaçamento interno (padding) em torno da cabeçalho quando extendido. O valor default é padding.symmetric(vertical=16.0). Leia mais sobre padding em Propriedades de Containers.
expanded_icon_color cor do ícone. Default é colors.BLACK_54 em modo de tema claro e colors.WHITE_60 em modo escuro.
spacing tamanho do intervalo entre os ExpansionPanels quando extendidos.

Evento de ExpansionPanelList

on_change dispara quando um ExpansionPanel muda de estado entre expandido e colapsado. A propriedade de evento e.data contem o índice do painel ExpansionPanel que causou o evento.

Propriedades de ExpansionPanel

Os objetos ExpansionPanel são os filhos diretos de ExpansionPanelList, que serão representados na página como painéis extendíveis.

bgcolor a cor de fundo do painel.
content os controles que aparecerão dentro desse painel. Esse conteúdo aparece abaixo do cabeçalho (header) quando o painel está expandido. Se content=None o painel terá um espaço reservado para Texto como conteúdo.
can_tap_header booleano. Se can_tap_header=True um toque sobre o cabeçalho do painel provocará sua expansão ou colapso. Defaults é False.
expanded booleano. Define o estado do painel como expandido ou colapsado. Defaults é False (colapsado).
header o controle a ser apresentado dentro do cabeçalho (header). Se header=None o ExpansionPanel terá terá um espaço reservado para Texto no cabeçalho.

Um exemplo de uso dos controles ExpansionPanelList e ExpansionPanel pode ser visto em Uso de ExpansionPanel.

Bibliografia

Todas as URLs foram visitadas em janeiro de 2024.

Python: Strings e Codificação


Outras informações sobre Strings

Aviso: Se você está lendo essas notas pela primeira vez essas informações podem ser consideradas um pouco complexas. É recomendável que você pule essa seção e volte a ela mais tarde.

Strings são variáveis usadas para armazenar texto. Nessas notas elas já foram estudadas na seção Python: Strings. Algumas informações adicionais podem ser úteis.

Prefixos Modificadores de Strings

Além das strings usuais é possível usar modificadores por meio de prefixos para representar coisas diferentes no Python. As strings resultantes são objetos de string, como qualquer outra string, admitindo seus métodos e propriedades. O prefixo f é o mais comum, permitindo a avaliação de expressões dentro de uma string. Além dele podemos mencionar outros prefixos que podem ser usados sozinhos ou em conjunto:

  • b: bytes literais
  • f: string formatada
  • r: strings brutos
  • u: string usando Unicode legado (PEP 414)

As f-strings são de uso muito comum no Python, que permitindo avaliar expressões dentro de literais de string. As strings brutas não são tão populares quanto as f-strings mas têm seus próprios usos, importantes em certas ocasiões. Resumidamente elas ignoram sequências de escape e retornam texto puro. Strings de bytes são necessárias para armazenar objetos que não contém apenas caracteres mas também códigos de controles e outros valores binários. Unicode legado serve para expressar string em sistemas antigos que ainda não tinham o UTF-8 incorporado.

Strings e bytes

O Python tem o datatype bytes, uma classe diferente das strings.

» string1 = 'Python Pool' 
» print(type(string1))  
↳ <class 'str'>

» string2 = b'Python Pool' 
» print(type(string2)) 
↳ <class 'bytes'>

Método bytes(): O método bytes() converte strings em bytes, retornando um objeto imutável. Seu construtor pode receber o conteúdo da string e sua codificação.

» texto = "Podemos converter strings em bytes"
# converta string em bytes
» texto_to_byte = bytes(texto, 'utf-8')
» print(texto_to_byte)
# Output:
↳ b'Podemos converter strings em bytes'

Lembrando: Nos blocos de código nesse site usamos a notação » para linhas de código, para resuktados (outputs) e # para comentários do Python.

O método bytes(texto, 'utf-8') converte a string em um objeto bytes usando a codificação utf-8.
Também é possível passar apenas o tamanho da variável, ou uma lista (um iterável):

» tamanho = 7
» bvar = bytes(tamanho)
» print(bvar)
# Output
↳ b'\x00\x00\x00\x00\x00'

» lista = [1, 2, 3, 4, 5]
» bvar = bytes(lista)
» print(bvar)

# Output
↳ b'\x01\x02\x03\x04\x05'

As saídas contém os números de entrada transformados para o formato hexadecimal: 0 &arr;\x00, …, 5 &arr;\x05, etc.

Método decode(): O método inverso, para receber entradas em formato codificado e retornar o argumento decodificado em uma string comum é decode().

» st1 = b'Decodificar \xE2\x9C\x85'
» st2 = b'Decodificar \xE2\x9B\x85'
» print(st1.decode("utf-8"))
↳ Decodificar ✅
» print(st2.decode("utf-8"))
↳ Decodificar ⛅

Nesse caso decode() converteu bytes em strings. Nesse exemplo o código utf-8 \xE2\x9C\x85 representa o sinal ✅ enquanto \xE2\x9B\x85 é o código para o sinal ⛅.

Método chr(): O método chr() converte um inteiro no caracter unicode correspondente.

» print("Usando chr(int):")
» print(f"Maiúsculas: chr(65) = {chr(65)}, ..., chr(90) = {chr(90)}")
» print(f"Minúsculas: chr(97) = {chr(97)}, ..., chr(122) = {chr(122)}")
    
↳ Usando chr(int):
↳ Maiúsculas: chr(65) = A, ..., chr(90) = Z
↳ Minúsculas: chr(97) = a, ..., chr(122) = z

Strings formatadas

Iniciando com o Python 3.12 passou a ser permitido usar strings formatadas, ou f-strings. Elas são uma forma de inserir avaliações de código dentro de texto, concatenando literais de strings com o resultado.

» idade_1 = 15
» idade_2 = 19
» print(f"A idade somada de nossos alunos é {idade_1 + idade_2}, com média {(idade_1 + idade_2)/2}.")
↳ 'A idade somada de nossos alunos é 34, com média 17.0.'

» aluno = {"nome": "Jean Paul", "sobrenome": "Dirac" ,"area": "Física"}
» print(f"Nosso aluno {aluno["nome"]} {aluno["sobrenome"]} se destacou em {aluno["area"]}!")
↳ 'Nosso aluno Jean Paul Dirac se destacou em Física!'

# uma forma mais compacta de conseguir o mesmo resultado consiste em usar 
» aluno = {"nome": "Jean Paul", "sobrenome": "Dirac" ,"area": "Física"}
» print("Nosso aluno {nome} {sobrenome} se destacou em {area}!".format(**aluno))
↳ 'Nosso aluno Jean Paul Dirac se destacou em Física!'


No último exemplo usamos os nomes dos campos diretamente e passamos a string para o método string.format(**aluno). Como argumento o operador ** de desempacotamento (unpacking) retorna os valores do dicionário, ignorando campos extras, caso existam. Essa última forma é mais concisa e legível.

Strings brutas

Strings brutas (raw strings) tem um comportamento igual ao de uma string literal normal, com uma diferença importante: elas ignoram quaisquer sequências de caracteres de escape contidas na string. Por exemplo:

» print("1 - Componentes do átomo:\n\t Prótons \n\t Nêutrons \n\t Elétrons")
↳ 1 - Componentes do átomo:
       Prótons 
       Nêutrons 
       Elétrons

» print(r"2 - Componentes do átomo:\n\t Prótons \n\t Nêutrons \n\t Elétrons")
↳ 2 - Componentes do átomo:\n\t Prótons \n\t Nêutrons \n\t Elétrons

No primeiro caso os carateres de controle \n e \t, respectivamente newline e tab, são interpretados como controles. No segundo caso os controles são representados literalmente, como strings.

Se nenhum caracter de controle for encontrado a string fica inalterada com o prefixo r.

» print(r"É muito legal programar com python!" == "É muito legal programar com python!")
↳ True

# mas...
» print(r"10\25\1991" == "10\25\1991")
↳ False

regex
Esse tipo de recurso é particularmente útil quando usamos expressões regulares. Nelas são passadas aos métodos parâmetros com strings incluídas barras invertidas, que têm significado especial para as regex, sem necessidade de escape. Você pode ler sobre as expressões regulares em Expressoes Regulares no Python e, no caso geral, Expressoes Regulares.

Raw strings também auxiliam na construção de caminhos de arquivos e diretórios que contêm barras invertidas que não devem ser “escapadas”. Finalmente elas são úteis para exibir strings com caracteres difíceis de digitar ou ler, caracteres de controle como nova linha ou tabulações.

Codificações de caracteres em Python

Para representar caracteres e sinais de controle no computador diversos sistemas de codificação foram criados. Uma codificação é um sistema de traduzir caracteres (letras, pontuações, símbolos, espaços em branco e controle) para números inteiros e posteriormente para bits.

O mais simples deles é o sistema ASCII, que usa apena 1 byte em toda a sua codificação. Isso pode ser confirmado com a operação:

» all(len(chr(i).encode("ascii")) == 1 for i in range(128))
↳ True

o que mostra que todos os caracteres em chr(i).encode("ascii") com i = 0 … 127 tem comprimento 1.
Tabela ASCII resumida:

Código representa
0 até 31 caracteres de controle, não imprimíveis,
32 até 64 pontuação, símbolos, núeros e espaço,
65 até 90 caracteres maiúsculos do alfabeto inglês,
91 até 96 sinais gráficos como [ \ ] ^ _ `,
97 até 122 caracteres minúsculos do alfabeto inglês,
123 até 126 sinais gráficos adicionais como { | } ~,
127 controle de apagamento (del) não imprimível.

No entanto, o sistema de codificação ASCII permite apenas o mapeamento de 127 sinais ou controles. Ele é pequeno demais para representar a variedade de caracteres acentuados, além daqueles usados em outras línguas como chinês e russo. Por isso se desenvolveu um sistema capaz de representar um conjunto muito maior de caracteres e sinais, de denominado Unicode. Muitos outros sistemas foram criados entes dele mas é predominante, hoje o uso do Unicode e um de seus esquemas de codificação, o UTF-8. Por motivo de compatibilidade os primeiros 127 códigos coicidem com os do ASCII



O Unicode não é um sistema de codificação mas engloba vários deles. Ele mapeia caracteres como “a”, “¢” ou “😎”, incluindo emojis. Ele contém praticamente todos os caracteres necessários para uma comunicção moderna internacional, incluindo a marca (código 8207) que indica que o texto será exibido da direita para a esquerda, usado em línguas árabes.

UTF-8: Para representar essa diversidade de caracteres, sinais e controles surgiu o UTF-8, que usa mais de 1 bite na codificação. Seu código tem comprimento variável para cada sinal, podendo chegar a até 4 bites. Por default o Python usa a codificação utf-8.

O código abaixo mostra um caracter, um emoji, com comprimento 4 nessa codificação.

» emoji = "🤨"
» print(len(emoji))
↳ 1
» print(emoji.encode("utf-8"))
↳ b'\xf0\x9f\xa4\xa8'
» print(len(ibrow.encode("utf-8")))
↳ 4

# construir uma lista com objeto de bytes retorna
# o valor decimal para cada byte
» print(list(b'\xf0\x9f\xa4\xa8'))
↳ [240, 159, 164, 168]

Nota, é importante lembrar: o comprimento de um único caracter em unicode no Python é 1, mesmo que ele ocupe vários bites. O comprimento desse caracter codificado em bytes será entre 1 e 4, no UTF-8.

Funções associadas do Python

O Python tem várias funções internas (built-in) associadas com systemas de numeração e codificação de caracteres:

Função retorna
ascii(objeto) string, representação ASCII do objeto com caracteres não-ASCII escapados,
bin(inteiro) string, representação binária do inteiro com prefixo “0b”,
bytes(iterável) converte para bytes, dados binários brutos,
chr(inteiro) string, caracter unicode,
hex(inteiro) string, representação hexadecimal com prefixo “0x”,
int(número) inteiro, converte para inteiro,
oct(número) inteiro, converte para número octal com prefixo “0o”,
ord(string) inteiro, converte caracter unicode para inteiro,
str(objeto) converte para string, texto.

Alguns exemplos:

» print(ascii("jalapeño"))
↳ "'jalape\\xf1o'"

» print(bin(400))
↳ '0b110010000'

» print(bytes(range(97, 123)))  # Iterável de inteiros
↳ b'abcdefghijklmnopqrstuvwxyz'

» print(chr(1114111))
↳ '\U0010ffff'

» print(hex(100))
↳ '0x64'

» print(int('11', base=2))
↳ 3

» print(ord("a"))
↳ 97

» print(str(5))
↳ '5'

» print(str(0xc0ffee))
↳ '12648430'

Módulo String

O Módulo de Strings vem pre-instalado no Python, contendo constantes e método úteis, e duas classes.

String, constantes:

» import string

# string module constants
» print(string.ascii_letters)
↳ abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ

» print(string.ascii_lowercase)
↳ abcdefghijklmnopqrstuvwxyz

» print(string.ascii_uppercase)
↳ ABCDEFGHIJKLMNOPQRSTUVWXYZ

» print(string.digits)
↳ 0123456789

» print(string.hexdigits)
↳ 0123456789abcdefABCDEF

» print(string.whitespace)  # ' \t\n\r\x0b\x0c'

» print(string.punctuation)
↳ !"#$%&'()*+,-./:;?@[\]^_`{|}~

Método capwords(): O módulo possui um único método, capwords(texto, sep=None). Ela quebra a string em texto, transforma em maiúscula a primeira letra de cada palavra e as junta na string de retorno. Se o argumento opcional sep="None" espaços em branco nas extremidaddes são removidos. Se outro sep é fornecido ela é usado para quebrar o texto original.

» import string
» texto = "  venha aprender o \n\n python  "
» print(string.capwords(texto))
↳ "Venha Aprender O Python"

Classe Formatter: O módulo string possui também duas classes. A classe Formatter tem o mesmo comportamento que string.format() mas é útil para ser herdada em uma classe nova do usuário que define formatação personalizada.

» from string import Formatter
» formatter = Formatter()
» print(formatter.format('Um bom site é {site}', site='Phylos.net.'))
↳ 'Um bom site é Phylos.net.'

# variáveis podem ser posicionais e nomeadas
» print(formatter.format('{} {site}', 'Visite nosso site: ', site='Phylos.net.'))
↳ 'Visite nosso site: Phylos.net.'

# O método de string "format()" tem o mesmo comportamento
» print('{} {site}'.format('Leia sobre vários tópicos em ', site='Phylos.net.'))
↳ 'Leia sobre vários tópicos em Phylos.net.'

Método Template: A outra classe de Formatter é Template, útil para a produção de aplicativos internacionalizados, onde um marcador informa a posição onde texto posterir será inserido.

» from string import Template
» temp = Template('Em $lingua a palavra "$palavra" é traduzida como  "$traducao".')
» texto = temp.substitute(lingua='português', palavra='intruder', traducao='intruso')
» print(texto)
↳ 'Em português a palavra "intruder" é traduzida como  "intruso".'

Bibliografia

Todas as URLs foram visitadas em janeiro de 2024.

Flet: Cores e Temas

Cores e Temas

É claro que uma escolha adequada de cores para o seu aplicativo é um passo essencial para a produção de um produto agradável de se olhar e usar. Essa escolha faz parte dos esforços de construir apps com boa usabilidade. O estudo completo de usabilidade e uso de cores é complexo. Algumas referências podem ser encontradas na bibliografia.

O Flet apresenta uma proposta de construção de interfaces gráficas que, por definição, devem ter boa aparência. Para isso podemos aplicar cores, transparências e sombreados aos controles.

Valores de Cores

Cores no Flet podem ser definidas por seu valor hexadecimal ou valores nomeados, idênticos aos usados com HTML e CSS. Em formato hexadecimal a cor é expressa por uma string com o seguinte formato:

Hexadecimal: #aarrggbb (0xaarrggbb) or #rrggbb (0xeeggbb).

Nesse código as letras significam: a opacidade, r vermelho, g verde, b azul; todas em notação hexadecimal.

Se o número de opacidade é omitido ele é ajustado para o valor máximo ff (255, totalmente opaco).

c1 = ft.Container(bgcolor='#ff0000')    # container de fundo vermelho
c2 = ft.Container(bgcolor='#00ff00')    # container de fundo verde
c3 = ft.Container(bgcolor='#0000ff')    # container de fundo azul

c4 = ft.Container(bgcolor='#80ff0000')  # fundo vermelho, 50% transparente

Valores nomeados: são strings que representam as cores e podem ser interpretadas pelos controles do Flet. Alternativamente elas podem ser passadas como propriedades do módulo flet.colors.

c5 = ft.Container(bgcolor=ft.colors.BLUE)
c6 = ft.Container(bgcolor='blue')

Opacidade: A opacidade (o inverso de transparência) pode ser ajustada com o método with_opacity, com valor entre 0.0 (100% transparente) e 1.0 (0% transparente).

color = ft.colors.with_opacity(0.5, ft.colors.PRIMARY)
color = ft.colors.with_opacity(0.5, '#ff6666')
color = ft.colors.with_opacity(0.5, '#ff6666')
color = "#80fff6666"
color = "blue, 0.5" #  50% azul

Cores de Tema

Esse esquema é baseado no ColorScheme do Flutter que, por sua vêz é baseado nas especificações do Material Design.

Material Design é uma linguagem de design desenvolvida pelo Google e apresentada em 2014. Ele layouts baseados em grade, animações e transições responsivas, e profundidade efeitos de iluminação e sombras. Seu principal objetivo é o estabelecimento de uma linguagem visual combinando princípios de bom design com inovação técnica e científica.

Existem 30 cores de temas nomeadas em theme.color_scheme que são geradas de acordo com a semente (seed) pela propriedade color_scheme_seed, sendo o azul a cor de seed default.

Figura 1: Cores em ft.colors. Por exemplo: ft.colors.INDIGO_ACCENT_200

Cores dos controles

Muitos controles do Flet recebem cores default que dependem do tema escolhido (color_scheme), que define as cores de todos os controles em todos os níveis do aplicativo. Essas cores podem ser sobrescritas em diversos níveis, tais como atribuindo a cor de fundo ou destaque (background e foreground) de um container ou botão, personalizando os componentes de uma barra de rolagem (scrollbar) ou de guias (tabs).

Cores locais nos controles
Para definir a cor local (“control level”) de um controle específico, usamos:

ctl = ft.Container(width=100, height=70, bgcolor=ft.colors.BLUE_400)
bt = ft.ElevatedButton("Go", icon="arrow_circle_left", bgcolor="white", width=150, height=30)
ctl2 = ft.Container(width=50, height=50,
                    border=ft.border.all(1, "#293B4A"),
                    border_radius=ft.border_radius.all(10),
                    bgcolor="#F1EBC5")

Cores dos controles com temas
Alguns controles não podem ter sua cor ajustada localmente e suas cores são definidas apenas pelo tema. É o caso de FilledButton cuja cor default primária (“primary” color) é definida pelo tema de seu controle pai (seu conteiner).

Para os controles sem definição de cores locais, como a barra de rolagem (ScrollBar), suas cores serão obtidas a partir de seu ancestral mais próximo, se eles possuem a definição de tema para essas barras. Isso também ocorre com Tabs ou Text.
Nota: ScrollBars são (usadas nos controles Page, View, Column, Row, ListView and GridView, Tabs e Text. Para personalizar um controle específico (ScrollBar, Text ou Tabs control) podemos envolvê-lo em um container e personalizar o tema respectivo (scrollbar_theme, text_theme ou tabs_theme) no tema desse container.

Níveis de temas
O Flet procura o tema definido na antecessor mais próximo, usando aquele ColorScheme. Por exemplo, o código abaixo define um FilledButton dentro de um Container, que é seu antecessor mais próximo. A cor primária do botão será obtida no tema do Container. Por exemplo, o código:

import flet as ft

def main(page: ft.Page):
    container = ft.Container(
        width=200,
        height=200,
        bgcolor="#E3DA2D",
        border=ft.border.all(5, ft.colors.INDIGO_ACCENT_200), 
        content=ft.FilledButton("Cor Primária\n(Primary color)"),
        theme=ft.Theme(color_scheme=ft.ColorScheme(primary=ft.colors.DEEP_PURPLE_100)))

    page.add(container)

ft.app(target=main)
Figura 2: resultado do código

Esse código gera a imagem na figura 2.
Caso a propriedade de cor não esteja definida localmente no controle ou por tema, nem no antecessor mais próximo, a cor será buscada no próximo antecessor disponível, que pode ser a página (page) com seu color_scheme definido (ou default).

Tema nas páginas

Temas são atribuídos às páginas (page) através da propriedade page.theme, que recebe uma instância da classe theme.Theme. No presente estado de desenvolvimento do Flet um tema só pode ser gerado automaticamente por meio de uma cor semente (seed). Por exemplo, para gerar um tema claro baseado na cor verde usamos:

page.theme = theme.Theme(color_scheme_seed="green")
page.update()
Figura 3: tema default e com seed

A figura mostra a renderização da interface com o mesmo código do exemplo na figura anterior, destituído de cores. Os objetos assumeme suas cores com o tema default. A imagem à direita mostra o efeito de page.theme = theme.Theme(color_scheme_seed=”green”).

Além disso a classe Theme possui outras propriedades:

Propriedade Descrição
color_scheme_seed cor usada pelo algoritmo para construir as demais cores do tema.
color_scheme instância da classe ft.ColorScheme para customização do esquema de cores derivado de color_scheme_seed.
text_theme instância da classe ft.TextTheme para customização de estilos de texto contrastando com as cores de card e canvas.
primary_text_theme instância da classe ft.TextTheme que descreve tema para textos em contraste com as cores primárias.
scrollbar_theme instância da classe ft.ScrollbarTheme para customização da aparência de scrollbars.
tabs_theme instância da classe ft.TabsTheme para customização da aparência de Tabs.
font_family a fonte base para todos os elementos gráficos.
use_material3 booleano (default=True) para usar design do Material 3 design. Caso contrário usa Material 2.
visual_density enun ThemeVisualDensity: STANDARD (default), COMPACT, COMFORTABLE, ADAPTIVE_PLATFORM_DENSITY.
page_transitions instância de PageTransitionsTheme para customização de transições de navegação na página para diferente plataformas. Veja a seção sobre transições.

ColorScheme
A classe ColorScheme tem propriedades como:

  • primary: cor primária de telas e componentes
  • secondary: cor de destaque usada em componentes menos proeminentes na interface
  • error: cor usada para validação de campos de entrada, como TextField e error_text
  • background: cor de fundo de conteúdos roláveis (com scroll)
  • shadow: cor das sombras sobre componentes elevados
Figura 4: Propriedades de ColorScheme

Uma lista completa das propriedades pode ser vista abaixo.



Classe TextTheme
É usada para a customização de estilos de texto. TextTheme tem as seguintes propriedades de ft.TextStyle:

Propriedade Descrição
body_large estilo de texto grande usados em passagens longas.
body_medium estilo de texto médio (junto com body_large). É o default da padronização Material.
body_small estilo de texto pequeno.
display_large estilo para o maior texto, reservado para trechos curtos e importantes. Melhor em telas grandes.
display_medium estilo de tamanho médio.
display_small estilo de tamanho pequeno.
headline_large estilo para cabeçalhos grandes. Cabeçalhos (headlines) são menores que display. Adequado para texto curto e telas menores.
headline_medium estilo para cabeçalhos (headlines) medios.
headline_small estilo para cabeçalhos (headlines) pequenos.
label_large estilo para labels grandes. Labels são menores e usados para texto dentro de componentes, como legendas. Úteis em ElevatedButton, TextButton e OutlinedButton.
label_medium Tamanho médio em estilo de labels.
label_small Tamanho pequeno em estilo de labels.
title_large estilo para títulos grandes. Títulos são menores que headlines e devem ser usados para textos curtos de ênfase média.
title_medium estilo para títulos médios.
title_small estilo para títulos pequenos.

Classe ScrollbarTheme
Customiza as cores, espessura e forma de barras de rolagem scrollbars em todo o aplicativo.

A classe ScrollbarTheme tem as seguintes propriedades:

thumb_visibility visibilidade da alça (thumb) da barra de rolagem (scrollbar) mesmo quando não em uso. Se False a scrollbar será exibida apenas quando em uso. A propriedade pode ser um valor booleano ou um dicionário tendo como chaves as propriedades de ft.MaterialState e os valores booleanos.
thickness largura da scrollbar no eixo de rolagem. Pode ser um valor de ponto flutuante ou um dicionário tendo como chaves as propriedades de ft.MaterialState e números como valores.
track_visibility Se o trilho (track) scrollbar deve ser visível. Se True o trilho permanecerá visível enquanto sua alça estiver visível. Se a alça não for visível o trilho também não será. Se a propriedade for ajustada para None essa visibilidade será False e o tema ScrollbarTheme.track_visibility do Theme.scrollbar_theme será usado. O valor default é False se esse último também for None. Essa propriedade também pode ser um booleano ou um dicionário tendo como chaves as propriedades de ft.MaterialState e números como valores.
radius o raio de arredondamento da alça da barra de rolamento.
thumb_color sobrescreve a cor default do Scrollbar thumb. Esse valor pode ser uma cor ou um dicionário de ft.MaterialState.
track_color sobrescreve a cor default do scrollbar track. Esse valor pode ser uma cor ou um dicionário de ft.MaterialState.
track_border_color sobrescreve a cor default da borda da alça. Esse valor pode ser uma cor ou um dicionário de ft.MaterialState.
cross_axis_margin distância entre a alça da barra até a borda mais próxima. A barra de rolamento preenche todo esse espaço. O valor não pode ser none e tem default 0.
main_axis_margin distância entre o início da alça da barra até a borda da caixa onde ele está desenhado. Esse valor afeta a área disponível para a barra. A barra de rolamento preenche todo esse espaço. O valor não pode ser Null e tem defaul 0.
min_thumb_length valor mínimo preferencial para o comprimento da alça da barra quando o comprimento da barra é muito grande.
interactive booleano, se True a barra de rolagem será interativa, repondendo ao arrasto da alça ou clique (ou toque) no trilho. Se False a barra não responderá a eventos de gestos ou hover, mas permitira cliques em toda a sua extensão. Defaults é True se valor = None (exceto no Android onde o default é False.
Figura 6: Tabs (abas ou guias)
Figura 5: Terminologia em Scrollbar

Aqui usamos a terminologia:
scrollbar: barra de rolagem
track: trilho
thumb: alça

tabs: abas ou guias

Classe TabsTheme
Customiza a aparência dos controles de Tabs em todo o aplicativo.

A classe TabsTheme tem as seguintes propriedades:

divider_color cor do divider.
indicator_border_radius raio das bordas do indicador.
indicator_border_side cor and peso (weight) da linha horizontal abaixo do tab selecionado.
indicator_padding indica localização do sublinhado sob o tab selecionado em relação à sua borda. A propriedade indicator_tab_size é marcada como False para definir o indicador centralizado e como True para todo o tab.
indicator_color cor da linha abaixo do tab selecionado.
indicator_tab_size se True o indicador ocupa todo o tab.
label_color cor dos rótulos labels dos tabs selecionados.
unselected_label_color cor dos rótulos labels dos tabs não selecionados.
overlay_color define a resposta de “tinta” sob ação de focus, hover e splash.

Transições de Navegação
theme.page_transitions permite a costumização dos efeitos de transição da página. Esses efeitos são diferentes para as diversas plataformas. Seu valor é uma instância da classe PageTransitionsTheme com as seguintes propriedades opcionais:

  • android (default: FADE_UPWARDS)
  • ios (default: CUPERTINO)
  • macos (default: ZOOM)
  • linux (default: ZOOM)
  • windows (default: ZOOM)

As transições admitidas estão no enum ft.PageTransitionTheme:

  • NONE, nenhum delay, transição sem animação,
  • FADE_UPWARDS, desaparece de baixo para cima,
  • OPEN_UPWARDS, abre de baixo para cima,
  • ZOOM, ampliação
  • CUPERTINO, abre no estilo Cupertino.

Segue um exemplo:

theme = ft.Theme()
theme.page_transitions.android = ft.PageTransitionTheme.OPEN_UPWARDS
theme.page_transitions.ios = ft.PageTransitionTheme.CUPERTINO
theme.page_transitions.macos = ft.PageTransitionTheme.FADE_UPWARDS
theme.page_transitions.linux = ft.PageTransitionTheme.ZOOM
theme.page_transitions.windows = ft.PageTransitionTheme.NONE
page.theme = theme
page.update()

Tema de Página
A propriedade page.theme admite como valor opcional o enum ThemeMode com valores:
SYSTEM (default), LIGHT ou DARK.

Bibliografia

Todas as URLs foram visitadas em janeiro de 2024.

Flet: Stack

Objeto Stack

Em alguns casos queremos colocar controles em cima de outros. No Flet é possível sobrepor vários filhos do controle Stack. Podemos, por exemplo, colocar um texto sobre uma imagem ou um container móvel, como fizemos no exemplo 3.

Propriedades de Stack

O controle Stack possui apenas 2 propriedades:

clip_behavior
Como será cortado (clipped) o conteúdo do objeto.
Os valores possíveis estão armazenados em no enum ClipBehavior com os valores:

  • NONE
  • ANTI_ALIAS
  • ANTI_ALIAS_WITH_SAVE_LAYER
  • HARD_EDGE (default)

controls
Uma lista contendo os controles a serem exibidos dentro de Stack, em ordem de exibição, sendo o último controle exibido por cima dos demais.

Controles dentro de Stack

Diversos controles, ao serem colocados dentro de um Stack passam a admitir propriedades de posição. São elas:

Figura 1: distâncias em Stack

left: distância entre a borda esquerda do controle filho até a borda esquerda do Stack.
right: distância entre a borda direita do controle filho até a borda direita do Stack.
bottom: distância entre a borda inferior do controle filho até a borda inferior do Stack.
top: distância entre a borda superior do controle filho até a borda superior do Stack.

Exemplo de uso do Stack

O exemplo de código a seguir insere 3 controles dentro de um Stack, 1 imagem e 2 linhas. Nessa posição eles admitem as propriedades listadas acima, em particular top e left que são usadas para mover a linha de texto.

1  import flet as ft
2  import time
3  
4  def main(page: ft.Page):
5      def voltar(e):
6          texto.size = 50
7          linha_texto.top=500
8          linha_texto.left=0
9          page.update()
10 
11     def partir(e):
12         while linha_texto.top > 0:
13             time.sleep(.1)
14             texto.size -= 1
15             linha_texto.top-=10
16             linha_texto.left+=10
17             page.update()
18 
19     imagem = ft.Image(src=f"https://phylos.net/wp-content/uploads/2020/02/deepfield.jpg",
20                       width=950, height=556, fit=ft.ImageFit.NONE)
21    
22     texto = ft.Text("Em uma galáxia muito muito distante...",
23                      color="yellow", size=50, weight="bold", opacity=0.3)
24   
25     linha_texto = ft.Row([texto], left = 0, top=500)
26
27     bt_partir = ft.ElevatedButton("Partir!", bgcolor="blue", color="white",
28                                   width=120, height= 35, on_click=partir)
29
30     bt_voltar = ft.ElevatedButton("Voltar", bgcolor="blue", color="white",
31                                    width=120, height= 35, on_click=voltar)
32
33     linha_botoes = ft.Row([bt_partir,bt_voltar], left = 10, top=10)
34
35     page.add(ft.Stack([imagem, linha_texto,linha_botoes], width=950, height=556))
36 
37 ft.app(target=main)
Figura 2: resultado do código com Stack. Apenas 3 frames são mostrados.

Um clique no botão Voltar retorna as propriedades do texto e da linha que o contém para seus valores iniciais. Observe que, se o botão Partir! for acionado antes que o loop na linha 12 tenha terminado a velocidade de subida do texto aumentará pois os incrementos serão aumentados (loops dentro de loops).

Exemplo: sobreposição de controles

Mais um exemplo ajudará a mostrar que, dentro de um Stack, controles desenhados estão dispostos em camadas. Se dois controles são posicionados no mesmo lugar o último deles, na camada superior, ficará visível, cobrindo o controle por baixo.

1  import flet as ft
2  
3  class Ct(ft.Container):
4      def __init__(self, cor, acima, direita, abaixo, esquerda):
5          super().__init__()
6          self.width=20
7          self.height=20
8          self.border_radius=5
9          self.bgcolor=cor
10         self.top=acima
11         self.right=direita
12         self.bottom=abaixo
13         self.left=esquerda
14   
15 def main(page: ft.Page):
16     page.horizontal_alignment = ft.CrossAxisAlignment.CENTER
17     page.vertical_alignment = ft.MainAxisAlignment.CENTER
18
19     def mover(e):
20         e.control.data = (e.control.data % 5) + 1
21         i = e.control.data
22         st.controls.pop()
23         if i==1: ct5=Ct("purple", 0, 60,60,0)
24         elif i==2: ct5=Ct("purple", 0,0,60,60)
25         elif i==3: ct5=Ct("purple", 60,0,0,60)
26         elif i==4: ct5=Ct("purple", 60,60,0,0)
27         else: ct5=Ct("purple", 30,30,30,30)
28         st.controls.append(ct5)
29         st.update()
30
31     ct1 = Ct("red", 0, 60,60,0)
32     ct2 = Ct("yellow", 0,0,60,60)
33     ct3 = Ct("blue", 60,0,0,60)
34     ct4 = Ct("green", 60,60,0,0)
35     ct5 = Ct("purple", 30,30,30,30)
36     ct5.data=0
37     bt_mover = ft.ElevatedButton("Mover", bgcolor="blue", color="white",
38                                   width=120, height= 35, data = 0, on_click=mover)
39
40     st = ft.Stack([ct1, ct2, ct3,ct4, ct5])
41     st.data = 0
42
43     page.add(ft.Container(st, border_radius=8, padding=5,
44                           width=100, height=100, bgcolor="black"), bt_mover)
45
46 ft.app(target=main)
Figura 3: sobreposição de controles com Stack.

O exemplo também ajuda a ilustrar o uso de POO no Flet. Como muitos controles com propriedades semelhantes serão criados, a classe Ct foi criado na linha 3, herdando de um ft.Container. Objetos criados nas linhas 31 a 35 passam para o construtor apenas a cor e a posição do container.

A função mover(), acionada por bt_mover remove o último objeto no Stack na linha 22 e cria um novo container roxo por cima dos demais, circularmente. O container coberto não deixa de existir e reaparece na próxima operação.

Flet: Exemplo 3

Captura de toques de teclado

Atalhos de Teclado

Na seção Widgets: Atalhos de Teclado vimos como capturar a interação do usuário com o aplicativo através de eventos de teclados. A captura de eventos produzidos pelo teclado se dá através do evento page.on_keyboard_event, que gerencia o pressionamento de teclas de letras em combinação com teclas de controle. Esse evento passa o parâmetro e que é uma instância da classe KeyboardEvent, e tem as seguintes propriedades:

e.key Representação textual da tecla pressionada.
e.shift Booleano: True se a tecla “Shift” foi pressionada.
e.ctrl Booleano: True se a tecla “Control” foi pressionada.
e.alt Booleano: True se a tecla “Alt” foi pressionada.
e.meta Booleano: True se a tecla “Meta” foi pressionada††.

Desta forma conseguimos capturar o pressionamento de uma única letra, além de Enter, Backspace, F1F12, Escape, Insert, Page Down, Pause, etc, ou de combinações como Ctrl-F ou Ctrl-Shift-Enter.

Como exemplo adicional vamos exibir o código de um jogo que captura a interação com o teclado. Uma combinação de teclas como Ctrl-H ou SHIFT-L é exibida em um container que desce do topo da página. Se o usuário digita a mesma combinação ele pontua (10 pontos de cada vez) e nova combinação é exibida.

1   import flet as ft
2   import time, random, string
3
4   cores = ["#FFC6C6", "#A8DCEA", "#BAF3E4", "#F1E9AF",
5            "#CBB1CE", "#B2C9F3", "#F0DCBE", "#F1D7B3"]
6
7   def main(page: ft.Page):
8       def sortear():
9           ct.top = 10        
10          ct.left = random.randrange(100, 1000)
11          ct.height=80
12          t1 = ["Shift", "Alt", "Ctrl"][random.randrange(0, 3)]
13          t2 = random.choice(string.ascii_letters).upper()
14          ct.content = ft.Text(f"{t1}-{t2}", size=20)
15          ct.bgcolor = cores[random.randrange(0, 8)]
16          ct.visible = True
17
18      def fim(e):
19          page.window_destroy()
20
21      def inicio(e):
22          while True:
23              sortear()
24              while ct.top < 400:
25                  time.sleep(.3)
26                  ct.top += 10
27                  page.update()
28              inicio(e)
29
30      def teclou(e):
31          txt = "Shift" if e.shift else "Alt" if e.alt else "Ctrl" if e.ctrl else ""
32          txt = f"{txt}-{e.key}"
33
34          if ct.content.value == txt:
35              pts = int(txtPontos.value.split(":")[1]) + 10
36              txtPontos.value = f"Sua pontuação: {str(pts)}"
37              ct.bgcolor = "red"
38              while ct.top < 400:
39                  time.sleep(.01)
40                  ct.top += 10
41                  ct.height -= 3
42                  page.update()
43
44      ct = ft.Container(padding = 15, width=180, alignment = ft.alignment.center,
45                        border = ft.border.all(2, "black"),
46                        border_radius = ft.border_radius.all(15),
47                        blur=90, visible = False)
48
49      btInicio = ft.ElevatedButton("Começar Jogo", bgcolor="white",
50                                  width=180, height= 40, on_click=inicio)
51      btFim = ft.ElevatedButton("Terminar Jogo", bgcolor="white",
52                                width=180, height= 40, on_click=fim)
53      txtPontos = ft.Text("Sua pontuação: 0", size=27, weight=ft.FontWeight.BOLD)
54      lin = ft.Text("Acerte a combinação de teclas mostrada na tela (Ctrl, Shift, Alt) + Letra",
55                    size=27)
56      linha1 = ft.Row([lin], alignment=ft.MainAxisAlignment.SPACE_EVENLY)
57      linha2 = ft.Row([btInicio, txtPontos, btFim],
58                      alignment=ft.MainAxisAlignment.SPACE_EVENLY)
59
60    page.add(linha1, linha2, ft.Stack([ct]))
61    page.on_keyboard_event = teclou
62
63  ft.app(target=main)

Nesse bloco de código usamos a notação Camel case para dar nomes às variáveis. Ela consiste em usar a primeira letra minúscula e a primeira letra de cada nova palavra subsequente maiúscula. Como exemplo temos casaDaVovo ou notaAluno. O desenvolvedor pode escolher que tipo de notação usar, sendo consistente em seu código e procurando respeitar as convenções da linguagem.

O início do jogo ocorre quando se clica o botão btInicio que dispara a função inicio(). Dentro dela um loop infinito promove várias interações do container descendo a tela (o que é obtido com container.top += 10). Quando o container chega em seu valor máximo estabelecido, no caso ct.top < 400, novos valores são sorteados como suas propriedades e o container e redesenhado no topo.

O sorteio define a posição (top) inicial, uma cor aleatória escolhida dentro de uma lista de cores (linha 15) e monta uma string para representar uma combinação de teclado (linhas 12 a 14). A linha 13 usa random.choice() para escolher aleatoriamente um dos caracteres de string.ascii_letters, e torná-lo maísculo. Esse carater é juntando à um controle. A tecla de controle Meta (Windows) não foi usada pois é comum que ela esteja associada a atalhos globais do computador, em algumas instalações.

A interação do usuário é capturada em teclou que compara a combinação sorteada com aquela teclada pelo usuário. Se ocorreu um acerto ela incrementa a pontuação, derruba o container (acelerando sua queda) e sorteia nova combinação. O loop infinito iniciado na linha 22 só é terminado com o evento btFim.on_click.

Captura de teclas no Python

Claro que podemos capturar o pressionamento de tecla no Python, sem usar os métodos do Flet. Para isso podemos instalar, entre outras escolhas, o módulo keyboard, que funciona em sistemas operacionais Windows e Linux.

# Instale o módulo de teclado usando PIP:
pip install keyboard

# No Python 3 instale usando o comando:
pip3 install keyboard

Para detectar pressionamentos de teclas podemos usar a função is_pressed(). Esse método recebe um caractere como entrada e retorna True se essa tecla foi pressionada. O exemplo seguinte usa um loop que só é terminado com o pressionamento da tecla f.

import keyboard

print("Pressione 'f' para terminar.")
while True:
    if keyboard.is_pressed("a"):
        print("A tecla 'f' foi pressionada!")
        break

A função keyboard.is_pressed("f") retorna False se qualquer outra tecla fopr pressionada.

Também podemos usar a função read_key() para capturar pressionamentos de teclas. Essa função retorna a tecla pressionada pelo usuário.

import keyboard
while True:
    print(keyboard.read_key())
    if keyboard.read_key() == "f":
        print("Você terminou o loop pressionando 'f'.")
        break

Esse código exibe na tela todas as teclas pressionadas até que se tecle “f”.

Outra forma de captura de pressionamento de teclas usa a função wait(), que recebe um caractere como entrada. Quando executado o programa é pausado até que seja pressionada a tecla passada como argumento para a função.

import keyboard
keyboard.wait("f")
print("Você digitou 'f'.")

Nesse último exemplo fica dispensado o uso de um loop para suspender a execução do código.

Bibiografia

Flet: Exemplo 2


Exemplos 2: Referências a controles

Variáveis são referências

Widgets são classes comuns do Python, com suas propriedades e métodos. Para atuar sobre os objetos criados fazemos uma referência a eles, por meio dos nomes de variáveis. Vale lembrar que no Python toda variável é uma refeerência a algum objeto. Por exemplo, no código abaixo, um objeto flet.Text foi atribuído à variável t.

1   import flet as ft
2   
3   def main(page: ft.Page):
4   
5       def mudar_texto(e):
6           t.value="Um texto em azul!"
7           t.color="blue"
8           page.update()
9   
10      t = ft.Text(value="Um texto em vermelho!", color="red", size=20)
11      bt=ft.ElevatedButton("Mudar texto", on_click=mudar_texto)
12      page.add(t,bt)
13
14   ft.app(target=main)
Figura 1

A linha 10 contém o código de inicialização do objeto Text onde atribuimos valor a duas propriedades, respectivamente value (o texto que será exibido) e color (a cor da fonte usada). Com um clique no botão bt as propriedades t.value e t.color são alteradas, usando a variável como referência ao objeto. A figura 1 mostra o estado inical do aplicativo, com o texto em vermelho, e após o clique no botão Mudar texto.

Obs.: para exibir t e bt na mesma linha podemos inserir uma linha na página contendo os dois widgets: page.add(ft.Row([t,bt])). Nesse caso o controle Row (em flet.Row(<lista-de-controles>))) serve de container para os controles na lista <lista-de-controles>.

Relembrando: t é uma referência à uma instância da classe Text.

Vimos que page recebe uma lista (de controles) no parâmetro controls. Essa lista pode ser tratada como qualquer outra lista do Python. Por exemplo, ela possui o método pop(n).

1   for i in range(6):
2       page.controls.append(ft.Text(f"Exibindo a linha {i}"))
3       page.update()
4
5   page.controls.pop()
6   page.update()
Figura 2

Esse código insere e exibe 6 linhas de texto na página. Em seguida ela exclui a última linha com page.controls.pop(). Assim com o fazemos com outras listas, page.controls.pop(n) excluiria a n-ésima linha.

A figura 2 mostra as 5 linhas restantes após a ação de page.controls.pop().

Referências para controles com a Classe Ref

Outro exemplo explora a mesma possibilidade manipulação das variáveis como referências aos objetos de controle. Os objetos TextField são criados com seus textos vazios, preenchidos pelo usuário, usados para inserir a linha 9de cumprimento com o clique do botão. O comando page.update() atualiza na tela apenas as campos que foram alterados. A propriedade autofocus=True na linha 4 garante que o cursor voltará para o objeto nome após a execução da função fala_oi.

1   import flet as ft
2
3   def main(page):
4       nome = ft.TextField(label="Nome", autofocus=True)
5       sobrenome = ft.TextField(label="Sobrenome")
6       coluna_ola = ft.Column()
7   
8       def fala_oi(e):
9           coluna_ola.controls.append(ft.Text(f"Olá, {nome.value} {sobrenome.value}!"))
10          nome.value = ""
11          sobrenome.value = ""
12          page.update()
13          nome.focus()
14    
15      page.add(
16          nome,
17          sobrenome,
18          ft.ElevatedButton("Diga oi!", on_click=fala_oi),
19          coluna_ola,
20      )
21
22  ft.app(target=main)

Em códigos mais longos e complexos um número maior de controles é adicionado, juntamente com suas propriedades e funções que manipulam seus eventos. Fica cada vez mais difícil, para o programador, se lembrar de todas as definições de nomes, por mais organizado que ele seja. A inserção de controles na página, efetuada nas linhas 15 a 20 não deixa muito clara a estrutura de controles inseridos.

Todos os controles do Flet possuem a propriedade ref. De acordo com os propgramadores do Flet esse é um conceito inspirado no React.

Para isso o Flet incluiu uma classe utilitária chamada Ref, usada para fazer uma referência a um controle que pode ser usada em gerenciadores de eventos (event handlers) e, posteriormente, ser conectada a um controle real quando se constroi a árvore do aplicativo.

Para demontrar o uso dessa classe vamos alterar o código do exemplo acima usando Refs. Listamos abaixo como definir uma nova referência, como usar essa referência para inserir valores e, finalmente, como atribuir um controle real a essa referência, aproveitando todas as suas propriedades já definidas.

1   # criar uma referência a um TextField (por exemplo)    
2   nome = ft.Ref[ft.TextField]()
3   
4   # acessar o controle referenciado
5   nome.current.value = "José Ninguém"
6   
7   # atribuindo a referência um controle real, que pode ser incluído na página
8   page.add(
9       ft.TextField(ref=nome, label="Nome", autofocus=True)
10   )

Vemos que objetos da classe Ref possuem a propriedade current que, por sua vez, contém as mesmas propriedades e métodos do pbjeto referenciado. Para ligar essa referência a um controle utilizamos a propriedade Control.ref do controle. Com essas definições a aplicativo pode ser escrito como:

1   import flet as ft
2   
3   def main(page):
4       nome = ft.Ref[ft.TextField]()
5       sobrenome = ft.Ref[ft.TextField]()
6       coluna_ola = ft.Ref[ft.Column]()
7   
8       def fala_oi(e):
9           coluna_ola.current.controls.append(
10              ft.Text(f"Olá, {nome.current.value} {sobrenome.current.value}!")
11          )
12          nome.current.value = ""
13          sobrenome.current.value = ""
14          page.update()
15          nome.current.focus()
16   
17      page.add(
18          ft.TextField(ref=nome, label="Nome", autofocus=True),
19          ft.TextField(ref=sobrenome, label="Sobrenome"),
20          ft.ElevatedButton("Diga oi!", on_click=fala_oi),
21          ft.Column(ref=coluna_ola),
22      )
23
24  ft.app(target=main)
Figura 3

Observe que, fazendo isso, a estrutura da árvore de controles se torna mais claramente visível quando os controles são adicionados à página, na linha 17 e seguintes.

A figura 3 mostra os campos preenchidos e o resultado do clique no botão Diga oi.

Interação com o usuário

Claro que a maioria dos aplicativos demandam algum tipo de interação com o usuário, seja para escolher um ítem de busca, inserir novos dados ou escolher entre opções. Os exemplos acima usam de flet.TextField para a inserção de textos e ft.ElevatedButton para receber cliques e acionar uma função. Claro que existem muitas outras formas de interação que podem ser simples como um clique em botão ou sofisticadas quanto capturar a posição na tela de um clique, dentro de algum objeto desenhado na página.

Caixas de checagens e “dropdowns”

Vimos vários exemplos de uso das caixas de texto puro flet.Text(“Texto a exibir!”), usadas para exibir texto, e caixas interativas flet.TextField onde o usuário pode digitar dados e inserí-los no aplicativo.

As caixas de checagem, flet.Checkbox são úteis quando a escolha de dados é binária, geralmente sim/não.

1   import flet as ft
2   
3   def main(page):
4       def checkbox_mudou(e):
5           texto_exibido.value = f"A caixa de checagem está  {'marcada' if check.value else 'desmarcada'}."
6           page.update()
7
8       texto_exibido = ft.Text()
9       check = ft.Checkbox(label="Clique na caixa de checagem...", value=False, on_change=checkbox_mudou)
10      page.add(check, texto_exibido)
11   
12  ft.app(target=main)
Figura 4

Esse código produz o resultado mostrado na figura, dependendo da caixa estar marcada ou não. O evento capturado nesse caso foi o flet.Checkbox.on_change que envia o evento para a função selecionada. Na função é lida a propriedade booleana check.value. A figura 4 mostra os dois estados possíveis do aplicativo.

Foi usado o operador trinário valor_se_verdadeiro if condição else valor_se_falso.

Dropdowns, flet.Dropdown são úteis quando várias escolhas de dados estão disponíveis. O conjunto permitido de escolhas pode ser inserido durante o desenvolvimento ou alterado pelo usuário.

1   import flet as ft
2
3   def main(page: ft.Page):
4       def dropdown_mudou(e):
5           escolhido.value = f"Você afirmou que prefere {dpd_linguagem.value}"
6           page.update()
7   
8       escolhido = ft.Text()
9       itens_escolher = [
10               ft.dropdown.Option("Python"),
11               ft.dropdown.Option("Java"),
12               ft.dropdown.Option("Javascript"),
13               ft.dropdown.Option("C, C+"),
14               ft.dropdown.Option("Rust"),
15               ft.dropdown.Option("Ruby"),
16               ]
17       dpd_linguagem = ft.Dropdown(
18           label="Linguagem de programação",
19           width=100,
20           options=itens_escolher,
21           on_change=dropdown_mudou,
22       )
23       page.add(dpd_linguagem, escolhido)
24   
25   ft.app(target=main)
Figura 5

Observe que flet.Dropdown.options é uma lista de objetos dropdown.Option(“nome”). O resultado está mostrado na figura 5.

Além do evento change, as caixas dropdown também podem responder a eventos on_blur, disparado quando o controle perde o foco, e on_focus, quando o controle recebe o foco.

Os ítens na caixa de seleção podem ser inseridos ou apagados dinamicamente, em tempo de execução, como mostramos no código seguinte.

1   import flet as ft
2   
3   def main(page: ft.Page):
4       def inserir(e):
5           if txt_insere.value:
6               dp_box.options.append(ft.dropdown.Option(txt_insere.value))
7               dp_box.value, txt_insere.value = txt_insere.value, ""
8               page.update()
9
10      def apagar(e):
11          for item in dp_box.options:
12              if item.key == dp_box.value:
13                  dp_box.options.remove(item)
14          page.update()
15
16      dp_box = ft.Dropdown(width=600)
17      txt_insere = ft.TextField(hint_text="Nome a inserir")
18      bt_insere = ft.ElevatedButton("Inserir", on_click=inserir)
19      bt_apaga = ft.OutlinedButton("Apagar item exibido", on_click=apagar)
20      page.add(dp_box, ft.Row(controls=[txt_insere, bt_insere, bt_apaga]))
21   
22  ft.app(target=main)
Figura 6

Na linha 6 um item é inserido na caixa dropdown. dp_box.options é uma lista que pode ser acrescida de elementos com append. Essa lista é composta de objetos flet.dropdown.Option(texto). A operação de apagar procura por um item nessa lista e o remove, caso encontrado. A propriedade item.key extrai o texto de cada componente da lista. O texte feito na linha 12 é necessário porque tentar remover um item inexistente de uma lista resulta em erro. O aplicativo executado exibe algo como o representado na figura 6.

Capturando cliques e “hovers”

Um exemplo de captura da posição do cursor do mouse no momento do clique pode ser visto na página Layout do Flet: View e Container na seção Container: Eventos onde um clique dentro de um container dispara um evento para exibir as posições do ponto clicado, relativo ao container e à página.

No exemplo abaixo as ações são iniciadas pelo evento flet.Container.on_hover, disparadas quando o cursor do mouse entra ou sai da área delimitada pelo controle. O evento e passado para a função limpar carrega a propriedade e.data == "true" se o cursor entrou na área, e e.data == "false" se saiu dela. Na saída uma cor é escolhida eleatoriamente entre as cores do dicionário cores.

1   import flet as ft
2   import random
3
4   def main(page: ft.Page):
5       cores = {0:"#000000", 1:"#1D74FF", 2:"#C51709", 3:"#FFE000", 4:"#00CB4F",
6                5:"#A866DC", 6:"#FF6600", 7:"#aaffff", 8:"#145375", 9:"#a4b3c5"}
7
8       def limpar(e):
9           page.controls.pop()
10          page.add(matriz())
11          page.update()
12
13      def on_hover(e):
14          if e.data == "true":
15              e.control.bgcolor = "#efefef"
16          else:
17              e.control.bgcolor = cores[random.randrange(10)]
18          e.control.update()
19
20      def matriz():
21          matr = ft.Column()
22          for i in range(10):
23              lin = []
24              for j in range(10):
25                  lin.append(ft.Container(width=50, height=50,
25                             border=ft.border.all(1, "#293B4A"),
27                             border_radius=ft.border_radius.all(10),
28                             bgcolor="#F1EBC5", on_hover=on_hover)
29                  )
30              linha = ft.Row(lin)
31              matr.controls.append(linha)
32          return matr
33
34      tit=ft.Text("Passe o cursor sobre os quadrados", size=18, weight=ft.FontWeight.BOLD)
35      bt_limpar=ft.ElevatedButton("Limpar tudo", icon="arrow_circle_left",
36                bgcolor="white", width=150, height=30, on_click=limpar)
37      page.add(ft.Row([tit,bt_limpar]), matriz())
38
39  ft.app(target=main)

 

Figura 7

O resultado do código está representado na figura 7, após o cursor ter sido passado sobre o objeto matriz(), que é uma coluna.

A construção da matriz de controles container é feita através da função matriz() que retorna 10 linhas, cada uma com 10 containeres, todos respondendo à container.on_hover com a mesma função, que altera a cor de fundo desse objeto.

O laço iniciado na linha 24 constroi um array com 10 objetos Containers (os quadrados). Esse array é inserido em uma linha flet.Row na linha 30. Essas 10 linhas são inseridas na coluna matr na linha 31.

O botão Limpar tudo remove o último controle inserido na página com page.controls.pop(), sendo que esse controle é a matriz de quadrados coloridos. Em seguida ele repõe novos quadrados com as cores originais.