[Tutor] Correct use of model-view-controller design pattern
Peter Otten
__peter__ at web.de
Sun May 29 10:28:35 EDT 2016
boB Stepp wrote:
[snip]
No direct answers to your questions, sorry;)
In my experience MVC is nearly always used to build GUIs and based on
classes. The idea is that you can add multiple views to a model, a bar chart
and a table of the values, sometimes at runtime. You may also swap out the
model underneath the view, retrieve text from a file system or a database
via the same interface. This is hard with modules.
The separation between view controller is often artificial or at least I
always end up with a mix of these two components. The model-view split on
the other side (also known as observer pattern) is straight-forward and
helpful to produce cleaner code; but even between model and view the
coupling tends to get stronger than you might wish.
Here's a commandline example with two views (the circle-drawing routine has
to be fixed); in real life I would probably manipulate the model directly
and omit the controller classes. There are two explict view classes, but the
while True: ...
loop could also be seen as a view -- or rather the dreaded view/controller
mix.
$ cat mvc_cli.py
#!/usr/bin/env python3
class CircleModel:
def __init__(self, radius):
self._radius = radius
self.views = []
def add(self, view):
self.views.append(view)
def changed(self):
for view in self.views:
view.changed(self)
@property
def radius(self):
return self._radius
@radius.setter
def radius(self, radius):
if self._radius != radius:
self._radius = radius
self.changed()
class CircleView:
def __init__(self, model):
self._model = None
self.model = model
self.changed(model)
@property
def model(self):
return self._model
@model.setter
def model(self, model):
if self._model is not None:
raise NotImplementedError
model.add(self)
self._model = model
def changed(self, model):
"""React to model changes."""
class RadiusDeltaController:
def __init__(self, model, delta):
self.model = model
self.delta = delta
def grow(self):
self.model.radius += self.delta
def shrink(self):
self.model.radius -= self.delta
class RadiusController:
def __init__(self, model):
self.model = model
def set_value(self, value):
self.model.radius = value
def dist(ax, ay, bx, by):
return ((ax-bx)**2 + (ay-by)**2)**.5
class CircleImageView(CircleView):
def changed(self, model):
self.update_radius(model.radius)
def update_radius(self, radius):
# FIXME
diameter = 2*radius
cx = cy = radius
for row in range(diameter):
for column in range(2*diameter):
if dist(cx, cy, row, column/2) < radius:
print("*", end="")
else:
print(" ", end="")
print()
class CircleValueView(CircleView):
def changed(self, model):
print("Circle radius is now", model.radius)
if __name__ == "__main__":
circle = CircleModel(5)
image_view = CircleImageView(circle)
value_view = CircleValueView(circle)
rc = RadiusController(circle)
dc = RadiusDeltaController(circle, 2)
print("Model-View-Controler Demo")
print("Enter an integer to set the circle radius")
print("or 'grow' to increase the radius")
print("or 'shrink' to decrease the radius")
print("or 'add' to add another CircleValueView")
while True:
try:
s = input("radius or 'grow' or 'shrink' or 'add': ")
except KeyboardInterrupt:
print("\nThat's all folks")
break
if s == "grow":
dc.grow()
elif s == "shrink":
dc.shrink()
elif s == "add":
CircleValueView(circle)
else:
try:
radius = int(s)
except ValueError:
print("don't know what to do with input {!r}".format(s))
else:
rc.set_value(radius)
Here's a sample session:
$ python3 mvc_cli.py
***********
***************
*******************
*******************
*******************
*******************
*******************
***************
***********
Circle radius is now 5
Model-View-Controler Demo
Enter an integer to set the circle radius
or 'grow' to increase the radius
or 'shrink' to decrease the radius
or 'add' to add another CircleValueView
radius or 'grow' or 'shrink' or 'add': add
Circle radius is now 5
radius or 'grow' or 'shrink' or 'add': grow
***************
*******************
***********************
*************************
***************************
***************************
***************************
***************************
***************************
*************************
***********************
*******************
***************
Circle radius is now 7
Circle radius is now 7
radius or 'grow' or 'shrink' or 'add': grow
*****************
***********************
***************************
*****************************
*********************************
*********************************
***********************************
***********************************
***********************************
***********************************
***********************************
*********************************
*********************************
*****************************
***************************
***********************
*****************
Circle radius is now 9
Circle radius is now 9
radius or 'grow' or 'shrink' or 'add': 6
*************
*****************
*********************
***********************
***********************
***********************
***********************
***********************
*********************
*****************
*************
Circle radius is now 6
Circle radius is now 6
radius or 'grow' or 'shrink' or 'add': add
Circle radius is now 6
radius or 'grow' or 'shrink' or 'add': ^C
That's all folks
$
There's also a tkinter version (which I wrote before the cli one). I think
it's easier to read the complete code than the diff; so there, with some
repetition:
$ cat mvc.py
#!/usr/bin/env python3
import tkinter as tk
class CircleModel:
def __init__(self, radius):
self._radius = radius
self.views = []
def add(self, view):
self.views.append(view)
def changed(self):
for view in self.views:
view.changed(self)
@property
def radius(self):
return self._radius
@radius.setter
def radius(self, radius):
if self._radius != radius:
self._radius = radius
self.changed()
class CircleView:
def __init__(self, model):
self._model = None
self.model = model
self.changed(model)
@property
def model(self):
return self._model
@model.setter
def model(self, model):
if self._model is not None:
raise NotImplementedError
model.add(self)
self._model = model
def changed(self, model):
"""React to model changes."""
class RadiusDeltaController:
def __init__(self, model, delta):
self.model = model
self.delta = delta
def grow(self):
self.model.radius += self.delta
def shrink(self):
self.model.radius -= self.delta
class RadiusController:
def __init__(self, model):
self.model = model
def set_value(self, value):
self.model.radius = value
class CircleImageView(CircleView):
def __init__(self, root, model, fill, **kw):
self.canvas = tk.Canvas(root, width=100, height=100)
self.fill = fill
self.canvas.grid(**kw)
self.id = None
super().__init__(model)
def changed(self, model):
self.update_radius(model.radius)
def update_radius(self, radius):
id = self.id
cx = cy = 50
extent = cx-radius, cy-radius, cx+radius, cx+radius
if id is None:
self.id = self.canvas.create_oval(
*extent, fill=self.fill)
else:
self.canvas.coords(id, *extent)
class ResizeView(CircleView):
def __init__(self, root, model, controller, **kw):
self.frame = tk.Frame(root)
self.frame.grid()
self.grow = tk.Button(
self.frame, text="Grow", command=controller.grow)
self.grow.pack(side=tk.LEFT)
self.shrink = tk.Button(
self.frame, text="Shrink", command=controller.shrink)
self.shrink.pack(side=tk.LEFT)
super().__init__(model)
def changed(self, model):
self.grow["text"] = "Grow to {}".format(model.radius + 10)
self.shrink["text"] = "Shrink to {}".format(model.radius - 10)
class RadiusView(CircleView):
def __init__(self, root, model, controller, **kw):
self.controller = controller
self.frame = tk.Frame(root)
self.frame.grid(**kw)
self.label = tk.Label(self.frame, text="Radius")
self.label.grid(row=0, column=0)
self.hint = tk.Label(self.frame, text="Hit <Return> to apply")
self.hint.grid(row=0, column=3)
self.textvariable = tk.StringVar()
self.entry = tk.Entry(self.frame, textvariable=self.textvariable)
self.entry.bind("<Key-Return>", self.set_value)
self.entry.grid(row=0, column=1)
super().__init__(model)
def set_value(self, *args):
try:
radius = int(self.textvariable.get())
except ValueError:
pass
else:
self.controller.set_value(radius)
def get_value(self, model):
return str(model.radius)
def changed(self, model):
self.textvariable.set(self.get_value(model))
if __name__ == "__main__":
root = tk.Tk()
circle = CircleModel(42)
red_view = CircleImageView(
root, circle, "red",
row=0, column=0)
green_view = CircleImageView(
root, circle, "green",
row=0, column=1)
resize_view = ResizeView(
root, circle, RadiusDeltaController(circle, delta=10),
row=1, column=0, columnspan=2)
radius_view = RadiusView(
root, circle, RadiusController(circle),
row=2, column=0, columnspan=2)
root.mainloop()
$
More information about the Tutor
mailing list