[Tkinter-discuss] Tkinter polling example: file copy with progress bar

John W. Shipman john at nmt.edu
Mon Dec 13 21:06:55 CET 2010


Attached is a Tkinter script that demonstrates polling, that is,
performing a long-running process in parallel with the GUI.  The
script asks for an input file name and an output file name and copies
the input file to the output file.  The copy operation is done in a
child process managed with pexpect, and the GUI reports the progress
of the file copy using a Scale widget as a progress bar.

Cordially,
John W. Shipman, NM Tech Computer Center, Socorro, NM; john at nmt.edu
Tkinter 8.4 reference: http://www.nmt.edu/tcc/help/pubs/tkinter/
-------------- next part --------------
#!/usr/bin/env python
#================================================================
# copyprogress:  File copy with a progress bar for Tkinter 8.4.
#   - Demonstrates Tkinter .after() and the pexpect module.
# Written by John W. Shipman (john at nmt.edu), New Mexico Tech
# Computer Center, Socorro, NM 87801 USA.  This script is in
# the public domain.
#----------------------------------------------------------------

# - - - - -   I m p o r t s

import sys, os, stat
import Tkinter as tk
import tkFileDialog, tkMessageBox
import pexpect

# - - - - -   M a n i f e s t   c o n s t a n t s

BUTTON_FONT = ("Helvetica", 17)
LABEL_FONT = ("Helvetica", 14)
ENTRY_FONT = ("DejaVu Sans Mono", 12)
POLL_TIME = 50   # Polling frequency in milliseconds


# - - - - -   m a i n

def main():
    """
    """
    app = App()
    app.master.title("Copy with progress bar")
    app.mainloop()

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

class App(tk.Frame):
    '''Copies a file with a progress bar.

      Widgets:
        .fromFileVar:    StringVar for source file name
        .fromFileEntry:  Entry for source file name
        .fromFileBrowse: Browse button for source file name
        .fromFileLabel:  Label for above
        .toFileVar:      StringVar for destination file name
        .toFileEntry:    Entry for destination file name
        .toFileBrowse:   Browse button for destination file name
        .toFileLabel:    Label for above
        .copyButton:     Button to start copying
        .progressVar:    DoubleVar for progress scale
        .progressScale:  Scale to show progress

      Grid plan:
         0                1                 2
        +----------------+-----------------+----------------+
      0 | .fromFileEntry | .fromFileBrowse | .fromFileLabel |
        +----------------+-----------------+----------------+
      1 | .toFileEntry   | .toFileBrowse   | .toFileLabel   |
        +----------------+-----------------+----------------+
      2 | .progress      | .copyButton     | .quitButton    |
        +----------------+-----------------+----------------+

      Internal state:
        .fromFileSize:   Source file size in bytes
        .child:          pexpect child process to do the copy
    '''

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

    def __init__(self, master=None):
        tk.Frame.__init__(self, master)
        self.grid()
        self.__createWidgets()

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

    def __createWidgets(self):
        '''Create all widgets and associated variables.
        '''
        self.fromFileVar = tk.StringVar()
        self.fromFileEntry = tk.Entry ( self,
            textvariable=self.fromFileVar,
            font=ENTRY_FONT, width=50 )
        rowx, colx = 0, 0
        self.fromFileEntry.grid(row=rowx, column=colx, sticky=tk.E)

        self.fromFileBrowse = tk.Button ( self,
            command=self.__browseFrom,
            font=BUTTON_FONT, text="Browse" )
        colx += 1
        self.fromFileBrowse.grid(row=rowx, column=colx)

        self.fromFileLabel = tk.Label ( self,
            font=LABEL_FONT, text="Source file" )
        colx += 1
        self.fromFileLabel.grid(row=rowx, column=colx, sticky=tk.W)

        self.toFileVar = tk.StringVar()
        self.toFileEntry = tk.Entry ( self,
            textvariable=self.toFileVar,
            font=ENTRY_FONT, width=50 )
        rowx, colx = rowx+1, 0
        self.toFileEntry.grid(row=rowx, column=colx, sticky=tk.E)

        self.toFileBrowse = tk.Button ( self,
            command=self.__browseTo,
            font=BUTTON_FONT, text="Browse" )
        colx += 1
        self.toFileBrowse.grid(row=rowx, column=colx)

        self.toFileLabel = tk.Label ( self,
            font=LABEL_FONT, text="Destination file")
        colx += 1
        self.toFileLabel.grid(row=rowx, column=colx, sticky=tk.W)

        self.progressVar = tk.DoubleVar()
        self.progressScale = tk.Scale ( self,
             length=400, orient=tk.HORIZONTAL,
             from_=0.0, to=100.0, resolution=0.1, tickinterval=20.0,
             variable=self.progressVar,
             label="Percent completion", font=LABEL_FONT )
        rowx, colx = rowx+1, 0
        self.progressScale.grid(row=rowx, column=colx, sticky=tk.E)
        
        self.copyButton = tk.Button ( self,
            command=self.__copyHandler,
            font=BUTTON_FONT, text="Copy" )
        colx += 1
        self.copyButton.grid(row=rowx, column=colx )

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


# - - -   A p p . _ _ b r o w s e F r o m

    def __browseFrom(self):
        '''Handler for Browse button for the source file.
        '''
        # [ if the user enters an existing file name in a popup ->
        #       self.fromFileVar  :=  that name
        #   else ->
        #       f  :=  an empty string ]

        f = tkFileDialog.askopenfilename(title="Source file name")
        if len(f) == 0:
            return
        else:
            self.fromFileVar.set(f)


