I'm really late to the party here but I can offer some useful advice for future readers.
The reason this is hard is that you're trying to get two event loops to work together. You have the Twisted reactor and the wxWidgets loop. There are two ways to mesh the loops
- Use a special-case reactor in Twisted that is designed to combine the Twisted and wx events into a single loop. Twisted was designed with this in mind so it is not super hard to brew a custom reactor for this purpose.
- Run the Twisted reactor and wx event loop in separate threads. In this case you're relying on the operating system to delegate execution time to each event loop.
I've actually just today finished getting both of these strategies working with Twisted and PyQt. Qt and wxWidgets aren't that different so I think you can probably adapt my solution with minimal effort. Note that I'm not using Perspective Broker here. Once you understand how I got this to work adding the Perspective Broker layer will be really easy.
First I describe my solution with method #1 which relies on the pyqt4reactor. Here's the complete working code (you need the pyqt4reactor which can be found in various unofficial places on the interwebz)
Chat client with special reactor
import sys
import PyQt4.QtGui as QtGui
import PyQt4.QtCore as QtCore
import PyQt4.uic as uic
import twisted.internet.defer as defer
import twisted.internet.protocol as protocol
import qt4reactor
import constants as C
class MainWindow(QtGui.QMainWindow):
def __init__(self):
QtGui.QMainWindow.__init__(self)
self.ui = uic.loadUi('ui.ui')
self.ui.sendButton.clicked.connect(self.sendMessage)
self.ui.inputBox.returnPressed.connect(self.sendMessage)
self.ui.connectButton.clicked.connect(self.getNetworkConnection)
self.ui.show()
def getNetworkConnection(self):
#This line should probably not be inside getNetworkConnection
factory = protocol.ClientCreator(reactor, ChatProtocol)
d = factory.connectTCP(C.HOST, C.PORT)
def onConnected(p):
self.cxn = p
p.emitter.signal.connect(self.onNewData)
self.ui.connectButton.setEnabled(False)
d.addCallback(onConnected)
def onNewData(self, data):
self.ui.outputBox.append(data)
def sendMessage(self):
message = str(self.ui.inputBox.text())
self.ui.inputBox.clear()
self.cxn.send(message)
class Emitter(QtCore.QObject):
signal = QtCore.pyqtSignal(str)
def __init__(self):
QtCore.QObject.__init__(self)
class ChatProtocol(protocol.Protocol):
def __init__(self):
self.emitter = Emitter()
def dataReceived(self, data):
self.emitter.signal.emit(data)
def send(self, data):
self.transport.write(data)
class ChatFactory(protocol.ClientFactory):
protocol = ChatProtocol
if __name__ == '__main__':
app = QtGui.QApplication(sys.argv)
qt4reactor.install()
from twisted.internet import reactor
mainWindow = MainWindow()
reactor.run()
Let's examine the ChatProtocol
and it's helper class Emitter
:
class ChatProtocol(protocol.Protocol):
def __init__(self):
self.emitter = Emitter()
def dataReceived(self, data):
self.emitter.signal.emit(data)
def send(self, data):
self.transport.write(data)
class Emitter(QtCore.QObject):
signal = QtCore.pyqtSignal(str)
The Protocol itself is really simple. When you call .send
it writes data over its transport.
Data reception is slightly more complicated. In order to get the Twisted code to notify the Qt event loop of incoming chat we endow the Protocol with an Emitter which is a QObject that can emit a single signal. In the main Qt Window we hook up this signal such that it posts data to the chat window. This hook-up happens when we make our connection. Let's examine:
class MainWindow(QtGui.QMainWindow):
<snip>
def getNetworkConnection(self):
#This line should probably not be inside getNetworkConnection
factory = protocol.ClientCreator(reactor, ChatProtocol)
d = factory.connectTCP(C.HOST, C.PORT)
def onConnected(p):
self.cxn = p
p.emitter.signal.connect(self.onNewData)
self.ui.connectButton.setEnabled(False)
d.addCallback(onConnected)
We tell our client factory to make a TCP connection. This gives a deferred which will be called with the resulting protocol as its argument. Our callback function onConnected
has the job of hooking up that protocol's emitter's signal to onNewData
. This means that whenever the protocol's emitter emits, which happens whenever dataReceived
is invoked, the data will propagate to the Qt signal/slot system and get displayed in outputBox. The rest of the functions should more or less make sense.
Still with me? If you are I'll now show how to do this with threads. Here's the complete working code
Chat client with threads
import sys
import PyQt4.QtGui as QtGui
import PyQt4.QtCore as QtCore
import PyQt4.uic as uic
import twisted.internet.reactor as reactor
import twisted.internet.defer as defer
import twisted.internet.protocol as protocol
import constants as C
class MainWindow(QtGui.QMainWindow):
def __init__(self):
QtGui.QMainWindow.__init__(self)
self.ui = uic.loadUi('ui.ui')
self.ui.sendButton.clicked.connect(self.sendMessage)
self.ui.inputBox.returnPressed.connect(self.sendMessage)
self.ui.connectButton.clicked.connect(self.getNetworkConnection)
self.ui.show()
self.networkThread = NetworkThread()
self.networkThread.start()
self.connect(self.networkThread,
self.networkThread.sigConnected,
self.onConnected)
def getNetworkConnection(self):
#This line should probably not be inside getNetworkConnection
factory = protocol.ClientCreator(reactor, ChatProtocol)
self.networkThread.callFromMain(factory.connectTCP,
self.networkThread.sigConnected,
C.HOST, C.PORT)
def onConnected(self, p):
self.cxn = p
p.emitter.signal.connect(self.onNewData)
self.ui.connectButton.setEnabled(False)
def onNewData(self, data):
self.ui.outputBox.append(data)
def sendMessage(self):
message = str(self.ui.inputBox.text())
self.networkThread.callFromMain(self.cxn.send, None, message)
self.ui.inputBox.clear()
class NetworkThread(QtCore.QThread):
"""Run the twisted reactor in its own thread"""
def __init__(self):
QtCore.QThread.__init__(self)
self.sigConnected = QtCore.SIGNAL("sigConnected")
def run(self):
reactor.run(installSignalHandlers=0)
def callFromMain(self, func, successSignal, *args):
"""Call an async I/O function with a Qt signal as it's callback"""
def succeed(result):
self.emit(successSignal, result)
def wrapped():
d = defer.maybeDeferred(func, *args)
if successSignal is not None:
d.addCallback(succeed)
reactor.callFromThread(wrapped)
class Emitter(QtCore.QObject):
#Not sure why I specified a name here...
signal = QtCore.pyqtSignal(str, name='newData')
class ChatProtocol(protocol.Protocol):
def __init__(self):
self.emitter = Emitter()
def dataReceived(self, data):
self.emitter.signal.emit(data)
def send(self, data):
self.transport.write(data)
class ChatFactory(protocol.ClientFactory):
protocol = ChatProtocol
if __name__ == '__main__':
app = QtGui.QApplication(sys.argv)
ex = MainWindow()
sys.exit(app.exec_())
The interesting difference here, aside from the reactor being run in a QThread, is in the way we hook up our callbacks in the Twisted parts of the code. In particular we use a helper function callFromMain
def callFromMain(self, func, successSignal, *args):
"""Call an async I/O function with a Qt signal as it's callback"""
def succeed(result):
self.emit(successSignal, result)
def wrapped():
d = defer.maybeDeferred(func, *args)
if successSignal is not None:
d.addCallback(succeed)
reactor.callFromThread(wrapped)
We provide a function we wish to be invoked in the Twisted thread, a Qt signal that we would like to be emitted when the result of our function is available, and extra arguments for our function. The reactor calls our function and attaches a callback to the resulting deferred that will emit the signal we provided.
I hope this is helpful to someone :)
If anyone sees simplifications let me know.