Dashboard Layout
Dashboard Layout
In the previous lesson you built the login screen.
Now you will build the dashboard layout that the user sees after logging in.
The dashboard will not be a simple stack of labels. It will use a real layout:
- a header bar at the top
- a row of colored stat cards showing summary numbers
- two side-by-side panels for tasks and expenses
You are still not connecting backend logic — you are building the structure first.
Goal of This Lesson
By the end of this lesson, you will:
- understand
grid()for two-column layouts - build a header bar
- build colored stat cards
- build reusable panel containers
- render the full dashboard structure
Final Layout
+---------------------------------------------------+
| Header (app title, welcome, logout) |
+---------------------------------------------------+
| [Total Tasks] [Completed] [Expenses] <- cards |
+---------------------------------------------------+
| Tasks panel | Expenses panel |
| | |
| | |
+---------------------------------------------------+
Step 1: Add More Theme Colors
The stat cards each use a soft accent color.
Open app_ui.py and extend THEME:
THEME = {
# ... existing entries ...
"accent_blue": "#dbeafe",
"accent_green": "#dcfce7",
"accent_amber": "#fef3c7",
"accent_blue_fg": "#1e40af",
"accent_green_fg": "#166534",
"accent_amber_fg": "#92400e",
}
Also add two more font constants:
FONT_STAT = ("Segoe UI", 22, "bold")
FONT_STAT_LBL = ("Segoe UI", 10)
Step 2: Build a Stat Card Helper
A stat card shows a big number and a small label, with an accent background.
def _build_stat_card(parent, label, value, accent_bg, accent_fg):
card = tk.Frame(
parent, bg=accent_bg,
highlightbackground=THEME["border"], highlightthickness=1,
)
inner = tk.Frame(card, bg=accent_bg)
inner.pack(fill="both", expand=True, padx=20, pady=18)
value_lbl = tk.Label(
inner, text=value, font=FONT_STAT,
bg=accent_bg, fg=accent_fg, anchor="w",
)
value_lbl.pack(anchor="w")
tk.Label(
inner, text=label, font=FONT_STAT_LBL,
bg=accent_bg, fg=accent_fg, anchor="w",
).pack(anchor="w", pady=(2, 0))
return {"card": card, "value": value_lbl}
The function returns a dictionary with two things:
card— the outer frame, so you can place it on the dashboardvalue— the big number label, so you can update it later
This pattern lets you change the stat value later without rebuilding the whole card.
Step 3: Build a Panel Helper
Panels are white containers with a title and a body area.
def _build_panel(parent, title):
card = tk.Frame(
parent, bg=THEME["card"],
highlightbackground=THEME["border"], highlightthickness=1,
)
title_bar = tk.Frame(card, bg=THEME["card"])
title_bar.pack(fill="x", padx=16, pady=(14, 6))
tk.Label(
title_bar, text=title, font=FONT_HEAD,
bg=THEME["card"], fg=THEME["text"],
).pack(side="left")
body = tk.Frame(card, bg=THEME["card"])
body.pack(fill="both", expand=True, padx=16, pady=(0, 14))
return {"card": card, "body": body}
Returns:
card— the outer panelbody— the inner frame where task/expense widgets will go
Step 4: Build the Dashboard
def show_dashboard(window, user_id, username=""):
_clear(window)
root = tk.Frame(window, bg=THEME["bg"])
root.pack(fill="both", expand=True)
# ---- Header bar ---------------------------------------------------
header = tk.Frame(
root, bg=THEME["card"], height=64,
highlightbackground=THEME["border"], highlightthickness=1,
)
header.pack(fill="x")
header.pack_propagate(False)
tk.Label(
header, text="📊 Productivity App", font=FONT_HEAD,
bg=THEME["card"], fg=THEME["text"],
).pack(side="left", padx=20)
welcome_text = f"Welcome, {username}" if username else "Welcome"
tk.Label(
header, text=welcome_text, font=FONT_LABEL,
bg=THEME["card"], fg=THEME["muted"],
).pack(side="left", padx=10)
_styled_button(
header, "Logout", lambda: show_login(window), kind="ghost"
).pack(side="right", padx=20, pady=14)
# ---- Content area -------------------------------------------------
content = tk.Frame(root, bg=THEME["bg"])
content.pack(fill="both", expand=True, padx=20, pady=20)
# ---- Stat cards row -----------------------------------------------
stats_row = tk.Frame(content, bg=THEME["bg"])
stats_row.pack(fill="x", pady=(0, 16))
# Make all three columns equal width.
for i in range(3):
stats_row.columnconfigure(i, weight=1, uniform="stats")
stat_total = _build_stat_card(stats_row, "Total Tasks", "0",
THEME["accent_blue"], THEME["accent_blue_fg"])
stat_done = _build_stat_card(stats_row, "Completed Tasks", "0",
THEME["accent_green"], THEME["accent_green_fg"])
stat_expense = _build_stat_card(stats_row, "Total Expenses", "$0",
THEME["accent_amber"], THEME["accent_amber_fg"])
stat_total["card"].grid( row=0, column=0, sticky="nsew", padx=(0, 8))
stat_done["card"].grid( row=0, column=1, sticky="nsew", padx=8)
stat_expense["card"].grid(row=0, column=2, sticky="nsew", padx=(8, 0))
# ---- Two-column panels --------------------------------------------
panels = tk.Frame(content, bg=THEME["bg"])
panels.pack(fill="both", expand=True)
panels.columnconfigure(0, weight=1, uniform="panel")
panels.columnconfigure(1, weight=1, uniform="panel")
panels.rowconfigure(0, weight=1)
task_panel = _build_panel(panels, title="Tasks")
task_panel["card"].grid(row=0, column=0, sticky="nsew", padx=(0, 8))
expense_panel = _build_panel(panels, title="Expenses")
expense_panel["card"].grid(row=0, column=1, sticky="nsew", padx=(8, 0))
return root
How grid() Works Here
Two key ideas:
1. columnconfigure(..., weight=1, uniform="...")
This says: “all columns with this uniform group should share width equally.”
Without it, columns would size based on their content.
2. sticky="nsew"
This makes a widget stretch to fill its grid cell in north, south, east, west directions.
Without it, widgets shrink to their natural size.
Step 5: Test the Dashboard
For now, your dashboard is empty — the panels have titles but no content.
Update main.py temporarily so you can see the dashboard directly:
from app.services.database import create_tables
from app.ui.app_ui import create_app, show_dashboard
def main():
create_tables()
window = create_app()
show_dashboard(window, user_id=1, username="demo")
window.mainloop()
if __name__ == "__main__":
main()
Run it. You should see:
- white header bar at the top
- three colored stat cards (blue, green, amber)
- two empty white panels labeled “Tasks” and “Expenses”
Step 6: Restore main.py
Once you’ve confirmed the layout works, restore main.py:
from app.services.database import create_tables
from app.ui.app_ui import create_app, show_login
def main():
create_tables()
window = create_app()
show_login(window)
window.mainloop()
if __name__ == "__main__":
main()
What You Built
You now have:
- a full dashboard layout
- reusable helpers (
_build_stat_card,_build_panel) - a header bar with logout button
- a structure ready for tasks and expenses
The panels are empty for now. You will fill them in the next lesson.
Why This Step Matters
This is the difference between:
hobby project → real product layout
You used:
pack()for top-to-bottom flowgrid()for rows and columnsplace()(in the previous lesson) for centering- helper functions for reuse
These are the same techniques real Tkinter apps use.
Important Notes
- Do not write logic inside layout functions
- Keep helpers (
_build_stat_card,_build_panel) small and reusable - Always use
sticky="nsew"when you want a widget to fill its grid cell - Always call
columnconfigure(..., weight=1)when you want columns to share space - Keep the theme dictionary as the only place colors live
Current Project Structure
app/
services/
database.py
user_service.py
task_service.py
expense_service.py
dashboard_service.py
ui/
app_ui.py
Summary
You built the full dashboard layout with a header bar, colored stat cards, and two-column panels using grid(). Your application now has a real, modern UI structure ready to be filled with functionality.