0

I am learning wxPython and twisted's Perspective Broker. I've been assigned to use them together to produce a chat client (I've already written the server, and a console based client.)

This is what's stumping me: PB has it's own 'flow' with callbacks and such, which doesn't intuitively mesh with the event driven flow of wxpython. What kind of program structure should I be using to get the two cooperating?

I've tried using the twisted pb client part of the program to get and store info from the server in local methods that the wxpython gui can then call in response to certain events, and use at the start to set up a list of online users and groups. I think I'm running into issues with the sequence--not storing the necessary variables before the wx code calls for them, because both are started at the same time. Perhaps inserting a time delay for the frame creation and such would help, but that feels like a clumsy solution, if a solution at all.

Another approach would be to pass the server reference directly to the wxPython frame (and sub-panels/notebooks). Here I'm running into issues because the callbacks need a different class, and wx needs the info in the same class...and perhaps there's a way to force them into the same mold, but again, it feels very clumsy (plus I haven't managed to make it work yet.

Is there a resource that addresses this problem? A standard approach?

In case these might illuminate a problem with my approach...

Here's my server code: http://pastebin.com/84fmhsRV GUI client code: http://pastebin.com/UimXe4RY

Thank you for your help.

Eowyn Dean
  • 15
  • 5

2 Answers2

0

You'll probably want to take a look at both of these pages on Twisted and wxPython:

I also found a recipe on the topic. The wiki link has a simple chat program already done.

Mike Driscoll
  • 32,629
  • 8
  • 45
  • 88
  • Thank you. I did look at the resources you linked to, but (perhaps due to my lack of experience) I didn't see how to apply that to the use of Perspective Broker. PB has that callback structure that is making life difficult for me. ;-) I've had a tough time finding anything that applied to PB specifically, however. Do you know of anything? – Eowyn Dean Jul 05 '12 at 14:46
  • I haven't used wx + twisted before. If you ask on the wxPython or Twisted mailing lists, I'm sure you would get a response. I know there are several guys on the wxPython list that have done this sort of thing, although I don't remember if they used PB or not. – Mike Driscoll Jul 05 '12 at 16:12
0

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

  1. 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.
  2. 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.

DanielSank
  • 3,303
  • 3
  • 24
  • 42