Design & Architecture

This document explains how myTk is built and why it is built that way. It sits between the narrative README and the per-module API Reference: read the README to see what myTk does, read this to understand its structure, then dive into the API for details.

Design goals

myTk exists to make Tkinter pleasant for scientists and other non-professional programmers who need a working desktop GUI without adopting a heavy framework. Five goals drive every design decision:

G1 — Stay simple and portable.

Tk ships with Python and is portable across macOS, Windows and Linux. There is nothing to install to get a window on screen, and apps move between machines and operating systems without surprises. This is the single biggest reason myTk is built on Tk rather than Qt or wxWidgets.

G2 — Bring modern macOS/Cocoa patterns to Tk.

Raw Tkinter scatters behaviour across loose callbacks. myTk borrows the patterns Apple matured over decades — Key-Value Observing (binding, bindable), NSNotificationCenter (notifications), and delegation — because they organize event-driven code far better than ad-hoc callbacks.

G3 — Encapsulate, but never hide.

Every Tk widget is wrapped in a View that exposes convenient behaviour, yet the underlying Tk widget is always reachable via .widget for anything myTk does not cover. You are never boxed in and limited.

G4 — Remove friction.

Where Tk forces long verbose text for no reason, myTk supplies a sensible default and lets you override it. For example, Tk requires you to name a widget’s parent at creation; myTk does not. Complex widgets like TableView work out of the box and let you refine behaviour through a delegate rather than wiring callbacks by hand.

G5 — Keep useful but less portable features optional.

3D rendering (View3D) and OS drag-and-drop pull in large or platform-specific dependencies. These are loaded on demand and degrade gracefully: if the dependency is absent, the feature reports itself unavailable and the rest of the app keeps running.

Architectural overview

myTk is a thin, layered wrapper around Tk. From the bottom up:

  • Bindable — the foundation. A property-observer and two-way binding mechanism with no Tk dependency of its own.

  • Base — combines Bindable with two mixins (EventCapable, DragAndDropCapable) to give every widget a uniform interface for state, grid geometry, event binding and lifecycle.

  • View and its many subclasses — everything visible on screen (except the window) is a View that wraps one Tk widget.

  • App and Window — the application controller and the top-level container.

The complete class hierarchy (regenerate any time with python -m mytk -c, which emits this Graphviz source). Solid arrows are subclassing; dashed arrows are the mixins folded into Base and App:

myTk class hierarchy

myTk class hierarchy (python -m mytk -c)

Note that App is not a Base/View: it is a controller, not something drawn on screen, so it derives from Bindable and EventCapable directly. Likewise TabularData is a pure model and derives only from Bindable.

Core mechanisms

These six mechanisms are the framework. Everything else is widgets built on top of them.

Binding and property observation

Bindable implements a Property-Value-Observer pattern inspired by macOS Key-Value Observing. Any object can observe a property on another object, and two properties can be bound so that changing one always updates the other, whether the change comes through the interface or from your own code.

It works uniformly on plain Python attributes and on Tk Variable objects, so reactive GUIs do not require you to think about StringVar/IntVar plumbing. This is the mechanism to reach for when two pieces of state must stay equal.

Notifications

NotificationCenter is a singleton providing one-to-many, decoupled communication, modelled on NSNotificationCenter. A notifier posts a named notification without knowing who, if anyone, is listening; observers subscribe by name.

Use notifications (rather than binding) when the relationship is one-to-many or when the sender should know nothing about the receivers — e.g. a device announcing will_move / did_move to whatever cares.

Delegation

For complex widgets, myTk prefers a delegate object over a pile of callbacks. The widget implements sensible default behaviour and calls optional methods on your delegate when it needs a decision. TableView is the canonical example; a delegate may implement any subset of:

  • selection_changed(event)

  • click_header(column) — default: sort rows by that column

  • click_cell(item_id, column_id) — default: open the value if it is a URL

  • doubleclick_header(column) / doubleclick_cell(item_id, column_id)

This keeps related behaviour in one cohesive object and lets you override only what you care about.

Events and scheduling

EventCapable is a mixin (on both Base and App) for timed callbacks and event wiring: after(), after_cancel() (and after_cancel_all), bind_event(), and event_generate(). It exists so that timer and event management is consistent across widgets and the application object, which otherwise handle their Tk widget very differently.

