Python: Does PubSub and WxPython with Threading require wx.CallAfter? - python

I am using:
wxPython 4.0.7.post2
Pypubsub 4.0.3
Python 3.8.1
I have the following example program I have written:
import wx
import time
from threading import Thread
from pubsub import pub
TIME_UPDATED = "time.updated"
class MyFrame(wx.Frame):
def __init__(self):
super().__init__(parent=None, title="Example")
self.text = wx.StaticText(self, label="I will display seconds elapsed!")
self.othertext = wx.StaticText(self, label="I will Update")
sizer = wx.BoxSizer(orient=wx.VERTICAL)
sizer.Add(self.text)
sizer.Add(self.othertext)
self.SetSizer(sizer)
self.timer = wx.Timer(self)
pub.subscribe(self.UpdateTime, TIME_UPDATED)
self.Bind(wx.EVT_TIMER, self.OnTime, self.timer)
self.Show()
self.i = 0
self.timer.Start(500)
def OnTime(self, _):
self.i += 1
self.othertext.SetLabel(str(self.i))
def UpdateTime(self, seconds):
self.text.SetLabel("{seconds} seconds have elapsed".format(seconds=seconds))
self.text.Refresh()
class BackgroundThread(Thread):
def run(self):
time_elapsed = 0
while True:
# Lets sleep 1 second
time.sleep(1)
time_elapsed += 1
# <<<<---- This line is what I am worried about.
pub.sendMessage(TIME_UPDATED, seconds=time_elapsed)
if __name__ == '__main__':
app = wx.App()
frame = MyFrame()
background = BackgroundThread(daemon=True)
background.start()
app.MainLoop()
I am performing a pub.sendMessage(TIME_UPDATED, seconds=time_elapsed) without a wx.CallAfter and it seems to be working fine. I am not sure why.
Could someone please explain if wx.CallAfter is necessary anymore?
If it is can you explain why that is? Is it that some wx methods put something onto the dispatch queue whereas others do not?

Yes, you should still ensure that UI operations occur on the UI thread. Just because something is not safe to do does not mean that it doesn't happen to work okay (or apprear to work okay) in some cases.

Related

PyQT5 - Add line one by one using a pause

The following code draw single random lines every merry one second. What I would like to do is to keep each line already drawn. What is the best way to do that ?
I know that I need to use a QTimer to do a responsive user interface but first I need to know how to draw more and more lines...
Maybe one way would be to draw all lines hidden and to show more and more lines... Or must I use a QGraphicsView ?
from random import random
import sys
from time import sleep
from PyQt5.QtWidgets import QWidget, QApplication
from PyQt5.QtGui import QPainter
from PyQt5.QtCore import QTimer
LINES = [
(500*random(), 500*random(), 500*random(), 500*random())
for _ in range(50)
]
class Interface(QWidget):
def __init__(self):
super().__init__()
self.max = len(LINES)
self.cursor = 0
self.painter = QPainter()
self.setFixedSize(500, 500)
self.show()
def paintEvent(self, e):
self.painter.begin(self)
self.drawsetpbystep()
self.painter.end()
def drawsetpbystep(self):
if self.cursor < self.max:
self.painter.drawLine(*LINES[self.cursor])
self.update()
sleep(0.25)
self.cursor += 1
if __name__ == '__main__':
app = QApplication(sys.argv)
interface = Interface()
sys.exit(app.exec_())
It is not recommended to use sleep in a GUI, and in the case of PyQt it is very dangerous, because Qt offers alternatives to create the same effect as QTimer, QEventLoop, etc.
Another error is that the QPainter has a very large life cycle, it should only be created and called in paintEvent.
And the last mistake is wanting to pause the task of paintEvent since you're doing it through the drawsetpbystep method. the paintEvent method not only will you use it but actually uses the application whenever you need it, the right thing to do is use a flag to indicate when you should paint as shown below:
LINES = [
(500*random(), 500*random(), 500*random(), 500*random())
for _ in range(50)
]
class Interface(QWidget):
def __init__(self):
super().__init__()
self.max = len(LINES)
self.cursor = 0
self.show()
self.paint = False
timer = QTimer(self)
timer.timeout.connect(self.onTimeout)
timer.start(250)
def paintEvent(self, e):
painter = QPainter(self)
if self.paint:
self.drawsetpbystep(painter)
def onTimeout(self):
self.paint = True
self.update()
def drawsetpbystep(self, painter):
if self.cursor < self.max:
painter.drawLine(*LINES[self.cursor])
self.cursor += 1
self.paint = False
if __name__ == '__main__':
app = QApplication(sys.argv)
interface = Interface()
sys.exit(app.exec_())
Using time.sleep in PyQt applications is not recommended because it blocks execution of the Qt event loop which is responsible for handling user input (via keyboard and mouse) and actually drawing the application window.
Instead, you should use QTimer to schedule execution of a specified method at the times you want. In this case, you probably want to use multiple QTimer.singleShot calls. Likely the first method called by the timer will draw one point/line and then set up a timer to call another method which will draw one point/line and set up a timer to call another method...etc. etc.

wx.ProgressDialog causing seg fault and/or GTK_IS_WINDOW failure when being destroyed

This only happens on Linux (possible OS X also, can't test atm), works fine on Windows.
I have a wx.ProgressDialog that is spawned with the main thread. I send the work off to another thread, and it periodically calls back to a callback function in the main thread that will update the ProgressDialog or, at the end of the work, destroy it. However, I get an interesting message on Linux when this happens:
(python:12728): Gtk-CRITICAL **: IA__gtk_window_set_modal: assertion 'GTK_IS_WINDOW (window)' failed
The dialog does close, but if I try to spawn it again it looks like it's already almost finished. Sometimes a seg fault will follow this message as well.
I've tried to simulate it with a stripped down version here:
import wxversion
wxversion.select("2.8")
import wx
import sys
import threading
MAX_COUNT = 100
## This class is in a different area of the codebase and
class WorkerThread(threading.Thread):
def __init__(self, callback):
threading.Thread.__init__(self)
self.callback = callback
def run(self):
# simulate work done. IRL, this calls another function in another
# area of the codebase. This function would generate an XML document,
# which loops through a list of items and creates a set of elements for
# each item, calling back after each item. Here, we simply set up a for
# loop and simulate work with wx.MilliSleep
for i in xrange(MAX_COUNT):
print i
wx.MilliSleep(30)
wx.CallAfter(self.callback, i)
# Send done signal to GUI
wx.CallAfter(self.callback, -1)
class Frame(wx.Frame):
def __init__(self, title):
wx.Frame.__init__(self, None, title=title, pos=(150,150), size=(350,200))
panel = wx.Panel(self)
box = wx.BoxSizer(wx.VERTICAL)
m_btn = wx.Button(panel, wx.ID_ANY, "Run Stuff")
m_btn.Bind(wx.EVT_BUTTON, self.OnRunButton)
box.Add(m_btn, 0, wx.ALL, 10)
panel.SetSizer(box)
panel.Layout()
def OnRunButton(self, event):
self.progressDialog = wx.ProgressDialog("Doing work",
"Doing Work",
maximum=MAX_COUNT, parent=self,
style=wx.PD_APP_MODAL | wx.PD_ELAPSED_TIME)
self.worker(self.threadCallback)
self.progressDialog.ShowModal()
def worker(self, callback):
# This bit is in another part of the codebase originally. In the test,
# I could have added it to OnRunButton, but I wanted function calls to
# be similar between test and actual code
thread = WorkerThread(callback)
thread.start()
def threadCallback(self, info):
# We update based on position, or destroy if we get a -1
if info == -1:
self.progressDialog.Destroy()
else:
self.progressDialog.Update(info)
app = wx.App(redirect=False)
top = Frame("ProgressDialog Test")
top.Show()
app.MainLoop()
(we select 2.8, but ideally any fix should work in both 2.8 and 3.0. I actually haven't been able to test it in 3.0 in linux due to a bad 3.0 build)
This does a good job at representing the issue: works fine in Windows, but seg fault when it tries to destroy the progress dialog. However, I can't get the example to show the GTK_IS_WINDOW
Ive tried searching for solutions. I've read that it might be due to the fact that the worker thread finishes too quickly, and thus leaves the GUI with some messages in it's queue. I'm not sure I completely understand this (never got the hang of Yields and messages, etc), but what I believe this to mean is that when the worker is at 100%, the ProgressDialog (being slower), might only be at 75%, and still has the extra 25% of messages to use to "Update" the GUI, but instead gets destroyed.
I'd like some clarification on if I'm understanding that correctly or not.
Also, I believe .Hide() works as a work around, but I'd like to Destroy it instead because that's the proper thing to do.
Regardless, any help would be greatly appreciated. =)
I've tried your code, also many modifications been tried to overcome this issue, but failed. Anyway, I've created the following wxPython script to fulfill your purpose, see below:
import wxversion
wxversion.select("2.8") # version 3.0 works, too.
import wx
import sys
import threading
import time
MAX_COUNT = 200
class WorkerThread(threading.Thread):
def __init__(self, target, countNum):
threading.Thread.__init__(self, target = target)
self.setDaemon(True)
self.cnt = countNum
self.target = target
self.pb = self.target.pb
def run(self):
for i in xrange(self.cnt):
print i+1
wx.MilliSleep(50)
wx.CallAfter(self.pb.SetValue, i+1)
wx.CallAfter(self.target.MakeModal, False)
wx.CallAfter(self.target.Close)
class ProgressBarFrame(wx.Frame):
def __init__(self, parent, title, range = 100) :
wx.Frame.__init__(self, parent = parent, title = title)
self.range = range
self.createProgressbar()
self.SetMinSize((400, 10))
self.Centre()
self.Show()
self.t0 = time.time()
self.elapsed_time_timer.Start(1000)
def createProgressbar(self):
self.pb = wx.Gauge(self)
self.pb.SetRange(range = self.range)
self.elapsed_time_st = wx.StaticText(self, label = 'Elapsed Time:')
self.elapsed_time_val = wx.StaticText(self, label = '00:00:00')
vbox_main = wx.BoxSizer(wx.VERTICAL)
hbox_time = wx.BoxSizer(wx.HORIZONTAL)
hbox_time.Add(self.elapsed_time_st, 0, wx.ALIGN_LEFT | wx.EXPAND | wx.ALL, 5)
hbox_time.Add(self.elapsed_time_val, 0, wx.ALIGN_LEFT | wx.EXPAND | wx.ALL, 5)
vbox_main.Add(self.pb, 0, wx.EXPAND | wx.ALL, 5)
vbox_main.Add(hbox_time, 0, wx.EXPAND | wx.ALL, 5)
self.SetSizerAndFit(vbox_main)
self.elapsed_time_timer = wx.Timer(self)
self.Bind(wx.EVT_TIMER, self.onTickTimer, self.elapsed_time_timer)
def onTickTimer(self, event):
fmt='%H:%M:%S'
self.elapsed_time_val.SetLabel(time.strftime(fmt, time.gmtime(time.time()-self.t0)))
class Frame(wx.Frame):
def __init__(self, title):
wx.Frame.__init__(self, None, title=title, pos=(150,150), size=(350,200))
panel = wx.Panel(self)
box = wx.BoxSizer(wx.VERTICAL)
m_btn = wx.Button(panel, wx.ID_ANY, "Run Stuff")
self.Bind(wx.EVT_BUTTON, self.OnRunButton, m_btn)
box.Add(m_btn, 0, wx.ALL, 10)
panel.SetSizer(box)
def OnRunButton(self, event):
self.progressbar = ProgressBarFrame(self, 'Working Processing', MAX_COUNT)
self.progressbar.MakeModal(True)
worker = WorkerThread(self.progressbar, MAX_COUNT)
worker.start()
app = wx.App(redirect=False)
top = Frame("ProgressDialog Test")
top.Show()
app.MainLoop()
I'm using wx.Gauge to do what wx.ProgressDialog does, as well as an additional wx.Timer to show the elapsed time. MakeModal() method is used to mimic the ShowModal effect which is the default style that Dialog shows, do not forget to release the Modal status by MakeModal(False) or the frame would be freezed. You can add more stuff in the ProgressBarFrame class.
I'm thinking the segment fault error may arise from the events calling, especially when multithreading issue is involved, maybe carefully inspect into the wx.ProgressDialog class would show some clue.

placing values on TextCtrl in subthread cause doesn't always work and cause random segmentation faults

I'm trying to create a gui application using wxpython and I got some issues with the TextCtrl element. The effect that I am trying to achieve is that the user will enter a command to a text field (command) and the command might pop a message that appears in the `(out) field. After some time (0.7 seconds on this example) the message will get back to a default message ("OutPut"). I have two problems:
The message doesn't always appear.
The program sometime crashes due to a segmentation fault and I don't get any Error message to handle that.
I guess that the two related in some way, but I don't know why. In the following example, I only type "test" and wait until the original message appear. Both problems happen on that scenario.
I post here two files that serve as smallest working example. File number 1, creates the GUI,
import wx
import os.path
import os
from threading import Thread
from time import sleep
from MsgEvent import *
class MainWindow(wx.Frame):
def __init__(self):
super(MainWindow, self).__init__(None, size=(400,200),)
#style=wx.MAXIMIZE)
self.CreateInteriorWindowComponents()
self.CreateKeyBinding()
self.command.SetFocus()
self.Layout()
def Test(self):
self.command.SetValue('open')
self.ParseCommand(None)
def PostMessage(self,msg):
'''For its some reason, this function is called twice,
the second time without any input. I could'nt understand why.
For that, the test :if msg == None'''
if msg == None: return
worker = MessageThread(self,msg,0.7,'OutPut')
worker.start()
def CreateKeyBinding(self):
self.command.Bind(wx.EVT_CHAR,self.KeyPressed)
def KeyPressed(self,event):
char = event.GetUniChar()
if char == 13 and not event.ControlDown(): #Enter
if wx.Window.FindFocus() == self.command:
self.ParseCommand(event)
else:
event.Skip()
def ParseCommand(self,event):
com = self.command.GetValue().lower() #The input in the command field
self.PostMessage(com)
def CreateInteriorWindowComponents(self):
''' Create "interior" window components. In this case it is just a
simple multiline text control. '''
self.panel = wx.Panel(self)
font = wx.SystemSettings_GetFont(wx.SYS_SYSTEM_FONT)
font.SetPointSize(12)
self.vbox = wx.BoxSizer(wx.VERTICAL)
#Out put field
self.outBox = wx.BoxSizer(wx.HORIZONTAL)
self.out = wx.TextCtrl(self.panel, style=wx.TE_READONLY|wx.BORDER_NONE)
self.out.SetValue('OutPut')
self.out.SetFont(font)
self.outBox.Add(self.out,proportion=1,flag=wx.EXPAND,border=0)
self.vbox.Add(self.outBox,proportion=0,flag=wx.LEFT|wx.RIGHT|wx.EXPAND,border=0)
#Setting the backgroudn colour to window colour
self.out.SetBackgroundColour(self.GetBackgroundColour())
#Commands field
self.commandBox = wx.BoxSizer(wx.HORIZONTAL)
self.command = wx.TextCtrl(self.panel, style=wx.TE_PROCESS_ENTER)
self.command.SetFont(font)
self.commandBox.Add(self.command, proportion=1, flag=wx.EXPAND)
self.vbox.Add(self.commandBox, proportion=0, flag=wx.LEFT|wx.RIGHT|wx.EXPAND, border=0)
self.panel.SetSizer(self.vbox)
return
#Close the window
def OnExit(self, event):
self.Close() # Close the main window.
app = wx.App()
frame = MainWindow()
frame.Center()
frame.Show()
app.MainLoop()
And file number 2, called MsgThread.py handle the events.
import wx
import threading
import time
myEVT_MSG = wx.NewEventType()
EVT_MSG = wx.PyEventBinder(myEVT_MSG,1)
class MsgEvent(wx.PyCommandEvent):
""" event to signal that a message is ready """
def __init__(self,etype,eid,msg='',wait=0,msg0=''):
""" create the event object """
wx.PyCommandEvent.__init__(self,etype,eid)
self._msg = unicode(msg)
self._wait_time = wait
self._reset_message = unicode(msg0)
def GetValue(self):
""" return the value from the event """
return self._msg
class MessageThread(threading.Thread):
def __init__(self,parent,msg='',wait=0,msg0=''):
"""
parent - The gui object that shuold recive the value
value - value to handle
"""
threading.Thread.__init__(self)
if type(msg) == int:
msg = unicode(msg)
self._msg = msg
self._wait_time = wait
self._reset_message = msg0
self._parent = parent
print self._msg
def run(self):
""" overide thread.run Don't call this directly, its called internally when you call Thread.start()"""
self._parent.out.SetValue(unicode(self._msg))
time.sleep(self._wait_time)
self._parent.out.SetValue(self._reset_message)
self._parent.MessageFlag = False
event = MsgEvent(myEVT_MSG,-1,self._msg)
wx.PostEvent(self._parent,event)
What is faulty?
WX Python is not thread safe except for 3 functions (wx.CallAfter, wx.CallLater, wx.PostEvent)
So basically what you have to do is ensure you never call any widget directly from within the subthread. You may use events or CallAfter.
You also might want to check this:
Nice writeup about threads and wxpython
This might help as well
Lokla

How to stop a QThread from the GUI

This is a follow up question to a previous one I posted earlier.
The problem is how to stop (terminate|quit|exit) a QThread from the GUI when using the recommended method of NOT subclassing Qthread, but rather vreating a QObject and then moving it to a QThread. Below if a working example. I can start the GUI and the Qthread and I can have the latter update the GUI. However, I cannot stop it. I tried several methods for qthread (quit(), exit(), and even terminate()) to no avail.
Help greatly appreciated.
Here is the complete code:
import time, sys
from PyQt4.QtCore import *
from PyQt4.QtGui import *
class SimulRunner(QObject):
'Object managing the simulation'
stepIncreased = pyqtSignal(int, name = 'stepIncreased')
def __init__(self):
super(SimulRunner, self).__init__()
self._step = 0
self._isRunning = True
self._maxSteps = 20
def longRunning(self):
while self._step < self._maxSteps and self._isRunning == True:
self._step += 1
self.stepIncreased.emit(self._step)
time.sleep(0.1)
def stop(self):
self._isRunning = False
class SimulationUi(QDialog):
'PyQt interface'
def __init__(self):
super(SimulationUi, self).__init__()
self.goButton = QPushButton('Go')
self.stopButton = QPushButton('Stop')
self.currentStep = QSpinBox()
self.layout = QHBoxLayout()
self.layout.addWidget(self.goButton)
self.layout.addWidget(self.stopButton)
self.layout.addWidget(self.currentStep)
self.setLayout(self.layout)
self.simulRunner = SimulRunner()
self.simulThread = QThread()
self.simulRunner.moveToThread(self.simulThread)
self.simulRunner.stepIncreased.connect(self.currentStep.setValue)
self.stopButton.clicked.connect(simulThread.qui) # also tried exit() and terminate()
# also tried the following (didn't work)
# self.stopButton.clicked.connect(self.simulRunner.stop)
self.goButton.clicked.connect(self.simulThread.start)
self.simulThread.started.connect(self.simulRunner.longRunning)
self.simulRunner.stepIncreased.connect(self.current.step.setValue)
if __name__ == '__main__':
app = QApplication(sys.argv)
simul = SimulationUi()
simul.show()
sys.exit(app.exec_())
I know its long ago but i just stumbled over the same problem.
I have been also searching for an appropriate way to do this. Finally it was easy. When exiting the application the task needs to be stopped and the thread needs to be stopped calling its quit method. See stop_thread method on bottom. And you need to wait for the thread to finish. Otherwise you will get QThread: Destroyed while thread is still running' message at exit.
(I also changed my code to use pyside)
import time, sys
from PySide.QtCore import *
from PySide.QtGui import *
class Worker(QObject):
'Object managing the simulation'
stepIncreased = Signal(int)
def __init__(self):
super(Worker, self).__init__()
self._step = 0
self._isRunning = True
self._maxSteps = 20
def task(self):
if not self._isRunning:
self._isRunning = True
self._step = 0
while self._step < self._maxSteps and self._isRunning == True:
self._step += 1
self.stepIncreased.emit(self._step)
time.sleep(0.1)
print "finished..."
def stop(self):
self._isRunning = False
class SimulationUi(QDialog):
def __init__(self):
super(SimulationUi, self).__init__()
self.btnStart = QPushButton('Start')
self.btnStop = QPushButton('Stop')
self.currentStep = QSpinBox()
self.layout = QHBoxLayout()
self.layout.addWidget(self.btnStart)
self.layout.addWidget(self.btnStop)
self.layout.addWidget(self.currentStep)
self.setLayout(self.layout)
self.thread = QThread()
self.thread.start()
self.worker = Worker()
self.worker.moveToThread(self.thread)
self.worker.stepIncreased.connect(self.currentStep.setValue)
self.btnStop.clicked.connect(lambda: self.worker.stop())
self.btnStart.clicked.connect(self.worker.task)
self.finished.connect(self.stop_thread)
def stop_thread(self):
self.worker.stop()
self.thread.quit()
self.thread.wait()
if __name__ == '__main__':
app = QApplication(sys.argv)
simul = SimulationUi()
simul.show()
sys.exit(app.exec_())
I found out that my original question was actually two questions in one: in order to stop a secondary thread from the main one, you need two things:
Be able to communicate from the main thread down to the secondary thread
Send the proper signal to stop the thread
I haven't been able to solve (2), but I figured out how to solve (1), which gave me a workaround to my original problem. Instead of stopping the thread, I can stop the thread's processing (the longRunning() method)
The problem is that a a secondary thread can only respond to signals if it runs its own event loop. A regular Qthread (which is what my code used) does not. It is easy enough, though, to subclass QThread to that effect:
class MyThread(QThread):
def run(self):
self.exec_()
and used self.simulThread = MyThread() in my code instead of the original self.simulThread = Qthread().
This ensures that the secondary thread runs an event loop. That was not enough, though. The longRunning() method needs to have a chance to actually process the event coming down from the main thread. With the help of this SO answer I figured out that the simple addition of a QApplication.processEvent() in the longRunning() method gave the secondary thread such a chance. I can now stop the processing carried out in the secondary thread, even though I haven't figured out how to stop the thread itself.
To wrap up. My longRunning method now looks like this:
def longRunning(self):
while self._step < self._maxSteps and self._isRunning == True:
self._step += 1
self.stepIncreased.emit(self._step)
time.sleep(0.1)
QApplication.processEvents()
and my GUI thread has these three lines that do the job (in addition to the QThread subclass listed above):
self.simulThread = MyThread()
self.simulRunner.moveToThread(self.simulThread)
self.stopButton.clicked.connect(self.simulRunner.stop)
Comments are welcome!
You can stop the thread by calling exit() or quit() . In extreme cases, you may want to forcibly terminate() an executing thread. However, doing so is dangerous and discouraged. Please read the documentation for terminate() and setTerminationEnabled() for detailed information.
src: https://doc.qt.io/qtforpython/PySide2/QtCore/QThread.html

wxpython - Running threads sequentially without blocking GUI

I've got a GUI script with all my wxPython code in it, and a separate testSequences module that has a bunch of tasks that I run based on input from the GUI. The tasks take a long time to complete (from 20 seconds to 3 minutes), so I want to thread them, otherwise the GUI locks up while they're running. I also need them to run one after another, since they all use the same hardware. (My rationale behind threading is simply to prevent the GUI from locking up.) I'd like to have a "Running" message (with varying number of periods after it, i.e. "Running", "Running.", "Running..", etc.) so the user knows that progress is occurring, even though it isn't visible. I'd like this script to run the test sequences in separate threads, but sequentially, so that the second thread won't be created and run until the first is complete. Since this is kind of the opposite of the purpose of threads, I can't really find any information on how to do this... Any help would be greatly appreciated.
Thanks in advance!
gui.py
import testSequences
from threading import Thread
#wxPython code for setting everything up here...
for j in range(5):
testThread = Thread(target=testSequences.test1)
testThread.start()
while testThread.isAlive():
#wait until the previous thread is complete
time.sleep(0.5)
i = (i+1) % 4
self.status.SetStatusText("Running"+'.'*i)
testSequences.py
import time
def test1():
for i in range(10):
print i
time.sleep(1)
(Obviously this isn't the actual test code, but the idea is the same.)
You cannot wait in the GUI-thread with a while loop because you block the processing of the event-queue. One solution is to poll the state of the thread with a timer:
import wx
import time
from threading import Thread
def test1():
for i in range(10):
print i
time.sleep(1)
class TestFrame(wx.Frame):
def __init__(self):
wx.Frame.__init__(self, None, -1, "Test")
panel = wx.Panel(self, -1)
sizer = wx.BoxSizer(wx.VERTICAL)
panel.SetSizer(sizer)
self.button = wx.Button(panel, 0, "Start")
sizer.Add(self.button, 0, wx.ALIGN_LEFT)
self.button.Bind(wx.EVT_BUTTON, self.OnButton)
self.text = wx.StaticText(panel, 0, "No test is running")
sizer.Add(self.text, 0, wx.ALIGN_LEFT)
self.timer = wx.Timer(self)
def OnButton(self, event):
self.testThread = Thread(target=test1)
self.testThread.start()
self.text.SetLabel("Running")
self.button.Disable()
self.Bind(wx.EVT_TIMER, self.PollThread)
self.timer.Start(20, oneShot=True)
event.Skip()
def PollThread(self, event):
if self.testThread.isAlive():
self.Bind(wx.EVT_TIMER, self.PollThread)
self.timer.Start(200, oneShot=True)
self.text.SetLabel(self.text.GetLabel() + ".")
else:
self.button.Enable()
self.text.SetLabel("Test completed")
app = wx.PySimpleApp()
TestFrame().Show()
app.MainLoop()
Figured out a way to do this. Instead of creating threads in my gui.py, I created a class that inherits from Thread, and runs all the tests in that class, then posts wxPython events when one test is done (so I can update the status bar) and when all tests are done (so I can inform the user that all tests are complete.
myEVT_TESTDONE = wx.NewEventType()
EVT_TESTDONE = wx.PyEventBinder(myEVT_TESTDONE , 1)
myEVT_ALLDONE = wx.NewEventType()
EVT_ALLDONE = wx.PyEventBinder(myEVT_ALLDONE, 1)
class TestDone(wx.PyCommandEvent):
def __init__(self, etype, eid, val=None):
wx.PyCommandEvent.__init__(self, etype, eid)
self._val = val
def GetValue(self):
return self._val
class AllDone(wx.PyCommandEvent):
def __init__(self, etype, eid):
wx.PyCommandEvent.__init__(self, etype, eid)
class TestSequence(Thread):
def __init__(self, parent, queue):
Thread.__init__(self)
self._queue = queue
self._parent = parent
self.start()
def run(self):
testCount = 0
for test in self._queue:
#Time-intensive task goes here
for i in range(10):
print i
sleep(1)
evt = TestDone(myEVT_TESTDONE, -1, i)
wx.PostEvent(self._parent, evt)
evt = AllDone(myEVT_ALLDONE, -1)
wx.PostEvent(self._parent, evt)
class MainSequence(wx.Frame):
def __init__(self, parent, id, title):
self.Bind(EVT_TESTDONE, self.testDoneEvt)
self.Bind(EVT_ALLDONE, self.allDoneEvt)
#...the rest of the wxPython code
def testDoneEvt(self, event):
#Set what to be done after every test, e.g. update progress bar
step = event.GetValue()
def allDoneEvt(self, event):
#Set what to be done after all tests, e.g. display "Tests complete"
program = wx.App()
window = MainSequence(None, -1, 'App title')
program.MainLoop()

Categories

Resources