wxPython BusyInfo widget no longer works - python

In my wxPython GUIs, the wx.BusyInfo widget no longer works. I'm working on OSX, and I recently upgraded to El Capitan.
This simple code below doesn't work anymore with either of the wx versions that I have available ('3.0.2.0' or '2.9.2.4'). As far as I can tell, wx.BusyInfo simply no longer shows up. Unfortunately, I don't know exactly when the widget stopped appearing.
class MyFrame(wx.Frame):
def __init__(self, parent, title):
super(MyFrame, self).__init__(parent, size=(450, 350))
self.panel = wx.Panel(self)
btn = wx.Button(self.panel, wx.ID_ANY, "Do thing")
self.Bind(wx.EVT_BUTTON, self.do_thing)
self.Centre()
self.Show()
def do_thing(self, event):
wait = wx.BusyInfo('Please wait...')
time.sleep(5)
del wait
Any ideas on the cause or solution to this problem?

It looks like something may have changed with respect to when the paint events for the busy info window are processed. What you are seeing is simply that the paint event is not being delivered until after your sleep is done. If you give it a chance to be painted before you block with your busyness (such as calling wx.Yield(True) before) then you should see it working like with earlier versions of OSX. Better yet, if you can organize your busy task so it periodically yields then the system can do things like keep the busy info panel updated and show a real busy cursor instead of the spinning beachball.

I tested the suggested "Yield" workaround.
I also tried using "WindowDisabler", but that didn't work.
My band-aid fix was to refresh the wx.BusyInfo itself, instead of updating it.
For example (in Python):
busy=wx.BusyInfo("Loading corresponding data.")
#then do some work.
busy=wx.BusyInfo("Processing data for display.") #instead of busy.UpdateLabel("text")
#then do some different work.
#work done, time to let the BusyInfo object go.
del busy

Related

Why can I not force a PyQt tabbed widget to resize? [duplicate]

This question already has an answer here:
adjust size after child widget resized in Qt
(1 answer)
Closed 2 years ago.
I have a QDialog containing a QTabWidget, whose tabs vary in size. Following these instructions, which are similar to another stack overflow answer, I have the following code (self.tabs is a dictionary of the tab widgets):
class Panel(NXDialog):
def __init__(self, panel, parent=None):
super(Panel, self).__init__(parent)
self.tabwidget = QtWidgets.QTabWidget()
self.tabwidget.currentChanged.connect(self.update)
...
#property
def tab(self):
return self.tabwidget.currentWidget()
def update(self):
if self.tabwidget.count() == 0:
self.setVisible(False)
else:
for tab in [tab for tab in self.tabs if tab is not self.tab]:
try:
self.tabs[tab].setSizePolicy(QtWidgets.QSizePolicy.Ignored,
QtWidgets.QSizePolicy.Ignored)
self.tabs[tab].update()
except Exception:
pass
self.tab.setSizePolicy(QtWidgets.QSizePolicy.Preferred,
QtWidgets.QSizePolicy.Preferred)
self.tab.update()
self.adjustSize()
When I add a new tab, this works very well. The tab does resize as I want. However, subsequent changes in the tab selection have no effect, even though my debugger confirms that this update function is called.
My application has an embedded shell that shares a namespace with the GUI so I can address all the PyQt widgets. If I call tab.adjustSize() in the shell, where tab points to the selected tab, the tab does resize! So my question is why the adjustSize function is ignored when I make the selection and trigger the currentChanged signal using the GUI, but is not ignored when I call the same slot function a little later from a shell. I've tried adding a sleep time and recursively cycling through the parent widgets calling adjustSize for each of them, but it has no effect. I've also tried adding self.repaint() as well as programmatically changing focus and back again, but nothing works except typing it from the shell.
I'm running PyQt5 v5.12.5 on Python 3.8 but I get the same behavior with PyQt v5.9.
Another stackoverflow question has helped me solve the problem. It seems that the previous changes in SizePolicy might still have been waiting in the GUI event loop. Clearing the loop first ensures that the adjustSize works.
This is all that is needed.
QtWidgets.QApplication.instance().processEvents()
self.adjustSize()
I confess I'm surprised this issue hasn't come up for me before, but I hope it's helpful to anyone else with the same issue.

