wxProgressDialog problems

Larry Bates wrote:

I am trying to implement a file upload dialog (see code below). It almost works, except that I can't get the cancel button to respond as expect. I'm certain it is something really small.

Events (like the cancel button event) won't be delivered if program flow is not in the main loop. In real life that probably won't be a problem for most apps, as long as you don't block some event handler in a long running task. In your sample it can be taken care of by using a timer to update the progress dialog. See LongRunningTasks - wxPyWiki for ideas on how to approach other situations.

     class MyApp(wx.App):
         def OnInit(self):
             self.callback = Callback("test file upload")
             self.callback.start(100)
             self.timer = wx.Timer(self)
             self.Bind(wx.EVT_TIMER, self.OnTimer, self.timer)
             self.timer.Start(200)
             self.count = 0
             return True

         def OnTimer(self, evt):
             if self.count < 100:
                 self.callback(100, self.count)
                 self.count += 1
             else:
                 self.callback.complete()

     app = MyApp(0)
     app.MainLoop()

···

--
Robin Dunn
Software Craftsman
http://wxPython.org Java give you jitters? Relax with wxPython!

Robin Dunn wrote:

Larry Bates wrote:

Robin Dunn wrote:

Larry Bates wrote:

I am trying to implement a file upload dialog (see code below). It almost works, except that I can't get the cancel button to respond as expect. I'm certain it is something really small.

Events (like the cancel button event) won't be delivered if program flow is not in the main loop. In real life that probably won't be a problem for most apps, as long as you don't block some event handler in a long running task. In your sample it can be taken care of by using a timer to update the progress dialog. See LongRunningTasks - wxPyWiki for ideas on how to approach other situations.

    class MyApp(wx.App):
        def OnInit(self):
            self.callback = Callback("test file upload")
            self.callback.start(100)
            self.timer = wx.Timer(self)
            self.Bind(wx.EVT_TIMER, self.OnTimer, self.timer)
            self.timer.Start(200)
            self.count = 0
            return True

        def OnTimer(self, evt):
            if self.count < 100:
                self.callback(100, self.count)
                self.count += 1
            else:
                self.callback.complete()

    app = MyApp(0)
    app.MainLoop()

Robin,

