-
-
Notifications
You must be signed in to change notification settings - Fork 0
How Programming Works
CTkMaker splits a CustomTkinter app into two layers — a visual layer you design on the canvas, and a behavior layer you write in plain Python. This page is the single map of how the two connect.
| Layer | What it is | Where it lives | Who writes it |
|---|---|---|---|
| Visual layer | Widgets, properties, layout, bindings |
.ctkproj files inside assets/pages/
|
You — by dragging on the canvas + editing Properties |
| Behavior layer | Methods that run when events fire |
.py files inside assets/scripts/<page>/
|
You — by hand, in your editor of choice |
The two layers are wired together by widget names. A widget called submit_btn in the builder becomes self.submit_btn in the generated code; an event bound to on_submit calls self._behavior.on_submit at runtime. Rename safely from inside the builder — both sides update together.
Four building blocks cover every piece of behavior wiring. Pick the right one for the right job.
| Ingredient | Purpose | Scope | Detailed page |
|---|---|---|---|
| Variables | Shared values that several widgets read or write together (entry text, switch state, accent color). Live two-way for Tk-native bindings, live one-way for cosmetic ones. | Global = whole page · Local = one window | Variables |
| Object References | Typed pointers that let one handler reach a widget — or another window — by a stable Python name. | Local = a widget inside the form · Global = a Window or Dialog | Object References |
| Event Handlers | Methods invoked when the user interacts with a widget (click, keypress, value change). The bridge from the visual layer into the behavior layer. | One method on the window's behavior class — or a library function — per bound event | Event Handlers |
| Library Scripts | Shared helpers across the windows on a page (helpers.py, services/auth.py). Imported by behavior files; can also be called directly from events when attached. |
Page-scoped, one folder per page | Scripts Panel |
| Goal | Reach for |
|---|---|
| Two widgets must show the same value | Variable — bind both to the same one |
| Theme color shared across the page |
Global color variable + bind every relevant fg_color / text_color
|
| Handler needs to grab a value out of an Entry |
Object Reference on the Entry; call self.username_entry.get()
|
| Click a Button in window A to open dialog B |
Global Object Reference on dialog B — self.b_dialog(self.window)
|
| Reusable validator / API client |
Library script + from . import helpers in the behavior file |
| Make a Button call a library function directly without writing a stub | Library function as event target — pick it from the event picker's Library tab (the script must be attached to the window first) |
The four ingredients in one window. The visual layer holds two Entries (username_entry, password_entry), one Button (submit_btn), one Label (status_label), one Switch (remember_switch), and a Toplevel dialog (ForgotPasswordDialog).
The behavior file at assets/scripts/login/login.py:
from __future__ import annotations
from typing import Generic, TypeVar, TYPE_CHECKING
from . import helpers # Library script
if TYPE_CHECKING:
import customtkinter as ctk
import tkinter as tk
T = TypeVar("T")
class ref(Generic[T]): ...
class LoginPage:
# Object References (declared in the Properties panel; annotations stay in sync)
username_entry: ref["CTkEntry"] = ref()
password_entry: ref["CTkEntry"] = ref()
status_label: ref["CTkLabel"] = ref()
forgot_dialog: ref["ForgotPasswordDialog"] = ref() # global ref
def setup(self, window: "ctk.CTk | ctk.CTkToplevel") -> None:
self.window = window # gives access to variables on the host class
# Event Handler — bound to submit_btn.command
def on_submit(self) -> None:
u = self.username_entry.get()
if not helpers.validate_email(u): # Library function call
self.status_label.configure(text="Invalid email")
return
# Variable read — var_remember is a local bool variable bound to remember_switch
if self.window.var_remember.get():
helpers.save_credentials(u, self.password_entry.get())
self.window.var_signed_in.set(True) # Variable write — propagates everywhere bound
# Event Handler — bound to "Forgot password?" link's <Button-1>
def on_forgot_click(self, event: "tk.Event | None" = None) -> None:
self.forgot_dialog(self.window) # instantiate the dialog via global refThe scaffold generates the typed signatures, from __future__ import annotations, and the TYPE_CHECKING block for you — those lines are not hand-written either.
Notice each ingredient pulling its weight:
| Line | Ingredient at work |
|---|---|
self.username_entry.get() |
Local Object Reference |
helpers.validate_email(...) |
Library Script (imported) |
self.window.var_remember.get() |
Local Variable (read) |
self.window.var_signed_in.set(True) |
Local Variable (write — fans out to every bound widget) |
self.forgot_dialog(self.window) |
Global Object Reference (Dialog target) |
on_submit itself |
Event Handler |
You did not write the wiring code (__init__, _build_ui, the command= kwarg, the import statement, the setup(self, window) call). The exporter does that — see Exporting Code.
What CTkMaker emits when you export the page:
from assets.scripts.login.login import LoginPage # behavior file
from assets.scripts.login import helpers # auto-emitted when a Library function is bound
class MainWindow(ctk.CTk):
def __init__(self):
super().__init__()
# Variables — both globals and locals declared on the main class
self.var_remember = tk.BooleanVar(value=False)
self.var_signed_in = tk.BooleanVar(value=False)
self._behavior = LoginPage()
self._build_ui()
# Object References wired AFTER _build_ui:
self._behavior.username_entry = self.username_entry
self._behavior.password_entry = self.password_entry
self._behavior.status_label = self.status_label
self._behavior.forgot_dialog = ForgotPasswordDialog
self._behavior.setup(self)
def _build_ui(self):
# Event Handlers become command= and bind() calls:
self.submit_btn = ctk.CTkButton(
self, text="Sign In",
command=self._behavior.on_submit,
)
# Variable bindings become textvariable= / variable= kwargs (Tier A)
# plus _bind_var_to_*(...) helpers for cosmetic ones (Tier B / C).
...The behavior file imports cleanly. There is no glue code on your side. Everything you set up visually surfaces in the generated .py exactly where you'd expect a hand-written app to put it.
New projects ship with three files at the project root:
| File | Why |
|---|---|
requirements.txt |
Pins customtkinter so a fresh .venv + pip install -r reproduces the runtime. |
pyrightconfig.json |
Points extraPaths at the live customtkinter install so Pyright / Pylance resolve ctk.CTk and tk.Event annotations in your behavior files the moment you open them — no .venv setup required for type checking alone. |
.gitignore |
Common Python ignores (__pycache__/, .venv/, …). |
Existing projects created before v1.41.4 get the same three files auto-written on first open — idempotent, so any customisations you've added are preserved.
The point: the moment you open a behavior file in VS Code, Pylance shows green annotations for the typed signatures in the scaffold. Your hand-written logic sits inside a file the IDE already understands.
| If you want to… | Read |
|---|---|
| Learn each ingredient in depth | Variables · Object References · Event Handlers · Scripts Panel |
| See the full property schema for the widgets you're using | Widgets |
| Understand what the exporter writes | Exporting Code |
| Run the project as you build | Preview |
See also — Home · Getting Started · Window Properties