Как использовать wxreactor с скрученным Perspective Broker [PB] для написания чат-клиента

Я изучаю wxPython и Perspective Broker от Twisted. Мне было поручено использовать их вместе для создания чат-клиента (я уже написал сервер и консольный клиент).

Вот что меня ставит в тупик: PB имеет свой собственный «поток» с обратными вызовами и тому подобное, который интуитивно не соединяется с управляемым событиями потоком wxpython. Какую структуру программы я должен использовать, чтобы заставить их сотрудничать?

Я пытался использовать извращенную клиентскую часть программы pb для получения и хранения информации с сервера в локальных методах, которые графический интерфейс wxpython затем может вызывать в ответ на определенные события и использовать в начале для настройки списка онлайн-пользователей. и группы. Я думаю, что у меня проблемы с последовательностью - я не сохраняю необходимые переменные до того, как их вызовет код wx, потому что оба запускаются одновременно. Возможно, вставка временной задержки для создания кадра и тому подобного помогла бы, но это кажется неуклюжим решением, если вообще решением.

Другим подходом может быть передача ссылки на сервер непосредственно во фрейм wxPython (и вложенные панели/ноутбуки). Здесь я сталкиваюсь с проблемами, потому что обратным вызовам нужен другой класс, а wx нужна информация в том же классе... и, возможно, есть способ заставить их в одну и ту же форму, но опять же, это выглядит очень неуклюже (плюс я еще не удалось заставить его работать.

Есть ли ресурс, решающий эту проблему? Стандартный подход?

В случае, если они могут осветить проблему с моим подходом...

Вот код моего сервера: http://pastebin.com/84fmhsRV Код клиента GUI: http://pastebin.com/UimXe4RY

Спасибо за помощь.


person Eowyn Dean    schedule 05.07.2012    source источник
comment
Также код рабочего клиента (консоли): pastebin.com/hR35xGbF   -  person Eowyn Dean    schedule 05.07.2012


Ответы (2)


Вы, вероятно, захотите взглянуть на обе эти страницы на Twisted и wxPython:

Я также нашел рецепт по этой теме. В вики-ссылке уже есть простая программа чата.

person Mike Driscoll    schedule 05.07.2012
comment
Спасибо. Я просмотрел ресурсы, на которые вы ссылались, но (возможно, из-за отсутствия у меня опыта) я не видел, как применить это к использованию Perspective Broker. PB имеет структуру обратного вызова, которая усложняет мне жизнь. ;-) Однако мне было трудно найти что-нибудь конкретное для ПБ. Вы знаете что-нибудь? - person Eowyn Dean; 05.07.2012
comment
Я раньше не использовал wx+twisted. Если вы спросите в списках рассылки wxPython или Twisted, я уверен, вы получите ответ. Я знаю, что в списке wxPython есть несколько парней, которые делали подобные вещи, хотя я не помню, использовали ли они PB или нет. - person Mike Driscoll; 05.07.2012

Я действительно опаздываю на вечеринку, но могу дать несколько полезных советов будущим читателям.

Причина, по которой это сложно, заключается в том, что вы пытаетесь заставить два цикла событий работать вместе. У вас есть Twisted реактор и цикл wxWidgets. Есть два способа связать петли

  1. Используйте особый реактор в Twisted, предназначенный для объединения событий Twisted и wx в один цикл. Twisted был разработан с учетом этого, так что несложно сварить специальный реактор для этой цели.
  2. Запустите реактор Twisted и цикл событий wx в отдельных потоках. В этом случае вы полагаетесь на то, что операционная система делегирует время выполнения каждому циклу обработки событий.

На самом деле я только сегодня закончил работать с обеими этими стратегиями с Twisted и PyQt. Qt и wxWidgets не настолько отличаются, поэтому я думаю, что вы, вероятно, сможете адаптировать мое решение с минимальными усилиями. Обратите внимание, что здесь я не использую Perspective Broker. Как только вы поймете, как я заставил это работать, добавление слоя Perspective Broker будет очень простым.

Сначала я описываю свое решение с помощью метода №1, основанного на pyqt4reactor. Вот полный рабочий код (вам нужен pyqt4reactor, который можно найти в различных неофициальных местах на сайте interwebz)

Клиент чата со специальным реактором

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()

Давайте рассмотрим ChatProtocol и его вспомогательный класс 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)

Сам протокол очень прост. Когда вы вызываете .send, он записывает данные через свой транспорт.

Прием данных несколько сложнее. Чтобы код Twisted уведомлял цикл событий Qt о входящем чате, мы снабжаем протокол эмиттером, который является QObject, который может излучать один сигнал. В главном окне Qt мы подключаем этот сигнал, чтобы он отправлял данные в окно чата. Эта связь происходит, когда мы устанавливаем нашу связь. Давайте рассмотрим:

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)

Мы говорим нашей клиентской фабрике установить TCP-соединение. Это дает отложенный вызов, который будет вызываться с результирующим протоколом в качестве аргумента. Наша функция обратного вызова onConnected должна подключить сигнал эмиттера этого протокола к onNewData. Это означает, что всякий раз, когда эмиттер протокола излучает, что происходит всякий раз, когда вызывается dataReceived, данные будут распространяться в систему сигналов/слотов Qt и отображаться в outputBox. Остальные функции должны более-менее иметь смысл.

Все еще со мной? Если да, то сейчас я покажу, как это сделать с помощью потоков. Вот полный рабочий код

Клиент чата с цепочками сообщений

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_())

Интересная разница здесь, помимо того, что реактор запускается в QThread, заключается в том, как мы подключаем наши обратные вызовы в Twisted частях кода. В частности, мы используем вспомогательную функцию 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)

Мы предоставляем функцию, которую мы хотим вызвать в потоке Twisted, сигнал Qt, который мы хотели бы посылать, когда результат нашей функции доступен, и дополнительные аргументы для нашей функции. Реактор вызывает нашу функцию и прикрепляет к полученному отложенному вызову обратный вызов, который будет испускать предоставленный нами сигнал.

Я надеюсь, что это полезно для кого-то :)

Если кто-то увидит упрощения, дайте мне знать.

person DanielSank    schedule 23.09.2013