Configuration

Scientific applications are full of adjustable parameters — exposure time, gain, wavelength, thresholds, paths. Configurable turns a declared set of typed, validated properties into a working settings dialog automatically:

Drag and drop

OS-level file drops are not part of core Tk. DragAndDropCapable is a per-widget mixin (mirroring EventCapable) that adds accept_dropped_files(); the low-level work of loading the tkdnd extension and parsing the payload lives in mytk.dnd. The dependency (tkinterdnd2/tkdnd) is installed on demand the first time it is used. If it cannot be enabled, is_drag_and_drop_available() returns False and the app continues without drops.

Data layer (model/view)

myTk separates tabular data from its display in a small MVC arrangement:

Editing a cell, sorting, resizing columns and following URL cells are handled for you; the model stays the single source of truth.

Visualization

  • Figure embeds a Matplotlib figure (provide your own plt.figure or use the one it creates) with an optional toolbar.

  • View3D embeds a 3D mesh viewer. It loads GLB/GLTF/OBJ/PLY/STL via trimesh, renders off-screen (drag to orbit, scroll to zoom), and picks its backend automatically: pyrender if available, otherwise a built-in moderngl renderer. You may instantiate View3DPyrender or View3DModernGL directly. Like drag-and-drop, the heavy dependencies are optional and loaded on demand.

Layout model

Positioning is the part of Tk that confuses newcomers most, so it is worth stating plainly. Tk offers three geometry managers — grid, pack and place — and myTk standardizes on grid. A view is divided into a grid of rows and columns, and children are placed into cells with grid_into.

Key levers:

  • Resizing is governed by per-row/column weight (which cells absorb extra space) and by each widget’s sticky option (whether the widget grows to fill its cell).

  • grid_propagate controls whether a container resizes to fit its children or holds a fixed size.

  • rowspan / columnspan let a widget occupy a range of cells.

A View can itself contain a grid, so a single grid cell can hold a self-contained sub-layout. Recommended background reading: the pythonguis.com grid/pack/place FAQ and TkDocs.

Conventions & idioms

  • “View” means anything visible on screen except the window. The window is a Window; everything inside it is a View.

  • The ``.widget`` escape hatch. Reach through any View to its raw Tk widget when you need behaviour myTk does not implement.

  • Mixin composition. Cross-cutting capabilities (events, drag-and-drop) are small mixins folded into Base rather than inheritance trees.

  • Enum-named notifications. Notification identifiers are Enum members, not strings.

  • Delegate methods over callbacks for non-trivial widgets.

Extending myTk

The fastest way to learn the extension points is to read mytk/example_apps/ (mytk.py, lensviewer_app.py, filters_app.py, microscope_app.py, dnd_app.py, view3d_app.py). In short:

  • A new widget subclasses Base (or an existing View) and implements create_widget to build its Tk widget.

  • Custom table behaviour is added by setting a delegate on TableView, not by subclassing it.

  • A new notification is a new Enum member posted through the shared NotificationCenter.

  • A new 3D backend follows the View3DPyrender / View3DModernGL pattern.

A minimal application is just:

from mytk import App, Label

class MyApp(App):
    def __init__(self):
        super().__init__(geometry="600x400", name="MyApp")
        label = Label("Hello, myTk")
        label.grid_into(self.window, row=0, column=0)

MyApp().mainloop()

Design decisions & trade-offs

Why three coordination mechanisms (binding, notifications, delegation)?

They solve different shapes of problem and overlap little. Binding is for “these two values must stay equal.” Notifications are for one-to-many, sender-knows-nothing broadcast. Delegation is for “this widget needs a decision from its owner.” Forcing all three through a single callback style is exactly the tangle myTk set out to avoid.

Why Tk rather than Qt/wx?

Portability and durability. Tk is in the standard library, so there is nothing to install and apps survive moves between machines and OSes. The author’s experience is that Qt, while powerful, is heavier than most scientific apps need and more fragile to transport. Encapsulating Tk turned out easier than simplifying Qt.

Why optional, on-demand dependencies?

3D rendering and OS drag-and-drop are valuable but expensive (large or platform-specific packages). Making them hard requirements would tax every user for features most do not need, and would undermine G1 (stay in the standard library by default). On-demand loading with graceful degradation keeps the baseline install tiny.