🎨 Theming
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
Phần tiêu đề “🎯 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
Phần tiêu đề “🌗 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.observableclass ThemeState: is_dark: bool = False
def toggle(self): self.is_dark = not self.is_dark
state = ThemeState()
@ft.componentdef ThemeToggle(): return ft.Switch( label="Dark Mode", value=state.is_dark, on_change=lambda _: state.toggle(), )
@ft.componentdef 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
Phần tiêu đề “🎨 Custom Color Scheme”import flet as ft
# Định nghĩa themesLIGHT_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.observableclass ThemeState: is_dark: bool = False
def toggle(self): self.is_dark = not self.is_dark
state = ThemeState()
@ft.componentdef ButtonsPreview(): return ft.Row([ ft.FilledButton("Filled"), ft.ElevatedButton("Elevated"), ft.OutlinedButton("Outlined"), ], wrap=True)
@ft.componentdef 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
Phần tiêu đề “🔤 Typography”Custom fonts và text styles:
import flet as ft
@ft.componentdef 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
Phần tiêu đề “🎛️ Control-level Styling”Styled Button Component
Phần tiêu đề “Styled Button Component”import flet as ft
@ft.componentdef 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.componentdef 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
Phần tiêu đề “Styled Container Component”import flet as ft
@ft.componentdef 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.componentdef 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.componentdef 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
Phần tiêu đề “🎯 Complete Theme Switcher”import flet as ft
@ft.observableclass 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.componentdef 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.componentdef 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"), ), padding=10, ), ), ], spacing=15)
@ft.componentdef 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.componentdef 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
Phần tiêu đề “📋 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
Phần tiêu đề “⏭️ Tiếp theo”Tuyệt! Tiếp tục với File Operations để làm việc với files.