I am new to wxPython, so I basically just copied something that will display a tray icon and made it a thread:
import wx
import wx.adv
from threading import Thread
TRAY_TOOLTIP = 'System Tray Demo'
TRAY_ICON = 'icon.png'
class Main(Thread):
def __init__(self):
Thread.__init__(self)
def run(self):
self.app = App()
self.app.MainLoop()
def create_menu_item(menu, label, func):
item = wx.MenuItem(menu, -1, label)
menu.Bind(wx.EVT_MENU, func, id=item.GetId())
menu.Append(item)
return item
class TaskBarIcon(wx.adv.TaskBarIcon):
def __init__(self, frame):
self.frame = frame
super(TaskBarIcon, self).__init__()
self.set_icon(TRAY_ICON)
self.Bind(wx.adv.EVT_TASKBAR_LEFT_DOWN, self.on_left_down)
def CreatePopupMenu(self):
menu = wx.Menu()
create_menu_item(menu, 'Exit', self.on_exit)
return menu
def set_icon(self, path):
icon = wx.Icon(wx.Bitmap(path))
self.SetIcon(icon, TRAY_TOOLTIP)
def on_left_down(self, event):
print('Tray icon was left-clicked.')
def on_exit(self, event):
wx.CallAfter(self.Destroy)
self.frame.Close()
class App(wx.App):
def __init__(self):
wx.App.__init__(self, False)
def OnInit(self):
frame = wx.Frame(None)
self.SetTopWindow(frame)
TaskBarIcon(frame)
return True
from my main thread, which is a very long running service, I am starting the GUI thread, activating the scheduler and checking if it should run like so:
gui = trayIcon.Main()
gui.start()
schedule.every(60).minutes.do(main)
while gui.is_alive():
schedule.run_pending()
time.sleep(1)
# if this is reached the gui thread has terminated and the program should shut down
sys.exit()
It works as it should be: When the exit item in the tray icon menu is clicked, the GUI thread shuts down, is then no longer detected in the while loop of the main thread and sys.exit() is called.
Unfortunately, wxPython then shows an error dialog with the following text:
wxWidgets Debug Alert
....\src\common\socket.cpp(767): assert "wxIsMainThread()" failed in
wxSocketBase::IsInitialized(): unsafe to call from other threads [in
thread 1284] Do you want to stop the program? You can also choose
[Cancel] to suppress further warnings.
How can I shut down the GUI correctly or at least suppress this warning? After it the program quits as it should be, although I suspect suppressing the message would leave a memory leak of some kind.
Thanks in advance
Taxel
Basically it looks like you have set it up backwards. The main thread should always be the wxPython one. MainLoop should not be called inside a thread. Instead you should let wxPython be in control and run your long running thread inside of it.
Then when your long running task is finished, you can use a thread-safe method, like wx.CallAfter to tell wxPython that it is time to exit. Since wxPython is the one in control, it can close itself correctly.
Here are some articles that might help you:
https://www.blog.pythonlibrary.org/2010/05/22/wxpython-and-threads/
https://wiki.wxpython.org/LongRunningTasks
Related
I have a wxPython TaskbarIcon class with no other Parent window, except a Tray Icon, since it runs as a sort of background process. Provided below is a complete working example -->
import subprocess as sp
import wx.adv
from threading import Thread, Event
import os, signal
####### Create Tray Icon #######
def create_menu_item(menu, label, func):
item = wx.MenuItem(menu, -1, label)
menu.Bind(wx.EVT_MENU, func, id=item.GetId())
menu.Append(item)
return item
class TaskBarIcon(wx.adv.TaskBarIcon):
def __init__(self, frame):
self.frame = frame
super(TaskBarIcon, self).__init__()
self.set_icon(TRAY_ICON)
self.Bind(wx.adv.EVT_TASKBAR_LEFT_UP, self.Run_Now)
self.Bind(wx.adv.EVT_TASKBAR_RIGHT_DCLICK, self.on_exit)
def CreatePopupMenu(self):
menu = wx.Menu()
create_menu_item(menu, 'Open Log_File', self.GoTo_Log)
menu.AppendSeparator()
create_menu_item(menu, 'Exit', self.on_exit)
return menu
def set_icon(self, path):
icon = wx.Icon(path)
self.SetIcon(icon, TRAY_TOOLTIP)
def GoTo_Log(self, event):
print("Log File will be Opened -- :)\n")
# sp.Popen(["explorer.exe", LogFile])
def Run_Now(self, event):
# Run my task
print("Running Now...")
def on_exit(self, event):
print('exiting . . . ')
wx.CallAfter(self.Destroy)
self.frame.Close(True)
self.RemoveIcon()
os.kill(pid, signal.SIGBREAK)
class App(wx.App):
def OnInit(self):
frame= wx.Frame(None)
self.SetTopWindow(frame)
sysTray= TaskBarIcon(frame)
print("Moved to Sys Tray !\n")
return True
################################
if __name__ == '__main__':
app = App(False)
app.MainLoop()
I want here to add the convenience to exit (close SysTray) on keypress (say 'F7'). I first decided to Bind EVT_KEY_DOWN with a method to detect keypress and then call on_exit. But It took me a while to realize that TaskBarIcon does not support EVT_KEY_DOWN, since there's no parent window !
Below is a my func to detect keypress ('F7') :
import keyboard, os, signal
def DetectEND(Force_END = False):
#Long Press "F7" button to Terminate this Background Application
if keyboard.is_pressed('f7') or Force_END:
print("'f7' press detected | Ending Service ...")
# cleanHandlers(B_Log) # OR some other cleaning activities before quitting
os.kill(pid, signal.SIGBREAK) # Using this instead of quit() since this would not run in the main thread !
return True
return False
Basically, I then just tried to use the above as a Thread to look up for the keypress and when returned True... wanted to communicate to the TaskBarIcon instance to call the self.on_exit method, but here too cannot figure out how to communicate without an Event binder !
Also, I am new to this wxpython and so... do not have any proper idea about the "MainLooping" of the wxpython... or else would have set up an threading.Event(), making the DetectEND func to set the Event when True, and then to check for Event.is_set() in the TaskBarIcon class, But I do know that this loop is way different from working on simple while True loops !!
Apart from the approaches mentioned above... I tried other ways too, like wx.CallAfter() from the DetectEND func itself and some global hotkey method, but None did work in this case or sometimes I could not figure out the implementation.
THANK YOU -- for staying with me till this end !
At last would appreciate any method that gets the job done ... and a Windows-specific method would even be too Great.
Some other methods just thought of by me -- like one that does not involve threading (or) a way to bind a Key_down event to TaskBarIcon (or) a workaround for the CallAfter to call the self.on_exit method from another non-class function... (does not matter)
I have a project where a passive GUI runs in its own thread and is manipulated by the main thread. Especially, the window is closed by the main thread using event_generate:
from tkinter import Tk
import threading
import time
import queue
q = queue.Queue()
class Window:
def __init__(self):
self.root = Tk()
self.root.title("test")
self.root.bind("<<custom_close_event>>", self.close)
def close(self, event):
print("quit")
self.root.destroy()
def create_window():
window = Window()
q.put(window)
window.root.mainloop()
print("###########")
# Window creation executed in different thread
t1 = threading.Thread(target=create_window)
t1.start()
window = q.get()
time.sleep(2)
window.root.event_generate("<<custom_close_event>>")
print("end")
The program crashes with the following output:
quit
###########
Tcl_AsyncDelete: async handler deleted by the wrong thread
[1] 21572 IOT instruction (core dumped) python window_test.py
According to this discussion, it seems that the order of objects cleanup in multithreaded environment is in fault. The advice to nullify objects (in my case window) and to call gc.collect did not solve the problem.
How should I do?
Instead of using a separate thread to create a second reference to Tk(),
Just inherit tk.Toplevel when you create the "Window" class.
This will allow you to have really as many windows as you want.
You can use tk.after in order to monitor processes and do pseudo-multithreading things. Here's an example of how to do that
class Window(Toplevel):
def __init__(self, parent, *args, **kwargs):
super().__init__(*args, **kwargs)
self.parent = parent
...
self.parent.after(1000, self.do_something)
def do_something(self):
...
<code>
...
self.parent.after(1000, self.do_something)
root = Tk()
Window(root)
root.mainloop()
Using #AndrewPye's answer but inheriting from Tk instead of Toplevel:
from tkinter import *
class Window(Tk):
def __init__(self, **kwargs):
super().__init__(**kwargs)
super().after(1000, self.do_something)
def do_something(self):
print("I am in a loop that runs every 1000ms = 1s")
super().after(1000, self.do_something)
root = Window()
root.mainloop()
I'm developing an application with pyqt5, which will hide window automatically when it starts to work. And I hope to build a tray icon with context menu to show the window again. Below is my brief code,
class MainWindow(QMainWindow, Ui_MainWindow):
def __init__(self):
super().__init__()
self.setupUi(self)
self.set_sytem_tray_icon()
self.current_status = Stop
self.track_str = ''
self.start_button.clicked.connect(self.start_button_clicked)
self.stop_button.clicked.connect(self.stop_button_clicked)
self.show()
def set_sytem_tray_icon(self):
contextMenu = QMenu()
action = contextMenu.addAction("Show Window")
action.triggered.connect(self.show)
contextMenu.addAction(action)
self.tray_icon = QSystemTrayIcon()
self.tray_icon.setIcon(QtGui.QIcon("img.ico"))
self.tray_icon.setContextMenu(contextMenu)
self.tray_icon.show()
def get_specific_window(self, class_name, title):
def check_ULink_status(self):
try:
while True:
# Doing something
break
def start_button_clicked(self):
self.hide()
thread = threading.Thread(target=self.check_ULink_status)
thread.setDaemon(True)
thread.start()
thread.join()
self.show()
def stop_button_clicked(self):
reply = QMessageBox.information(self, "Warning", "Stop", QMessageBox.Yes, QMessageBox.No)
if reply == QMessageBox.Yes:
self.current_status = Stop
self.change_button_availability()
Here is my problem, when I clicked start button, the application started working but tray icon didn't response to any action. I believe there was some conflict with my main thread, but I still can't figure out what's going on. Does anyone have answer for this?
Update,
I would like to provide another solution using qthread.
class MainWindow(QMainWindow, Ui_MainWindow):
def __init__(self):
super().__init__()
self.setupUi(self)
self.set_sytem_tray_icon()
self.current_status = Stop
self.track_str = ''
self.start_button.clicked.connect(self.start_button_clicked)
self.stop_button.clicked.connect(self.stop_button_clicked)
self.show()
def set_sytem_tray_icon(self):
contextMenu = QMenu()
action = contextMenu.addAction("Show Window")
action.triggered.connect(self.show)
contextMenu.addAction(action)
self.tray_icon = QSystemTrayIcon()
self.tray_icon.setContextMenu(contextMenu)
self.tray_icon.show()
def set_sytem_tray_icon_with_qthread(self):
set_sytem_tray_icon_qthread = qthread_definition.MyQThread2()
set_sytem_tray_icon_qthread.signal.connect(self.set_sytem_tray_icon)
set_sytem_tray_icon_qthread.start()
set_sytem_tray_icon_qthread.wait()
def show_mainwindow_with_qthread(self):
show_mainwindow_qthread = qthread_definition.MyQThread2()
show_mainwindow_qthread.signal.connect(self.show)
show_mainwindow_qthread.start()
show_mainwindow_qthread.wait()
def get_specific_window(self, class_name, title):
# doing something
def check_ULink_status(self):
self.set_sytem_tray_icon_with_qthread() # add new qthread here
try:
while True:
# Doing something
break
self.show_mainwindow_with_qthread()
def start_button_clicked(self):
self.hide()
thread = threading.Thread(target=self.check_ULink_status)
thread.setDaemon(True)
thread.start()
def stop_button_clicked(self):
reply = QMessageBox.information(self, "Warning", "Stop", QMessageBox.Yes, QMessageBox.No)
if reply == QMessageBox.Yes:
self.current_status = Stop
self.change_button_availability()
where MyQThread2 class is shown below,
class MyQThread2(QtCore.QThread):
# this thread is to create the tray icon
signal = QtCore.pyqtSignal()
def __init__(self):
super().__init__()
def run(self):
self.signal.emit()
To show the main window in the thread, we need to create a qthread to finish this task, because showing the window is a kind of modification of qt object, which is not allowed outside main thread.
As the docs of the join() method points out:
join(timeout=None)
Wait until the thread terminates. This blocks the
calling thread until the thread whose join() method is called
terminates – either normally or through an unhandled exception – or
until the optional timeout occurs.
When the timeout argument is present and not None, it should be a
floating point number specifying a timeout for the operation in
seconds (or fractions thereof). As join() always returns None, you
must call is_alive() after join() to decide whether a timeout happened
– if the thread is still alive, the join() call timed out.
When the timeout argument is not present or None, the operation will
block until the thread terminates.
A thread can be join()ed many times.
join() raises a RuntimeError if an attempt is made to join the current
thread as that would cause a deadlock. It is also an error to join() a
thread before it has been started and attempts to do so raise the same
exception.
(emphasis mine)
This method locks until the thread finishes executing preventing the GUI event loop from executing, causing it not to respond to events since it is frozen.
The solution is to remove this method:
def start_button_clicked(self):
self.hide()
thread = threading.Thread(target=self.check_ULink_status)
thread.setDaemon(True)
thread.start()
# thread.join() # remove this line
self.show()
It seems that the OP use join() so that when the task is finished the window is displayed again, if so, the correct solution is to use a signal:
class MainWindow(QMainWindow, Ui_MainWindow):
finished = QtCore.pyqtSignal()
def __init__(self):
super().__init__()
self.finished.connect(self.show)
# ...
# ...
def check_ULink_status(self):
# After finishing the task the signal must be emitted
self.finished.emit()
# ...
def start_button_clicked(self):
self.hide()
thread = threading.Thread(target=self.check_ULink_status)
thread.setDaemon(True)
thread.start()
# ...
My python application stops responding whenever I do anything "wx" a few times...whether that be clicking buttons, menus, inside the text area, etc.
This seems to be a problem with my use of windows hooks elsewhere in the program. When the two are used together, things go bad.
While the example below might not make much sense outside the context of the real application, it is what I came up with as a minimal example. I took everything out I could in order to try to diagnose the problem with minimal extra complexities.
It really seems like the second windows message pump called from the worker thread is the factor that makes it happen. If I comment that out, the app doesn't hang...or do anything useful...Hooks requires a pump, and I can't put it on my UI thread or it will the hooks will timeout, which led me to this POC in the first place...
If you run this and click the mouse a dozen times or so, you will notice the text blocks get garbled and then the app just hangs. Does anyone have any insight as to why?
I am using
python 2.7.16 32 bit
pyHook 1.5.1
wxPython 3.0.2.0 32bit for python 2.7
(update) I've also tried with the same results
python 2.7.16 32 bit
pyHook 1.5.1
wxPython 4.0.6 32 bit for python 2.7
We use these in the real application, because the my higher ups want to continue to support Windows XP (A whole different conversation)
main.py
import wx
from twisted.internet import wxreactor
from windows_hooks import WindowsHooksWrapper
from main_window import MainWindow
def main():
hook_wrapper = WindowsHooksWrapper()
hook_wrapper.start()
app = wx.App(False)
frame = MainWindow(None, 'Hooks Testing', hook_wrapper)
from twisted.internet import reactor
reactor.registerWxApp(app)
reactor.run()
hook_wrapper.stop()
if __name__ == "__main__":
wxreactor.install()
main()
windows_hooks.py
import pyHook
import threading
import pythoncom
class WindowsHooksWrapper(object):
def __init__(self):
self.hook_manager = None
self.started = False
self.thread = threading.Thread(target=self.thread_proc)
self.window_to_publish_to = None
print "HookWrapper created on Id {}".format(threading.current_thread().ident)
def __del__(self):
self.stop()
def start(self):
if self.started:
self.stop()
self.started = True
self.thread.start()
def stop(self):
if not self.started:
return
self.started = False
self.thread.join()
def on_mouse_event(self, event):
"""
Called back from pyHooks library on a mouse event
:param event: event passed from pyHooks
:return: True if we are to pass the event on to other hooks and the process it was intended
for. False to consume the event.
"""
if self.window_to_publish_to:
from twisted.internet import reactor
reactor.callFromThread(self.window_to_publish_to.print_to_text_box, event)
return True
def thread_proc(self):
print "Thread started with Id {}".format(threading.current_thread().ident)
# Evidently, the hook must be registered on the same thread with the windows msg pump or
# it will not work and no indication of error is seen
# Also note that for exception safety, when the hook manager goes out of scope, the
# documentation says that it unregisters all outstanding hooks
self.hook_manager = pyHook.HookManager()
self.hook_manager.MouseAll = self.on_mouse_event
self.hook_manager.HookMouse()
while self.started:
pythoncom.PumpMessages()
print "Thread exiting..."
self.hook_manager.UnhookMouse()
self.hook_manager = None
main_window.py
import threading
import wx
class MainWindow(wx.Frame):
def __init__(self, parent, title, hook_manager):
wx.Frame.__init__(self, parent, title=title, size=(800, 600))
self.hook_manager = hook_manager
self.CreateStatusBar()
menu_file = wx.Menu()
menu_item_exit = menu_file.Append(wx.ID_EXIT, "E&xit", " Terminate the program")
menu_help = wx.Menu()
menu_item_about = menu_help.Append(wx.ID_ABOUT, "&About", " Information about this program")
menu_bar = wx.MenuBar()
menu_bar.Append(menu_file, "&File")
menu_bar.Append(menu_help, "&Help")
self.SetMenuBar(menu_bar)
self.panel = MainPanel(self, hook_manager)
self.Bind(wx.EVT_MENU, self.on_about, menu_item_about)
self.Bind(wx.EVT_MENU, self.on_exit, menu_item_exit)
self.Show(True)
def on_about(self, e):
dlg = wx.MessageDialog(self, "A window to test Windows Hooks", "About Test Windows Hooks",
wx.OK)
dlg.ShowModal()
dlg.Destroy()
def on_exit(self, e):
self.Close(True)
class MainPanel(wx.Panel):
def __init__(self, parent, hook_manager):
self.hook_manager = hook_manager
hook_manager.window_to_publish_to = self
self.consuming = False
wx.Panel.__init__(self, parent)
self.textbox = wx.TextCtrl(self, style=wx.TE_MULTILINE | wx.TE_READONLY)
self.horizontal = wx.BoxSizer()
self.horizontal.Add(self.textbox, proportion=1, flag=wx.EXPAND)
self.sizer_vertical = wx.BoxSizer(wx.VERTICAL)
self.sizer_vertical.Add(self.horizontal, proportion=1, flag=wx.EXPAND)
self.SetSizerAndFit(self.sizer_vertical)
self.called_back_count = 0
def print_to_text_box(self, event):
self.called_back_count += 1
print "Printing message {} on Thread with Id {}".format(self.called_back_count,
threading.current_thread().ident)
self.textbox.AppendText('MessageName: {}\n'.format(event.MessageName))
self.textbox.AppendText('Message: {}\n'.format(event.Message))
self.textbox.AppendText('Time: {}\n'.format(event.Time))
self.textbox.AppendText('Window: {}\n'.format(event.Window))
self.textbox.AppendText('WindowName: {}\n'.format(event.WindowName))
self.textbox.AppendText('Position: {}\n'.format(event.Position))
self.textbox.AppendText('Wheel: {}\n'.format(event.Wheel))
self.textbox.AppendText('Injected: {}\n'.format(event.Injected))
self.textbox.AppendText('---\n')
I've also tried a version without Twisted and used wxPostEvent with a custom event instead, but we were suspecting that might be the problem, so I changed it to use twisted and it's still no good.
I'll post an edited listing with that in a bit.
I'm running a function in another thread that is supposed to fill out a dialog and then show it but it just seg faults as soon as I tried to alter the dialog in any way. I've read that this is a common issue with WxPython and that devs are not intended to directly alter dialogs in another thread.
How do I get around this? I can just call the function in my main thread but that will block my GUI and it is a lengthy operation to initialize the dialog - I would like to avoid this.
My code is similar to the below.
In the main thread
# Create the dialog and initialize it
thread.start_new_thread(self.init_dialog, (arg, arg, arg...))
The function I am calling
def init_dialog(self, arg, arg, arg....):
dialog = MyFrame(self, "Dialog")
# Setup the dialog
# ....
dialog.Show()
Even with a blank dialog and just a simple call to show inside the function I get a segmentation fault. Any help would be greatly appreciated, thanks.
I have made an applet to demonstrate keeping GUI responsive during calculations and calling the message box after the calculations.
import wx
import threading
import time
class TestFrame(wx.Frame):
def __init__(self):
wx.Frame.__init__(self, None, -1, "I am a test frame")
self.clickbtn = wx.Button(self, label="click me!")
self.Bind(wx.EVT_BUTTON, self.onClick)
def onClick(self, event):
self.clickbtn.Destroy()
self.status = wx.TextCtrl(self)
self.status.SetLabel("0")
print "GUI will be responsive during simulated calculations..."
thread = threading.Thread(target=self.runCalculation)
thread.start()
def runCalculation(self):
print "you can type in the GUI box during calculations"
for s in "1", "2", "3", "...":
time.sleep(1)
wx.CallAfter(self.status.AppendText, s)
wx.CallAfter(self.allDone)
def allDone(self):
self.status.SetLabel("all done")
dlg = wx.MessageDialog(self,
"This message shown only after calculation!",
"",
wx.OK)
result = dlg.ShowModal()
dlg.Destroy()
if result == wx.ID_OK:
self.Destroy()
mySandbox = wx.App()
myFrame = TestFrame()
myFrame.Show()
mySandbox.MainLoop()
GUI stuff is kept in the main thread, while calculations continue unhindered. The results of the calculation are available at time of dialog creation, as you required.