wxpython frame initialization error with threads

Here's an example:
class DemoFrame(wx.Frame):
def __init__(self, parent):
wx.Frame.__init__(self, parent)
self.panel = wx.Panel(self, -1)
...
initialize other elements
...
self.DoStuff()
def DoStuff(self):
self.panel.SetBackGroundColour(wx.Colour(240, 240, 240))
...
do something
...
Now as you know this is definitely not a good example of initializing your GUI since do something would most probably freeze the GUI while it's running, so I tweaked it to this:
import threading
class DemoFrame(wx.Frame):
def __init__(self, parent):
wx.Frame.__init__(self, parent)
self.panel = wx.Panel(self, -1)
...
initialize other elements
...
DoStuffThead = threading.Thread(target = self.DoStuff, ())
DoStuffThead.start()
def DoStuff(self):
wx.CallAfter(self.ChangeBG, )
...
do something
...
def ChangeBG(self):
self.panel.SetBackGroundColour(wx.Colour(240, 240, 240))
Above code should work exactly the same as the first one does when do something is blank, but to my surprise I noticed there's little background drawing glitches when running the latter codes.
What part went wrong? Isn't this the right way to update GUI in threads?
It's a bad approach to update GUI from worker thread, not event saying it's not thread safe.
You have to communicate with main thread to update GUI.
The best way to achieve desired result is to user wx.PostEvent method. You can create custom events for your needs, inheriting from wx.PyEvent, and you better inherit threading.Thread to keep window you want to communicate within that thread class as an instance variable.
The best illustration of how to update GUI having long-running task can be found in wxPython wiki (first example).
After searching and playing with wxpython for a while, I finally found a solution for this, and it's actually quite simple, just refresh the panel and everything will be all right(add this line into ChangeBG method): self.panel.refresh(). I've no idea why the glitch exists though.
As to Rostyslav's answer, thanks a lot mate!
"It's a bad approach to update GUI from worker thread:", I think you mean it's rude to directly insert GUI codes into worker thread(which is exactly what I did in the first example) in terms of thread-safety concern, basically those GUI codes should be wrapped into thread-safe method(which is exactly what I was trying to do in my second example) and then queued into GUI main thread.
I found that there are basically three thread-safe methods as to GUI updating in worker thread: wx.PostEvent, wx.CallAfter and wx.CallLater, but I never liked wx.PostEvent, it's kind of cumbersome and you have to come up with your own event too, that's why wx.CallAfter is a better choice for me, it's more pythonic and easy to use, and actually wx.CallAfter is like a high level wrapper for wx.PostEvent if you check out the source code in _core.py:
def CallAfter(callable, *args, **kw):
"""
Call the specified function after the current and pending event
handlers have been completed. This is also good for making GUI
method calls from non-GUI threads. Any extra positional or
keyword args are passed on to the callable when it is called.
:see: `wx.CallLater`
"""
app = wx.GetApp()
assert app is not None, 'No wx.App created yet'
if not hasattr(app, "_CallAfterId"):
app._CallAfterId = wx.NewEventType()
app.Connect(-1, -1, app._CallAfterId,
lambda event: event.callable(*event.args, **event.kw) )
evt = wx.PyEvent()
evt.SetEventType(app._CallAfterId)
evt.callable = callable
evt.args = args
evt.kw = kw
wx.PostEvent(app, evt)
Well, I never tried wx.PostEvent implementation in my app, but I'm sure it would work as well.
Oh also I found this article very helpful: wxPython and Threads

Creating Toplevel widgets that are thread safe

