Last active
September 17, 2025 08:51
-
-
Save jdittrich/7f03622695256bf6e56a459e1187a433 to your computer and use it in GitHub Desktop.
an example for an MVC (where view+controller are combined) with python and tkinter. I saw approaches separating View and Controller, but they seemed leaky, too.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import tkinter as tk | |
from tkinter import ttk | |
from collections.abc import Callable | |
# A simple demo app. | |
# Press the button to update the label | |
class AppView(): | |
def __init__(self, title="apptitle"): | |
root = tk.Tk() | |
root.title(title) | |
root.columnconfigure(0, weight=1) | |
root.rowconfigure(0, weight=1) | |
root.minsize(200, 100) | |
main_frame = ttk.Frame(root, padding="3 3 3 3", relief="raised") | |
main_frame.grid(column=0, row=0, sticky=("nesw")) | |
self.root = root | |
self.main_frame = main_frame | |
def change_titel(self,title="apptitle"): | |
self.root.title(title) | |
def start(self): | |
self.root.mainloop() | |
# Views allow to retrieve a tk frame | |
# if the view can be used to append other | |
# tk elements to it as parent | |
# this is leaky in regards to tk | |
# but it is far less code than a complex wrapper. | |
# The approach also allows to retrieve different tk frames | |
# e.g. by having a get_left_panel method. | |
def get_main_tk(self): | |
return self.main_frame | |
class LabelView(): | |
#views that are not the app view need to pass a parent_tk element. | |
def __init__(self,parent_tk,model): | |
parent_tk = parent_tk | |
label = ttk.Label(parent_tk, text=model.get_label()) | |
label.grid(column=0, row=0) | |
button = ttk.Button(parent_tk,text="update!", command=self.trigger_update_label) | |
button.grid(column=0,row=1) | |
self.button = button | |
self.label = label | |
self.model = model | |
model.on("update",self.handle_update_label) # to update the view on model update | |
def handle_update_label(self,model,*args): | |
self.label.config(text = model.get_label()) | |
def trigger_update_label(self): #upon press of the button | |
self.model.update_lable("new label") | |
# Models inherent from Observeable model to | |
# inform subscribers like views about the updated data | |
# You can do something like on("update", self.handle_model_update) | |
class ObservableModel(): | |
_event_types = {} | |
def __init__(self): | |
pass | |
def on(self,event_type:str,listener:Callable): | |
if event_type in self._event_types: | |
self._event_types[event_type].append(listener) | |
else: | |
self._event_types[event_type] = [listener] | |
def off(self,eventtype:str,listener:Callable): | |
self._event_types[eventtype].remove(listener) | |
def trigger(self,eventtype:str,*args, **kwargs)->None: | |
for event_handler in self._event_types[eventtype]: | |
event_handler(*args,**kwargs) | |
class LabelModel(ObservableModel): | |
label = "testlabel" #default label | |
def __init__(self, label, *args,**kwargs): | |
super().__init__(*args,**kwargs) | |
self.label = label | |
def get_label(self): | |
return self.label | |
def update_lable(self,new_label): | |
self.label = new_label | |
self.trigger("update",self) #inform subscribers about update! | |
#-- Setup --- | |
app = AppView() | |
labelModel = LabelModel("my old label"); | |
labelView = LabelView(parent_tk=app.get_main_tk(),model=labelModel) | |
app.start() | |
# would it not be simpler to ditch the button and just do a labelModel.update_label("newlabel") here? | |
# nope. | |
# a) I want to demonstrate controller actions, so the button is useful and, more relevant, | |
# b) app.start() (or rather the tk mainloop it wraps) does not return as long as the programm is running | |
# so code after start() is never reached while the app runs. |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment