Background.
The suggested environment for learning to program using graphics instead of text was LiveWires . This includes a curriculum and associated product, making it a tidy package.
There are a number of alternatives, however.
- Python includes tkinter , section 20.1 of the Python Library Reference, v2.5.
- Python includes turtle, section 20.4 of the Python Library Reference. v2.5.
- Section 20.5 of the Python Library reference lists 7 other toolkits -- most of which are for building GUI applications, and aren't terribly pedagogical.
- OLPC's Sugar is based on GTK, and pyGTK. For pedagogical purposes, this interests me. Recently ("Sugar, GTK and OLPC ") I put together the PyGTK environment so I could look at updating my Building Skills In Python book to align it with Dr. Ceder's approach to programming. Also, I need to align the book with CP4E .
Here's some little scraps of code which might amount to a livewires-like environment which is strictly PyGTK in implementation.
graphicApp.py
The foundation is a small hierarchy of classes which embody a non-document graphic application with an extensible control panel for simple controls. By default, a simple Save As... and Quit button can be provided.
Part 1 - Python Basics
#!/usr/bin/env python """graphicApp module. Define a simple pyGTK Graphic Application with a simple user interface. This application is built in two layers: TinyApplication is a small pyGTK no-document application. It handles the basic GTK application initialization, run, and termination. This superclass provides a build_main_area() method which must be overridden by a subclass. GraphicApplication is a subclass of TinyApplication which overrides build_main_area() to create an area with a control area and a graphic area. The control area is seeded with a save button and quit button. The GraphicApplication provides two stub methods: build_application_controls() and drawImage(). An application will override these two methods to add controls and draw an image based on the control setting. This is usually imported with a from graphicApp import * So that the full gtk, gobject and pango libraries are brought in, also. A more sophisticated application would involve a document, and the main application window would be a document window, with a menu bar in addition to any other buttons and controls. """ import pygtk pygtk.require('2.0') # Selects version library. import gtk print "Check for version 2.6:", gtk.check_version(2,6,3) or "V2.6.3 found" import gobject print "GLIB Version:", gobject.glib_version import pango import os _version_ = "0.2"
Part 2 - TinyApplication
This class handles the minimum GTK handshake to start and stop cleanly. Subclasses will override methods to extend this class into something more useful.
class TinyApplication( object ): """A simple single-window application superclass. A subclass must override the build_main_area() function to build the main window content. This parent application will provide self.window, which is the top-level window for the application. self.status, which is a status bar including a grow icon. """ def delete_event(self, widget, event, data=None): """Handle delete of the top-level window. Override this to provide a confirm-to-quit dialog box. """ # If you return FALSE in the "delete_event" signal handler, # GTK will emit the "destroy" signal. Returning TRUE means # you don't want the window to be destroyed. # This is useful for popping up 'are you sure you want to quit?' # type dialogs. print "delete-event signal occurred" # Change FALSE to TRUE and the main window will not be destroyed # with a "delete_event". return False def destroy(self, widget, data=None): """Handle destroy of the top-level window.""" print "destroy-event signal occurred" gtk.main_quit() def __init__(self): """Build the top-level single-window application. This will call the subclass build_main_area() to construct the interesting bits of the application.""" # create a new top-level window self.window = gtk.Window(gtk.WINDOW_TOPLEVEL) # When the window is given the "delete_event" signal (this is given # by the window manager, usually by the "close" option, or on the # titlebar), we ask it to call the delete_event () function # as defined above. The data passed to the callback # function is NULL and is ignored in the callback function. self.window.connect("delete_event", self.delete_event) # Here we connect the "destroy" event to a signal handler. # This event occurs when we call gtk_widget_destroy() on the window, # or if we return FALSE in the "delete_event" callback. self.window.connect("destroy", self.destroy) # Sets the border width of the window. self.window.set_border_width(2) # Build the main working area of the window. self.main_area= self.build_main_area() # Create a Statusbar to hold messages. self.status= gtk.Statusbar() self.status.set_has_resize_grip( True ) # Create a VBox to hold the controls and the StatusBar self.app_box= gtk.VBox() self.app_box.add( self.main_area ) self.app_box.add( self.status ) # This packs the box into the window (a GTK container). self.window.add(self.app_box) # Show the status bar, the main control panel and the window self.status.show() self.app_box.show() self.window.show() def main(self): """Run the application.""" # All PyGTK applications must have a gtk.main(). Control ends here # and waits for an event to occur (like a key press or mouse event). gtk.main() def hello_world( self, widget, param ): """A function to demonstrate that the application works.""" ctx= self.status.get_context_id("hello world") print "hello: %r" % ( param, ) self.status.pop(ctx) self.status.push(ctx,"hello: %r" % ( param, ) ) def build_main_area( self ): """Build the main display area. A subclass will override this to build a more interesting main area. """ controls= gtk.HButtonBox() controls.set_border_width(16) self.b_hello = gtk.Button("Hello") self.b_quit= gtk.Button("Quit",gtk.STOCK_QUIT) # When the button receives the "clicked" signal, it will call the # method hello_world() passing it None as its argument. self.b_hello.connect("clicked", self.hello_world, "world") # This will cause the window to be destroyed by calling # gtk_widget_destroy(window) when "clicked". Again, the destroy # signal could come from here, or the window manager. self.b_quit.connect_object("clicked", gtk.Widget.destroy, self.window) controls.add(self.b_hello) controls.add(self.b_quit) # The final step is to display this newly created widget. self.b_hello.show() self.b_quit.show() controls.show() return controls
Part 3 - GraphicApplication
This class adds structure for a graphic application with a simple control panel. Specifically, it narrows the final application down to providing a method that replaces drawImage.
class GraphicApplication( TinyApplication ): """A Tiny Application which displays a control panel and a graphic area. The control area has a save and quit button. A subclass application can add controls to this area to adjust the image which is displayed. The save button will save the image as a PNG file. The quit button will quit. The graphic area is a simple DrawingArea into which a pixmap is drawn. A subclass application will redefine the method for drawing this pixmap. """ def fileName( self ): return "image.png" def fileFormat( self ): return "png" def defaultSize( self ): return 414, 256 def save(self, widget, data=None): """Handle the clicked event on the Save As button.""" ctx= self.status.get_context_id("save") self.status.pop(ctx) # show a file chooser # TODO: include a selector for file formats handled formats= [ f for f in gtk.gdk.pixbuf_get_formats() if f['is_writable'] ] for f in formats: print ' ', f['name'], f['extensions'][0] self.chooser= gtk.FileChooserDialog( title="Save the Drawing", parent=None, action=gtk.FILE_CHOOSER_ACTION_SAVE, buttons=( gtk.STOCK_SAVE, gtk.RESPONSE_ACCEPT, gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL), ) # TODO: add keyboard accelerators so Enter key works. self.chooser.set_current_name( self.fileName() ) #self.chooser.set_do_overwrite_confirmation(True) # 2.8 only event= self.chooser.run() if event == gtk.RESPONSE_ACCEPT: name= self.chooser.get_filename() # TODO: Prior to 2.8, must manually Prevent Overwrite # If overwrite, need to confirm. # If overwrite and confirmation == no, continue a loop # Create a Pixbuf from the drawing area Pixmap width, height= self.drawing.get_size() pb= gtk.gdk.Pixbuf(gtk.gdk.COLORSPACE_RGB, False, 8, width, height) pb.get_from_drawable( self.drawing, self.drawing.get_colormap(), 0, 0, 0, 0, width, height ) # Save the resulting Pixbuf as a PNG pb.save(name, self.fileFormat() ) self.status.push(ctx,"Saved %s" % name) else: self.status.push(ctx,"File not saved.") self.chooser.destroy() def build_application_controls( self, controls ): pass def build_control_area( self ): """Build the top control area and the two default buttons (save and quit). Call build_application_controls to build any additional controls. A subclass would override build_application_controls to build application-specific buttons or fields. """ # Create a ButtonBox to hold the buttons. controls= gtk.HButtonBox() controls.set_border_width(8) self.b_save = gtk.Button("Save As...", gtk.STOCK_SAVE_AS) self.b_quit= gtk.Button("Quit",gtk.STOCK_QUIT) # When the button receives the "clicked" signal, it will call the # method save() passing it None as its argument. self.b_save.connect("clicked", self.save, None) # This will cause the window to be destroyed by calling # gtk_widget_destroy(window) when "clicked". Again, the destroy # signal could come from here, or the window manager. self.b_quit.connect_object("clicked", gtk.Widget.destroy, self.window) # Add any additional controls, if necessary. self.build_application_controls( controls ) # Pack the buttons into the box controls.add(self.b_save) controls.add(self.b_quit) # The final step is to display this newly created control area widget. for b in controls.get_children(): b.show() controls.show() return controls def drawImage( self, pixmap, widget ): # Create a Pango Context for applying text labels to the diagram pangoContext= widget.get_pango_context() graphicContext= widget.get_style().fg_gc[gtk.STATE_NORMAL] fontAttrList= pango.AttrList() fontAttrList.change( pango.AttrSize( 24*1000, 0, 2 ) ) label_s1= pango.Layout( pangoContext ) label_s1.set_text( "Hello World" ) page_width, page_height= pixmap.get_size() ex1_ink, ex1_log = label_s1.get_pixel_extents() x, y, label_width, label_height= ex1_log pixmap.draw_layout( graphicContext, page_width/2-label_width/2, page_height*2/5-label_height/2, label_s1 ) def expose( self, widget, event, data=None ): """Connected to the expose-event for the graphic area. This will refresh the image by first creating the necessary Pixmap (self.drawing) and then drawing that Pixmap into the DrawingArea (widget). """ # What are we drawing? x , y, width, height = event.area # Create the selected image self.drawImage( self.drawing, widget ) # Apply to the Image widget widget.window.draw_drawable( widget.get_style().fg_gc[gtk.STATE_NORMAL], self.drawing, x, y, x, y, width, height) return False # We're not done; the Event can propagate def configure( self, widget, event, data ): """Connected to the configure-event for the graphic area. This will create the initial Pixmap, and set the default size for the DrawingArea. It will also blank the Pixmap to assure that it has some initial content. """ # Create an empty drawing that we will insert into the graphic_area width, height = self.defaultSize() self.drawing= gtk.gdk.Pixmap(widget.window, width, height) self.drawing.draw_rectangle( widget.get_style().white_gc, True, 0, 0, width, height) # Stake out the preferred size, since the drawing area has # no internal elements to request screen space. widget.set_property( "height-request", height ) widget.set_property( "width-request", width ) # 1x1.6 ratio return False # We're not done; the Event can propagate def build_graphic_area( self ): """Build the Drawing Area, connect two events. The configure-event creates the initial, empty Pixmap, and establishes the default size. The expose-event then creates the Pixmap, and draws it into the graphic area widget. """ graphic_area= gtk.DrawingArea() graphic_area.connect( "configure-event", self.configure, None ) graphic_area.connect( "expose-event", self.expose, None ) graphic_area.show() return graphic_area def build_main_area(self): """Build the graphic application panel.""" # Create the main graphics + buttons area area= gtk.VBox() #area.set_property( "style", "draw-border", 1 ) # pyGTK 2.8 # Create the content of the main area self.control_area= self.build_control_area() self.graphic_area= self.build_graphic_area() sep= gtk.HSeparator() sep.set_property("height-request",16) sep.show() area.add( self.control_area ) area.add( sep ) area.add( self.graphic_area ) area.show() return area
Part 4 - The Main Switch
This main switch is essential, and shows how the final application is self-contained. The main loop is part of the application's main method. I'm not a fan of having the main loop outside the application class definition.
if __name__ == "__main__": helloWorld = GraphicApplication() helloWorld.main()
Some Design Issues
This is, essentially, a TODO list.
First, I don't like doing so much in __init__. While the pyGTK examples make heavy use of __init__, and I preserved that approach, I'm not generally happy with it. Too many things happen automagically. In other GUI's, I have had an explicit three step build, add, show. However, those were big and complex applications, and I need to split the difference between small applications for learning and large expensive-to-maintain applications.
Second, I'm unhappy with the exposed sophistication of Pango. Typesetting, while complex in reality, seems simple, and should be simple for newbies. A wrapper for Pango with a lot of defaults and assumptions would be helpful.
Third, I need to fold in the Application-Document-Window abstractions. This design pattern is central to the most usable GUI's. Apple describes it nicely in "Windows Considerations ". You can read some interesting stuff, followed by pointless invective in Tom Yager's "Mac sense and nonsense " in InfoWorld .
The old Think/Lightspeed C libraries had some great designs for this essential application structure. But that was long ago and far away; some of those design patterns don't seem to be well preserved. Or perhaps I'm just not looking in all the right places. Rather than find good stuff on Application, Document and Window, I can only find things on Single Document Interface (SDI), which is a Micro$oft-ism.