I'm trying to learn how to use the thread module. I followed along with the instructions here: http://effbot.org/zone/tkinter-threads.htm
My hope is the test script will:
Print out the "count" every two seconds
Show a pop-up dialog window (also every 2 seconds)
The pop-ups should be allowed to accumulate (if I don't click "OK" for a while, there should be
multiple pop-ups)
However, when I run this script it will freeze the main window and after a while crash. I think I'm not implementing the thread module correctly.
Could someone please have a look and point out what I'm doing wrong?
Here is what I've tried so far:
from Tkinter import *
import thread
import Queue
import time
class TestApp:
def __init__(self, parent):
self.super_Parent = parent
self.main_container = Frame(parent)
self.main_container.pack()
self.top_frame = Frame(self.main_container)
self.top_frame.pack(side=TOP)
self.bottom_frame = Frame(self.main_container)
self.bottom_frame.pack(side=TOP)
self.text_box = Text(self.top_frame)
self.text_box.config(height=20, width=20)
self.text_box.pack()
self.queue = Queue.Queue()
self.update_me()
def show_popup(self):
self.my_popup = Toplevel(self.main_container)
self.my_popup.geometry('100x100')
self.popup_label = Label(self.my_popup, text="Hello!")
self.popup_label.pack(side=TOP)
self.pop_button = Button(self.my_popup, text="OK", command=self.my_popup.destroy)
self.pop_button.pack(side=TOP)
def write(self, line):
self.queue.put(line)
def update_me(self):
try:
while 1:
line = self.queue.get_nowait()
if line is None:
self.text_box.delete(1.0, END)
else:
self.text_box.insert(END, str(line))
self.text_box.see(END)
self.text_box.update_idletasks()
except Queue.Empty:
pass
self.text_box.after(100, self.update_me)
def pipeToWidget(input, widget):
widget.write(input)
def start_thread():
thread.start_new(start_test, (widget,))
def start_test(widget):
count = 0
while True:
pipeToWidget(str(count) + "\n", widget)
count += 1
time.sleep(2)
widget.show_popup()
root = Tk()
widget = TestApp(root)
start_button = Button(widget.bottom_frame, command=start_thread)
start_button.configure(text="Start Test")
start_button.pack(side=LEFT)
root.title("Testing Thread Module")
root.mainloop()
I can't reproduce your problem, but I can see why it would happen.
You're using the queue to pass messages from the background thread to the main thread for updating text_box, which is correct. But you're also calling widget.show_popup() from the background thread, which means it creates and displays a new Toplevel in the background thread. That's not correct.
All UI code must run in the same thread—not all UI code for each top-level window, all UI code period. On some platforms, you may get away with running each window in its own thread (or even free-threading everything), but that isn't supposed to work, and definitely will crash or do improper things on some platforms. (Also, that single UI thread has to be the initial thread on some platforms, but that isn't relevant here.)
So, to fix this, you need to do the same dance for creating the popups that you do for updating the textbox.
The obvious way to do that is to move the widget.show_popup() to the loop in update_me(). If you want it to happen 2 seconds after the textbox updates, just add self.top_frame.after(2000, self.show_popup) to the method.
But I'm guessing you're trying to teach yourself how to have multiple independent updating mechanisms, so telling you "just use a single update queue for everything" may not be a good answer. In that case, just create two queues, and a separate update method servicing each queue. Then, do your pipeToWidget, sleep 2 seconds, then pipeToPopup.
Another way around this is to use mtTkinter. It basically does exactly what you're doing, but makes it automatic, pushing each actual Tk GUI call onto a queue to be run later by the main loop. Of course your objects themselves have to be thread-safe, and this also means that you have to deal with the GUI calls from one thread getting interleaved with calls from another thread. But as long as neither of those is a problem (and they don't seem to be in your case), it's like magic.
If you want to know why this is freezing and/or crashing for you on Win7 and not for me on OS X 10.8… well, you really need to look into a mess of Tcl, C, and Python code, and also at how each thing is built. And, unless it's something simple (like your Tk build isn't free-threaded), it wouldn't tell you much anyway. The code isn't supposed to work, and if it seems to work for me… that probably just means it would work every time until the most important demo of my career, at which point it would fail.

Python: How to show a dialog window, and work simultaneously

