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.
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.
bt6 ⇾ torna a cor mais translúcida (ct.opacity -= .1),
bt7 ⇾ retorna a imagem para o estado inicial,
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.
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.
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
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:
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.
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)
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.
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.