Next / Previous / Contents / TCC Help System / NM Tech homepage

11. The App class: The application as a whole

The App class inherits from the Tkinter Frame class, which effectively makes it a widget. The documentation string for this class describes the attributes used to control the application and the gridding of widgets.

mazeratty
# - - - - -   c l a s s   A p p

class App(tk.Frame):
    '''Maze-drawing application.

      State/Invariants:
        .nRows:      [ number of rows of cells in the maze ]
        .nCols:      [ number of columns of cells in the maze ]
        .seed:       [ user-requested initial seed ]
        .maze:
           [ the Maze instance containing the maze ]
        .can:        [ the Canvas on which the maze is displayed ]
        .nRowsEntry: [ an Entry widget for the number of rows ]
        .nRowsVar:   [ an IntVar for .nRowsEntry ]
        .nColsEntry: [ an Entry widget for the number of rows ]
        .nColsVar:   [ an IntVar for .nColsEntry ]
        .seedEntry:  [ an Entry widget for the user-requested seed ]
        .seedVar:    [ an IntVar for .seedEntry ]
        .showCB:     [ a Checkbutton to show the solution path ]
        .showVar:    [ an IntVar for .showPathCB ]
        .redoButton: [ a Redo button ]
        .saveButton: [ a Save button ]
        .quitButton: [ a Quit button ]

In the top-level grid, there are only two cells. The canvas is on the left, with the controls on the right in a separate frame named .controls.

mazeratty
      Widgets and gridding:
         0      1
        +------+-----------+
      0 | .can | .controls |
        +------+-----------+

      Layout of self.controls Frame:
         0             1
        +-------------+---------+
      0 | .nRowsEntry | (label) |
        +-------------+---------+
      1 | .nColsEntry | (label) |
        +-------------+---------+
      2 | .seedEntry  | (label) |
        +-------------+---------+
      3 | .showCB               |
        +-------------+---------+
      4 | .redoButton           |
        +-------------+---------+
      5 | .saveButton           |
        +-------------+---------+
      6 | .quitButton           |
        +-------------+---------+
    '''

11.1. App.__init__(): The constructor

The first step is to invoke the parent class's constructor and then grid the master frame.

mazeratty
# - - -   A p p . _ _ i n i t _ _

    def __init__(self):
        #-- 1 --
        # [ self  :=  self initialized as a Frame with parent (master)
        #             and gridded ]
        tk.Frame.__init__(self, None)
        self.grid()

Command line argument processing is done in Section 11.2, “App.__commandLine(): Process the command line arguments”.

mazeratty
        #-- 2 --
        # [ if command line arguments are valid ->
        #     self.nRows  :=  NROWS as an int
        #     self.nCols  :=  NCOLS as an int
        #     self.maze  :=  as invariant, all walls solid
        #   else ->
        #     sys.stderr  +:=  error message
        #     stop execution ]
        self.__commandLine()

For widget creation and placement, see Section 11.4, “App.__createWidgets(): Create all Tkinter widgets”.

mazeratty
        #-- 3 --
        # [ self  :=  self with all widgets in place ]
        self.__createWidgets()

All the initialization that remains is to create the Maze instance (see Section 12.1, “Maze.__init__(): Constructor”) and then call the method that generates the initial maze, Section 12.2, “Maze.generate(): Create a new maze”. The call to self.update_idletasks() causes the application to appear before generation of the maze begins.

mazeratty
        #-- 4 --
        # [ self  :=  self with maze created and displayed ]
        self.maze = Maze ( self.can )
        self.update_idletasks()
        self.maze.generate ( self.nRows, self.nCols, 0, self.seed)

11.2. App.__commandLine(): Process the command line arguments

mazeratty
# - - -   A p p . _ _ c o m m a n d L i n e

    def __commandLine(self):
        '''Process command line arguments and dependent values.

          [ if command line arguments are valid ->
              self.nRows  :=  NROWS as an int
              self.nCols  :=  NCOLS as an int
              self.seed  :=  SEED if given, else None
            else ->
              sys.stderr  +:=  error message
              stop execution ]
        '''

The count of arguments must be either two or there, depending on whether the optional random seed is specified. If no seed is given, one is generated based on the time of day; see Section 11.3, “App.__newSeed(): Randomize the random seed”. Fatal errors are logged using Section 16, “fatal(): Write a message and terminate”.