Consider this:
import wx, time
# STEP 1: Setup Window
class Window(wx.Frame):
def __init__(self,parent,id):
wx.Frame.__init__(self,parent,id,'Bobby the Window', size=(300,200))
panel = wx.Panel(self)
self.Bind(wx.EVT_CLOSE, self.closewindow)
wx.StaticText(panel, -1, "Yo user, the program is busy doing stuff!", (50,100), (200,100))
def closewindow(self, event):
self.Destroy()
# STEP 2: Show Window (run in background, probably with threading, if that is the best way)
if __name__=='__main__':
app = wx.PySimpleApp()
frame = Window(parent=None,id=-1)
frame.Show()
app.MainLoop()
# STEP 3: Do work
time.sleep(5)
# STEP 4: Close Window, and show user evidence of work
## *Update window to say: "I am done, heres what I got for you: ^blah blah info!^"*
exit()
My questions are:
In step 4, how do I change the text in the window (esp. if it is in a thread)?
And in step 2, how do I run the window in the background, but still be able to communicate with it? (to update the text and such)
This is similar to my question about how to run a cli progress bar and work at the same time, except with gui windows.
I know that to change StaticText, I would do 'text.SetLabel("BLAH!")', but how would I communicate that with the window class if it is running in the background?
Update:
This thread was also some help.
If you need to execute a long running process, then you will have to use some type of threading or perhaps the multiprocessing module. Otherwise you will block the GUI's main loop and make it unresponsive. You will also need to use wxPython's threadsafe methods to communicate with the GUI from the thread. They are wx.CallAfter, wx.CallLater and wx.PostEvent.
You can read more about wxPython and threads at the following:
http://wiki.wxpython.org/LongRunningTasks
http://www.blog.pythonlibrary.org/2010/05/22/wxpython-and-threads/

Why does my buffered GraphicsContext application have a flickering problem?

import wx
class MainFrame(wx.Frame):
def __init__(self,parent,title):
wx.Frame.__init__(self, parent, title=title, size=(640,480))
self.mainPanel=DoubleBufferTest(self,-1)
self.Show(True)
class DoubleBufferTest(wx.Panel):
def __init__(self,parent=None,id=-1):
wx.Panel.__init__(self,parent,id,style=wx.FULL_REPAINT_ON_RESIZE)
self.SetBackgroundColour("#FFFFFF")
self.timer = wx.Timer(self)
self.timer.Start(100)
self.Bind(wx.EVT_TIMER, self.update, self.timer)
self.Bind(wx.EVT_PAINT,self.onPaint)
def onPaint(self,event):
event.Skip()
dc = wx.MemoryDC()
dc.SelectObject(wx.EmptyBitmap(640, 480))
gc = wx.GraphicsContext.Create(dc)
gc.PushState()
gc.SetBrush(wx.Brush("#CFCFCF"))
bgRect=gc.CreatePath()
bgRect.AddRectangle(0,0,640,480)
gc.FillPath(bgRect)
gc.PopState()
dc2=wx.PaintDC(self)
dc2.Blit(0,0,640,480,dc,0,0)
def update(self,event):
self.Refresh()
app = wx.App(False)
f=MainFrame(None,"Test")
app.MainLoop()
I've come up with this code to draw double buffered GraphicsContext content onto a panel, but there's a constant flickering across the window. I've tried different kinds of paths, like lines and curves but it's still there and I don't know what's causing it.
You get flicker because each Refresh() causes the background to get erased before calling onPaint. You need to bind to EVT_ERASE_BACKGROUND and make it a no-op.
class DoubleBufferTest(wx.Panel):
def __init__(self,parent=None,id=-1):
# ... existing code ...
self.Bind(wx.EVT_ERASE_BACKGROUND, self.onErase)
def onErase(self, event):
pass
# ... existing code ...
If you're using a relatively modern wxWidgets, you can use wx.BufferedPaintDC and avoid having to muck around with the memory DC and painting and blitting on your own. Also, on windows, FULL_REPAINT_ON_RESIZE often causes flickering even when you're not resizing the window due to funny things going on under the covers - if you don't need it, going with NO_FULL_REPAINT_ON_RESIZE may help. Otherwise, you'll want to simplify your code some to make sure you can get the simplest thing to work, and perhaps take a look at the DoubleBufferedDrawing wiki page at wxpython.org.

Categories

Resources