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)
Related
I was having trouble formatting the title of this question, because I wasn't sure I'm going about this the right way, so let me explain.
I want to try and add a right-click context menu to an existing program for which I don't have the source code. wxPython is generally my framework of choice. I figured there was a couple ways of doing this:
1) Create a transparent wx.Frame which is tied to and sits on top of the existing program, intercepting mouse events. If I did this, I wasn't sure if the mouse events could then be passed to the underlying window. I like this option, because it would allow adding more useful information in the overlay.
2) Create a headless program which globally intercepts right-click events, and spawns the context menu at the pointer location when certain conditions are met. Based on the research I've done so far, this didn't seem possible without continuously polling for mouse position.
What am I missing? Is there a more elegant solution for this? Is this even possible using Python?
edit: I have a partial proof-of-concept working which looks like this:
import wx
import win32gui
import win32api
import win32con
class POC_Frame(wx.Frame):
def __init__(self, parent):
wx.Frame.__init__(self, parent, id=wx.ID_ANY, title='POC', pos=(0,0), size=wx.Size(500, 500), style=wx.DEFAULT_FRAME_STYLE)
self.ToggleWindowStyle(wx.STAY_ON_TOP)
extendedStyleSettings = win32gui.GetWindowLong(self.GetHandle(), win32con.GWL_EXSTYLE)
win32gui.SetWindowLong(self.GetHandle(), win32con.GWL_EXSTYLE,
extendedStyleSettings | win32con.WS_EX_LAYERED | win32con.WS_EX_TRANSPARENT)
win32gui.SetLayeredWindowAttributes(self.GetHandle(), win32api.RGB(0,0,0), 100, win32con.LWA_ALPHA)
self.Bind(wx.EVT_RIGHT_DOWN, self.onRightDown)
self.Bind(wx.EVT_RIGHT_UP, self.onRightUp)
self.CaptureMouse()
def onRightDown(self, event):
print(event)
def onRightUp(self, event):
print(event)
app = wx.App(False)
MainFrame = POC_Frame(None)
MainFrame.Show()
app.MainLoop()
This seems to work OK, as it passes the right click events to the underlying window, while still recognizing them, but it only does it exactly once. As soon as it loses focus, it stops working and nothing I've tried to return focus to it seems to work.
I've always had better luck hooking global mouse and keyboard events with pyHook rather than wx. Here is a simple example:
import pyHook
import pyHook.cpyHook # ensure its included by cx-freeze
class ClickCatcher:
def __init__(self):
self.hm = None
self._is_running = True
self._is_cleaned_up = False
self._is_quitting = False
self.set_hooks()
# this is only necessary when not using wx
# def send_quit_message(self):
# import ctypes
# win32con_WM_QUIT = 18
# ctypes.windll.user32.PostThreadMessageW(self.pump_message_thread.ident, win32con_WM_QUIT, 0, 0)
def __del__(self):
self.quit()
def quit(self):
if not self._is_running:
return
self._is_quitting = True
self._is_running = False
if self.hm:
# self.hm.UnhookKeyboard()
self.hm.UnhookMouse()
# self.send_quit_message()
self._is_cleaned_up = True
def set_hooks(self):
self._is_running = True
self._is_cleaned_up = False
self.hm = pyHook.HookManager()
self.hm.MouseRightUp = self.on_right_click
# self.hm.HookKeyboard()
self.hm.HookMouse()
def on_right_click(self):
# create your menu here
pass
If you weren't using wx, you'd have to use pythoncom.PumpMessages to push mouse and keyboard events to you program, but App.Mainloop() accomplishes the same thing (if you use PumpMessages and Mainloop together about half of the events won't be push to your program).
Creating a wx.Menu is easy enough. You can find the mouse coordinates using wx.GetMousePosition()
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
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.
I have a messageDialog set up so that its default response is gtk.RESPONSE_OK so the okay button is clicked when the user hits enter even if the okay button does not have focus. I would like to also have the space bar trigget the default_response. What is the best way to do this?
This is with python 2.4 in a linux environment. Unfortunately I don't have permission to upgrade python.
Connect to the key-press-event signal on the message dialog:
def on_dialog_key_press(dialog, event):
if event.string == ' ':
dialog.response(gtk.RESPONSE_OK)
return True
return False
dialog = gtk.MessageDialog(message_format='Some message', buttons=gtk.BUTTONS_OK_CANCEL)
dialog.add_events(gtk.gdk.KEY_PRESS_MASK)
dialog.connect('key-press-event', on_dialog_key_press)
dialog.run()
Bear in mind, though, that changing users' expectations of the user interface is generally considered Not Cool.
I'm a total noob at pygtk, but I could not get #ptomato's example + "hello world" boilerplate to work unless I responded to space and return plus added a call to dialog.destroy(). Take it for what it is worth.
#!/usr/bin/env python
# example helloworld.py
import pygtk
pygtk.require('2.0')
import gtk
def md_event(dialog, event):
if event.keyval in (gtk.keysyms.Return, gtk.keysyms.space):
dialog.response(gtk.RESPONSE_OK)
dialog.destroy()
return True
elif event.keyval == gtk.keysyms.Escape:
dialog.response(gtk.RESPONSE_CANCEL)
dialog.destroy()
return True
return False
class HelloWorld:
# This is a callback function. The data arguments are ignored
# in this example. More on callbacks below.
def hello(self, widget, data=None):
print "Hello World"
# Another callback
def destroy(self, widget, data=None):
gtk.main_quit()
def create_message_dialog(self, x, y):
md = gtk.MessageDialog(buttons=gtk.BUTTONS_OK_CANCEL, message_format="wawawawaaaaa")
md.add_events(gtk.gdk.KEY_PRESS_MASK)
md.connect("key-press-event", md_event)
result = md.run()
print result
def __init__(self):
# create a new window
self.window = gtk.Window(gtk.WINDOW_TOPLEVEL)
# Here we connect the "destroy" event to a signal handler.
# This event occurs when we call gtk_widget_destroy() on the window,
# or if we return FALSE in the "delete_event" callback.
self.window.connect("destroy", self.destroy)
# Sets the border width of the window.
self.window.set_border_width(10)
self.button2 = gtk.Button("Message Dialog")
self.button2.connect("clicked", self.create_message_dialog, None)
self.window.add(self.button2)
self.button2.show()
# and the window
self.window.show()
def main(self):
# All PyGTK applications must have a gtk.main(). Control ends here
# and waits for an event to occur (like a key press or mouse event).
gtk.main()
def run_hello():
hello = HelloWorld()
hello.main()
# If the program is run directly or passed as an argument to the python
# interpreter then create a HelloWorld instance and show it
if __name__ == "__main__":
run_hello()
When I try to call self.Close(True) in the top level Frame's EVT_CLOSE event handler, it raises a RuntimeError: maximum recursion depth exceeded. Here's the code:
from PicEvolve import PicEvolve
import wx
class PicEvolveFrame(wx.Frame):
def __init__(self, parent, id=-1,title="",pos=wx.DefaultPosition,
size=wx.DefaultSize, style=wx.DEFAULT_FRAME_STYLE,
name="frame"):
wx.Frame.__init__(self,parent,id,title,pos,size,style,name)
self.panel = wx.ScrolledWindow(self)
self.panel.SetScrollbars(1,1,600,400)
statusBar = self.CreateStatusBar()
menuBar = wx.MenuBar()
menu1 = wx.Menu()
m = menu1.Append(wx.NewId(), "&Initialize", "Initialize population with random images")
menuBar.Append(menu1,"&Tools")
self.Bind(wx.EVT_MENU,self.OnInit,m)
self.Bind(wx.EVT_CLOSE,self.OnClose)
self.SetMenuBar(menuBar)
def OnInit(self, event):
dlg = wx.TextEntryDialog(None,"Enter Population Size:","Population Size")
popSize = 0
if dlg.ShowModal() == wx.ID_OK:
popSize = int(dlg.GetValue())
self.pEvolver = PicEvolve(popSize,(200,200),True)
box = wx.BoxSizer(wx.VERTICAL)
filenames = []
for i in range(popSize):
filenames.append("img"+str(i)+".png")
for fn in filenames:
img = wx.Image(fn,wx.BITMAP_TYPE_ANY)
box.Add(wx.StaticBitmap(self.panel,wx.ID_ANY,wx.BitmapFromImage(img)), 0,wx.BOTTOM)
self.panel.SetSizer(box)
def OnClose(self,event):
self.Close(True)
class PicEvolveApp(wx.App):
def OnInit(self):
self.frame = PicEvolveFrame(parent=None,title="PicEvolve")
self.frame.Show()
self.SetTopWindow(self.frame)
return True
if __name__ == "__main__":
app = PicEvolveApp()
app.MainLoop()
When you call window.Close it triggers EVT_CLOSE.
Quoted from http://www.wxpython.org/docs/api/wx.CloseEvent-class.html
The handler function for EVT_CLOSE is
called when the user has tried to
close a a frame or dialog box using
the window manager controls or the
system menu. It can also be invoked by
the application itself
programmatically, for example by
calling the wx.Window.Close function.
so obviously you will go into a infinite recursive loop. Instead in handler of EVT_CLOSE either destroy the window
def OnClose(self,event):
self.Destroy()
or Skip the event
def OnClose(self,event):
event.Skip(True)
or do not catch the EVT_CLOSE.
Edit:
Btw why you want to catch the event, in other question you have put some comment, you should update the question accordingly, so that people can give better answers.
e.g when your program is still waiting on command prompt after close, it may mean you have some top level window still not closed.
To debug which one is still open, try this
for w in wx.GetTopLevelWindows():
print w
You don't need to catch EVT_CLOSE unless you want to do something special, like prompt the user to save. If you do that sort of thing, then call self.Destroy() instead. Right now you call OnClose when you hit the upper right "x", which then calls "Close", which fires the OnClose event....that's why you get the recursion error.
If you don't catch EVT_CLOSE and use self.Close() it should work. When it doesn't, then that usually means you have a timer, thread or hidden top-level window somewhere that also needs to be stopped or closed. I hope that made sense.
def OnClose(self,event):
event.Skip()
see http://wiki.wxpython.org/EventPropagation