Tutorial 2 - Tornando as coisas interessantes

No Tutorial 1, geramos um projeto inicial que executava, mas não escrevemos nenhum código por conta própria. Vamos dar uma olhada no que foi criado para nós.

O que foi gerado

No diretório src/helloworld, você deverá ver 3 arquivos: __init__.py, __main__.py e app.py.

__init__.py torna o diretório helloworld um módulo Python importável. Trata-se de um arquivo vazio; sua simples existência indica ao interpretador Python que há um módulo definido pelo diretório helloworld.

__main__.py define o módulo helloworld como um tipo especial de módulo - um módulo executável. Ao tentar executar o módulo helloworld usando python -m helloworld, o Python iniciará a execução no arquivo __main__.py. O conteúdo de __main__.py é relativamente simples:

from helloworld.app import main

if __name__ == "__main__":
    main().main_loop()

Isto é - ele importa o método main do aplicativo helloworld; quando executado como ponto de entrada, esse método main() é chamado, dando início ao loop principal da aplicação. Nesse loop, a aplicação de interface gráfica (GUI) espera pelas interações do usuário (como cliques do mouse e pressionamentos de teclas).

O arquivo mais interessante é o app.py - nele está contido a lógica que cria a janela da nossa aplicação:

import toga
from toga.style import Pack
from toga.style.pack import COLUMN, ROW

class HelloWorld(toga.App):
    def startup(self):
        main_box = toga.Box()

        self.main_window = toga.MainWindow(title=self.formal_name)
        self.main_window.content = main_box
        self.main_window.show()

def main():
    return HelloWorld()

Vamos analisar isso linha por linha:

import toga
from toga.style import Pack
from toga.style.pack import COLUMN, ROW

Primeiro, importamos a biblioteca de widgets toga, juntamente com algumas classes e constantes que são úteis para o estilo. Por enquanto, nosso código ainda não faz uso delas, mas em breve começaremos a utilizá-las.

Em seguida, definimos uma classe:

class HelloWorld(toga.App):

Cada aplicação Toga possui uma única instância chamada toga.App, que representa o aplicativo em si sendo executado. A aplicação pode acabar gerenciando várias janelas; mas para aplicativos simples, haverá uma única janela principal.

Depois, definimos um método chamado startup():

def startup(self):
    main_box = toga.Box()

A primeira coisa que o método startup() faz é definir um container principal. O esquema de layout do Toga se comporta de maneira semelhante ao HTML. A construção de uma aplicação se dá por meio da criação de uma coleção de containers, cada um contendo outros containers ou os widgets propriamente dito. Depois, você aplica estilos a esses containers para definir como vão usar o espaço na janela.

Neste aplicativo, definimos um único container, mas não colocamos nada dentro dele.

Em seguida, definimos uma janela na qual podemos colocar o container vazio:

self.main_window = toga.MainWindow(title=self.formal_name)

Isso cria uma instância de toga.MainWindow, que terá um título correspondente ao nome do aplicativo. A MainWindow (Janela Principal) é um tipo especial de janela no Toga, pois está intimamente ligada ao ciclo de vida da aplicação. Quando a MainWindow é fechada, a aplicação também se encerra. Além disso, a MainWindow também é a janela que contém o menu do aplicativo (se você estiver em uma plataforma como o Windows, onde as barras de menu fazem parte da janela)

Então, adicionamos nosso container vazio como conteúdo da janela principal e instruímos o aplicativo a mostrar nossa janela:

self.main_window.content = main_box
self.main_window.show()

Por último, definimos um método chamado main(). Isso é o que cria a instância de nossa aplicação:

def main():
    return HelloWorld()

O método main() é aquele que é importado e chamado pelo arquivo __main__.py. Ele cria e retorna uma instância da nossa aplicação HelloWorld.

Essa é a aplicação Toga mais básica possível. Vamos agora integrar conteúdo personalizado à aplicação e torná-la mais interessante.

Adicionando conteúdo personalizado

Modifique sua classe HelloWorld dentro de src/helloworld/app.py para que fique assim:

class HelloWorld(toga.App):
    def startup(self):
        main_box = toga.Box(style=Pack(direction=COLUMN))

        name_label = toga.Label(
            "Your name: ",
            style=Pack(padding=(0, 5)),
        )
        self.name_input = toga.TextInput(style=Pack(flex=1))

        name_box = toga.Box(style=Pack(direction=ROW, padding=5))
        name_box.add(name_label)
        name_box.add(self.name_input)

        button = toga.Button(
            "Say Hello!",
            on_press=self.say_hello,
            style=Pack(padding=5),
        )

        main_box.add(name_box)
        main_box.add(button)

        self.main_window = toga.MainWindow(title=self.formal_name)
        self.main_window.content = main_box
        self.main_window.show()

    def say_hello(self, widget):
        print(f"Hello, {self.name_input.value}")