My problem is that the thing that calls this Callback is WAY down deep in
other code. I want to register the Callback instance with that and it
gets called as the file is streamed (once per (65K block) over a WEBDAV connection. I don't see how to adapt what you wrote or the examples on
the wiki like you sent. When the cancel button is pushed it needs to
return False so I can cancel the streaming upload and work my way back
through all the layers back to my main program.

Thoughts?

Do the other code in a thread, and make the callback use wx.CallAfter or post an event so the update of the progress dialog can be done in the gui thread. If the cancel button was pressed then set a flag that the download thread will check periodically so it knows to stop (or send the message in some other thread-safe way).

Real newbie here. It sounds like this would be the best way:

> or post an event so the update of the progress dialog can be done in
> the gui thread.

I "really" tried on this one, but it exhibits the same behavior. Can't ever click on the Cancel button. See sample code below.

Thanks for all your help.

-Larry

import wx
import time
myEVT_UPDATE_PROGRESS = wx.NewEventType()
EVT_UPDATE_PROGRESS = wx.PyEventBinder(myEVT_UPDATE_PROGRESS, 1)

class MyProgressEvent(wx.PyCommandEvent):
     def __init(self, evtType, id):
         wxPYcommendEvent.__init(self, evType, id)
         self.myVal=None

     def SetVal(self, val):
         self.myVal=val

     def GetVal(self):
         return self.myVal

class GUIprogressCallback(object):
     def __init__(self, parent):
         self.uploadTotal=0
         self.uploadPosition=0

···

#
         # Bind my custom event that gets posted by caller to update the progress
         #
         parent.Bind(EVT_UPDATE_PROGRESS, self.update)

     def setUploadTotal(self, uploadTotal):
         '''
         setUploadTotal - if user is doing a mult-file upload and would like to
                          set the total upload size for all files, use this
                          method. Otherwise we will assume a single-file upload
                          and the total will be the size of the file being
                          uploaded.
         '''
         self.uploadTotal=uploadTotal

     def start(self, fileName, fileSize):
         '''
         start - This method is called to start the upload of a file
         '''
         #
         # Truncate the fileName if it is too long to display
         #
         if len(fileName) > 90:
             fileName=".."+fileName[-90:]
         #
         # Create the dialog unless I've already created it (e.g. previous file
         # in a multi-file upload).
         #
         if not hasattr(self, 'dlg'):
             self._createDialog(msg=fileName, maximum=self.uploadTotal)

         #
         # Handle the possibility that files changed sizes between the time that
         # I calculated uploadTotal and when I actually get to uploading them by
         # backing down from uploadTotal, but can't be less than 0.
         #
         if self.uploadPosition+fileSize > self.uploadTotal:
             self.uploadPosition=max(self.uploadTotal-fileSize, 0)
         #
         # Update the dialog with the info
         #
         self.dlg.Update(self.uploadPosition, newmsg=fileName)
         return True

     def update(self, event):
         '''
         update - Events are posted as file upload is progressing. This method
                  is bound so it picks up those events (every block). We
                  calculate the percentage complete and update the progessDialog
                  with this information and update the progress gauge.
         '''
         filePosition=event.GetVal()
         #
         # Handle both single- and mult-file upload percentage complete.
         #
         uploadPosition=self.uploadPosition+filePosition
         #
         # Calculate and display the percent complete and update the position
         # of the progress dialog gauge.
         #
         percentstr=int(100.0*float(uploadPosition)/float(self.uploadTotal))
         msg="WebSafe Upload-%s%%" % percentstr
         self.dlg.SetLabel(msg)
         wx.Yield()
         keepgoing, skip=self.dlg.Update(uploadPosition)
         #
         # See if user clicked the cancel button
         #
         if not keepgoing:
             self.close()

         return True

     def _createDialog(self, msg='', maximum=100):
         #
         # Create progress dialog and display it, but it must run in a different
         # thread or user won't be able to click cancel button.
         #
         self.dlg=wx.ProgressDialog(title="WebSafe Upload",
                                    message="",
                                    maximum=maximum,
                                    style=wx.PD_APP_MODAL |
                                          #wx.PD_AUTO_HIDE | \
                                          wx.PD_CAN_ABORT | \
                                          wx.PD_REMAINING_TIME)

         self.dlg.SetDimensions(-1, -1, width=500, height=-1,
                                sizeFlags=wx.SIZE_AUTO_HEIGHT)

         self.dlg.Show(True)

     def complete(self, fileSize):
         self.uploadPosition=self.uploadPosition+fileSize
         return True

     def close(self):
         self.isRunning=False
         self.dlg.Destroy()
         return False

if __name__ == "__main__":
     import time
     app=wx.PySimpleApp(0)
     wx.InitAllImageHandlers()
     evt=MyProgressEvent(myEVT_UPDATE_PROGRESS, -1)
     blockSize=1<<16
     #
     # Test multi-file upload
     #
     fileNames=['filename1.txt', 'filename2.txt', 'filename3.txt']
     fileSizes=[(1<<20), 2*(1<<20), 3*(1<<20)]
     CB=GUIprogressCallback(app)
     CB.setUploadTotal(sum(fileSizes))
     for fileName, fileSize in zip(fileNames, fileSizes):
         CB.start(fileName, fileSize)
         for i in xrange(blockSize, fileSize, blockSize):
             evt.SetVal(i)
             app.ProcessEvent(evt)
             time.sleep(0.5)

         CB.complete(fileSize)
     CB.close()

Robin Dunn wrote:

Larry Bates wrote:

Robin Dunn wrote:

Larry Bates wrote:

Thoughts?

Do the other code in a thread, and make the callback use wx.CallAfter or post an event so the update of the progress dialog can be done in the gui thread. If the cancel button was pressed then set a flag that the download thread will check periodically so it knows to stop (or send the message in some other thread-safe way).

Real newbie here. It sounds like this would be the best way:

> or post an event so the update of the progress dialog can be done in
> the gui thread.

I "really" tried on this one, but it exhibits the same behavior. Can't ever click on the Cancel button. See sample code below.

Thanks for all your help.

You still are not calling the app's MainLoop method. And you seem to have missed the "Do the other code in a thread" part of my comment.

>>> Do the other code in a thread, and make the callback use wx.CallAfter
>>> or post an event so the update of the progress dialog can be done in
>>> the gui thread.

Sorry, I read your instructions as "Do in a thread and make callback or post
an event..." Note the OR.

I tried running my other code in a separate thread, but Python DAVLIB refuses run in a separate thread and I'm not up to a complete rewrite of that code at the present.

I tried running the MainLoop in another thread, but it makes no difference.

import wx
import time
myEVT_UPDATE_PROGRESS = wx.NewEventType()
EVT_UPDATE_PROGRESS = wx.PyEventBinder(myEVT_UPDATE_PROGRESS, 1)

class MyProgressEvent(wx.PyCommandEvent):
     def __init(self, evtType, id):
         wxPYcommendEvent.__init(self, evType, id)
         self.myVal=None

     def SetVal(self, val):
         self.myVal=val

     def GetVal(self):
         return self.myVal

class GUIprogressCallback(object):
     def __init__(self, parent):
         self.uploadTotal=0
         self.uploadPosition=0

···

#
         # Bind my custom event that gets posted by caller to update the progress
         #
         parent.Bind(EVT_UPDATE_PROGRESS, self.update)

     def setUploadTotal(self, uploadTotal):
         '''
         setUploadTotal - if user is doing a mult-file upload and would like to
                          set the total upload size for all files, use this
                          method. Otherwise we will assume a single-file upload
                          and the total will be the size of the file being
                          uploaded.
         '''
         self.uploadTotal=uploadTotal

     def start(self, fileName, fileSize):
         '''
         start - This method is called to start the upload of a file
         '''
         #
         # Truncate the fileName if it is too long to display
         #
         if len(fileName) > 90:
             fileName=".."+fileName[-90:]
         #
         # Create the dialog unless I've already created it (e.g. previous file
         # in a multi-file upload).
         #
         if not hasattr(self, 'dlg'):
             self._createDialog(msg=fileName, maximum=self.uploadTotal)

         #
         # Handle the possibility that files changed sizes between the time that
         # I calculated uploadTotal and when I actually get to uploading them by
         # backing down from uploadTotal, but can't be less than 0.
         #
         if self.uploadPosition+fileSize > self.uploadTotal:
             self.uploadPosition=max(self.uploadTotal-fileSize, 0)
         #
         # Update the dialog with the info
         #
         self.dlg.Update(self.uploadPosition, newmsg=fileName)
         return True

     def update(self, event):
         '''
         update - Events are posted as file upload is progressing. This method
                  is bound so it picks up those events (every block). We
                  calculate the percentage complete and update the progessDialog
                  with this information and update the progress gauge.
         '''
         filePosition=event.GetVal()
         #
         # Handle both single- and mult-file upload percentage complete.
         #
         uploadPosition=self.uploadPosition+filePosition
         #
         # Calculate and display the percent complete and update the position
         # of the progress dialog gauge.
         #
         percentstr=int(100.0*float(uploadPosition)/float(self.uploadTotal))
         msg="WebSafe Upload-%s%%" % percentstr
         self.dlg.SetLabel(msg)
         wx.Yield()
         keepgoing, skip=self.dlg.Update(uploadPosition)
         #
         # See if user clicked the cancel button
         #
         if not keepgoing:
             self.close()

         return True

     def _createDialog(self, msg='', maximum=100):
         #
         # Create progress dialog and display it, but it must run in a different
         # thread or user won't be able to click cancel button.
         #
         self.dlg=wx.ProgressDialog(title="WebSafe Upload",
                                    message="",
                                    maximum=maximum,
                                    style=wx.PD_APP_MODAL |
                                          #wx.PD_AUTO_HIDE | \
                                          wx.PD_CAN_ABORT | \
                                          wx.PD_REMAINING_TIME)

         self.dlg.SetDimensions(-1, -1, width=500, height=-1,
                                sizeFlags=wx.SIZE_AUTO_HEIGHT)

         self.dlg.Show(True)

     def complete(self, fileSize):
         self.uploadPosition=self.uploadPosition+fileSize
         return True

     def close(self):
         self.isRunning=False
         self.dlg.Destroy()
         return False

class MyApp(wx.PySimpleApp):
     def OnInit(self):
         wx.InitAllImageHandlers(0)
         return True

if __name__ == "__main__":
     import time
     import thread
     app=wx.PySimpleApp(0)
     wx.InitAllImageHandlers()
     thread.start_new_thread(app.MainLoop, ())
     evt=MyProgressEvent(myEVT_UPDATE_PROGRESS, -1)
     blockSize=1<<16
     #
     # Test multi-file upload
     #
     fileNames=['filename1.txt', 'filename2.txt', 'filename3.txt']
     fileSizes=[(1<<20), 2*(1<<20), 3*(1<<20)]
     CB=GUIprogressCallback(app)
     CB.setUploadTotal(sum(fileSizes))
     for fileName, fileSize in zip(fileNames, fileSizes):
         CB.start(fileName, fileSize)
         for i in xrange(blockSize, fileSize, blockSize):
             evt.SetVal(i)
             app.ProcessEvent(evt)
             time.sleep(0.5)

         CB.complete(fileSize)
     CB.close()

Robin Dunn wrote:

Larry Bates wrote:

Robin Dunn wrote:

Larry Bates wrote:

Robin Dunn wrote:

Larry Bates wrote:

Thoughts?

Do the other code in a thread, and make the callback use wx.CallAfter or post an event so the update of the progress dialog can be done in the gui thread. If the cancel button was pressed then set a flag that the download thread will check periodically so it knows to stop (or send the message in some other thread-safe way).

Real newbie here. It sounds like this would be the best way:

> or post an event so the update of the progress dialog can be done in
> the gui thread.

I "really" tried on this one, but it exhibits the same behavior. Can't ever click on the Cancel button. See sample code below.

Thanks for all your help.

You still are not calling the app's MainLoop method. And you seem to have missed the "Do the other code in a thread" part of my comment.

>>> Do the other code in a thread, and make the callback use wx.CallAfter
>>> or post an event so the update of the progress dialog can be done in
>>> the gui thread.

Sorry, I read your instructions as "Do in a thread and make callback or post
an event..." Note the OR.

I tried running my other code in a separate thread, but Python DAVLIB refuses run in a separate thread and I'm not up to a complete rewrite of that code at the present.

I tried running the MainLoop in another thread, but it makes no difference.

To use wx in an alternate thread you need to do almost everything wx-related from that thread, essentially anything that interacts with the UI, including creating or modifying the app and window objects. It is safe to use wx.PostEvent or wx.CallAfter from the non-gui thread in order to pass messages or to invoke other functions in the context of the gui thread.

Also, in your case you'll either need to create a top-level window, or call app.SetExitOnFrameDelete(False), otherwise the MainLoop will exit immediately since there are not any top-level windows existing at the time it is called.

Robin,

I appreciate your attempts to help me but I'm still dead-in-the-water. For some reason the MailLoop does NOT exit immediately (even without app.SetExitOnFrameDelete(False)). The messages that I post via wx.PostEvent in my (non-GUI) loop DO get processed properly and the GUI dialog including the title, message1 and gauge are all updated as expected, but I can't click on the Cancel button (hourglass cursor whenever I hover over the dialog and clicks do nothing). This is soooo... close, that its frustrating.

-Larry