1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170
|
"""
This example shows a Tkinter GUI application that uses a worker thread to
run Pyro's event loop.
Usually, the GUI toolkit requires that GUI operations are done from within
the GUI thread. So, if Pyro interfaces with the GUI, it cannot do that
directly because the method calls are done from a different thread.
This means we need a layer between them, this example uses a Queue to
submit GUI operations to Tkinter's main loop.
For this example, the mainloop runs a callback function every so often
to check for new work in that Queue and will process it if the Pyro worker
thread has put something in it.
"""
import time
import threading
import queue
from tkinter import *
import tkinter.simpledialog as simpledialog
from Pyro5.api import expose, Daemon
# The frequency with which the GUI mainloop checks for work in the Pyro queue.
PYRO_QUEUE_HZ = 50
class PyroGUI(object):
"""
The Tkinter GUI application that also listens for Pyro calls.
"""
def __init__(self):
self.pyro_queue = queue.Queue()
self.tk = Tk()
self.tk.wm_title("Pyro in a Tkinter GUI eventloop - with threads")
self.tk.wm_geometry("500x500")
buttonframe = Frame(self.tk)
button = Button(buttonframe, text="Messagebox", command=self.button_msgbox_clicked)
button.pack(side=LEFT)
button = Button(buttonframe, text="Add some text", command=self.button_text_clicked)
button.pack(side=LEFT)
button = Button(buttonframe, text="Clear all text", command=self.button_clear_clicked)
button.pack(side=LEFT)
quitbutton = Button(buttonframe, text="Quit", command=self.tk.quit)
quitbutton.pack(side=RIGHT)
frame = Frame(self.tk, padx=2, pady=2)
buttonframe.pack(fill=X)
rlabel = Label(frame, text="Pyro server messages:")
rlabel.pack(fill=X)
self.msg = Message(frame, anchor=NW, width=500, aspect=80, background="white", fg="black", relief="sunken")
self.msg.pack(fill=BOTH, expand=1)
frame.pack(fill=BOTH)
self.serveroutput = []
def install_pyro_queue_callback(self):
"""
Add a callback to the tkinter event loop that is invoked every so often.
The callback checks the Pyro work queue for work and processes it.
"""
def check_pyro_queue():
try:
while True:
# get a work item from the queue (until it is empty)
workitem = self.pyro_queue.get_nowait()
# execute it in the gui's mainloop thread
workitem["callable"](*workitem["vargs"], **workitem["kwargs"])
except queue.Empty:
pass
self.tk.after(1000 // PYRO_QUEUE_HZ, check_pyro_queue)
self.tk.after(1000 // PYRO_QUEUE_HZ, check_pyro_queue)
def mainloop(self):
self.tk.mainloop()
def button_msgbox_clicked(self):
# this button event handler is here only to show that gui events are still processed normally
number = simpledialog.askinteger("A normal popup", "Hi there enter a number", parent=self.tk)
def button_clear_clicked(self):
self.serveroutput = []
self.msg.config(text="")
def button_text_clicked(self):
# add some random text to the message list
self.add_message("The quick brown fox jumps over the lazy dog!")
def add_message(self, message):
message = "[{0}] {1}".format(time.strftime("%X"), message)
self.serveroutput.append(message)
self.serveroutput = self.serveroutput[-27:]
self.msg.config(text="\n".join(self.serveroutput))
@expose
class MessagePrinter(object):
"""
The Pyro object that interfaces with the GUI application.
It uses a Queue to transfer GUI update calls to Tkinter's mainloop.
"""
def __init__(self, gui):
self.gui = gui
def message(self, messagetext):
# put a gui-update work item in the queue
self.gui.pyro_queue.put({
"callable": self.gui.add_message,
"vargs": ("from Pyro: " + messagetext,),
"kwargs": {}
})
def sleep(self, duration):
# Note that you *can* perform blocking stuff now because the method
# call is running in its own thread. It won't freeze the GUI anymore.
# However you cannot do anything that requires GUI interaction because
# that needs to go through the queue so the mainloop can pick that up.
# (opening a dialog from this worker thread will still freeze the GUI)
# But a simple sleep() call works fine and the GUI stays responsive.
self.gui.pyro_queue.put({
"callable": self.gui.add_message,
"vargs": ("from Pyro: sleeping {0} seconds...".format(duration),),
"kwargs": {}
})
time.sleep(duration)
self.gui.pyro_queue.put({
"callable": self.gui.add_message,
"vargs": ("from Pyro: woke up!",),
"kwargs": {}
})
class MyPyroDaemon(threading.Thread):
def __init__(self, gui):
threading.Thread.__init__(self)
self.gui = gui
self.started = threading.Event()
def run(self):
daemon = Daemon()
obj = MessagePrinter(self.gui)
self.uri = daemon.register(obj, "pyrogui.message2")
self.started.set()
daemon.requestLoop()
def main():
gui = PyroGUI()
# create a pyro daemon with object, running in its own worker thread
pyro_thread = MyPyroDaemon(gui)
pyro_thread.daemon = True
pyro_thread.start()
pyro_thread.started.wait()
gui.add_message("Pyro server started. Using Pyro worker thread.")
gui.add_message("Use the command line client to send messages.")
urimsg = "Pyro object uri = {0}".format(pyro_thread.uri)
gui.add_message(urimsg)
print(urimsg)
# add a Pyro event callback to the gui's mainloop
gui.install_pyro_queue_callback()
# enter the mainloop
gui.mainloop()
if __name__ == "__main__":
main()
|