# - - -   A p p . _ _ b r o w s e T o

    def __browseTo(self):
        '''Handler for Browse button for the source file.
        '''
        # [ if the user enters a nonexistent existing file name in
        #   a popup, or enters an existing name and then says it's
        #   okay to overwrite it ->
        #       self.toFileVar  :=  that name
        #   else ->
        #       f  :=  an empty string ]
        f = tkFileDialog.asksaveasfilename(title="Destination file name")
        if len(f) == 0:
            return
        else:
            self.toFileVar.set(f)

# - - -   A p p . _ _ c o p y H a n d l e r

    def __copyHandler(self):
        '''Start the file copy process.
        '''
        # [ if the source file name is empty ->
        #       display a popup error message
        #       return
        #   else -> I ]
        if len(self.fromFileVar.get()) == 0:
            tkMessageBox.showerror("Error",
                "Please enter a source file name." )
            return

        # [ if the destination file name is empty ->
        #       show an error popup
        #       return
        #   else if the destination file exists and the user's reply
        #   to a popup indicates they do not want to proceed ->
        #       return
        #   else -> I ]
        toFileName = self.toFileVar.get()
        if len(toFileName) == 0:
            tkMessageBox.showerror("Error",
                "Please enter a destination file name." )
            return
        elif os.path.exists(toFileName):
            message = ( "File '%s' exists.\nDo you want to overwrite "
                        "it?" % toFileName )
            answer = tkMessageBox.askokcancel("Destination file exists",
                message, default=tkMessageBox.CANCEL)
            if not answer:
                return

        # [ if the source file exists ->
        #     self.fromFileSize  :=  that file's size in bytes
        #   else ->
        #     display a popup and return ]
        if not self.__copySetup():
            return

        # [ self.child  :=  a pexpect.spawn child process that copies
        #       the source file to the destination file with -f
        #   self  :=  self with a callback to self.__poll after
        #       POLL_TIME ]
        self.__startCopy()

# - - -   A p p . _ _ c o p y S e t u p

    def __copySetup(self):
        '''Operations done before the copy is started.

          [ if the source file exists ->
              self.fromFileSize  :=  that file's size in bytes
              return True
            else ->
              display an error popup
              return False ]
        '''
        fromFileName = self.fromFileVar.get()
        try:
            self.fromFileSize = self.__measureFile(fromFileName)
        except OSError, details:
            tkMessageBox.showerror ( "Source file error",
                "File %s: %s" % (fromFileName, str(details)) )
            return False

        return True

# - - -   A p p . _ _ s t a r t C o p y

    def __startCopy ( self ):
        '''Start up a file copy operation.

          [ (self.fromFileVar contains the name of a readable file) and
            (self.toFileVar contains the name of a writeable file) ->
              self.child  :=  a pexpect.spawn child process that copies
                  the source file to the destination file with -f
              self  :=  self with a callback to self.__poll after
                  POLL_TIME ]
        '''
        # [ command  :=  a copy command from the source file to the
        #       destination file, with a force option ]
        command = ( "cp -f %s %s" %
                    (self.fromFileVar.get(), self.toFileVar.get()) )

        # [ self.progressVar  :=  0
        #   self  :=  self with a callback after POLL_TIME to
        #       self.__poll ]
        self.progressVar.set(0.0)
        self.after(POLL_TIME, self.__poll)

        # [ self.child  :=  a pexpect.spawn process to run command ]
        self.child = pexpect.spawn(command)


# - - -   A p p . _ _ m e a s u r e F i l e

    def __measureFile(self, fileName):
        '''Determine the current length of a file, if it exists.

          [ if fileName can be statted ->
                return the current length of that file
            else -> raise OSError ]
        '''
        status = os.stat ( fileName )
        return status[stat.ST_SIZE]

# - - -   A p p . _ _ p o l l

    def __poll(self):
        '''Periodic check of the copy progress.

          [ if self.child has terminated ->
              self.progressVar  :=  100.0
              self.child  :=  (closed)
              show a status popup
            else if we can stat the output file ->
              self.progressVar  :=  (destination file size /
                  self.fromFileSize) as a percentage
              self  :=  self with a callback to self.__poll after
                  POLL_TIME ]
        '''
        # [ if self.child has terminated ->
        #     self.progressVar  :=  100.0
        #     return
        #   else -> I ]
        if not self.child.isalive():
            self.child.close()
            self.progressVar.set(100.0)
            tkMessageBox.showinfo ( "Success",
                 "File %s has been copied to %s, size %s." %
                 (self.fromFileVar.get(), self.toFileVar.get(),
                  self.fromFileSize) )
            return

        # [ if we can stat the output file ->
        #       outFileSize  :=  its size in bytes
        #   else ->
        #       display an error popup
        #       return ]
        toFileName = self.toFileVar.get()
        try:
            toFileSize = self.__measureFile ( toFileName )
        except OSError, details:
            tkMessageBox.showerror ( "Destination file error",
                "File %s: %s" % (toFileName, str(details)) )
            return

        # [ self.progressVar  :=  toFileSize / self.fromFileSize
        #                         as a percentage
        #   self  :=  self with a callback to self.__poll after
        #             POLL_TIME ]
        self.progressVar.set ( 100.0 * float(toFileSize) /
                               float(self.fromFileSize) )
        self.after(POLL_TIME, self.__poll)



# - - - - -   E p i l o g u e

if __name__ == "__main__":
    main()


More information about the Tkinter-discuss mailing list