mazeratty
        #-- 1 --
        # [ if there are three command line arguments ->
        #       argList  :=  the first two
        #       rawSeed  :=  the third
        #   else if there are two command line arguments ->
        #       argList  :=  those arguments as a list
        #       rawSeed  :=  None
        #   else ->
        #     sys.stderr  +:=  error message
        #     stop execution ]
        argList = sys.argv[1:]
        if not ( 2 <= len(argList) <= 3 ):
            fatal ( "Usage: %s NROWS NCOLS" % sys.argv[0])
        if len(argList) == 3:
            rawSeed = argList.pop()
        else:
            rawSeed = None

        #-- 2 --
        # [ if rawSeed is None ->
        #       self.seed  :=  a random integer derived from
        #           the current epoch time
        #   else if rawSeed is an integer ->
        #       self.seed  :=  int(rawSeed)
        #  
        #   else ->
        #     sys.stderr  +:=  error message
        #     stop execution ]
        if rawSeed is None:
            self.__newSeed()
        else:
            try:
                self.seed = int(rawSeed)
            except ValueError:
                fatal ( "Third argument must be an integer random "
                    "seed: '%s'." % rawSeed )

        #-- 3 --
        # [ if there are two integers on the command line ->
        #     self.nRows  :=  the first one as an int
        #     self.nCols  :=  the second one as an int
        #   else ->
        #     sys.stderr  +:=  error message
        #     stop execution ]
        try:
            self.nRows = int(argList[0])
        except ValueError:
            fatal ( "Number of rows not an integer: '%s'" % argList[0] )

        try:
            self.nCols = int(argList[1])
        except ValueError:
            fatal ( "Number of columns not an integer: '%s'" %
                    argList[1] )

11.3. App.__newSeed(): Randomize the random seed

The intent of re-randomizing is to get a different maze each time. We can achieve this by extracting some digits from the epoch time.

mazeratty
# - - -   A p p . _ _ n e w S e e d

    def __newSeed ( self ):
        '''Set up a new random seed.
        '''
        self.seed = int(time.time()*100 % 100000000)

11.4. App.__createWidgets(): Create all Tkinter widgets

For the names and layout of widgets, see Section 11, “The App class: The application as a whole”.

mazeratty
# - - -   A p p . _ _ c r e a t e w i d g e t s

    def __createWidgets(self):
        '''Create and grid all widgets and control variables.
        '''

For the canvas's dimensions, see Section 7.1, “Constants for the canvas”.

mazeratty
        self.can = tk.Canvas(self, height=CAN_HIGH, width=CAN_WIDE,
            bg='white' )
        self.can.grid(row=0, column=0)

        self.controls = tk.Frame(self)
        self.controls.grid(row=0, column=1, sticky=tk.S)

        self.nRowsVar = tk.IntVar()
        self.nRowsVar.set(self.nRows)
        self.nRowsEntry = tk.Entry(self.controls,
            width=4, font=MONO_FONT, justify=tk.RIGHT,
            textvariable=self.nRowsVar)
        rowx = 0
        self.nRowsEntry.grid(row=rowx, column=0, sticky=tk.E)

        tk.Label(self.controls,
            font=LABEL_FONT,
            text="# rows").grid(row=rowx, column=1, sticky=tk.W)

        self.nColsVar = tk.IntVar()
        self.nColsVar.set(self.nCols)
        self.nColsEntry = tk.Entry(self.controls,
            width=4, font=MONO_FONT, justify=tk.RIGHT,
            textvariable=self.nColsVar)
        rowx += 1
        self.nColsEntry.grid(row=rowx, column=0, sticky=tk.E)

        tk.Label(self.controls, font=LABEL_FONT,
            text="# columns").grid(row=rowx, column=1, sticky=tk.W)

We bind an event to the Entry widget for the random seed so that when the user presses the Enter key, it calls Section 11.5, “App.__seedHandler(): Regenerate with a given random seed” to regenerate the maze using the supplied seed.

