Flet: CupertinoListTile

Layout

Controle CupertinoListTile

CupertinoListTile é um controle similar ao ListTile mas seguindo a estilização do iOS. O controle possui a propriedade notched que, se marcada como False (default) renderiza o controle no modo padrão do iOS, ou, se marcada como True gera o controle na aparência de “Inset Grouped”, o mesmo usado em iOS Notes ou Reminders.

Ele exibe uma linha de altura fixa com controles internos, geralmente imagens, ícones, textos e menus. 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.

import flet as ft

def main(page: ft.Page):
    page.bgcolor="#ABE6DB"

    cuperLT_1=ft.CupertinoListTile(
        title=ft.Text("Patches de recuperação do Windows 12", color="BLACK"),
        subtitle=ft.Text("Equipe Mostarda", color="#333355"),
        additional_info=ft.Text("Completar tarefa até 21:45H", color="#333355"),
        leading=ft.Icon(name=ft.cupertino_icons.CHAT_BUBBLE),
        trailing=ft.Icon(name=ft.cupertino_icons.BELL_CIRCLE_FILL),
        bgcolor="#BDAB8D"
    )

    cuperLT_2= ft.CupertinoListTile(
        title=ft.Text("Patches de recuperação do iOS 17", color="BLACK"),
        subtitle=ft.Text("Equipe Catchup", color="#333355"),
        additional_info=ft.Text("Completar tarefa até 09:15H", color="#333355"),
        leading=ft.Icon(name=ft.cupertino_icons.CHAT_BUBBLE_2),
        trailing=ft.Icon(name=ft.cupertino_icons.BELL_CIRCLE, color="RED"),
        bgcolor="#AD9B7D"
    )

    page.add(cuperLT_1, cuperLT_2)

ft.app(target=main)
Figura 1: renderização do código de exemplo de CupertinoListTile

Propriedades de CupertinoListTile

Propriedade Descrição
additional_info controle a ser exibido à direita do título mas antes do controle ao final da linha. Com frequência é usado um texto, seguido por um ícone, embora isso não seja obrigatório.
bgcolor_activated cor de fundo do controle após ter sido clicado ou tocado.
leading A Control para exibir antes do title.
leading_size restringe largura e altura do controle. Default é 28.0.
leading_to_title espaço horizontal entre leading e title. Default é 16.0.
notched booleano. Se notched=True o controle será criado com a forma de um “Inset Grouped”, usado em aplicativos do iOS como Notes ou Reminders. Default é False.
padding espaçamento interno no controle CupertinoListTile, descrevendo distância entre os controles internos: title, subtitle, additional_info e trailing.* Veja a propriedade Container.padding para mais informações e possíveis valores.
subtitle conteúdo adicional exibido abaixo do título. Em geral é usado um controle de texto.
title usado para exibir o conteúdo principal no controle CupertinoListTile. Em geral é usado um controle de texto.
toggle_inputs booleano. Se toggle_inputs=True um clique na lista de altera o estado dos controles internos Radio, Checkbox ou Switch. Default é False.
trailing controle a ser exibido após o título. Normalmente é usado um controle de ícone.
url URL usada para localizar um recurso quando a lista recebe um clique. Se o evento on_click estiver registrado (se houver uma função a ele associada), o evento é disparado.
url_target como abrir URL no modo web. Pode ser:

  • _blank: o recurso é aberto em nova aba ou janela (default).
  • _self: o recurso é aberto na aba ou janela atual.

Evento de CupertinoListTile

on_click dispara quando o usuário clica ou toca na lista.

Apesar de não encontrar documentação a respeito, na minha experiência os nomes dos ícones podem ser idênticos aos nomes do flutter, mas escritas em maiúsculas. Exemplos são:

  • ft.cupertino_icons.FOLDER,
  • ft.cupertino_icons.FUNCTION,
  • ft.cupertino_icons.HAMMER

Um aplicativo que busca ícones cupertino pode ser encontrada em Cupertino Icons.

Exemplo de uso de CupertinoListTile

import flet as ft

def main(page: ft.Page):
    page.bgcolor="#ABE6DB"

    def click(e):
        if e.control.notched:
            clt2.data +=1
            txt = f"Esse controle foi clicado {clt2.data} vez{'es' if clt2.data>1 else ''}."
            clt2.subtitle=ft.Text(txt, color="#333355")
        else:
            clt1.data +=1
            txt = f"Esse controle foi clicado {clt1.data} vez{'es' if clt1.data>1 else ''}."
            clt1.subtitle=ft.Text(txt, color="#333355")

        titulo.value =f"Os controles foram clicados {clt1.data+clt2.data} vezes."
        page.update()

    clt1=ft.CupertinoListTile(
        title=ft.Text("Patches de recuperação do Windows 12", color="BLACK"),
        subtitle=ft.Text(
            "Esse controle ainda não foi clicado.",
            color="#333355"
        ),
        additional_info=ft.Text("Completar tarefa até 21:45H", color="#333355"),
        leading=ft.Icon(name=ft.cupertino_icons.CAR_DETAILED, color="BLACK"),
        trailing=ft.Icon(name=ft.cupertino_icons.WRENCH, color="RED", size=30),
        padding=ft.padding.only(15, 20, 30, 40),
        bgcolor="#BDAB8D",
        bgcolor_activated=ft.colors.AMBER_600,
        data=0,
        on_click=click,
    )
    clt2=ft.CupertinoListTile(
        title=ft.Text("Patches de recuperação do iOS 17", color="BLACK"),
        subtitle=ft.Text(
            "Esse controle ainda não foi clicado.",
            color="#333355"
        ),
        additional_info=ft.Text("Completar tarefa até 09:15H", color="#333355"),
        leading=ft.Icon(
            name=ft.cupertino_icons.CAR_DETAILED,
            color="BLUE",
            size=30,
        ),
        trailing=ft.Icon(
            name=ft.cupertino_icons.GAUGE,
            color="RED",
            size=30,
        ),
        padding=20,
        bgcolor="#CDBB9D",
        bgcolor_activated=ft.colors.AMBER_100,
        notched=True,
        data=0,
        on_click=click,
    )
    titulo=ft.Text(
        "Nenhum controle foi clicado até agora",
        color="#0b4b8f",
        size=30,
        weight=ft.FontWeight.BOLD,
    )
    ct=ft.Container(
        content=titulo,
        margin=5,
        padding=5,
        alignment=ft.alignment.center,
        bgcolor="#95d3c8",
        width=850,
        height=50,
        border_radius=10,
    )
    page.add(ct, clt1, clt2)

ft.app(target=main)
Figura 2: renderização do código de exemplo de CupertinoListTile depois de 2 cliques em cada tile. O primeiro controle está sob efeito do clique, exibindo a cor bgcolor_activated (amber).

Valem as observações: um container é inserido na cabeçalho, contendo um título que usa titulo=ft.Text(...). A variável titulo foi definida separadamente para estar disponível para alterações na função click().

As duas tiles foram definidas com a propriedade data em clt=ft.CupertinoListTile(..., data=0), usada para contar quantos cliques foram aplicados sobre o controle. Essa mesma propriedade é usada para construir a propriedade clt.subtitle, também atualizada a cada clique.

A função click() distingue entre os dois controles através da propriedade e.control.notched, que só é verdadeira no segundo controle. Desta forma não é necessário criar uma função para cada controle.

Flet: GridView e ResponsiveRow

Outros controles de Layout: GridView e ResponsiveRow

Controle GridView

GridView, assim como ListView, é um controle apropriado para apresentar listas longas (milhares de ítens). Esses controles devem ser escolhidos ao invés de Column ou Row para se obter um rolamento suave de conteúdo. O Navegador de Ícones (Icons Brower) do Flet é construído com um GridView.

Um exemplo curto do uso do controle é mostrado abaixo. Nesse código um array (uma lista) de 8 containeres com cores de fundo escolhidas entre os valores da lista, cores. Essa lista é passada em flet.GridView.controls = array.

import flet as ft

cores = {0:"black",  1:"#1D74FF", 2:"#C51709", 3:"#FFE000",
         4:"#00CB4F",5:"#A866DC", 6:"#FF6600", 7:"#145375"}

def main(page: ft.Page):
    array = []
    for i in range(0, 8):
        array.append(
            ft.Container(
                bgcolor=cores[i],
                border=ft.border.all(1, "BLACK"),
                border_radius=ft.border_radius.all(15),
            )
        )

    grid_imagens = ft.GridView(
        controls = array,
        expand=1,
        runs_count=15,
        max_extent=100,
        child_aspect_ratio=1.0,
        spacing=2,
        run_spacing=2,
    )
    page.add(grid_imagens)

ft.app(target=main)

A execução desse código resulta na tela mostrada na figura 1.

Figura 1: oito containeres de cores diferentes adicionados a um GridView

Propriedades de GridView

Propriedade Descrição
auto_scroll booleano, auto_scroll=True se a barra de rolamento se move automaticamente para a posição onde o último filho foi inserido. Essa propriedade deve ser False para que o método scroll_to() funcione.
child_aspect_ratio proporção entre dimensões vertical e horizontal (cross-axis e main-axis) de cada filho.
controls lista de controles a serem renderizados dentro do GridView.
horizontal booleano, horizontal=True para que a grade empilhe itens horizontalmente.
max_extent largura ou altura máxima de cada ítem na grade.
on_scroll_interval intervalo em milisegundos para o disparo do evento on_scroll. Default é 10.
padding espaço interno de separação entre os filhos e a grade.

Veja a propriedade Container.padding para maiores informações.

reverse booleano, reverse=True faz com que o rolamento se dê invertido, de baixo para cima. Default é False.
run_spacing espaçamento entre cada filho em pixeis, ao longo do eixo vertical.
runs_count o número de controles filhos a serem apresentados na vertical.
spacing espaçamento entre cada filho em pixeis, ao longo do eixo horizontal.

Método de GridView

scroll_to(offset, delta, key, duration, curve) move a barra de rolamento para a posição definida, em termos absoluto, relativo ou salto para uma chave especificada.

Veja o método Column.scroll_to() para maiores informações.

Evento de GridView

on_scroll Dispara quando a posição de rolamento é alterada pelo usuário.Veja o evento Column.on_scroll para maiores informações.

Exemplo de uso de GridView

No exemplo abaixo usamos o controle flet.Image(src, fit, repeat, border_radius) que ainda não descrevemos nessas notas. Seu objetivo é o de inserir imagens, no caso atual lidas no repositório Image Gallery localizada em picsum.photos.

import flet as ft

def main(page: ft.Page):
    page.title = "GridView Example"
    page.theme_mode = ft.ThemeMode.DARK
    page.padding = 10
    
    txt = ft.Text("Controle GridView com Imagens", size = 28)
    titulo=ft.Container(txt, data=0)
    
    def incrementa(e):
        titulo.data+=12
        carregar_imagens()

    bt_mais=ft.ElevatedButton(
        "Carregar mais imagens",
        icon=ft.icons.BEACH_ACCESS,
        color="WHITE",
        bgcolor="#6E432B", width=300, height=40,
        on_click=incrementa
    )

    grid_imagens = ft.GridView(
        expand=1,
        runs_count=15,
        max_extent=150,
        child_aspect_ratio=1.0,
        spacing=5,
        run_spacing=5,
    )

    def carregar_imagens():
        grid_imagens.controls=[]
        for i in range(titulo.data, titulo.data+12):
            grid_imagens.controls.append(
                ft.Image(
                    src=f"https://picsum.photos/150/150?{i}",
                    fit=ft.ImageFit.NONE,
                    repeat=ft.ImageRepeat.NO_REPEAT,
                    border_radius=ft.border_radius.all(10),
                )
            )
        page.update()

    carregar_imagens()
    page.add(ft.Column([ft.Row([titulo,  bt_mais]), grid_imagens]))

