Related
I have a tkinter app running alongside two different threads that are logging into it using a queue.
One of the threads is in the same code file as the tkinter app. The other one is imported from another file, even thought their code is similar. What I verify is that only the thread defined in the same file manages to write into the UI. Do you know why this happens?
The code for the main file is:
import time
import queue
import threading
import tkinter as tk
from tkinter.scrolledtext import ScrolledText
from tkinter import ttk
import logging
from logging.handlers import QueueHandler
from foo import ImportedLoggerThread
logger = logging.getLogger(__name__)
class LoggerThread(threading.Thread):
def __init__(self):
super().__init__()
self._stop_event = threading.Event()
def run(self):
logger.debug('LoggerThread: running')
i = 0
while not self._stop_event.is_set():
logger.info("LoggerThread: iteration %d" % i)
i += 1
time.sleep(1)
def stop(self):
self._stop_event.set()
class LoggingWindow:
def __init__(self, frame):
self.frame = frame
self.scrolled_text = ScrolledText(frame, height=12)
self.scrolled_text.pack()
self.log_queue = queue.Queue()
self.queue_handler = QueueHandler(self.log_queue)
logger.addHandler(self.queue_handler)
# start polling
self.frame.after(100, self.poll_log_queue)
def write(self, record):
msg = self.queue_handler.format(record)
self.scrolled_text.insert(tk.END, msg + '\n')
# Scroll to the bottom
self.scrolled_text.yview(tk.END)
def poll_log_queue(self):
# Poll every 100ms
while True:
try:
record = self.log_queue.get(block=False)
except queue.Empty:
break
else:
self.write(record)
self.frame.after(100, self.poll_log_queue)
class App:
def __init__(self, root):
self.root = root
frame = ttk.Labelframe(text="Log")
frame.pack()
self.console = LoggingWindow(frame)
self.th = LoggerThread()
self.th.start()
self.imported = ImportedLoggerThread()
self.imported.start()
self.root.protocol('WM_DELETE_WINDOW', self.quit)
def quit(self):
self.th.stop()
self.imported.stop()
self.root.destroy()
def main():
logging.basicConfig(level=logging.DEBUG)
root = tk.Tk()
app = App(root)
app.root.mainloop()
if __name__ == '__main__':
main()
and for the second file foo.py:
import threading
import logging
import time
logger = logging.getLogger(__name__)
class ImportedLoggerThread(threading.Thread):
def __init__(self):
super().__init__()
self._stop_event = threading.Event()
def run(self):
logger.debug('Imported: running')
i = 0
while not self._stop_event.is_set():
logger.info("Imported: iteration %d" % i)
i += 1
time.sleep(2)
def stop(self):
self._stop_event.set()
Thanks in advance!
You define 2 logger instances in your files (logger = logging.getLogger(__name__)) and it causes your issue. If you use the same logger instance, it should work. It means in your case, you should pass the logger instance from your main file to the imported module (foo.py). Please see below the fixed foo.py and the fixed App class in the main file.
foo.py:
import threading
import time
class ImportedLoggerThread(threading.Thread):
def __init__(self, my_logger):
super().__init__()
self._stop_event = threading.Event()
self.my_logger = my_logger # Should be passed from caller side.
def run(self):
self.my_logger.debug('Imported: running')
i = 0
while not self._stop_event.is_set():
self.my_logger.info("Imported: iteration %d" % i)
i += 1
time.sleep(2)
def stop(self):
self._stop_event.set()
As you can see above the "imported" module uses a getting logger (It should comes from the "main" file)
App class:
class App:
def __init__(self, root):
self.root = root
frame = ttk.Labelframe(text="Log")
frame.pack()
self.console = LoggingWindow(frame)
self.th = LoggerThread()
self.th.start()
self.imported = ImportedLoggerThread(my_logger=logger) # Should be passed the defined logger instance.
self.imported.start()
self.root.protocol('WM_DELETE_WINDOW', self.quit)
def quit(self):
self.th.stop()
self.imported.stop()
self.root.destroy()
As you can see in the App class, the defined logger instance is passed to the imported ImportedLoggerThread class.
Output:
>>> python3 test.py
DEBUG:__main__:LoggerThread: running
DEBUG:__main__:Imported: running
INFO:__main__:LoggerThread: iteration 0
INFO:__main__:Imported: iteration 0
INFO:__main__:LoggerThread: iteration 1
INFO:__main__:Imported: iteration 1
INFO:__main__:LoggerThread: iteration 2
GUI:
I'm writing a PyQt programe where I'd like to allow the user to launch their preferred editor to fill in a TextEdit field.
So the goal is to launch an editor (say vim) externally on a tmp file, and upon editor closing, get its contexts into a python variable.
I've found a few similar questions like Opening vi from Python, call up an EDITOR (vim) from a python script, invoke an editor ( vim ) in python. But they are all in a "blocking" manner that works like the git commit command. What I am after is a "non-blocking" manner (because it is a GUI), something like the "Edit Source" function in zimwiki.
My current attempt:
import os
import tempfile
import threading
import subprocess
def popenAndCall(onExit, popenArgs):
def runInThread(onExit, popenArgs):
tmppath=popenArgs[-1]
proc = subprocess.Popen(popenArgs)
# this immediately finishes OPENING vim.
rec=proc.wait()
print('# <runInThread>: rec=', rec)
onExit(tmppath)
os.remove(tmppath)
return
thread = threading.Thread(target=runInThread, args=(onExit, popenArgs))
thread.start()
return thread
def openEditor():
fd, filepath=tempfile.mkstemp()
print('filepath=',filepath)
def cb(tmppath):
print('# <cb>: cb tmppath=',tmppath)
with open(tmppath, 'r') as tmp:
lines=tmp.readlines()
for ii in lines:
print('# <cb>: ii',ii)
return
with os.fdopen(fd, 'w') as tmp:
cmdflag='--'
editor_cmd='vim'
cmd=[os.environ['TERMCMD'], cmdflag, editor_cmd, filepath]
print('#cmd = ',cmd)
popenAndCall(cb, cmd)
print('done')
return
if __name__=='__main__':
openEditor()
I think it failed because the Popen.wait() only waits until the editor is opened, not until its closing. So it captures nothing from the editor.
Any idea how to solve this? Thanks!
EDIT:
I found this answer which I guess is related. I'm messing around trying to let os wait for the process group, but it's still not working. Code below:
def popenAndCall(onExit, popenArgs):
def runInThread(onExit, popenArgs):
tmppath=popenArgs[-1]
proc = subprocess.Popen(popenArgs, preexec_fn=os.setsid)
pid=proc.pid
gid=os.getpgid(pid)
#rec=proc.wait()
rec=os.waitid(os.P_PGID, gid, os.WEXITED | os.WSTOPPED)
print('# <runInThread>: rec=', rec, 'pid=',pid, 'gid=',gid)
onExit(tmppath)
os.remove(tmppath)
return
thread = threading.Thread(target=runInThread, args=(onExit, popenArgs))
thread.start()
return thread
I assume this gid=os.getpgid(pid) gives me the id of the group, and os.waitid() wait for the group. I also tried os.waitpid(gid, 0), didn't work either.
I'm on the right track?
UPDATE:
It seems that for some editors that works, like xed. vim and gvim both fails.
With QProcess you can launch a process without blocking the Qt event loop.
In this case I use xterm since I do not know which terminal is established in TERMCMD.
from PyQt5 import QtCore, QtGui, QtWidgets
class EditorWorker(QtCore.QObject):
finished = QtCore.pyqtSignal()
def __init__(self, command, parent=None):
super(EditorWorker, self).__init__(parent)
self._temp_file = QtCore.QTemporaryFile(self)
self._process = QtCore.QProcess(self)
self._process.finished.connect(self.on_finished)
self._text = ""
if self._temp_file.open():
program, *arguments = command
self._process.start(
program, arguments + [self._temp_file.fileName()]
)
#QtCore.pyqtSlot()
def on_finished(self):
if self._temp_file.isOpen():
self._text = self._temp_file.readAll().data().decode()
self.finished.emit()
#property
def text(self):
return self._text
def __del__(self):
self._process.kill()
class Widget(QtWidgets.QWidget):
def __init__(self, parent=None):
super(Widget, self).__init__(parent)
self._button = QtWidgets.QPushButton(
"Launch VIM", clicked=self.on_clicked
)
self._text_edit = QtWidgets.QTextEdit(readOnly=True)
lay = QtWidgets.QVBoxLayout(self)
lay.addWidget(self._button)
lay.addWidget(self._text_edit)
#QtCore.pyqtSlot()
def on_clicked(self):
worker = EditorWorker("xterm -e vim".split(), self)
worker.finished.connect(self.on_finished)
#QtCore.pyqtSlot()
def on_finished(self):
worker = self.sender()
prev_cursor = self._text_edit.textCursor()
self._text_edit.moveCursor(QtGui.QTextCursor.End)
self._text_edit.insertPlainText(worker.text)
self._text_edit.setTextCursor(prev_cursor)
worker.deleteLater()
if __name__ == "__main__":
import sys
app = QtWidgets.QApplication(sys.argv)
w = Widget()
w.resize(640, 480)
w.show()
sys.exit(app.exec_())
I guess in your case you should change
"xterm -e vim".split()
to
[os.environ['TERMCMD'], "--", "vim"]
Possible commands:
- xterm -e vim
- xfce4-terminal --disable-server -x vim
Update:
Implementing the same logic that you use with pyinotify that is to monitor the file, but in this case using QFileSystemWatcher which is a multiplatform solution:
from PyQt5 import QtCore, QtGui, QtWidgets
class EditorWorker(QtCore.QObject):
finished = QtCore.pyqtSignal()
def __init__(self, command, parent=None):
super(EditorWorker, self).__init__(parent)
self._temp_file = QtCore.QTemporaryFile(self)
self._process = QtCore.QProcess(self)
self._text = ""
self._watcher = QtCore.QFileSystemWatcher(self)
self._watcher.fileChanged.connect(self.on_fileChanged)
if self._temp_file.open():
self._watcher.addPath(self._temp_file.fileName())
program, *arguments = command
self._process.start(
program, arguments + [self._temp_file.fileName()]
)
#QtCore.pyqtSlot()
def on_fileChanged(self):
if self._temp_file.isOpen():
self._text = self._temp_file.readAll().data().decode()
self.finished.emit()
#property
def text(self):
return self._text
def __del__(self):
self._process.kill()
class Widget(QtWidgets.QWidget):
def __init__(self, parent=None):
super(Widget, self).__init__(parent)
self._button = QtWidgets.QPushButton(
"Launch VIM", clicked=self.on_clicked
)
self._text_edit = QtWidgets.QTextEdit(readOnly=True)
lay = QtWidgets.QVBoxLayout(self)
lay.addWidget(self._button)
lay.addWidget(self._text_edit)
#QtCore.pyqtSlot()
def on_clicked(self):
worker = EditorWorker("gnome-terminal -- vim".split(), self)
worker.finished.connect(self.on_finished)
#QtCore.pyqtSlot()
def on_finished(self):
worker = self.sender()
prev_cursor = self._text_edit.textCursor()
self._text_edit.moveCursor(QtGui.QTextCursor.End)
self._text_edit.insertPlainText(worker.text)
self._text_edit.setTextCursor(prev_cursor)
worker.deleteLater()
if __name__ == "__main__":
import sys
app = QtWidgets.QApplication(sys.argv)
w = Widget()
w.resize(640, 480)
w.show()
sys.exit(app.exec_())
The issue I reproduced is that proc is the gnome-terminal process and not the vim process.
Here are the two options that work for me.
1) Find the process of your text editor and not that of your terminal. With the right process ID, the code can wait for the process of your text editor to finish.
With psutil (portable)
Finds the latest editor process in the list of all running processes.
import psutil
def popenAndCall(onExit, popenArgs):
def runInThread(onExit, popenArgs):
tmppath=popenArgs[-1]
editor_cmd=popenArgs[-2] # vim
proc = subprocess.Popen(popenArgs)
proc.wait()
# Find the latest editor process in the list of all running processes
editor_processes = []
for p in psutil.process_iter():
try:
process_name = p.name()
if editor_cmd in process_name:
editor_processes.append((process_name, p.pid))
except:
pass
editor_proc = psutil.Process(editor_processes[-1][1])
rec=editor_proc.wait()
print('# <runInThread>: rec=', rec)
onExit(tmppath)
os.remove(tmppath)
return
thread = threading.Thread(target=runInThread, args=(onExit, popenArgs))
thread.start()
return thread
Without psutil (works on Linux, but not portable to Mac OS or Windows)
Draws from https://stackoverflow.com/a/2704947/241866 and the source code of psutil.
def popenAndCall(onExit, popenArgs):
def runInThread(onExit, popenArgs):
tmppath=popenArgs[-1]
editor_cmd=popenArgs[-2] # vim
proc = subprocess.Popen(popenArgs)
proc.wait()
# Find the latest editor process in the list of all running processes
pids = [pid for pid in os.listdir('/proc') if pid.isdigit()]
editor_processes = []
for pid in pids:
try:
process_name = open(os.path.join('/proc', pid, 'cmdline'), 'rb').read().split('\0')[0]
if editor_cmd in process_name:
editor_processes.append((process_name, int(pid)))
except IOError:
continue
editor_proc_pid = editor_processes[-1][1]
def pid_exists(pid):
try:
os.kill(pid, 0)
return True
except:
return
while True:
if pid_exists(editor_proc_pid):
import time
time.sleep(1)
else:
break
onExit(tmppath)
os.remove(tmppath)
return
thread = threading.Thread(target=runInThread, args=(onExit, popenArgs))
thread.start()
return thread
2) As a last resort, you can catch a UI event before updating the text:
def popenAndCall(onExit, popenArgs):
def runInThread(onExit, popenArgs):
tmppath=popenArgs[-1]
proc = subprocess.Popen(popenArgs)
# this immediately finishes OPENING vim.
rec=proc.wait()
raw_input("Press Enter") # replace this with UI event
print('# <runInThread>: rec=', rec)
onExit(tmppath)
os.remove(tmppath)
return
thread = threading.Thread(target=runInThread, args=(onExit, popenArgs))
thread.start()
return thread
I think #eyllanesc's solution is very close to what zim is doing (zim is using GObject.spawn_async() and GObject.child_watch_add(), I've no experiences with GObject, I guess that's the equivalent to QProcess.start()). But we run into some problems regarding how some terminals (like gnome-terminal) handles new terminal session launching.
I tried to monitor the temporary file opened by the editor, and on writing/saving the temp file I could call my callback. The monitoring is done using pyinotify. I've tried gnome-terminal, xterm, urxvt and plain gvim, all seem to work.
Code below:
import threading
from PyQt5 import QtCore, QtGui, QtWidgets
import pyinotify
class EditorWorker(QtCore.QObject):
file_close_sig = QtCore.pyqtSignal()
edit_done_sig = QtCore.pyqtSignal()
def __init__(self, command, parent=None):
super(EditorWorker, self).__init__(parent)
self._temp_file = QtCore.QTemporaryFile(self)
self._process = QtCore.QProcess(self)
#self._process.finished.connect(self.on_file_close)
self.file_close_sig.connect(self.on_file_close)
self._text = ""
if self._temp_file.open():
program, *arguments = command
self._process.start(
program, arguments + [self._temp_file.fileName()]
)
tmpfile=self._temp_file.fileName()
# start a thread to monitor file saving/closing
self.monitor_thread = threading.Thread(target=self.monitorFile,
args=(tmpfile, self.file_close_sig))
self.monitor_thread.start()
#QtCore.pyqtSlot()
def on_file_close(self):
if self._temp_file.isOpen():
print('open')
self._text = self._temp_file.readAll().data().decode()
self.edit_done_sig.emit()
else:
print('not open')
#property
def text(self):
return self._text
def __del__(self):
try:
self._process.kill()
except:
pass
def monitorFile(self, path, sig):
class PClose(pyinotify.ProcessEvent):
def my_init(self):
self.sig=sig
self.done=False
def process_IN_CLOSE(self, event):
f = event.name and os.path.join(event.path, event.name) or event.path
self.sig.emit()
self.done=True
wm = pyinotify.WatchManager()
eventHandler=PClose()
notifier = pyinotify.Notifier(wm, eventHandler)
wm.add_watch(path, pyinotify.IN_CLOSE_WRITE)
try:
while not eventHandler.done:
notifier.process_events()
if notifier.check_events():
notifier.read_events()
except KeyboardInterrupt:
notifier.stop()
return
class Widget(QtWidgets.QWidget):
def __init__(self, parent=None):
super(Widget, self).__init__(parent)
self._button = QtWidgets.QPushButton(
"Launch VIM", clicked=self.on_clicked
)
self._text_edit = QtWidgets.QTextEdit(readOnly=True)
lay = QtWidgets.QVBoxLayout(self)
lay.addWidget(self._button)
lay.addWidget(self._text_edit)
#QtCore.pyqtSlot()
def on_clicked(self):
worker = EditorWorker(["gnome-terminal", '--', "vim"], self)
worker.edit_done_sig.connect(self.on_edit_done)
#QtCore.pyqtSlot()
def on_edit_done(self):
worker = self.sender()
prev_cursor = self._text_edit.textCursor()
self._text_edit.moveCursor(QtGui.QTextCursor.End)
self._text_edit.insertPlainText(worker.text)
self._text_edit.setTextCursor(prev_cursor)
worker.deleteLater()
if __name__ == "__main__":
import sys
app = QtWidgets.QApplication(sys.argv)
w = Widget()
w.resize(640, 480)
w.show()
sys.exit(app.exec_())
BUT pyinotify only works in Linux. If you could find a cross-platform solution (at least on Mac) please let me know.
UPDATE: this doesn't seem to be robust. pyinotify reports file writing instead of just file closing. I'm depressed.
I have an installer I am creating for a game and as of now there are two buttons. One downloads the game, and one starts the game if it detects the executable. I multi-threaded both buttons so that my GUI will not freeze when I click either button. The problem is, if I click one of the buttons, the other will not work until restarting the application. I need some way for the thread to close after its process is completed so that the thread is open for the other button to work.
Here is what I have so far:
# Import Libraries
import requests, os, sys, zipfile, shutil, subprocess, wx, urllib, time
from threading import *
# Define global variables
url = "{ENTER DROPBOX URL HERE}" # The url to the file we are downloading
myEVT_PROGRESS = wx.NewEventType() # Custom Event Type
EVT_PROGRESS = wx.PyEventBinder(myEVT_PROGRESS, 1) # Bind specific events to event handlers
ID_START = wx.NewId()# Button definitions
EVT_RESULT_ID = wx.NewId()# Define notification event for thread completion
# Version Check
def VersionCheck():
try:
CurrentVersion = os.listdir("./RFMB6_WINDOWS/")[0] # Checks the version currently downloaded
VersionCheck = requests.get('https://pastebin.com/raw/yc30uwAh') # Checks the newest version
NewestVersion = VersionCheck.text # Converts VersionCheck to a string
if CurrentVersion == NewestVersion:
message = 'It looks like you have the newest version already.\n Are you sure you want to download?'
wx.MessageBox(message=message, caption='RFMP GUIntaller | Complete!', style=wx.OK | wx.ICON_INFORMATION)
else:
print('\n\nThere is an update available, would you like to install it?')
pass
except:
print("It looks like you don't have RFMP installed yet. Let me fix that for you.")
# Downloads new file
def Download():
urllib.request.urlretrieve(url, 'RFMP.zip')
# Extracts new file
def Extract():
zip_ref = zipfile.ZipFile("RFMP.zip", 'r')
zip_ref.extractall("RFMB6_WINDOWS")
zip_ref.close()
# Deletes the .zip file but leave the folder
def Clean():
os.remove("RFMP.zip")
class ProgressEvent(wx.PyCommandEvent):
"""Event to signal that a status or progress changed"""
def __init__(self, etype, eid, status=None, progress=None):
"""Creates the event object"""
wx.PyCommandEvent.__init__(self, etype, eid)
self._status = status # field to update label
self._progress = progress # field to update progress bar
def GetValue(self):
"""Returns the value from the event.
#return: the tuple of status and progress
"""
return (self._status, self._progress)
# Thread class that executes processing
class DLThread(Thread):
"""Worker Thread Class."""
def __init__(self, notify_window):
"""Init Worker Thread Class."""
Thread.__init__(self)
self._notify_window = notify_window
self.start()
# This is what runs on a separate thread when you click the download button
def run(self):
# This is the code executing in the new thread.
self.sendEvent('Checking for old files...', 00)
self.sendEvent('Checking for old files...', 100)
time.sleep(.5)
if os.path.exists("RFMB6_WINDOWS"):
self.sendEvent('Removing old files...', 200)
subprocess.check_call(('attrib -R ' + 'RFMB6_WINDOWS' + '\\* /S').split())
shutil.rmtree('RFMB6_WINDOWS')
time.sleep(.3)
self.sendEvent('Removed old files.', 300)
else:
time.sleep(.3)
self.sendEvent('No old files found.', 300)
time.sleep(.3)
pass
self.sendEvent('Downloading Package...', 400)
Download()
self.sendEvent('Downloading complete.', 600)
time.sleep(.3)
self.sendEvent('Extracting...', 650)
Extract()
self.sendEvent('Extraction complete.', 900)
time.sleep(.3)
self.sendEvent('Cleaning up...', 950)
Clean()
time.sleep(.3)
self.sendEvent('Cleaning complete.', 1000)
time.sleep(.5)
done = ("Installation the RFMP Private Alpha has been completed!")
wx.MessageBox(message=done, caption='RFMP GUIntaller | Complete!', style=wx.OK | wx.ICON_INFORMATION)
self._notify_window.worker = None
def sendEvent(self, status=None, progress=None):
# Send event to main frame, first param (str) is for label, second (int) for the progress bar
evt = ProgressEvent(myEVT_PROGRESS, -1, status, progress)
wx.PostEvent(self._notify_window, evt)
class StartAppThread(Thread):
"""Worker Thread Class."""
def __init__(self, notify_window):
"""Init Worker Thread Class."""
Thread.__init__(self)
self._notify_window = notify_window
# This starts the thread running on creation.
self.start()
# This is what runs on a separate thread when you click the download button
def run(self):
try:
subprocess.run('RFMB6_WINDOWS/RFMB6_WINDOWS/RFMB6.exe')
except:
error = ("Failed to locate RFMB6.exe. Please don't move any game files after downloading.")
wx.MessageBox(message=error, caption='RFMP GUIntaller | Error!',
style=wx.OK | wx.ICON_ERROR)
self._notify_window.worker = None
# GUI Frame class that spins off the worker thread
class MainFrame(wx.Frame):
"""Class MainFrame."""
def __init__(self, parent, id):
"""Create the MainFrame."""
wx.Frame.__init__(self, parent, id, 'RFMP GUInstaller',
style=wx.DEFAULT_FRAME_STYLE ^ wx.RESIZE_BORDER
^ wx.MAXIMIZE_BOX)
self.SetSize(400, 350)
self.Centre()
DLStart = wx.Button(self.bitmap1, ID_START, 'Download RFMP', size=(175,50), pos=(50,260))
DLStart.Bind(wx.EVT_BUTTON, self.OnButton_DLStart)
AppStart = wx.Button(self.bitmap1, ID_START, 'Start RFMP', size=(175,50), pos=(50,160))
AppStart.Bind(wx.EVT_BUTTON, self.OnButton_AppStart)
self.status = wx.StaticText(self.bitmap1, -1, '', pos=(10,215), style=wx.NO_BORDER)
self.status.SetBackgroundColour((255,255,0)) # set text back color
self.gauge = wx.Gauge(self.bitmap1, range = 1000, size = (375, 30), pos=(10,230),
style = wx.GA_HORIZONTAL)
# And indicate we don't have a worker thread yet
self.worker = None
self.Bind(EVT_PROGRESS, self.OnResult) # Bind the custom event to a function
def OnButton_DLStart(self, event):
# Trigger the worker thread unless it's already busy
VersionCheck()
if not self.worker:
self.worker = DLThread(self)
def OnButton_AppStart(self, event):
if not self.worker:
self.worker = StartAppThread(self)
def OnResult(self, event):
"""Our handler for our custom progress event."""
status, progress = event.GetValue()
self.status.SetLabel(status)
if progress:
self.gauge.SetValue(progress)
class MainApp(wx.App):
"""Class Main App."""
def OnInit(self):
"""Init Main App."""
self.frame = MainFrame(None, -1)
self.frame.Show(True)
self.SetTopWindow(self.frame)
return True
# Main Loop
if __name__ == '__main__':
app = MainApp(0)
app.MainLoop()
Your issue is caused by the fact that self.worker has a value.
You need to reset self.worker.
Below I have adjusted your code to do that and in doing so I have renamed notify_window to parent, simply because it makes what is going on more obvious and fits with python standards. I'm sure that there are many others ways of achieving this, this is just a simplistic way of achieving it, in this case.
import requests, os, sys, zipfile, shutil, subprocess, wx, urllib, time
from threading import *
class DLThread(Thread):
"""Worker Thread Class."""
def __init__(self, parent):
"""Init Worker Thread Class."""
Thread.__init__(self)
self.parent = parent
self.stop_download = 0
self.setDaemon(1)
self.start()
def run(self):
# This is the code executing in the new thread.
'''
This is what runs on a separate thread when you click the download button
'''
x = 0
while self.stop_download == 0:
time.sleep(0.5)
x +=1
if x > 20:
self.stop_download = 1
print ("Downloading App", x)
print("Download finished")
self.parent.worker = None
def stop(self):
self.stop_download = 1
print ("Download Cancelled")
class StartAppThread(Thread):
"""Worker Thread Class."""
def __init__(self, parent):
"""Init Worker Thread Class."""
Thread.__init__(self)
self.parent = parent
self.stop_app_thread = 0
self.setDaemon(1)
self.start()
def run(self):
# This is the code executing in the new thread.
'''
This is what runs on a separate thread when you click the Start App button.
'''
x= 0
while self.stop_app_thread == 0:
print ("Game in progress",str(x))
time.sleep(0.5)
x +=1
print ("Game finished")
self.parent.worker = None
def stop(self):
self.stop_app_thread = 1
# GUI Frame class that spins off the worker thread
class MainFrame(wx.Frame):
"""Class MainFrame."""
#Main Window
def __init__(self, parent, id):
"""Create the MainFrame."""
wx.Frame.__init__(self, parent, id, 'RFMP GUInstaller',
style=wx.DEFAULT_FRAME_STYLE ^ wx.RESIZE_BORDER
^ wx.MAXIMIZE_BOX)
self.SetSize(400, 350)
#self.bitmap1 = wx.StaticBitmap(self)
self.bitmap1 = wx.Panel(self)
self.Centre()
# Variables
myEVT_PROGRESS = wx.NewEventType() # Custom Event Type
EVT_PROGRESS = wx.PyEventBinder(myEVT_PROGRESS, 1) # Bind specific events to event handlers
ID_START = wx.NewId()# Button definitions
EVT_RESULT_ID = wx.NewId()# Define notification event for thread completion
# Download button
DLStart = wx.Button(self.bitmap1, ID_START, 'Download', size=(175,50), pos=(50,260))
DLStart.Bind(wx.EVT_BUTTON, self.OnButton_DLStart)
# App Start button
AppStart = wx.Button(self.bitmap1, ID_START, 'Start App', size=(75,50), pos=(50,160))
AppStart.Bind(wx.EVT_BUTTON, self.OnButton_AppStart)
# App Stop button
AppStop = wx.Button(self.bitmap1, ID_START, 'Stop', size=(75,50), pos=(150,160))
AppStop.Bind(wx.EVT_BUTTON, self.OnButton_AppStop)
# Progress bar
self.gauge = wx.Gauge(self.bitmap1, range = 1000, size = (375, 30), pos=(10,230), style = wx.GA_HORIZONTAL)
# And indicate we don't have a worker thread yet
self.worker = None
self.Bind(EVT_PROGRESS, self.OnResult) # Bind the custom event to a function
def OnButton_DLStart(self, event):
# Trigger the worker thread unless it's already busy
if not self.worker:
self.worker = DLThread(self)
def OnButton_AppStart(self, event):
if not self.worker:
self.worker = StartAppThread(self)
def OnButton_AppStop(self, event):
if self.worker:
self.worker.stop()
print ("App Stop command")
def OnResult(self, event):
"""Our handler for our custom progress event."""
status, progress = event.GetValue()
self.status.SetLabel(status)
if progress:
self.gauge.SetValue(progress)
class MainApp(wx.App):
"""Class Main App."""
def OnInit(self):
"""Init Main App."""
self.frame = MainFrame(None, -1)
self.frame.Show(True)
self.SetTopWindow(self.frame)
return True
# Main Loop
if __name__ == '__main__':
app = MainApp(0)
app.MainLoop()
I'm trying to create an application in wxpython that only uses a TaskBarIcon and no frames.
There is a question on this here, but that example doesn't work for me; it just exits with no error.
The code I've written below is a much simplified version of the code I'm working with:
import wx
class Systray_Icon(wx.TaskBarIcon):
def __init__(self):
icon = wx.Icon('yellow.ico', wx.BITMAP_TYPE_ICO)
self.SetIcon(icon, "Test")
self.Bind(wx.EVT_MENU, self.Destroy(), id=wx.ID_EXIT)
def CreatePopupMenu(self):
menu = wx.Menu()
menu.Append(wx.ID_EXIT, "Quit")
return menu
app = wx.App()
sysicon = Systray_Icon()
app.MainLoop()
I'm getting the following error:
$ python2 systray.py
Traceback (most recent call last):
File "systray.py", line 15, in <module>
sysicon = TaskBarIcon()
File "systray.py", line 6, in __init__
self.SetIcon(icon, "Test")
File "/usr/lib/python2.7/dist-packages/wx-3.0-gtk2/wx/_windows.py", line 2841, in SetIcon
return _windows_.TaskBarIcon_SetIcon(*args, **kwargs)
TypeError: in method 'TaskBarIcon_SetIcon', expected argument 1 of type 'wxPyTaskBarIcon *'
So, my questions:
1: Why won't SetIcon accept my class? I've tried moving the SetIcon call to a function like in the question I linked, but it still doesn't work. I can fiddle around with it and probably get something to work, but I'd like to know the reason it won't work.
2: The question I linked to runs, but exits immediately. Is that because a TaskBarIcon won't hold MainLoop() open? What can I do about this?
Here is a working sample on Linux with python 2.7 wxpython 2.8:
import wx
TRAY_TOOLTIP = 'System Tray Demo'
TRAY_ICON = '/usr/share/pixmaps/thunderbird.xpm'
def create_menu_item(menu, label, func):
item = wx.MenuItem(menu, -1, label)
menu.Bind(wx.EVT_MENU, func, id=item.GetId())
menu.AppendItem(item)
return item
class TaskBarIcon(wx.TaskBarIcon):
def __init__(self):
wx.TaskBarIcon.__init__(self)
self.set_icon(TRAY_ICON)
self.Bind(wx.EVT_TASKBAR_LEFT_DOWN, self.on_left_down)
def CreatePopupMenu(self):
menu = wx.Menu()
create_menu_item(menu, 'Say Hello', self.on_hello)
menu.AppendSeparator()
create_menu_item(menu, 'Exit', self.on_exit)
return menu
def set_icon(self, path):
icon = wx.IconFromBitmap(wx.Bitmap(path))
self.SetIcon(icon, TRAY_TOOLTIP)
def on_left_down(self, event):
print 'Tray icon was left-clicked.'
def on_hello(self, event):
print 'Hello, world!'
def on_exit(self, event):
wx.CallAfter(self.Destroy)
def main():
app = wx.App()
TaskBarIcon()
app.MainLoop()
if __name__ == '__main__':
main()
EDIT:
For anyone still getting grief this is a version that essentially uses a dummy frame.
Edit 2019: Updated for python3/wxpython 4+
import wx
import wx.adv
TRAY_TOOLTIP = 'System Tray Demo'
TRAY_ICON = '/usr/share/pixmaps/python.xpm'
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):
wx.adv.TaskBarIcon.__init__(self)
self.myapp_frame = frame
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, 'Say Hello', self.on_hello)
menu.AppendSeparator()
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_hello(self, event):
print ('Hello, world!')
def on_exit(self, event):
self.myapp_frame.Close()
class My_Application(wx.Frame):
#----------------------------------------------------------------------
def __init__(self):
wx.Frame.__init__(self, None, wx.ID_ANY, "", size=(1,1))
panel = wx.Panel(self)
self.myapp = TaskBarIcon(self)
self.Bind(wx.EVT_CLOSE, self.onClose)
#----------------------------------------------------------------------
def onClose(self, evt):
"""
Destroy the taskbar icon and the frame
"""
self.myapp.RemoveIcon()
self.myapp.Destroy()
self.Destroy()
if __name__ == "__main__":
MyApp = wx.App()
My_Application()
MyApp.MainLoop()
OK, figured it out the next day, after 30 minutes of messing about.
SetIcon wasn't accepting my class because I needed to add the following:
super(TaskBarIcon, self).__init__()
to __init__.
I suspect it's duck typing biting me here; without the wx.TaskBarIcon constructor running, python doesn't see it as a wx.TaskBarIcon object.
To keep the application open with only a TaskBarIcon, create a derived class from wx.App and override the OnInit function, like so:
class App(wx.App):
def OnInit(self):
self.SetTopWindow(wx.Frame(None, -1))
TaskBarIcon()
return True
I'd just need a quick example on how to easily put an icon with python on my systray. This means: I run the program, no window shows up, just a tray icon (I've got a png file) shows up in the systray and when I right-click on it a menu appears with some options (and when I click on an option, a function is run).
Is that possible? I don't need any window at all...
Examples / code snippets are REALLY appreciated! :D
For Windows & Gnome
Here ya go! wxPython is the bomb. Adapted from the source of my Feed Notifier application.
import wx
TRAY_TOOLTIP = 'System Tray Demo'
TRAY_ICON = 'icon.png'
def create_menu_item(menu, label, func):
item = wx.MenuItem(menu, -1, label)
menu.Bind(wx.EVT_MENU, func, id=item.GetId())
menu.AppendItem(item)
return item
class TaskBarIcon(wx.TaskBarIcon):
def __init__(self):
super(TaskBarIcon, self).__init__()
self.set_icon(TRAY_ICON)
self.Bind(wx.EVT_TASKBAR_LEFT_DOWN, self.on_left_down)
def CreatePopupMenu(self):
menu = wx.Menu()
create_menu_item(menu, 'Say Hello', self.on_hello)
menu.AppendSeparator()
create_menu_item(menu, 'Exit', self.on_exit)
return menu
def set_icon(self, path):
icon = wx.IconFromBitmap(wx.Bitmap(path))
self.SetIcon(icon, TRAY_TOOLTIP)
def on_left_down(self, event):
print 'Tray icon was left-clicked.'
def on_hello(self, event):
print 'Hello, world!'
def on_exit(self, event):
wx.CallAfter(self.Destroy)
def main():
app = wx.PySimpleApp()
TaskBarIcon()
app.MainLoop()
if __name__ == '__main__':
main()
2018 version
import wx.adv
import wx
TRAY_TOOLTIP = 'Name'
TRAY_ICON = 'icon.png'
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, 'Site', self.on_hello)
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 on_left_down(self, event):
print ('Tray icon was left-clicked.')
def on_hello(self, event):
print ('Hello, world!')
def on_exit(self, event):
wx.CallAfter(self.Destroy)
self.frame.Close()
class App(wx.App):
def OnInit(self):
frame=wx.Frame(None)
self.SetTopWindow(frame)
TaskBarIcon(frame)
return True
def main():
app = App(False)
app.MainLoop()
if __name__ == '__main__':
main()
wx.PySimpleApp deprecated, here's how to use wx.App instead
Took me while to figure this out so I thought I'd share. wx.PySimpleApp is deprecated in wxPython 2.9 and beyond. Here's FogleBird's original script using wx.App instead.
import wx
TRAY_TOOLTIP = 'System Tray Demo'
TRAY_ICON = 'icon.png'
def create_menu_item(menu, label, func):
item = wx.MenuItem(menu, -1, label)
menu.Bind(wx.EVT_MENU, func, id=item.GetId())
menu.AppendItem(item)
return item
class TaskBarIcon(wx.TaskBarIcon):
def __init__(self, frame):
self.frame = frame
super(TaskBarIcon, self).__init__()
self.set_icon(TRAY_ICON)
self.Bind(wx.EVT_TASKBAR_LEFT_DOWN, self.on_left_down)
def CreatePopupMenu(self):
menu = wx.Menu()
create_menu_item(menu, 'Say Hello', self.on_hello)
menu.AppendSeparator()
create_menu_item(menu, 'Exit', self.on_exit)
return menu
def set_icon(self, path):
icon = wx.IconFromBitmap(wx.Bitmap(path))
self.SetIcon(icon, TRAY_TOOLTIP)
def on_left_down(self, event):
print 'Tray icon was left-clicked.'
def on_hello(self, event):
print 'Hello, world!'
def on_exit(self, event):
wx.CallAfter(self.Destroy)
self.frame.Close()
class App(wx.App):
def OnInit(self):
frame=wx.Frame(None)
self.SetTopWindow(frame)
TaskBarIcon(frame)
return True
def main():
app = App(False)
app.MainLoop()
if __name__ == '__main__':
main()
If you can guarantee windows and you do not want to introduce the heavy dependencies of wx, you can do this with the pywin32 extensions.
Also see this question.
For Ubuntu
class TrayIcon:
def init():
iconPath = {"Windows":os.path.expandvars("%PROGRAMFILES%/MyProgram/icon.png"),
"Linux":"/usr/share/icons/myprogramicon.png"}
if platform.system()=="Linux":
import gtk
import appindicator # Ubuntu apt-get install python-appindicator
# Create an application indicator
try:
gtk.gdk.threads_init()
gtk.threads_enter()
icon = iconPath[platform.system()]
indicator = appindicator.Indicator("example-simple-client", "indicator-messages", appindicator.CATEGORY_APPLICATION_STATUS)
indicator.set_icon(icon)
indicator.set_status (appindicator.STATUS_ACTIVE)
indicator.set_attention_icon ("indicator-messages-new")
menu = gtk.Menu()
menuTitle = "Quit"
menu_items = gtk.MenuItem(menuTitle)
menu.append(menu_items)
menu_items.connect("activate", TrayIcon.QuitApp, menuTitle)
menu_items.show()
menuTitle = "About My Program"
menu_items = gtk.MenuItem(menuTitle)
menu.append(menu_items)
menu_items.connect("activate", TrayIcon.AboutApp, menuTitle)
menu_items.show()
indicator.set_menu(menu)
except:
pass
# Run the app indicator on the main thread.
try:
t = threading.Thread(target=gtk.main)
t.daemon = True # this means it'll die when the program dies.
t.start()
#gtk.main()
except:
pass
finally:
gtk.threads_leave()
#staticmethod
def AboutApp(a1,a2):
gtk.threads_enter()
dialog = gtk.Dialog("About",
None,
gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT,
(gtk.STOCK_OK, gtk.RESPONSE_ACCEPT))
label = gtk.Label("My Program v0.0.1, (C)opyright ME 2015. All rights reserved.")
dialog.vbox.pack_start(label)
label.show()
label2 = gtk.Label("example.com\n\nFor more support contact me#gmail.com")
label2.show()
dialog.action_area.pack_end(label2)
response = dialog.run()
dialog.destroy()
gtk.threads_leave()
#staticmethod
def QuitApp(a1, a2):
sys.exit(0)
Cross-Platform
See PyQt: Show menu in a system tray application
There is a package called pystray (bad name, just say it out loud) but works like a charm and is more lightweight than wx or Qt. These are the links:
https://pystray.readthedocs.io/en/latest/index.html
https://pypi.org/project/pystray/
Yes. There is a cross-platform example on wiki.wxpython.org that I've tested with python 2.7 (minconda install) on macOS High Sierra (10.13.3), Windows 7, and gnome 3/centos7. It is here (ignore the page title):
https://wiki.wxpython.org/Custom%20Mac%20OsX%20Dock%20Bar%20Icon
Small mods are needed for python 3.6:
you must import wx.adv
wx.TaskBarIcon becomes wx.adv.TaskBarIcon
wx.IconFromBitmap becomes wx.Icon
Gnome 3 required installation of TopIcons Plus.
Since you don't want to have the window display (" no window shows up, just a tray icon"), simply comment out the following line (though you still want to keep the wx.Frame parent):
frame.Show(True)
And since you want to use your own .png icon, remove the WXPdemo image and embeddedimage stuff and replace
icon = self.MakeIcon(WXPdemo.GetImage())
with, for example
icon = wx.Icon('icon.png')
In my experience, this will provide a good start for adapting or extending further.
An alternative if you are trying to run a python based program in the background you can run it as a service. Check out this active state recipe its pretty useful. I believe one of the options is to convert your application to exe with py2exe or pyinstall.
http://code.activestate.com/recipes/551780/
For an example, refer to this thread -> wx question.
wxPython "classic" -> [new API] wxPython 'Phoenix' (Py3)