mazeratty
        self.seedVar = tk.IntVar()
        self.seedVar.set(self.seed)
        self.seedEntry = tk.Entry ( self.controls,
            width=10, font=MONO_FONT, justify=tk.RIGHT,
            textvariable=self.seedVar)
        rowx += 1
        self.seedEntry.grid(row=rowx, column=0, sticky=tk.E)
        self.seedEntry.bind ( "<Key-Return>", self.seedHandler )

        tk.Label(self.controls, font=LABEL_FONT,
            text="Random seed").grid(row=rowx, column=1, sticky=tk.W)

        self.showVar = tk.IntVar()
        self.showCB = tk.Checkbutton ( self.controls, font=LABEL_FONT,
            command=self.showHandler, text="Show the solution",
            variable=self.showVar)
        rowx += 1
        self.showCB.grid(row=rowx, column=0, columnspan=2,
                         sticky=tk.E+tk.W)

        self.redoButton = tk.Button ( self.controls, text="Redo",
            font=BUTTON_FONT,
            command=self.redoHandler )
        rowx += 1
        self.redoButton.grid(row=rowx, column=0, columnspan=2,
            sticky=tk.E+tk.W)

        self.saveButton = tk.Button ( self.controls, text="Save",
            font=BUTTON_FONT,
            command=self.saveHandler )
        rowx += 1
        self.saveButton.grid(row=rowx, column=0, columnspan=2,
            sticky=tk.E+tk.W)

        self.quitButton = tk.Button ( self.controls, text="Quit",
            font=BUTTON_FONT,
            command=self.quit )
        rowx += 1
        self.quitButton.grid(row=rowx, column=0, columnspan=2,
            sticky=tk.E+tk.W)

11.5. App.__seedHandler(): Regenerate with a given random seed

mazeratty
# - - -   A p p . s e e d H a n d l e r

    def seedHandler(self, *p):
        '''When the user changes the seed.
        '''
        self.nRows = self.nRowsVar.get()
        self.nCols = self.nColsVar.get()
        self.seed = self.seedVar.get()
        visible = self.showVar.get()
        self.maze.generate ( self.nRows, self.nCols, visible, self.seed)

11.6. App.__showHandler(): Turn display of the solution on or off

If the checkbox is on now, display the solution (see Section 12.19, “Maze.pathDraw(): Show the solution path”, otherwise erase the solution (see Section 12.23, “Maze.pathErase(): Remove the solution path from the canvas”).

mazeratty
# - - -   A p p . s h o w H a n d l e r

    def showHandler(self, *p):
        '''Handle changes to the 'show' checkbox.
        '''
        #-- 1 --
        if self.showVar.get():
            self.maze.pathDraw()
        else:
            self.maze.pathErase()

11.7. App.redoHandler(): Regenerate the maze

When the user clicks the Redo button, we retrieve the current row and column count, and the state of the Show the solution checkbox, generate a new random seed from the time of day, and redraw the maze.

mazeratty
# - - -   A p p . r e d o H a n d l e r

    def redoHandler ( self, *p ):
        '''Regenerate the maze.
        '''
        self.nRows = self.nRowsVar.get()
        self.nCols = self.nColsVar.get()
        visible = self.showVar.get()
        self.__newSeed()
        self.seedVar.set(self.seed)
        self.maze.generate ( self.nRows, self.nCols, visible, self.seed )

11.8. App.saveHandler(): Save the current image

Tkinter's Canvas widget has a method called .postscript() that generates a PostScript representation of its contents. We use Python's tempfile module to generate a unique file name in the current directory, write the PostScript there, and then use the third-party pexpect module to run the ps2pdf converter to create the PDF equivalent. For documentation on these modules, see Section 6, “Imports”.

mazeratty
# - - -   A p p . s a v e H a n d l e r

    def saveHandler ( self, *p ):
        '''Save the image as a PDF.
        '''
        #-- 1 --
        # [ pdfName  :=  a .pdf file name chosen by the user with a
        #                pop-up menu ]
        pdfName = tkFileDialog.asksaveasfilename (
            initialfile="maze.pdf",
            defaultextension='.pdf',
            title="Save as a .pdf file")

        #-- 2 --
        # [ f  :=  a new, temporary file ending in '.ps' and
        #          containing a PostScript rendering of self.can
        #   psName  :=  name of that file ]
        f = tempfile.NamedTemporaryFile(dir='.', suffix='.ps',
            delete=False)
        psName = f.name
        self.maze.postscript(psName)
        f.close()

        #-- 3 --
        # [ file (pdfName)  :=  file (psName) converted from PS to PDF
        #   file (psName)  :=  deleted ]
        command = "ps2pdf %s %s" % (psName, pdfName)
        pexpect.run(command)
        os.remove(psName)