ft.app(target=main)
Figura 2: Execução do código de exemplo de GridView

Nesse código o controle flet.GridView é criado com o array de controles vazio e depois populado pela função carregar_imagens() que insere 12 imagens na propriedade GridView.controls. A propriedade titulo.data armazena a posição das imagens baixadas e é incrementada de 12 a cada disparo do botão bt_mais.

Controle ResponsiveRow

ResponsiveRow traz para o Flet o mesmo conceito de layout de grade usado no framework Bootstrap. Com ele se pode alinhar os controles filhos em colunas virtuais que podem ser redimensionadas. Por padrão uma grade possui 12 colunas, número que pode ser modificado na propriedade ResponsiveRow.columns.

Cada coluna inserida em um ResponsiveRow possui a propriedade col especificando quantas colunas cada controle deve preencher, de modo análogo ao uso da propriedade expand. Por exemplo, para criar um layout que contém duas colunas abrangendo 6 colunas virtuais cada usamos:

import flet as ft

ft.ResponsiveRow([
    ft.Column(col=6, controls=[ft.Text("Coluna 1")]),
    ft.Column(col=6, controls=[ft.Text("Coluna 2")])
])

O controle é dito “responsivo” porque pode ajustar o tamanho de seus filhos à geometrias variáveis de telas, que pode ser a página, a janela, etc. No exemplo acima a propriedade col é um número constante, significando que o filho ocupará 6 colunas para qualquer tamanho de tela. Se a propriedade col de um filho não for especificada ele terá o número máximo de colunas.

A propriedade col pode ser configurada para ter um valor diferente para “pontos de interrupção” (breakpoints) específicos, de acordo com o tamanho da tela. Os pontos de interrupção, como intervalos de dimensão, recebem nomes:

Por exemplo, no código abaixo o conteúdo é colapsado em apenas uma coluna em uma tela pequena (como a de um telefone celular) e assume 2 colunas em telas grandes:

import flet as ft
ft.ResponsiveRow([
    ft.Column(col={"sm": 6}, controls=[ft.Text("Column 1")]),
    ft.Column(col={"sm": 6}, controls=[ft.Text("Column 2")])
])

Propriedades de ResponsiveRow

Propriedade Descrição
alignment alinhamento horizontal dos controles filhos. Por exemplo, MainAxisAlignment.START (que é o default) posiciona os filhos à esquerda da linha.

Os valores possíveis da propriedade estão no enum MainAxisAlignment com os valores:

  • START (default)
  • END
  • CENTER
  • SPACE_BETWEEN
  • SPACE_AROUND
  • SPACE_EVENLY
columns o número de colunas virtuais a usar no layout dos filhos. Default é columns=12.
controls uma lista de controles a serem exibidos dentro do ResponsiveRow.
run_spacing espaçamento entre as linhas quando o conteúdo está quebrado em múltiplas linhas. Default run_spacing=10.
spacing espaçamento entre os controles em uma linha. Default é spacing=10 (pixeis virtuais). Esse espaçamento só é aplicado quando o alinhamento (alignment) é start, end ou center.
vertical_alignment como os controles filhos são posicionados verticalmente.

Os valores possíveis da propriedade estão no enum CrossAxisAlignment com os valores:

  • START (default)
  • CENTER
  • END
  • STRETCH
  • BASELINE

Exemplo de uso de ResponsiveRow

No exemplo abaixo tanto as funções coluna() como linha() retornam containeres com um texto. Esses controles preenchem as posições de coluna do ResponsiveRow, a primeira delas especificando os pontos de quebra em
col={"sm": 6, "md": 4, "xl": 2}, a segunda em col={"md": 4}. Observe que Container não tem a propriedade col, exceto quando dentro de uma “linha responsiva”.

import flet as ft

def main(page: ft.Page):

    def coluna(texto, cor):
        # retorna um Container com um texto
        return ft.Container(
            ft.Text(texto),
            padding=15,
            bgcolor=cor,
            col={"sm": 6, "md": 4, "xl": 2}
        )

    def linha(texto, cor):
        # retorna um Container com um texto
        return ft.Container(
            ft.Text(texto, size=18),
            width=200, height=60, padding=15,
            border_radius=10,
            bgcolor=cor,
            col={"md": 4}
        )

    def page_resize(e):
        info.value = f"{page.width} px"
        info.update()

    page.on_resize = page_resize
    info = ft.Text(size=28)

    page.overlay.append(
        ft.Container(
            info,
            width=200, height=70, padding=10, border_radius=15,
            bottom=50, right=50, bgcolor="#acdeff",
        )
    )

    page.add(
        ft.ResponsiveRow(
            [
                coluna("Coluna 1", ft.colors.YELLOW),
                coluna("Coluna 2", ft.colors.GREEN),
                coluna("Coluna 3", ft.colors.BLUE),
                coluna("Coluna 4", ft.colors.RED),
            ],
        ),
        ft.ResponsiveRow(
            [
                linha("texto 1", ft.colors.BLUE_50),
                linha("texto 2", ft.colors.RED_50),
                linha("texto 3", ft.colors.GREEN_50),
            ],
            run_spacing={"xs": 10},
        ),
    )
    page_resize(None)

ft.app(target=main)

A propriedade de largura da página page.width é exibida dentro de um Container inserido na camada page.overlay para flutuar em cima dos controles da página. Esse controle está definido em espaçamento fixo bottom=50 e right=50, podendo se sobrepor aos controles da camada abaixo. A atualização é disparada no evento page.on_resize.

Observe que a atualização realizada no redimensionamento altera info.value e a renderização é exposta na página com info.update(). O mesmo efeito seria obtido com page.update() pois esse método cuida de atualizar apenas as partes modificadas da página.

O resultado da execução do código aparece nas figuras 3 e 4.

Figura 3: estado inicial do aplicativo com tela de largura superior a 1200 px.
Figura 4: estado do aplicativo com tela redimensionada para 567 px, depois 288px.

Flet: Row e Column

Layout do Flet: Row e Column

Flet: Row

Row (linha) é um controle que serve de container para outros controles e os exibe horizontalmente. Ele possui propriedades, eventos e métodos relativos ao layout e gerenciamento de rolagem da página (scroll) para evitar overflow (quando o conteúdo ocupa áerea maior que a disponível na página).

Propriedades de Row

Propriedades Descrição
alignment alinhamento horizontal dos filhos.
A propriedade MainAxisAlignment recebe um ENUM com os valores:

  • START(default) alinha à esquerda da Linha
  • END
  • CENTER
  • SPACE_BETWEEN
  • SPACE_AROUND
  • SPACE_EVENLY

auto_scroll booleano, auto_scroll=True para mover a posição de scroll para o final quando ocorre atualização dos filhos. Para que o método scroll_to() funcione é necessário ajustar auto_scroll=False.
controls uma lista de controles a serem exibidos na linha.
run_spacing espaçamento entre as várias linhas (runs) quando wrap=True. Default: 10.
Linhas adicionais aparecem quando os controles não cabem dentro de uma linha única.
scroll permite rolagem horizontal da linha para evitar overflow. A propriedade pode receber o ENUM ScrollMode com os valores:

  • None (default): não é rolagem e pode haver overflow.
  • AUTO: rolagem habilitada e a barra de rolagem só aparece quando ocorre o scroll.
  • ADAPTIVE: rolagem habilitada e a barra sempre visível quando aplicativo roda na web ou desktop.
  • ALWAYS: rolagem habilitada e a barra sempre visível.
  • HIDDEN: rolagem habilitada e a barra sempre oculta.
spacing espaçamento entre controles na linha. Default: 10 pixeis. O espaçamento só é aplicado quando alignment é start, end ou center.
on_scroll_interval limitação para o evento on_scroll em milisegundos. Default: 10.
tight booleano, espaço a ser ocupado horizontalmente. Default: tight = False, usar todo o espaço os controles.
vertical_alignment alinhamento vertical. A propriedade pode receber o ENUM CrossAxisAlignment os valores:

  • START (default)
  • CENTER
  • END
  • STRETCH
  • BASELINE
wrap booleano, se wrap=True os controles filhos serão colocados em linhas adicionais (chamadas runs), caso não caibam em uma única linha.

Métodos de Row

Método Descrição
scroll_to(offset, delta, key, duration, curve) move a posição de rolagem para o offset absoluto, para um salto (delta) ou para o controle com key especificada.
Detalhes são idênticos ao do método de Column.scroll_to().

Eventos de Row

Evento Dispara quando
on_scroll a posição de rolagem da linha é alterada por um usuário. Consulte Column.on_scroll() para detalhes e exemplos do evento.

Uso das propriedades de row

