wxPython And Twisted
This was copied from wxPython-users as a starting point for using wxPython and Twisted together. Feel free to expand and adapt this document as you gain your own experience with wxPython and Twisted.
David Bolen writes:
Mike Driscoll writes: > I looked at Twisted one wxPython example, but it didn't really show > how to get info from a twisted server, just how to call a wx method > from its reactor. I combined Twisted and wxPython all the time, and it works well. Fundamentally you just need to select a mechanism for handling both event loops and after that just write the usual wx or twisted code. I've used each of the following mechanisms in the past: 1. Twisted and wxPython in separate threads (typically wx in the main thread when the application was GUI in nature). Use any of Queues, reactor.callFromThread/deferToThread, wxPostEvent/wxCallAfter to shift requests between the two threads. This approach just uses standard wxPython and Twisted initialization sequences in separate threads. The wx thread blocks in MainLoop and the Twisted thread in reactor.run() 2. Iterate the twisted reactor from within a wxPython idle or timer based loop. This actually works quite well although there can be a small latency to servicing twisted events due to latency in the loop (I tended to use something in the 100-150ms range). Generally I was working on GUI applications with relatively light networking loads (from a time critical nature) so a few hundred ms here or there never caused any problems. One example (with default reactor) using a timer would be in your wxApp: - In OnInit() or a separate method if you want to initialize it prior to calling MainLoop(): reactor.startRunning(installSignalHandlers=0) timerid = wx.wxNewId() self.timer = wx.wxTimer(self, timerid) wx.EVT_TIMER(self, timerid, self.OnTimer) self.timer.Start(150, False) - Define the timer callback: def OnTimer(self, event): reactor.runUntilCurrent() reactor.doIteration(0) 3. Integrate the two loops using the threadedselect reactor, using wxCallAfter for the interleave waker. There's an example of this in the twisted examples, but essentially: - Install the threadedselect reactor: from twisted.internet import _threadedselect _threadedselect.install() from twisted.internet import reactor The last statement gets access to the reactor instance that was installed under the standard name. Modules other than the one installing can just use the last statement per normal, but it's important to make sure no other module tries to import the reactor until you execute the above installation code, or else the regular select reactor will have already been installed. - Then somewhere during wxApp initialization (or really anywhere that you want to set things up), interleave the reactor: reactor.interleave(wx.CallAfter) - In your main window, add a shutdown trigger, so that calling reactor.stop will shut down the GUI side of the application (and you should use reactor.stop in any normal circumstance when you want to exit): For example, in your main window, you might: reactor.addSystemEventTrigger('after', 'shutdown', self.Close, true) to close the window when the reactor is done, and thus exit MainLoop. You could also probably use wxApp.ExitMainLoop as the callback although the recommendation is generally to close the top level window to exit. I tend to reserve (1) for when the two purposes (network and GUI) are more "ships in the night" that just happen to share a process but don't tend to have a lot of tight coupling or cross-operations. (2) was my most common method for a long time and it's convenient because everything in a single thread means you can interleave twisted and wxPython operations without any special handling at all. I originally used (3) under OSX when integrating with a PyObjC application, and after good results with that, started using it with wxPython in other cases. If only because I don't have to manually iterate the reactor, I suppose it feels a little cleaner up in the code level. It also can be more responsive since the network select has no latency (is just in a background thread) though actually servicing a deferred callback or network I/O does go through the wx event loop so can be delayed by other pending UI events. The remainder of the reactor processing is done in the main thread so you can still inter-mix wx and twisted processing with no special protections. About the only thing I've never done is use the wxreactor in twisted. When I first started it was still fairly buggy, and when that started to clear up, I was already more than happy enough with (2). -- David
A Simple Chat Application
To demonstrate using Twisted + wxPython together, here is a simple multi-client chat program. To begin, run python server.py to initially setup the server waiting as a background process. Then, python gui.py will launch the chat window.
I haven't coded this with the best programming practices -- but it should serve as a decent example
View a screenshot of the program in action
gui.py
1 import wx
2 from twisted.internet import wxreactor
3 wxreactor.install()
4
5 # import twisted reactor *only after* installing wxreactor
6 from twisted.internet import reactor, protocol
7 from twisted.protocols import basic
8
9
10 class ChatFrame(wx.Frame):
11 def __init__(self):
12 wx.Frame.__init__(self, parent=None, title="Twisted Chat")
13 self.protocol = None # twisted Protocol
14
15 sizer = wx.BoxSizer(wx.VERTICAL)
16 self.text = wx.TextCtrl(self, style=wx.TE_MULTILINE | wx.TE_READONLY)
17 self.ctrl = wx.TextCtrl(self, style=wx.TE_PROCESS_ENTER, size=(300, 25))
18
19 sizer.Add(self.text, 5, wx.EXPAND)
20 sizer.Add(self.ctrl, 0, wx.EXPAND)
21 self.SetSizer(sizer)
22 self.ctrl.Bind(wx.EVT_TEXT_ENTER, self.send)
23
24 def send(self, evt):
25 self.protocol.sendLine(str(self.ctrl.GetValue()))
26 self.ctrl.SetValue("")
27
28
29 class DataForwardingProtocol(basic.LineReceiver):
30 def __init__(self):
31 self.output = None
32
33 def dataReceived(self, data):
34 gui = self.factory.gui
35
36 gui.protocol = self
37 if gui:
38 val = gui.text.GetValue()
39 gui.text.SetValue(val + data)
40 gui.text.SetInsertionPointEnd()
41
42 def connectionMade(self):
43 self.output = self.factory.gui.text # redirect Twisted's output
44
45
46 class ChatFactory(protocol.ClientFactory):
47 def __init__(self, gui):
48 self.gui = gui
49 self.protocol = DataForwardingProtocol
50
51 def clientConnectionLost(self, transport, reason):
52 reactor.stop()
53
54 def clientConnectionFailed(self, transport, reason):
55 reactor.stop()
56
57
58 if __name__ == '__main__':
59 app = wx.App(False)
60 frame = ChatFrame()
61 frame.Show()
62 reactor.registerWxApp(app)
63 reactor.connectTCP("localhost", 5001, ChatFactory(frame))
64 reactor.run()
server.py
1 from twisted.internet import reactor, protocol
2 from twisted.protocols import basic
3 import time
4
5 def t():
6 return "["+ time.strftime("%H:%M:%S") +"] "
7
8 class EchoProtocol(basic.LineReceiver):
9 name = "Unnamed"
10
11 def connectionMade(self):
12 self.sendLine("Welcome, what is your name?")
13 self.sendLine("")
14 self.count = 0
15 self.factory.clients.append(self)
16 print t() + "+ Connection from: "+ self.transport.getPeer().host
17
18 def connectionLost(self, reason):
19 self.sendMsg("- %s left." % self.name)
20 print t() + "- Connection lost: "+ self.name
21 self.factory.clients.remove(self)
22
23 def lineReceived(self, line):
24 if line == 'quit':
25 self.sendLine("Goodbye.")
26 self.transport.loseConnection()
27 return
28 elif line == "userlist":
29 self.chatters()
30 return
31 if not self.count:
32 self.username(line)
33 else:
34 self.sendMsg(self.name +": " + line)
35
36 def username(self, line):
37 for x in self.factory.clients:
38 if x.name == line:
39 self.sendLine("This username is taken; please choose another")
40 return
41
42 self.name = line
43 self.chatters()
44 self.sendLine("Chat away!")
45 self.sendLine("")
46 self.count += 1
47 self.sendMsg("+ %s joined." % self.name)
48 print '%s~ %s is now known as %s' % (t(), self.transport.getPeer().host, self.name)
49
50 def chatters(self):
51 x = len(self.factory.clients) - 1
52 s = 'is' if x == 1 else 'are'
53 p = 'person' if x == 1 else 'people'
54 self.sendLine("There %s %i %s connected:" % (s, x, p) )
55
56 for client in self.factory.clients:
57 if client is not self:
58 self.sendLine(client.name)
59 self.sendLine("")
60
61 def sendMsg(self, message):
62 for client in self.factory.clients:
63 client.sendLine(t() + message)
64
65
66 class EchoServerFactory(protocol.ServerFactory):
67 protocol = EchoProtocol
68 clients = []
69
70 if __name__ == "__main__":
71 reactor.listenTCP(5001, EchoServerFactory())
72 reactor.run()
Code Explanation
Client
For the gui, we simply set up two text boxes: one that is read-only, and expands inside the sizer to take up all the remaining space. The other text control is for typing in your chat messages, and is pretty small. On a text event, we send a "line" to our Twisted protocol, the DataForwardingProtocol.
When a connection is initially made to the server, the DataForwardingProtocol's attribute, "output" is changed to the read-only text control. Twisted will redirect all output received from the server into this text control (and scrolls the control to the bottom with SetInsertionPointEnd)
The ChatFactory is used when connecting to Twisted's reactor. It stores a reference to the gui, so that it can update its text box, and also sets its protocol to the DataForwardingProtocol. This tells twisted which Protocol handles any data received from the server.
Server
This is some basic code. When the script runs, it creates an instance of the EchoFactory, and listens over TCP at localhost, port 5001 for incoming connections. The functions connectionMade, connectionLost and lineReceived overwrite the Factory's default methods. What the server does is track each client that connects in a Python list, and then whenever a line is received from a connected client, it simply "echoes" the received message back to all registered clients.
Each client is prompted for their username, which must be unique. This allows the server to identify each client, and to echo their name, alongside their message.