'How to implement a tkinter app with an MVC architecture?
I currently started with python and the MVC architecture. I use MVC because I've read that its very easy to display simple GUI applications with this architecture.
I've read about the MVC architecture, but when trying to implement it myself I have several issues. I think I didn't quite understand the connection between the model, view and controller.
With my program I want to display a button. When the button is pressed a file selection opens and the user can select a file. Afterwards the filename should be written in the entry box.
The problem I'm facing is, that I dont know how to connect the view with the controller and model. So when the button is pressed I want to inform my controller about it, so he manage the file selection. After the file selection I want to save the path in the model.
Here is my code:
View
from tkinter import filedialog
import tkinter as tk
class View(object):
def __init__(self, controller):
self.controller = controller
def showGUI(self, title):
self.projectWindow = tk.Tk()
self.projectWindow.title(title)
self.importEntry= tk.Entry(projectWindow, width=100)
self.importEntry.pack(pady=10,padx=10)
importButton = tk.Button(projectWindow, text ="Import File", command= lambda : self.controller.importButtonPressed(controller)) #Here I need the connection to the controller but I cant access the controllers methods
#I know this is a very bad practise but I dont really know how to inform the controller in a other way
importButton.pack(pady=10, padx=10,side=tk.LEFT)
def display_file_selection_view(self):
file_path = filedialog.askopenfilename(title = "Select a Excel File", filetypes=[("Excel files", ".xlsx .xls")])
return file_path
Controller
import Model
import View
class Controller(object):
def __init__(self, model, view):
self.model = model
self.view = view
#I tried something like this to inform the model about the state change
self.model.register_observer(self.view.importEntry)
def update(self):
self.view.importEntry.config(state="normal")
self.view.importEntry.delete(0,'end')
self.view.importEntry.insert(0,self.model.selectedElement)
self.view.importEntry.config(state="readonly")
def importButtonPressed(self):
selectedElement = self.view.display_file_selection_view()
#check if data has been selected
if selectedElement:
self.model.selectedExcel = selectedElement
else:
self.view.showGUI("File Selection")
Model
class Model(object):
def __init__(self):
self._args = ""
self._selectedExcel = ""
self.observers = []
def register_observer(self, observer):
self.observers.append(observer)
def notify(self):
[observer.update() for observer in self.observers]
@property
def selectedExcel(self):
return self._selectedExcel
@selectedExcel.setter
def selectedExcel(self, value):
self._selectedExcel = value
self.notify()
Main
import Model
import View
import Controller
if __name__ == "__main__":
model = Model.Model()
view = View.View(Controller)
controller = Controller.Controller(model, view)
Solution 1:[1]
Your controller class object needs to be initialized in the View class.
def __init__(self,controller):
self. controller = controller
The flaw in this bit of code is that controller is always going to be an empty object. And you are basically assigning the value of an empty object to your instance variable.
What you need to is instantiate an instance of the controller class in your View constructor like so
def __init__(self,controller):
self.controller = Controller(model, view)
You need to correct the variables model and view to objects of model and view constructors.
Solution 2:[2]
The point of MVC is encapsulating elements. It means dependencies always goes in one direction. View cannot "know" the Controller. The Controller have to "know" the View. The way to keep M V and C separate is to make them sockets and plugs.
View
from tkinter import filedialog
import tkinter as tk
class View(object):
# The view will only be responsible for displaying widgets
# and getting some information from user
def __init__(self):
# it means it arranges all widgets just as it initialize
self.callbacks = {}
self._showGUI("File Selection")
def _showGUI(self, title):
# This method will only build widgets. I prefixed it underline,
# as it is private method of the view class, and shouldn't be
# used outside of it (i.e. by the controller)
self.projectWindow = tk.Tk()
self.projectWindow.title(title)
self.importEntry= tk.Entry(self.projectWindow, width=100)
self.importEntry.pack(pady=10,padx=10)
self.importButton = tk.Button(self.projectWindow, text ="Import File")
# In this moment we don't define command of this button
self.importButton.pack(pady=10, padx=10,side=tk.LEFT)
def add_callback(self, key, method):
# This method makes connections, and lets the
# controller share its own methods to view, and prevents
# view being dependent on controller. This method will be
# used specifically by the controller.
self.callbacks[key] = method
def bind_commands(self):
# This method also will be used by the controller. And this
# going to happened at the moment, when callbacks will be defined
self.importButton.config(command=self.callbacks['import'])
def run(self):
# This method lets program works. It also is used by the controller
self.projectWindow.mainloop()
def get_file_selection_view(self):
file_path = filedialog.askopenfilename(title = "Select a Excel File", filetypes=[("Excel files", ".xlsx .xls")])
return file_path
def display_selection(self, content):
self.importEntry.config(state="normal")
self.importEntry.delete(0,'end')
self.importEntry.insert(0,self.model.selectedElement)
self.importEntry.config(state="readonly")
Controller
class Controller(object):
def __init__(self, model, view):
self.model = model
self.view = view
# Here we pass to the view controller method without making view
# dependent on it
self.view.add_callback('import', self.importButtonPressed)
# Now, callback exist, so the controller can ask the view to bind
# its widgets to passed values of callbacks dictionary
self.view.bind_commands()
self.view.run()
def importButtonPressed(self):
# This method, as its controller method, should lead all actions
# First it gets file path
selectedElement = self.view.get_file_selection_view()
# Check if data has been selected
if selectedElement:
# Here it should use some model method to do whatever you expect
# to model do, and return result
# Honestly - I don't know what do you want from your model
# But whatever it is let model do it itself, and return result here.
result = self.model.some_method(selectedElement)
else:
pass
# Now you can easily display result. But still, to stick with MVC
# architecture you should use view method to got it done, instead of
# directly using some view objects method. It should work (both view and
# model), like you're
# going to make in the future another View class with same interface
# (all public methods and parameters) and all you need to do to start using
# your new class, would be change imports in main.py
self.view.display_filename(result)
Main
import model
import view
import controller
if __name__ == "__main__":
model = model.Model()
# The view doesn't need to have a controller parameter
view = view.View()
controller = controller.Controller(model, view)
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
| Solution | Source |
|---|---|
| Solution 1 | |
| Solution 2 | kubablo |