No código abaixo são criados 30 controles container numerados que são dispostos em uma linha. Dois controle de deslizar (slide) ajustam as propriedades row.width (o número de colunas em cada linha) e row.spacing (o espaçamento entre cada objeto. Quando o número de objetos em uma linha é maior que o espaço permite, novas linhas são inseridas (runs).

import flet as ft # era 52

def main(page: ft.Page):
    def items(count):
        items = []
        for i in range(1, count + 1):
            items.append(ft.Container(ft.Text(value=str(i), color="white", size=20), alignment=ft.alignment.center,
            width=40, height=40, bgcolor="#40A4D2", border_radius=ft.border_radius.all(10)))
        return items

    def muda_largura(e):
        linha.width = float(e.control.value)
        linha.update()

    def muda_separa(e):
        linha.spacing = int(e.control.value)
        linha.update()

    slid_separa = ft.Slider(min=0, max=50, divisions=50, value=0, label="{value}", on_change=muda_separa,)

    slid_largura = ft.Slider(min=0, max=page.window_width, divisions=20, value=page.window_width,
                             label="{value}", on_change=muda_largura,)

    linha = ft.Row(wrap=True, spacing=10, run_spacing=10, controls=items(30), width=page.window_width,)

    txt1 = ft.Text("O primeiro controle seleciona o número de colunas:")
    txt2 = ft.Text("O segundo controle seleciona espaçamento entre colunas:")
    page.add(ft.Column([txt1, slid_largura,]),ft.Column([txt2, slid_separa,]), linha,)

ft.app(target=main)

O código gera, em algum ajuste dos controles de deslizar, a janela abaixo.

Expandindo controles na linha

A expansão de controles na linha e na coluna são análogas. Veja Expandindo controles na linha e na coluna.

Flet: Column

Column é um controle que serve de container para outros controles e os exibe verticalmente. Ele possui propriedades eventos e métodos relativos ao layout e gerenciamento do rolamento da página (scroll) para evitar overflow (quando o conteúdo ocupa áerea maior que a disponível na página.

Propriedades de Column

Propriedades Descrição
alignment define como os controles filhos devem ser colocados verticalmente.
A propriedade MainAxisAlignment recebe um ENUM com os valores:

  • START(default) alinha à esquerda da Linha
  • END
  • CENTER
  • SPACE_BETWEEN
  • SPACE_AROUND
  • SPACE_EVENLY
auto_scroll auto_scroll=True para mover a posição de scroll o final quando ocorre atualização dos filhos. Para que o método scroll_to() funcione devemos fazer auto_scroll=False.
controls lista de controles a serem exibidos na coluna.
horizontal_alignment posicionamento horizontal dos controles filhos.
A propriedade recebe o ENUM CrossAxisAlignment com os valores:

  • START(default) alinha à esquerda da Linha
  • END
  • CENTER
  • STRETCH
  • BASELINE
on_scroll_interval limita o evento on_scroll (em milisegundos). Default: 10.
scroll habilita a rolagem vertical na coluna para evitar overflow de conteúdo.
A propriedade recebe um ENUM opcional ScrollMode com valores:

  • None (default): coluna não é rolável e pode haver overflow.
  • AUTO: rolagem habilitada e a barra de rolagem só aparece quando a rolagem é necessária.
  • ADAPTIVE: rolagem habilitada e a barra sempre visível em aplicativos na web ou desktop.
  • ALWAYS: rolagem habilitada e a barra sempre vivível.
  • HIDDEN: rolagem habilitada, a barra de rolagem está sempre oculta.
spacing espaçamento entre os controles da coluna. Default: 10 pixeis. O espaçamento só é aplicado quando alignment = start, end, center.
run_spacing espaçamento entre “runs” quando wrap=True. Default: 10.
tight espaçamento vertical. Default: False (alocar todo o espaço para filhos).
wrap se wrap=True a coluna distribuirá os controles filho em colunas adicionais (runs) se não couberem em uma coluna.

Métodos de Column

Método Descrição
scroll_to(
offset, delta,
key, duration,
curve)
move a posição de rolagem para o offset absoluto, para um salto (delta) ou para o controle com key especificada.
Por exemplo:

(1) products.scroll_to(offset=100, duration=1000)
(2) products.scroll_to(offset=-1, duration=1000)
(3) products.scroll_to(delta=50)
(4) products.scroll_to(key="20", duration=1000)

(1) offset é a posição do controle, um valor entre a extensão mínima e máxima do controle de scroll.
(2) offset negativo conta a partir do final do scroll. offset=-1 para posicionar no final.
(3) delta move o scroll relativo à posição atual. Rola para trás, se negativo.
(4) key move o scroll para a posição especificada com key.
A maioria dos controles do Flet tem a propriedade key (equivalente ao global key do Flutter. keydeve ser única para toda a page/view.
duration define a duração da animação de rolagem, em milisegundos. Default: 0 (sem animação).
curve configura a curva de animação. Default: ft.AnimationCurve.EASE.

Eventos de Column

Evento Dispara quando
on_scroll a posição de rolagem é alterada por um usuário.
O argumento do gerenciador de eventos é instância da ft.OnScrollEvent com as propriedades:

  • event_type (str), tipo do evento de rolagem:
    • start: início da rolagem;
    • update: controle de rolagem mudou de posição;
    • end: início da rolagem (parou de rolar);
    • user: usuário mudou a direção da rolagem;
    • over: controle ficou inalterado por estar no início ou final;
  • pixels(float): posição de rolagem atual, em pixeis lógicos.
  • min_scroll_extent (float): valor mínimo no intervalo permitido, em pixeis.
  • max_scroll_extent (float): valor máximo no intervalo permitido, em pixeis.
  • viewport_dimension (float): extensão da viewport.
  • scroll_delta (float): distância rolada, em pixeis. Definido apenas em eventos update.
  • direction (str) : direção da rolagem: idle, forward, reverse. Definido apenas em evento user.
  • overscroll (float): número de pixeis não rolados, por overflow. Definido apenas em eventos over.
  • velocity (float): velocidade na posição de ScrollPosition quando ocorreu overscroll. Definido apenas em eventos over.

Expandindo controles na linha e na coluna

Todos os controles possuem a propriedade expand que serve para expandi-lo para preencher os espaços disponíveis dentro de uma linha. Ela pode receber um booleano ou um inteiro. expand=True significa expandir o controle para preencher todo o espaço. expand=int introduz um “fator de expansão” especificando como dividir um espaço com outros controles expandidos na mesma linha, ou coluna. O código:

ft.Row([
    ft.TextField(hint_text="Esse será expandido", expand=True),
    ft.ElevatedButton(text="Esse não...")
])

cria uma linha contendo um TextField que ocupa o espaço disponível, e um ElevatedButton, sem expansão. A expansão é calculada em termos do tamanho de todos os controle na linha. É possível fornecer fatores que definem a proporção de expansão de cada controle. Por exemplo:

linha = ft.Row([
        ft.Container(expand=1, content=ft.Text("A")),
        ft.Container(expand=3, content=ft.Text("B")),
        ft.Container(expand=1, content=ft.Text("C"))
])

cria uma linha com 3 controles ocupando 1, 3, 1 partes em 5, como exibido na figura,

Percentualmente a largura resultante de um filho é calculada como largura = expand / soma( todas as expands) * 100%.

Da mesma forma um controle filho em uma coluna pode ser expandido para preencher o espaço vertical disponível. Por exemplo, o código abaixo cria uma coluna com um Container que ocupando todo o espaço disponível e um controle Text na parte inferior servindo como uma barra de status:

coluna = ft.Column([
         ft.Container(expand=True, content=ft.Text("Esse será expandido")),
         ft.Text("Um texto usado como label")
])

Assim como no controle das linhas, podemos usar fatores numéricos em expand=n.

col = ft.Column([
      ft.Container(expand=1, content=ft.Text("Acima")),
      ft.Container(expand=3, content=ft.Text("No centro")),
      ft.Container(expand=1, content=ft.Text("Abaixo"))
])


Isso cria uma coluna com 3 containeres com alturas de 20% (1/5), 60% (3/5)e 20% (1/5) respectivamente.

Percentualmente a altura resultante de um filho é calculada como altura = expand / soma( todas as expands) * 100%.

Bibiografia

Flet: View e Container


Layout do Flet: View e Container

Flet: View

A palavra container do inglês é traduzida como contêiner (pt-br), plural contêineres. Para não criar confusão com a palavra técnica nós o chamaremos aqui por container, containers.

Um aplicativo do Flet abre sempre uma page que serve de container para o objeto View. Uma View é criado automaticamente quando uma nova sessão é iniciada. Ela é basicamente uma coluna (column) básica, que abriga todos os demais controles que serão inseridos na página. Dessa forma ele tem comportamento semelhante ao de uma column, e as mesmas propriedades. Uma descrição resumida será apresentada aqui. Para maiores detalhes consulte a descrição de column.

O objeto View é o componente visual de uma página Flet, responsável por renderizar os elementos da UI e gerenciar seu estado. Ele pode abrigar outros objetos como botões, campos de texto, imagens, etc, e organizá-los em uma estrutura hierárquica. Esses elementos são então renderizados na tela. O objeto View também possui métodos para lidar com eventos do usuário, como cliques em botões ou textos digitados nas caixas de texto.

Por exemplo, o código:

page.controls.append(ft.Text("Um texto na página!"))
page.update()
# ou, o que é equivalente
page.add(ft.Text("Um texto na página!"))

insere o texto na View que está diretamente criada sobre page. View possui as seguintes propriedades e métodos.

View: Propriedades

Propriedade Descrição
appbar recebe um controle AppBar para exibir na parte superior da página.
auto_scroll Booleano. True para que a barra de rolagem se mova para o final quando os filhos são atualizados. Para que scroll_to() funcione deve ser atribuído auto_scroll=False.
bgcolor Cor de fundo da página.
controls Lista de controles a serem inseridos na página. O último controle da lista pode se removido com page.controls.pop(); page.update().
fullscreen_dialog Booleano. True se a página atual é um diálogo em tela cheia.
route Rota da visualização (não usada atualmente). Pode ser usada para atualizar page.route em caso de nova visualização.
floating_action_button Recebe um controle FloatingActionButton a ser exibido no alto da página.
horizontal_alignment Alinhamento horizontal dos filhos. Default: horizontal_alignment=CrossAxisAlignment.START.
on_scroll_interval Definição do intervalo de tempo para o evento on_scrollo, em milisegundos. Default: 10.
padding Espaço entre o conteúdo do objeto e suas bordas, em pixeis. Default: 10.
scroll Habilita rolagem (scroll) vertical para a página, evitando overflow. O valor da propriedade está em um ENUM ScrollMode com as possibilidades:

  • None (padrão): nenhum scroll. Pode haver overflow.
  • AUTO: scroll habilitado, a barra só aparece quando a rolagem é necessária.
  • ADAPTIVE: scroll habilitado, a barra de rolagem visível quando aplicativo é web ou desktop.
  • ALWAYS: scroll habilitado, a barra sempre exibida.
  • HIDDEN: scroll habilitado, barra de rolagem invisível.
spacing Espaço vertical entre os controles da página, em pixeis. Default: 10. Só aplicado quando alignment = start, end, center.
vertical_alignment Alinhamento vertical dos filhos. A propriedade está em um ENUM MainAxisAlignmente com as possibilidades:

  • START (padrão)
  • END
  • CENTER
  • SPACE_BETWEEN
  • SPACE_AROUND
  • SPACE_EVENLY

Exemplos:

page.vertical_alignment = ft.MainAxisAlignment.CENTER
page.horizontal_alignment = ft.CrossAxisAlignment.CENTER
scroll_to(offset, delta, key, duration, curve) Move a barra de scroll para uma posição absoluta ou salto relativo para chave especificada.

View: Evento

Evento Descrição
on_scroll Dispara quando a posição da barra de rolagem é alterada pelo usuário.

O controle View é útil em situações que se apresenta mais em uma visualização na mesma página e será visto mais tarde com maiores detalhes.

Flet: Container

Um objeto Container é basicamente um auxiliar de layout, um controle onde se pode inserir outros controles, permitindo a decoração de cor de fundo, borda, margem, alinhamento e preenchimento. Ele também pode responder a alguns eventos.

Como exemplo, o código abaixo:

import flet as ft

def main(page: ft.Page):
    page.title = "Contêineres com cores de fundo"

    def cor(e):
        c4 = ft.Container(content=ft.Text("Outro conteiner azul!"), bgcolor=ft.colors.BLUE, padding=5)
        page.add(c4)

    c1 = ft.Container(content=ft.ElevatedButton("Um botão \"Elevated\""),
                      bgcolor=ft.colors.YELLOW, padding=5)

    c2 = ft.Container(content=ft.ElevatedButton("Elevated Button com opacidade=0.5",
                      opacity=0.5), bgcolor=ft.colors.YELLOW, padding=5)

    c3 = ft.Container(content=ft.Text("Coloca outra área azul"),
                      bgcolor=ft.colors.YELLOW, padding=5, on_click=cor)
    page.add(c1, c2, c3)

ft.app(target=main)

gera a janela na figura 1, após 1 clique no botão c3.

Figura 1: Novo container é adicionado ao clicar em “Coloca outra área azul”.

O container c3 reage ao evento clique, adicionando um (ou mais) botão azul à janela.

Container: Propriedades

Figura 2

A figura 2 mostra o esquema de espaçamentos entre controles: a largura e altura (width, height) do container, a margem (margin) entre a caixa de decoração e o container, a borda (border) da caixa e o espaçamento interno (padding) entre o controle e a caixa.

Propriedade Descrição
alignment Alinhamento do controle filho dentro do Container para exibir na parte superior da página. Alignment é uma instância do objeto alignment.Alignment com propriedades x e y que representam a distância do centro de um retângulo.

  • x=0, y=0: o centro do retângulo,
  • x=-1, y=-1: parte superior esquerda do retângulo,
  • x=1.0, y=1.0: parte inferior direita do retângulo.
Figura 3

Constantes de alinhamento pré-definidas no módulo flet.alignment são: top_left, top_center, top_right, center_left, center, center_right, bottom_left, bottom_center, bottom_right. Por exemplo, mostrado na figura 4:

  • container_1.alignment = alignment.center
  • container_2.alignment = alignment.top_left
  • container_3.alignment = alignment.Alignment(-0.5, -0.5)

Figura 4
animate Ativa a animação predefinida do container, alterando valores de suas propriedades de modo gradual. O valor pode ser um dos seguintes tipos:

  • bool: True para ativar a animação do container com curva linear de duração de 1000 milisegundos.
  • int: ajusta tempo em milisegundos, com curva linear.
  • animation: Animation(duration: int, curve: str): ativa animação do container com duração e curva de transição especificadas.

Por exemplo:

import flet as ft

def main(page: ft.Page):

    c = ft.Container(width=200, height=200, bgcolor="red", animate=ft.animation.Animation(1000, "bounceOut"))

    def animar_container(e):
        c.width = 100 if c.width == 200 else 200
        c.height = 100 if c.height == 200 else 200
        c.bgcolor = "blue" if c.bgcolor == "red" else "red"
        c.update()

    page.add(c, ft.ElevatedButton("Animate container", on_click=animar_container))

ft.app(target=main)

O código resulta na animação mostrada abaixo, na figura 5:

Figura 5: animação

bgcolor Cor de fundo do container.
blend_mode modo de mistura (blend) de cores ou gradientes no fundo container.
blur Aplica o efeito de desfoque (blur) gaussiano sobre o container.

O valor desta propriedade pode ser um dos seguintes:

  • um número: especifica o mesmo valor para sigmas horizontais e verticais, por exemplo 10.
  • uma tupla: especifica valores separados para sigmas horizontais e verticais, por exemplo (10, 1).
  • uma instância de ft.Blur: especifica valores para sigmas horizontais e verticais, bem como tile_mode para o filtro. tile_mode é o valor de ft.BlurTileMode tendo como default ft.BlurTileMode.CLAMP.
border Borda desenhada em torno do controle e acima da cor de fundo. Bordas são descritas por uma instância de border.BorderSide, com as propriedades: width (número) e color (string). O valor da propriedade border é instância de border.Borderclasse, descrevendo os 4 lados do retângulo. Métodos auxiliares estão disponíveis para definir estilos de borda:

  • border.all(width, color)
  • border.symmetric(vertical: BorderSide, horizontal: BorderSide)
  • border.only(left: BorderSide, top: BorderSide, right: BorderSide, bottom: BorderSide).

Por exemplo:

container_1.border = ft.border.all(10, ft.colors.PINK_600)
container_1.border = ft.border.only(bottom=ft.border.BorderSide(1, "black"))
border_radius Permite especificar (opcional) o raio de arredondamento das bordas. O raio é instância de border_radius.BorderRadius com as propriedades: top_left, top_right, bottom_left, bottom_right. Esses valores podem ser passados no construtor da instância, ou por meio de métodos auxiliares:

  • border_radius.all(value)
  • border_radius.horizontal(left: float = 0, right: float = 0)
  • border_radius.vertical(top: float = 0, bottom: float = 0)
  • border_radius.only(top_left, top_right, bottom_left, bottom_right)

Por exemplo: container_1.border_radius= ft.border_radius.all(30), fará todas as bordas do container_1 igual 1 30.

clip_behavior Opção para cortar (ou não) o conteúdo do objeto. A propriedade ClipBehavior é um ENUM com valores suportados:

  • NONE
  • ANTI_ALIAS
  • ANTI_ALIAS_WITH_SAVE_LAYER
  • HARD_EDGE

Se border_radius=None o default é ClipBehavior.ANTI_ALIAS. Caso contrário o default é ClipBehavior.HARD_EDGE.

content Define um controle filho desse container.
gradient O gradiente na cor de fundo. O valor deve ser uma instância de uma das classes: LinearGradient, RadialGradient e SweepGradient.


Uma descrição mais detalhada está em Detalhes sobre o gradiente de cores.

image_fit Descrita junto com o objeto image.
image_opacity Define a opacidade da imagem ao mesclar com um plano de fundo: valor entre 0.0 e 1.0.
image_repeat Descrita junto com o objeto image.
image_src Define imagem do plano de fundo.
image_src_base64 Define imagem codificada como string Base-64 como plano de fundo do container.
ink True para efeito de ondulação quando o usuário clica no container. Default: False.
margin Espaço vazio que envolve o controle. margin é uma instância de margin.Margin, definindo a propriedade para os 4 lados do retângulo: left, top, right e bottom. As propriedades podem ser dadas no construtor ou por meio de métodos auxiliares:

  • margin.all(value)
  • margin.symmetric(vertical, horizontal)
  • margin.only(left, top, right, bottom)

Por exemplo:

container_1.margin = margin.all(10)
container_2.margin = 20 # same as margin.all(20)
container_3.margin = margin.symmetric(vertical=10)
container_3.margin = margin.only(left=10)
padding Espaço vazio de decoração entre borda do objeto e seu container. Padding é instância da padding.Padding com propriedades definidas como padding para todos os lados do retângulo: left, top, right e bottom. As propriedades podem ser dadas no construtor ou por meio de métodos auxiliares:

  • padding.all(value: float)
  • padding.symmetric(vertical, horizontal)
  • padding.only(left, top, right, bottom)

Por exemplo:

container_1.padding = ft.padding.all(10)
container_2.padding = 20 # same as ft.padding.all(20)
container_3.padding = ft.padding.symmetric(horizontal=10)
container_4.padding=padding.only(left=10)
shadow Efeito de sombras projetadas pelo container. O valor dessa propriedade é uma instância ou uma lista de ft.BoxShadow, com as seguintes propriedades:

  • spread_radius: espalhamento, quanto a caixa será aumentada antes do desfoque. Default: 0.0.
  • blur_radius: desvio padrão da gaussiano de convolução da forma. Default: 0.0.
  • color: Cor da sombra.
  • offset: Uma instância de ft.Offsetclasse, deslocamentos da sombra, relativos à posição do elemento projetado. Os deslocamentos positivos em x e y deslocam a sombra para a direita e para baixo. Deslocamentos negativos deslocam para a esquerda e para cima. Os deslocamentos são relativos à posição do elemento que o está projetando. Default: ft.Offset(0,0).
  • blur_style: tipo de estilo, ft.BlurStyleque a ser usado na sombra. Default: ft.BlurStyle.NORMAL.

Exemplo:

ft.Container(
    shadow=ft.BoxShadow(
        spread_radius=1,
        blur_radius=15,
        color=ft.colors.BLUE_GREY_300,
        offset=ft.Offset(0, 0),
        blur_style=ft.ShadowBlurStyle.OUTER,
    )
)
shape A forma do conteiner. O valor é ENUM BoxShape: RECTANGLE (padrão), CIRCLE
theme_mode O ajuste do theme_mode redefine o tema usado no container e todos os objetos dentro dele. Se não for definido o tema em theme é válido para o container e seus filhos.
theme Ajuste o tema global e dos filhos na árvore de controle.

Segue um exemplo de uso:

import flet as ft

def main(page: ft.Page):
    page.theme = ft.Theme(color_scheme_seed=ft.colors.RED)

    b1 = ft.ElevatedButton("Botão com tema da página")
    b2 = ft.ElevatedButton("Botão com tema herdado")
    b3= ft.ElevatedButton("Botão com tema dark")

    c1 = ft.Container(
        b1,
        bgcolor=ft.colors.SURFACE_TINT,
        padding=20,
        width=300
    )
    c2 = ft.Container(
        b2,
        theme=ft.Theme(
            color_scheme=ft.ColorScheme(primary=ft.colors.PINK)
        ),
        bgcolor=ft.colors.SURFACE_VARIANT,
        padding=20,
        width=300
    )
    c3 = ft.Container(
        b3,
        theme=ft.Theme(
            color_scheme_seed=ft.colors.INDIGO
        ),
        theme_mode=ft.ThemeMode.DARK,
        bgcolor=ft.colors.SURFACE_VARIANT,
        padding=20,
        width=300
    )

    page.add(c1, c2, c3)

ft.app(main)
Figura 6: Temas

O tema principal da página é definido em page.theme, usando um seed vermelho. Os botões b1 e b2 simnplesmente herdam o tema da página. O botão b3 está no container definido com theme_mode=ft.ThemeMode.DARK, exibindo o tema escuro. O código gera a janela mostrada na figura 6.

Vale lembrar que c1 = ft.Container(b1,...) é equivalente à c1 = ft.Container(content = b1,...) sendo que o content só pode ser omitido se o conteúdo for inserido como primeiro parâmetro.urlDefine a URL a ser abertta quando o container é clicado, disparando o evento on_click.url_target

Define onde abrir URL no modo web:

  • _blank (default): em nova janela ou aba,
  • _self: na mesma janela ou aba aberta.

Container: Eventos

Evento Dispara quando
on_click o usuário clica no container.

class ft.ContainerTapEvent():
    local_x: float
    local_y: float
    global_x: float
    global_y: float

Obs.: O objeto de evento e é uma instância de ContainerTapEvent, exceto se a propriedade ink = True. Nesse caso e será apenas ControlEvent com data vazio.

Um exemplo simples de uso:

import flet as ft

def main(page: ft.Page):

    t = ft.Text()

    def clicou_aqui(e: ft.ContainerTapEvent):
        t.value = (
            f"local_x: {e.local_x}\nlocal_y: {e.local_y}"
            f"\nglobal_x: {e.global_x}\nglobal_y: {e.global_y}"
        )
        t.update()

    c1 = ft.Container(ft.Text("Clique dentro\ndo container"),
                      alignment=ft.alignment.center, bgcolor=ft.colors.TEAL_300, 
                      width=200, height=200, border_radius=10,  on_click=clicou_aqui)
    col = ft.Column([c1, t], horizontal_alignment=ft.CrossAxisAlignment.CENTER)
    page.add(col)

ft.app(target=main)
Figura 8: posição do click.

As propriedades e.local_x e e.local_y se referem à posição dentro do container c1, enquanto e.global_x e e.global_y se referem à posição global, dentro da página.

on_hover o cursor do mouse entra ou abandona a área do container. A propriedade data do evento contém um string (não um booleano) e.data = "true" quando o cursor entra na área, e e.data = "false" quando ele sai.

Um exemplo de um container que altera sua cor de fundo quando o mouse corre sobre ele:

import flet as ft

def main(page: ft.Page):
    def on_hover(e):
        e.control.bgcolor = "blue" if e.data == "true" else "red"
        e.control.update()

    c1 = ft.Container(width=100, height=100, bgcolor="red",
                      ink=False, on_hover=on_hover)
    page.add(c1)

ft.app(target=main)
on_long_press quando o container recebe um click longo (pressionado por um certo tempo).

Detalhes sobre o gradiente de cores

O gradiente na cor de fundo admite como valor uma instância de uma das classes: LinearGradient, RadialGradient e SweepGradient.

Um exmplo de uso está no código abaixo:

import flet as ft
import math

def main(page: ft.Page):
    c1 = ft.Container(
        gradient=ft.LinearGradient(
        begin=ft.alignment.top_center,
        end=ft.alignment.bottom_center,
        colors=[ft.colors.AMBER_900, ft.colors.BLUE],),
        width=150, height=150, border_radius=5,)

    c2 = ft.Container(
         gradient=ft.RadialGradient(colors=[ft.colors.GREY, ft.colors.CYAN_900],),
         width=150, height=150, border_radius=5,)

    c3 = ft.Container(
         gradient=ft.SweepGradient(center=ft.alignment.center,
         start_angle=0.0, end_angle=math.pi * 2,
         colors=[ft.colors.DEEP_PURPLE_800, ft.colors.DEEP_ORANGE_400],),
         width=150, height=150, border_radius=5,)
    
    page.add(ft.Row([c1, c2, c3]))

ft.app(target=main)

O código acima gera as imagens na figura 9:

Figura 9: LinearGradient, a segundo com RadialGradient e a última com SweepGradient

A primeira imagem é gerada com LinearGradient, a segunda com RadialGradient e a última com SweepGradient.

São propriedades da classe LinearGradient:

begin instância de Alignment. Posicionamento inicial (0.0) do gradiente.
end instância de Alignment. Posicionamento final (1.0) do gradiente.
colors cores assumidas pelo gradiente a cada parada. Essa lista deve ter o mesmo tamanho que stops se a lista for não nula. A lista deve ter pelo menos duas cores.
stops lista de valores de 0.0 a 1.0 marcando posições ao longo do gradiente. Se não nula essa lista deve ter o mesmo comprimento que colors. Se o primeiro valor não for 0.0 fica implícita uma parada em 0,0 com cor igual à primeira cor em colors. Se o último valor não for 1.0 fica implícita uma parada em 1.0 e uma cor igual à última cor em colors.
tile_mode como o gradiente deve preencher (tile) a região antes de begin depois de end. O valor é um ENUM GradientTileMode com valores: CLAMP (padrão), DECAL, MIRROR, REPEATED.
rotation rotação do gradiente em radianos, em torno do ponto central de sua caixa container.

Mais Informações:

Gradiente linear na documentação do Flutter.
Unidade de medida de radianos na Wikipedia.

São propriedades da classe RadialGradient:

colors, stops, tile_mode, rotation propriedades idênticas às de LinearGradient.
center instância de Alignment. O centro do gradiente em relação ao objeto que recebe o gradiente. Por exemplo, alinhamento de (0.0, 0.0) coloca o centro do gradiente radial no centro da caixa.
radius raio do gradiente, dado como fração do lado mais curto da caixa. Supondo uma caixa com largura = 100 e altura = 200 pixeis, um raio de 1 no gradiente radial colocará uma parada de 1,0 afastado 100,0 pixeis do centro.
focal ponto focal do gradiente. Se especificado, o gradiente parecerá focado ao longo do vetor do centro até esse ponto focal.
focal_radius raio do ponto focal do gradiente, dado como fração do lado mais curto da caixa. Ex.: um gradiente radial desenhado sobre uma caixa com largura = 100,0 e altura = 200,0 (pixeis), um raio de 1,0 colocará uma parada de 1,0 a 100,0 pixels do ponto focal.

São propriedades da classe SweepGradient:

colors, stops, tile_mode, rotation propriedades idênticas às de LinearGradient.
center centro do gradiente em relação ao objeto que recebe o gradiente. Por exemplo, alinhamento de (0.0, 0.0) coloca o centro do gradiente no centro da caixa.
start_angle ângulo em radianos onde será colocada a parada 0.0 do gradiente. Default: 0.0.
end_angle ângulo em radianos onde será colocada a parada 1.0 do gradiente. Default: math.pi * 2.

Graus e radianos

Figura 10: graus e radianos.

A maiora das medidas angulares na programação do flet (e do python em geral) é dada em radianos. Segue uma breve imagem explicativa do uso de radianos.

Bibiografia


Controles do Flet: Row e Column

Flet: Botões

Widgets, propriedades e eventos

Estritamente dizendo deveríamos iniciar nosso estudo do Flet considerando os objetos mais básicos, no sentido de serem containeres de outros objetos. No entanto já vimos vários casos de pequenos aplicativos que usam botões. É difícil sequer exibir exemplos de código de GUI sem botões. Por isso vamos descrever aqui o uso dos botões, deixando para depois uma descrição dos controles de layout.

Alguns controles tem a função principal de obter informações do usuário, como botões, menus dropdown ou caixas de texto, para inserção de dados. Outros servem para a exibição de informações calculadas pelo código, mostrando gráficos, figuras ou textos. As caixas de textos podem ser usadas em ambos os casos.

Buttons (botões)

Os seguintes tipos de botões estão disponíveis (e ilustrados na figura 1), contendo as propriedades, métodos e respondendo a eventos:

Botões ElevatedButton FilledButton FilledTonalButton FloatingActionButton IconButton OutlinedButtonPopupMenuButton TextButton
Propriedades autofocus, bgcolor,data, color, content, elevation, icon, icon_color, style, text, tooltip, url, url_target
Eventos on_blur, on_click, on_focus, on_hover, on_long_press
Método focus()
Figura 1

Vamos usar alguns exemplos para ilustrar as propriedades e métodos desses objetos.

import flet as ft

def main(page: ft.Page):
    def mudar(e):
        bt2.disabled = not bt2.disabled
        bt2.text = "Desabilitado" if bt2.disabled else "Habilitado" 
        page.update()

    bt1 = ft.FilledButton(text="FilledButton", on_click=mudar, width=200)
    bt2 = ft.ElevatedButton("Habilitado", disabled=False, width=200)
    page.add(bt1, bt2)

ft.app(target=main)

Nesse caso um FilledButton aciona a função mudar que alterna a propriedade disabled do botão elevado. Observe que um botão com disabled=True não reage ao clique, nem à nenhum outro evento. O operador ternário foi usado: valor_se_true if condicao else valor_se_false.

Dois novos eventos são mostrados a seguir: on_hover, que é disparado quando o cursor se move sobre o botão (sem necessidade de clicar) e on_long_press, disparado com um clique longo. Um ícone é inserido nos botões ElevatedButton, cujas cores são alteradas pelos eventos descritos.

import flet as ft

def main(page: ft.Page):

    def azular(e):
        bt2.icon_color="blue"
        page.update()

    def vermelho(e):
        bt2.icon_color="red"
        page.update()

    bt1 = ft.ElevatedButton("Tornar azul", icon="chair_outlined", on_hover= azular, width=250)
    bt2 = ft.ElevatedButton("Tornar Vermelho", icon="park_rounded", on_long_press=vermelho, icon_color= "green", width=250)
    page.add(bt1, bt2)

ft.app(target=main)

Os botões assumem os estados mostrados na figura 2.

Figura 2: Execução do código acima.

 

Alguns Ícones pré-definidos do Flet

Uma grande quantidade de ícones está disponível e pode ser pesquisada em Flet.dev: Icons Browser.

Botões possuem a propriedade data que pode armazenar um objeto a ser passado para as funções acionadas por eventos. As propriedades dos widgets funcionam como variáveis globais. No exemplo abaixo temos uma caixa de texto, que exibe a propriedade data. O botão elevado também tem uma propridade data que não é exibida mas serve para armazenar quantas vezes o botão foi clicado. Ele serve de container para um Row contendo 3 ícones (ft.Icon(ft.icons.NOME_DO_ICONE)).

import flet as ft

def main(page: ft.Page):
    def bt_clicou(e):
        bt.data += 1
        txt.value = f"O botão foi clicado {bt.data} {'vezes' if bt.data >1 else 'vêz'}"
        page.update()

    txt = ft.Text("O botão não foi clicado", size=25, color="navyblue", italic=True)
    bt = ft.ElevatedButton("Clica!",
            content=ft.Row(
                [
                    ft.Icon(ft.icons.CHAIR, color="red"),
                    ft.Icon(ft.icons.COTTAGE, color="green"),
                    ft.Icon(ft.icons.AGRICULTURE, color="blue"),
                ],  alignment=ft.MainAxisAlignment.SPACE_AROUND,),
                    on_click=bt_clicou, data=0, width=150,
         )
    page.add(txt, bt)

ft.app(target=main)

Se o nome do ícone não for fornecido como primeiro parâmetro o nome do parâmetro deve ser nomeado: ft.Icon(color="red", name=ft.icons.CHAIR). E execução do código resulta nas janelas exibidas na figura 3.

Figura 3

As caixas de texto podem receber formatações diversas como size (tamanho da fonte), color (cor da fonte) bgcolor (cor do fundo), italic (itálico), weight (peso da fonte). A propriedade selectable informa se o texto exibido pode ser selecionado, e estilos diversos podem ser atribuídos em style. A página recebe a propriedade page.scroll = "adaptive" para que possa apresentar uma barra de scroll.

import flet as ft

def main(page: ft.Page):
    page.scroll = "adaptive"

    t1 = ft.Text("Tamanho 12 (Size 12)", size=12)
    t2 = ft.Text("Tamanho 20, Italic", size=32, color="red", italic=True)
    t3 = ft.Text("Tamanho 30, peso 100", size=30, color=ft.colors.WHITE,
                  bgcolor=ft.colors.BLUE_600, weight=ft.FontWeight.W_100)
    t4 = ft.Text(
            "Size 40, Normal",
            size=40,
            color=ft.colors.WHITE,
            bgcolor=ft.colors.ORANGE_800,
            weight=ft.FontWeight.NORMAL
        )
    t5 = ft.Text(
            "Size 30, Bold, Italic",
            size=30,
            color=ft.colors.WHITE,
            bgcolor=ft.colors.GREEN_700,
            weight=ft.FontWeight.BOLD,
            italic=True
        )
    t6 = ft.Text("Size 20, w900, selecionável", size=20, weight=ft.FontWeight.W_900, selectable=True)

    page.add(t1, t2, t3, t4, t5, t6) 

ft.app(target=main)

O resultado é mostrado na figura 4. A janela foi dimensionada para ser menor que o texto existente, mostrando a barra de scroll.

Figura 4

O número máximo de linhas exibidas, max_lines, ou largura e altura do texto, width e height são úteis quando se apresenta texto logo em uma janela.

import flet as ft

def main(page: ft.Page):
    page.scroll = "adaptive"
    
    texto1 = (
        "René Descartes (La Haye en Touraine, 31 de março de 1596, Estocolmo, 11 de"
        "fevereiro de 1650) foi um filósofo, físico e matemático francês. Durante a"
        "Idade Moderna, também era conhecido por seu nome latino Renatus Cartesius."
    )
    texto2 = (
        "Descartes, por vezes chamado de o fundador da filosofia moderna e o pai da"
        "matemática moderna, é considerado um dos pensadores mais importantes e"
        "influentes da História do Pensamento Ocidental. Inspirou contemporâneos e"
        "várias gerações de filósofos posteriores; boa parte da filosofia escrita "
        "a partir de então foi uma reação às suas obras ou a autores supostamente"
        "influenciados por ele. Muitos especialistas afirmam que, a partir de"
        "Descartes, inaugurou-se o racionalismo da Idade Moderna."
    )

    t7 = ft.Text("Limita texto longo a 1 linha, com elipses", style=ft.TextThemeStyle.HEADLINE_SMALL)
    t8 = ft.Text(texto1, max_lines=1, overflow="ellipsis")
    t9 = ft.Text("Limita texto longo a 2 linhas", style=ft.TextThemeStyle.HEADLINE_SMALL)
    t10 = ft.Text(texto2, max_lines=2)
    t11 = ft.Text("Limita largura e altura do texto longo", style=ft.TextThemeStyle.HEADLINE_SMALL)
    t12 = ft.Text(texto2, width=700, height=100)

    page.add(t7, t8, t9, t10, t11, t12)

ft.app(target=main)
Figura 5

Resultando na janela mostrada na figura 5. O parâmetro overflow="ellipsis" mostra uma elipses onde on texto foi quebrado. As definições de texto1 e texto2 correspondem a uma das formas de declarar strings longas no python.

Existem estilos pré-definidos. código abaixo usamos o texto do widget igual ao nome do estilo, para facilitar a visualização: style=ft.TextThemeStyle.NOME_DO_ESTILO. Apenas as definições estão mostradas e o resultado está na figura 6.

    page.add(
        ft.Text("DISPLAY_LARGE",   style=ft.TextThemeStyle.DISPLAY_LARGE),
        ft.Text("DISPLAY_MEDIUM",  style=ft.TextThemeStyle.DISPLAY_MEDIUM),
        ft.Text("DISPLAY_SMALL",   style=ft.TextThemeStyle.DISPLAY_SMALL),
        ft.Text("HEADLINE_LARGE",  style=ft.TextThemeStyle.HEADLINE_LARGE),
        ft.Text("HEADLINE_MEDIUM", style=ft.TextThemeStyle.HEADLINE_MEDIUM),
        ft.Text("HEADLINE_MEDIUM", style=ft.TextThemeStyle.HEADLINE_MEDIUM),
        ft.Text("TITLE_LARGE",     style=ft.TextThemeStyle.TITLE_LARGE),
        ft.Text("TITLE_MEDIUM",    style=ft.TextThemeStyle.TITLE_MEDIUM),
        ft.Text("TITLE_SMALL",     style=ft.TextThemeStyle.TITLE_SMALL),
        ft.Text("LABEL_LARGE",     style=ft.TextThemeStyle.LABEL_LARGE),
        ft.Text("LABEL_MEDIUM",    style=ft.TextThemeStyle.LABEL_MEDIUM),
        ft.Text("LABEL_SMALL",     style=ft.TextThemeStyle.LABEL_SMALL),
        ft.Text("BODY_LARGE",      style=ft.TextThemeStyle.BODY_LARGE),
        ft.Text("BODY_MEDIUM",     style=ft.TextThemeStyle.BODY_MEDIUM),
        ft.Text("BODY_SMALL",      style=ft.TextThemeStyle.BODY_SMALL)
    )
Figura 6

Propriedades dos controles pode ser alterados dinamicamente por meio de inputs recebidos do usuário (ou outra origem qualquer). No próximo exemplo o tamanho da fonte é controlado por um flet.Slider.

import flet as ft
def main(page: ft.Page):
    def font_size(e):
        t.size = int(sl.value)
        t.value = f"Texto escrito com fonte Bookerly, size={t.size}"
        t.update()

    t = ft.Text("Texto escrito com fonte Bookerly, size=10", size=10, font_family="Bookerly")
    sl = ft.Slider(min=0, max=100, divisions=10, value=10, label="{value}", width=500, on_change=font_size)

    page.add(t, sl)

ft.app(target=main)
Figura 7

O app gerado está na figura 7. Observe que a propriedade do Slider.label="value" exibe acima do cursor (como um tooltip) o valor do controle. O tamanho da fonte é ajustado de acordo com esse valor.

Se a fonte está instalada localmente basta usar font_family="Nome_da_Fonte". Para fontes remotas podemos definir uma ou várias fontes a serem usadas. page.fonts recebe um dicionário com nomes e locais das fontes.

    page.fonts = {
        "Kanit": "https://raw.githubusercontent.com/google/fonts/master/ofl/kanit/Kanit-Bold.ttf",
        "Open Sans": "fonts/OpenSans-Regular.ttf",
    }
    page.theme = Theme(font_family="Kanit")
    page.add(
        ft.Text("Esse texto é renderizado com fonte Kanit"),
        ft.Text("Esse texto é renderizado com fonte 'Open Sans'", font_family="Open Sans"),

Ícones

O objeto fleet.Icon pode ser inserido em vários conteineres. Ele possui as propriedades (entre outras) color, name, size e tooltip. O tamanho default é size=24 enquanto tooltip é o texto exibido quando o cursor está sobre o ícone.
O código ilustra esse uso:

import flet as ft

def main(page: ft.Page):
    ic1 = ft.Icon(name=ft.icons.ADD_HOME_ROUNDED, color=ft.colors.AMBER_900)
    ic2 = ft.Icon(name=ft.icons.ZOOM_IN, color=ft.colors.BLUE_ACCENT_700, size=30)
    ic3 = ft.Icon(name=ft.icons.AIR_SHARP, color="blue", size=50)
    ic4 = ft.Icon(name="child_care", color="#ffc1c1", size=70, tooltip="ajustes")
    page.add(ft.Row([ic1, ic2, ic3, ic4, ft.Image(src=f"img/Search.png")]))

ft.app(target=main)
Figura 8: Resultado do código de exibição de ícones.

O nome do ícone pode ser dado como name=ft.icons.ZOOM_IN ou uma string name="child_care", onde o nome pode ser pesquisado no Icons Browser. Note que ft.icons contém ENUMS predefinidos e name=ft.icons.AIR_SHARP é o mesmo que name="air_sharp".

Ícones personalizados podem ser inseridos como imagens, como em flet.Image(src=f"img/Search.png"), onde o caminho pode ser absoluto ou relativo, em referência ao diretório onde está o aplicativo. Isso significa que a imagem da lupa exibida na figura está armazenada em pasta_do_aplicativo/img/Search.png.

Bibiografia


Flet: Objeto Page

Python com Flet: Widgets

Widgets, propriedades e eventos

Vimos no primeiro artigo dessa série que um aplicativo Python com Flet consiste em código Python para a realização da lógica do aplicativo, usando o Flet como camada de exibição. Mais tarde nos preocuparemos em fazer uma separação explícita das camadas. Por enquanto basta notar que o Flet cria uma árvore de widgets cujas propriedades são controladas pelo código. Widgets também podem estar associados à ações ligadas a funções. Portanto, para construir aplicativos com Flet, precisamos conhecer esses widgets, suas propriedades e eventos que respondem.

Alguns controles tem a função principal de obter informações do usuário, como botões, menus dropdown ou caixas de texto, para inserção de dados. Outros servem para a exibição de informações calculadas pelo código, mostrando gráficos, figuras ou textos. As caixas de textos podem ser usadas em ambos os casos.

Figura 10: Estrutura de árvore de um aplicativo Flet

A interface do Flet é montada como uma composição de controles, arranjados em uma hierarquia sob forma de árvore que se inicia com uma Page. Dentro dela são dispostos os demais controles, sendo que alguns deles são também conteineres, abrigando outros controles. Todos os controles, portanto, possuem um pai, exceto a Page. Controles como Column, Row e Dropdown podem conter controles filhos, como mostrado na figura 10.

Categorias de Controles

🔘 Controles Categoria Itens
🔘 Layout diagramação 16 itens
🔘 Navigation navegação 3 itens
🔘 Information Displays exibição de informação 8 itens
🔘 Buttons botões 8 itens
🔘 Input and Selections entrada e seleções 6 itens
🔘 Dialogs, Alerts, Panels dialogo e paineis 4 itens
🔘 Charts gráficos 5 itens
🔘 Animations animações 1 item
🔘 Utility utilidades 13 itens

Propriedades comuns a vários controles

Algumas propriedades são comuns a todos (ou quase todos) os controles. Vamos primeiro listá-las e depois apresentar alguns exemplos de uso. As propriedades marcadas com o sinal ≜ só são válidas quando estão dentro de uma controle Stack, que será descrito após a tabela.

bottom Distância entre a borda inferior do filho e a borda inferior do Stack.
data Um campo auxiliar de dados arbitrários que podem ser armazenados junto a um controle.
disabled Desabilitação do controle. Por padrão disabled = False. Um controle desabilitado fica sombreado e não reage a nenhum evento. Todos os filhos de um controle desabilitado ficam desabilitados.
expand Um controle dentro de uma coluna ou linha pode ser expandido para preencher o espaço disponível. expand=valor, onde valor pode ser booleano ou inteiro, um fator de expansão, usado para dividir o espaço entre vários controles.
hight Altura do controle, em pixeis.
left Distância da borda esquerda do filho à borda esquerda do Stack.
right Distância da borda direita do filho à borda direita do Stack.
top Distância da borda superior do filho ao topo do Stack.
visible Visibilidade do controle e seus filhos. vivible = True por padrão. Controles invisíveis não recebem foco, não podem ser selecionados nem respondem a eventos.
widht Largura de controle em pixeis.

Um Stack é um controle usado para posicionar controles em cima de outros (empilhados). Veremos mais sobre ele na seção sobre layouts.

Transformações (Transformations)

Transformações são operações realizadas sobre os controles

offset É uma translação aplicada sobre um controle, antes que ele seja renderizado. A translação é dada em uma escala relativa ao tamanho do controle. Um deslocamento de 0,25 realizará uma translação horizontal de 1/4 da largura do controle. Ex,: ft.Container(..., offset=ft.transform.Offset(-1, -1).
opacity Torna o controle parcialmente transparente. O default é opacity=1.0, nenhuma transparência. Se opacity=0.0controle é 100% transparente.
rotate Aplica uma rotação no controle em torno de seu centro. O parâmetro rotate pode receber um número, que é interpretado com o ângulo, em radianos, de rotação anti-horária. A rotação também pode ser especificada por meio de transform.Rotate, que permite estabelecer ângulo, alinhamento e posição de centro de rotação. Ex,: ft.Image(..., rotate=Rotate(angle=0.25 * pi, alignment=ft.alignment.center_left) representa uma rotação de 45° (π/4).
scale Controla a escala ao longo do plano 2D. O fator de escala padrão é 1,0. Por ex.: ft.Image(..., scale=Scale(scale_x=2, scale_y=0.5) multiplica as dimensões em x por 2 e em y por .5. Alternativamente Scale(scale=3) multiplica por 3 nas duas direções.

Exemplo de uso das Propriedades e Transformações

import flet as ft

def main(page: ft.Page):
    def mover_x(e):
        ct.left += 20
        page.update()

    def mover_y(e):
        ct.top += 20
        page.update()

    def mover(e):
        bt3.value += .2
        ct.offset=ft.transform.Offset(bt3.value, bt3.value)
        page.update()

    def sumir(e):
        ct.visible = not ct.visible
        page.update()

    def rodar(e):
        bt5.value+=.5
        ct.rotate=ft.Rotate(angle=bt5.value, alignment=ft.alignment.center)
        page.update()

    def opaco(e):
        ct.opacity -= .1
        page.update()

    def zerar(e):
        bt3.value=0
        bt5.value=0
        ct.left=0
        ct.top=0
        ct.offset=ft.transform.Offset(0, 0)
        ct.opacity = 1
        ct.visible = True
        ct.rotate=ft.Rotate(angle=0, alignment=ft.alignment.center)
        page.update()

    bt1 = ft.ElevatedButton(" ", icon="arrow_circle_right", on_click= mover_x, width=50)
    bt2 = ft.ElevatedButton(" ", icon="arrow_circle_down", on_click= mover_y, width=50)
    bt3 = ft.ElevatedButton(" ", icon="SUBDIRECTORY_ARROW_RIGHT", on_click= mover, width=50)
    bt3.value=0
    bt4 = ft.ElevatedButton("on/off", on_click= sumir, width=150)
    bt5 = ft.ElevatedButton("Rodar", on_click= rodar, width=150)
    bt5.value=0
    bt6 = ft.ElevatedButton("Opaco", on_click= opaco, width=150)
    bt7 = ft.ElevatedButton("Zerar", on_click= zerar, width=150)

    ct = ft.Container(bgcolor="red", width=50, height=50, left=0, top=0,
                      offset=ft.transform.Offset(0, 0))

    page.add(ft.Row([bt1, bt2, bt3, bt4, bt5, bt6, bt7]), ft.Stack([ct], width=1000, height=1000))

ft.app(target=main)

Os botões executam as funções:

  • bt1 ⇾ move para a direita, horizontalmente (ct.left += 20),
  • bt2 ⇾ move para baixo, na vertical (ct.top += 20),
  • bt3 ⇾ aumenta offset, nas duas direções (ct.offset=ft.transform.Offset(bt3.value, bt3.value)),
  • bt4 ⇾ torna a imagem invisível/visível (ct.visible = not ct.visible),
  • bt5 ⇾ gira imagem, anti-horário:
    ct.rotate=ft.Rotate(angle=bt5.value, alignment=ft.alignment.center),
  • bt6 ⇾ torna a cor mais translúcida (ct.opacity -= .1),
  • bt7 ⇾ retorna a imagem para o estado inicial,
Figura 12: Propriedades e Transformações

onde ct = ft.Container, é o container de cor vermelha, mostrado no figura 12.

Atalhos de Teclado

Qualquer pessoa que faz uso intensivo do computador sabe da importância dos Atalhos de Teclado (Keyboard shortcuts). A possibilidade de não mover a mão do teclado para acionar o mouse pode significar melhor usabilidade e aumento de produtividade. Para isso o Flet oferece para o programador a possibilidade de inserir atalhos em seus aplicativos para que seu usuário possa dinamizar seu uso.

Para capturar eventos produzidos pelo teclado o objeto page implementa o método on_keyboard_event, que gerencia o pressionamento de teclas de caracter, em combinação com teclas de controle. Esse evento passa o parâmetro eque é uma instância da classe KeyboardEvent, e tem as seguintes propriedades:

e.key Representação textual da tecla pressionada. Veja nota.
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††.

Nota: Além dos caracteres A … Z (todos apresentados em maiúsculas) também são representadas as teclas Enter, Backspace, F1 … F12, Escape, Insert … Page Down, Pause, etc. Alguns teclados permitem a reconfiguração de teclas, por exemplo fazendo F1 = Help, etc.
Nota††: A tecla Meta é representada em geral no Windows como tecla Windows e no Mac como tecla Command.

O seguinte código ilustra esse uso. A linha page.on_keyboard_event = on_teclado faz com que eventos de teclado acionem a função on_teclado. O objeto e leva as propriedades booleanas e.ctrl, e.alt, e.shift, e.meta e o texto e.key.

import flet as ft                                                          
                                                                            
def main(page: ft.Page):

    class BtControle(ft.TextField):
        def __init__(self, text):
            super().__init__()
            self.value = text
            self.width=100
            self.text_size=25
            self.bgcolor="blue"
            self.color="white"
            self.visible = False
            
    def on_teclado(e: ft.KeyboardEvent):
        c_ctrl.visible = e.ctrl
        c_alt.visible = e.alt
        c_shift.visible = e.shift
        c_meta.visible = e.meta
        c_key.visible = True
        c_key.value = e.key
        page.update()

    page.on_keyboard_event = on_teclado

    t1= ft.Text("Pressione qualquer tecla, combinada com \nCTRL, ALT, SHIFT ou META", size=25)
    c_ctrl = BtControle("Ctrl")
    c_alt = BtControle("Alt")
    c_shift = BtControle("Shift")
    c_meta = BtControle("Meta")
    c_key = BtControle("")

    page.add(t1)
    page.add(ft.Row(controls=[c_ctrl, c_alt, c_shift, c_meta, c_key]))

ft.app(target=main) 
Figura 13: Uso de “keyboards shortcuts”

O resultado desse código, quando executado e após o pressionamento simultaneo das teclas CTRL-ALT-SHIFT-J, está mostrado na figura 13.

O exemplo acima ilustra ainda uma característica da POO (programação orientada a objetos). Como temos que criar 5 caixas de texto iguais, exceto pelo seu valor, criamos uma classe BtControle (herdando de ft.TextField) e criamos cada botão como instância dessa classe. No código manipulamos a visibilidade desses botões.

Bibiografia


Python com Flet: Botões

Python com Flet


Flet: Flutter para Python

Baseado no Flutter (veja seção abaixo) foi desenvolvida a biblioteca Flet, um framework que permite a construção de aplicações web, desktop e mobile multiusuário interativas usando o Python. O Flet empacota os widgets do Flutter e adiciona algumas combinações próprias de widgets menores, ocultando complexidades e estimulando o uso de boas práticas de construção da interface do usuário. Ele pode ser usado para construir aplicativos que rodam do lado do servidor, eliminando a necessidade de uso de HTML, CSS e Javascrip, e também aplicativos para celulares e desktop. Seu uso exige o conhecimento prévio de Python e um pouco de POO (programação orientada a objetos).

Atualmente (em junho de 2024) o Flet está na versão v0.23.0 e em rápido processo de desenvolvimento.

Flutter e Widgets


Flutter é um framework para o desenvolvimento (um SDK) de interface do usuário de software de código aberto criado pelo Google, e lançado em maio de 2017. Em outras palavras ele serve para a construção de GUIs (Interfaces Gráficas de Usuários), e é usado para desenvolver aplicativos em diversas plataformas usando um único código base.

A primeira versão do Flutter (Flutter Sky) rodava no sistema operacional Android e, segundo seus desenvolvedores, podia renderizar 120 quadros por segundo. O Flutter 1.0, a primeira versão estável do framework, foi lançado em 2018. Em 2020 surgiu o kit de desenvolvimento de software Dart (SDK) versão 2.8 com o Flutter 1.17.0, em que foi adicionado suporte para API que melhora o desempenho em dispositivos iOS, juntamente com novos widgets de materiais e ferramentas rastreamento em rede.

O Flutter 2 foi lançado pelo Google em 2021, incluindo um novo renderizador Canvas Kit para aplicativos baseados na web e aperfeiçoamento no suporte de aplicativos web e desktop para Windows, macOS e Linux. Em setembro de 2021, o Dart 2.14 e o Flutter 2.5 foram lançados, com melhorias para o modo de tela cheia do Android e a versão mais recente do Material Design do Google. Em 2022 o Flutter foi lançado expandindo o suporte a plataformas, com versões estáveis para Linux e macOS em arquiteturas diversas. O Flutter 3.3 trouxe interoperabilidade com Objective-C e Swift e uma versão preliminar de um novo mecanismo de renderização chamado “Impeller”. Em janeiro de 2023 foi anunciado o Flutter 3.7.

Aplicativos elaborados com Flutter são baseados em Widgets. Widgets são pequenos blocos de aplicativo com representação gráfica, que podem ser inseridos dentro de ambientes gráficos mais gerais , usados em aplicativos web ou desktop. Eles aparecem na forma de botões, caixas de texto, relógios e calendários selecionáveis, menus drop-down, etc, e servem basicamente para a interação com o usuário, ou recebendo inputs, como um clique em um botão, ou exibindo resultados, como um texto de resposta ou um gráfico.

Instalando o Flet

Podemos descobrir se o Flet está instalado iniciando uma sessão interativa do Python e tentando sua importação. Se não estiver instalado uma mensagem de erro será emitida:

$ python
Python 3.12.0 (... etc.)
>>> import flet
ModuleNotFoundError: No module named 'flet'

Mesmo após a instalação do flet, vista abaixo, um erro pode aparecer. Para executar código do python com flet no Linux são necessárias as bibliotecas do GStreamer. A maioria das distribuições do Linux as instalam por default. Caso isso não aconteça e a mensagem de erro abaixo for emitida, instale o GStreamer.

# mensagem de erro ao executar python com flet
error while loading shared libraries: libgstapp-1.0.so.0: cannot open shared object file: No such file or directory

# Instalando GStreamer no fedora
$ sudo dnf update
$ dnf install gstreamer1-devel gstreamer1-plugins-base-tools gstreamer1-doc gstreamer1-plugins-base-devel gstreamer1-plugins-good gstreamer1-plugins-good-extras gstreamer1-plugins-ugly gstreamer1-plugins-bad-free gstreamer1-plugins-bad-free-devel gstreamer1-plugins-bad-free-extras

# Instalando GStreamer no ubuntu
$ sudo apt-get update
$ sudo dnf install libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev libgstreamer-plugins-bad1.0-dev gstreamer1.0-plugins-base gstreamer1.0-plugins-good gstreamer1.0-plugins-bad gstreamer1.0-plugins-ugly gstreamer1.0-libav gstreamer1.0-doc gstreamer1.0-tools gstreamer1.0-x gstreamer1.0-alsa gstreamer1.0-gl gstreamer1.0-gtk3 gstreamer1.0-qt5 gstreamer1.0-pulseaudio

Flet exige Python 3.7 ou superior. Para instalar o módulo podemos usar o pip. Como ocorre em outros casos, é recomendado (mas não obrigatório) instalar a nova biblioteca dentro de um ambiente virtual.

# criamos um ambiente virtual com o comando
$ python3 -m venv ~/Projetos/.venv
# para ativar o ambiente virtual
$ cd ~/Projetos/.venv
$ source bin/activate

# agora podemos instalar o flet
$ pip install flet

# para upgrade do flet, se já instalado
$ pip install flet --upgrade

# para verificar a versão do flet instalado
$ flet --version

Em alguns casos uma mensagem de erro é emitida quando se tenta rodar um aplicativo do flet. Para resolver essa questão podemos instalar os pacotes mpv-libs e mpv-devel:

# ao executar um arquivo flet obtemos uma mensagem de erro
$ python flet3.py
/home/guilherme/.flet/bin/flet-0.23.2/flet/flet: error while loading shared libraries:
libmpv.so.1: cannot open shared object file: No such file or directory

# para resolver instalamos os pacotes (no fedora)
$sudo dnf install mpv-libs
$sudo dnf install mpv-devel

# podemos verificar a existência dos pacotes na pasta apropriada
$ cd /usr/lib64
$ find *mpv*

# devemos ver a resposta
libmpv.so libmpv.so.2 libmpv.so.2.1.0 

# criamos um link simbólico para a pasta /usr/lib64/
$ sudo ln -s /usr/lib64/libmpv.so /usr/lib64/libmpv.so.1

Outras instruções de instalação podem ser encontradas na documentação do Flet, ou a instalação com Anaconda.

Feito isso podemos escrever nosso primeiro código flet, apenas com o esqueleto de um aplicativo. Ao ser executado ele apenas abre uma janela sem conteúdo. Abra um editor de texto, ou sua IDE preferida, e grave o seguinte arquivo, com nome flet1.py:

import flet as ft

def main(page: ft.Page):
    # controles da Página
    pass

ft.app(target=main)

Esse código pode ser executado com:

$ python flet1.py
# ou
$ flet flet1.py

Ao executar python flet1.py veremos apenas uma janela vazia, que pode ser fechada com os controles usuais de janela (ou CTRL-F4). O programa termina com flet.app(target=main), recebendo no parâmetro target a função que apenas recebe o objeto fleet.Page, main (podia ter outro nome qualquer). O objeto Page é como um Canvas onde, mais tarde, inseriremos outros widgets.

Da forma como escrevemos esse código, uma janela nativa do sistema operacional será aberta no desktop. Para abrir o mesmo aplicativo no browser padrão trocamos a última linha por:

ft.app(target=main, view=ft.AppView.WEB_BROWSER)

Nota: Aplicativos do Flet, rodando no desktop ou dentro do navegador, são aplicativos web. Ele se utiliza de um servidor chamado “Fletd” que, por default usa uma porta TCP aleatória. Uma porta específica pode ser designada atribuindo-se um valor para a parâmetro port:

flet.app(port=8550, target=main)

Em seguida abra o navegador com o endereço http://localhost:8550 para ver o aplicativo em ação.

Criando um aplicativo do Flet

O flet inclui uma rotina que pode ser usada para gerar um aplicativo mínimo, que pode ser aumentado com os widgets e comandos dio usuário. Para isso executamos:

$ flet create <nome_do_projeto>

# <nome_do_projeto> será usado como nome do diretório que recebe o aplicativo.
# por exemplo:
flet create meu_aplicativo_flet

# para criar um aplicativo no diretório atual, execute:
$ flet criate .

# para criar um aplicativo basedo no modelo "contador", execute:
$ flet create --template contador <nome_do_projeto>

# para criar o aplicativo a partir do modelo, no diretório atual, execute:
$ flet criar --template contador .

O Flet criará o diretório <nome_do_projeto> contendo o arquivo main.py:

import flet as ft

def main(page: ft.Page):
    page.add(ft.SafeArea(ft.Text("Hello, Flet!")))

ft.app(main)

No corpo da função main() podemos adicionar elementos de UI (controles) e código a ser executado. O código termina com a função ft.app(main) que inicializa o aplicativo Flet e executa main().

Rodando no destop: Para rodar o aplicativo no desktop basta executar:

# roda main.py no diretório atual
$ flet run

# se for nevessário especificar um caminho diferente:
$ flet run [script]

# por exemplo:
$ flet run /Users/Usuario/Projetos/flet-app

Em qualquer dos casos a função main() será executada e o aplicativo será iniciado em janela nativa do sistema operacional usado.

Rodando no navegador: Para rodar um aplicativo como app da web (no navegador, portanto) usamos o comando:

$ flet run --web [script]
# uma nova janela (ou aba) será aberta no navegador, usando uma porta aleatória.

Recarregamento automático: Por default o Flet carregará e rodará o arquivo principal, carregando novamente sempre que esse arquivo for alterado e gravado. Mas ele não será afetado por alterações em outros arquivos no projeto. Para que todos os arquivos sejam recarregados quando alterados usamos:

$ poetry run flet run -d [script]

# para que sub-diretórios sejam incluídos recursivamente use:
$ poetry run flet run -d -r [script]

Sintaxe do comando run

O comando run é usado para executar aplicativos do Flet e tem a seguinte sintaxe:

flet run [-h] [-v] [-p PORT] [--host HOST] [--name APP_NAME] [-m] [-d] [-r] [-n] [-w] [ --ios] [--android] [-a ASSETS_DIR]
         [--ignore-dirs IGNORE_DIRS] [script]

script é um argumento posicional, servindo para designar o caminho do script Python.

As demais opções estão listadas na tabela abaixo.

Argumento Significado
-h, –help mostra esta mensagem de ajuda e termina
-v, –verbose -v para saída detalhada e -vv para mais detalhes
-p PORT, –port PORT Porta TCP personalizada para execução do aplicativo
–host HOST host para execução do aplicativo web. Use “*” para escutar em todos os IPs.
–name APP_NAME nome do aplicativo para distingui-lo de outros apps na mesma porta
-m, –module trata o script como um caminho de módulo python, e não como caminho de arquivo
-d, –directory observar o diretório de script
-r, –recursive observar diretório e subdiretórios de script recursivamente
-n, –hidden manter a janela do aplicativo oculta na inicialização
-w, –web abrir aplicativo em um navegador da web
–ios abrir aplicativo em dispositivo iOS
–android abrir aplicativo no dispositivo Android
-a ASSETS_DIR, –assets ASSETS_DIR caminho para o diretório de assets
:–ignore-dirs IGNORE_DIRS diretórios a serem ignorados durante a observação. Para mais de um, separe com vírgulas.

Notas: † Observar, nesse caso, é estar ciente de que houve alterações no código usado para recarregamento automático, descrito acima.

Inserindo Widgets

Para obter alguma funcionalidade em nosso aplicativo temos que inserir nele controles, também chamados de widgets. Controles são inseridas na Page, o widget de nível mais alto, ou aninhados dentro de outros controles. Por exemplo, para inserir texto diretamente na page fazemos:

import flet as ft

def main(page=ft.Page):
    txt = ft.Text(value="Olá mundo!", color="blue")
    page.controls.append(txt)
    page.update()

ft.app(target=main)


Widgets são objetos do python com uma representação visual, com características controladas por seus parâmetros. value e color são parâmetros que recebem strings, esse último declarado no formato de cor do HTML. São válidas as cores, por exemplo: navyblue, #ff0000 (vermelho), etc. O objeto page possui uma lista controls, à qual adicionamos o elemento txt.

No código seguinte usamos um atalho: page.add(t) significa o mesmo que page.controls.append(t) seguido de page.update(). Também importamos o módulo time para provocar uma pausa na execução do código em time.sleep(1).

import flet as ft
import time

def main(page=ft.Page):
    t = ft.Text()
    page.add(t)
    cidades = ["Belo Horizonte","Curitiba","São Paulo","Salvador","** fim **"]
    for i in range(5):
        t.value = cidades[i]
        page.update()
        time.sleep(1)

ft.app(target=main)

Ao ser executado as quatro cidades armazenadas na lista cidades são exibidas e o loop é terminado com a exibição de ** fim **.


Alguns controles, como Row e Line são containers, podendo conter dentro deles outros widgets, da mesma forma que Page. Por exemplo, inicializamos abaixo uma linha (um objeto ft.Row) contendo três outros objetos que são strings, e a inserimos em page.

import flet as ft
import time

def main(page=ft.Page):
    linha = ft.Row(controls=[ft.Text("Estas são"), ft.Text("cidades"), ft.Text("brasileiras")])
    page.add(linha)
    t = ft.Text()
    page.add(t) # it's a shortcut for page.controls.append(t) and then page.update()
    cidades = ["Belo Horizonte","Curitiba","São Paulo","Salvador","** fim **"]
    for i in range(5):
        t.value = cidades[i]
        page.update()
        time.sleep(1)

ft.app(target=main)

O resultado é exibido na figura 1, com a cidade sendo substituída a cada momento. A linha page.update() é necessária pois o valor do ft.Text() foi alterado.

Figura 1

Vemos que Row recebe no parâmetro controls  uma lista com 3 widgets de texto.

Claro que muitos outros controles pode ser inseridos. Com o bloco abaixo podemos inserir um campo de texto, que pode ser editado pelo usuário, e um botão.

    page.add(
        ft.Row(controls=[
            ft.TextField(label="Sua cidade"),
            ft.ElevatedButton(text="Clique aqui para inserir o nome de sua cidade!")
        ])
    )

Podemos também criar novas entradas de texto e as inserir consecutivamente em page.

import flet as ft
import time

def main(page=ft.Page):
    page.add(ft.Row(controls=[ft.Text("Estas são cidades brasileiras")]))
    cidades = ["Belo Horizonte","Curitiba","São Paulo","Salvador","** fim **"]
    for i in range(5):
        t = ft.Text()
        t.value = cidades[i]
        page.add(t)
        page.update()
        time.sleep(1)

ft.app(target=main)
Figura 2

Nesse caso não estamos substituindo o conteúdo de um objeto de texto fixo na página, e sim inserindo novas linhas (figura 2). Observe que nenhum procedimento foi designado a esse botão que, no momento, não executa nenhuma tarefa.

O comando page.update(), que pode estar embutido em page.add(), envia para a página renderizada apenas as alterações feitas desde sua última execução.

Observe que o argumento controls, aqui usado em Row é um argumento posicional noemado. O nome pode ser omitido se o argumento aparecer no primreiro lugar. Ou seja:

# a linha
ft.Row(controls=[ft.Text("Estas são cidades brasileiras")])
# pode ser escrita como
ft.Row([ft.Text("Estas são cidades brasileiras")])

# se outros argumentos estão presentes, controls deve aparecer primeiro
page.add(ft.Row([ft.Text("Estas são cidades brasileiras")], wrap=True))           # certo!
page.add(ft.Row(wrap=True, [ft.Text("Estas são cidades brasileiras")]))           # erro!
page.add(ft.Row(wrap=True, controls=[ft.Text("Estas são cidades brasileiras")]))  # certo!


Para incluir alguma ação útil em nosso projeto usamos a capacidade de alguns controles de lidar com eventos (os chamados event handlers). Botões podem executar tarefas quando são clicados usando o evento on_click.

import flet as ft
def main(page: ft.Page):
    def button_clicked(e):
        page.add(ft.Text("Clicou"))
        
    page.add(ft.ElevatedButton(text="Clica aqui", on_click=button_clicked))

ft.app(target=main)

Esse código associa a função button_clicked() com o evento on_click do botão. A cada clique um novo elemento de texto é colocado na página.

Várias outras propriedades podem ser usadas para alterar a aparência dos controles. Vamos usar width (largura) no código abaixo, além do controle Checkbox, uma caixa de texto que pode ser marcada. A função entrar_tarefa() verifica se há conteúdo em nova_tarefa, um TextField e, se houver, cria e insere na página uma nova Checkbox.
Depois limpa o valor de nova_tarefa. O comando nova_tarefa.focus() coloca no comando de texto o foco da ação dentro da página, independente de ela ter ou não sido usada no if.

import flet as ft

def main(page):
    def entrar_tarefa(e):
        if nova_tarefa.value:
            page.add(ft.Checkbox(label=nova_tarefa.value))
            nova_tarefa.value = ""            
        nova_tarefa.focus()

    nova_tarefa = ft.TextField(hint_text="Digite sua nova tarefa...", width=400)
    page.add(ft.Row([nova_tarefa, ft.ElevatedButton("Inserir tarefa", on_click=entrar_tarefa, width=300)]))
    nova_tarefa.focus()

ft.app(target=main)

É claro que muitas outras ações podem ser inseridas nesse pequeno programa, tal como testar se uma entrada já existe, eliminar espaços em branco desnecessários ou gravar as tarefas em um banco de dados.

Exemplo: Controles e propriedades

É comum os tutoriais do Flet apresentarem um pequeno bloco ilustrativo de código com a operação de somar e subtrair uma unidade a um número em uma caixa de texto. Mostramos aqui um código um pouco mais elaborado para apresentar propriedades adicionais. Usamos page.title = "Soma e subtrai" para inserir um título na barra de tarefas (ou na aba do navegador, se o codigo for executado no modo web), as propriedades de alignment. Além disso declaramos os botões e caixas de texto separadamente e depois os inserimos nas linhas.

import flet as ft

def main(page: ft.Page):
    def operar(e):
        numero = int(txt_number.value) + int(e.control.text)
        txt_number.value = str(numero)
        txt_operacao.value = "soma"
        page.update()

    def mult(e):
        numero =int(txt_number.value) * int(e.control.text.replace("*",""))
        txt_number.value = str(numero)
        txt_operacao.value = "multiplica"
        page.update()

    page.title = "Soma, subtrai, multiplica"
    txt_number = ft.TextField(value="0", text_align=ft.TextAlign.RIGHT, width=100)
    txt_info = ft.Text("Você pode somar ou subtrair 1 ou 10, multiplicar por 2, 3, 5, 7")
    txt_operacao = ft.TextField(value="", text_align=ft.TextAlign.RIGHT, width=100)
    
    bt1 = ft.ElevatedButton("-10", on_click=operar, width=100)
    bt2 = ft.ElevatedButton("-1", on_click=operar, width=100)
    bt3 = ft.ElevatedButton("+1", on_click=operar, width=100)
    bt4 = ft.ElevatedButton("+10", on_click=operar, width=100)    

    bt5 = ft.ElevatedButton("*2", on_click=mult, width=100)
    bt6 = ft.ElevatedButton("*3", on_click=mult, width=100)
    bt7 = ft.ElevatedButton("*5", on_click=mult, width=100)
    bt8 = ft.ElevatedButton("*7", on_click=mult, width=100)

    page.add(ft.Row([txt_info], alignment=ft.MainAxisAlignment.CENTER))
    page.add(ft.Row([bt1, bt2, txt_number, bt3, bt4], alignment=ft.MainAxisAlignment.CENTER))
    page.add(ft.Row([bt5, bt6, txt_operacao, bt7, bt8], alignment=ft.MainAxisAlignment.CENTER))

ft.app(main)

O resultado do código é mostrado na figura 3.

Figura 3

Observe que, ao se clicar nos botões de soma, por ex., o evento on_click chama a função operar(e) passando para ela o parâmetro e, que é um objeto de evento. Este objeto tem propriedades que usamos nas funções de chamada. Na soma (e subtração) capturamos e.control.text, a propriedade de texto exibida nesse botão (-1, +1, etc.). e a usamos para fazer a operação requerida. Os controles possuem diversas propriedades e respondem a eventos específicos, que devemos manipular para contruir a aparência e funcionalidade da interface gráfica.

Vale ainda mencionar que construímos as linhas com a sintaxe ft.Row([bt1, bt2, txt_number, bt3, bt4]), usando uma lista de controles previamente definidos. Essa lista está na primeira posição, onde se insere o valor do parâmetro nomeado controls. Se esse parâmetro não estiver na primeira entrada seu nome deve ser fornecido, como em: page.add(ft.Row(alignment=ft.MainAxisAlignment.CENTER, controls=[txt_info])).

Em uma questão meramente de estilo mas que pode facilitar a organização e leitura do código, podemos definir as linhas separadamente e depois inserí-las na página.

    linha1 = ft.Row([txt_info], alignment=ft.MainAxisAlignment.CENTER)
    linha2 = ft.Row(controls=[bt1, bt2, txt_number, bt3, bt4], alignment=ft.MainAxisAlignment.CENTER)
    linha3 = ft.Row(controls=[bt5, bt6, txt_operacao, bt7, bt8],
             alignment=ft.MainAxisAlignment.CENTER)

    page.add(linha1, linha2, linha3)

Bibiografia

Todas as URLs acessadas em Julho de 2023.


Python com Flet: Widgets