Here's a quick tutorial to give you an idea of how it all fits together in practice. It takes the form of an annotated listing of blobedit.py, a simple example application included with the source.
BlobEdit edits Blob Documents. Blob Documents are documents containing Blobs. Blobs are red squares that you place by clicking and move around by dragging.
BlobEdit demonstrates how to:
This tutorial may be extended in the future to cover more features of the framework.
We'll start by importing the modules and classes that we'll need (using clairvoyance to determine what they are):
import pickle
from GUI import Application, View, Document, Window, ScrollFrame, rgb
from GUI.Geometry import pt_in_rect, offset_rect
from GUI.StdColors import black, red
Because we want to work with a custom Document, we'll have to define our own subclass of Application.
class BlobApp(Application):
The initialisation method will first initialise Application, and then call new_cmd to create an initial empty document when the application starts up.
def __init__(self):
Application.__init__(self)
self.new_cmd()
The new_cmd method is the method that's invoked by the standard "New" menu command. The default implementation of new_cmd knows almost everything about how to do this, but there are a few things we need to tell it. First, we need to tell it how to create a Document object of the appropriate kind. We do this by providing a make_new_document method:
def make_new_document(self):
return BlobDoc()
All this method has to do is create an object of the appropriate class and return it. Further initialization will be done by new_cmd.
While we're at it, we'll also tell our Application class how to create a Document when the user opens a file. In our case, we need to do the same thing as before -- just create a Document object and return it. The standard open_cmd handler takes care of the rest.
def make_file_document(self, fileref):
return BlobDoc()
Finally, we need to tell our Application how to create a window for viewing our document. We do this by providing a make_window method. This method is passed the document object for which a window is to be made. Since our application only deals with one type of document, we know what class it will be. If we had defined more than one Document class, we would have to do some testing to find out which kind it was and construct a window accordingly.
def make_window(self, document):
First, we'll create a top-level Window object and associate it with the Document. The purpose of associating the Window with the Document is so that, when the Window is closed, the Document will get the chance to ask the user whether to save changes.
win = Window(size = (400, 400), document = document)
Next, we'll create a View for displaying our data structure. We'll call our View class BlobView here and define it later. We make our document the view's model, so that the view will be notified of any changes that require it to be redrawn.
view = BlobView(model = document, extent = (0, 0, 1000, 1000))
Since we made the extent of our view bigger than the window which will contain it, we'll need to be able to scroll it. We do this by creating a ScrollFrame and putting the view inside it. We won't bother specifying the size of the scroll frame at this stage; its size will be established a bit further down.
frame = ScrollFrame(view)
Next we place the scroll frame inside the window with options that determine
its position, size and resizing behaviour. Without going deeply into details,
we're saying that the edges of the scroll frame are to initially have offsets
of 0 from the corresponding edges of the window, and that when the window
is resized, the scroll frame is to be resized along with it.
win.place(frame, left = 0, top = 0, right = 0, bottom = 0, sticky = 'nsew')
(The options to the place method are very flexible, and there's
much more you can do with it than is demonstrated here. See the documentation
for it in the Frame class for details.)
Finally, we make the window visible on the screen. (It's easy to forget this step. If you leave it out, you won't see anything!)
win.show()
We'll represent the data structure within our document by means of a blobs attribute which will hold a list of Blobs.
class BlobDoc(Document):
blobs = None
We won't define an __init__ method for the document, because there are two different ways that a Document object can get initialised. If it was created by a "New" command, it gets initialised by calling new_contents, whereas if it was created by an "Open..." command, it gets initialised by calling read_contents. So, we'll put our initialisation in those methods. The new_contents method will create a new empty list of blobs, and the read_contents method will use pickle to read a list of blobs from the supplied file.
def new_contents(self):
self.blobs = []
def read_contents(self, file):
self.blobs = pickle.load(file)
The counterpart to read_contents is write_contents, which gets called during the processing of a "Save" or "Save As..." command.
def write_contents(self, file):
pickle.dump(self.blobs, file)
We'll also define some methods for modifying our data structure. Later we'll call these from our View in response to user input. After each modification, we call self.changed() to mark the document as needing to be saved, and self.notify_views() to notify any attached views that they need to be redrawn.
def add_blob(self, blob):
self.blobs.append(blob)
self.changed()
self.notify_views()
def move_blob(self, blob, dx, dy):
blob.move(dx, dy)
self.changed()
self.notify_views()
Our View class will have two responsibilities: (1) drawing the blobs on the screen; (2) handling user input actions.
class BlobView(View):
Drawing is done by the draw method. It is passed a Canvas object on which the drawing should be done. In our draw method, we'll traverse the list of blobs and tell each one to draw itself on the canvas.
def draw(self, canvas):
for blob in self.model.blobs:
blob.draw(canvas)
Mouse clicks are handled by the mouse_down method. There are two things we want the user to be able to do with the mouse. If the click is in empty space, a new blob should be created; if the click is within an existing blob, it should be dragged. So the first thing we will do is search the blob list to find out whether the clicked coordinates are within an existing blob.
def mouse_down(self, event):
x, y = event.position
for blob in self.model.blobs:
if blob.contains(x, y):
self.drag_blob(blob, x, y)
return
If not, we add a new blob to the data structure:
self.model.add_blob(Blob(x, y))
If we're dragging a blob, we need to track the movements of the mouse
until the mouse button is released. To do this we use the track_mouse
method of class View.
The track_mouse method returns an iterator which produces a series
of mouse events as long as the mouse is dragged around with the button held
down. It's designed to be used in a for-loop like this:
def drag_blob(self, blob, x0, y0):
for event in self.track_mouse():
x, y = event.position
self.model.move_blob(blob, x - x0, y - y0)
x0 = x
y0 = y
Here's the implementation of the Blob class, representing an individual blob.
class Blob:
def __init__(self, x, y):
self.rect = (x - 20, y - 20, x + 20, y + 20)
def contains(self, x, y):
return pt_in_rect((x, y), self.rect)
def move(self, dx, dy):
self.rect = offset_rect(self.rect, (dx, dy))
def draw(self, canvas):
l, t, r, b = self.rect
canvas.newpath()
canvas.moveto(l, t)
canvas.lineto(r, t)
canvas.lineto(r, b)
canvas.lineto(l, b)
canvas.closepath()
canvas.forecolor = red
canvas.fill()
canvas.forecolor = black
canvas.stroke()
BlobApp().run()