Flet 1 - Inleiding en TodoApp Tutorial
Inleiding
Wat is Flet?
Flet is een framework waarmee je moderne, interactieve user interfaces kunt bouwen. Met Flet maak je niet alleen een web-applicatie, maar ook desktop en mobile versies. En dit allemaal vanuit dezelfde code.
Flet gebruikt Flutter als UI-engine. De Flet UI-componenten (knoppen, layouts, navigatie, …) zijn eigenlijk Flutter-widgets die via Flet worden aangestuurd.
Flutter is een UI toolkit van Google en gebruikt de Dart programmeertaal.
Om met Flet te werken hoef je Flutter en Dart niet te kennen.
Een belangrijk verschil tussen Flutter en Flet is dat Flutter een declaratief model gebruikt, waarbij de UI automatisch opnieuw wordt opgebouwd wanneer de applicatiedata verandert.
Daar tegenover implementeert Flet een imperatief UI-model waarbij je de gebruikersinterface van je applicatie “handmatig” opbouwt met stateful controls en deze vervolgens aanpast door eigenschappen van die controls te wijzigen.
Voorbereiding
Zoals gebruikelijk starten we een nieuw project (nieuwe folder) en initialiseren we git en uv. Flet zelf installeren we via het flet package. Met flet[all] krijgen we alle extra componenten, zoals bvb. flet-web voor web-applicatie rendering.
Net als Flask heeft Flet een commando, flet. Een eerste commando is flet create, waarmee een standaard projectstructuur wordt opgezet.
Flet Hello World
Met het flet create commando krijgen we al direct een eenvoudige voorbeeld-applicatie voorgeschoteld. Met het run commando kan deze uitgevoerd worden (als lokale desktop-applicatie).
We bekijken hieronder de bijhorende code.
Bij Flet spreken we niet over widgets maar over controls. Elk component in de Flet applicatie is eigenlijk zo een control. Door controls in elkaar te nesten wordt de visuele structuur van de applicatie opgebouwd.
De Page control staat aan de basis en stelt de volledige applicatie voor.
src/main.py
1import flet as ft
2def main(page: ft.Page):
3 counter = ft.Text("0", size=50, data=0)
6 def increment_click(e):
7 counter.data += 1
counter.value = str(counter.data)
8 counter.update()
5 page.floating_action_button = ft.FloatingActionButton(
icon=ft.Icons.ADD, on_click=increment_click
)
4 page.add(
ft.SafeArea(
ft.Container(counter, alignment=ft.alignment.center),
expand=True
)
)
ft.app(main)- 1
-
Niet verplicht maar de conventie is om
fletvolledig te importeren alsft - 2
-
De
mainfunctie krijgt als argument automatisch dePagecontrol. - 3
-
Een
Textcontrol wordt gebruikt om de huidige waarde van de counter bij te houden en weer te geven. Belangrijk: het eerste argument ("0"), bevat de tekst (eenstrdus) die getoond wordt. Indatakan om het even wat opgeslagen worden. In dit geval eenintdie we zullen kunnen incrementeren. - 4
-
De
countercontrol wordt in eenContainercontrol geplaatst, met als doel decounterte centreren binnen dieContainer. Die laatste komt op zijn beurt in eenSafeAreacontrol. Die zorgt ervoor dat de inhoud niet overlapt met andere externe componenten (bvb. camera uitsparing bij mobile toestellen). Uiteindelijk wordt dezeSafeArea(met daarin dus deContaineren daarin deText) aan dePagetoegevoegd. - 5
-
Met de
FloatingActionButtoncontrol wordt de+knop gemaakt. Deze bevat eenicon, gekozen uit deIconsenum. Heton_clickargument krijgt een functie (Callable) die zal worden aangeroepen wanneer er op de button geklikt wordt. - 6
-
increment_clickis dus die functie. Deze functies krijgen altijd als argument eenControlEventmee, met o.a. informatie over de oorsprong van het event. In dit geval wordt het argument verder niet gebruikt. - 7
-
De
datavan decounter(Text) is eeninten kunnen we gewoon incrementeren. Devalueis eenstr(dit is wat effectief wordt weergegeven). - 8
-
De
updatemethod van hetTextobject zorgt ervoor dat de weergave ook daadwerkelijk wordt aangepast.
De update method update niet alleen de control zelf, maar ook alle geneste controls. Met page.update() zouden we dus hetzelfde resultaat krijgen als met counter.update().
Flet run opties
Met uv run flet run krijg je een lokaal applicatievenster. We bekijken een aantal andere opties
Web
Met de --web optie wordt de applicatie beschikbaar gemaakt als web applicatie via een lokale webserver.
Auto-reload:
Met de -d (directory) optie wordt de applicatie automatisch opnieuw geladen als een van de source-code bestanden wordt aangepast. Deze optie is te gebruiken in combinatie met andere opties zoals --web.
Mobile
Via een aanvullende iOS of Android app kan je je applicatie gemakkelijk testen op een fysiek iOS of Android device.
Controls
Vooraleer we aan een meer uitgebreide applicatie beginnen bekijken we een aantal belangrijke Flet controls individueel.
Op https://flet-controls-gallery.fly.dev krijg je een erg handig overzicht van alle controls met verschillende voorbeelden en bijhorende code. Op deze manier kan je ontdekken wat allemaal mogelijk is met Flet. Merk op dat die site zelf een Flet applicatie is!
Text
De Text control hebben we al gebruikt in het eerste voorbeeld om de counter waarde te tonen (en ook de huidig waarde bij te houden).
src/main.py
import flet as ft
def main(page: ft.Page):
page.add(ft.Text("Size 10", size=10))
page.add(
ft.Text(
"Size 40, w100",
size=40,
color=ft.Colors.RED,
bgcolor=ft.Colors.BLUE_600,
weight=ft.FontWeight.W_100,
)
)
page.add(ft.Text("Selectable Text", size=40, selectable=True))
page.add(
ft.Text(
"Proin rutrum, purus sit amet elementum volutpat, nunc lacus vulputate orci, cursus "
"ultrices neque dui quis purus. Ut ultricies purus nec nibh bibendum, eget vestibulum "
"metus various. Duis convallis maximus justo, eu rutrum libero maximus id.",
size=40,
max_lines=2,
overflow=ft.TextOverflow.ELLIPSIS,
)
)
page.add(
ft.Text(
"theme_style=ft.TextThemeStyle.HEADLINE_SMALL",
theme_style=ft.TextThemeStyle.HEADLINE_SMALL
)
)
ft.app(target=main)TextField
Met een TextField control maak je tekst velden die gebruikers van de applicatie kunnen invullen.
src/main.py
import flet as ft
def main(page: ft.Page):
def button_clicked(e):
text.value = f"Values: '{tf1.value}', '{tf2.value}', '{tf3.value}', '{tf4.value}'."
page.update()
tf1 = ft.TextField(label="Standard")
tf2 = ft.TextField(label="Multiline", multiline=True)
tf3 = ft.TextField(label="With Prefix", prefix_text="https://")
tf4 = ft.TextField(
label="Secret + Icon",
icon=ft.Icons.KEY,
password=True,
can_reveal_password=True,
)
button = ft.ElevatedButton(text="Submit", on_click=button_clicked)
text = ft.Text()
page.add(tf1, tf2, tf3, tf4, button, text)
ft.app(target=main)Checkbox
Met Checkbox maak je natuurlijk een checkbox (selectievakje) die de gebruiker kan aan- of af-vinken.
Zoals de meeste andere controls kan de huidige waarde van de checkbox worden uitgelezen via het .value attribuut.
Met de on_change parameter kan een functie worden aangeroepen als de waarde verandert.
src/main.py
import flet as ft
def main(page: ft.Page):
def checkbox(e):
text.value = f"c1={c1.value}, c2={c2.value}, c3={c3.value}"
page.update()
text = ft.Text()
c1 = ft.Checkbox(
label="Checked by default checkbox", value=True, on_change=checkbox
)
c2 = ft.Checkbox(
label="No default, tristate checkbox", tristate=True, on_change=checkbox
)
c3 = ft.Checkbox(label="Disabled checkbox", disabled=True, on_change=checkbox)
page.add(text, c1, c2, c3)
ft.app(main)Dropdown
Een Dropdown is zoals een TextField maar met een vaste lijst mogelijkheden waar de gebruiker uit kan kiezen.
De options parameter verwacht een lijst van DropdownOption objecten die elk een mogelijke keuze voorstellen.
Met enable_filter kan de gebruiker tekst ingeven om de mogelijke keuzes te filteren. Hiervoor is de optie editable ook noodzakelijk zodat er ook tekst kan worden ingevoerd in het dropdown veld.
src/main.py
import flet as ft
def main(page: ft.Page):
options = [
ft.DropdownOption(
key="Smile", leading_icon=ft.Icons.SENTIMENT_SATISFIED_OUTLINED
),
ft.DropdownOption(key="Cloud", leading_icon=ft.Icons.CLOUD_OUTLINED),
ft.DropdownOption(key="Brush", leading_icon=ft.Icons.BRUSH_OUTLINED),
ft.DropdownOption(key="Heart", leading_icon=ft.Icons.FAVORITE),
]
dd = ft.Dropdown(
enable_filter=True,
editable=True,
leading_icon=ft.Icons.SEARCH,
label="Icon",
options=options,
)
page.add(dd)
ft.app(main)Container
Met Container kan je eender welke andere control plaatsen binnen een zone en daarbij de exacte plaatsing en opmaak van die zone controleren.
In het volgende voorbeeld plaatsen we een eenvoudige Text control in een Container. Met verschillende parameters van de Container bepalen we zowel de opmaak (bvb. bgcolor voor de achtergrondkleur) als de layout (bvb. alignment om de Text control centraal in de Container te plaatsen).
We introduceren tegelijk ook een aantal parameters van de hoofd Page control om ervoor te zorgen dat alle controls van de Page ook daar centraal geplaatst worden.
src/main.py
import flet as ft
def main(page: ft.Page):
page.title = "Container example"
page.vertical_alignment = ft.MainAxisAlignment.CENTER
page.horizontal_alignment = ft.CrossAxisAlignment.CENTER
container = ft.Container(
content=ft.Text("Some text to be placed somewhere"),
padding=10,
alignment=ft.alignment.center,
bgcolor=ft.Colors.ORANGE_800,
width=150,
height=150,
border_radius=20,
on_click=lambda e: print("Container was clicked"),
)
page.add(container)
ft.app(main)Column en Row
Column en Row zijn twee andere belangrijke controls die de layout van een applicatie bepalen.
Ze bevatten een of meerdere andere controls die als een kolom of rij zullen worden uitgelijnd. Door Columns en Rows te combineren en te nesten heb je op een vrij eenvoudige manier veel controle over de plaatsing van de verschillende andere controls waar de applicatie uit bestaat.
In het volgende voorbeeld maken we één Column die bestaat uit drie Rows. Elke Row bestaat dan weer uit drie Containers. Op die manier maken we een rooster van Container controls. Met de spacing parameter in zowel Column als Row bepalen we hoe ver de verschillende Containers uit elkaar liggen, zowel vertical als horizontaal
src/main.py
import flet as ft
def create_row(color: ft.Colors) -> ft.Row:
row = ft.Row(spacing=2)
for i in range(1, 4):
row.controls.append(
ft.Container(
content=ft.Text(str(i)),
alignment=ft.alignment.center,
width=50,
height=50,
bgcolor=color,
)
)
return row
def main(page: ft.Page):
column = ft.Column(
controls=[
create_row(ft.Colors.AMBER),
create_row(ft.Colors.CYAN),
create_row(ft.Colors.GREEN),
],
alignment=ft.MainAxisAlignment.SPACE_AROUND,
)
page.add(column)
ft.app(main)To-Do App Tutorial
In dit hoofdstuk bouwen we stap voor stap een eenvoudige ToDo applicatie, gebaseerd op deze tutorial uit de Flet documentatie. Op die manier krijgen we wat meer inzicht hoe verschillende controls gecombineerd kunnen worden tot een volledige applicatie.
Het uiteindelijke resultaat moet er als volgt uitzien en kan je hier uittesten.
We werken verder in het flet-tutorial project.
ToDo’s toevoegen
Als eerste stap willen we gewoon taken kunnen toevoegen.
- Om tekst in te geven gebruiken we
TextField. - Voor de “+” knop
FloatingActionButton. - Elke toegevoegde taak wordt een
Checkbox.
Deze eerste stap kan als oefening gemaakt worden. Vertrek dan van de volgende basis:
src/main.py
Je zult in de FloatingActionButton de on_click parameter moeten gebruiken.
page.add kan ook binnen de on_click functie worden uitgevoerd.
Een nieuwe control toegevoegd via page.add wordt pas zichtbaar na een page.update()!
Oplossing
src/main.py
import flet as ft
def main(page: ft.Page):
def add_clicked(e):
page.add(ft.Checkbox(label=new_task.value))
new_task.value = ""
page.update()
new_task = ft.TextField(hint_text="What's needs to be done?")
page.add(new_task, ft.FloatingActionButton(icon=ft.Icons.ADD, on_click=add_clicked))
ft.app(main)Basis Layout
We willen dat de applicatie de volgende layout krijgt:
Hiervoor gebruiken we de basis layout-control Column.
Om de volledige applicatie centraal de plaatsen (bvb. in de webpagina) geef je page.horizontal_alignment de waarde ft.CrossAxisAlignment.CENTER.
Als eerste stap plaatsen we de bestaande controls in deze layout.
Column (en Row) hebben ook een width parameter. Je hebt dus geen extra Container control nodig om de breedte van de “view Column” te bepalen.
Kijk goed naar het layout-schema. Je zult Columns in elkaar moeten nesten.
Oplossing
src/main.py
import flet as ft
def main(page: ft.Page):
def add_clicked(e):
tasks_view.controls.append(ft.Checkbox(label=new_task.value))
new_task.value = ""
view.update()
new_task = ft.TextField(hint_text="What needs to be done?", expand=True)
tasks_view = ft.Column()
view=ft.Column(
width=600,
controls=[
ft.Row(
controls=[
new_task,
ft.FloatingActionButton(icon=ft.Icons.ADD, on_click=add_clicked),
],
),
tasks_view,
],
)
page.horizontal_alignment = ft.CrossAxisAlignment.CENTER
page.add(view)
ft.app(main)Tasks aanpassen
Naast elke task (checkbox) moet een edit knop komen waarmee de tekst van die task aangepast kan worden.
Elke task krijgt dus eigenlijk twee verschillende visuele voorstellingen (views):
- De normale (display) view toont de checkbox met de task tekst en een edit knop.
- De edit view toont een input tekstveld (
TextField) met een save knop om te nieuwe tekst te bevestigen.
Elke view kan een aparte Row zijn, die we groeperen in een nieuwe Column. Op elke moment is maar één van beide Rows zichtbaar. Dit kunnen we bepalen m.b.v. de visible property van de Row.
Het resultaat kunnen we als volgt representeren:
Voor de knoppen kan IconButton gebruikt worden met Icons.
Een eerste versie, waarbij de knoppen nog niets doen, kan er als volgt uitzien:
src/main.py
import flet as ft
def main(page: ft.Page):
def add_clicked(e):
checkbox = ft.Checkbox(label=new_task.value)
display_view = ft.Row(
controls=[
checkbox,
ft.Row(
spacing=0,
controls=[
ft.IconButton(icon=ft.Icons.CREATE_OUTLINED),
ft.IconButton(icon=ft.Icons.DELETE_OUTLINE),
],
),
],
)
edit_text = ft.TextField()
edit_view = ft.Row(
visible=False,
controls=[
edit_text,
ft.IconButton(icon=ft.Icons.DONE_OUTLINE_OUTLINED),
],
)
task = ft.Column(controls=[display_view, edit_view])
tasks_view.controls.append(task)
new_task.value = ""
view.update()
new_task = ft.TextField(hint_text="What needs to be done?", expand=True)
tasks_view = ft.Column()
view = ft.Column(
width=600,
controls=[
ft.Row(
controls=[
new_task,
ft.FloatingActionButton(icon=ft.Icons.ADD, on_click=add_clicked),
],
),
tasks_view,
],
)
page.horizontal_alignment = ft.CrossAxisAlignment.CENTER
page.add(view)
ft.app(main)Om de knoppen actief te maken binden we nieuwe functies aan elke IconButton, via de on_click parameter (net zoals bij de FloatingActionButton om tasks toe te voegen).
Als de edit knop aangeklikt wordt moet dus de display_view (Row) onzichtbaar gemaakt worden (visible = False) en de edit_view zichtbaar. Bij het aanklikken van de save knop net het omgekeerde.
Afhankelijke van welke knop aangeklikt is zal het Checkbox object aangepast moeten worden, of de juiste task Column verwijderd.
De nieuwe on_click functies moeten toegang hebben tot het juiste Checkbox object, om het te kunnen aanpassen of verwijderen. Ze zullen dus binnen de bestaande add_clicked functie genest moeten worden!
Om een task (Column) te verwijderen gebruik je bvb. tasks_view.controls.remove(task)
Na elke aanpassing van een control zal er opnieuw ge-update moeten worden. Het is niet nodig de volledige page te updaten (ook al zou dit wel werken) - enkel het control object dat aangepast is (bvb. view.update())
Oplossing
src/main.py
import flet as ft
def main(page: ft.Page):
def add_clicked(e):
def edit_clicked(e):
display_view.visible = False
edit_view.visible = True
view.update()
def save_clicked(e):
display_view.visible = True
edit_view.visible = False
checkbox.label = edit_text.value
view.update()
def delete_clicked(e):
tasks_view.controls.remove(task)
view.update()
checkbox = ft.Checkbox(label=new_task.value)
display_view = ft.Row(
controls=[
checkbox,
ft.Row(
spacing=0,
controls=[
ft.IconButton(
icon=ft.Icons.CREATE_OUTLINED,
on_click=edit_clicked,
),
ft.IconButton(
ft.Icons.DELETE_OUTLINE,
on_click=delete_clicked,
),
],
),
],
)
edit_text = ft.TextField(new_task.value)
edit_view = ft.Row(
visible=False,
controls=[
edit_text,
ft.IconButton(
icon=ft.Icons.DONE_OUTLINE_OUTLINED,
on_click=save_clicked,
),
],
)
task = ft.Column(controls=[display_view, edit_view])
tasks_view.controls.append(task)
new_task.value = ""
view.update()
new_task = ft.TextField(hint_text="What needs to be done?", expand=True)
tasks_view = ft.Column()
view = ft.Column(
width=600,
controls=[
ft.Row(
controls=[
new_task,
ft.FloatingActionButton(icon=ft.Icons.ADD, on_click=add_clicked),
],
),
tasks_view,
],
)
page.horizontal_alignment = ft.CrossAxisAlignment.CENTER
page.add(view)
ft.app(main)Refactoring!
De laatste code werkt wel, maar begint erg onoverzichtelijk te worden en moeilijk te begrijpen. En we zijn nog niet klaar! Tijd om te refactoren om daarna makkelijker te kunnen verderwerken.
Task
Als we het concept van een task implementeren in een eigen component (eigen control) krijgen we een handige abstractie van de verschillende views. Momenteel is een task een Column dus we kunnen hiervan vertrekken en een nieuwe Task class maken als subclass van Column.
- Een
Taskkrijgt natuurlijk de tekst van de task mee als argument, maar ook de uit te voeren functie om een task te deleten. Immers het deleten van een task wordt wel geïnitieerd vanuit de task zelf, maar wat er dan precies moet gebeuren is extern aan de task. In dit geval zal de task verwijderd moeten worden uit een lijstcontrolsvan eenColumn. - De
Taskclass kan deze functie dan aanroepen wanneer het delete icon aangeklikt wordt, en zichzelf (self) meegeven als argument. - De class kan zelf de twee
Rowobjecten (display en edit) initialiseren en instellen als zijn eigencontrols.
src/main.py
from typing import Callable
import flet as ft
class Task(ft.Column):
def __init__(self, task_text: str, task_delete: Callable):
super().__init__()
self.task_text = task_text
self.task_delete = task_delete
self.display_task = ft.Checkbox(value=False, label=self.task_text)
self.edit_name = ft.TextField()
self.display_view = ft.Row(
controls=[
self.display_task,
ft.Row(
controls=[
ft.IconButton(
icon=ft.Icons.CREATE_OUTLINED,
on_click=self.edit_clicked,
),
ft.IconButton(
ft.Icons.DELETE_OUTLINE,
on_click=self.delete_clicked,
),
],
),
],
)
self.edit_view = ft.Row(
visible=False,
controls=[
self.edit_name,
ft.IconButton(
icon=ft.Icons.DONE_OUTLINE_OUTLINED,
on_click=self.save_clicked,
),
],
)
self.controls = [self.display_view, self.edit_view]
def edit_clicked(self, e):
self.edit_name.value = self.display_task.label
self.display_view.visible = False
self.edit_view.visible = True
self.update()
def save_clicked(self, e):
self.display_task.label = self.edit_name.value
self.display_view.visible = True
self.edit_view.visible = False
self.update()
def delete_clicked(self, e):
self.task_delete(self)Probeer als oefening de bestaande main code aan te passen om gebruik te maken van deze nieuwe class. Je zult heel wat bestaande code kunnen verwijderen!
I.p.v. een Column wordt een nieuwe task nu een Task.
De functie om een task te deleten (functie die we meegeven aan elke Task) hoeven we nu niet meer te nesten binnen de add_clicked functie. We krijgen de “juiste” Task immers mee als argument.
Oplossing
src/main.py
def main(page: ft.Page):
def task_delete(task: Task):
tasks_view.controls.remove(task)
tasks_view.update()
def add_clicked(e):
task = Task(task_text=new_task.value, task_delete=task_delete)
tasks_view.controls.append(task)
new_task.value = ""
view.update()
new_task = ft.TextField(hint_text="What needs to be done?", expand=True)
tasks_view = ft.Column()
view = ft.Column(
width=600,
controls=[
ft.Row(
controls=[
new_task,
ft.FloatingActionButton(icon=ft.Icons.ADD, on_click=add_clicked),
],
),
tasks_view,
],
)
page.horizontal_alignment = ft.CrossAxisAlignment.CENTER
page.add(view)To-Do App
Ook de code van de volledige applicatie kunnen we herstructureren in een eigen class. Opnieuw kunnen we vertrekken van een Column.
De main functie wordt dan enkel nog:
src/main.py
Gebruik de Task class als inspiratie. TodoApp kan op een gelijkaardige manier zijn eigen controls definiëren in __init__ en aanpassen in andere methodes.
Functies als add_clicked en task_delete zullen methodes worden in de TodoApp class.
Oplossing voor class TodoApp
src/main.py
class TodoApp(ft.Column):
def __init__(self):
super().__init__()
self.new_task = ft.TextField(hint_text="What needs to be done?", expand=True)
self.tasks = ft.Column()
self.width = 600
self.controls = [
ft.Row(
controls=[
self.new_task,
ft.FloatingActionButton(icon=ft.Icons.ADD, on_click=self.add_clicked)
],
),
self.tasks,
]
def add_clicked(self, e):
task = Task(task_text=self.new_task.value, task_delete=self.task_delete)
self.tasks.controls.append(task)
self.new_task.value = ""
self.update()
def task_delete(self, task: Task):
self.tasks.controls.remove(task)
self.update()
def main(page: ft.Page):
page.horizontal_alignment = ft.CrossAxisAlignment.CENTER
page.add(TodoApp())Filter Tabs
Met Tabs kunnen we tabs toevoegen om de lijst met tasks die getoond worden te filteren. We willen opties om alle tasks te zien, of enkel de actieve, of enkel de afgewerkte.
Met volgende code in de Todo class voegen we de tabs toe:
src/main.py
class TodoApp(ft.Column):
def __init__(self):
super().__init__()
self.new_task = ft.TextField(hint_text="What needs to be done?", expand=True)
self.tasks = ft.Column()
self.tabs = ft.Tabs(
selected_index=0,
tabs=[ft.Tab(text="all"), ft.Tab(text="active"), ft.Tab(text="completed")],
)
self.width = 600
self.controls = [
ft.Row(
controls=[
self.new_task,
ft.FloatingActionButton(
icon=ft.Icons.ADD, on_click=self.add_clicked
),
]
),
self.tabs,
self.tasks,
]We zouden nu in elke Tab.content een verschillende Column control kunnen plaatsen met daarin de “juiste” Task objecten. Telkens een task wordt toegevoegd, aan/af-gevinkt of verwijderd moet deze dan in de juiste Column worden toegevoegd en zo nodig verwijderd.
Hier kiezen we voor een alternatieve aanpak waarbij de Tabs control enkel als filter wordt gebruikt om te bepalen welke Tasks objecten al dan niet zichtbaar moeten zijn (via de visible property).
De before_update method wordt automatisch uitgevoerd wanneer een Control wordt geupdate. We gebruiken dit in de TodoApp class om te bepalen welke Tasks zichtbaar moeten zijn, op basis van de geselecteerde tab en de huidige waarde (aangevinkt of niet) van elke Task.
src/main.py
De TodoApp class moet hier de interne structuur van Tasks inspecteren (task.display_task.value). Dat is een weinig elegante designkeuze. In de plaats willen we dat onze Task class rechtstreeks zijn status kan aangeven, bvb. via een nieuw bool completed attribuut. De Task kan dit attribuut dan updaten via de on_change parameter van de self.display_task Checkbox:
src/main.py
class Task(ft.Column):
def __init__(self, task_text: str, task_delete: Callable):
super().__init__()
self.completed: bool = False
self.display_task = ft.Checkbox(
value=False, label=self.task_text, on_change=self.status_changed
)
def status_changed(self, e):
self.completed = self.display_task.value
class TodoApp(ft.Column):
def before_update(self):
status = self.tabs.tabs[self.tabs.selected_index].text
for task in self.tasks.controls:
task.visible = (
status == "all"
or (status == "active" and not task.completed)
or (status == "completed" and task.completed)
)Deze before_update gebeurt nu enkel wanneer de TodoApp zelf wordt geupdate. Dit moet ook nog gebeuren:
- Wanneer een andere tab geselecteerd wordt. Dat kan via de
on_changeparameter van deTabscontrol. - Wanneer een individuele
TaskCheckboxwordt aan/af-gevinkt. Dat kan door een functie mee te geven aan elkeTask(net zoals detask_deletefunctie) en deze in deTaskuit te voeren wanneer de status van deCheckboxverandert.
src/main.py
class Task(ft.Column):
def __init__(self, task_text: str, task_delete: Callable, task_status_change: Callable):
super().__init__()
self.completed: bool = False
self.task_text = task_text
self.task_status_change = task_status_change
def status_changed(self, e):
self.completed = self.display_task.value
self.task_status_change()
class TodoApp(ft.Column):
def __init__(self):
self.tabs = ft.Tabs(
selected_index=0,
on_change=self.tabs_changed,
tabs=[ft.Tab(text="all"), ft.Tab(text="active"), ft.Tab(text="completed")],
)
def add_clicked(self, e):
task = Task(
task_text=self.new_task.value,
task_delete=self.task_delete,
task_status_change=self.update,
)
def tabs_changed(self, e):
self.update()De volledige code is nu:
Oplossing
src/main.py
from typing import Callable
import flet as ft
class Task(ft.Column):
def __init__(
self, task_text: str, task_delete: Callable, task_status_change: Callable
):
super().__init__()
self.completed: bool = False
self.task_text = task_text
self.task_status_change = task_status_change
self.task_delete = task_delete
self.display_task = ft.Checkbox(
value=False,
label=self.task_text,
on_change=self.status_changed,
)
self.edit_name = ft.TextField()
self.display_view = ft.Row(
controls=[
self.display_task,
ft.Row(
controls=[
ft.IconButton(icon=ft.Icons.CREATE_OUTLINED, on_click=self.edit_clicked),
ft.IconButton(icon=ft.Icons.DELETE_OUTLINE, on_click=self.delete_clicked),
],
),
],
)
self.edit_view = ft.Row(
visible=False,
controls=[
self.edit_name,
ft.IconButton( icon=ft.Icons.DONE_OUTLINE_OUTLINED, on_click=self.save_clicked),
],
)
self.controls = [self.display_view, self.edit_view]
def edit_clicked(self, e):
self.edit_name.value = self.display_task.label
self.display_view.visible = False
self.edit_view.visible = True
self.update()
def save_clicked(self, e):
self.display_task.label = self.edit_name.value
self.display_view.visible = True
self.edit_view.visible = False
self.update()
def delete_clicked(self, e):
self.task_delete(self)
def status_changed(self, e):
self.completed = self.display_task.value
self.task_status_change()
class TodoApp(ft.Column):
def __init__(self):
super().__init__()
self.new_task = ft.TextField(hint_text="What needs to be done?", expand=True)
self.tasks = ft.Column()
self.tabs = ft.Tabs(
selected_index=0,
on_change=self.tabs_changed,
tabs=[ft.Tab(text="all"), ft.Tab(text="active"), ft.Tab(text="completed")],
)
self.width = 600
self.controls = [
ft.Row(
controls=[
self.new_task,
ft.FloatingActionButton(
icon=ft.Icons.ADD, on_click=self.add_clicked
),
]
),
self.tabs,
self.tasks,
]
def add_clicked(self, e):
task = Task(
task_text=self.new_task.value,
task_delete=self.task_delete,
task_status_change=self.update,
)
self.tasks.controls.append(task)
self.new_task.value = ""
self.update()
def task_delete(self, task: Task):
self.tasks.controls.remove(task)
self.update()
def tabs_changed(self, e):
self.update()
def before_update(self):
status = self.tabs.tabs[self.tabs.selected_index].text
for task in self.tasks.controls:
task.visible = (
status == "all"
or (status == "active" and not task.completed)
or (status == "completed" and task.completed)
)
def main(page: ft.Page):
# page.title = "To-Do App"
page.horizontal_alignment = ft.CrossAxisAlignment.CENTER
page.add(TodoApp())
page.update()
ft.app(main)Final touches
Tabs uitlijnen
We willen de 3 tabs graag gespreid over de breedte van de app. Dat kan met de Tabs.scrollable parameter.
Tasks uitlijnen
De tasks kunnen wat beter uitgelijnd worden, zodat de edit, delete en save iconen altijd op dezelfde plaats staan.
Dit kan simpelweg door de expand=True parameter toe te voegen aan de Checkbox en TextField componenten in de Task class.
src/main.py
We willen ook vermijden dat de tekst van een Task overloopt op de IconButtons er naast. De CheckBox control zelf maar een beperkt aantal opties. Om dit op te lossen kan de CheckBox in een Container control geplaatst worden met de parameter clip_behavior=ft.ClipBehavior.HARD_EDGE. Hierdoor wordt de control “afgesneden” indien er niet genoeg plaats is. expand=True is ook nodig zodat de Container de beschikbare breedte inneemt (en niet meer).
Icon tool-tips en kleur
We IconButton componenten kunnen we een tool-tip meegeven met de tooltip parameter. Bvb. tooltip="Edit To-Do" voor de edit knop.
Het icoon voor de delete knop willen we graag in het rood en de save knop in het groen. Daarvoor gebruiken we de icon_color parameter met een waarde van de ft.Colors enum.
src/main.py
self.display_view = ft.Row(
controls=[
self.display_task,
ft.Row(
controls=[
ft.IconButton(
icon=ft.Icons.CREATE_OUTLINED,
tooltip="Edit To-Do",
on_click=self.edit_clicked,
),
ft.IconButton(
icon=ft.Icons.DELETE_OUTLINE,
icon_color=ft.Colors.RED,
tooltip="Delete To-Do",
on_click=self.delete_clicked,
),
],
),
],
)
self.edit_view = ft.Row(
visible=False,
controls=[
self.edit_name,
ft.IconButton(
icon=ft.Icons.DONE_OUTLINE_OUTLINED,
icon_color=ft.Colors.GREEN,
tooltip="Update To-Do",
on_click=self.save_clicked,
),
],
)Enter submit & Focus
Bij het ingeven van een nieuwe task is het handig als we deze kunnen toevoegen met de Enter toets, i.p.v. op de + knop te moeten klikken. Het self.new_task TextField heeft een on_submit parameter die we hiervoor kunnen gebruiken.
Daarnaast willen we na het toevoegen onmiddellijk een volgende task kunnen ingeven. Hiervoor moeten we op het gepaste moment de focus() methode van het self.new_task TextField aanroepen.
Complete code
Oplossing
src/main.py
from typing import Callable
import flet as ft
class Task(ft.Column):
def __init__(
self, task_text: str, task_delete: Callable, task_status_change: Callable
):
super().__init__()
self.completed: bool = False
self.task_text = task_text
self.task_status_change = task_status_change
self.task_delete = task_delete
self.display_task = ft.Checkbox(
value=False,
label=self.task_text,
on_change=self.status_changed,
expand=True,
)
self.edit_name = ft.TextField(expand=True)
self.display_view = ft.Row(
controls=[
ft.Container(
content=self.display_task,
expand=True,
clip_behavior=ft.ClipBehavior.HARD_EDGE,
),
ft.Row(
controls=[
ft.IconButton(
icon=ft.Icons.CREATE_OUTLINED,
tooltip="Edit To-Do",
on_click=self.edit_clicked,
),
ft.IconButton(
icon=ft.Icons.DELETE_OUTLINE,
icon_color=ft.Colors.RED,
tooltip="Delete To-Do",
on_click=self.delete_clicked,
),
],
),
],
)
self.edit_view = ft.Row(
visible=False,
controls=[
self.edit_name,
ft.IconButton(
icon=ft.Icons.DONE_OUTLINE_OUTLINED,
icon_color=ft.Colors.GREEN,
tooltip="Update To-Do",
on_click=self.save_clicked,
),
],
)
self.controls = [self.display_view, self.edit_view]
def edit_clicked(self, e):
self.edit_name.value = self.display_task.label
self.display_view.visible = False
self.edit_view.visible = True
self.update()
def save_clicked(self, e):
self.display_task.label = self.edit_name.value
self.display_view.visible = True
self.edit_view.visible = False
self.update()
def delete_clicked(self, e):
self.task_delete(self)
def status_changed(self, e):
self.completed = self.display_task.value
self.task_status_change()
class TodoApp(ft.Column):
def __init__(self):
super().__init__()
self.new_task = ft.TextField(
hint_text="What needs to be done?", on_submit=self.add_clicked, expand=True
)
self.tasks = ft.Column()
self.tabs = ft.Tabs(
scrollable=False,
selected_index=0,
on_change=self.tabs_changed,
tabs=[ft.Tab(text="all"), ft.Tab(text="active"), ft.Tab(text="completed")],
)
self.width = 600
self.controls = [
ft.Row(
controls=[
self.new_task,
ft.FloatingActionButton(
icon=ft.Icons.ADD, on_click=self.add_clicked
),
]
),
self.tabs,
self.tasks,
]
def add_clicked(self, e):
task = Task(
task_text=self.new_task.value,
task_delete=self.task_delete,
task_status_change=self.update,
)
self.tasks.controls.append(task)
self.new_task.value = ""
self.new_task.focus()
self.update()
def task_delete(self, task: Task):
self.tasks.controls.remove(task)
self.update()
def tabs_changed(self, e):
self.update()
def before_update(self):
status = self.tabs.tabs[self.tabs.selected_index].text
for task in self.tasks.controls:
task.visible = (
status == "all"
or (status == "active" and not task.completed)
or (status == "completed" and task.completed)
)
def main(page: ft.Page):
page.title = "To-Do App"
page.horizontal_alignment = ft.CrossAxisAlignment.CENTER
page.add(TodoApp())
page.update()
ft.app(main)
