Theming trong Flet
Học cách tùy chỉnh giao diện ứng dụng với theme và styling. ⏱️ 15 phút
🎯 Mục tiêu
Nắm vững:
- Theme mode (Light/Dark)
- Custom ColorScheme
- Typography và Font
- Control-level styling
🌗 Theme Mode với Declarative UI
Flet hỗ trợ 3 theme modes. Với Declarative UI, ta quản lý qua state:
import flet as ft
@ft.observable
class ThemeState:
is_dark: bool = False
def toggle(self):
self.is_dark = not self.is_dark
state = ThemeState()
@ft.component
def ThemeToggle():
return ft.Switch(
label="Dark Mode",
value=state.is_dark,
on_change=lambda _: state.toggle(),
)
@ft.component
def ThemeApp():
return ft.Column([
ThemeToggle(),
ft.Text("Hello Flet!", size=24),
])
def main(page: ft.Page):
# Thiết lập theme dựa trên state
page.theme_mode = ft.ThemeMode.DARK if state.is_dark else ft.ThemeMode.LIGHT
page.add(ThemeApp())
ft.run(main)🎨 Custom Color Scheme
import flet as ft
# Định nghĩa themes
LIGHT_THEME = ft.Theme(
color_scheme=ft.ColorScheme(
primary=ft.Colors.PURPLE,
secondary=ft.Colors.ORANGE,
surface=ft.Colors.WHITE,
background=ft.Colors.GREY_100,
error=ft.Colors.RED,
),
)
DARK_THEME = ft.Theme(
color_scheme=ft.ColorScheme(
primary=ft.Colors.PURPLE_200,
secondary=ft.Colors.ORANGE_200,
surface=ft.Colors.GREY_900,
background=ft.Colors.BLACK,
),
)
@ft.observable
class ThemeState:
is_dark: bool = False
def toggle(self):
self.is_dark = not self.is_dark
state = ThemeState()
@ft.component
def ButtonsPreview():
return ft.Row([
ft.FilledButton("Filled"),
ft.ElevatedButton("Elevated"),
ft.OutlinedButton("Outlined"),
], wrap=True)
@ft.component
def ThemeDemo():
return ft.Column([
ft.Switch(
label="Dark Mode",
value=state.is_dark,
on_change=lambda _: state.toggle(),
),
ft.Divider(),
ft.Text("Buttons Preview", weight=ft.FontWeight.BOLD),
ButtonsPreview(),
], spacing=20)
def main(page: ft.Page):
page.title = "Theme Demo"
page.theme = LIGHT_THEME
page.dark_theme = DARK_THEME
page.theme_mode = ft.ThemeMode.DARK if state.is_dark else ft.ThemeMode.LIGHT
page.padding = 30
page.add(ThemeDemo())
ft.run(main)🔤 Typography
Custom fonts và text styles:
import flet as ft
@ft.component
def TypographyDemo():
return ft.Column([
ft.Text("Default font", size=16),
ft.Text("Bold Text", weight=ft.FontWeight.BOLD, size=20),
ft.Text("Italic Text", italic=True, size=18),
ft.Text("Colored Text", color=ft.Colors.BLUE, size=18),
], spacing=10)
def main(page: ft.Page):
# Thêm custom font (đặt file .ttf trong thư mục assets)
page.fonts = {
"Roboto": "fonts/Roboto-Regular.ttf",
"RobotoBold": "fonts/Roboto-Bold.ttf",
}
# Hoặc Google Fonts (tự động tải)
page.fonts = {
"Kanit": "Kanit",
"Open Sans": "Open Sans",
}
page.theme = ft.Theme(font_family="Kanit")
page.add(TypographyDemo())
ft.run(main)🎛️ Control-level Styling
Styled Button Component
import flet as ft
@ft.component
def StyledButton(text: str, color=ft.Colors.GREEN):
return ft.ElevatedButton(
text,
style=ft.ButtonStyle(
color=ft.Colors.WHITE,
bgcolor=color,
padding=20,
shape=ft.RoundedRectangleBorder(radius=10),
elevation=5,
),
)
@ft.component
def ButtonsRow():
return ft.Row([
StyledButton("Success", ft.Colors.GREEN),
StyledButton("Warning", ft.Colors.ORANGE),
StyledButton("Danger", ft.Colors.RED),
], spacing=10)
def main(page: ft.Page):
page.add(ButtonsRow())
ft.run(main)Styled Container Component
import flet as ft
@ft.component
def StyledCard(title: str, content: str):
return ft.Container(
content=ft.Column([
ft.Text(title, weight=ft.FontWeight.BOLD, size=18),
ft.Text(content, color=ft.Colors.GREY_600),
], spacing=5),
width=250,
padding=20,
bgcolor=ft.Colors.BLUE_50,
border=ft.border.all(2, ft.Colors.BLUE),
border_radius=15,
shadow=ft.BoxShadow(
blur_radius=10,
spread_radius=2,
color=ft.Colors.BLACK26,
),
)
@ft.component
def GradientCard(title: str):
return ft.Container(
content=ft.Text(title, color=ft.Colors.WHITE, size=20),
width=200,
height=100,
alignment=ft.alignment.center,
border_radius=15,
gradient=ft.LinearGradient(
begin=ft.alignment.top_left,
end=ft.alignment.bottom_right,
colors=[ft.Colors.BLUE_400, ft.Colors.PURPLE_400],
),
)
@ft.component
def CardsDemo():
return ft.Column([
StyledCard("Card Title", "This is card content"),
GradientCard("Gradient Card"),
], spacing=20)
def main(page: ft.Page):
page.padding = 30
page.add(CardsDemo())
ft.run(main)🎯 Complete Theme Switcher
import flet as ft
@ft.observable
class ThemeState:
is_dark: bool = False
primary_color: str = "indigo"
colors_map = {
"indigo": (ft.Colors.INDIGO, ft.Colors.INDIGO_200),
"green": (ft.Colors.GREEN, ft.Colors.GREEN_200),
"purple": (ft.Colors.PURPLE, ft.Colors.PURPLE_200),
"orange": (ft.Colors.ORANGE, ft.Colors.ORANGE_200),
}
def toggle_mode(self):
self.is_dark = not self.is_dark
def set_color(self, color: str):
self.primary_color = color
def get_primary(self):
light, dark = self.colors_map.get(self.primary_color, (ft.Colors.INDIGO, ft.Colors.INDIGO_200))
return dark if self.is_dark else light
state = ThemeState()
@ft.component
def ColorPicker():
colors = ["indigo", "green", "purple", "orange"]
return ft.Row([
ft.Container(
width=40,
height=40,
bgcolor=state.colors_map[c][0],
border_radius=20,
border=ft.border.all(3, ft.Colors.WHITE) if state.primary_color == c else None,
on_click=lambda _, color=c: state.set_color(color),
)
for c in colors
], spacing=10)
@ft.component
def ComponentsPreview():
return ft.Column([
ft.Text("Preview", size=20, weight=ft.FontWeight.BOLD),
ft.Divider(),
ft.Row([
ft.FilledButton("Filled"),
ft.ElevatedButton("Elevated"),
ft.OutlinedButton("Outlined"),
], wrap=True),
ft.TextField(label="Sample Input"),
ft.Slider(value=60),
ft.ProgressBar(value=0.7),
ft.Card(
content=ft.Container(
content=ft.ListTile(
leading=ft.Icon(ft.Icons.PERSON),
title=ft.Text("John Doe"),
subtitle=ft.Text("[email protected]"),
),
padding=10,
),
),
], spacing=15)
@ft.component
def ThemeSettings():
return ft.Card(
content=ft.Container(
content=ft.Column([
ft.Text("Theme Settings", size=20, weight=ft.FontWeight.BOLD),
ft.Divider(),
ft.Row([
ft.Icon(ft.Icons.DARK_MODE if state.is_dark else ft.Icons.LIGHT_MODE),
ft.Text("Dark Mode"),
ft.Container(expand=True),
ft.Switch(
value=state.is_dark,
on_change=lambda _: state.toggle_mode(),
),
]),
ft.Text("Primary Color", weight=ft.FontWeight.W_500),
ColorPicker(),
]),
padding=20,
),
)
@ft.component
def ThemeSwitcherApp():
return ft.Row([
ft.Container(content=ThemeSettings(), width=300),
ft.VerticalDivider(),
ft.Container(content=ComponentsPreview(), expand=True, padding=20),
], expand=True)
def main(page: ft.Page):
page.title = "Theme Switcher"
page.padding = 20
# Dynamic theme based on state
page.theme = ft.Theme(color_scheme=ft.ColorScheme(primary=state.get_primary()))
page.dark_theme = ft.Theme(color_scheme=ft.ColorScheme(primary=state.get_primary()))
page.theme_mode = ft.ThemeMode.DARK if state.is_dark else ft.ThemeMode.LIGHT
page.add(ThemeSwitcherApp())
ft.run(main)📋 ColorScheme Properties
| Property | Description |
|---|---|
primary | Màu chính (buttons, links) |
secondary | Màu phụ |
surface | Màu surfaces (cards) |
background | Màu nền |
error | Màu lỗi |
on_primary | Text trên primary |
on_surface | Text trên surface |
⏭️ Tiếp theo
Tuyệt! Tiếp tục với File Operations để làm việc với files.
Last updated on