File: gui_threads.py

package info (click to toggle)
pyro5 5.16-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 2,124 kB
  • sloc: python: 14,328; makefile: 161; sh: 66; javascript: 62
file content (170 lines) | stat: -rw-r--r-- 6,068 bytes parent folder | download | duplicates (2)
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()