Nota

Não remova as importações já feitas no topo do arquivo ou o main() ao final. Você só precisa atualizar a classe HelloWorld.

Vamos ver em detalhes o que mudou.

Ainda estamos fazendo o container principal; porém, agora estamos aplicando um estilo:

main_box = toga.Box(style=Pack(direction=COLUMN))

O Toga possui um sistema de layout interno denominado «Pack». Seu funcionamento se assemelha ao CSS. Você define objetos em uma hierarquia - no HTML, os objetos são <div> e <span>, e outros elementos do DOM; enquanto no Toga, são widgets e containers. Estilos podem ser atribuídos a cada elemento individualmente. Neste caso específico, estamos indicando que o container é um 'COLUMN' - isto é, ele ocupará toda a largura disponível e aumentará sua altura à medida que conteúdo for adicionado, porém buscando sempre ser o mais compacto possível.

A seguir, vamos definir dois widgets:

name_label = toga.Label(
    "Your name: ",
    style=Pack(padding=(0, 5)),
)
self.name_input = toga.TextInput(style=Pack(flex=1))

Nesta etapa, definimos um Label e um TextInput. Ambos os widgets possuem estilos associados; o Label terá 5px de preenchimento à esquerda e à direita, e nenhum preenchimento na parte superior e inferior. O TextInput é marcado como flexível, ou seja, ele absorverá todo o espaço disponível em seu eixo de layout.

O TextInput é atribuído como uma variável de instância da classe. Isso nos permite acessar facilmente a instância do widget, o que será utilizado em breve.

Após isso, definimos um container para colocar esses dois widgets:

name_box = toga.Box(style=Pack(direction=ROW, padding=5))
name_box.add(name_label)
name_box.add(self.name_input)

A container name_box é parecido com o container principal, só que agora ele é do tipo ROW. Isso significa que o conteúdo será adicionado horizontalmente e tentará ficar o mais estreito possível. Esse container também possui preenchimento - 5px em todos os lados.

Agora definimos um botão:

button = toga.Button(
    "Say Hello!",
    on_press=self.say_hello,
    style=Pack(padding=5),
)

O botão também possui 5px de preenchimento em todos os lados. Além disso, definimos um gatilho - um método a ser invocado quando o botão for pressionado.

Então, adicionamos o container name_box e o botão ao container principal:

main_box.add(name_box)
main_box.add(button)

Com isso, finalizamos o layout; o restante do código do método startup() permanece como antes - definindo uma MainWindow (Janela principal) e a atribuição do container principal como conteúdo dessa janela:

self.main_window = toga.MainWindow(title=self.formal_name)
self.main_window.content = main_box
self.main_window.show()

A última etapa consiste na definição do gatilho do botão. Um gatilho pode ser qualquer método, gerador ou corrotina assíncrona; ele aceita como argumento o widget que gerou o evento e será invocado sempre que o botão for pressionado:

def say_hello(self, widget):
    print(f"Hello, {self.name_input.value}")

O corpo do método é uma simples instrução de impressão. No entanto, ele consultará o valor atual do campo de entrada de nome e usará esse conteúdo como o texto a ser impresso.

Ao concluirmos essas alterações, podemos visualizar o resultado reiniciando a aplicação. Seguindo o procedimento anterior, utilizaremos o modo de desenvolvedor:

(beeware-venv) $ briefcase dev

[helloworld] Starting in dev mode...
===========================================================================

Dessa vez, você vai reparar que as dependências não são instaladas. O Briefcase consegue identificar se o app já foi aberto antes e, pra economizar tempo, só executa ele. Se você adicionar novas dependências, pode garantir que elas sejam instaladas usando a opção -r quando executar o comando briefcase dev.

Isso deve abrir uma janela GUI:

Tutorial 2: Janela "Hello World", no macOS

Se você escrever um nome na caixa de texto e clicar no botão da interface, vai ver uma mensagem aparecer no console onde você abriu a aplicação.

Próximos passos

Criamos uma aplicação com recursos mais interessantes. Porém, ela somente funciona em nosso computador. Vamos empacotar esta aplicação para distribuição. No Tutorial 3, encapsularemos nossa aplicação em um instalador independente, que poderemos enviar a amigos, clientes ou publicar em uma App Store.