Skip to content

How Programming Works

Lasha Kandelaki edited this page May 19, 2026 · 2 revisions

How Programming Works in CTkMaker

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.

The two-layer model

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.

The four ingredients

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

When to use which

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)

A worked Login example

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 ref

The 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.

How export ties it together

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.

IDE-friendly scaffold

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.

Where to go next

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 alsoHome · Getting Started · Window Properties

Clone this wiki locally